WP-0: SwiftPM scaffold, shared model, signed app bundle script

This commit is contained in:
2026-08-30 20:56:37 +04:00
parent c06b8da052
commit 97d21be470
12 changed files with 495 additions and 2 deletions
+43
View File
@@ -0,0 +1,43 @@
import Foundation
/// One screenshot plus everything needed to place it on a PDF page.
public struct Capture: Codable, Sendable, Identifiable, Equatable {
public let id: UUID
/// 1-based position within its session. Stable; never renumbered on delete.
public let sequence: Int
/// File name only (e.g. "001-A1B2C3D4.png"), never a path.
/// The directory is always the owning session's directory.
public let fileName: String
/// Pixel dimensions of the PNG on disk.
public let pixelWidth: Int
public let pixelHeight: Int
/// Backing scale the capture was taken at (2.0 on Retina). Needed so the PDF
/// composer lays the image out at its true point size, not its pixel size.
public let scale: CGFloat
/// When the screenshot was taken. Always stored as an absolute instant;
/// rendered in Asia/Dubai wherever a human sees it.
public let capturedAt: Date
public init(
id: UUID,
sequence: Int,
fileName: String,
pixelWidth: Int,
pixelHeight: Int,
scale: CGFloat,
capturedAt: Date
) {
self.id = id
self.sequence = sequence
self.fileName = fileName
self.pixelWidth = pixelWidth
self.pixelHeight = pixelHeight
self.scale = scale
self.capturedAt = capturedAt
}
/// True when the image is wider than it is tall. Drives page orientation.
public var isLandscape: Bool {
pixelWidth > pixelHeight
}
}
@@ -0,0 +1,73 @@
import Foundation
public enum SessionState: String, Codable, Sendable {
case open // accepting captures
case archived // its PDF has been built and sent; kept forever, never deleted
}
/// The manifest persisted as session.json alongside the PNGs.
public struct CaptureSession: Codable, Sendable, Identifiable, Equatable {
public let id: UUID
public let createdAt: Date
public private(set) var state: SessionState
public private(set) var captures: [Capture]
/// Set when the PDF is built, so a re-send reuses the same file.
public private(set) var pdfFileName: String?
public init(
id: UUID,
createdAt: Date,
state: SessionState,
captures: [Capture],
pdfFileName: String?
) {
self.id = id
self.createdAt = createdAt
self.state = state
self.captures = captures
self.pdfFileName = pdfFileName
}
public var isEmpty: Bool {
captures.isEmpty
}
/// max(sequence)+1, or 1 when empty.
public var nextSequence: Int {
(captures.map(\.sequence).max() ?? 0) + 1
}
/// Value-semantic: returns a new session; does not mutate `self`.
public func appending(_ capture: Capture) -> CaptureSession {
CaptureSession(
id: id,
createdAt: createdAt,
state: state,
captures: captures + [capture],
pdfFileName: pdfFileName
)
}
/// Value-semantic: returns a new session; does not mutate `self`.
/// Sequence numbers of remaining captures are left unchanged.
public func removing(captureID: UUID) -> CaptureSession {
CaptureSession(
id: id,
createdAt: createdAt,
state: state,
captures: captures.filter { $0.id != captureID },
pdfFileName: pdfFileName
)
}
/// Value-semantic: returns a new session; does not mutate `self`.
public func markArchived(pdfFileName: String) -> CaptureSession {
CaptureSession(
id: id,
createdAt: createdAt,
state: .archived,
captures: captures,
pdfFileName: pdfFileName
)
}
}
@@ -0,0 +1,36 @@
import Foundation
public enum ShotdeckError: Error, LocalizedError, Sendable {
case screenRecordingNotGranted
case noRegionRemembered
case displayNoLongerConnected(displayID: UInt32)
case captureFailed(underlying: String)
case spoolWriteFailed(path: String, underlying: String)
case manifestCorrupt(path: String)
case pdfCompositionFailed(reason: String)
case airDropUnavailable
case noCommentedReturns
public var errorDescription: String? {
switch self {
case .screenRecordingNotGranted:
return "Screen Recording is turned off. Grant it in System Settings to capture."
case .noRegionRemembered:
return "No capture region is set. Press ⌥⇧1 to pick one."
case .displayNoLongerConnected:
return "The display used for capture is no longer connected."
case .captureFailed(let underlying):
return "The screenshot could not be taken. \(underlying)"
case .spoolWriteFailed(_, let underlying):
return "The screenshot could not be saved. \(underlying)"
case .manifestCorrupt:
return "This session's file is damaged and cannot be opened."
case .pdfCompositionFailed(let reason):
return "The PDF could not be built. \(reason)"
case .airDropUnavailable:
return "AirDrop is not available right now."
case .noCommentedReturns:
return "None of the returned PDFs have comments on them."
}
}
}