feat: Albums view, auto albums, EXIF details, download, bulk select

Albums tab with manual album CRUD (create/delete, cover photo grid)
and Smart Albums auto-generated by year, city, and camera model using
SQLite window functions — no extra storage, computed from existing data.

Lightbox gets a download-original button (streams raw file with
Content-Disposition: attachment).

DetailPanel Camera section expands to show ISO, aperture, shutter speed,
focal length, and lens model extracted at ingest time; five new DB columns
added via safe ALTER TABLE migration.

Bulk selection: hover a photo to reveal a circle checkbox; selecting
any photo activates a floating action bar for star/unstar and add-to-album
across the whole selection. PhotoCard wrapped in React.memo to limit
re-renders to only affected cards.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-18 15:02:52 -04:00
1 parent f8b32dd440
commit 51c752602a
11 files changed
+598 -31

No files matched your search

+9 -2
View File
@@ -115,6 +115,11 @@ export function openDb(dbPath) {
try { db.exec('ALTER TABLE photos ADD COLUMN place_country TEXT'); } catch {}
try { db.exec("ALTER TABLE photos ADD COLUMN media_type TEXT NOT NULL DEFAULT 'photo'"); } catch {}
try { db.exec('ALTER TABLE photos ADD COLUMN duration REAL'); } catch {}
try { db.exec('ALTER TABLE photos ADD COLUMN iso INTEGER'); } catch {}
try { db.exec('ALTER TABLE photos ADD COLUMN aperture REAL'); } catch {}
try { db.exec('ALTER TABLE photos ADD COLUMN shutter_speed TEXT'); } catch {}
try { db.exec('ALTER TABLE photos ADD COLUMN focal_length REAL'); } catch {}
try { db.exec('ALTER TABLE photos ADD COLUMN lens_model TEXT'); } catch {}
db.exec('CREATE INDEX IF NOT EXISTS idx_photos_starred ON photos(starred)');
db.exec('CREATE INDEX IF NOT EXISTS idx_photos_place_city ON photos(place_city)');
db.exec('CREATE INDEX IF NOT EXISTS idx_photos_media_type ON photos(media_type)');
@@ -128,11 +133,13 @@ export function openDb(dbPath) {
INSERT OR IGNORE INTO photos
(filepath, thumb_path, preview_path, date_taken, date_imported,
camera_model, gps_lat, gps_lon, phash, width, height, filesize,
place_city, place_country, media_type, duration)
place_city, place_country, media_type, duration,
iso, aperture, shutter_speed, focal_length, lens_model)
VALUES
(@filepath, @thumb_path, @preview_path, @date_taken, @date_imported,
@camera_model, @gps_lat, @gps_lon, @phash, @width, @height, @filesize,
@place_city, @place_country, @media_type, @duration)
@place_city, @place_country, @media_type, @duration,
@iso, @aperture, @shutter_speed, @focal_length, @lens_model)
`),
exists: db.prepare('SELECT 1 FROM photos WHERE filepath = ? LIMIT 1'),
remove: db.prepare('DELETE FROM photos WHERE filepath = ?'),