feat(exif): extract capture date and surface as sortable taken_at field

Adds EXIF date extraction during indexing so photos can be sorted by when
they were actually taken, not just when the file was modified on disk.

- Add `taken_at` (nullable TEXT, ISO 8601) column to the images table via
  ensure_column migration and a new idx_images_taken_at index
- Populate taken_at in build_record via extract_exif_date, which tries
  DateTimeOriginal → DateTimeDigitized → DateTime tags and rejects all-zero
  sentinel dates written by uninitialised cameras
- upsert_image uses taken_at = excluded.taken_at so a re-indexed file gets
  fresh EXIF; stale dates from replaced files are not preserved
- Add taken_asc / taken_desc sort using COALESCE(taken_at, created_at) so
  images without EXIF gracefully fall back to their creation date
- Surface as "Taken: newest / oldest" in the Toolbar sort dropdown
- Shift map_image_row column indices after the new taken_at (index 10)
This commit is contained in:
2026-06-08 00:26:36 +01:00
parent 0ca4d142d8
commit 9ee5b08c93
6 changed files with 105 additions and 27 deletions
+2
View File
@@ -5,6 +5,8 @@ import { tileSizeForZoom, useGalleryStore, SortOrder, MediaFilter, SearchCommand
const BASE_SORT_OPTIONS: { value: SortOrder; label: string }[] = [
{ value: "date_desc", label: "Newest first" },
{ value: "date_asc", label: "Oldest first" },
{ value: "taken_desc", label: "Taken: newest" },
{ value: "taken_asc", label: "Taken: oldest" },
{ value: "name_asc", label: "Name AZ" },
{ value: "name_desc", label: "Name ZA" },
{ value: "rating_desc", label: "Highest rated" },
+8 -1
View File
@@ -37,6 +37,7 @@ export interface ImageRecord {
file_size: number;
created_at: string | null;
modified_at: string | null;
taken_at: string | null;
mime_type: string;
media_kind: MediaKind;
duration_ms: number | null;
@@ -214,7 +215,9 @@ export type SortOrder =
| "rating_desc"
| "rating_asc"
| "duration_desc"
| "duration_asc";
| "duration_asc"
| "taken_desc"
| "taken_asc";
interface GalleryState {
folders: Folder[];
@@ -494,6 +497,10 @@ function compareImages(a: ImageRecord, b: ImageRecord, sort: SortOrder): number
return compareNullableNumber(a.duration_ms, b.duration_ms);
case "duration_desc":
return compareNullableNumber(b.duration_ms, a.duration_ms);
case "taken_asc":
return compareNullableDate(a.taken_at ?? a.created_at, b.taken_at ?? b.created_at);
case "taken_desc":
return compareNullableDate(b.taken_at ?? b.created_at, a.taken_at ?? a.created_at);
default:
return compareNullableDate(b.modified_at, a.modified_at);
}