feat: photo/video capture-date pipeline + premium UI/UX overhaul

Backend — Photos timeline now groups by real capture date instead of upload time. New MediaMetadataService (FileLifecycleHook) extracts EXIF DateTimeOriginal from images and container creation_time from videos (mov/mp4/mkv) via nom-exif, timezone-correct (OffsetTimeOriginal), persisting captured_at so the existing media_sort_date trigger takes over. Adds POST /admin/photos/metadata/reextract to backfill existing media. Falls back to upload date when no embedded date exists.

Frontend — premium grid cards: combined metadata line (relative date · size, owner avatar when shared), custom selection checkbox with a clear checked state, uniform full-width 4:3 thumbnail tiles independent of filename length, centered file-type icons, and a hit-test fix so checkbox/star/kebab clicks reach the controls (the decorative thumbnail no longer captures pointer events). Notification messages internationalised across all 16 locales. Broader polish: design tokens, a11y/focus-visible states, brand + PWA assets.

Chore — bump semver-compatible dependencies (cargo upgrade); add nom-exif 3.6.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DioCrafts
2026-06-15 00:17:17 +02:00
parent dac299fea6
commit 81a93a489b
129 changed files with 8194 additions and 2473 deletions
+17 -2
View File
@@ -1,6 +1,6 @@
// OxiCloud Service Worker
// FIXME: generate cache name according build ?
const CACHE_NAME = 'oxicloud-cache-v26';
const CACHE_NAME = 'oxicloud-cache-v27';
// Only cache static assets — NOT HTML files.
// HTML files are served network-first so browsers always get the latest
@@ -50,6 +50,11 @@ self.addEventListener('activate', (event) => {
})
);
})
.then(() => {
// Navigation preload: let the browser fetch the navigation in
// parallel with the service worker booting (faster first paint).
return self.registration.navigationPreload?.enable();
})
.then(() => self.clients.claim()) // Take control of clients
);
});
@@ -65,7 +70,17 @@ self.addEventListener('fetch', (event) => {
const pathname = new URL(event.request.url).pathname;
const isHtml = HTML_PATHS.includes(pathname) || pathname.endsWith('.html') || event.request.headers.get('accept')?.includes('text/html');
if (isHtml) {
event.respondWith(fetch(event.request).catch(() => caches.match(event.request)));
event.respondWith(
(async () => {
try {
// Use the navigation-preload response when present.
const preload = await event.preloadResponse;
return preload || (await fetch(event.request));
} catch {
return (await caches.match(event.request)) || Response.error();
}
})()
);
return;
}