Skip to content

Latest commit

 

History

History
611 lines (430 loc) · 21 KB

File metadata and controls

611 lines (430 loc) · 21 KB

PicChronoImporter Specification

1. Purpose

PicChronoImporter is a macOS SwiftUI application for importing photos and videos from an SD card or a selected folder.

The app scans source files, resolves capture dates from metadata, organizes imported files into date-based destination folders, and lets the user review, filter, sort, select, and resolve conflicts before importing.

The primary expected camera workflow includes FUJIFILM X100VI RAW and compressed image files, but the app must not be specific to RAF files.

2. Primary workflow

The intended user workflow is:

  1. Select a source volume or folder.
  2. Select a scan scope.
  3. Select a destination root folder.
  4. Select a folder naming template.
  5. Scan source files.
  6. Review detected files.
  7. Filter, sort, and select files to import.
  8. Resolve conflicts before execution.
  9. Import selected files.
  10. Review the result summary.

3. Source

v1 supports one source at a time.

A source can be:

  • a mounted volume
  • a selected folder

Source selection persistence:

  • The app supports selecting a mounted volume root, including default volume names such as /Volumes/Untitled.
  • In the current non-sandboxed macOS app, if macOS rejects security-scoped bookmark creation for a selected volume root, the app may persist that selection using a standard bookmark.
  • This is a v1 compatibility behavior for non-sandboxed builds. If App Sandbox support is introduced, volume-root source selection and bookmark restoration must be re-evaluated because standard bookmarks must not be treated as long-term sandbox access grants.

The user can choose a scan scope:

  • DCIM only
  • entire volume
  • selected custom folder

The app may remember the previous scan scope for a known volume when a volume identifier is available.

4. Destination

The user selects a destination root folder.

The app creates subfolders under the destination root according to the resolved capture date and selected folder naming template.

If a capture date cannot be resolved, the file must be placed under:

UnknownDate

The selected folder template must not be applied to unknown dates.

5. Folder templates

Folder templates are limited to year, month, and day.

Allowed placeholders:

  • yyyy
  • MM
  • M
  • dd

Allowed separators:

  • /
  • -

/ means folder hierarchy.

5.1 1-level templates

  • yyyy-MM-dd
  • yyyyMMdd

5.2 2-level templates

  • yyyy/MM-dd
  • yyyy/yyyy-MM-dd
  • yyyy/MM
  • yyyy/M

5.3 3-level templates

  • yyyy/MM/dd
  • yyyy/yyyy-MM/yyyy-MM-dd

6. Capture date resolution

Use this priority order:

  1. EXIF DateTimeOriginal
  2. Alternate metadata such as digitized date, image create date, or movie container creation date
  3. File timestamp
  4. Unknown date

The resolved date source should be represented in the domain model so the UI can show where the date came from.

Conceptual date sources:

  • exifDateTimeOriginal
  • exifAlternate
  • fileTimestamp
  • unknown

On macOS, video creation dates are loaded from the media container with AVFoundation and represented as exifAlternate by the existing Core contract. A readable video without container creation metadata falls back to its file timestamp. A video metadata read failure remains an item-level review error, and cancellation propagates.

7. File model

Each file is treated as an independent import item.

Do not assume a strict RAW+JPEG pair model.

This is important because camera workflows may generate multiple compressed images from one RAW file, such as camera-internal RAW development with different film simulations.

RAW files and compressed images are both first-class import items.

8. Media classification

Classify files into:

  • rawImage
  • compressedImage
  • video
  • sidecar
  • other

Use UTType where practical, then extension fallback.

File extension sets should be centralized and extensible.

Do not hardcode behavior to RAF only.

RAW extensions may include RAF, DNG, CR2, CR3, NEF, ARW, RW2, ORF, and others as the implementation grows.

Compressed-image extension fallback includes both HIF and HEIC. FUJIFILM X100VI HEIF files may use HIF on the memory card and HEIC after transfer.

9. Review screen

After scanning, the app displays a review list.

Each row should show:

  • selection checkbox
  • thumbnail or icon
  • filename
  • extension
  • media kind
  • capture date
  • date source
  • planned destination path
  • file size
  • status
  • conflict action, where applicable

9.1 Thumbnail presentation

