feat(web): sidebar root folder + folder counts, drill-in colors/tags/ratings, click semantics rework
Sidebar
- New `/` root-folder entry at the top of the Folders group. Active
when the timeline is scoped to root; the photo grid post-filters to
`Path === ''` because PhotoPrism's `path:` operator can't express an
exact-root match. Collapsible chevron, persisted to its own
localStorage key, and a kebab carrying just "New subfolder".
- Per-folder count badges. `/api/v1/photos?q=path:X&count=1000` per
folder in parallel via `listFolderCounts`; root count derived from
`config.count.all − Σ subfolder counts`.
- Folder tree starts at depth=1 under the root so nested rows indent
visually relative to `/`.
- Footer matches the Toolbar / action-bar h-9 height.
Timeline interaction
- Single click on a tile selects only that tile (clears others); the
preview now lives on dblclick. Modifier clicks still go through
`gridKeyNav`'s document handler (shift = range, cmd/ctrl = toggle).
- `x` (archive) now actually archives — PhotoPrism's photo PUT
silently drops the Archived field, so we route through
/batch/photos/{archive,restore} the same way the BulkActionBar
already did. Mirror for `u`.
- Preview close restores the timeline focus + scrolls the last-shown
photo into view via `forcedExpand`+`scrollTileIntoView` so it
actually mounts (selection ring would otherwise stay invisible when
the user navigated far in preview).
- `applyFolderScope` only narrows the timeline to root when the active
view is a folder view (no heap / search / non-default section), so
label clicks / heap views / favorites no longer drop subfolder
photos.
Action bar
- Inline `h-9` row at the bottom of the main column (not `fixed`),
matching the Toolbar's visual language. Right sidebar stays full
height — the bar only spans the timeline width.
- Approve action wired for the review pile.
Colors / Tags / Ratings drill-ins
- New shared `PhotoGrid` component owning tile rendering, selection
styling, single-click-selects + dblclick-previews, and `setOrder`
for arrow-key nav.
- Each route's drill-in `<main>` carries `use:gridKeyNav` and a
trailing `<BulkActionBar />` so shift/cmd/ctrl click, arrow keys,
and the keyboard shortcuts work the same as the timeline.
- Tags switches from `goto('/?q=label:…')` to an in-place drill-in
with a back button, mirroring `/colors`'s flow.
- Category cards + drill-in photo cards honour the global
`view.thumbnailSize` (XS–XL) so the timeline's size selector now
reaches into all four grids.
Settings
- General-settings dialog merges Appearance into UI and switches free
text inputs to selects for the PhotoPrism theme / language / start
page / map style (the value-from-server prepends if it's outside
the curated list so we never silently rewrite a custom value). Time
zone uses `<datalist>` with `Intl.supportedValuesOf('timeZone')`.
Sidecar
- Heap convert runs reindex synchronously per source path so the
client's invalidate-and-refetch sees the moved files.
Inbox
- New /inbox route stub for the upcoming import workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,9 @@
|
||||
export type Section =
|
||||
| 'all-photos'
|
||||
| 'favorites'
|
||||
| 'review'
|
||||
| 'archive'
|
||||
| 'hidden'
|
||||
| 'heap';
|
||||
|
||||
export interface FilterState {
|
||||
@@ -24,10 +26,14 @@ export interface FilterState {
|
||||
search: string;
|
||||
}
|
||||
|
||||
// Default landing = root folder (`/`). The Folders group sits at the top
|
||||
// of the sidebar; landing inside it gives users a stable starting point
|
||||
// instead of dumping them into the full library. Picking any other view
|
||||
// (favorites, a heap, "All photos") clears `folderPath` to `null`.
|
||||
export const filters = $state<FilterState>({
|
||||
section: 'all-photos',
|
||||
heapUid: null,
|
||||
folderPath: null,
|
||||
folderPath: '/',
|
||||
search: ''
|
||||
});
|
||||
|
||||
@@ -65,9 +71,21 @@ export function filtersToQ(f: FilterState = filters): string {
|
||||
case 'favorites':
|
||||
parts.push('favorite:true');
|
||||
break;
|
||||
case 'review':
|
||||
// PhotoPrism's review pile: photos the indexer flagged as
|
||||
// uncertain (low quality score). Cleared per-photo via the
|
||||
// `/approve` endpoint or by archiving.
|
||||
parts.push('review:true');
|
||||
break;
|
||||
case 'archive':
|
||||
parts.push('archived:true');
|
||||
break;
|
||||
case 'hidden':
|
||||
// Auto-hidden by the indexer (broken files, very low quality
|
||||
// score). Excluded from every other view — this section is the
|
||||
// only way to see them without a manual `q=hidden:true`.
|
||||
parts.push('hidden:true');
|
||||
break;
|
||||
case 'heap':
|
||||
if (f.heapUid) parts.push(`album:${f.heapUid}`);
|
||||
break;
|
||||
@@ -75,7 +93,13 @@ export function filtersToQ(f: FilterState = filters): string {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (f.folderPath) parts.push(`path:${quoteIfNeeded(f.folderPath)}`);
|
||||
// `/` is the root-folder sentinel. PhotoPrism's `path:` operator can't
|
||||
// express "exact root match" (path:"" / path:/ both fall back to "no
|
||||
// filter"), so we leave the server query unfiltered and let the
|
||||
// timeline post-filter to `Path === ''` client-side.
|
||||
if (f.folderPath && f.folderPath !== '/') {
|
||||
parts.push(`path:${quoteIfNeeded(f.folderPath)}`);
|
||||
}
|
||||
if (f.search) parts.push(quoteIfNeeded(f.search));
|
||||
return parts.join(' ');
|
||||
}
|
||||
@@ -84,13 +108,24 @@ export function filtersToQ(f: FilterState = filters): string {
|
||||
export function parseUrlParams(params: URLSearchParams): Partial<FilterState> {
|
||||
const sectionRaw = params.get('section') as Section | null;
|
||||
const section: Section =
|
||||
sectionRaw && ['all-photos', 'favorites', 'archive', 'heap'].includes(sectionRaw)
|
||||
sectionRaw && ['all-photos', 'favorites', 'review', 'archive', 'hidden', 'heap'].includes(sectionRaw)
|
||||
? sectionRaw
|
||||
: 'all-photos';
|
||||
// Bare URL (no section/folder/heap/q params) lands on the root folder
|
||||
// — same default the store carries. Any explicit param means the user
|
||||
// asked for a specific view, so the folder filter clears unless
|
||||
// `folder=` is supplied on top.
|
||||
const bare =
|
||||
!params.has('section') &&
|
||||
!params.has('folder') &&
|
||||
!params.has('heap') &&
|
||||
!params.has('q');
|
||||
const folderRaw = params.get('folder');
|
||||
const folderPath = folderRaw !== null ? folderRaw : bare ? '/' : null;
|
||||
return {
|
||||
section,
|
||||
heapUid: params.get('heap'),
|
||||
folderPath: params.get('folder'),
|
||||
folderPath,
|
||||
search: params.get('q') ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user