add rating system
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user