Compare commits
2 Commits
505fef5dfc
...
aa63d4c11d
| Author | SHA1 | Date | |
|---|---|---|---|
| aa63d4c11d | |||
| 79ec511482 |
@@ -225,6 +225,13 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) {
|
||||
// archived/restored — relevant when the cull targets came from a
|
||||
// multi-selection rather than the single focused tile.
|
||||
focusAfter(ids);
|
||||
// Drop the now-stale selection set. The archived UIDs are about
|
||||
// to leave the timeline on refetch, but the SvelteSet membership
|
||||
// keeps the selection ring on them until then — confusing for
|
||||
// the user and a footgun if they Ctrl-click to add more and end
|
||||
// up re-archiving the same photos. The BulkActionBar button path
|
||||
// clears for the same reason; mirror it here.
|
||||
clearSelection();
|
||||
invalidatePhotos(ids);
|
||||
const label = target ? `Archived ${ids.length}` : `Restored ${ids.length}`;
|
||||
toast.success(label);
|
||||
@@ -259,6 +266,8 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) {
|
||||
toast.error(err instanceof Error ? err.message : 'Delete failed');
|
||||
return;
|
||||
}
|
||||
focusAfter(ids);
|
||||
clearSelection();
|
||||
invalidatePhotos(ids);
|
||||
toast.success(`Deleted ${ids.length}`);
|
||||
}
|
||||
@@ -277,6 +286,12 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) {
|
||||
return;
|
||||
}
|
||||
const { updated, errors } = await batchEdit(ids, (id) => approvePhoto(id));
|
||||
// Approve moves photos out of the review pile, so the same
|
||||
// stale-selection trap as archive/delete applies — advance focus
|
||||
// past the approved set and drop the now-irrelevant selection
|
||||
// before invalidate refetches the (smaller) view.
|
||||
focusAfter(ids);
|
||||
clearSelection();
|
||||
invalidatePhotos(ids);
|
||||
if (errors.length) {
|
||||
toast.error(`Kept ${updated.length}; ${errors.length} failed`, {
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
import { isAuthenticated } from '$lib/stores/session.svelte';
|
||||
import { push as pushUndo } from '$lib/stores/undo.svelte';
|
||||
import { thumbUrl } from '$lib/stores/session.svelte';
|
||||
import { getMetadataSectionOpen, setMetadataSection } from '$lib/stores/view.svelte';
|
||||
import { primaryFile, type PpPhoto } from '$lib/types/photoprism';
|
||||
|
||||
interface Props {
|
||||
@@ -522,8 +523,16 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- GPS detail (collapsed by default) -->
|
||||
<details class="rounded border border-border" open={Boolean(photo.Lat || photo.Lng)}>
|
||||
<!-- GPS detail. Static default (closed); user's expand/collapse
|
||||
choice persists across photo switches via the view store.
|
||||
Avoid data-driven defaults here — they make the `open` attr
|
||||
change between photos, which fires a programmatic `toggle`
|
||||
event and would silently overwrite the user's preference. -->
|
||||
<details
|
||||
class="rounded border border-border"
|
||||
open={getMetadataSectionOpen('gps', false)}
|
||||
ontoggle={(e) => setMetadataSection('gps', e.currentTarget.open)}
|
||||
>
|
||||
<summary
|
||||
class="cursor-pointer px-2 py-1 text-[10px] uppercase tracking-wide text-muted-foreground"
|
||||
>
|
||||
@@ -564,10 +573,11 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- IPTC credits (collapsed unless something set) -->
|
||||
<!-- IPTC credits. Static default (closed); user's choice persists. -->
|
||||
<details
|
||||
class="rounded border border-border"
|
||||
open={Boolean(subject || artist || copyright || license || notes)}
|
||||
open={getMetadataSectionOpen('credits', false)}
|
||||
ontoggle={(e) => setMetadataSection('credits', e.currentTarget.open)}
|
||||
>
|
||||
<summary
|
||||
class="cursor-pointer px-2 py-1 text-[10px] uppercase tracking-wide text-muted-foreground"
|
||||
@@ -623,8 +633,12 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- File (collapsed by default) -->
|
||||
<details class="rounded border border-border">
|
||||
<!-- File metadata. Closed by default; persists once opened. -->
|
||||
<details
|
||||
class="rounded border border-border"
|
||||
open={getMetadataSectionOpen('file', false)}
|
||||
ontoggle={(e) => setMetadataSection('file', e.currentTarget.open)}
|
||||
>
|
||||
<summary
|
||||
class="cursor-pointer px-2 py-1 text-[10px] uppercase tracking-wide text-muted-foreground"
|
||||
>
|
||||
|
||||
@@ -24,6 +24,13 @@ interface Persisted {
|
||||
thumbnailSize?: ThumbnailSize;
|
||||
leftSidebarWidth?: number;
|
||||
rightSidebarWidth?: number;
|
||||
/**
|
||||
* Per-section expanded state for the right-sidebar metadata panel
|
||||
* (GPS, Credits, File). Keyed by section id; missing entries use a
|
||||
* static default supplied by the component, so the user's chosen
|
||||
* collapse state stays put as they navigate between photos.
|
||||
*/
|
||||
metadataSections?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export const MIN_LEFT_WIDTH = 180;
|
||||
@@ -62,6 +69,7 @@ export const view = $state<{
|
||||
thumbnailSize: ThumbnailSize;
|
||||
leftSidebarWidth: number;
|
||||
rightSidebarWidth: number;
|
||||
metadataSections: Record<string, boolean>;
|
||||
}>({
|
||||
rightSidebarCollapsed: initial.rightSidebarCollapsed ?? false,
|
||||
leftSidebarCollapsed: initial.leftSidebarCollapsed ?? false,
|
||||
@@ -77,7 +85,11 @@ export const view = $state<{
|
||||
typeof initial.rightSidebarWidth === 'number' ? initial.rightSidebarWidth : DEFAULT_RIGHT_WIDTH,
|
||||
MIN_RIGHT_WIDTH,
|
||||
MAX_RIGHT_WIDTH
|
||||
)
|
||||
),
|
||||
metadataSections:
|
||||
initial.metadataSections && typeof initial.metadataSections === 'object'
|
||||
? { ...initial.metadataSections }
|
||||
: {}
|
||||
});
|
||||
|
||||
function persist(): void {
|
||||
@@ -87,7 +99,8 @@ function persist(): void {
|
||||
leftSidebarCollapsed: view.leftSidebarCollapsed,
|
||||
thumbnailSize: view.thumbnailSize,
|
||||
leftSidebarWidth: view.leftSidebarWidth,
|
||||
rightSidebarWidth: view.rightSidebarWidth
|
||||
rightSidebarWidth: view.rightSidebarWidth,
|
||||
metadataSections: view.metadataSections
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
|
||||
}
|
||||
@@ -121,3 +134,24 @@ export function toggleLeftSidebar(): void {
|
||||
view.leftSidebarCollapsed = !view.leftSidebarCollapsed;
|
||||
persist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the persisted expanded state for a right-sidebar metadata
|
||||
* section, falling back to `defaultOpen` when the user has never
|
||||
* toggled it. `defaultOpen` should be a *static* value — using a
|
||||
* per-photo data-driven default would change between photos, fire a
|
||||
* programmatic `toggle` event, and silently overwrite the user's
|
||||
* preference.
|
||||
*/
|
||||
export function getMetadataSectionOpen(id: string, defaultOpen: boolean): boolean {
|
||||
const v = view.metadataSections[id];
|
||||
return typeof v === 'boolean' ? v : defaultOpen;
|
||||
}
|
||||
|
||||
/** Record the user's explicit collapse/expand choice for a metadata
|
||||
* section. Persists immediately so a reload keeps the layout the
|
||||
* user picked. */
|
||||
export function setMetadataSection(id: string, open: boolean): void {
|
||||
view.metadataSections[id] = open;
|
||||
persist();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user