MediaRecipe (snake_case Codable, validated() strict decode) + MediaLibraryStore actor persisting media_library.json (sibling of settings.json, cap 200 refuse-with-error, .throwCorrupt keeps bytes, atomic writes).
MediaLibraryViewModel (@MainActorObservableObject): single apply path through the existing applyPreset (#82) mapping; wizard.printerName = recipe.printerDisplayName; queue re-enumerated via serialized PrintSessionViewModel.enumeratePrinters() (fixed ProcessIDs — overlapping listPrinters would throw duplicateID).
Literal CAL_ refusal on the bound .cal stem and live wizard.basename: applyCalibration forced off, buildPrinttargConfig().calibrationFile == nil — printtarg -K can never see a CAL_ file. Blocked apply returns success-with-warning (permanent refusal; re-clicking cannot unstick).
Missing preset / colour-space mismatch / missing queue / missing-or-unparseable .cal → apply returns false, manage sheet stays open, picker reverts (selectedRecipeID written only on success).
Capture: name+paper+ink required; no preset selected → auto-snapshot custom preset bound to the recipe; bound-preset colour-space mismatch refuses Save; .cal path stored verbatim, saveMediaApplyCal forced off for CAL_/missing paths (calApplyable).
Staleness badge mediaRecipeStale: .printer when printerID absent from a non-empty enumerated queue list (empty = indeterminate); .calibration = bound cal CREATED + calibration_stale_days < now (age-only isStale(), no descriptor name compare). Re-evaluates on settingsDidChange and printer-list changes.
Sidebar Media block between preset Save/Manage and Calibrate Printer; mediaSelect ≠ presetSelect; no id on the sidebar container. Sheets live on RootView only; manage sheet is List (macOS 12), double-click/Return applies + dismisses on success.
docs/21 id roster (242 → 267), docs/22 media-library section.
→ 34 unit + 5 UI tests, 0 failures, every selector matched tests.
Full host-arch suite (same command minus -only-testing:):
→ ICCeryCoreTests: 365 executed, 0 failures. ICCeryUITests: 32 executed, 0 failures, real Automation attach (no skip). The known ArgyllRunnerStreamingLoopTests.testNonZeroExitThrowsToolFailed flake did not recur.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Refs #146
Summary
MediaRecipe(snake_case Codable,validated()strict decode) +MediaLibraryStoreactor persistingmedia_library.json(sibling ofsettings.json, cap 200 refuse-with-error,.throwCorruptkeeps bytes, atomic writes).MediaLibraryViewModel(@MainActorObservableObject): single apply path through the existingapplyPreset(#82) mapping;wizard.printerName = recipe.printerDisplayName; queue re-enumerated via serializedPrintSessionViewModel.enumeratePrinters()(fixedProcessIDs — overlappinglistPrinterswould throwduplicateID).CAL_refusal on the bound.calstem and livewizard.basename:applyCalibrationforced off,buildPrinttargConfig().calibrationFile == nil—printtarg -Kcan never see aCAL_file. Blocked apply returns success-with-warning (permanent refusal; re-clicking cannot unstick)..cal→ apply returnsfalse, manage sheet stays open, picker reverts (selectedRecipeIDwritten only on success)..calpath stored verbatim,saveMediaApplyCalforced off forCAL_/missing paths (calApplyable).mediaRecipeStale:.printerwhenprinterIDabsent from a non-empty enumerated queue list (empty = indeterminate);.calibration= bound calCREATED + calibration_stale_days < now(age-onlyisStale(), no descriptor name compare). Re-evaluates onsettingsDidChangeand printer-list changes.mediaSelect≠presetSelect; no id on the sidebar container. Sheets live onRootViewonly; manage sheet isList(macOS 12), double-click/Return applies + dismisses on success.Gates (local — no Gitea Actions on this branch)
Targeted:
→ 34 unit + 5 UI tests, 0 failures, every selector matched tests.
Full host-arch suite (same command minus
-only-testing:):→ ICCeryCoreTests: 365 executed, 0 failures. ICCeryUITests: 32 executed, 0 failures, real Automation attach (no skip). The known
ArgyllRunnerStreamingLoopTests.testNonZeroExitThrowsToolFailedflake did not recur.xcresult:
build/DerivedData-feat-146-media-library/Logs/Test/Test-ICCery-2026.09.12_22-46-57-+0100.xcresultAcceptance criteria
media_library.json(sibling ofsettings.json)PresetMapping; no second Stage 1 formCAL_/ missing.calcannot enableprinttarg -KTextmediaSelectandpresetSelectare distinct identifiersRootViewsheets (preset-dialog style)testHelpOverlaysDoNotChangeSidebarHeightgreen)Hardware gate (real Epson/Canon + real
.cal) is the milestone-level check, not this PR.