feat: group rated view by star rating
Mirrors the tags view: rated section now buckets photos 5★→1★ with an "Unrated" tail group, instead of a flat ratingMin=1 stream. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -233,7 +233,7 @@ export function LeftSidebar({ onCollapse, onOpenSettings }: LeftSidebarProps) {
|
||||
navigateToSection('all-photos', {})
|
||||
break
|
||||
case 'rated':
|
||||
navigateToSection('rated', { ratingMin: 1 })
|
||||
navigateToSection('rated', { ratingMin: 1, groupBy: 'rating' })
|
||||
break
|
||||
case 'discarded':
|
||||
navigateToSection('discarded', { flag: 'discarded' })
|
||||
|
||||
@@ -26,9 +26,11 @@ type TimelineItem =
|
||||
/**
|
||||
* Build the flat header|row item array the virtualizer renders.
|
||||
*
|
||||
* Three modes:
|
||||
* Four modes:
|
||||
* - groupBy='tag': one bucket per unique tag (plus an "Untagged" bucket
|
||||
* for photos with no tags). A photo with N tags appears in N buckets.
|
||||
* - groupBy='rating': one bucket per star rating 5..1 (plus "Unrated"
|
||||
* for rating 0). Each photo lands in exactly one bucket.
|
||||
* - groupBy='date' AND sortBy is a date field: month buckets (existing).
|
||||
* - otherwise: one un-headered stream.
|
||||
*/
|
||||
@@ -37,7 +39,7 @@ function buildItems(
|
||||
columns: number,
|
||||
rowHeight: number,
|
||||
sortBy: string,
|
||||
groupBy: 'date' | 'tag'
|
||||
groupBy: 'date' | 'tag' | 'rating'
|
||||
): TimelineItem[] {
|
||||
if (photos.length === 0) return []
|
||||
|
||||
@@ -105,6 +107,53 @@ function buildItems(
|
||||
return items
|
||||
}
|
||||
|
||||
// ── Rating grouping ───────────────────────────────────────────────────
|
||||
if (groupBy === 'rating') {
|
||||
// Bucket by star rating. Each photo lands in exactly one bucket;
|
||||
// rating 0 goes into "Unrated".
|
||||
const ratingBuckets = new Map<number, PhotoCell[]>()
|
||||
const unrated: PhotoCell[] = []
|
||||
|
||||
photos.forEach((photo, globalIndex) => {
|
||||
const cell: PhotoCell = { photo, globalIndex }
|
||||
if (photo.rating > 0) {
|
||||
const arr = ratingBuckets.get(photo.rating) ?? []
|
||||
arr.push(cell)
|
||||
ratingBuckets.set(photo.rating, arr)
|
||||
} else {
|
||||
unrated.push(cell)
|
||||
}
|
||||
})
|
||||
|
||||
// Highest rating first; Unrated goes at the end.
|
||||
const sortedRatings = Array.from(ratingBuckets.keys()).sort((a, b) => b - a)
|
||||
|
||||
let bucketIndex = 0
|
||||
for (const rating of sortedRatings) {
|
||||
items.push({
|
||||
type: 'header',
|
||||
key: `rating::${bucketIndex}::${rating}`,
|
||||
label: '★'.repeat(rating),
|
||||
height: HEADER_HEIGHT,
|
||||
})
|
||||
pushRowsForGroup(
|
||||
`rating::${bucketIndex}::${rating}`,
|
||||
ratingBuckets.get(rating)!
|
||||
)
|
||||
bucketIndex++
|
||||
}
|
||||
if (unrated.length > 0) {
|
||||
items.push({
|
||||
type: 'header',
|
||||
key: `rating::${bucketIndex}::__unrated`,
|
||||
label: 'Unrated',
|
||||
height: HEADER_HEIGHT,
|
||||
})
|
||||
pushRowsForGroup(`rating::${bucketIndex}::unrated`, unrated)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// ── Date grouping (existing) ──────────────────────────────────────────
|
||||
const isDateSort = sortBy === 'taken_at' || sortBy === 'added_at'
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export type SortField =
|
||||
| 'file_size'
|
||||
| 'rating'
|
||||
export type SortOrder = 'asc' | 'desc'
|
||||
export type GroupBy = 'date' | 'tag'
|
||||
export type GroupBy = 'date' | 'tag' | 'rating'
|
||||
|
||||
export interface FilterState {
|
||||
q: string
|
||||
|
||||
Reference in New Issue
Block a user