diff --git a/web/src/lib/components/layout/LeftSidebar.svelte b/web/src/lib/components/layout/LeftSidebar.svelte index 4d1c1f8..f0e3417 100644 --- a/web/src/lib/components/layout/LeftSidebar.svelte +++ b/web/src/lib/components/layout/LeftSidebar.svelte @@ -385,7 +385,7 @@ ]; const manageViews: ViewItem[] = [ - { kind: 'section', id: 'review', label: 'Review', getCount: () => configQuery.data?.count?.review }, + { kind: 'route', href: '/review', label: 'Review', getCount: () => configQuery.data?.count?.review }, { kind: 'route', href: '/duplicates', label: 'Duplicates', getCount: () => undefined }, { kind: 'section', id: 'hidden', label: 'Hidden', getCount: () => configQuery.data?.count?.hidden }, { kind: 'section', id: 'archive', label: 'Archive', getCount: () => configQuery.data?.count?.archived } diff --git a/web/src/lib/components/review/CauseBadges.svelte b/web/src/lib/components/review/CauseBadges.svelte new file mode 100644 index 0000000..6ba2763 --- /dev/null +++ b/web/src/lib/components/review/CauseBadges.svelte @@ -0,0 +1,26 @@ + + + +
+ {#each causes as c (c)} + + {CAUSES[c].chip} + + {/each} +
diff --git a/web/src/lib/components/review/CauseGroupCard.svelte b/web/src/lib/components/review/CauseGroupCard.svelte new file mode 100644 index 0000000..3b9e338 --- /dev/null +++ b/web/src/lib/components/review/CauseGroupCard.svelte @@ -0,0 +1,375 @@ + + + + + +
+
+
+
+ {group.meta.title} + ({group.photos.length}) +
+
+
+ + +
+
+ + +
+ {group.meta.suggestion} + {#if group.meta.suggestedAction !== 'manual'} + + {/if} +
+ +
+ {#each group.photos as photo, i (photo.UID)} + {@const causes = deriveCauses(photo)} + {@const isFocused = i === focusedIdx} + +
onSelect?.(photo)} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onSelect?.(photo); + } + }} + class:ring-2={isFocused} + class:ring-blue-500={isFocused} + class:ring-offset-2={isFocused} + class:ring-offset-background={isFocused} + class="group relative flex cursor-pointer flex-col overflow-hidden rounded-md border border-border bg-secondary text-left transition-shadow" + > +
+ {photo.FileName + {#if dims(photo)} + + {dims(photo)} + + {/if} + +
+ + +
+
+
+ +
+ {photo.FileName ?? photo.Name ?? ''} +
+
+
+ {/each} +
+
diff --git a/web/src/lib/components/review/ReviewView.svelte b/web/src/lib/components/review/ReviewView.svelte new file mode 100644 index 0000000..4ea6bf1 --- /dev/null +++ b/web/src/lib/components/review/ReviewView.svelte @@ -0,0 +1,48 @@ + + + +
+ {#if pending} +

Loading review queue…

+ {:else if error} +

+ Could not load review queue: {error instanceof Error ? error.message : 'unknown error'} +

+ {:else if groups.length === 0} +
+

The review queue is empty.

+

+ PhotoPrism's indexer flags photos with a low quality score for human review. New + arrivals with missing EXIF, low resolution, or unknown cameras will land here. +

+
+ {:else} +

+ {totalCount} photos in {groups.length} groups · arrow keys to focus, S to approve, A + to archive, Enter to inspect +

+
+ {#each groups as g, i (g.cause)} + + {/each} +
+ {/if} +
diff --git a/web/src/lib/components/sidebar/RelatedStrip.svelte b/web/src/lib/components/sidebar/RelatedStrip.svelte new file mode 100644 index 0000000..4b5b69f --- /dev/null +++ b/web/src/lib/components/sidebar/RelatedStrip.svelte @@ -0,0 +1,100 @@ + + + +{#if stripQuery.isPending} +
Loading {title.toLowerCase()}…
+{:else if stripQuery.isError} + + {null} +{:else if photos.length > 0} +
+ +
+ {#each photos as p (p.UID)} + + {/each} +
+
+{/if} diff --git a/web/src/lib/components/sidebar/RightSidebar.svelte b/web/src/lib/components/sidebar/RightSidebar.svelte index 6f20bce..8fb94b0 100644 --- a/web/src/lib/components/sidebar/RightSidebar.svelte +++ b/web/src/lib/components/sidebar/RightSidebar.svelte @@ -40,11 +40,15 @@ import { thumbUrl } from '$lib/stores/session.svelte'; import { getMetadataSectionOpen, setMetadataSection } from '$lib/stores/view.svelte'; import { primaryFile, type PpPhoto } from '$lib/types/photoprism'; + import RelatedStrip from './RelatedStrip.svelte'; interface Props { + /** When true, append related-photo strips (folder/camera/year) below + * Keywords. Used by the /review route; left off on the timeline. */ + showRelated?: boolean; photo: PpPhoto; } - let { photo }: Props = $props(); + let { photo, showRelated = false }: Props = $props(); const qc = useQueryClient(); @@ -496,6 +500,35 @@ + + {#if showRelated} +
+ + {#if photo.CameraID && photo.CameraID !== 1} + + {/if} + {#if photo.Year} + + {/if} +
+ {/if} + + + + + + Review + + {#snippet trailing()} +
+ {#each THUMBNAIL_SIZE_PRESETS as size, i (size)} + + {/each} +
+ {/snippet} +
+ +
+
+ +
+ + {#if !view.rightSidebarCollapsed && selection.focused} + + {/if} +