221 lines
6.1 KiB
Svelte
221 lines
6.1 KiB
Svelte
<script lang="ts">
|
||
import "../app.css";
|
||
import Sidebar from "$lib/components/Sidebar.svelte";
|
||
import BottomTabBar from "$lib/components/BottomTabBar.svelte";
|
||
import AiLockBanner from "$lib/components/AiLockBanner.svelte";
|
||
import CommandBar from "$lib/components/CommandBar.svelte";
|
||
import { commandBarOpen } from "$lib/stores/commandBar";
|
||
import { sidebarCollapsed } from "$lib/stores/sidebarCollapsed";
|
||
import { seedIfEmpty } from "$lib/db/seed";
|
||
import { sync } from "$lib/sync/syncService";
|
||
import { refreshLockStatus } from "$lib/stores/aiLock";
|
||
import { onMount, onDestroy } from "svelte";
|
||
import { afterNavigate } from "$app/navigation";
|
||
import type { Snippet } from "svelte";
|
||
import {
|
||
handleRedirect,
|
||
login,
|
||
isAuthenticated,
|
||
} from "$lib/auth/authStore.js";
|
||
import { scopeColor } from "$lib/stores/scopeContext";
|
||
|
||
let { children }: { children: Snippet } = $props();
|
||
let sidebarOpen = $state(false);
|
||
let ready = $state(false);
|
||
let authReady = $state(false);
|
||
let authenticated = $state(false);
|
||
let syncInterval: ReturnType<typeof setInterval> | null = null;
|
||
let lockInterval: ReturnType<typeof setInterval> | null = null;
|
||
|
||
isAuthenticated.subscribe((v) => (authenticated = v));
|
||
|
||
function startSync() {
|
||
sync();
|
||
syncInterval = setInterval(sync, 30_000);
|
||
refreshLockStatus();
|
||
lockInterval = setInterval(refreshLockStatus, 30_000);
|
||
document.addEventListener("visibilitychange", onVisibilityChange);
|
||
}
|
||
|
||
function onVisibilityChange() {
|
||
if (document.visibilityState === "visible") {
|
||
sync();
|
||
refreshLockStatus();
|
||
}
|
||
}
|
||
|
||
onMount(async () => {
|
||
await handleRedirect();
|
||
authReady = true;
|
||
if (authenticated) {
|
||
await seedIfEmpty();
|
||
ready = true;
|
||
startSync();
|
||
}
|
||
});
|
||
|
||
onDestroy(() => {
|
||
if (syncInterval) clearInterval(syncInterval);
|
||
if (lockInterval) clearInterval(lockInterval);
|
||
document.removeEventListener("visibilitychange", onVisibilityChange);
|
||
});
|
||
|
||
$effect(() => {
|
||
if (authReady && authenticated && !ready) {
|
||
seedIfEmpty().then(() => {
|
||
ready = true;
|
||
startSync();
|
||
});
|
||
}
|
||
});
|
||
|
||
function toggleSidebar() {
|
||
sidebarOpen = !sidebarOpen;
|
||
}
|
||
|
||
function closeSidebar() {
|
||
sidebarOpen = false;
|
||
}
|
||
|
||
function handleGlobalKeydown(e: KeyboardEvent) {
|
||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
||
e.preventDefault();
|
||
$commandBarOpen = !$commandBarOpen;
|
||
}
|
||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
|
||
e.preventDefault();
|
||
sidebarCollapsed.toggle();
|
||
}
|
||
}
|
||
|
||
afterNavigate(({ to }) => {
|
||
closeSidebar();
|
||
if (to?.url.pathname.startsWith("/context/")) {
|
||
const id = to.url.pathname.split("/").pop();
|
||
if (id && id !== "daily-log") {
|
||
// Exclude daily-log from recents
|
||
try {
|
||
const stored = sessionStorage.getItem("recentContexts");
|
||
let recent = stored ? JSON.parse(stored) : [];
|
||
recent = [
|
||
id,
|
||
...recent.filter((x: string) => x !== id),
|
||
].slice(0, 5);
|
||
sessionStorage.setItem(
|
||
"recentContexts",
|
||
JSON.stringify(recent),
|
||
);
|
||
} catch (e) {
|
||
console.error("Failed to save recent context", e);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
$effect(() => {
|
||
document.documentElement.style.setProperty(
|
||
"--scope-color",
|
||
$scopeColor,
|
||
);
|
||
});
|
||
</script>
|
||
|
||
<svelte:window onkeydown={handleGlobalKeydown} />
|
||
|
||
{#if !authReady}
|
||
<div class="flex h-screen items-center justify-center bg-bg">
|
||
<span class="text-muted">Loading...</span>
|
||
</div>
|
||
{:else if !authenticated}
|
||
<div class="flex h-screen items-center justify-center bg-bg">
|
||
<div class="text-center">
|
||
<h1 class="mb-4 text-2xl font-bold text-accent">KaNote</h1>
|
||
<p class="mb-6 text-muted">Sign in to access your notes</p>
|
||
<button
|
||
class="rounded bg-accent px-6 py-3 font-semibold text-white transition-colors hover:bg-accent/80"
|
||
onclick={() => login()}
|
||
>
|
||
Sign in with Microsoft
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{:else if ready}
|
||
<div class="flex h-screen overflow-hidden">
|
||
<!-- Sidebar: fullscreen on mobile, static on desktop -->
|
||
{#if sidebarOpen}
|
||
<!-- Backdrop -->
|
||
<div
|
||
class="fixed inset-0 z-[59] bg-black/50 backdrop-blur-sm md:hidden"
|
||
onclick={closeSidebar}
|
||
></div>
|
||
<!-- Mobile: full-screen overlay -->
|
||
<div
|
||
class="fixed inset-0 z-[60] flex flex-col bg-sidebar md:hidden"
|
||
>
|
||
<!-- Header with close button -->
|
||
<div
|
||
class="flex items-center justify-between border-b border-border px-5 py-4 shrink-0"
|
||
style="padding-top: max(1rem, env(safe-area-inset-top));"
|
||
>
|
||
<span
|
||
class="text-lg font-bold text-accent uppercase tracking-wider"
|
||
>KaNote</span
|
||
>
|
||
<button
|
||
class="text-2xl text-muted hover:text-white leading-none"
|
||
onclick={closeSidebar}
|
||
aria-label="Close">×</button
|
||
>
|
||
</div>
|
||
<!-- Scrollable content -->
|
||
<div
|
||
class="flex-1 overflow-y-auto px-5 pb-8"
|
||
style="-webkit-overflow-scrolling: touch;"
|
||
>
|
||
<Sidebar onnavigate={closeSidebar} hideLogo />
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Desktop sidebar -->
|
||
<aside
|
||
class="hidden md:flex md:flex-col md:border-r md:border-border md:bg-sidebar md:overflow-y-auto transition-all duration-200
|
||
{$sidebarCollapsed ? 'md:w-0 md:p-0 md:overflow-hidden md:border-r-0' : 'md:w-[250px] md:p-5'}"
|
||
>
|
||
{#if !$sidebarCollapsed}
|
||
<Sidebar onnavigate={closeSidebar} />
|
||
{/if}
|
||
</aside>
|
||
|
||
<!-- Main content -->
|
||
<main class="flex-1 overflow-y-auto p-5 pt-safe md:pt-5 pb-20 md:pb-5">
|
||
<div class="mx-auto max-w-[900px]">
|
||
<!-- Desktop sidebar toggle button -->
|
||
<div class="hidden md:flex mb-3 -mt-1">
|
||
<button
|
||
onclick={() => sidebarCollapsed.toggle()}
|
||
class="text-muted hover:text-white transition-colors text-xs flex items-center gap-1 opacity-40 hover:opacity-100"
|
||
aria-label="Sidebar ein-/ausblenden"
|
||
title="Sidebar ein-/ausblenden (Ctrl+B)"
|
||
>
|
||
{$sidebarCollapsed ? '›' : '‹'}
|
||
</button>
|
||
</div>
|
||
<AiLockBanner />
|
||
{@render children()}
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Bottom tab bar (mobile only, hidden when sidebar open) -->
|
||
{#if !sidebarOpen}
|
||
<BottomTabBar onsidebaropen={toggleSidebar} />
|
||
{/if}
|
||
</div>
|
||
{:else}
|
||
<div class="flex h-screen items-center justify-center bg-bg">
|
||
<span class="text-muted">Loading...</span>
|
||
</div>
|
||
{/if}
|
||
|
||
<CommandBar />
|