mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 18:35:05 +00:00
Use native macOS layout for OpenLinkSheet
This commit is contained in:
@@ -47,6 +47,10 @@ struct OpenLinkSheet: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
OpenLinkFormView(prefilledURL: prefilledURL, onRequestDismiss: { dismiss() })
|
||||
.frame(minWidth: 520, minHeight: 440)
|
||||
#else
|
||||
NavigationStack {
|
||||
OpenLinkFormView(prefilledURL: prefilledURL, onRequestDismiss: { dismiss() })
|
||||
.toolbar {
|
||||
@@ -57,6 +61,7 @@ struct OpenLinkSheet: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,19 +111,7 @@ struct OpenLinkFormView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
urlInputSection
|
||||
extractionResultsSection
|
||||
actionButtonsSection
|
||||
yatteeServerWarningSection
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "openLink.title"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
#endif
|
||||
platformBody
|
||||
.onAppear {
|
||||
checkClipboard()
|
||||
if urlText.isEmpty {
|
||||
@@ -141,6 +134,227 @@ struct OpenLinkFormView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var platformBody: some View {
|
||||
#if os(macOS)
|
||||
macOSBody
|
||||
#else
|
||||
Form {
|
||||
urlInputSection
|
||||
extractionResultsSection
|
||||
actionButtonsSection
|
||||
yatteeServerWarningSection
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "openLink.title"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@ViewBuilder
|
||||
private var macOSBody: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text(String(localized: "openLink.title"))
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
TextEditor(text: $urlText)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.focused($isTextEditorFocused)
|
||||
.disabled(isExtracting)
|
||||
.scrollContentBackground(.hidden)
|
||||
.padding(6)
|
||||
.frame(minHeight: 90, maxHeight: 140)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(Color(nsColor: .textBackgroundColor))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.strokeBorder(Color.secondary.opacity(0.35), lineWidth: 1)
|
||||
)
|
||||
|
||||
HStack {
|
||||
Text(supportedSitesHint)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
if isTooManyURLs {
|
||||
Label(
|
||||
String(localized: "openLink.tooManyUrls \(Self.maxURLs)"),
|
||||
systemImage: "exclamationmark.triangle"
|
||||
)
|
||||
.foregroundStyle(.orange)
|
||||
.font(.caption)
|
||||
} else if urlCount > 0 {
|
||||
Text(String(localized: "openLink.urlCount \(urlCount)"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !clipboardURLs.isEmpty, !isExtracting {
|
||||
let clipboardText = clipboardURLs.map(\.absoluteString).joined(separator: "\n")
|
||||
if clipboardText != urlText {
|
||||
Button {
|
||||
urlText = clipboardText
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "doc.on.clipboard")
|
||||
if clipboardURLs.count > 1 {
|
||||
Text(String(localized: "openLink.pasteMultiple \(clipboardURLs.count)"))
|
||||
} else {
|
||||
Text(String(localized: "openLink.pasteClipboard"))
|
||||
}
|
||||
Text("·")
|
||||
.foregroundStyle(.secondary)
|
||||
Text(clipboardURLs.first?.host ?? "")
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.font(.subheadline)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.regular)
|
||||
}
|
||||
}
|
||||
|
||||
if !extractedItems.isEmpty {
|
||||
Divider()
|
||||
macOSResultsList
|
||||
}
|
||||
|
||||
if !hasYatteeServer, hasExternalURLs, !isExtracting {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.yellow)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(String(localized: "openLink.yatteeServerNotConfigured"))
|
||||
.font(.subheadline)
|
||||
Text(String(localized: "openLink.yatteeServerMessage"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
if onRequestDismiss != nil {
|
||||
Button(String(localized: "common.cancel")) {
|
||||
onRequestDismiss?()
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
}
|
||||
Spacer()
|
||||
if isExtracting {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
Text(String(localized: "openLink.extracting"))
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.subheadline)
|
||||
} else {
|
||||
Button {
|
||||
isTextEditorFocused = false
|
||||
Task { await downloadAllURLs() }
|
||||
} label: {
|
||||
Label(
|
||||
isMultipleURLs
|
||||
? String(localized: "openLink.downloadAll")
|
||||
: String(localized: "openLink.download"),
|
||||
systemImage: "arrow.down.circle"
|
||||
)
|
||||
}
|
||||
.disabled(!isValidInput)
|
||||
|
||||
Button {
|
||||
isTextEditorFocused = false
|
||||
Task { await openAllURLs() }
|
||||
} label: {
|
||||
Label(
|
||||
isMultipleURLs
|
||||
? String(localized: "openLink.openAll")
|
||||
: String(localized: "openLink.open"),
|
||||
systemImage: "play.fill"
|
||||
)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(!isValidInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var macOSResultsList: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
if isExtracting {
|
||||
let processed = extractedItems.filter { item in
|
||||
if case .pending = item.status { return false }
|
||||
return true
|
||||
}.count
|
||||
Text(String(localized: "openLink.extractingProgress \(processed) \(extractedItems.count)"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text(String(localized: "openLink.results"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
ForEach(extractedItems) { item in
|
||||
HStack(spacing: 10) {
|
||||
Group {
|
||||
switch item.status {
|
||||
case .pending:
|
||||
Image(systemName: "circle").foregroundStyle(.secondary)
|
||||
case .extracting:
|
||||
ProgressView().controlSize(.small)
|
||||
case .success:
|
||||
Image(systemName: "checkmark.circle.fill").foregroundStyle(.green)
|
||||
case .failed:
|
||||
Image(systemName: "xmark.circle.fill").foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
.frame(width: 18)
|
||||
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
if let video = item.video {
|
||||
Text(video.title).lineLimit(1)
|
||||
Text(item.displayHost)
|
||||
.font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
||||
} else {
|
||||
Text(item.url.absoluteString)
|
||||
.font(.caption).foregroundStyle(.secondary).lineLimit(2)
|
||||
}
|
||||
if case .failed(let error) = item.status {
|
||||
Text(error).font(.caption2).foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 120)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - URL Input Section
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
Reference in New Issue
Block a user