Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Keep these four files consistent when architecture or agent guidance changes.
- Do not delete a second `DialogIcon.swift in Sources` (or other shared files) from `project.pbxproj` — that is two-target membership, not a duplicate file.
- Do not force-unwrap `UTType("com.apple.finalcutpro.xml")` / `.xmld` — resolve with `UTType(identifier) ?? UTType(importedAs:identifier, conformingTo:)` and keep `UTImportedTypeDeclarations` in `Marker-Data-Info.plist`. First Extract layout evaluates these types; FCP may be absent (App Preview). Do not split the Share Destination **Asset Description File** document type.
- File menu custom items replace `.newItem` so system Close stays last — do not empty `.newItem` and do not add a second Close.
- Do not discard `Task { try await … }` — Xcode 27 warns (`#NoUseUnstructuredThrowingTask`) because those errors are swallowed. Catch inside, drop a spurious `throws`/`try`, or call `@MainActor` methods directly (not `_ = Task`).

## Definition of done for code changes
- App builds **unsigned** Debug and Release for arm64 (`CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO`; Workflow Extension still embeds).
Expand Down
1 change: 1 addition & 0 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ Exact overlay strings and always/never rules: **`GUARDRAILS.md`**.
- **File menu Close last:** custom File items replace `.newItem` (`FileCommands`). Do not empty `.newItem` (that puts system Close first) and do not add a second Close.
- **Pagemaker `confirm` is unreachable in-app:** the only `confirm(` in bundled `Resources/Pagemaker.html` sits behind `isMacOSSafari()`, and WKWebView’s default user agent has no `Safari` token. Keep the delegate correct, but you cannot exercise it from the UI — and `update_pagemaker.yml` can change that HTML upstream.
- **Tolerant `Color ==`:** `ColorExtension.swift` overrides `Color ==` with a 0.1-per-channel tolerance that shadows SwiftUI’s and drives `SettingsStore` equality, so small colour edits do not mark a configuration dirty. It is not transitive — never put `Color` or `SettingsStore` in a `Set`, key a dictionary on them, or dedupe an array. Full reasoning in **GUARDRAILS.md → Signs**.
- **Unused throwing unstructured `Task`:** Xcode 27 (`#NoUseUnstructuredThrowingTask`) warns when `Task { try await … }` is discarded — those errors were already being swallowed. Catch inside, drop a spurious `throws`/`try`, or call `@MainActor` methods directly. Do not silence with `_ = Task`. `DatabaseManager.loadProfilesFromDisk` keeps a **non-throwing** deferred `Task { await MainActor.run }` so profiles still appear after `init`.

## Definition of done for most changes
- App builds **unsigned** Debug **and** Release for **arm64** (`CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO`; Workflow Extension still embeds).
Expand Down
4 changes: 3 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ Settings model: `ColorSwatchSettingsModel` (nested under SettingsStore, Codable)
- Validation: unique profile names; must not collide with extract-only format display names
- Property spelling **`plaform`** is intentional in current code — preserve when editing
- `duplicateProfile(profileName:)` appends `" copy"` and runs `addProfile(saveToDisk: true)` synchronously on the main actor, so `DatabaseValidationError.nameAlreadyExists` reaches `DatabaseSettingsView`’s “Failed to duplicate profile” alert. Duplicating the same profile twice is refused by design (matching `ConfigurationsViewModel.duplicateConfiguration`, which also uses a flat `" copy"`); neither panel auto-increments.
- `loadProfilesFromDisk()` still defers `addProfile(..., saveToDisk: false)` with a non-throwing `Task { await MainActor.run }` so profiles can appear after `init` (same launch timing as before). Do not put `try` on that task.

| Model | Notable fields |
|-------|----------------|
Expand Down Expand Up @@ -855,4 +856,5 @@ Source/Marker Data/Marker Data Uninstaller/
15. Workflow Extension decodes `SettingsStore` without running migrations — main app must migrate on launch first.
16. FCPXML UTTypes must resolve without Final Cut Pro installed: public `UTType.fcpxml` / `.fcpxmld` (private `finalCutProType`: `UTType(identifier)` then `importedAs:conformingTo:`) plus `UTImportedTypeDeclarations` in `Marker-Data-Info.plist`. Never `UTType("com.apple.finalcutpro.xml")!`. Keep the Share Destination **Asset Description File** document type (do not rename/split it).
17. File menu custom items replace `.newItem` (`FileCommands`). Do not empty `.newItem` in `Marker_DataApp` (puts system Close first) and do not add a second Close.
18. Definition of done: unsigned arm64 Debug+Release build; settings migrate; `.fcpxml`/`.fcpxmld` + pasteboard intake; FCPXML UTTypes use `importedAs` fallback; File menu Close last; WE Extract + Roles overlays; queue finds/uploads via `manifestURL`; agent docs (`AGENT.md` / `ARCHITECTURE.md` / `GUARDRAILS.md` / `.cursorrules`) stay aligned.
18. Do not discard throwing unstructured `Task { try await … }` (Xcode 27 `#NoUseUnstructuredThrowingTask`). Catch inside, drop a spurious `throws`/`try`, or call `@MainActor` methods directly. `loadProfilesFromDisk` may keep a **non-throwing** deferred `Task { await MainActor.run }` so launch timing stays unchanged.
19. Definition of done: unsigned arm64 Debug+Release build; settings migrate; `.fcpxml`/`.fcpxmld` + pasteboard intake; FCPXML UTTypes use `importedAs` fallback; File menu Close last; WE Extract + Roles overlays; queue finds/uploads via `manifestURL`; agent docs (`AGENT.md` / `ARCHITECTURE.md` / `GUARDRAILS.md` / `.cursorrules`) stay aligned.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

