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:
+17
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user