diff --git a/web/src/pages/DiscoverDetailModal.tsx b/web/src/pages/DiscoverDetailModal.tsx
index 773338f..fc29f62 100644
--- a/web/src/pages/DiscoverDetailModal.tsx
+++ b/web/src/pages/DiscoverDetailModal.tsx
@@ -1,64 +1,30 @@
import { useState } from 'react'
import toast from 'react-hot-toast'
-import { Download, Rss, X } from 'lucide-react'
import type { DiscoverItem } from '../api/discover'
-import { imageURL } from '../api/client'
-import { buildSiteSearchFeedURL, buildSubscriptionAliases, subscriptionsAPI } from '../api/subscriptions'
-import { buildSubscribeKeyword, discoverItemSource } from './discoverPageModel'
+import { subscriptionsAPI } from '../api/subscriptions'
+import { discoverItemSource } from './discoverPageModel'
+import {
+ DiscoverArtworkPanel,
+ DiscoverModalHeader,
+ DiscoverOverviewPanel,
+ DiscoverSubscriptionRules,
+} from './DiscoverDetailModalSections'
+import {
+ apiErrorMessage,
+ buildDiscoverSubscriptionInput,
+ initialDiscoverSubscriptionForm,
+} from './discoverDetailModalModel'
export function DiscoverDetailModal({ item, onClose }: { item: DiscoverItem; onClose: () => void }) {
const source = discoverItemSource(item)
- const keyword = item.subscribe_keyword || buildSubscribeKeyword(item)
- const [form, setForm] = useState({
- keyword,
- search_mode: 'keyword',
- imdb_id: '',
- media_type: item.media_type || '',
- resolution: 'best',
- quality: '',
- effects: '',
- release_groups: '',
- exclude_words: 'cam,ts,tc,枪版',
- wash_enabled: false,
- wash_priority: 'balanced',
- save_path: '',
- media_category: '',
- priority: 50,
- run_now: true,
- })
+ const [form, setForm] = useState(() => initialDiscoverSubscriptionForm(item))
const [busy, setBusy] = useState(false)
const submit = async () => {
- const finalKeyword = form.keyword.trim() || keyword
- const feed = buildSiteSearchFeedURL(finalKeyword, source, buildSubscriptionAliases(item))
setBusy(true)
try {
- const sub = await subscriptionsAPI.create({
- name: `${item.title} 自动订阅`,
- feed_url: feed,
- filter: finalKeyword,
- media_type: form.media_type || undefined,
- media_category: form.media_category || undefined,
- save_path: form.save_path || undefined,
- search_mode: form.search_mode,
- imdb_id: form.imdb_id || undefined,
- source,
- poster_url: item.poster_url || undefined,
- backdrop_url: item.backdrop_url || undefined,
- overview: item.overview || undefined,
- original_name: item.original_name || undefined,
- year: item.year || undefined,
- resolution: form.resolution === 'best' ? 'best' : form.resolution,
- quality: form.quality || undefined,
- effects: form.effects || undefined,
- release_groups: form.release_groups || undefined,
- exclude_words: form.exclude_words || undefined,
- wash_enabled: form.wash_enabled,
- wash_priority: form.wash_priority,
- priority: form.priority,
- enabled: true,
- })
+ const sub = await subscriptionsAPI.create(buildDiscoverSubscriptionInput(item, form, source))
if (form.run_now) {
const run = await subscriptionsAPI.runNow(sub.id)
toast.success(run.queued > 0 ? `已订阅并加入 ${run.queued} 个下载` : '已订阅,暂未命中可下载资源')
@@ -67,10 +33,7 @@ export function DiscoverDetailModal({ item, onClose }: { item: DiscoverItem; onC
}
onClose()
} catch (err) {
- const msg =
- (err as { response?: { data?: { error?: string } } })?.response?.data?.error ??
- '订阅失败'
- toast.error(msg)
+ toast.error(apiErrorMessage(err, '订阅失败'))
} finally {
setBusy(false)
}
@@ -79,137 +42,17 @@ export function DiscoverDetailModal({ item, onClose }: { item: DiscoverItem; onC
return (
-
-
-
{source}
-
{item.title}
-
- {[item.media_type, item.year && item.year > 0 ? item.year : '', item.rating ? `★ ${item.rating.toFixed(1)}` : '']
- .filter(Boolean)
- .join(' · ')}
-
-
-
-
-
+
-
-
- {item.poster_url ? (
-
})
- ) : (
-
无海报
- )}
-
- {item.backdrop_url && (
-
})
- )}
-
-
+
-
- 简介
- {item.overview || '当前数据源没有返回简介。'}
-
-
-
-
-
- 订阅下载规则
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
setForm((current) => ({ ...current, ...patch }))}
+ onSubmit={submit}
+ />
diff --git a/web/src/pages/DiscoverDetailModalSections.tsx b/web/src/pages/DiscoverDetailModalSections.tsx
new file mode 100644
index 0000000..6221828
--- /dev/null
+++ b/web/src/pages/DiscoverDetailModalSections.tsx
@@ -0,0 +1,172 @@
+import { Download, Rss, X } from 'lucide-react'
+
+import type { DiscoverItem } from '../api/discover'
+import { imageURL } from '../api/client'
+import { discoverItemMetaText, type DiscoverSubscriptionForm } from './discoverDetailModalModel'
+
+type FormPatch = Partial
+
+type FieldOption = {
+ label: string
+ value: string
+}
+
+const searchModeOptions: FieldOption[] = [
+ { value: 'keyword', label: '标题关键词' },
+ { value: 'imdb', label: 'IMDB ID' },
+]
+
+const mediaTypeOptions: FieldOption[] = [
+ { value: '', label: '自动识别' },
+ { value: 'movie', label: '电影' },
+ { value: 'tv', label: '电视剧' },
+ { value: 'anime', label: '动漫' },
+ { value: 'variety', label: '综艺' },
+]
+
+const resolutionOptions: FieldOption[] = [
+ { value: 'best', label: '自动择优' },
+ { value: '2160p', label: '2160p / 4K' },
+ { value: '1080p', label: '1080p' },
+ { value: '720p', label: '720p' },
+]
+
+const qualityOptions: FieldOption[] = [
+ { value: '', label: '不限' },
+ { value: 'remux', label: 'REMUX' },
+ { value: 'bluray', label: 'BluRay' },
+ { value: 'web-dl', label: 'WEB-DL' },
+ { value: 'hdtv', label: 'HDTV' },
+]
+
+const washPriorityOptions: FieldOption[] = [
+ { value: 'balanced', label: '均衡' },
+ { value: 'resolution', label: '分辨率优先' },
+ { value: 'quality', label: '片源质量优先' },
+ { value: 'effects', label: 'HDR/DV/Atmos 优先' },
+ { value: 'seeders', label: '做种数优先' },
+]
+
+export function DiscoverModalHeader({ item, source, onClose }: { item: DiscoverItem; source: string; onClose: () => void }) {
+ return (
+
+
+
{source}
+
{item.title}
+
{discoverItemMetaText(item)}
+
+
+
+ )
+}
+
+export function DiscoverArtworkPanel({ item }: { item: DiscoverItem }) {
+ return (
+
+
+ {item.poster_url ? (
+
})
+ ) : (
+
无海报
+ )}
+
+ {item.backdrop_url && (
+
})
+ )}
+
+ )
+}
+
+export function DiscoverOverviewPanel({ overview }: { overview?: string }) {
+ return (
+
+ 简介
+ {overview || '当前数据源没有返回简介。'}
+
+ )
+}
+
+export function DiscoverSubscriptionRules({
+ form,
+ busy,
+ onChange,
+ onSubmit,
+}: {
+ form: DiscoverSubscriptionForm
+ busy: boolean
+ onChange: (patch: FormPatch) => void
+ onSubmit: () => void
+}) {
+ return (
+
+
+
+ 订阅下载规则
+
+
+ onChange({ keyword })} className="md:col-span-2" />
+ onChange({ search_mode })} />
+ onChange({ imdb_id })} />
+ onChange({ media_type })} />
+ onChange({ resolution })} />
+ onChange({ quality })} />
+ onChange({ effects })} />
+ onChange({ wash_priority })} />
+ onChange({ wash_enabled })} />
+ onChange({ release_groups })} />
+ onChange({ exclude_words })} />
+ onChange({ media_category })} />
+ onChange({ save_path })} />
+
+ onChange({ run_now })} onSubmit={onSubmit} />
+
+ )
+}
+
+function TextField({ label, value, placeholder, className = '', onChange }: { label: string; value: string; placeholder?: string; className?: string; onChange: (value: string) => void }) {
+ return (
+
+ )
+}
+
+function SelectField({ label, value, options, disabled, onChange }: { label: string; value: string; options: FieldOption[]; disabled?: boolean; onChange: (value: string) => void }) {
+ return (
+
+ )
+}
+
+function CheckboxField({ label, checked, onChange }: { label: string; checked: boolean; onChange: (checked: boolean) => void }) {
+ return (
+
+ )
+}
+
+function DiscoverSubmitBar({ busy, runNow, onRunNowChange, onSubmit }: { busy: boolean; runNow: boolean; onRunNowChange: (value: boolean) => void; onSubmit: () => void }) {
+ return (
+
+
+
+
+ )
+}
diff --git a/web/src/pages/discoverDetailModalModel.ts b/web/src/pages/discoverDetailModalModel.ts
new file mode 100644
index 0000000..88d9b6a
--- /dev/null
+++ b/web/src/pages/discoverDetailModalModel.ts
@@ -0,0 +1,92 @@
+import type { DiscoverItem } from '../api/discover'
+import { buildSiteSearchFeedURL, buildSubscriptionAliases } from '../api/subscriptions'
+import { buildSubscribeKeyword } from './discoverPageModel'
+
+export type DiscoverSubscriptionForm = {
+ keyword: string
+ search_mode: string
+ imdb_id: string
+ media_type: string
+ resolution: string
+ quality: string
+ effects: string
+ release_groups: string
+ exclude_words: string
+ wash_enabled: boolean
+ wash_priority: string
+ save_path: string
+ media_category: string
+ priority: number
+ run_now: boolean
+}
+
+export function discoverSubscriptionKeyword(item: DiscoverItem): string {
+ return item.subscribe_keyword || buildSubscribeKeyword(item)
+}
+
+export function initialDiscoverSubscriptionForm(item: DiscoverItem): DiscoverSubscriptionForm {
+ return {
+ keyword: discoverSubscriptionKeyword(item),
+ search_mode: 'keyword',
+ imdb_id: '',
+ media_type: item.media_type || '',
+ resolution: 'best',
+ quality: '',
+ effects: '',
+ release_groups: '',
+ exclude_words: 'cam,ts,tc,枪版',
+ wash_enabled: false,
+ wash_priority: 'balanced',
+ save_path: '',
+ media_category: '',
+ priority: 50,
+ run_now: true,
+ }
+}
+
+export function discoverItemMetaText(item: DiscoverItem): string {
+ return [
+ item.media_type,
+ item.year && item.year > 0 ? item.year : '',
+ item.rating ? `★ ${item.rating.toFixed(1)}` : '',
+ ]
+ .filter(Boolean)
+ .join(' · ')
+}
+
+export function buildDiscoverSubscriptionInput(
+ item: DiscoverItem,
+ form: DiscoverSubscriptionForm,
+ source: string,
+) {
+ const finalKeyword = form.keyword.trim() || discoverSubscriptionKeyword(item)
+ return {
+ name: `${item.title} 自动订阅`,
+ feed_url: buildSiteSearchFeedURL(finalKeyword, source, buildSubscriptionAliases(item)),
+ filter: finalKeyword,
+ media_type: form.media_type || undefined,
+ media_category: form.media_category || undefined,
+ save_path: form.save_path || undefined,
+ search_mode: form.search_mode,
+ imdb_id: form.imdb_id || undefined,
+ source,
+ poster_url: item.poster_url || undefined,
+ backdrop_url: item.backdrop_url || undefined,
+ overview: item.overview || undefined,
+ original_name: item.original_name || undefined,
+ year: item.year || undefined,
+ resolution: form.resolution === 'best' ? 'best' : form.resolution,
+ quality: form.quality || undefined,
+ effects: form.effects || undefined,
+ release_groups: form.release_groups || undefined,
+ exclude_words: form.exclude_words || undefined,
+ wash_enabled: form.wash_enabled,
+ wash_priority: form.wash_priority,
+ priority: form.priority,
+ enabled: true,
+ }
+}
+
+export function apiErrorMessage(err: unknown, fallback: string): string {
+ return (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? fallback
+}