202 lines
5.4 KiB
Svelte
202 lines
5.4 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 { 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;
|
|
}
|
|
}
|
|
|
|
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:w-[250px] md:flex-col md:border-r md:border-border md:bg-sidebar md:p-5 md:overflow-y-auto"
|
|
>
|
|
<Sidebar onnavigate={closeSidebar} />
|
|
</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]">
|
|
<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 />
|