feat: recently added albums, timeline scrubber, soft delete & trash
- Smart albums: "Last 7 days" and "Last 30 days" filter by date_imported - Timeline scrubber: fixed right-edge year bar with click-to-jump navigation - Soft delete: trash photos from lightbox; restore or permanently delete from Trash view - EXIF columns: iso, aperture, shutter_speed, focal_length, lens_model - Bulk actions: star/unstar and add-to-album with multi-select - Download original button in lightbox toolbar - EXIF detail expansion in detail panel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
51c752602a
commit
d35836131a
10 files changed
+367
-32
No files matched your search
+41
-7
@@ -1,19 +1,21 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { fetchPhotos, fetchLibraryStats, fetchVideoStats, bulkStar, bulkAddToCollection } from './api/photos.js';
|
||||
import { fetchPhotos, fetchLibraryStats, fetchVideoStats, bulkStar, bulkAddToCollection, deletePhoto } from './api/photos.js';
|
||||
import Timeline from './components/Timeline.jsx';
|
||||
import Lightbox from './components/Lightbox.jsx';
|
||||
import FilterSidebar from './components/FilterSidebar.jsx';
|
||||
import IngestBadge from './components/IngestBadge.jsx';
|
||||
import DuplicatesView from './components/DuplicatesView.jsx';
|
||||
import SettingsPanel from './components/SettingsPanel.jsx';
|
||||
import TimelineScrubber from './components/TimelineScrubber.jsx';
|
||||
|
||||
import { lazy, Suspense } from 'react';
|
||||
const MapView = lazy(() => import('./components/MapView.jsx'));
|
||||
const MemoriesView = lazy(() => import('./components/MemoriesView.jsx'));
|
||||
const VideosView = lazy(() => import('./components/VideosView.jsx'));
|
||||
const AlbumsView = lazy(() => import('./components/AlbumsView.jsx'));
|
||||
const TrashView = lazy(() => import('./components/TrashView.jsx'));
|
||||
|
||||
const EMPTY_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' };
|
||||
const EMPTY_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '', importedFrom: '' };
|
||||
const EMPTY_VIDEO_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' };
|
||||
const SIDEBAR_W = 210;
|
||||
|
||||
@@ -121,17 +123,35 @@ export default function App() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function handleSelectAlbum({ type, collectionId, year, city, camera }) {
|
||||
const f = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' };
|
||||
function handleSelectAlbum({ type, collectionId, year, city, camera, days }) {
|
||||
const f = { ...EMPTY_FILTERS };
|
||||
if (type === 'collection') f.collectionId = String(collectionId);
|
||||
else if (type === 'year') { f.from = `${year}-01-01`; f.to = `${year}-12-31`; }
|
||||
else if (type === 'city') f.city = city;
|
||||
else if (type === 'year') { f.from = `${year}-01-01`; f.to = `${year}-12-31`; }
|
||||
else if (type === 'city') f.city = city;
|
||||
else if (type === 'camera') f.q = camera;
|
||||
else if (type === 'recent') {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - days);
|
||||
f.importedFrom = d.toISOString().slice(0, 10);
|
||||
}
|
||||
setFilters(f);
|
||||
setView('library');
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
async function handleDeletePhoto(photoId) {
|
||||
try {
|
||||
await deletePhoto(photoId);
|
||||
setPhotos(prev => prev.filter(p => p.id !== photoId));
|
||||
if (lbPhotos) {
|
||||
const next = lbPhotos.filter(p => p.id !== photoId);
|
||||
if (next.length === 0) { setLbPhotos(null); setLbIndex(null); }
|
||||
else { setLbPhotos(next); setLbIndex(i => Math.min(i, next.length - 1)); }
|
||||
}
|
||||
loadStats();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function openLightbox(photosArr, idx) {
|
||||
setLbPhotos(photosArr);
|
||||
setLbIndex(idx);
|
||||
@@ -154,7 +174,7 @@ export default function App() {
|
||||
loadStats();
|
||||
}
|
||||
|
||||
const hasFilter = filters.q || filters.from || filters.to || filters.tagId || filters.collectionId || filters.starred || filters.city;
|
||||
const hasFilter = filters.q || filters.from || filters.to || filters.tagId || filters.collectionId || filters.starred || filters.city || filters.importedFrom;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -194,6 +214,7 @@ export default function App() {
|
||||
</span>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab active={view === 'trash'} onClick={() => { setView('trash'); clearSelection(); }}>Trash</Tab>
|
||||
</nav>
|
||||
|
||||
{/* Right side: ingest badge + gear */}
|
||||
@@ -324,9 +345,21 @@ export default function App() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Trash */}
|
||||
{view === 'trash' && (
|
||||
<Suspense fallback={<p style={{ padding: 24, color: '#777', fontSize: 13 }}>Loading…</p>}>
|
||||
<TrashView />
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline scrubber — right-edge year nav for library */}
|
||||
{view === 'library' && (
|
||||
<TimelineScrubber years={stats?.years ?? []} />
|
||||
)}
|
||||
|
||||
{/* Bulk action bar */}
|
||||
{selected.size > 0 && view === 'library' && (
|
||||
<div style={{
|
||||
@@ -373,6 +406,7 @@ export default function App() {
|
||||
onClose={() => { setLbPhotos(null); setLbIndex(null); }}
|
||||
onChange={setLbIndex}
|
||||
onStarChange={handleStarChange}
|
||||
onDelete={handleDeletePhoto}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in new issue
Block a user