mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 19:36:36 +08:00
fix: improve discover and manual scrape controls
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState, type Dispatch, type SetStateAction } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Check, LoaderCircle, Search, Sparkles, X } from 'lucide-react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
@@ -27,8 +27,6 @@ const providers = [
|
||||
{ value: 'adult', label: 'Adult / 番号' },
|
||||
]
|
||||
|
||||
type ProviderMode = 'all' | 'single' | 'multi'
|
||||
|
||||
export function ManualScrapeDialog({
|
||||
open,
|
||||
media,
|
||||
@@ -41,7 +39,6 @@ export function ManualScrapeDialog({
|
||||
onApplied,
|
||||
}: ManualScrapeDialogProps) {
|
||||
const [query, setQuery] = useState('')
|
||||
const [providerMode, setProviderMode] = useState<ProviderMode>('all')
|
||||
const [selectedProviders, setSelectedProviders] = useState<string[]>([])
|
||||
const [includeEpisodeArtwork, setIncludeEpisodeArtwork] = useState(false)
|
||||
const [searching, setSearching] = useState(false)
|
||||
@@ -56,7 +53,6 @@ export function ManualScrapeDialog({
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
setQuery(defaultQuery || media?.title || '')
|
||||
setProviderMode('all')
|
||||
setSelectedProviders([])
|
||||
setIncludeEpisodeArtwork(episodeArtwork ?? false)
|
||||
setItems([])
|
||||
@@ -73,7 +69,7 @@ export function ManualScrapeDialog({
|
||||
}
|
||||
setSearching(true)
|
||||
setItems([])
|
||||
const providerValues = manualSearchProvidersForMode(providerMode, selectedProviders)
|
||||
const providerValues = manualSearchProvidersForSelection(selectedProviders, text)
|
||||
if (providerValues.length === 0) {
|
||||
toast.error('至少选择一个刮削源')
|
||||
setSearching(false)
|
||||
@@ -146,51 +142,32 @@ export function ManualScrapeDialog({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 border-b border-sand-200 p-5 lg:flex-row">
|
||||
<div className="flex min-w-0 flex-col gap-2 lg:max-w-sm">
|
||||
<div className="flex min-w-0 flex-col gap-2 lg:max-w-md">
|
||||
<span className="text-xs font-bold text-sand-500">刮削源</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => changeProviderMode('all', setProviderMode, setSelectedProviders)}
|
||||
className={providerModeButtonClass(providerMode === 'all')}
|
||||
onClick={() => setSelectedProviders([])}
|
||||
className={providerButtonClass(selectedProviders.length === 0)}
|
||||
>
|
||||
{providerMode === 'all' && <Check size={13} />}
|
||||
{selectedProviders.length === 0 && <Check size={13} />}
|
||||
全部源
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => changeProviderMode('single', setProviderMode, setSelectedProviders)}
|
||||
className={providerModeButtonClass(providerMode === 'single')}
|
||||
>
|
||||
{providerMode === 'single' && <Check size={13} />}
|
||||
单源
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => changeProviderMode('multi', setProviderMode, setSelectedProviders)}
|
||||
className={providerModeButtonClass(providerMode === 'multi')}
|
||||
>
|
||||
{providerMode === 'multi' && <Check size={13} />}
|
||||
多源
|
||||
</button>
|
||||
{providers.map((item) => {
|
||||
const active = selectedProviders.includes(item.value)
|
||||
return (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => setSelectedProviders((current) => toggleProvider(current, item.value))}
|
||||
className={providerButtonClass(active)}
|
||||
>
|
||||
{active && <Check size={13} />}
|
||||
{item.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{providerMode !== 'all' && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{providers.map((item) => {
|
||||
const active = selectedProviders.includes(item.value)
|
||||
return (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => chooseProvider(item.value, providerMode, setSelectedProviders)}
|
||||
className={providerButtonClass(active)}
|
||||
>
|
||||
{active && <Check size={13} />}
|
||||
{item.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-sand-500" />
|
||||
@@ -263,37 +240,39 @@ function candidateKey(item: ManualScrapeCandidate): string {
|
||||
return `${item.source}:${item.tmdb_id || item.bangumi_id || item.douban_id || item.thetvdb_id || item.title}:${item.media_type || ''}`
|
||||
}
|
||||
|
||||
function manualSearchProvidersForMode(mode: ProviderMode, selectedProviders: string[]): string[] {
|
||||
if (mode === 'all') return providers.map((provider) => provider.value)
|
||||
if (mode === 'single') return [selectedProviders[0] || providers[0].value]
|
||||
return selectedProviders
|
||||
function manualSearchProvidersForSelection(selectedProviders: string[], query: string): string[] {
|
||||
const explicitProvider = providerFromQueryPrefix(query)
|
||||
if (explicitProvider) return [explicitProvider]
|
||||
return selectedProviders.length > 0 ? selectedProviders : providers.map((provider) => provider.value)
|
||||
}
|
||||
|
||||
function changeProviderMode(
|
||||
mode: ProviderMode,
|
||||
setProviderMode: Dispatch<SetStateAction<ProviderMode>>,
|
||||
setSelectedProviders: Dispatch<SetStateAction<string[]>>,
|
||||
) {
|
||||
setProviderMode(mode)
|
||||
setSelectedProviders((current) => {
|
||||
if (mode === 'all') return []
|
||||
if (mode === 'single') return [current[0] || providers[0].value]
|
||||
return current.length > 0 ? current : providers.slice(0, 4).map((provider) => provider.value)
|
||||
})
|
||||
function toggleProvider(current: string[], value: string): string[] {
|
||||
if (current.includes(value)) {
|
||||
return current.filter((item) => item !== value)
|
||||
}
|
||||
const next = [...current, value]
|
||||
return next.length === providers.length ? [] : next
|
||||
}
|
||||
|
||||
function chooseProvider(
|
||||
value: string,
|
||||
mode: ProviderMode,
|
||||
setSelectedProviders: Dispatch<SetStateAction<string[]>>,
|
||||
) {
|
||||
setSelectedProviders((current) => {
|
||||
if (mode === 'single') return [value]
|
||||
if (current.includes(value)) {
|
||||
return current.filter((item) => item !== value)
|
||||
}
|
||||
return [...current, value]
|
||||
})
|
||||
function providerFromQueryPrefix(query: string): string {
|
||||
const prefix = query.trim().toLowerCase().match(/^([a-z]+)\s*:/)?.[1]
|
||||
switch (prefix) {
|
||||
case 'tmdb':
|
||||
return 'tmdb'
|
||||
case 'douban':
|
||||
return 'douban'
|
||||
case 'bangumi':
|
||||
case 'bgm':
|
||||
return 'bangumi'
|
||||
case 'thetvdb':
|
||||
case 'tvdb':
|
||||
return 'thetvdb'
|
||||
case 'adult':
|
||||
case 'jav':
|
||||
return 'adult'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function mergeManualCandidates(current: ManualScrapeCandidate[], incoming: ManualScrapeCandidate[]): ManualScrapeCandidate[] {
|
||||
@@ -305,15 +284,6 @@ function mergeManualCandidates(current: ManualScrapeCandidate[], incoming: Manua
|
||||
return Array.from(byKey.values())
|
||||
}
|
||||
|
||||
function providerModeButtonClass(active: boolean): string {
|
||||
return (
|
||||
'inline-flex h-10 items-center gap-1.5 rounded-xl border px-3 text-xs font-bold transition ' +
|
||||
(active
|
||||
? 'border-brand-300 bg-brand-50 text-brand-700'
|
||||
: 'border-sand-200 bg-white text-sand-600 hover:border-brand-200 hover:text-brand-600')
|
||||
)
|
||||
}
|
||||
|
||||
function providerButtonClass(active: boolean): string {
|
||||
return (
|
||||
'inline-flex h-11 items-center gap-1.5 rounded-xl border px-3 text-xs font-bold transition ' +
|
||||
|
||||
Reference in New Issue
Block a user