2026-06-17 17:06:30 -06:00
|
|
|
<script lang="ts">
|
|
|
|
|
import { goto } from '$app/navigation';
|
2026-06-21 12:08:29 +02:00
|
|
|
import { page, updated } from '$app/state';
|
2026-06-17 17:06:30 -06:00
|
|
|
import { onMount } from 'svelte';
|
|
|
|
|
import '$lib/styles/app.css';
|
|
|
|
|
import AppShell from '$lib/components/AppShell.svelte';
|
2026-06-17 22:07:18 -06:00
|
|
|
import DialogHost from '$lib/components/DialogHost.svelte';
|
2026-06-17 17:06:30 -06:00
|
|
|
import Toaster from '$lib/components/Toaster.svelte';
|
|
|
|
|
import { session } from '$lib/stores/session.svelte';
|
2026-06-21 12:08:29 +02:00
|
|
|
import { ui } from '$lib/stores/ui.svelte';
|
2026-06-17 22:07:18 -06:00
|
|
|
import { hashUrlToPath } from '$lib/utils/hashRedirect';
|
2026-06-17 17:06:30 -06:00
|
|
|
|
|
|
|
|
let { children } = $props();
|
|
|
|
|
|
2026-06-21 12:08:29 +02:00
|
|
|
// A new build was deployed (`_app/version.json` changed, polled per
|
|
|
|
|
// svelte.config.js) — reload so an open tab never keeps running stale code
|
|
|
|
|
// after a rebuild. Deferred while a progress notification (e.g. an upload) is
|
|
|
|
|
// in flight so we don't interrupt it; this effect re-runs when that clears
|
|
|
|
|
// (notifications are reactive) and reloads then.
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (!updated.current) return;
|
|
|
|
|
const busy = ui.notifications.some((n) => n.progress !== undefined);
|
|
|
|
|
if (!busy) location.reload();
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-17 17:06:30 -06:00
|
|
|
// Routes reachable without an authenticated session.
|
|
|
|
|
const PUBLIC_PREFIXES = ['/login', '/device', '/s/', '/nextcloud'];
|
|
|
|
|
|
|
|
|
|
function isPublic(pathname: string): boolean {
|
|
|
|
|
return PUBLIC_PREFIXES.some((p) => pathname === p || pathname.startsWith(p));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ready = $state(false);
|
|
|
|
|
|
|
|
|
|
onMount(async () => {
|
2026-06-19 16:36:54 +00:00
|
|
|
// The instant HTML boot splash has done its job — the app is mounted, so
|
|
|
|
|
// the route (login renders immediately; protected routes show their own
|
|
|
|
|
// loading state) is already in the DOM behind it.
|
|
|
|
|
document.getElementById('app-splash')?.remove();
|
|
|
|
|
|
2026-06-17 22:07:18 -06:00
|
|
|
// Redirect old `#/...` bookmarks to the new path before anything else.
|
2026-06-17 17:06:30 -06:00
|
|
|
if (typeof location !== 'undefined' && location.hash.startsWith('#/')) {
|
2026-06-17 22:07:18 -06:00
|
|
|
const mapped = hashUrlToPath(location.hash);
|
2026-06-17 17:06:30 -06:00
|
|
|
if (mapped) await goto(mapped, { replaceState: true });
|
|
|
|
|
}
|
|
|
|
|
await session.load();
|
|
|
|
|
ready = true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Guard: once the session is known, bounce unauthenticated users off
|
|
|
|
|
// protected routes. Runs client-side only (ssr=false).
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (!ready) return;
|
|
|
|
|
const path = page.url.pathname;
|
|
|
|
|
if (!session.isAuthenticated && !isPublic(path)) {
|
|
|
|
|
void goto(`/login?redirect=${encodeURIComponent(path)}`, { replaceState: true });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
{#if isPublic(page.url.pathname)}
|
|
|
|
|
{@render children()}
|
|
|
|
|
{:else if ready && session.isAuthenticated}
|
|
|
|
|
<AppShell {children} />
|
|
|
|
|
{:else if ready}
|
|
|
|
|
{@render children()}
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="app-loading" aria-busy="true">Loading…</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<Toaster />
|
2026-06-17 22:07:18 -06:00
|
|
|
<DialogHost />
|
2026-06-17 17:06:30 -06:00
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.app-loading {
|
|
|
|
|
display: grid;
|
|
|
|
|
place-items: center;
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
}
|
|
|
|
|
</style>
|