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:
1 parent
f8b32dd440
commit
51c752602a
11 files changed
+598
-31
No files matched your search
+19
-3
@@ -28,6 +28,11 @@ export async function ingestFile(filepath, db, posterDir, settings = {}) {
|
||||
return ingestPhoto(filepath, db, posterDir, settings);
|
||||
}
|
||||
|
||||
function fmtShutter(t) {
|
||||
if (t >= 1) return `${t}s`;
|
||||
return `1/${Math.round(1 / t)}s`;
|
||||
}
|
||||
|
||||
// ── Photo ingest ───────────────────────────────────────────────────────────────
|
||||
|
||||
async function ingestPhoto(filepath, db, posterDir, settings) {
|
||||
@@ -43,12 +48,18 @@ async function ingestPhoto(filepath, db, posterDir, settings) {
|
||||
let exif = {};
|
||||
try {
|
||||
exif = await exifr.parse(filepath, {
|
||||
pick: ['DateTimeOriginal', 'Make', 'Model', 'Orientation'],
|
||||
pick: ['DateTimeOriginal', 'Make', 'Model', 'Orientation',
|
||||
'ISO', 'FNumber', 'ExposureTime', 'FocalLength', 'LensModel'],
|
||||
}) ?? {};
|
||||
} catch {}
|
||||
|
||||
const dateTaken = exif.DateTimeOriginal ?? null;
|
||||
const cameraModel = [exif.Make, exif.Model].filter(Boolean).join(' ') || null;
|
||||
const dateTaken = exif.DateTimeOriginal ?? null;
|
||||
const cameraModel = [exif.Make, exif.Model].filter(Boolean).join(' ') || null;
|
||||
const iso = exif.ISO ?? null;
|
||||
const aperture = exif.FNumber != null ? Math.round(exif.FNumber * 10) / 10 : null;
|
||||
const focalLength = exif.FocalLength != null ? Math.round(exif.FocalLength) : null;
|
||||
const lensModel = exif.LensModel ?? null;
|
||||
const shutterSpeed = exif.ExposureTime != null ? fmtShutter(exif.ExposureTime) : null;
|
||||
|
||||
let gpsLat = null, gpsLon = null;
|
||||
try {
|
||||
@@ -109,6 +120,11 @@ async function ingestPhoto(filepath, db, posterDir, settings) {
|
||||
place_country: placeCountry,
|
||||
media_type: 'photo',
|
||||
duration: null,
|
||||
iso,
|
||||
aperture,
|
||||
shutter_speed: shutterSpeed,
|
||||
focal_length: focalLength,
|
||||
lens_model: lensModel,
|
||||
});
|
||||
|
||||
console.log(`[ingest] ${path.basename(filepath)}`);
|
||||
|
||||
Reference in new issue
Block a user