mirror of
https://github.com/yattee/yattee.git
synced 2026-05-13 10:55:03 +00:00
Use native macOS layout for OpenLinkSheet
This commit is contained in:
@@ -47,6 +47,10 @@ struct OpenLinkSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
OpenLinkFormView(prefilledURL: prefilledURL, onRequestDismiss: { dismiss() })
|
||||||
|
.frame(minWidth: 520, minHeight: 440)
|
||||||
|
#else
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
OpenLinkFormView(prefilledURL: prefilledURL, onRequestDismiss: { dismiss() })
|
OpenLinkFormView(prefilledURL: prefilledURL, onRequestDismiss: { dismiss() })
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@@ -57,6 +61,7 @@ struct OpenLinkSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +111,34 @@ struct OpenLinkFormView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
platformBody
|
||||||
|
.onAppear {
|
||||||
|
checkClipboard()
|
||||||
|
if urlText.isEmpty {
|
||||||
|
isTextEditorFocused = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(tvOS)
|
||||||
|
.sheet(isPresented: $showingDownloadSheet, onDismiss: {
|
||||||
|
// Close sheet when download sheet is dismissed (if no errors and we're in sheet mode)
|
||||||
|
if !hasErrors {
|
||||||
|
dismissIfRequested()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
BatchDownloadQualitySheet(videoCount: pendingDownloadItems.count) { quality, includeSubtitles in
|
||||||
|
Task {
|
||||||
|
await downloadPendingItems(quality: quality, includeSubtitles: includeSubtitles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var platformBody: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
macOSBody
|
||||||
|
#else
|
||||||
Form {
|
Form {
|
||||||
urlInputSection
|
urlInputSection
|
||||||
extractionResultsSection
|
extractionResultsSection
|
||||||
@@ -119,28 +152,209 @@ struct OpenLinkFormView: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.scrollDismissesKeyboard(.immediately)
|
.scrollDismissesKeyboard(.immediately)
|
||||||
#endif
|
#endif
|
||||||
.onAppear {
|
#endif
|
||||||
checkClipboard()
|
}
|
||||||
if urlText.isEmpty {
|
|
||||||
isTextEditorFocused = true
|
#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 !os(tvOS)
|
if !clipboardURLs.isEmpty, !isExtracting {
|
||||||
.sheet(isPresented: $showingDownloadSheet, onDismiss: {
|
let clipboardText = clipboardURLs.map(\.absoluteString).joined(separator: "\n")
|
||||||
// Close sheet when download sheet is dismissed (if no errors and we're in sheet mode)
|
if clipboardText != urlText {
|
||||||
if !hasErrors {
|
Button {
|
||||||
dismissIfRequested()
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
BatchDownloadQualitySheet(videoCount: pendingDownloadItems.count) { quality, includeSubtitles in
|
if !extractedItems.isEmpty {
|
||||||
Task {
|
Divider()
|
||||||
await downloadPendingItems(quality: quality, includeSubtitles: includeSubtitles)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
.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
|
// MARK: - URL Input Section
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
Reference in New Issue
Block a user