mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
优化
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, Check, Shuffle } from 'lucide-react'
|
||||
|
||||
import {
|
||||
SORT_OPTIONS,
|
||||
type SortField,
|
||||
type SortOption,
|
||||
type SortOrder,
|
||||
} from '../utils/mediaSort'
|
||||
|
||||
type MediaSortDropdownProps = {
|
||||
value: SortField
|
||||
order: SortOrder
|
||||
onChange: (field: SortField, order: SortOrder) => void
|
||||
onReshuffle?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function MediaSortDropdown({
|
||||
value,
|
||||
order,
|
||||
onChange,
|
||||
onReshuffle,
|
||||
className = '',
|
||||
}: MediaSortDropdownProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const currentOption = SORT_OPTIONS.find((opt) => opt.id === value) ?? SORT_OPTIONS[0]
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const handleSelect = (option: SortOption) => {
|
||||
if (option.id === 'random') {
|
||||
if (value === 'random') {
|
||||
onReshuffle?.()
|
||||
} else {
|
||||
onChange('random', 'desc')
|
||||
}
|
||||
setIsOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (value === option.id) {
|
||||
// 再次点击相同项:切换升降序
|
||||
const nextOrder: SortOrder = order === 'asc' ? 'desc' : 'asc'
|
||||
onChange(option.id, nextOrder)
|
||||
} else {
|
||||
// 点击新项:应用默认排序方向
|
||||
onChange(option.id, option.defaultOrder)
|
||||
}
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative inline-block text-left ${className}`} ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
className="inline-flex h-9 items-center gap-1.5 rounded-xl border border-sand-200 bg-white/90 px-3 py-1.5 text-xs font-semibold text-ink-600 shadow-sm transition-all hover:border-brand-300 hover:bg-brand-50/50 hover:text-brand-700"
|
||||
title="更改排序方式"
|
||||
>
|
||||
<ArrowUpDown size={14} className="text-sand-500" />
|
||||
<span>排序: {currentOption.label}</span>
|
||||
{value === 'random' ? (
|
||||
<Shuffle size={12} className="text-brand-600" />
|
||||
) : order === 'asc' ? (
|
||||
<ArrowUp size={13} className="text-brand-600 font-bold" />
|
||||
) : (
|
||||
<ArrowDown size={13} className="text-brand-600 font-bold" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 z-50 mt-1.5 w-52 origin-top-right rounded-2xl border border-sand-200/80 bg-white/95 p-1.5 shadow-xl backdrop-blur-md transition-all animate-in fade-in-0 zoom-in-95">
|
||||
<div className="px-2.5 py-1.5 text-[11px] font-bold text-sand-500 border-b border-sand-100">
|
||||
排序方式
|
||||
</div>
|
||||
<div className="max-h-80 overflow-y-auto py-1 space-y-0.5">
|
||||
{SORT_OPTIONS.map((option) => {
|
||||
const isSelected = value === option.id
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
onClick={() => handleSelect(option)}
|
||||
className={`flex w-full items-center justify-between rounded-xl px-3 py-2 text-xs font-medium transition-colors ${
|
||||
isSelected
|
||||
? 'bg-brand-50 text-brand-700 font-semibold'
|
||||
: 'text-ink-600 hover:bg-sand-50 hover:text-brand-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isSelected && <Check size={13} className="text-brand-600" />}
|
||||
<span className={isSelected ? '' : 'pl-5'}>{option.label}</span>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="flex items-center text-brand-600">
|
||||
{option.id === 'random' ? (
|
||||
<Shuffle size={13} />
|
||||
) : order === 'asc' ? (
|
||||
<ArrowUp size={14} className="stroke-[2.5]" />
|
||||
) : (
|
||||
<ArrowDown size={14} className="stroke-[2.5]" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useLocation, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
import { historyAPI } from '../api/history'
|
||||
import type { Media } from '../types'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import type { SeriesCard } from '../utils/groupSeries'
|
||||
import {
|
||||
sortMediaList,
|
||||
sortSeriesList,
|
||||
type SortField,
|
||||
type SortOrder,
|
||||
} from '../utils/mediaSort'
|
||||
import { LibraryPageDialogs } from './LibraryPageDialogs'
|
||||
import { LibraryPageHeader } from './LibraryPageHeader'
|
||||
import { LibraryMediaSections } from './LibraryMediaSections'
|
||||
@@ -25,6 +32,53 @@ export function LibraryPage() {
|
||||
const [seriesMetadataEditOpen, setSeriesMetadataEditOpen] = useState(false)
|
||||
const [manualMovie, setManualMovie] = useState<Media | null>(null)
|
||||
|
||||
// 排序状态(支持按库记忆)
|
||||
const [sortField, setSortField] = useState<SortField>(() => {
|
||||
const saved = localStorage.getItem(`mmtl_lib_sort_field_${id}`) || localStorage.getItem('mmtl_lib_sort_field')
|
||||
return (saved as SortField) || 'title'
|
||||
})
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>(() => {
|
||||
const saved = localStorage.getItem(`mmtl_lib_sort_order_${id}`) || localStorage.getItem('mmtl_lib_sort_order')
|
||||
return (saved as SortOrder) || 'asc'
|
||||
})
|
||||
const [randomSeed, setRandomSeed] = useState(() => Date.now())
|
||||
const [historyMap, setHistoryMap] = useState<Map<string, string>>(new Map())
|
||||
|
||||
useEffect(() => {
|
||||
historyAPI
|
||||
.list(1000)
|
||||
.then((historyItems) => {
|
||||
const map = new Map<string, string>()
|
||||
for (const item of historyItems ?? []) {
|
||||
if (item.media_id && item.watched_at) {
|
||||
if (!map.has(item.media_id) || new Date(item.watched_at) > new Date(map.get(item.media_id)!)) {
|
||||
map.set(item.media_id, item.watched_at)
|
||||
}
|
||||
}
|
||||
}
|
||||
setHistoryMap(map)
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const handleSortChange = (field: SortField, order: SortOrder) => {
|
||||
setSortField(field)
|
||||
setSortOrder(order)
|
||||
if (id) {
|
||||
localStorage.setItem(`mmtl_lib_sort_field_${id}`, field)
|
||||
localStorage.setItem(`mmtl_lib_sort_order_${id}`, order)
|
||||
}
|
||||
localStorage.setItem('mmtl_lib_sort_field', field)
|
||||
localStorage.setItem('mmtl_lib_sort_order', order)
|
||||
if (field === 'random') {
|
||||
setRandomSeed(Date.now())
|
||||
}
|
||||
}
|
||||
|
||||
const handleReshuffle = () => {
|
||||
setRandomSeed(Date.now())
|
||||
}
|
||||
|
||||
// 剧集模式:选中某个剧集后展开详情
|
||||
const [selectedSeries, setSelectedSeries] = useState<SeriesCard | null>(null)
|
||||
const [selectedSeason, setSelectedSeason] = useState<number | null>(null)
|
||||
@@ -43,6 +97,14 @@ export function LibraryPage() {
|
||||
reloadCurrentLibrary,
|
||||
} = useLibraryData(id, selectedSeries)
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
return sortMediaList(items, sortField, sortOrder, randomSeed, historyMap)
|
||||
}, [items, sortField, sortOrder, randomSeed, historyMap])
|
||||
|
||||
const sortedSeriesCards = useMemo(() => {
|
||||
return sortSeriesList(seriesCards, sortField, sortOrder, randomSeed, historyMap)
|
||||
}, [seriesCards, sortField, sortOrder, randomSeed, historyMap])
|
||||
|
||||
const {
|
||||
scanning,
|
||||
scanProgress,
|
||||
@@ -66,7 +128,7 @@ export function LibraryPage() {
|
||||
isSeriesLibrary,
|
||||
isSeries,
|
||||
loading,
|
||||
seriesCards,
|
||||
seriesCards: sortedSeriesCards,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
selectedSeries,
|
||||
@@ -116,7 +178,7 @@ export function LibraryPage() {
|
||||
{!selectedSeries && (
|
||||
<LibraryPageHeader
|
||||
library={library}
|
||||
itemCount={isSeries ? seriesCards.length : total}
|
||||
itemCount={isSeries ? sortedSeriesCards.length : total}
|
||||
loadingAllText={loadingAllText}
|
||||
scanProgress={scanProgress}
|
||||
isAdmin={role === 'admin'}
|
||||
@@ -124,6 +186,10 @@ export function LibraryPage() {
|
||||
scanning={scanning}
|
||||
scraping={scraping}
|
||||
repairing={repairing}
|
||||
sortField={sortField}
|
||||
sortOrder={sortOrder}
|
||||
onSortChange={handleSortChange}
|
||||
onReshuffle={handleReshuffle}
|
||||
onScrapeEpisodeArtworkChange={setScrapeEpisodeArtwork}
|
||||
onScan={handleScan}
|
||||
onScrape={() => setScrapeDialogOpen(true)}
|
||||
@@ -133,8 +199,8 @@ export function LibraryPage() {
|
||||
|
||||
<LibraryMediaSections
|
||||
isSeries={isSeries}
|
||||
items={items}
|
||||
seriesCards={seriesCards}
|
||||
items={sortedItems}
|
||||
seriesCards={sortedSeriesCards}
|
||||
selectedSeries={selectedSeries}
|
||||
loading={loading}
|
||||
movieActions={movieActions}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Library } from '../types'
|
||||
import { EpisodeArtworkToggle } from '../components/EpisodeArtworkToggle'
|
||||
import { MediaSortDropdown } from '../components/MediaSortDropdown'
|
||||
import type { SortField, SortOrder } from '../utils/mediaSort'
|
||||
import { libraryDisplayPath } from './libraryDisplayModel'
|
||||
|
||||
type LibraryPageHeaderProps = {
|
||||
@@ -12,6 +14,10 @@ type LibraryPageHeaderProps = {
|
||||
scanning: boolean
|
||||
scraping: boolean
|
||||
repairing: boolean
|
||||
sortField: SortField
|
||||
sortOrder: SortOrder
|
||||
onSortChange: (field: SortField, order: SortOrder) => void
|
||||
onReshuffle: () => void
|
||||
onScrapeEpisodeArtworkChange: (checked: boolean) => void
|
||||
onScan: () => void
|
||||
onScrape: () => void
|
||||
@@ -28,6 +34,10 @@ export function LibraryPageHeader({
|
||||
scanning,
|
||||
scraping,
|
||||
repairing,
|
||||
sortField,
|
||||
sortOrder,
|
||||
onSortChange,
|
||||
onReshuffle,
|
||||
onScrapeEpisodeArtworkChange,
|
||||
onScan,
|
||||
onScrape,
|
||||
@@ -46,30 +56,38 @@ export function LibraryPageHeader({
|
||||
{loadingAllText && <p className="mt-1 text-xs text-sand-500">{loadingAllText}</p>}
|
||||
{scanProgress && <p className="mt-1 text-xs text-brand-500">{scanProgress}</p>}
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<EpisodeArtworkToggle
|
||||
checked={scrapeEpisodeArtwork}
|
||||
onChange={onScrapeEpisodeArtworkChange}
|
||||
title="关闭后仍会获取主海报和每集文字元数据,只跳过每集图片"
|
||||
className="h-10"
|
||||
/>
|
||||
<button onClick={onScan} disabled={scanning} className="btn-outline">
|
||||
{scanning ? '扫描中…' : '扫描媒体库'}
|
||||
</button>
|
||||
<button onClick={onScrape} disabled={scraping} className="btn-outline" title="对整个媒体库执行刮削元数据">
|
||||
{scraping ? '刮削中…' : '整库刮削元数据'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onRepairRescrape}
|
||||
disabled={repairing}
|
||||
className="btn-outline"
|
||||
title="回填本库占位符外部 ID 并重刮,修正空 ID / 拆集问题"
|
||||
>
|
||||
{repairing ? '修复中…' : '修复+重刮整库'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<MediaSortDropdown
|
||||
value={sortField}
|
||||
order={sortOrder}
|
||||
onChange={onSortChange}
|
||||
onReshuffle={onReshuffle}
|
||||
/>
|
||||
{isAdmin && (
|
||||
<>
|
||||
<EpisodeArtworkToggle
|
||||
checked={scrapeEpisodeArtwork}
|
||||
onChange={onScrapeEpisodeArtworkChange}
|
||||
title="关闭后仍会获取主海报和每集文字元数据,只跳过每集图片"
|
||||
className="h-9"
|
||||
/>
|
||||
<button onClick={onScan} disabled={scanning} className="btn-outline">
|
||||
{scanning ? '扫描中…' : '扫描媒体库'}
|
||||
</button>
|
||||
<button onClick={onScrape} disabled={scraping} className="btn-outline" title="对整个媒体库执行刮削元数据">
|
||||
{scraping ? '刮削中…' : '整库刮削元数据'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onRepairRescrape}
|
||||
disabled={repairing}
|
||||
className="btn-outline"
|
||||
title="回填本库占位符外部 ID 并重刮,修正空 ID / 拆集问题"
|
||||
>
|
||||
{repairing ? '修复中…' : '修复+重刮整库'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import type { Media } from '../types'
|
||||
import { seriesTitle, type SeriesCard } from './groupSeries'
|
||||
|
||||
export type SortField =
|
||||
| 'title'
|
||||
| 'release_date'
|
||||
| 'year'
|
||||
| 'created_at'
|
||||
| 'updated_at'
|
||||
| 'rating'
|
||||
| 'imdb_rating'
|
||||
| 'last_played'
|
||||
| 'duration'
|
||||
| 'bitrate'
|
||||
| 'random'
|
||||
|
||||
export type SortOrder = 'asc' | 'desc'
|
||||
|
||||
export type SortOption = {
|
||||
id: SortField
|
||||
label: string
|
||||
defaultOrder: SortOrder
|
||||
}
|
||||
|
||||
export const SORT_OPTIONS: SortOption[] = [
|
||||
{ id: 'title', label: '标题', defaultOrder: 'asc' },
|
||||
{ id: 'release_date', label: '发行日期', defaultOrder: 'desc' },
|
||||
{ id: 'year', label: '年份', defaultOrder: 'desc' },
|
||||
{ id: 'created_at', label: '加入日期', defaultOrder: 'desc' },
|
||||
{ id: 'updated_at', label: '最后一集添加日期', defaultOrder: 'desc' },
|
||||
{ id: 'rating', label: '影评人评分', defaultOrder: 'desc' },
|
||||
{ id: 'imdb_rating', label: 'IMDB评分', defaultOrder: 'desc' },
|
||||
{ id: 'last_played', label: '播放日期', defaultOrder: 'desc' },
|
||||
{ id: 'duration', label: '播放时长', defaultOrder: 'desc' },
|
||||
{ id: 'bitrate', label: '比特率', defaultOrder: 'desc' },
|
||||
{ id: 'random', label: '随机', defaultOrder: 'desc' },
|
||||
]
|
||||
|
||||
export function getSortOption(field: SortField): SortOption {
|
||||
return SORT_OPTIONS.find((opt) => opt.id === field) ?? SORT_OPTIONS[0]
|
||||
}
|
||||
|
||||
// 简单伪随机洗牌(基于 seed 保证同一渲染周期内稳定)
|
||||
function pseudoRandomShuffle<T>(array: T[], seed = 1): T[] {
|
||||
const result = [...array]
|
||||
let currentSeed = seed
|
||||
const random = () => {
|
||||
currentSeed = (currentSeed * 9301 + 49297) % 233280
|
||||
return currentSeed / 233280
|
||||
}
|
||||
for (let i = result.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(random() * (i + 1))
|
||||
const temp = result[i]
|
||||
result[i] = result[j]
|
||||
result[j] = temp
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function compareStrings(a?: string, b?: string, order: SortOrder = 'asc'): number {
|
||||
const valA = (a ?? '').trim()
|
||||
const valB = (b ?? '').trim()
|
||||
if (!valA && !valB) return 0
|
||||
if (!valA) return 1
|
||||
if (!valB) return -1
|
||||
const cmp = valA.localeCompare(valB, 'zh-CN', { numeric: true, sensitivity: 'base' })
|
||||
return order === 'asc' ? cmp : -cmp
|
||||
}
|
||||
|
||||
function compareNumbers(a: number, b: number, order: SortOrder = 'asc'): number {
|
||||
if (isNaN(a) && isNaN(b)) return 0
|
||||
if (isNaN(a)) return 1
|
||||
if (isNaN(b)) return -1
|
||||
return order === 'asc' ? a - b : b - a
|
||||
}
|
||||
|
||||
function compareDates(a?: string, b?: string, order: SortOrder = 'asc'): number {
|
||||
const timeA = a ? new Date(a).getTime() : 0
|
||||
const timeB = b ? new Date(b).getTime() : 0
|
||||
const safeTimeA = isNaN(timeA) ? 0 : timeA
|
||||
const safeTimeB = isNaN(timeB) ? 0 : timeB
|
||||
if (!safeTimeA && !safeTimeB) return 0
|
||||
if (!safeTimeA) return 1
|
||||
if (!safeTimeB) return -1
|
||||
return order === 'asc' ? safeTimeA - safeTimeB : safeTimeB - safeTimeA
|
||||
}
|
||||
|
||||
export function sortMediaList(
|
||||
items: Media[],
|
||||
field: SortField,
|
||||
order: SortOrder,
|
||||
randomSeed = 1,
|
||||
historyMap?: Map<string, string>,
|
||||
): Media[] {
|
||||
if (!items || items.length === 0) return []
|
||||
if (field === 'random') {
|
||||
return pseudoRandomShuffle(items, randomSeed)
|
||||
}
|
||||
|
||||
const list = [...items]
|
||||
list.sort((a, b) => {
|
||||
switch (field) {
|
||||
case 'title':
|
||||
return compareStrings(a.title || a.original_name, b.title || b.original_name, order)
|
||||
case 'release_date': {
|
||||
const dateA = a.release_date || (a.year ? `${a.year}-01-01` : '')
|
||||
const dateB = b.release_date || (b.year ? `${b.year}-01-01` : '')
|
||||
return compareDates(dateA, dateB, order) || compareStrings(a.title, b.title, 'asc')
|
||||
}
|
||||
case 'year': {
|
||||
const cmp = compareNumbers(a.year || 0, b.year || 0, order)
|
||||
return cmp !== 0 ? cmp : compareStrings(a.title, b.title, 'asc')
|
||||
}
|
||||
case 'created_at':
|
||||
return compareDates(a.created_at, b.created_at, order) || compareStrings(a.title, b.title, 'asc')
|
||||
case 'updated_at':
|
||||
return compareDates(a.updated_at || a.created_at, b.updated_at || b.created_at, order) || compareStrings(a.title, b.title, 'asc')
|
||||
case 'rating':
|
||||
case 'imdb_rating': {
|
||||
const cmp = compareNumbers(a.rating || 0, b.rating || 0, order)
|
||||
return cmp !== 0 ? cmp : compareStrings(a.title, b.title, 'asc')
|
||||
}
|
||||
case 'last_played': {
|
||||
const playedA = historyMap?.get(a.id)
|
||||
const playedB = historyMap?.get(b.id)
|
||||
return compareDates(playedA, playedB, order) || compareStrings(a.title, b.title, 'asc')
|
||||
}
|
||||
case 'duration': {
|
||||
const cmp = compareNumbers(a.duration_sec || 0, b.duration_sec || 0, order)
|
||||
return cmp !== 0 ? cmp : compareStrings(a.title, b.title, 'asc')
|
||||
}
|
||||
case 'bitrate': {
|
||||
// 比特率 / 大小
|
||||
const cmp = compareNumbers(a.size_bytes || 0, b.size_bytes || 0, order)
|
||||
return cmp !== 0 ? cmp : compareStrings(a.title, b.title, 'asc')
|
||||
}
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
export function sortSeriesList(
|
||||
cards: SeriesCard[],
|
||||
field: SortField,
|
||||
order: SortOrder,
|
||||
randomSeed = 1,
|
||||
historyMap?: Map<string, string>,
|
||||
): SeriesCard[] {
|
||||
if (!cards || cards.length === 0) return []
|
||||
if (field === 'random') {
|
||||
return pseudoRandomShuffle(cards, randomSeed)
|
||||
}
|
||||
|
||||
const list = [...cards]
|
||||
list.sort((a, b) => {
|
||||
const repA = a.rep
|
||||
const repB = b.rep
|
||||
const titleA = seriesTitle(repA)
|
||||
const titleB = seriesTitle(repB)
|
||||
|
||||
switch (field) {
|
||||
case 'title':
|
||||
return compareStrings(titleA, titleB, order)
|
||||
case 'release_date': {
|
||||
const dateA = repA.release_date || (repA.year ? `${repA.year}-01-01` : '')
|
||||
const dateB = repB.release_date || (repB.year ? `${repB.year}-01-01` : '')
|
||||
return compareDates(dateA, dateB, order) || compareStrings(titleA, titleB, 'asc')
|
||||
}
|
||||
case 'year': {
|
||||
const cmp = compareNumbers(repA.year || 0, repB.year || 0, order)
|
||||
return cmp !== 0 ? cmp : compareStrings(titleA, titleB, 'asc')
|
||||
}
|
||||
case 'created_at':
|
||||
return compareDates(repA.created_at, repB.created_at, order) || compareStrings(titleA, titleB, 'asc')
|
||||
case 'updated_at': {
|
||||
const updateA = repA.updated_at || repA.created_at
|
||||
const updateB = repB.updated_at || repB.created_at
|
||||
return compareDates(updateA, updateB, order) || compareStrings(titleA, titleB, 'asc')
|
||||
}
|
||||
case 'rating':
|
||||
case 'imdb_rating': {
|
||||
const cmp = compareNumbers(repA.rating || 0, repB.rating || 0, order)
|
||||
return cmp !== 0 ? cmp : compareStrings(titleA, titleB, 'asc')
|
||||
}
|
||||
case 'last_played': {
|
||||
const playedA = historyMap?.get(repA.id)
|
||||
const playedB = historyMap?.get(repB.id)
|
||||
return compareDates(playedA, playedB, order) || compareStrings(titleA, titleB, 'asc')
|
||||
}
|
||||
case 'duration': {
|
||||
const cmp = compareNumbers(repA.duration_sec || 0, repB.duration_sec || 0, order)
|
||||
return cmp !== 0 ? cmp : compareStrings(titleA, titleB, 'asc')
|
||||
}
|
||||
case 'bitrate': {
|
||||
const cmp = compareNumbers(repA.size_bytes || 0, repB.size_bytes || 0, order)
|
||||
return cmp !== 0 ? cmp : compareStrings(titleA, titleB, 'asc')
|
||||
}
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
return list
|
||||
}
|
||||
Reference in New Issue
Block a user