add rating system

This commit is contained in:
Garret Patti
2026-04-21 10:57:08 -04:00
parent d2057fb81c
commit d854bbe99b
16 changed files with 568 additions and 62 deletions

View File

@@ -1,7 +1,8 @@
'use client'
import { useEffect, useState, useCallback, useRef } from 'react'
import type { DirectoryListing, FileEntry } from '@/types'
import { useEffect, useState, useCallback, useRef, useMemo } from 'react'
import type { DirectoryListing, FileEntry, RatingOperator } from '@/types'
import { useDebounce } from '@/hooks/useDebounce'
import VideoPlayerModal from './VideoPlayerModal'
import ImageLightbox from './ImageLightbox'
import TagSelector from '@/components/tags/TagSelector'
@@ -34,6 +35,7 @@ export default function MixedView({ libraryId, libraryName, initialPath, readOnl
const [search, setSearch] = useState('')
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
const debouncedSearch = useDebounce(search, 200)
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
const [showFilters, setShowFilters] = useState(
() => typeof window !== 'undefined' && window.innerWidth >= 768
@@ -110,7 +112,19 @@ export default function MixedView({ libraryId, libraryName, initialPath, readOnl
.catch(() => {})
}, [])
const filtersActive = search !== '' || selectedTagIds.size > 0
const [ratingValue, setRatingValue] = useState<number | null>(null)
const [ratingOperator, setRatingOperator] = useState<RatingOperator>('gte')
const handleRatingChange = (value: number | null, operator: RatingOperator) => {
if (value === ratingValue && operator === ratingOperator) {
setRatingValue(null)
} else {
setRatingValue(value)
setRatingOperator(operator)
}
}
const filtersActive = search !== '' || selectedTagIds.size > 0 || ratingValue !== null
const fetchRecursive = useCallback(() => {
if (recursiveLoaded || recursiveLoading) return
@@ -155,14 +169,22 @@ export default function MixedView({ libraryId, libraryName, initialPath, readOnl
const sourceEntries = filtersActive ? recursiveEntries : (listing?.entries ?? [])
const filteredEntries = sourceEntries.filter((entry) => {
if (search && !entry.name.toLowerCase().includes(search.toLowerCase())) return false
const filteredEntries = useMemo(() => sourceEntries.filter((entry) => {
if (debouncedSearch && !entry.name.toLowerCase().includes(debouncedSearch.toLowerCase())) return false
if (selectedTagIds.size > 0 && entry.type !== 'directory') {
const entryTags = assignments[itemKeyFor(entry)] ?? []
if (![...selectedTagIds].every((id) => entryTags.includes(id))) return false
}
if (ratingValue !== null && entry.type !== 'directory') {
const r = entry.userRating ?? null
if (r === null) return false
if (ratingOperator === 'gte' && r < ratingValue) return false
if (ratingOperator === 'eq' && r !== ratingValue) return false
if (ratingOperator === 'lte' && r > ratingValue) return false
}
return true
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}), [sourceEntries, debouncedSearch, selectedTagIds, assignments, ratingValue, ratingOperator])
const mediaEntries = filteredEntries.filter(
(e) => e.mediaType === 'video' || e.mediaType === 'image'
@@ -337,6 +359,9 @@ export default function MixedView({ libraryId, libraryName, initialPath, readOnl
selectedTagIds={selectedTagIds}
onTagToggle={toggleTag}
refreshKey={filterRefreshKey}
ratingValue={ratingValue}
ratingOperator={ratingOperator}
onRatingChange={handleRatingChange}
/>
</div>
)}