The Review table shows the thumbnail immediately to the right of the selection checkbox inside the Import cell. Its media-kind column shows text only so an icon fallback is not duplicated in the same row. The Result table keeps the thumbnail inside the Source cell. Neither table adds a dedicated thumbnail column.

  • The thumbnail frame is fixed at 32 x 32 pt, uses a 4 pt corner radius, and center-crops generated images.
  • RAW images, compressed images, and videos use asynchronous macOS Quick Look thumbnail generation.
  • Sidecars, other files, loading state, explicit generation errors, and generation timeouts use the media-kind icon in the same fixed frame. Loading does not add a spinner or progress indicator.
  • The Review screen always requests the thumbnail from ScanItem.file.absolutePath in the selected source folder.
  • The Result screen first reuses a successful in-memory thumbnail for the same review session and item. On a cache miss, copied, renamed, and overwritten outcomes use the actual destination path recorded in destinationRelativePath; all other outcomes use the source path.
  • A renamed result must use the resulting destination path, not the originally planned path.

Thumbnail generation is presentation infrastructure and must not be added to Core use cases or result models.

  • App-managed thumbnail caching is memory-only and scoped to one review session. Starting a new review session clears successful images, failures, and pending requests.
  • At most four Quick Look generations may execute concurrently. Identical pending requests are coalesced.
  • The successful-image cache is limited to 32 MiB and 512 entries.
  • A 30 second insurance timeout starts only after a request receives a generation slot and Quick Look generation actually starts. Time spent waiting for a slot is not included. Task cancellation or timeout must cancel the corresponding Quick Look request.
  • An explicit error or timeout is not retried automatically during the same session for the same file URL, file size, modification date, requested size, and scale. A changed URL or file identity is a new request.
  • Cancellation caused by a row leaving the visible table is not failure-cached, so redisplaying the row may request the thumbnail again.
  • An error does not trigger a second source/destination fallback request. It changes only the thumbnail to the localized unavailable icon and tooltip; it must not show a workflow banner or alert, change item status or import results, or block selection and import operations.

9.2 Large review presentation

The Review table displays at most 100 filtered items per page. Search, filters, sorting, status counts, and bulk selection continue to operate on the complete review session rather than only the current page. Changing search, filter, or sort criteria returns the table to the first page.

This presentation bound keeps table accessibility, row hosting, and thumbnail work responsive without changing per-file import semantics.

10. Item statuses

Use these conceptual statuses:

  • unclassified
  • notImported
  • checking
  • duplicate
  • conflict
  • needsReview
  • conflictDuringImport

Meanings:

  • unclassified: initial state before classification or duplicate detection
  • notImported: no duplicate or conflict was detected
  • checking: background duplicate/hash check is in progress
  • duplicate: content hash matches an indexed destination file
  • conflict: conflict candidate exists and requires user decision
  • needsReview: metadata, permission, or file-system problem requires user attention
  • conflictDuringImport: destination state changed after review and before import execution

11. Review operations

Required bulk actions:

  • select all
  • clear selection
  • select not-imported only
  • select conflicts only

select not-imported only must exclude:

  • duplicates
  • conflicts
  • needsReview items
  • conflictDuringImport items

Required filters:

  • status
  • media kind
  • selection state
  • filename search

Required sort keys:

  • capture date
  • status
  • media kind
  • size
  • filename

12. Duplicate and conflict detection

Use two-stage detection.

12.1 Stage A: fast candidate detection

Stage A is fast and approximate.

Use:

  • app-maintained destination index
  • same destination filename check where useful
  • file size where useful

v1 must not scan and index the entire existing destination folder.

Stage A must not conclude that a file is a true duplicate by filename alone.

The v1 Stage A implementation queries the app-maintained destination cache for an entry with the same persistent destination volume and file size. The query uses a dedicated role + volume + size index and does not enumerate the destination folder. If there is no same-size candidate, duplicate detection skips source hashing but still performs cached planned-path and live destination conflict checks.

12.2 Stage B: strict confirmation

Stage B confirms duplicates by content hash.

A file is duplicate only if its content hash matches an indexed destination file.

Use cached source hash where possible.

Use SHA-256 or equivalent strong hash.

Hash computation may run in the background.

13. Conflict handling

Default conflict action is:

skip

Conflicts are resolved before import in the review screen.

Individual conflicted files may override the default action:

  • skip
  • rename
  • overwrite

Do not show repeated modal popups during import for each conflict.

During import, determine the destination path and prepare the source hash before the final destination re-check. The per-file order is:

  1. determine the destination path, including rename, and create its parent directory
  2. prepare the source hash and upsert the source cache entry when available
  3. perform the final destination existence/hash check
  4. copy or replace the file
  5. update the destination cache only when its persistent volume scope is available

