feat(library): reindex button, live grid, archive-reappear fix, bigger carets
- Add one-click "reindex new files" button to the Library sidebar header (RefreshCw, calls startIndex rescan:false), spins + disables while active. - Refresh the photos grid from the indexer WS stream (throttled during the scan + once on completion) so newly indexed files appear live. - Fix archived photos flashing back into the grid when archiving others: drop the per-action settle-driven clearRemoved and reconcile removedIds against the actual cache instead (clears an id only once it's gone from the deduped pages). Covers archive, delete, and bulk-bar removals. - Replace the tiny Unicode caret triangles with a 16px Lucide ChevronRight that rotates 90deg on expand, across folder tree rows, root, Tags, Review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@
|
||||
import { filters } from '$lib/stores/filters.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { untrack } from 'svelte';
|
||||
import { FolderInput, FolderPlus, Pencil, Trash2 } from 'lucide-svelte';
|
||||
import { ChevronRight, FolderInput, FolderPlus, Pencil, Trash2 } from 'lucide-svelte';
|
||||
import Self from './FolderTree.svelte';
|
||||
import KebabMenu, { Item, Separator } from './KebabMenu.svelte';
|
||||
|
||||
@@ -164,19 +164,21 @@
|
||||
>
|
||||
{#if hasChildren}
|
||||
<button
|
||||
class="flex h-[18px] w-4 items-center justify-center text-[10px]"
|
||||
class="flex h-[18px] w-5 items-center justify-center rounded hover:text-foreground"
|
||||
class:text-muted-foreground={!active}
|
||||
onclick={() => toggle(node.path)}
|
||||
title={open ? 'Collapse' : 'Expand'}
|
||||
aria-label={open ? 'Collapse' : 'Expand'}
|
||||
>
|
||||
{open ? '▾' : '▸'}
|
||||
<ChevronRight
|
||||
class="h-4 w-4 transition-transform duration-150 {open ? 'rotate-90' : ''}"
|
||||
/>
|
||||
</button>
|
||||
{:else}
|
||||
<!-- Spacer keeps childless siblings aligned with their chevroned
|
||||
peers at every depth, so labels share a common left edge
|
||||
across the sidebar (folders, heaps, views, manage). -->
|
||||
<span class="inline-block h-[18px] w-4" aria-hidden="true"></span>
|
||||
<span class="inline-block h-[18px] w-5" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<!--
|
||||
Count badge lives INSIDE the button so the entire row (label
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
renameFolder,
|
||||
renameHeap,
|
||||
scanCrossFolderDuplicates,
|
||||
startIndex,
|
||||
triggerDownload,
|
||||
type CrossFolderScanResult,
|
||||
type PpAlbum,
|
||||
@@ -44,12 +45,14 @@
|
||||
} from '$lib/stores/filters.svelte';
|
||||
import { isAuthenticated, session, userBasePath } from '$lib/stores/session.svelte';
|
||||
import { openMove } from '$lib/stores/moveDialog.svelte';
|
||||
import { indexer } from '$lib/stores/indexer.svelte';
|
||||
import FolderTree, { buildTree } from './FolderTree.svelte';
|
||||
import GeneralSettingsDialog from './GeneralSettingsDialog.svelte';
|
||||
import KebabMenu, { Item, Separator } from './KebabMenu.svelte';
|
||||
import SettingsDialog from './SettingsDialog.svelte';
|
||||
import UsersDialog from './UsersDialog.svelte';
|
||||
import {
|
||||
ChevronRight,
|
||||
Copy,
|
||||
Download,
|
||||
FolderInput,
|
||||
@@ -59,6 +62,7 @@
|
||||
LogOut,
|
||||
Moon,
|
||||
Pencil,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
Sun,
|
||||
Trash2,
|
||||
@@ -322,6 +326,22 @@
|
||||
toast.error(err instanceof Error ? err.message : 'Delete failed')
|
||||
}));
|
||||
|
||||
// One-click "reindex new files": kicks off a scan of the whole library
|
||||
// with rescan off, so PhotoPrism only picks up files it hasn't indexed
|
||||
// yet. Progress streams in via the WebSocket indexer pill, and the grid
|
||||
// auto-refreshes as new tiles land (see indexer store). Guarded against
|
||||
// double-trigger while a scan is already running.
|
||||
async function onReindex() {
|
||||
if (indexer.active) return;
|
||||
const tid = toast.loading('Starting reindex…');
|
||||
try {
|
||||
await startIndex({ path: '/', rescan: false, cleanup: false });
|
||||
toast.success('Reindex started — new files will appear as they’re found', { id: tid });
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Reindex failed', { id: tid });
|
||||
}
|
||||
}
|
||||
|
||||
function onCreateFolder(parent: string | null = null) {
|
||||
const name = prompt(parent ? `New subfolder under "${parent}"` : 'New folder name')?.trim();
|
||||
if (!name) return;
|
||||
@@ -484,6 +504,17 @@
|
||||
<span class="flex-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Library
|
||||
</span>
|
||||
<button
|
||||
class="rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground group-hover/header:opacity-100"
|
||||
class:opacity-0={!indexer.active}
|
||||
class:opacity-100={indexer.active}
|
||||
onclick={onReindex}
|
||||
disabled={indexer.active}
|
||||
title="Reindex new files"
|
||||
aria-label="Reindex new files"
|
||||
>
|
||||
<RefreshCw class="h-3 w-3 {indexer.active ? 'animate-spin' : ''}" />
|
||||
</button>
|
||||
<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)}
|
||||
@@ -518,18 +549,20 @@
|
||||
{#if hasSubfolders}
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-[18px] w-4 items-center justify-center text-[10px]"
|
||||
class="flex h-[18px] w-5 items-center justify-center rounded hover:text-foreground"
|
||||
class:text-muted-foreground={!rootActive}
|
||||
onclick={toggleRoot}
|
||||
title={rootExpanded ? 'Collapse' : 'Expand'}
|
||||
aria-label={rootExpanded ? 'Collapse root' : 'Expand root'}
|
||||
>
|
||||
{rootExpanded ? '▾' : '▸'}
|
||||
<ChevronRight
|
||||
class="h-4 w-4 transition-transform duration-150 {rootExpanded ? 'rotate-90' : ''}"
|
||||
/>
|
||||
</button>
|
||||
{:else}
|
||||
<!-- Spacer keeps chevronless rows aligned with their chevroned
|
||||
peers, so labels share a common left edge across the sidebar. -->
|
||||
<span class="inline-block h-[18px] w-4" aria-hidden="true"></span>
|
||||
<span class="inline-block h-[18px] w-5" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
@@ -698,9 +731,11 @@
|
||||
aria-expanded={tagsExpanded}
|
||||
>
|
||||
<span
|
||||
class="flex h-[18px] w-4 items-center justify-center text-[10px] text-muted-foreground"
|
||||
class="flex h-[18px] w-5 items-center justify-center text-muted-foreground"
|
||||
>
|
||||
{tagsExpanded ? '▾' : '▸'}
|
||||
<ChevronRight
|
||||
class="h-4 w-4 transition-transform duration-150 {tagsExpanded ? 'rotate-90' : ''}"
|
||||
/>
|
||||
</span>
|
||||
<span class="flex min-w-0 flex-1 items-center pl-1">
|
||||
<span class="truncate">Tags</span>
|
||||
@@ -766,9 +801,11 @@
|
||||
aria-expanded={reviewExpanded}
|
||||
>
|
||||
<span
|
||||
class="flex h-[18px] w-4 items-center justify-center text-[10px] text-muted-foreground"
|
||||
class="flex h-[18px] w-5 items-center justify-center text-muted-foreground"
|
||||
>
|
||||
{reviewExpanded ? '▾' : '▸'}
|
||||
<ChevronRight
|
||||
class="h-4 w-4 transition-transform duration-150 {reviewExpanded ? 'rotate-90' : ''}"
|
||||
/>
|
||||
</span>
|
||||
<span class="flex min-w-0 flex-1 items-center pl-1">
|
||||
<span class="truncate">Review</span>
|
||||
|
||||
@@ -33,8 +33,7 @@
|
||||
doneBulk,
|
||||
removedBulk,
|
||||
failBulk,
|
||||
markRemoved,
|
||||
clearRemoved
|
||||
markRemoved
|
||||
} from '$lib/stores/bulkAction.svelte';
|
||||
import { EmptyState, InlineLoader } from '$lib/components/feedback';
|
||||
import { Layers } from 'lucide-svelte';
|
||||
@@ -156,15 +155,14 @@
|
||||
throw e;
|
||||
} finally {
|
||||
busy = false;
|
||||
const settled = Promise.all([
|
||||
qc.invalidateQueries({ queryKey: ['photos'] }),
|
||||
qc.invalidateQueries({ queryKey: ['marks'] }),
|
||||
qc.invalidateQueries({ queryKey: ['review-groups'] })
|
||||
]);
|
||||
// Clear the optimistic-removal overlay only once the refetch has
|
||||
// landed, so tiles never flash back in before the fresh (archived-
|
||||
// filtered) page replaces the old one.
|
||||
if (bulk) void settled.then(() => clearRemoved(bulk.ids));
|
||||
void qc.invalidateQueries({ queryKey: ['photos'] });
|
||||
void qc.invalidateQueries({ queryKey: ['marks'] });
|
||||
void qc.invalidateQueries({ queryKey: ['review-groups'] });
|
||||
// The optimistic-removal overlay (removedIds) is reconciled against
|
||||
// the cache in +page.svelte — each id drops once the fresh, archived-
|
||||
// filtered page has actually replaced it. Clearing here off this
|
||||
// action's own settle raced other in-flight removals and flashed
|
||||
// tiles back in.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user