import { useMemo, useState } from 'react' import { Check, Minus, Search, X } from 'lucide-react' import type { ReaderSource } from '../../api/reader' /** * SearchScopeDialog 搜索范围弹窗(对应 legado 搜索页的 SearchScopeDialog)。 * * 只列出书源维度:默认全选所有「已启用」书源(对应 legado SearchScope("") 的 * 「全部书源」语义),取消勾选即可把本次搜索收敛到指定书源。已停用的书源会显示 * 但不可勾选,让用户知道它们不会被搜索。分组标题支持整组勾选/取消(kind 批量选择)。 * * 确认时若「全部启用书源」都被选中,回传空数组表示「全部」——与后端 * enabledSourcesForScope 的空范围语义一致,也让后续新增的书源自动纳入搜索。 */ export function SearchScopeDialog({ sources, selectedIds, onConfirm, onClose, }: { sources: ReaderSource[] /** 已保存的搜索范围;空数组 = 全部启用书源(默认全选)。 */ selectedIds: string[] onConfirm: (ids: string[]) => void onClose: () => void }) { const enabled = useMemo(() => sources.filter((s) => s.enabled), [sources]) const enabledIds = useMemo(() => enabled.map((s) => s.id), [enabled]) // 初始选中:保存过范围就用它(过滤掉已删除/停用的),否则默认全选已启用。 const [selected, setSelected] = useState>(() => { const valid = selectedIds.filter((id) => enabledIds.includes(id)) return new Set(valid.length > 0 ? valid : enabledIds) }) const [filter, setFilter] = useState('') const keyword = filter.trim().toLowerCase() const visible = useMemo( () => sources.filter( (s) => !keyword || s.name.toLowerCase().includes(keyword) || (s.group ?? '').toLowerCase().includes(keyword), ), [sources, keyword], ) // 按分组聚合并保持书源原顺序(ListSources 已按 customOrder 排好)。 const groups = useMemo(() => { const map = new Map() for (const s of visible) { const key = (s.group ?? '').trim() || '未分组' const list = map.get(key) if (list) list.push(s) else map.set(key, [s]) } return [...map.entries()].map(([name, items]) => ({ name, items })) }, [visible]) const pickedCount = enabledIds.filter((id) => selected.has(id)).length const allSelected = pickedCount === enabledIds.length && enabledIds.length > 0 const toggle = (id: string) => setSelected((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) const toggleGroup = (ids: string[]) => setSelected((prev) => { const next = new Set(prev) const allIn = ids.every((id) => next.has(id)) ids.forEach((id) => (allIn ? next.delete(id) : next.add(id))) return next }) // 全选/清空只作用于当前可见(可能被搜索词过滤)的已启用书源。 const visibleEnabledIds = visible.filter((s) => s.enabled).map((s) => s.id) const selectVisible = () => setSelected((prev) => new Set([...prev, ...visibleEnabledIds])) const clearVisible = () => setSelected((prev) => { const next = new Set(prev) visibleEnabledIds.forEach((id) => next.delete(id)) return next }) const confirm = () => { const picked = enabledIds.filter((id) => selected.has(id)) // 全选(含边界:启用源为空)→ 空数组 = 全部,新增书源自动纳入。 onConfirm(picked.length === enabledIds.length ? [] : picked) } return (
event.stopPropagation()} >

搜索范围

默认搜索全部启用书源 · 已启用 {enabledIds.length} 个

setFilter(e.target.value)} placeholder="过滤书源名 / 分组" className="w-full bg-transparent text-xs text-[var(--app-text)] outline-none placeholder:text-[var(--app-muted)]" />
{sources.length === 0 ? (

还没有书源,先去书源管理导入

) : groups.length === 0 ? (

没有匹配「{filter.trim()}」的书源

) : (
{groups.map((group) => { const groupEnabledIds = group.items.filter((s) => s.enabled).map((s) => s.id) const groupChecked = groupEnabledIds.length > 0 && groupEnabledIds.every((id) => selected.has(id)) const groupPartial = !groupChecked && groupEnabledIds.some((id) => selected.has(id)) return (
{group.items.map((src) => ( ))}
) })}
)}
{allSelected ? '全部书源' : `已选 ${pickedCount} / ${enabledIds.length} 个书源`}
) } /** 勾选标记(纯展示,点击由外层按钮承载,避免交互元素嵌套)。 */ function CheckMark({ checked, partial = false, disabled = false }: { checked: boolean; partial?: boolean; disabled?: boolean }) { return ( {checked && } {!checked && partial && } ) }