If the destination changed since review:

  1. mark the item as conflictDuringImport
  2. skip that file
  3. continue importing other files
  4. report it in the result summary

If a destination appears after the final check but before a non-overwrite copy, the importer must return a typed destination-exists error. Treat it as conflictDuringImport, preserve the destination, and continue with later items.

14. Import session policy

v1 supports only one active import session at a time.

Do not implement multiple simultaneous import sessions in v1.

v1 presents one main workflow window. Do not expose commands for creating additional windows or using window tabs, because multiple views must not control the same import session.

The app may use a lock file or equivalent guard to avoid accidental double execution.

The UI should disable the start button while an import session is active.

15. Cache

Use a cache store behind the CacheStore protocol.

The intended production implementation is SQLite.

The cache stores:

  • source file identity to content hash
  • destination content hash index for files imported by this app

The cache must not store:

  • image bodies
  • thumbnails
  • large binary previews

The presentation-layer thumbnail store described in section 9.1 is separate from CacheStore and SQLite. The app must not create its own thumbnail disk cache. OS-managed caching used internally by Quick Look is allowed.

The v1 cache maintenance policy is fixed, not user-configurable:

  • remove entries whose last_seen_at is strictly earlier than now - 365 days
  • after the age sweep, keep the checkpointed SQLite main database at or below 128 MiB (128 * 1024 * 1024 bytes) where physically possible

The size sweep uses least-recently-used order, with last_seen_at ASC, id ASC as the deterministic eviction order. It checkpoints WAL first. If reclaiming free pages can satisfy the limit, it vacuums before deleting rows; otherwise it deletes a batch proportional to the measured excess, vacuums, remeasures, and repeats. If an empty database still cannot meet the limit, the result must explicitly report that the limit was not reached.

Settings reports databaseSizeBytes as the SQLite main file plus WAL size. The 128 MiB maintenance decision is made against the main file after checkpoint, because WAL bytes have been incorporated before the size decision.

Live mode starts one asynchronous maintenance task during ViewModel startup. The first scan, Settings refresh, clear-all, or clear-volume operation waits for that task. Maintenance failure is logged but does not make the app unusable. Fixture and preview modes do not automatically run startup maintenance.

16. Cache identity

File-system root paths locate files but are not stable cache volume identities. ScanRequest, ReviewSession, and ImportPlan carry separate source and destination CacheVolumeScope values from scan through import.

For a persistent live scope, PlatformMac must resolve the symlink-normalized selected root to:

  • the underlying volume UUID, normalized to lowercase
  • a rootRelativePath relative to the underlying volume root, with no leading slash

Cache relative_path values are volume-root-relative:

source rootRelativePath + scanned file relative path
destination rootRelativePath + planned destination relative path

UI display, file operations, and DestinationConflictSnapshot continue to use paths relative to the selected source or destination root as appropriate.

If the UUID, volume root, or safe relative path cannot be resolved, use CacheVolumeScope.unavailable. Do not substitute a mount path as an identifier. Processing continues, but persistent reads and writes for that volume are bypassed. Live destination lookup and the import-time final check remain mandatory even when destination cache scope is unavailable.

Fixture mode uses existing JSON volumeUuid values. If a fixture has no file or cache entry for one side, that side uses a deterministic fixture-only identifier. Fixture version 1 also accepts an additive optional volumes array containing volumeUuid, optional displayName, optional capacityBytes, and lastUsedAt. A fixture without this array derives deterministic fallback volume summaries from its UUIDs and fixture now value.

Use id INTEGER PRIMARY KEY for SQLite row identity.

Use uniqueness equivalent to:

role + volume_uuid + relative_path + size_bytes + mtime

Roles:

  • source
  • dest

Use indexes for:

  • content hash lookup
  • destination role, volume, and size candidate lookup
  • last seen cleanup

The SQLite cache schema version is 2. Opening a version 1 database deletes all cache_entries and then all volumes in one transaction before setting user_version = 2, because v1 path-scoped rows are derived data and cannot be safely reinterpreted as UUID-backed identities. Existing version 2 rows are preserved, and versions newer than supported are rejected.

17. Cache operations

Support:

  • clear all cache
  • clear cache for a volume
  • automatic cleanup by age
  • automatic cleanup by size
  • removal of entries for missing files where practical
  • stats for settings UI

Clearing all cache or one volume removes only app-managed cache records. It must never delete source photos, videos, or files in the destination tree.

