feat: PhotoPrism M0 bring-up — compose stack, web client, sidecar, migrate
Replace the legacy mule-image backend with PhotoPrism plus a thin SvelteKit client and a Node sidecar for endpoints PhotoPrism doesn't expose (file rename), and add a two-phase migrator (metadata via PUT, heaps → albums) for the existing Postgres library. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
84
web/src/routes/+layout.svelte
Normal file
84
web/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { QueryClientProvider } from '@tanstack/svelte-query';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
import { Toaster } from 'svelte-sonner';
|
||||
import { isAuthenticated } from '$lib/stores/session.svelte';
|
||||
import { setLeftSidebarWidth, view } from '$lib/stores/view.svelte';
|
||||
import { resizable } from '$lib/actions/resizable';
|
||||
import { queryClient } from '$lib/queryClient';
|
||||
import PreviewOverlay from '$lib/components/preview/PreviewOverlay.svelte';
|
||||
import LeftSidebar from '$lib/components/layout/LeftSidebar.svelte';
|
||||
import AnimatedMule from '$lib/components/mule/AnimatedMule.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
// Auth guard. Anything outside /login requires a session; otherwise
|
||||
// punt to the login page (which itself redirects authenticated users
|
||||
// back to /).
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
const onLogin = page.url.pathname === '/login';
|
||||
if (!isAuthenticated() && !onLogin) {
|
||||
void goto('/login', { replaceState: true });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Mulimage</title>
|
||||
</svelte:head>
|
||||
|
||||
<ModeWatcher />
|
||||
<Toaster richColors position="bottom-right" />
|
||||
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{#if isAuthenticated() && page.url.pathname !== '/login'}
|
||||
<!-- App shell locks to viewport height; only the main thumbnail
|
||||
region inside each page scrolls. The mule header, toolbar, and
|
||||
sidebars stay fixed regardless of how far you scroll the grid. -->
|
||||
<div class="flex h-screen flex-col overflow-hidden">
|
||||
<AnimatedMule />
|
||||
<div class="flex min-h-0 flex-1">
|
||||
{#if !view.leftSidebarCollapsed}
|
||||
<aside
|
||||
class="relative hidden h-full shrink-0 border-r border-border bg-card/30 md:block"
|
||||
style="width: {view.leftSidebarWidth}px;"
|
||||
>
|
||||
<div class="h-full overflow-y-auto p-3">
|
||||
<LeftSidebar />
|
||||
</div>
|
||||
<div
|
||||
class="group absolute -right-1.5 top-0 z-20 hidden h-full w-3 cursor-col-resize md:block"
|
||||
use:resizable={{
|
||||
edge: 'right',
|
||||
getWidth: () => view.leftSidebarWidth,
|
||||
setWidth: setLeftSidebarWidth
|
||||
}}
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label="Resize left panel"
|
||||
>
|
||||
<div
|
||||
class="ml-1 h-full w-0.5 bg-transparent transition-colors group-hover:bg-primary/40"
|
||||
></div>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
<!-- Right column hosts the route's content. Pages render as
|
||||
a flex column whose first child is the Toolbar (shrink-0)
|
||||
and whose remaining content takes the rest, so each route
|
||||
can decide which of its panes is the scrollable one. -->
|
||||
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
<PreviewOverlay />
|
||||
</QueryClientProvider>
|
||||
Reference in New Issue
Block a user