From 66657ac53291806be00a420588bcb0dd1b7de97b Mon Sep 17 00:00:00 2001 From: kua-agent Date: Mon, 31 Aug 2026 16:07:41 +0400 Subject: [PATCH] WP-4c: returns list + settings per SPEC-A2b MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ReturnsList (newest-first, max 8, click-to-Finder, hidden when empty) and SettingsView (hotkey row, folder Choose… via FolderSettings + AppModel seam mutators, async updateWatchFolder(url) only). --- Sources/Shotdeck/ReturnsList.swift | 53 ++++++++++++++++ Sources/Shotdeck/SettingsView.swift | 98 +++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 Sources/Shotdeck/ReturnsList.swift create mode 100644 Sources/Shotdeck/SettingsView.swift diff --git a/Sources/Shotdeck/ReturnsList.swift b/Sources/Shotdeck/ReturnsList.swift new file mode 100644 index 0000000..7df705a --- /dev/null +++ b/Sources/Shotdeck/ReturnsList.swift @@ -0,0 +1,53 @@ +import AppKit +import SwiftUI +import ShotdeckCore + +func newestReturnsForDisplay(_ returns: [ReturnedDocument], limit: Int = 8) -> [ReturnedDocument] { + Array(returns.sorted { $0.detectedAt > $1.detectedAt }.prefix(limit)) +} + +func returnMarkLabel(_ document: ReturnedDocument) -> String { + guard document.isCommented else { return "not marked" } + let count = document.annotatedPages.count + return "\(count) page\(count == 1 ? "" : "s") marked" +} + +struct ReturnsList: View { + let returns: [ReturnedDocument] + + var body: some View { + let visible = newestReturnsForDisplay(returns) + if visible.isEmpty { + EmptyView() + } else { + VStack(alignment: .leading, spacing: 4) { + Text("Came back from your device") + .font(.caption) + .foregroundStyle(.secondary) + ForEach(visible) { document in + Button { + NSWorkspace.shared.activateFileViewerSelecting([document.fileURL]) + } label: { + HStack(spacing: 8) { + Text(document.fileURL.lastPathComponent) + .lineLimit(1) + Spacer(minLength: 8) + Text(returnMarkLabel(document)) + .font(.caption) + .foregroundStyle(document.isCommented ? .primary : .secondary) + } + } + .buttonStyle(.plain) + .help(DubaiTime.stamp(document.detectedAt)) + } + } + } + } +} + +extension AppModel: ReturnsSectionProviding { + public func returnsSection() -> AnyView { + guard !allReturns.isEmpty else { return AnyView(EmptyView()) } + return AnyView(ReturnsList(returns: allReturns)) + } +} diff --git a/Sources/Shotdeck/SettingsView.swift b/Sources/Shotdeck/SettingsView.swift new file mode 100644 index 0000000..624044e --- /dev/null +++ b/Sources/Shotdeck/SettingsView.swift @@ -0,0 +1,98 @@ +import AppKit +import SwiftUI +import ShotdeckCore + +struct SettingsView: View { + @Environment(AppModel.self) private var model + + var body: some View { + Form { + Section("Hotkey") { + LabeledContent("Capture", value: "⌥⇧2 — fixed in this version") + } + Section("Folders") { + LabeledContent("Watch folder") { + HStack(spacing: 8) { + Text(model.watchFolderURL.path) + .lineLimit(1) + .truncationMode(.middle) + .foregroundStyle(.secondary) + Button("Choose…") { model.chooseWatchFolder() } + } + } + LabeledContent("Output folder") { + HStack(spacing: 8) { + Text(model.outboxURL.path) + .lineLimit(1) + .truncationMode(.middle) + .foregroundStyle(.secondary) + Button("Choose…") { model.chooseOutboxFolder() } + } + } + } + Section { + Button("Reveal spool folder") { model.openSpoolFolder() } + } + } + .padding() + .frame(width: 360) + .controlSize(.small) + } +} + +extension AppModel: SettingsWindowPresenting { + private static var settingsWindowController: NSWindowController? + + public func presentSettingsWindow() { + if let existing = Self.settingsWindowController { + existing.window?.makeKeyAndOrderFront(nil) + NSApp.activate() + return + } + let hosting = NSHostingController(rootView: SettingsView().environment(self)) + let window = NSWindow(contentViewController: hosting) + window.title = "Shotdeck Settings" + window.styleMask = [.titled, .closable] + window.isReleasedWhenClosed = false + window.center() + let controller = NSWindowController(window: window) + Self.settingsWindowController = controller + controller.showWindow(nil) + NSApp.activate() + } + + func chooseOutboxFolder() { + guard let url = chooseDirectory(startingAt: outboxURL) else { return } + FolderSettings.setOutbox(url) + setFolderURLs(outbox: url, watch: watchFolderURL) + setStatus("Output folder set to \(url.lastPathComponent).") + } + + func chooseWatchFolder() { + guard let url = chooseDirectory(startingAt: watchFolderURL) else { return } + FolderSettings.setWatchFolder(url) + setFolderURLs(outbox: outboxURL, watch: url) + Task { + do { + try await watcher.updateWatchFolder(url) + setStatus("Watch folder set to \(url.lastPathComponent).") + } catch { + setStatus( + (error as? ShotdeckError)?.errorDescription ?? "Could not switch the watch folder." + ) + } + } + } + + private func chooseDirectory(startingAt directory: URL) -> URL? { + let panel = NSOpenPanel() + panel.canChooseDirectories = true + panel.canChooseFiles = false + panel.allowsMultipleSelection = false + panel.canCreateDirectories = true + panel.prompt = "Choose" + panel.directoryURL = directory + guard panel.runModal() == .OK else { return nil } + return panel.url + } +} -- 2.54.0