diff --git a/client/src/App.jsx b/client/src/App.jsx
index 41bc190..d4e087f 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -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() {
)}
+ { setView('trash'); clearSelection(); }}>Trash
{/* Right side: ingest badge + gear */}
@@ -324,9 +345,21 @@ export default function App() {
}
/>
)}
+
+ {/* Trash */}
+ {view === 'trash' && (
+ Loadingβ¦
}>
+
+
+ )}
+ {/* Timeline scrubber β right-edge year nav for library */}
+ {view === 'library' && (
+
+ )}
+
{/* Bulk action bar */}
{selected.size > 0 && view === 'library' && (
{ setLbPhotos(null); setLbIndex(null); }}
onChange={setLbIndex}
onStarChange={handleStarChange}
+ onDelete={handleDeletePhoto}
/>
)}
>
diff --git a/client/src/api/photos.js b/client/src/api/photos.js
index 03fa471..aeba49d 100644
--- a/client/src/api/photos.js
+++ b/client/src/api/photos.js
@@ -10,7 +10,7 @@ const json = (body) => ({
});
// Library
-export function fetchPhotos({ cursor, limit = 100, q, from, to, tagId, collectionId, starred, city } = {}) {
+export function fetchPhotos({ cursor, limit = 100, q, from, to, tagId, collectionId, starred, city, importedFrom } = {}) {
const p = new URLSearchParams({ limit });
if (cursor) p.set('cursor', cursor);
if (q) p.set('q', q);
@@ -20,6 +20,7 @@ export function fetchPhotos({ cursor, limit = 100, q, from, to, tagId, collectio
if (collectionId) p.set('collectionId', collectionId);
if (starred) p.set('starred', starred);
if (city) p.set('city', city);
+ if (importedFrom) p.set('importedFrom', importedFrom);
return api(`/api/photos?${p}`);
}
@@ -143,6 +144,23 @@ export function deleteCollection(id) {
return api(`/api/collections/${id}`, { method: 'DELETE' });
}
+// Trash / delete
+export function deletePhoto(id) {
+ return api(`/api/photos/${id}`, { method: 'DELETE' });
+}
+
+export function restorePhoto(id) {
+ return api(`/api/photos/${id}/restore`, { method: 'POST' });
+}
+
+export function permanentDeletePhoto(id) {
+ return api(`/api/photos/${id}/permanent`, { method: 'DELETE' });
+}
+
+export function fetchTrash() {
+ return api('/api/trash');
+}
+
// Bulk operations
export function bulkStar(ids, starred) {
return api('/api/photos/bulk', { method: 'PATCH', ...json({ ids, starred }) });
diff --git a/client/src/components/AlbumsView.jsx b/client/src/components/AlbumsView.jsx
index 3eb7ec7..b6c8e77 100644
--- a/client/src/components/AlbumsView.jsx
+++ b/client/src/components/AlbumsView.jsx
@@ -98,6 +98,23 @@ export default function AlbumsView({ onSelectAlbum }) {
+ {auto.recent?.length > 0 && (
+ <>
+ Recently Added
+
+ {auto.recent.map(r => (
+ onSelectAlbum({ type: 'recent', days: r.days, name: r.label })}
+ />
+ ))}
+
+ >
+ )}
+
{auto.years.length > 0 && (
<>
Years
diff --git a/client/src/components/Lightbox.jsx b/client/src/components/Lightbox.jsx
index e79e6bc..acf7271 100644
--- a/client/src/components/Lightbox.jsx
+++ b/client/src/components/Lightbox.jsx
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { toggleStar } from '../api/photos.js';
import DetailPanel from './DetailPanel.jsx';
-export default function Lightbox({ photos, index, onClose, onChange, onStarChange }) {
+export default function Lightbox({ photos, index, onClose, onChange, onStarChange, onDelete }) {
const [showInfo, setShowInfo] = useState(false);
const [starring, setStarring] = useState(false);
const photo = photos[index];
@@ -72,6 +72,17 @@ export default function Lightbox({ photos, index, onClose, onChange, onStarChang
>
β
+ {onDelete && (
+ {
+ if (confirm('Move this photo to Trash?')) onDelete(photo.id);
+ }}
+ title="Move to Trash"
+ style={{ color: '#f87171' }}
+ >
+ π
+
+ )}
setShowInfo(s => !s)}
active={showInfo}
@@ -123,7 +134,7 @@ export default function Lightbox({ photos, index, onClose, onChange, onStarChang
);
}
-function IconBtn({ onClick, active, title, children }) {
+function IconBtn({ onClick, active, title, style: extraStyle, children }) {
return (