diff --git a/client/src/App.jsx b/client/src/App.jsx
index 6aeda4b..e2c6f32 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef, useCallback } from 'react';
-import { fetchPhotos, fetchLibraryStats } from './api/photos.js';
+import { fetchPhotos, fetchLibraryStats, fetchVideoStats } from './api/photos.js';
import Timeline from './components/Timeline.jsx';
import Lightbox from './components/Lightbox.jsx';
import FilterSidebar from './components/FilterSidebar.jsx';
@@ -12,7 +12,8 @@ const MapView = lazy(() => import('./components/MapView.jsx'));
const MemoriesView = lazy(() => import('./components/MemoriesView.jsx'));
const VideosView = lazy(() => import('./components/VideosView.jsx'));
-const EMPTY_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' };
+const EMPTY_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' };
+const EMPTY_VIDEO_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' };
const SIDEBAR_W = 210;
export default function App() {
@@ -24,8 +25,10 @@ export default function App() {
const [error, setError] = useState(null);
const [dupeCount, setDupeCount] = useState(0);
- const [filters, setFilters] = useState(EMPTY_FILTERS);
- const [stats, setStats] = useState(null);
+ const [filters, setFilters] = useState(EMPTY_FILTERS);
+ const [stats, setStats] = useState(null);
+ const [videoFilters, setVideoFilters] = useState(EMPTY_VIDEO_FILTERS);
+ const [videoStats, setVideoStats] = useState(null);
const [lbPhotos, setLbPhotos] = useState(null);
const [lbIndex, setLbIndex] = useState(null);
@@ -40,6 +43,12 @@ export default function App() {
useEffect(() => { loadStats(); }, []);
+ useEffect(() => {
+ if (view === 'videos') {
+ fetchVideoStats().then(setVideoStats).catch(() => {});
+ }
+ }, [view]);
+
// Reset + reload when filters change (debounce text search)
useEffect(() => {
const delay = filters.q ? 300 : 0;
@@ -167,20 +176,31 @@ export default function App() {
{/* ── Body (sidebar + content) ── */}
- {/* Sidebar — only in library view */}
- {view === 'library' && (
+ {/* Sidebar — library and videos views */}
+ {(view === 'library' || view === 'videos') && (
)}
@@ -221,7 +241,7 @@ export default function App() {
{/* Videos */}
{view === 'videos' && (
Loading…}>
-
+
)}
diff --git a/client/src/api/photos.js b/client/src/api/photos.js
index c8a4cb8..0cbbf13 100644
--- a/client/src/api/photos.js
+++ b/client/src/api/photos.js
@@ -110,8 +110,17 @@ export function triggerSync(source) {
}
// Videos
-export function fetchVideos() {
- return api('/api/videos');
+export function fetchVideoStats() {
+ return api('/api/videos/stats');
+}
+
+export function fetchVideos({ from, to, city } = {}) {
+ const p = new URLSearchParams();
+ if (from) p.set('from', from);
+ if (to) p.set('to', to);
+ if (city) p.set('city', city);
+ const qs = p.toString();
+ return api(`/api/videos${qs ? '?' + qs : ''}`);
}
// Memories
diff --git a/client/src/components/FilterSidebar.jsx b/client/src/components/FilterSidebar.jsx
index a75beaa..fa31422 100644
--- a/client/src/components/FilterSidebar.jsx
+++ b/client/src/components/FilterSidebar.jsx
@@ -2,7 +2,7 @@ import { useState } from 'react';
const MAX_DEFAULT = 5;
-export default function FilterSidebar({ filters, onChange, stats, total }) {
+export default function FilterSidebar({ filters, onChange, stats, total, allLabel = 'All Photos' }) {
const [expanded, setExpanded] = useState({});
function toggle(section) {
@@ -55,7 +55,7 @@ export default function FilterSidebar({ filters, onChange, stats, total }) {
active={isAll && !filters.q}
onClick={selectAll}
icon="⊞"
- label="All Photos"
+ label={allLabel}
count={stats?.total ?? total}
/>
diff --git a/client/src/components/VideosView.jsx b/client/src/components/VideosView.jsx
index 4a736de..feda2a5 100644
--- a/client/src/components/VideosView.jsx
+++ b/client/src/components/VideosView.jsx
@@ -44,16 +44,18 @@ function fmtSize(b) {
// ── Component ─────────────────────────────────────────────────────────────────
-export default function VideosView() {
+export default function VideosView({ filters = {} }) {
const [videos, setVideos] = useState(null);
const [playing, setPlaying] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
- fetchVideos()
+ setVideos(null);
+ setError(null);
+ fetchVideos({ from: filters.from, to: filters.to, city: filters.city })
.then(d => setVideos(d.videos))
.catch(e => setError(e.message));
- }, []);
+ }, [filters.from, filters.to, filters.city]);
useEffect(() => {
if (!playing) return;
diff --git a/server/index.js b/server/index.js
index 90946c3..11d02e5 100644
--- a/server/index.js
+++ b/server/index.js
@@ -515,12 +515,28 @@ app.post('/api/admin/clear-dismissals', (_req, res) => {
// ── Videos ────────────────────────────────────────────────────────────────────
-app.get('/api/videos', (_req, res) => {
- const rows = db.raw.prepare(`
- SELECT ${SELECT_COLS} FROM photos p
- WHERE p.media_type = 'video'
- ORDER BY p.date_taken DESC, p.id DESC
- `).all();
+app.get('/api/videos/stats', (_req, res) => {
+ const total = db.raw.prepare("SELECT COUNT(*) AS n FROM photos WHERE media_type = 'video'").get().n;
+ const years = db.raw.prepare(
+ "SELECT strftime('%Y', date_taken) AS year, COUNT(*) AS count FROM photos WHERE media_type = 'video' AND date_taken IS NOT NULL GROUP BY year ORDER BY year DESC"
+ ).all();
+ const cities = db.raw.prepare(
+ "SELECT place_city AS city, place_country AS country, COUNT(*) AS count FROM photos WHERE media_type = 'video' AND place_city IS NOT NULL GROUP BY place_city ORDER BY count DESC LIMIT 100"
+ ).all();
+ res.json({ total, years, cities, starred: 0, tags: [], collections: [] });
+});
+
+app.get('/api/videos', (req, res) => {
+ const { from, to, city } = req.query;
+ const conditions = ["p.media_type = 'video'"];
+ const params = [];
+ if (from) { conditions.push('p.date_taken >= ?'); params.push(from); }
+ if (to) { conditions.push('p.date_taken <= ?'); params.push(to); }
+ if (city) { conditions.push('p.place_city = ?'); params.push(city); }
+ const where = 'WHERE ' + conditions.join(' AND ');
+ const rows = db.raw.prepare(
+ `SELECT ${SELECT_COLS} FROM photos p ${where} ORDER BY p.date_taken DESC, p.id DESC`
+ ).all(...params);
res.json({ videos: rows.map(mapPhoto), total: rows.length });
});