feat(sidebar): library + general settings dialogs, sidebar footer, alignment polish

- Add a Settings cog to the Folders header that opens a tabbed library
  admin dialog (Library / Index / Import / Logs) wrapping PhotoPrism's
  /api/v1 settings, index, import and errors endpoints.
- Add a sticky footer to the left sidebar with the signed-in user's
  display name plus quick-toggle theme, general-settings cog (separate
  dialog for app prefs), and sign-out. Pull these out of the top
  Toolbar trailing slot.
- Align depth-0 folder rows with the rest of the sidebar entries (drop
  the leading chevron column when no children) and bring heap rows in
  line with folder rows so the kebab is part of the row's hover
  background instead of a detached chip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 16:51:56 +02:00
parent 17df1ecd09
commit 0766b47bb2
7 changed files with 729 additions and 43 deletions

View File

@@ -2,6 +2,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query';
import { mode, toggleMode } from 'mode-watcher';
import { toast } from 'svelte-sonner';
import {
createFolder,
@@ -12,6 +13,7 @@
heapDownloadUrl,
listFolders,
listHeaps,
logout,
renameFolder,
renameHeap,
triggerDownload,
@@ -24,11 +26,23 @@
setSection,
type Section
} from '$lib/stores/filters.svelte';
import { isAuthenticated } from '$lib/stores/session.svelte';
import { isAuthenticated, session } from '$lib/stores/session.svelte';
import FolderTree, { buildTree } from './FolderTree.svelte';
import GeneralSettingsDialog from './GeneralSettingsDialog.svelte';
import HeapConvertDialog from './HeapConvertDialog.svelte';
import KebabMenu, { Item, Separator } from './KebabMenu.svelte';
import { Copy, Download, FolderInput, Pencil, Trash2 } from 'lucide-svelte';
import SettingsDialog from './SettingsDialog.svelte';
import {
Copy,
Download,
FolderInput,
LogOut,
Moon,
Pencil,
Settings,
Sun,
Trash2
} from 'lucide-svelte';
const qc = useQueryClient();
@@ -88,6 +102,18 @@
// mounts <HeapConvertDialog>; the dialog clears it on close.
let convertingHeap = $state<PpAlbum | null>(null);
// Library/admin settings dialog visibility.
let settingsOpen = $state(false);
// App-wide preferences dialog (theme etc.). Distinct from the library
// admin dialog above — opened from the bottom-of-sidebar footer.
let generalSettingsOpen = $state(false);
async function onSignOut() {
await logout();
await goto('/login', { replaceState: true });
}
async function navigateTo(section: Section, heapUid: string | null = null) {
setSection(section, heapUid);
setFolderPath(null);
@@ -220,7 +246,8 @@
}
</script>
<nav class="space-y-3">
<div class="flex h-full flex-col">
<nav class="flex-1 space-y-3 overflow-y-auto p-3">
<!-- Views — section-driven entries + route-driven entries under a
single uppercase eyebrow. Compact rows, no icons. -->
<div>
@@ -279,12 +306,14 @@
<ul>
{#each heapsQuery.data ?? [] as heap (heap.UID)}
{@const active = isActive('heap', heap.UID)}
<li class="group flex items-center">
<li
class="group flex h-[24px] items-center rounded text-[12px] leading-tight hover:bg-accent"
class:bg-primary={active}
class:text-primary-foreground={active}
class:hover:bg-primary={active}
>
<button
class="flex h-[24px] flex-1 items-center gap-2 rounded px-2 text-left text-[12px] leading-tight hover:bg-accent"
class:bg-primary={active}
class:text-primary-foreground={active}
class:hover:bg-primary={active}
class="flex flex-1 items-center gap-2 px-2 text-left"
onclick={() => navigateTo('heap', heap.UID)}
ondblclick={() => onRenameHeap(heap)}
title={`${heap.Title} (${heap.PhotoCount ?? 0})`}
@@ -298,7 +327,7 @@
{heap.PhotoCount ?? 0}
</span>
</button>
<div class="pl-0.5">
<div class="mr-1">
<KebabMenu label="Heap actions">
<Item
class="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-[12px] outline-none hover:bg-accent focus:bg-accent"
@@ -345,10 +374,18 @@
</div>
<div>
<div class="group/header flex items-center px-3 pb-1">
<div class="group/header flex items-center gap-0.5 px-3 pb-1">
<span class="flex-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
Folders
</span>
<button
class="rounded p-0.5 text-muted-foreground opacity-0 hover:bg-accent hover:text-foreground group-hover/header:opacity-100"
onclick={() => (settingsOpen = true)}
title="Library settings"
aria-label="Library settings"
>
<Settings class="h-3 w-3" />
</button>
<button
class="rounded p-0.5 text-xs text-muted-foreground opacity-0 hover:bg-accent hover:text-foreground group-hover/header:opacity-100"
onclick={() => onCreateFolder(null)}
@@ -386,4 +423,57 @@
</div>
</nav>
<!--
Footer — fixed to the bottom of the sidebar. Holds the per-user
affordances (display name, quick theme toggle, general preferences,
sign-out) that used to live in the top toolbar.
-->
<footer
class="flex shrink-0 items-center gap-1 border-t border-border bg-card/50 px-3 py-2"
>
<span
class="min-w-0 flex-1 truncate text-[12px] text-foreground"
title={session.user?.DisplayName ?? session.user?.Name ?? ''}
>
{session.user?.DisplayName ?? session.user?.Name ?? 'Signed in'}
</span>
<button
type="button"
class="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
onclick={toggleMode}
title="Toggle theme"
aria-label="Toggle theme"
>
{#if mode.current === 'dark'}
<Sun class="h-3.5 w-3.5" />
{:else}
<Moon class="h-3.5 w-3.5" />
{/if}
</button>
<button
type="button"
class="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
onclick={() => (generalSettingsOpen = true)}
title="General settings"
aria-label="General settings"
>
<Settings class="h-3.5 w-3.5" />
</button>
<button
type="button"
class="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
onclick={onSignOut}
title="Sign out"
aria-label="Sign out"
>
<LogOut class="h-3.5 w-3.5" />
</button>
</footer>
</div>
<HeapConvertDialog heap={convertingHeap} onClose={() => (convertingHeap = null)} />
<SettingsDialog open={settingsOpen} onClose={() => (settingsOpen = false)} />
<GeneralSettingsDialog
open={generalSettingsOpen}
onClose={() => (generalSettingsOpen = false)}
/>