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', {})
|
navigateToSection('all-photos', {})
|
||||||
break
|
break
|
||||||
case 'rated':
|
case 'rated':
|
||||||
navigateToSection('rated', { ratingMin: 1 })
|
navigateToSection('rated', { ratingMin: 1, groupBy: 'rating' })
|
||||||
break
|
break
|
||||||
case 'discarded':
|
case 'discarded':
|
||||||
navigateToSection('discarded', { flag: 'discarded' })
|
navigateToSection('discarded', { flag: 'discarded' })
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ type TimelineItem =
|
|||||||
/**
|
/**
|
||||||
* Build the flat header|row item array the virtualizer renders.
|
* 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
|
* - 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.
|
* 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).
|
* - groupBy='date' AND sortBy is a date field: month buckets (existing).
|
||||||
* - otherwise: one un-headered stream.
|
* - otherwise: one un-headered stream.
|
||||||
*/
|
*/
|
||||||
@@ -37,7 +39,7 @@ function buildItems(
|
|||||||
columns: number,
|
columns: number,
|
||||||
rowHeight: number,
|
rowHeight: number,
|
||||||
sortBy: string,
|
sortBy: string,
|
||||||
groupBy: 'date' | 'tag'
|
groupBy: 'date' | 'tag' | 'rating'
|
||||||
): TimelineItem[] {
|
): TimelineItem[] {
|
||||||
if (photos.length === 0) return []
|
if (photos.length === 0) return []
|
||||||
|
|
||||||
@@ -105,6 +107,53 @@ function buildItems(
|
|||||||
return items
|
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) ──────────────────────────────────────────
|
// ── Date grouping (existing) ──────────────────────────────────────────
|
||||||
const isDateSort = sortBy === 'taken_at' || sortBy === 'added_at'
|
const isDateSort = sortBy === 'taken_at' || sortBy === 'added_at'
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export type SortField =
|
|||||||
| 'file_size'
|
| 'file_size'
|
||||||
| 'rating'
|
| 'rating'
|
||||||
export type SortOrder = 'asc' | 'desc'
|
export type SortOrder = 'asc' | 'desc'
|
||||||
export type GroupBy = 'date' | 'tag'
|
export type GroupBy = 'date' | 'tag' | 'rating'
|
||||||
|
|
||||||
export interface FilterState {
|
export interface FilterState {
|
||||||
q: string
|
q: string
|
||||||
|
|||||||
Reference in New Issue
Block a user