diff --git a/apps/frontend/web/app/layouts/Sidebar/sidebar.tsx b/apps/frontend/web/app/layouts/Sidebar/sidebar.tsx index ce70d840..14a7991f 100644 --- a/apps/frontend/web/app/layouts/Sidebar/sidebar.tsx +++ b/apps/frontend/web/app/layouts/Sidebar/sidebar.tsx @@ -78,15 +78,16 @@ const SidebarContent = memo(() => { - + ); }); -const QuickLinks = memo(() => { +const QuickLinks = memo((props: { collection: Collection }) => { + const { collection } = props; const location = useLocation(); - const match = useMemo(() => getActivePageTab(location), [location]); + const match = useMemo(() => getActivePageTab(location, collection), [location, collection]); const className = 'ml1 mr2 px1 py2 cursor-pointer select-none block text-sm text-base-700 flex items-center hover:bg-layer-subtle-overlay rounded-md'; const activeClassName = 'bg-layer-muted'; @@ -122,7 +123,9 @@ const QuickLinks = memo(() => { }); const Collection = memo((props: { collection: Collection }) => { + const location = useLocation(); const { collection } = props; + const match = useMemo(() => getActivePageTab(location, collection), [location, collection]); return (
@@ -147,6 +150,7 @@ const Collection = memo((props: { collection: Collection }) => { key={item.searchParams} collection={collection} item={item} + active={match === item.searchParams} > ))}
@@ -166,325 +170,330 @@ const Collection = memo((props: { collection: Collection }) => { ); }); -const CollectionItemContent = memo((props: { collection: Collection; item: CollectionItem }) => { - const { collection, item } = props; - const name = inferCollectionItemName(props.item); - const fansub = name.fansubs?.map((f) => f.name).join(' '); - const title = item.name - ? item.name - : name.title - ? name.title + (fansub ? ' 字幕组:' + fansub : '') - : name.text!; - const [collections, setCollections] = useAtom(collectionsAtom); - const display = useMemo(() => resolveFilterOptions(item), [item]); +const CollectionItemContent = memo( + (props: { collection: Collection; item: CollectionItem; active: boolean }) => { + const { collection, item, active } = props; + const name = inferCollectionItemName(props.item); + const fansub = name.fansubs?.map((f) => f.name).join(' '); + const title = item.name + ? item.name + : name.title + ? name.title + (fansub ? ' 字幕组:' + fansub : '') + : name.text!; + const [collections, setCollections] = useAtom(collectionsAtom); + const display = useMemo(() => resolveFilterOptions(item), [item]); - // --- Open state - const [tipOpen, setTipOpen] = useState(false); - const [menuOpen, setMenuOpen] = useState(false); + // --- Open state + const [tipOpen, setTipOpen] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); - const copyRSS = useCallback(async () => { - const feedURL = generateFeed(new URLSearchParams(item.searchParams)); - try { - if (!feedURL) throw new Error(`RSS URL is empty`); - await navigator.clipboard.writeText(`https://${APP_HOST}/feed.xml?filter=${feedURL}`); - toast.success('复制 RSS 订阅成功', { - dismissible: true, - duration: 3000, - closeButton: true - }); - } catch (error) { - console.error(error); - toast.error('复制 RSS 订阅失败', { closeButton: true }); - } - }, [item]); + const copyRSS = useCallback(async () => { + const feedURL = generateFeed(new URLSearchParams(item.searchParams)); + try { + if (!feedURL) throw new Error(`RSS URL is empty`); + await navigator.clipboard.writeText(`https://${APP_HOST}/feed.xml?filter=${feedURL}`); + toast.success('复制 RSS 订阅成功', { + dismissible: true, + duration: 3000, + closeButton: true + }); + } catch (error) { + console.error(error); + toast.error('复制 RSS 订阅失败', { closeButton: true }); + } + }, [item]); - const deleteItem = useCallback(() => { - const newCollections = collections.map((c) => { - if (c.name === collection.name) { - const idx = c.items.findIndex((i) => i.searchParams === item.searchParams); - if (idx !== -1) { - return { - ...c, - items: [...c.items.slice(0, idx), ...c.items.slice(idx + 1)] - }; + const deleteItem = useCallback(() => { + const newCollections = collections.map((c) => { + if (c.name === collection.name) { + const idx = c.items.findIndex((i) => i.searchParams === item.searchParams); + if (idx !== -1) { + return { + ...c, + items: [...c.items.slice(0, idx), ...c.items.slice(idx + 1)] + }; + } } - } - return c; - }); - setCollections(newCollections); - }, [collection, item, collections, setCollections]); + return c; + }); + setCollections(newCollections); + }, [collection, item, collections, setCollections]); - // --- Rename title - const titleRef = useRef(null); - const focusTime = useRef(); - const [editable, setEditable] = useState(false); - const focusTitle = useCallback(() => { - focusTime.current = new Date().getTime(); - const dom = titleRef.current; - dom?.focus(); - // 设置选区 - const selection = window.getSelection(); - if (dom && selection) { - selection.removeAllRanges(); - const range = document.createRange(); - range.selectNodeContents(dom); - range.collapse(false); - selection.addRange(range); - } - }, []); - const startRename = useCallback(() => { - if (editable) return; - setEditable(true); - setTimeout(() => { - focusTitle(); - }); - }, [titleRef, editable, setEditable]); - const commitRename = useCallback(() => { - const dom = titleRef.current; - if (!dom) return; - const newTitle = dom.textContent || title; - if (!newTitle) return; + // --- Rename title + const titleRef = useRef(null); + const focusTime = useRef(); + const [editable, setEditable] = useState(false); + const focusTitle = useCallback(() => { + focusTime.current = new Date().getTime(); + const dom = titleRef.current; + dom?.focus(); + // 设置选区 + const selection = window.getSelection(); + if (dom && selection) { + selection.removeAllRanges(); + const range = document.createRange(); + range.selectNodeContents(dom); + range.collapse(false); + selection.addRange(range); + } + }, []); + const startRename = useCallback(() => { + if (editable) return; + setEditable(true); + setTimeout(() => { + focusTitle(); + }); + }, [titleRef, editable, setEditable]); + const commitRename = useCallback(() => { + const dom = titleRef.current; + if (!dom) return; + const newTitle = dom.textContent || title; + if (!newTitle) return; - const newCollections = collections.map((c) => { - if (c.name === collection.name) { - const idx = c.items.findIndex((i) => i.searchParams === item.searchParams); - if (idx !== -1) { - return { - ...c, - items: [ - ...c.items.slice(0, idx), - { ...item, name: newTitle }, - ...c.items.slice(idx + 1) - ] - }; + const newCollections = collections.map((c) => { + if (c.name === collection.name) { + const idx = c.items.findIndex((i) => i.searchParams === item.searchParams); + if (idx !== -1) { + return { + ...c, + items: [ + ...c.items.slice(0, idx), + { ...item, name: newTitle }, + ...c.items.slice(idx + 1) + ] + }; + } + } + return c; + }); + setEditable(false); + setCollections(newCollections); + }, [setEditable, collection, item, title, collections, setCollections]); + const handleTitleKeydown = useCallback( + (e: React.KeyboardEvent) => { + if (!editable) return; + if (e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + commitRename(); + } + }, + [editable, commitRename] + ); + const handleTitleBlur = useCallback( + (e: React.FocusEvent) => { + if (!editable) return; + // Blur immediate after focus + if (new Date().getTime() - (focusTime.current ?? 0) < 200) { + focusTitle(); + return; } - } - return c; - }); - setEditable(false); - setCollections(newCollections); - }, [setEditable, collection, item, title, collections, setCollections]); - const handleTitleKeydown = useCallback( - (e: React.KeyboardEvent) => { - if (!editable) return; - if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); commitRename(); - } - }, - [editable, commitRename] - ); - const handleTitleBlur = useCallback( - (e: React.FocusEvent) => { - if (!editable) return; - // Blur immediate after focus - if (new Date().getTime() - (focusTime.current ?? 0) < 200) { - focusTitle(); - return; - } - e.preventDefault(); - e.stopPropagation(); - commitRename(); - }, - [editable, commitRename] - ); - // --- Rename title + }, + [editable, commitRename] + ); + // --- Rename title - return ( - - { - if (menuOpen || editable) { - setTipOpen(false); - } else { - setTipOpen(flag); - } - }} - > - - { - if (editable) { - e.preventDefault(); - e.stopPropagation(); - } - }} - > - - {title} - - { - setMenuOpen(flag); - setTipOpen(false); - }} - > - { + return ( + + { + if (menuOpen || editable) { + setTipOpen(false); + } else { + setTipOpen(flag); + } + }} + > + + { + if (editable) { e.preventDefault(); e.stopPropagation(); - setTipOpen(false); - }} + } + }} + > + - - - - - - - { - e.preventDefault(); - e.stopPropagation(); + {title} + + { + setMenuOpen(flag); + setTipOpen(false); }} > - - { - e.preventDefault(); - e.stopPropagation(); - window.open(`/resources/1${item.searchParams}`); - console.log('open', e); - }} - > - - 在新页面中打开 - - - copyRSS()}> - - 复制 RSS 订阅链接 - - - startRename()}> - - 重命名 - - - deleteItem()} + { + e.preventDefault(); + e.stopPropagation(); + setTipOpen(false); + }} > - - 删除 - - - - - - -
- {/*
搜索条件
*/} -
- {item.name && ( -
- 条件别名 - {item.name} -
- )} - {display.type && ( -
- 类型 - - {display.type.name} + + + + -
- )} - {display.search.length > 0 && ( -
- 标题搜索 - {display.search.map((text, idx) => ( - - {idx > 0 && |} - {text} + + { + e.preventDefault(); + e.stopPropagation(); + }} + > + + { + e.preventDefault(); + e.stopPropagation(); + window.open(`/resources/1${item.searchParams}`); + console.log('open', e); + }} + > + + 在新页面中打开 + + + copyRSS()}> + + 复制 RSS 订阅链接 + + + startRename()}> + + 重命名 + + + deleteItem()} + > + + 删除 + + + + + + +
+ {/*
搜索条件
*/} +
+ {item.name && ( +
+ 条件别名 + {item.name} +
+ )} + {display.type && ( +
+ 类型 + + {display.type.name} - ))} -
- )} - {display.include.length > 0 && ( -
- 标题匹配 - {display.include.map((text, idx) => ( - - {idx > 0 && |} - {text} +
+ )} + {display.search.length > 0 && ( +
+ 标题搜索 + {display.search.map((text, idx) => ( + + {idx > 0 && |} + {text} + + ))} +
+ )} + {display.include.length > 0 && ( +
+ 标题匹配 + {display.include.map((text, idx) => ( + + {idx > 0 && |} + {text} + + ))} +
+ )} + {display.keywords.length > 0 && ( +
+ 包含关键词 + {display.keywords.map((text, idx) => ( + + {idx > 0 && &} + {text} + + ))} +
+ )} + {display.exclude.length > 0 && ( +
+ 排除关键词 + {display.exclude.map((text) => ( + {text} + ))} +
+ )} + {display.fansubs && display.fansubs.length > 0 && ( +
+ 字幕组 + {display.fansubs.map((fansub) => ( + + {fansub.name} + + ))} +
+ )} + {display.after && ( +
+ 搜索开始于 + + {safeFormat(display.after, 'yyyy 年 M 月 d 日 hh:mm')} - ))} -
- )} - {display.keywords.length > 0 && ( -
- 包含关键词 - {display.keywords.map((text, idx) => ( - - {idx > 0 && &} - {text} +
+ )} + {display.before && ( +
+ 搜索结束于 + + {safeFormat(display.before, 'yyyy 年 M 月 d 日 hh:mm')} - ))} -
- )} - {display.exclude.length > 0 && ( -
- 排除关键词 - {display.exclude.map((text) => ( - {text} - ))} -
- )} - {display.fansubs && display.fansubs.length > 0 && ( -
- 字幕组 - {display.fansubs.map((fansub) => ( - - {fansub.name} - - ))} -
- )} - {display.after && ( -
- 搜索开始于 - - {safeFormat(display.after, 'yyyy 年 M 月 d 日 hh:mm')} - -
- )} - {display.before && ( -
- 搜索结束于 - - {safeFormat(display.before, 'yyyy 年 M 月 d 日 hh:mm')} - -
- )} +
+ )} +
-
- - - - ); -}); + + + + ); + } +); function inferCollectionItemName(item: CollectionItem) { let title; diff --git a/apps/frontend/web/app/utils/routes.ts b/apps/frontend/web/app/utils/routes.ts index cd57162b..3cc680a4 100644 --- a/apps/frontend/web/app/utils/routes.ts +++ b/apps/frontend/web/app/utils/routes.ts @@ -1,8 +1,15 @@ import { Location } from '@remix-run/react'; -export function getActivePageTab(location: Location) { +import { Collection } from '~/states/collection'; + +export function getActivePageTab(location: Location, collection: Collection) { const pathname = location.pathname; if (pathname.startsWith('/resources/')) { + for (const item of collection.items) { + if (location.search === item.searchParams) { + return item.searchParams; + } + } return 'resources'; } if (pathname === '/' || pathname === '') {