clearVolume removes both source and destination entries for the same UUID. Per-volume summaries are ordered by last_used_at DESC, volume_uuid and expose the display name, total capacity at metadata collection time, last-used date, source count, destination count, total count, and UUID. Metadata updates affect existing volume rows only; they do not create empty volume rows, and a nil new value preserves the stored name or capacity.

The UI should identify volumes using:

  • display name
  • capacity
  • last used date
  • UUID shown only as detail information

Settings shows the fixed 365 days / 128 MiB policy and a destructive, confirmed clear action for each volume. The confirmation includes the display name and entry count and states that original and destination files are not deleted. When a display name is unavailable, show the UUID; when capacity is unavailable, show a localized unavailable value. Notices and all Settings copy must follow an in-app language change.

Before using an app-cached destination content-hash or planned-path candidate, validate only that candidate against the currently mounted volume. Resolve the lowercase UUID, accept only safe volume-relative paths, and compare the current file size and modification date with the exact cached identity. Treat not-found as missing and a size or modification-date mismatch as changed; remove that exact cache row in either case. A stale hash candidate triggers another cache lookup, while a stale path candidate falls through to the normal live conflict check. Permission, volume-resolution, or other I/O failures keep the row and mark only the affected item needsReview; cancellation propagates. Do not scan an entire volume or build a full destination index for this validation.

18. Fixture policy

Use JSON fixtures for UI tests and fake dependencies.

Fixture files should live under:

MacApp/PicChronoImporter/Resources/Fixtures/

Suggested fixtures:

  • basic_20items.json
  • has_duplicates.json
  • has_conflicts.json
  • unknown_dates.json
  • large_1000items.json

Fixtures must be deterministic.

A fixture file fault may include an optional thumbnailError so thumbnail success and explicit failure can be reproduced without real image files or an SD card.

UI tests must not require:

  • a real SD card
  • a real user photo library
  • manual file selection
  • network access

19. macOS UI presentation

Prefer standard macOS window, toolbar, form, table, group, and control hierarchy over decorative chrome.

Use Liquid Glass selectively for the unified toolbar, important actions, and transient or floating surfaces. Do not wrap every content section in Glass or rely on decorative background gradients.

Display PicChronoImporter only once in the unified toolbar. Do not duplicate the app name in both the native window title and a content header.

Keep the app-name label visually plain inside the unified toolbar; do not place it in its own decorative Glass container.

Screen headings may pair an SF Symbol with text for recognition, but the icon must not be isolated inside a decorative circular Glass frame.

Keep the unified toolbar and content visually continuous on every workflow screen. Suppress the automatic AppKit titlebar separator and render the toolbar over the same single subdued bar-material layer that fills the window background. Do not stack separate toolbar and content material layers, because even equivalent materials can leave a visible seam. Individual toolbar controls may retain their own Liquid Glass surfaces.

When a workflow table is inset from the window edges, give its outer boundary a subtle continuous corner radius. Apply the same treatment consistently to the Review and Result tables. Preserve the native table header, rows, scrolling, and system background instead of turning the table into a decorative card.

For related form groups, align label and value columns across group boundaries and keep trailing actions in a stable column. A Picker with an adjacent row label must hide its own visual label so the setting name is not repeated, and its selected value must remain readable without truncation at the minimum window size.

20. Testing requirements

Automated tests should cover:

  • folder template generation
  • UnknownDate handling
  • capture date resolution priority
  • media classification
  • filtering
  • sorting
  • bulk selection
  • conflict policy
  • duplicate state transitions
  • cache reuse
  • import result behavior
  • fixture-driven UI flows
  • Review and Result thumbnail source routing and in-memory cache reuse
  • thumbnail concurrency, coalescing, timeout, cancellation, and failure-cache behavior using short injected durations rather than a real 30-second wait
  • non-blocking thumbnail fallback behavior in fixture-driven UI flows
  • opt-in representative real-media validation for RAF, JPEG, HIF or HEIC, and MOV without making private media a deterministic test dependency

The Phase 11D representative baseline passed on Xcode 26.6 and macOS 26.5.2 with X100VI RAF, JPEG, HIF, HEIC, and MOV files. The run covered live scanning, metadata dates and sources, destination planning, source/destination Quick Look thumbnails, duplicate detection, real copies, sizes, and SHA-256 equality.

Use Swift Testing for Core tests where practical.

Use XCUITest for UI tests.

21. v1 non-goals

Do not implement in v1:

  • multiple simultaneous import sessions
  • full destination library indexing
  • strict RAW+JPEG pair UI
  • menu bar resident behavior
  • Windows UI
  • real-time folder watching
  • background automatic import