Wires up everything from the previous commits into actual UI surfaces, and applies Tailwind dark: variants throughout. All pages now support the 'system' / 'light' / 'dark' preference set in the onboarding step or in Mein Konto → Design. Layout & nav: - routes/+layout.svelte: initTheme(), global pin-reset SSE handler that filters by user_id and calls clearPin(), one-shot /me/context fetch on boot to hydrate privacyNote + quota. - components/BottomNav.svelte: dark variants on the frosted-glass bar. - components/UploadSheet.svelte: dark variants on backdrop, sheet, source buttons. - components/OnboardingGuide.svelte: new "Helles oder dunkles Design?" step (3-option custom-radio grid), reactive currentStep with proper type narrowing, dark variants throughout. Privacy-note nudge appears on the PIN step only when one is configured. Feed: - routes/feed/+page.svelte: diashow entry icon (tablet/desktop only), long-press → ContextSheet (Löschen for own posts, Original anzeigen for all), upload-deleted + feed-delta SSE handlers, dark variants on header, search, autocomplete, filter chips, empty states. - components/FeedListCard.svelte: long-press wireup, double-tap-to-like, data-mode-aware mediaSrc via pickMediaUrl, kebab fallback for desktop, isOwn prop, dark variants. - components/FeedGrid.svelte: long-press wireup, dark variants. - components/LightboxModal.svelte: data-mode-aware src, double-tap heart burst, dark variants on card / comments / input. - components/HashtagChips.svelte: dark variants. Account: - routes/account/+page.svelte: theme picker (3-button radio grid), data mode picker (with confirm sheet for Original), live quota widget, preformatted Datenschutzhinweis block, diashow tile (mobile only), pin now sourced from the $currentPin store so a global pin-reset clears it live, clearQueue() on explicit logout, dark variants across every card + both bottom sheets. Upload: - routes/upload/+page.svelte: per-user quota progress bar above the submit button, dark variants. Host & Admin: - routes/host/+page.svelte: PIN-reset confirm + one-time PIN modal, hosts may demote other hosts, canResetPinFor() helper, dark variants on all cards, modals, stats, toast. - routes/admin/+page.svelte: Config form rebuilt as CONFIG_GROUPS with per-field kind (number / bool / text), renders toggles for the rate-limit + quota switches and a textarea for the privacy_note; Nutzer tab gains PIN reset + hosts-may-demote-hosts wiring; same one-time PIN modal; dark variants everywhere. - routes/admin/login/+page.svelte: dark variants. Join / Recover / Export: - routes/join/+page.svelte: rename inline link to "Ich habe bereits einen Account", dark variants. - routes/recover/+page.svelte: dark variants. - routes/export/+page.svelte: dark variants on status cards + HTML guide modal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
3.8 KiB
Svelte
92 lines
3.8 KiB
Svelte
<script lang="ts">
|
|
import type { FeedUpload } from '$lib/types';
|
|
import { dataMode } from '$lib/data-mode-store';
|
|
import { longpress } from '$lib/actions/longpress';
|
|
|
|
interface Props {
|
|
uploads: FeedUpload[];
|
|
onlike: (id: string) => void;
|
|
oncomment: (id: string) => void;
|
|
onselect: (upload: FeedUpload) => void;
|
|
oncontextmenu?: (upload: FeedUpload) => void;
|
|
threeCol?: boolean;
|
|
}
|
|
|
|
let { uploads, onlike, oncomment, onselect, oncontextmenu, threeCol = false }: Props =
|
|
$props();
|
|
|
|
function isVideo(mime: string): boolean {
|
|
return mime.startsWith('video/');
|
|
}
|
|
|
|
// Grid uses small thumbnails by design even in Original mode — full media is one tap
|
|
// away in the lightbox, where the data-mode picker decides for real.
|
|
function tileUrl(upload: FeedUpload): string {
|
|
if (upload.thumbnail_url) return upload.thumbnail_url;
|
|
if (upload.preview_url) return upload.preview_url;
|
|
return $dataMode === 'original' ? `/api/v1/upload/${upload.id}/original` : '';
|
|
}
|
|
</script>
|
|
|
|
<div class="grid gap-0.5 {threeCol ? 'grid-cols-3' : 'grid-cols-2 sm:grid-cols-3'}">
|
|
{#each uploads as upload (upload.id)}
|
|
<div
|
|
class="group relative aspect-square cursor-pointer overflow-hidden rounded-lg bg-gray-100 dark:bg-gray-800"
|
|
use:longpress={{ duration: 500 }}
|
|
onlongpress={() => oncontextmenu?.(upload)}
|
|
>
|
|
<button
|
|
onclick={() => onselect(upload)}
|
|
class="block h-full w-full"
|
|
aria-label="Upload anzeigen"
|
|
>
|
|
{#if isVideo(upload.mime_type)}
|
|
<div class="flex h-full items-center justify-center bg-gray-800">
|
|
{#if tileUrl(upload)}
|
|
<img src={tileUrl(upload)} alt="" class="h-full w-full object-cover" />
|
|
{/if}
|
|
<div class="absolute inset-0 flex items-center justify-center">
|
|
<svg class="h-10 w-10 text-white/80" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M8 5v14l11-7z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
{:else if tileUrl(upload)}
|
|
<img src={tileUrl(upload)} alt="" class="h-full w-full object-cover" loading="lazy" />
|
|
{:else}
|
|
<div class="flex h-full items-center justify-center text-gray-400">
|
|
<svg class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
</div>
|
|
{/if}
|
|
</button>
|
|
|
|
<!-- Overlay with name and stats -->
|
|
<div class="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-2">
|
|
<p class="truncate text-xs font-medium text-white">{upload.uploader_name}</p>
|
|
<div class="mt-0.5 flex items-center gap-3 text-xs text-white/80">
|
|
<button
|
|
class="pointer-events-auto flex items-center gap-0.5"
|
|
onclick={(e) => { e.stopPropagation(); onlike(upload.id); }}
|
|
>
|
|
<svg class="h-3.5 w-3.5 {upload.liked_by_me ? 'fill-red-400 text-red-400' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
|
</svg>
|
|
{upload.like_count}
|
|
</button>
|
|
<button
|
|
class="pointer-events-auto flex items-center gap-0.5"
|
|
onclick={(e) => { e.stopPropagation(); oncomment(upload.id); }}
|
|
>
|
|
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
|
</svg>
|
|
{upload.comment_count}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|