WP-4c: returns list + settings per SPEC-A2b
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).
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user