Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c54471ee2e | ||
|
|
79fa9ee1da | ||
|
|
672f8d495f | ||
|
|
66657ac532 |
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+95
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
APP_BUNDLE="${ROOT}/.build/Shotdeck.app"
|
||||||
|
STAGING="${ROOT}/.build/dmg-staging"
|
||||||
|
DMG="${ROOT}/.build/Shotdeck.dmg"
|
||||||
|
MOUNT_POINT="${ROOT}/.build/dmg-mnt"
|
||||||
|
|
||||||
|
SKIP_SIGN=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "${arg}" in
|
||||||
|
--skip-sign)
|
||||||
|
SKIP_SIGN=1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: ${arg}" >&2
|
||||||
|
echo "Usage: $0 [--skip-sign]" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> Building Shotdeck.app"
|
||||||
|
if [[ "${SKIP_SIGN}" -eq 1 ]]; then
|
||||||
|
./scripts/build-app.sh --skip-sign
|
||||||
|
else
|
||||||
|
./scripts/build-app.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "${APP_BUNDLE}" ]]; then
|
||||||
|
echo "App bundle not found at ${APP_BUNDLE}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Staging DMG contents"
|
||||||
|
rm -rf "${STAGING}"
|
||||||
|
mkdir -p "${STAGING}"
|
||||||
|
ditto "${APP_BUNDLE}" "${STAGING}/Shotdeck.app"
|
||||||
|
ln -s /Applications "${STAGING}/Applications"
|
||||||
|
|
||||||
|
echo "==> Creating ${DMG}"
|
||||||
|
mkdir -p "$(dirname "${DMG}")"
|
||||||
|
hdiutil create -volname "Shotdeck" -srcfolder "${STAGING}" -ov -format UDZO "${DMG}"
|
||||||
|
|
||||||
|
MOUNTED=0
|
||||||
|
detach_dmg() {
|
||||||
|
if [[ "${MOUNTED}" -eq 1 ]]; then
|
||||||
|
hdiutil detach "${MOUNT_POINT}" || hdiutil detach "${MOUNT_POINT}" -force || true
|
||||||
|
MOUNTED=0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap detach_dmg EXIT
|
||||||
|
|
||||||
|
if [[ -d "${MOUNT_POINT}" ]] && /sbin/mount | grep -F -q "${MOUNT_POINT}"; then
|
||||||
|
hdiutil detach "${MOUNT_POINT}" || hdiutil detach "${MOUNT_POINT}" -force
|
||||||
|
fi
|
||||||
|
rm -rf "${MOUNT_POINT}"
|
||||||
|
mkdir -p "${MOUNT_POINT}"
|
||||||
|
|
||||||
|
echo "==> Verifying ${DMG}"
|
||||||
|
hdiutil attach "${DMG}" -nobrowse -readonly -mountpoint "${MOUNT_POINT}"
|
||||||
|
MOUNTED=1
|
||||||
|
|
||||||
|
echo "==> Mount contents"
|
||||||
|
ls -la "${MOUNT_POINT}"
|
||||||
|
|
||||||
|
if [[ ! -d "${MOUNT_POINT}/Shotdeck.app" ]]; then
|
||||||
|
echo "Verification failed: Shotdeck.app missing from mounted DMG" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -L "${MOUNT_POINT}/Applications" ]]; then
|
||||||
|
echo "Verification failed: Applications symlink missing from mounted DMG" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$(readlink "${MOUNT_POINT}/Applications")" != "/Applications" ]]; then
|
||||||
|
echo "Verification failed: Applications does not point at /Applications" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> codesign --verify --deep"
|
||||||
|
codesign --verify --deep --verbose=2 "${MOUNT_POINT}/Shotdeck.app"
|
||||||
|
|
||||||
|
echo "==> Detaching ${MOUNT_POINT}"
|
||||||
|
hdiutil detach "${MOUNT_POINT}"
|
||||||
|
MOUNTED=0
|
||||||
|
trap - EXIT
|
||||||
|
|
||||||
|
SHA256="$(shasum -a 256 "${DMG}" | awk '{print $1}')"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "DMG path: ${DMG}"
|
||||||
|
echo "SHA256: ${SHA256}"
|
||||||
Reference in New Issue
Block a user