### 2.1.4 (28)

**🎉 Released:**
- 15th September 2026

**🐞 Bug Fix:**
- Fixed build warnings that appeared in Xcode 27

---

### 2.1.3 (27)

**🎉 Released:**
Expand Down
3 changes: 2 additions & 1 deletion GUARDRAILS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,13 @@ Shared component: `Views/Components/DropTargetOverlay.swift` (main app + Workflo
15. **Workflow Extension does not migrate settings.** `RolesManager` JSON-decodes `SettingsStore` with no `SettingsVersioningManager`. Open the main app after a schema bump.
16. **FCPXML `UTType` force-unwrap** — `UTType("com.apple.finalcutpro.xml")!` traps on Extract first layout when Final Cut Pro is absent (App Preview / review Macs). Use `importedAs` fallback and Info.plist imported types. Do not copy the old Marker Data force-unwrap.
17. **File menu Close last:** custom File items replace `.newItem`. Emptying `.newItem` makes system Close the first File item; do not duplicate Close.
18. **`Task { try await MainActor.run { … } }` inside a `throws` method swallows the error.** The task result is discarded, so the method always returns normally and the caller’s `catch` is dead. `DatabaseManager` is already `@MainActor` — call directly and let the error propagate. `duplicateProfile` was fixed this way; `setActiveProfile` and `loadProfilesFromDisk` still use the wrapper but throw nothing, so they are harmless. Do not reintroduce the pattern for anything that validates.
18. **`Task { try await MainActor.run { … } }` inside a `throws` method swallows the error.** The task result is discarded, so the method always returns normally and the caller’s `catch` is dead. `DatabaseManager` is already `@MainActor` — call directly and let the error propagate. `duplicateProfile` was fixed this way; `setActiveProfile` and `loadProfilesFromDisk` still use a non-throwing `Task { await MainActor.run }` wrapper (no `try`) so launch timing stays deferred and Xcode 27 does not warn. Do not reintroduce `try` on those wrappers, and do not use the pattern for anything that validates.
19. **Record an extract failure once.** `extractAndUpdateProgress` must rethrow the `MarkersExtractor` error, not append to `failedTasks` — appending there and then tripping the `exportResultisNil` guard produced two entries for one file, the second reading “Failed to get export result”, and duplicate `ExtractionFailure.id` (the URL) in `FailedExtractionsView`. The task group `catch` is the single recording point.
20. **`NSAlert` does not pick up the Icon Composer icon**, and its `alertStyle` defaults to `.warning`. Pagemaker’s WebView JavaScript panels are the app’s only `NSAlert`s — route them through `PagemakerUIDelegate.makeAlert(message:)` (sets `MarkerDataAppIcon.alertImage`, `.informational`, message as `messageText`). Never re-add generic “Alert” / “Confirm” / “Prompt” titles; SwiftUI dialogs elsewhere have no such title. Keep `confirm` returning `.alertFirstButtonReturn` so Cancel reaches JavaScript as `false`.
21. **`Color ==` is tolerant, app-wide, and not transitive.** `ColorExtension.swift` defines `static func == (Color, Color)` delegating to `isEqual(to:tolerance: 0.1)`; Swift prefers the same-module declaration, so this shadows SwiftUI's exact `==` for **every** `Color` comparison compiled into the main app — including synthesized `Equatable` on `SettingsStore` (verified). Consequences: a colour tweak under ~25/255 in all three channels leaves `SettingsContainer.checkForUnsavedChanges()` false, so the Configurations “Changed” badge and ⌘S / ⌘Z stay disabled even though `preferences.json` was already updated; and `0.0 == 0.1`, `0.1 == 0.2`, `0.0 != 0.2` breaks `Equatable` transitivity, so never put `Color` (or `SettingsStore`) in a `Set`, use it as a dictionary key, or dedupe an array of them. The tolerance exists because `Color.hex` **truncates** (`Int(components.red * 255.0)`): 179 of 256 sampled colours drift one step through encode/decode, which under exact equality would mark the store permanently dirty. Fix properly by rounding in `hex` and comparing `lhs.hex == rhs.hex` — measured zero round-trip failures — not by lowering the tolerance alone. Note `hex` also feeds `imageLabelFontColor` / `imageLabelFontStrokeColor`, so the drift reaches rendered labels.
22. **Pagemaker’s `confirm` panel is unreachable in-app — do not “verify” it by clicking.** The only `confirm(` in bundled `Resources/Pagemaker.html` is gated behind `isMacOSSafari()`, and WKWebView’s default user agent carries no `Safari` token, so the branch never runs inside Marker Data. `webView(_:runJavaScriptConfirmPanelWithMessage:…)` must still return `.alertFirstButtonReturn` correctly (Sign 20), but that contract cannot be exercised from the UI — reason about it, don’t test-and-conclude. Two things would make it live: setting `applicationNameForUserAgent` to include `Safari`, or an upstream Pagemaker change, since `update_pagemaker.yml` refreshes that HTML outside this repo’s review.
23. **`.appDialogIcon()` propagates through the environment.** It wraps SwiftUI `.dialogIcon(_:)`, so one trailing call covers every `.alert` / `.confirmationDialog` earlier in the same modifier chain. `ConfigurationSettingsView` has four dialogs and **one** call — that is correct, not a missing icon; `DatabaseSettingsView`’s three calls are redundant but harmless. Only a dialog-bearing chain with **no** call anywhere is broken. Keep adding one per dialog in new code (safe habit), but do not “fix” an existing single trailing call.
24. **Unused throwing unstructured `Task` (Xcode 27 / `#NoUseUnstructuredThrowingTask`).** `Task { try await … }` without storing the task or catching inside silently drops errors. Handle with `do/catch` (or drop a spurious `throws` / `try`) so the task is non-throwing; on `@MainActor` types, call directly. Do not silence with `_ = Task`.

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,14 @@ class DatabaseManager: ObservableObject {
do {
let data = try Data(contentsOf: url)
let decoded: DatabaseProfileModel = try decoder.decode(T.self, from: data)


// Same deferred MainActor hop as before: decode here, append
// after init returns. Non-throwing Task so Xcode 27 does not
// warn (#NoUseUnstructuredThrowingTask). saveToDisk: false
// does not throw.
Task {
try await MainActor.run {
try self.addProfile(decoded, saveToDisk: false)
await MainActor.run {
try? self.addProfile(decoded, saveToDisk: false)
}
}
} catch {
Expand Down
8 changes: 4 additions & 4 deletions Source/Marker Data/Marker Data/Models/Queue/QueueModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class QueueModel: ObservableObject {
self.databaseManager = databaseManager
}

func scanFolder(at url: URL, append: Bool = false) async throws {
func scanFolder(at url: URL, append: Bool = false) async {
// Skip if upload is in progress
if self.uploadInProgress {
return
Expand Down Expand Up @@ -84,10 +84,10 @@ class QueueModel: ObservableObject {
throw QueueError.missingOutputDirectory
}

try await self.scanFolder(at: exportFolder)
await self.scanFolder(at: exportFolder)
}

func upload() async throws {
func upload() async {
defer {
self.uploadInProgress = false
self.taskGroup = nil
Expand Down Expand Up @@ -148,7 +148,7 @@ class QueueModel: ObservableObject {
// Check file type
if url.hasDirectoryPath {
Task { [weak self] in
try await self?.scanFolder(at: url, append: true)
await self?.scanFolder(at: url, append: true)
}
} else {
Self.logger.notice("Skipping file \(url.path(percentEncoded: false)). Not supported.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,12 @@ extension RolesManager: DropDelegate {
return
}

if let extractedRoles = await self.getRoles(fcpxml: try FCPXMLFile(at: urlUnwrapped)) {
await self.setRoles(extractedRoles)
do {
if let extractedRoles = await self.getRoles(fcpxml: try FCPXMLFile(at: urlUnwrapped)) {
await self.setRoles(extractedRoles)
}
} catch {
Self.logger.error("Failed to load FCPXMLD: \(error.localizedDescription, privacy: .public)")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ class SettingsContainer: ObservableObject {
.dropFirst() // Ignore initial value
.sink { _ in
Task(priority: .background) { @MainActor in
try await self.store.saveAsCurrent()
do {
try await self.store.saveAsCurrent()
} catch {
Self.logger.error("Failed to save store: \(error.localizedDescription, privacy: .public)")
}

await self.checkForUnsavedChanges()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ extension Task where Failure == Error {
static func synchronous(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) {
let semaphore = DispatchSemaphore(value: 0)

Task(priority: priority) {
// Explicit non-throwing Task so Xcode 27 does not warn about a discarded
// throwing unstructured task (#NoUseUnstructuredThrowingTask). Errors
// cannot be rethrown across the semaphore wait; the only caller is
// settings migration, which does not throw.
Task<Void, Never>(priority: priority) {
defer { semaphore.signal() }
return try await operation()

do {
_ = try await operation()
} catch {}
}

semaphore.wait()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ struct QueueView: View {
// Start upload button
Button {
Task {
try await queueModel.upload()
await queueModel.upload()
}
} label: {
Label("Start Upload", systemImage: "square.and.arrow.up")
Expand Down