WP-0: SwiftPM scaffold, shared model, signed app bundle script
This commit is contained in:
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user