Files
EventSnap/frontend/src/lib/data-mode-store.ts
fabi e79e020566 feat(eventsnap): UX review batch-3 — feed virtualization, a11y/UX fixes, shared primitives
Frontend
- Feed: DOM-windowing via @tanstack/svelte-virtual (new VirtualFeed). The window
  virtualizer keeps the sticky header, pull-to-refresh, infinite-scroll sentinel and
  bottom nav working. List = dynamic measured heights keyed by upload id +
  anchorTo:start so an SSE prepend doesn't jump a scrolled reader; grid = measured
  square rows. Drops the content-visibility band-aid and the old FeedGrid.
  measureElement(null) on row unmount prevents ResizeObserver retention; stable
  option callbacks + guarded setOptions avoid O(n) re-measure on like/comment patches.
- Global: viewport-fit=cover (activates safe-area insets), lang=de, PWA manifest +
  maskable icon + apple-touch metas, prefers-reduced-motion, safe-area-top headers.
- Feed reactivity: like/comment SSE patch the single card in place; upload-processed
  debounced in-place merge; IntersectionObserver leak fixed; shared 60s clock.
- CLS: list cards reserve the skeleton aspect box.
- Destructive actions (promote/demote/unban, gallery release) routed through
  ConfirmSheet (host + admin).
- Export OOM: streamed download via single-use ticket instead of res.blob().
- Shared primitives: IconButton (44px hit area), scrollLock action; UploadSheet a11y.
- Polish: api timeout + non-JSON guard, focus-trap offsetParent fix, pull-to-refresh
  passive:false + guards, diashow keyboard a11y, Toaster assertive errors, dark-mode
  gaps, aria-pressed on like/chip state, CameraCapture mic-on-video + retry,
  ConfirmSheet spinner, upload beforeunload warning, emoji->SVG icons.

Backend
- social.rs: like/comment SSE broadcasts now carry the fresh count so feed clients
  patch one card in place instead of refetching page 1.
- admin.rs / main.rs: supporting changes for the above.

Verified: svelte-check 0 errors, frontend build clean, /feed SSR 200. Runtime feed
scroll-feel validation still owed (documented in FOLLOWUPS.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 19:16:48 +02:00

57 lines
1.9 KiB
TypeScript

// Per-device "Datenmodus" — Saver loads compressed previews (default), Original loads
// the full file via the auth-gated `/api/v1/upload/{id}/original` endpoint.
//
// Stored per-device in localStorage (not per-user) because data plans are a property
// of the device the guest is currently holding, not their identity.
//
// Used by:
// - Feed cards (FeedListCard / VirtualFeed grid tiles) to pick which URL to render
// - Lightbox
// - Diashow
// See [docs/FEATURES.md §2.5] for the user-facing model.
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
export type DataMode = 'saver' | 'original';
const KEY = 'eventsnap_data_mode';
const DEFAULT: DataMode = 'saver';
function readInitial(): DataMode {
if (!browser) return DEFAULT;
const raw = localStorage.getItem(KEY);
return raw === 'original' || raw === 'saver' ? raw : DEFAULT;
}
export const dataMode = writable<DataMode>(readInitial());
if (browser) {
dataMode.subscribe((value) => {
try {
localStorage.setItem(KEY, value);
} catch {
// localStorage may be unavailable (Safari private mode); ignore.
}
});
}
/**
* Build the URL for a feed upload given the current data mode and the URL variants
* the backend returned. Centralised so every consumer (cards, lightbox, diashow)
* follows the same fallback rule:
* Original mode → original API route. Falls back to preview if no upload id is
* available (defensive — shouldn't happen in practice).
* Saver mode → preview URL (compressed), falling back to thumbnail and then
* original.
*/
export function pickMediaUrl(
mode: DataMode,
upload: { id: string; preview_url: string | null; thumbnail_url: string | null }
): string {
if (mode === 'original') {
return `/api/v1/upload/${upload.id}/original`;
}
return upload.preview_url ?? upload.thumbnail_url ?? `/api/v1/upload/${upload.id}/original`;
}