Files
feie9454andClaude Opus 5.5 b84fcec77b feat: 播放页按来源列表切换,新增作者/搜索/hashtag 侧栏
- 播放上下文:作品链接带上来源(搜索、标签、作者、hashtag),上一条/下一条
  在该列表内移动;当前作品不在列表中时退回全部作品的发布时间顺序
- 顶部提示标签显示当前列表与位置,点击打开侧栏列表,✕ 退出
- 侧栏改为「评论 | TA的作品 | 搜索结果/#hashtag」标签页,作品封面网格可就地切换
- 作者模式:打开「TA的作品」进入,关闭侧栏不退出,点提示 ✕ 后回到原来的列表
- 从搜索结果进入或点 hashtag 时自动打开对应列表
- 描述中的 hashtag 可点击,按完整 hashtag 精确匹配(#丰川祥子 不含 #丰川祥子cos)
- 新增 /api/playlist;搜索 SQL 抽到 lib/search-query.ts 与播放页共用,保证顺序一致
- 移动端:底部抽屉、点作品后收起,媒体区支持上下滑切换

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 00:50:09 +08:00

127 lines
4.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from "react";
/** comments:评论;works:TA的作品;list:搜索结果 / hashtag / 标签列表 */
export type SidePanelTab = "comments" | "works" | "list";
// 侧栏开关沿用评论区原来的 localStorage 键;停在哪个标签页、是否处于作者模式只在本次会话内记住,
// 这样下次从首页点进来不会莫名其妙停在作者模式。
// 切换作品时播放页会整体重新挂载,靠这些存储把状态带到下一个作品。
const OPEN_KEY = "aweme_player_comments_open";
const TAB_KEY = "aweme_player_panel_tab";
const AUTHOR_MODE_KEY = "aweme_player_author_mode";
function readStorage(storage: () => Storage, key: string): string | null {
try {
return storage().getItem(key);
} catch {
return null;
}
}
function writeStorage(
storage: () => Storage,
key: string,
value: string | null,
) {
try {
if (value === null) storage().removeItem(key);
else storage().setItem(key, value);
} catch {}
}
const local = () => window.localStorage;
const session = () => window.sessionStorage;
export function isPortraitViewport(): boolean {
return (
typeof window !== "undefined" &&
window.matchMedia("(orientation: portrait)").matches
);
}
/**
* 侧栏状态与作者模式。
* 作者模式和侧栏开关是分开的:打开「TA的作品」进入作者模式(上下切换在该作者的作品里移动),
* 关掉侧栏不会退出,只有点顶部提示的 ✕、或切到「搜索结果 / #hashtag」列表才退出。
*/
export function useSidePanelState(authorSecUid: string | undefined) {
const [open, setOpen] = useState(false);
const [tab, setTab] = useState<SidePanelTab>("comments");
// 处于作者模式时记下是哪位作者;切到别的作者的作品(例如浏览器后退)时自动不生效
const [authorModeFor, setAuthorModeFor] = useState<string | null>(null);
const [restored, setRestored] = useState(false);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setOpen(readStorage(local, OPEN_KEY) === "true");
const savedTab = readStorage(session, TAB_KEY);
setTab(savedTab === "works" || savedTab === "list" ? savedTab : "comments");
setAuthorModeFor(readStorage(session, AUTHOR_MODE_KEY));
setRestored(true);
// 恢复完成后再开启过渡动画,避免进入页面时侧栏滑入
requestAnimationFrame(() => {
requestAnimationFrame(() => setMounted(true));
});
}, []);
useEffect(() => {
if (!restored) return;
writeStorage(local, OPEN_KEY, String(open));
writeStorage(session, TAB_KEY, tab);
writeStorage(session, AUTHOR_MODE_KEY, authorModeFor);
}, [open, tab, authorModeFor, restored]);
// 切到哪个列表标签页,上下切换就改用哪个列表的顺序;「评论」不影响顺序
const applyTabMode = useCallback(
(nextTab: SidePanelTab) => {
if (nextTab === "works" && authorSecUid) setAuthorModeFor(authorSecUid);
else if (nextTab === "list") setAuthorModeFor(null);
},
[authorSecUid],
);
const selectTab = useCallback(
(nextTab: SidePanelTab) => {
setTab(nextTab);
applyTabMode(nextTab);
},
[applyTabMode],
);
const show = useCallback(
(nextTab: SidePanelTab) => {
selectTab(nextTab);
setOpen(true);
},
[selectTab],
);
const close = useCallback(() => setOpen(false), []);
/** 打开到指定标签页;已经打开在这个标签页时则关闭(关闭不影响顺序) */
const toggle = useCallback(
(nextTab: SidePanelTab) => {
if (open && tab === nextTab) close();
else show(nextTab);
},
[close, open, show, tab],
);
const exitAuthorMode = useCallback(() => setAuthorModeFor(null), []);
return {
open,
tab,
mounted,
/** 上下切换在当前作者的作品里移动 */
authorMode: Boolean(authorSecUid) && authorModeFor === authorSecUid,
/** 已从存储里恢复完状态(之后才能安全地自动打开侧栏) */
restored,
selectTab,
show,
close,
toggle,
exitAuthorMode,
};
}