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, useRef, useState, useCallback } from 'react'
import type { TvSeries, TvSeason, TvEpisode } from '@/types'
import { useEffect, useRef, useState, useCallback, useMemo } from 'react'
import type { TvSeries, TvSeason, TvEpisode, RatingOperator } from '@/types'
import { useDebounce } from '@/hooks/useDebounce'
import FilterPanel from '@/components/FilterPanel'
import VideoPlayerModal from '@/components/mixed/VideoPlayerModal'
@@ -33,6 +34,9 @@ export default function TvView({ libraryId, readOnly }: Props) {
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
const [seriesEpisodeTags, setSeriesEpisodeTags] = useState<Record<string, string[]>>({})
const [ratingValue, setRatingValue] = useState<number | null>(null)
const [ratingOperator, setRatingOperator] = useState<RatingOperator>('gte')
const debouncedSearch = useDebounce(search, 200)
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
const [showFilters, setShowFilters] = useState(
() => typeof window !== 'undefined' && window.innerWidth >= 768
@@ -375,29 +379,58 @@ export default function TvView({ libraryId, readOnly }: Props) {
}
}, [view, menuOpen, showTagPanel, selectedSeries])
const filtersActive = search !== '' || selectedTagIds.size > 0
const filtersActive = search !== '' || selectedTagIds.size > 0 || ratingValue !== null
const filteredSeries = series.filter((s) => {
if (search && !s.title.toLowerCase().includes(search.toLowerCase())) return false
const handleRatingChange = (value: number | null, operator: RatingOperator) => {
if (value === ratingValue && operator === ratingOperator) {
setRatingValue(null)
} else {
setRatingValue(value)
setRatingOperator(operator)
}
}
const filteredSeries = useMemo(() => series.filter((s) => {
if (debouncedSearch) {
const q = debouncedSearch.toLowerCase()
if (![s.title, s.plot, s.aiDescription, s.extractedText, s.extractedTextTranslated]
.some((f) => f?.toLowerCase().includes(q))) return false
}
if (selectedTagIds.size > 0) {
const seriesTags = assignments[s.item_key!] ?? []
const episodeTags = seriesEpisodeTags[s.id] ?? []
const allTags = seriesTags.length === 0 ? episodeTags
: episodeTags.length === 0 ? seriesTags
: [...new Set([...seriesTags, ...episodeTags])]
const allTags = [...new Set([...seriesTags, ...episodeTags])]
if (![...selectedTagIds].every((id) => allTags.includes(id))) return false
}
if (ratingValue !== null) {
const r = s.userRating
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
})
}), [series, debouncedSearch, selectedTagIds, assignments, seriesEpisodeTags, ratingValue, ratingOperator])
const filteredEpisodes = episodes.filter((ep) => {
if (search && !ep.title.toLowerCase().includes(search.toLowerCase())) return false
const filteredEpisodes = useMemo(() => episodes.filter((ep) => {
if (debouncedSearch) {
const q = debouncedSearch.toLowerCase()
if (![ep.title, ep.plot, ep.aiDescription, ep.extractedText, ep.extractedTextTranslated]
.some((f) => f?.toLowerCase().includes(q))) return false
}
if (selectedTagIds.size > 0) {
const epTags = assignments[ep.item_key!] ?? []
if (![...selectedTagIds].every((id) => epTags.includes(id))) return false
}
if (ratingValue !== null) {
const r = ep.userRating
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
})
}), [episodes, debouncedSearch, selectedTagIds, assignments, ratingValue, ratingOperator])
// Arrow key navigation for series/season levels (mirrors the prev/next UI buttons)
useEffect(() => {
@@ -524,6 +557,9 @@ export default function TvView({ libraryId, readOnly }: Props) {
selectedTagIds={selectedTagIds}
onTagToggle={toggleTag}
refreshKey={filterRefreshKey}
ratingValue={ratingValue}
ratingOperator={ratingOperator}
onRatingChange={handleRatingChange}
/>
</div>
)}