feat: native player#97
Conversation
SafeDep Report SummaryPackage Details
This report is generated by SafeDep Github App |
Walkthrough将播放器与下载子系统从 react-native-track-player/本地实现迁移至原生 Orpheus:移除旧的播放器、下载服务与相关 schema,重写大量 Hook、工具与组件以使用 Orpheus API 与 react-query,同时新增若干迁移与构建配置。(≤50 字) Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant UI as 前端 UI
participant JS as JS 层 (hooks/utils)
participant Orpheus as Orpheus 原生模块
participant DB as 本地 DB / trackService
Note over UI,JS: 用户触发播放或队列操作
UI->>JS: 调用 addToQueue / convertToOrpheusTrack / playNext
JS->>DB: 可选:findOrCreateTrack (track metadata)
DB-->>JS: 返回 track details
JS->>Orpheus: Orpheus.addToQueue / Orpheus.play / Orpheus.playNext
Orpheus-->>JS: 触发 onTrackStarted/onPositionUpdate/onTrackFinished 事件
JS->>UI: Hooks / react-query 更新 -> 界面重渲染(NowPlaying、Queue、进度)
Note over Orpheus,JS: 播放期间进度与缓冲更新
Orpheus-->>JS: onPositionUpdate(position,duration,buffered)
JS->>UI: 更新进度条与歌词同步(useLyricSync/useTrackProgress)
Orpheus->>JS: onTrackFinished(trackId,duration,finalPosition)
JS->>DB: finalizeAndRecordCurrentTrack(trackId,duration,finalPosition)
DB-->>JS: 写入完成 -> Hooks/Queries invalidate/refetch
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 重点复核项:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/_layout.tsx (1)
96-117:Orpheus.setBilibiliCookie()must be awaited, andglobal.playerIsReadyis never set to true.Line 104 calls
Orpheus.setBilibiliCookie()withoutawait. Throughout the codebase, all other Orpheus methods are awaited (e.g.,await Orpheus.clear(),await Orpheus.play(),await Orpheus.seekTo()), which means this call is async. Withoutawait, errors insetBilibiliCookie()bypass the try-catch block and cause unhandled promise rejections.Additionally,
global.playerIsReadyis never set totrueon success, rendering the guard condition on line 97 ineffective. Every app start will re-execute the initialization logic.Suggested fix:
-const initializePlayer = () => { +const initializePlayer = async () => { if (!global.playerIsReady) { try { const cookie = useAppStore.getState().bilibiliCookie if (!cookie) { logger.info('没有 bilibili cookie,跳过播放器初始化') return } - Orpheus.setBilibiliCookie(serializeCookieObject(cookie)) + await Orpheus.setBilibiliCookie(serializeCookieObject(cookie)) + global.playerIsReady = true } catch (error) { logger.error('播放器初始化失败: ', error) reportErrorToSentry( error, '播放器初始化失败', ProjectScope.Player, ) global.playerIsReady = false } } } -void initializePlayer() +void initializePlayer()
♻️ Duplicate comments (2)
src/features/playlist/remote/toview/components/Item.tsx (1)
3-3: ToView 列表中同样存在每项各自订阅当前曲目的性能隐患这里的
useIsCurrentTrack(data.uniqueKey)与普通播放列表项实现一致,会为每个ToViewTrackListItem分别订阅 Orpheus 事件并调用getCurrentTrack。在分 P 视频列表较长时,同样会放大切歌时的 native 调用次数。可以和普通 PlaylistItem 一并考虑:
- 在列表级别维护一次当前曲目,然后在 item 里比较
uniqueKey;- 或让
useIsCurrentTrack基于共享的当前曲目状态实现。功能上没问题,只是后续有精力时可以做这类小优化。
Also applies to: 66-69
src/features/library/local/LocalPlaylistList.tsx (1)
3-3: 本地歌单列表中haveTrack的用法与多页列表类似,建议统一成更语义化的写法const haveTrack = useCurrentTrack() // ... contentContainerStyle={{ paddingBottom: haveTrack ? 70 : 10 }}这里同样是用“当前曲目对象是否存在”来控制底部留白,行为正确。但变量名与类型略不匹配,和其它使用
useCurrentTrack的组件相比也不够直观。建议与其它页面统一,例如:
const currentTrack = useCurrentTrack() // ... contentContainerStyle={{ paddingBottom: currentTrack ? 70 : 10 }}或者抽成
const hasTrack = !!useCurrentTrack(),便于一眼看出是布尔语义。Also applies to: 21-23, 100-102
🧹 Nitpick comments (18)
src/lib/player/progressListener.ts (1)
2-19: Orpheus 进度事件接入整体 OK,可考虑增强健壮性整体实现看起来没问题:在模块级别注册
onPositionUpdate监听,并通过playerProgressEmitter以 sticky 事件形式广播,结构也与Events['progress']对齐,同时补充了buffered字段,这对现有依赖这个 emitter 的 Hook 来说应该是兼容增强。有两个可以考虑的细节优化:
buffered的兜底处理(健壮性)
取决于@roitium/expo-orpheus的事件定义,如果e.buffered可能为undefined或null,下游在做进度百分比计算时容易得到NaN。可以用空值合并做个兜底,例如:Orpheus.addListener('onPositionUpdate', (e) => { playerProgressEmitter.emitSticky('progress', { position: e.position, duration: e.duration,
buffered: e.buffered,
})buffered: e.buffered ?? 0,
})
- 事件名与 payload 字段的 API 校验(类型/文档一致性)
- 建议确认
'onPositionUpdate'是否为 Orpheus 官方暴露的事件名,以及position/duration/buffered三个字段名与类型与文档保持一致。- 如果包里有类型定义(例如事件 Map 类型),可以给
e标注具体类型,避免后续 Orpheus 升级时静默 break。这些都属于可选增强,不影响当前合并,但后续有时间可以顺手补一下。
src/components/modals/player/SleepTimerModal.tsx (1)
38-41:handleSetTimer仅关闭弹窗,自定义输入仍做“伪校验”,行为略迷惑现在:
handleSetTimer = (_minutes: number) => { close('SleepTimer') },参数完全未使用;- 预设按钮点击时依然传入
minutes调用handleSetTimer(minutes);- 自定义输入中依然解析
customMinutes、校验> 0,但校验通过后同样只是调用handleSetTimer(minutes)→ 关闭弹窗。对用户而言:
- 选择/输入时间后没有任何定时效果,只是弹窗关闭;
- 对非法输入(空字符串、0、负数等),“设置”按钮静默无反应,看起来像是“点了没反应”的 Bug。
在当前 stub 阶段可以考虑:
- 要么在“设置”按钮点击时直接调用
close('SleepTimer'),去掉对minutes的解析和条件判断,降低迷惑性;- 要么保留解析逻辑,但在无实际定时行为的情况下用文案/Toast 更明确说明“当前不会真正生效”,避免给用户“已设置成功”的错觉;
- 如果是刻意保留接口形状,为后续接回真正的
setSleepTimer,可以在函数上加一行 TODO 注释(例如// TODO: 接 Orpheus 实现 sleep timer,恢复使用 minutes 参数)以提醒后续开发者。属于体验和可读性问题,不是阻塞性 bug,但建议在确定产品策略后顺手整理一下。
Also applies to: 62-73, 74-96
src/components/modals/PlayerQueueModal.tsx (1)
153-164: 建议:renderItem可能导致不必要的重渲染。
renderItem回调依赖于switchTrackHandler和removeTrackHandler,而这两个处理器在queue或refetch变化时会重新创建。这可能导致整个列表重渲染。考虑到
TrackItem已使用memo包裹,当前实现应该可以接受,但如果遇到性能问题,可以考虑将处理器稳定化。src/features/playlist/remote/components/PlaylistItem.tsx (1)
2-2: 在每个列表项中直接使用 useIsCurrentTrack 可能带来多重订阅与性能开销
useIsCurrentTrack(data.uniqueKey)会在每个TrackListItem实例中创建一个 Orpheus 监听和一次初始getCurrentTrack调用。对于包含大量歌曲的播放列表,这意味着一次切歌会触发 N 次 native 调用和状态更新。建议后续考虑:
- 在更高层(例如列表容器)用一个
useCurrentTrack或共享 context 维护当前曲目,再在 item 内做简单比较;或- 重构
useIsCurrentTrack,让它复用全局的当前曲目状态,而不是每次自己订阅onTrackTransition。这样可以保持现有高亮逻辑不变,同时避免列表规模增大时潜在的性能/功耗问题。
Also applies to: 61-65
src/features/playlist/local/components/LocalPlaylistHeader.tsx (1)
183-189:onPress={() => onClickPlayAll()}可以简化为直接传入回调这里外包一层箭头函数在行为上与
onPress={onClickPlayAll}等价,但会在每次渲染创建一个新函数,且当前签名中也不需要额外参数。如果没有特殊原因(例如需要显式捕获额外闭包变量),建议改回:
<Button mode='contained' icon='play' onPress={onClickPlayAll} >以减少不必要的函数创建并提升可读性。
src/app/(tabs)/settings.tsx (1)
4-4: 用useCurrentTrack作为有无曲目的判断是合理的,但命名可以更语义化目前:
const haveTrack = useCurrentTrack() // ... paddingBottom: haveTrack ? 70 : insets.bottom
useCurrentTrack返回Track | null,haveTrack实际上是“当前曲目对象”,而不是布尔值。运行上没问题,但阅读时容易误解类型。可以考虑两种微调(选其一即可):
显式转成布尔:
const hasTrack = !!useCurrentTrack() ... paddingBottom: hasTrack ? 70 : insets.bottom或者保留对象,但改名更贴近含义,例如:
const currentTrack = useCurrentTrack() ... paddingBottom: currentTrack ? 70 : insets.bottom纯可读性建议,不影响现有行为。
Also applies to: 35-46
src/features/library/multipage/MultiPageVideosList.tsx (1)
4-4: 使用useCurrentTrack控制列表底部内边距是合理的,命名可再明确一点const haveTrack = useCurrentTrack() // ... contentContainerStyle={{ paddingBottom: haveTrack ? 70 : 10 }}逻辑上“有当前曲目就多留出 70 像素”的意图是清晰的,不过:
haveTrack实际类型是Track | null,语义更接近“当前曲目对象”;- 后续维护者可能会误以为这是布尔值。
可以考虑改为:
const currentTrack = useCurrentTrack() const paddingBottom = currentTrack ? 70 : 10 // ... contentContainerStyle={{ paddingBottom }}或者直接
const hasTrack = !!useCurrentTrack(),按需选择。属于小的可读性优化。Also applies to: 25-27, 106-106
src/app/leaderboard.tsx (1)
60-60: 变量命名与实际类型不符。
haveTrack命名暗示这是一个布尔值,但useCurrentTrack()返回的是Track | null。虽然在 JavaScript 中作为布尔条件判断时可以正常工作,但建议重命名以提高代码可读性。- const haveTrack = useCurrentTrack() + const currentTrack = useCurrentTrack()对应 Line 125 也需要同步修改:
- paddingBottom: haveTrack ? 70 + insets.bottom : insets.bottom, + paddingBottom: currentTrack ? 70 + insets.bottom : insets.bottom,src/features/playlist/local/components/LocalPlaylistItem.tsx (1)
50-50: 潜在的性能问题:每个列表项都会触发独立的异步调用和事件订阅
useIsCurrentTrack钩子会为每个列表项创建独立的Orpheus.getCurrentTrack()调用和onTrackTransition事件监听器。当列表包含大量曲目时,这会导致:
- 初始渲染时触发 N 次异步调用
- 每次曲目切换时触发 N 次事件回调
建议考虑在父组件层级获取当前曲目 ID,然后通过 props 或 context 向下传递,以减少重复的订阅和调用。
src/hooks/player/useIsCurrentTrack.ts (2)
17-51: 重复逻辑可提取为辅助函数初始获取(Lines 20-36)和事件监听回调(Lines 38-51)中的曲目比较逻辑完全相同,违反 DRY 原则。
useEffect(() => { let isMounted = true + + const updateCurrentState = (currentTrack: { id: string } | null) => { + if (!isMounted) return + setIsCurrent(currentTrack?.id === trackUniqueKey) + } fetchTrack() - .then(({ currentTrack }) => { - if (isMounted) { - if (!currentTrack) { - setIsCurrent(false) - return - } - if (currentTrack.id === trackUniqueKey) { - setIsCurrent(true) - } else { - setIsCurrent(false) - } - } - }) + .then(({ currentTrack }) => updateCurrentState(currentTrack)) .catch(() => { // ignore }) const sub = Orpheus.addListener('onTrackTransition', async () => { const { currentTrack } = await fetchTrack() - if (isMounted) { - if (!currentTrack) { - setIsCurrent(false) - return - } - if (currentTrack.id === trackUniqueKey) { - setIsCurrent(true) - } else { - setIsCurrent(false) - } - } + updateCurrentState(currentTrack) })
7-15:fetchTrack函数可移至组件外部
fetchTrack不依赖任何组件状态或 props,可以移至模块顶层以避免每次渲染时重新创建。+const fetchTrack = async () => { + try { + const currentTrack = await Orpheus.getCurrentTrack() + return { currentTrack } + } catch (e) { + console.warn('Failed to fetch current track', e) + return { currentTrack: null } + } +} + export function useIsCurrentTrack(trackUniqueKey: string) { const [isCurrent, setIsCurrent] = useState(false) - - const fetchTrack = async () => { - try { - const currentTrack = await Orpheus.getCurrentTrack() - return { currentTrack } - } catch (e) { - console.warn('Failed to fetch current track', e) - return { currentTrack: null } - } - }src/hooks/player/useCurrentTrack.ts (2)
10-18: 与useIsCurrentTrack存在代码重复
fetchTrack函数在useIsCurrentTrack.ts和useCurrentTrack.ts中完全相同。建议提取为共享工具函数,或让useIsCurrentTrack基于useCurrentTrack实现。可考虑在共享位置创建工具函数:
// 例如在 src/hooks/player/utils.ts export async function fetchCurrentOrpheusTrack() { try { const currentTrack = await Orpheus.getCurrentTrack() return { currentTrack } } catch (e) { console.warn('Failed to fetch current track', e) return { currentTrack: null } } }或者让
useIsCurrentTrack内部使用useCurrentTrack:export function useIsCurrentTrack(trackUniqueKey: string) { const currentTrack = useCurrentTrack() return currentTrack?.uniqueKey === trackUniqueKey }
23-70: 初始获取和事件回调中的曲目解析逻辑重复Lines 23-47 和 Lines 49-70 的逻辑几乎相同,可提取为辅助函数以减少重复。
useEffect(() => { let isMounted = true + const resolveAndSetTrack = async (currentTrack: { id: string } | null) => { + if (!isMounted) return + if (!currentTrack) { + setTrack(null) + return + } + const internalTrack = await trackService.getTrackByUniqueKey(currentTrack.id) + if (!isMounted) return + if (internalTrack.isErr()) { + setTrack(null) + toastAndLogError('读取当前曲目信息失败', internalTrack.error, 'Hooks.useCurrentTrack') + return + } + setTrack(internalTrack.value) + } + fetchTrack() - .then(async ({ currentTrack }) => { - if (isMounted) { - // ... 23 lines of duplicate code - } - }) + .then(({ currentTrack }) => resolveAndSetTrack(currentTrack)) .catch(() => {}) const sub = Orpheus.addListener('onTrackTransition', async () => { const { currentTrack } = await fetchTrack() - if (isMounted) { - // ... same 17 lines - } + await resolveAndSetTrack(currentTrack) })src/features/player/hooks/usePlayerSlider.ts (2)
37-44: 缺少错误处理
Promise.all([Orpheus.getPosition(), Orpheus.getDuration()])失败时会静默失败。如果 Orpheus 尚未初始化或没有当前曲目,这些调用可能会抛出异常。useEffect(() => { - void Promise.all([Orpheus.getPosition(), Orpheus.getDuration()]).then( - ([position, duration]) => { - sharedPosition.set(position) - sharedDuration.set(duration) - }, - ) + Promise.all([Orpheus.getPosition(), Orpheus.getDuration()]) + .then(([position, duration]) => { + sharedPosition.set(position) + sharedDuration.set(duration) + }) + .catch((e) => { + console.warn('Failed to get initial progress', e) + }) }, [sharedDuration, sharedPosition])
58-74:Orpheus.seekTo缺少错误处理seek 操作失败时用户不会收到任何反馈,且
overridePosition仍会被设置,可能导致 UI 状态与实际播放位置不一致。const handleSlidingComplete = useCallback( async (value: number) => { void Haptics.performAndroidHapticsAsync( Haptics.AndroidHaptics.Gesture_End, ) overridePosition.set(value) - await Orpheus.seekTo(value) + try { + await Orpheus.seekTo(value) + } catch (e) { + console.warn('Seek failed', e) + } sharedPosition.set(value)src/features/player/components/PlayerControls.tsx (1)
96-108: 乐观更新缺少错误恢复机制
setRepeatMode(nextMode)在Orpheus.setRepeatMode(nextMode)之后立即调用,但如果 Orpheus 操作失败,本地状态不会回滚,导致 UI 与实际状态不一致。onPress={() => { void Haptics.performAndroidHapticsAsync( Haptics.AndroidHaptics.Confirm, ) const nextMode = repeatMode === RepeatMode.OFF ? RepeatMode.TRACK : repeatMode === RepeatMode.TRACK ? RepeatMode.QUEUE : RepeatMode.OFF - void Orpheus.setRepeatMode(nextMode) - setRepeatMode(nextMode) + Orpheus.setRepeatMode(nextMode) + .then(() => setRepeatMode(nextMode)) + .catch((e) => console.warn('Failed to set repeat mode', e)) }}src/utils/player.ts (2)
29-33: 建议清理注释掉的调试代码。多处调试日志被注释而非删除(Lines 29-33、44-51、76-80)。如果不再需要,建议移除以保持代码整洁。
363-378: 建议为addToQueue添加返回值以反馈操作结果。函数在多个分支静默返回(Lines 377、385、412),调用方无法得知操作是否成功。考虑返回布尔值或 Result 类型以便调用方处理失败情况。
async function addToQueue({ tracks, playNow, clearQueue, startFromKey, playNext, }: { tracks: Track[] playNow: boolean clearQueue: boolean startFromKey?: string playNext: boolean -}) { +}): Promise<boolean> { if (!tracks || tracks.length === 0) { - return + return false } // ... 其他分支类似处理
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (44)
index.js(0 hunks)package.json(2 hunks)src/app/(tabs)/settings.tsx(2 hunks)src/app/_layout.tsx(2 hunks)src/app/download.tsx(2 hunks)src/app/leaderboard.tsx(2 hunks)src/app/player.tsx(2 hunks)src/app/playlist/remote/toview.tsx(2 hunks)src/app/test.tsx(3 hunks)src/components/NowPlayingBar.tsx(6 hunks)src/components/modals/PlayerQueueModal.tsx(4 hunks)src/components/modals/player/SleepTimerModal.tsx(2 hunks)src/features/leaderboard/LeaderBoardItem.tsx(3 hunks)src/features/library/collection/CollectionList.tsx(2 hunks)src/features/library/favorite/FavoriteFolderList.tsx(2 hunks)src/features/library/local/LocalPlaylistList.tsx(2 hunks)src/features/library/multipage/MultiPageVideosList.tsx(2 hunks)src/features/player/components/PlayerControls.tsx(5 hunks)src/features/player/components/PlayerFunctionalMenu.tsx(1 hunks)src/features/player/components/PlayerLyrics.tsx(1 hunks)src/features/player/hooks/useLyricSync.ts(4 hunks)src/features/player/hooks/usePlayerSlider.ts(3 hunks)src/features/playlist/local/components/LocalPlaylistHeader.tsx(1 hunks)src/features/playlist/local/components/LocalPlaylistItem.tsx(2 hunks)src/features/playlist/local/components/LocalTrackList.tsx(2 hunks)src/features/playlist/local/hooks/useLocalPlaylistMenu.ts(2 hunks)src/features/playlist/local/hooks/useLocalPlaylistPlayer.ts(2 hunks)src/features/playlist/remote/components/PlaylistItem.tsx(2 hunks)src/features/playlist/remote/components/RemoteTrackList.tsx(2 hunks)src/features/playlist/remote/hooks/useRemotePlaylist.ts(1 hunks)src/features/playlist/remote/search-result/hooks/useSearchInteractions.ts(2 hunks)src/features/playlist/remote/toview/components/Item.tsx(2 hunks)src/hooks/player/useAnimatedTrackProgress.ts(2 hunks)src/hooks/player/useCurrentQueue.ts(0 hunks)src/hooks/player/useCurrentTrack.ts(1 hunks)src/hooks/player/useIsCurrentTrack.ts(1 hunks)src/hooks/player/useTrackProgress.ts(3 hunks)src/hooks/stores/usePlayerStore.ts(0 hunks)src/lib/player/playbackService.ts(0 hunks)src/lib/player/playerLogic.ts(0 hunks)src/lib/player/progressListener.ts(2 hunks)src/lib/services/downloadService.ts(0 hunks)src/types/core/playerStore.ts(0 hunks)src/utils/player.ts(5 hunks)
💤 Files with no reviewable changes (7)
- src/types/core/playerStore.ts
- src/lib/player/playerLogic.ts
- index.js
- src/lib/services/downloadService.ts
- src/lib/player/playbackService.ts
- src/hooks/stores/usePlayerStore.ts
- src/hooks/player/useCurrentQueue.ts
🧰 Additional context used
🧬 Code graph analysis (29)
src/features/player/components/PlayerFunctionalMenu.tsx (2)
src/hooks/stores/usePlayerStore.ts (1)
durationInSeconds(66-76)src/components/modals/app/UpdateAppModal.tsx (1)
UpdateAppModal(18-90)
src/features/playlist/remote/components/PlaylistItem.tsx (2)
src/hooks/player/useIsCurrentTrack.ts (1)
useIsCurrentTrack(4-60)src/hooks/stores/usePlayerStore.ts (7)
state(401-436)currentTrackUniqueKey(624-650)Track(83-88)track(403-405)index(719-824)track(689-717)state(254-262)
src/app/playlist/remote/toview.tsx (1)
src/hooks/stores/usePlayerStore.ts (2)
addToQueueParams(320-451)currentTrackUniqueKey(624-650)
src/features/library/collection/CollectionList.tsx (2)
src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)src/hooks/stores/usePlayerStore.ts (6)
state(401-436)track(403-405)track(689-717)currentTrackUniqueKey(624-650)track(354-354)Track(83-88)
src/features/playlist/remote/search-result/hooks/useSearchInteractions.ts (1)
src/hooks/stores/usePlayerStore.ts (9)
state(401-436)addToQueueParams(320-451)currentTrackUniqueKey(624-650)Sentry(454-530)Sentry(567-583)Sentry(532-565)track(403-405)state(254-262)state(283-295)
src/features/leaderboard/LeaderBoardItem.tsx (2)
src/hooks/player/useIsCurrentTrack.ts (1)
useIsCurrentTrack(4-60)src/hooks/stores/usePlayerStore.ts (1)
currentTrackUniqueKey(624-650)
src/features/library/multipage/MultiPageVideosList.tsx (1)
src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)
src/features/player/hooks/usePlayerSlider.ts (1)
src/hooks/stores/usePlayerStore.ts (5)
span(702-715)reason(96-224)Sentry(454-530)track(689-717)startAtPosition(841-927)
src/app/leaderboard.tsx (2)
src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)src/hooks/stores/usePlayerStore.ts (6)
track(689-717)track(403-405)track(354-354)Track(83-88)currentTrackUniqueKey(624-650)currentTrack(226-230)
src/features/playlist/remote/components/RemoteTrackList.tsx (1)
src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)
src/features/library/local/LocalPlaylistList.tsx (1)
src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)
src/features/playlist/local/components/LocalPlaylistItem.tsx (1)
src/hooks/player/useIsCurrentTrack.ts (1)
useIsCurrentTrack(4-60)
src/features/playlist/local/hooks/useLocalPlaylistPlayer.ts (2)
src/lib/db/schema.ts (1)
tracks(51-84)src/hooks/stores/usePlayerStore.ts (6)
addToQueueParams(320-451)state(401-436)Sentry(532-565)Sentry(454-530)currentTrackUniqueKey(624-650)Sentry(567-583)
src/hooks/player/useIsCurrentTrack.ts (1)
src/hooks/stores/usePlayerStore.ts (9)
track(354-354)track(689-717)currentTrackUniqueKey(624-650)track(403-405)Track(83-88)state(401-436)track(400-400)uniqueKey(232-307)currentTrack(226-230)
src/features/library/favorite/FavoriteFolderList.tsx (2)
src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)src/hooks/stores/usePlayerStore.ts (7)
state(401-436)track(689-717)currentTrackUniqueKey(624-650)track(403-405)track(354-354)state(830-834)Track(83-88)
src/app/(tabs)/settings.tsx (2)
src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)src/hooks/stores/usePlayerStore.ts (7)
state(401-436)track(403-405)track(689-717)Track(83-88)track(354-354)currentTrackUniqueKey(624-650)state(830-834)
src/features/player/components/PlayerControls.tsx (2)
src/types/core/appStore.ts (1)
AppState(31-31)src/hooks/stores/usePlayerStore.ts (5)
Sentry(532-565)state(401-436)Sentry(454-530)index(719-824)currentTrackUniqueKey(624-650)
src/components/NowPlayingBar.tsx (1)
src/hooks/stores/usePlayerStore.ts (3)
Sentry(454-530)activeList(538-563)Sentry(567-583)
src/features/playlist/remote/toview/components/Item.tsx (2)
src/hooks/player/useIsCurrentTrack.ts (1)
useIsCurrentTrack(4-60)src/hooks/stores/usePlayerStore.ts (6)
state(401-436)track(689-717)track(403-405)currentTrackUniqueKey(624-650)track(354-354)Track(83-88)
src/hooks/player/useAnimatedTrackProgress.ts (1)
src/hooks/stores/usePlayerStore.ts (3)
span(702-715)track(689-717)reason(96-224)
src/features/player/hooks/useLyricSync.ts (1)
src/hooks/stores/usePlayerStore.ts (2)
position(585-602)Sentry(567-583)
src/app/download.tsx (2)
src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)src/hooks/stores/usePlayerStore.ts (6)
state(401-436)track(689-717)track(403-405)currentTrackUniqueKey(624-650)Track(83-88)currentTrack(226-230)
src/app/test.tsx (2)
src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)src/hooks/stores/usePlayerStore.ts (8)
state(401-436)currentTrackUniqueKey(624-650)track(689-717)Sentry(454-530)logger(238-305)index(719-824)checkPlayerReady(339-449)track(403-405)
src/features/player/components/PlayerLyrics.tsx (1)
src/features/player/hooks/useLyricSync.ts (1)
useLyricSync(9-145)
src/hooks/player/useCurrentTrack.ts (4)
src/types/core/media.ts (1)
Track(72-72)src/lib/services/trackService.ts (1)
trackService(885-885)src/utils/error-handling.ts (1)
toastAndLogError(11-41)src/hooks/stores/usePlayerStore.ts (5)
track(689-717)Track(83-88)currentTrack(226-230)track(403-405)currentTrackUniqueKey(624-650)
src/features/playlist/local/components/LocalTrackList.tsx (2)
src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)src/hooks/stores/usePlayerStore.ts (8)
state(401-436)currentTrackUniqueKey(624-650)track(403-405)track(689-717)currentTrack(226-230)span(702-715)index(719-824)Track(83-88)
src/components/modals/PlayerQueueModal.tsx (2)
src/hooks/router/usePreventRemove.ts (1)
usePreventRemove(5-37)src/hooks/stores/usePlayerStore.ts (9)
state(401-436)currentTrackUniqueKey(624-650)addToQueueParams(320-451)index(719-824)activeList(725-822)activeList(538-563)Sentry(532-565)checkPlayerReady(339-449)logger(238-305)
src/app/player.tsx (1)
src/hooks/stores/usePlayerStore.ts (6)
track(689-717)span(702-715)currentTrack(226-230)index(719-824)track(403-405)Track(83-88)
src/app/_layout.tsx (1)
src/hooks/stores/useAppStore.ts (2)
useAppStore(57-249)serializeCookieObject(39-45)
🪛 Biome (2.1.2)
src/hooks/player/useTrackProgress.ts
[error] 70-70: Comparing to itself is potentially pointless.
(lint/suspicious/noSelfCompare)
[error] 97-97: Comparing to itself is potentially pointless.
(lint/suspicious/noSelfCompare)
🔇 Additional comments (31)
package.json (3)
4-5: 版本号提升合理,符合重大框架迁移。从 1.4.3 升至 2.0.0(versionCode 81 → 82)合理地反映了从 TrackPlayer 到 Orpheus 的重大播放器框架迁移。
116-116: 验证 zustand 是否在迁移后仍被使用。据 AI 摘要,本次迁移移除了本地 Zustand 播放器存储逻辑,改用 Orpheus API。请确认 zustand 是否在代码库中的其他地方使用,或是否可作为孤立依赖移除。
37-37: @roitium/expo-orpheus@0.2.2 is compatible with your project dependencies.The package exists on npm (latest: 0.2.2, published Dec 5, 2025) and declares wildcard peer dependencies for
expo,react, andreact-native, making it compatible with Expo ~54.0.21 and the current dependency tree. As a pre-1.0 package, the caret range^0.2.2could allow breaking changes in 0.3.0, but the package has no transitive dependencies, so version changes won't cause cascading issues. Since the author (Roitium) maintains both this package and BBPlayer, version alignment is controlled. The recent release date (34 minutes ago at time of check) may warrant testing to ensure stability in your environment.src/features/playlist/remote/search-result/hooks/useSearchInteractions.ts (3)
25-31: 确认参数兼容性。
addToQueue的调用参数结构与旧实现保持一致,这很好。请验证新的工具函数是否接受相同的参数对象结构,特别是:
tracks数组中的BilibiliTrack类型playNow和playNext的互斥逻辑startFromKey的行为(在 Orpheus 框架下是否正常工作)
33-33: 依赖数组更新正确。从依赖数组中移除
addToQueue是正确的做法。由于addToQueue现在是从模块直接导入的工具函数(而不是从 store 派生),它是一个稳定的引用,不需要包含在useCallback的依赖数组中。保留router依赖也是正确的。
4-4: The code is correctly implemented and requires no changes.The import of
addToQueuefrom@/utils/playeris appropriate.addToQueueis a stable utility function that doesn't depend on component props, state, or other hooks. Therefore, it should not be included in the dependency array at line 33—the current dependency array[router]is correct. The pattern is consistent across the codebase in all files that consumeaddToQueue.Likely an incorrect or invalid review comment.
src/features/player/hooks/useLyricSync.ts (2)
67-82: LGTM!迁移到 Orpheus.seekTo 正确。
handleJumpToLyric的逻辑保持不变,依赖数组正确地移除了seekTo(现在使用模块级别的 Orpheus API)。
108-118: LGTM!数据获取逻辑迁移正确。从
TrackPlayer.getProgress()返回对象改为Orpheus.getPosition()返回数字,数据处理逻辑相应调整正确。src/features/playlist/local/hooks/useLocalPlaylistPlayer.ts (1)
5-5: LGTM!正确迁移到模块级别的addToQueue。从
usePlayerStore迁移到@/utils/player的模块导入,依赖数组正确移除了addToQueue(模块级函数引用稳定)。Also applies to: 34-34
src/hooks/player/useAnimatedTrackProgress.ts (1)
39-49: LGTM!并行获取进度数据效率良好。使用
Promise.all并行调用三个 Orpheus API 是正确的优化方式,shared value 的.set()方法使用正确。src/components/modals/PlayerQueueModal.tsx (1)
133-149: LGTM!处理器迁移到索引方式正确。
switchTrackHandler和removeTrackHandler正确使用索引调用 Orpheus API,并在操作后触发refetch()更新 UI 状态。src/app/test.tsx (2)
33-33: LGTM - 使用 Hook 检测曲目存在性。将
haveTrack的来源从 store selector 改为useCurrentTrack()hook,与 PR 中其他组件的迁移模式一致。由于haveTrack在第 156 行用于真值检查(haveTrack ? 80 : 20),Track | null类型可以正确工作。
202-202:Orpheus.clear()clears only the playback queue; verify it replaces all originalresetStore()cleanup.The UI button explicitly labels this as "清空播放器队列" (Clear player queue), and the code shows
Orpheus.clear()is called conditionally whenclearQueue=true. This indicates the method clears only the queue. However, if the originalusePlayerStore.resetStore()also cleared repeat mode, shuffle mode, or other player state, those will not be reset byOrpheus.clear()alone—those states are managed separately viaOrpheus.setRepeatMode(),Orpheus.setShuffleMode(), etc. Confirm whether a full player state reset is still needed.src/features/playlist/remote/components/RemoteTrackList.tsx (1)
2-2: LGTM - 迁移到集中式的 Hook。与 PR 中其他组件保持一致,使用
useCurrentTrack()替代 store selector 来检测当前曲目。第 185 行的paddingBottom: haveTrack ? 70 + insets.bottom : insets.bottom可以正确处理Track | null类型。Also applies to: 120-120
src/app/download.tsx (1)
4-4: LGTM - 迁移模式一致。与项目中其他组件的重构保持一致,使用
useCurrentTrack()hook 进行曲目存在性检查。Also applies to: 30-30
src/features/library/collection/CollectionList.tsx (1)
4-4: LGTM - Hook 迁移正确。采用
useCurrentTrack()替代 store selector,与 PR 的整体迁移策略一致。Also applies to: 21-21
src/features/player/components/PlayerLyrics.tsx (1)
305-305: LGTM - 简化 Hook 调用。移除
seekTo参数后,useLyricSynchook 内部直接使用Orpheus.seekTo(见相关代码片段 src/features/player/hooks/useLyricSync.ts 第 70 行),使职责划分更清晰。这与 PR 从 TrackPlayer 迁移到 Orpheus 的整体架构一致。src/features/library/favorite/FavoriteFolderList.tsx (1)
4-4: LGTM - 一致的迁移实现。使用
useCurrentTrack()进行曲目检测,与项目中其他组件的重构方式保持一致。Also applies to: 23-23
src/features/playlist/local/components/LocalTrackList.tsx (1)
2-2: LGTM - 完成 Hook 迁移。与 PR 中其他组件保持一致,使用
useCurrentTrack()替代 store-based 检查。Also applies to: 94-94
src/features/player/components/PlayerFunctionalMenu.tsx (1)
123-124: Verify the status and scope of sleep timer feature removal.The sleep timer feature is completely disabled with placeholder implementations: PlayerFunctionalMenu shows a "not implemented" toast instead of opening the modal, and SleepTimerModal contains only commented-out code with an error message. All references to
sleepTimerEndAtexist only within comments, not in active code.Clarify whether this is a planned re-implementation for a future PR or if the entire feature (component, modal registration, and navigation type) should be removed.
src/app/player.tsx (1)
13-13: 切换到@roitium/expo-orpheus的useCurrentTrack后请确认返回数据结构这里从本地
useCurrentTrack切到了 Orpheus 包里的实现,并改为:const currentTrack = useCurrentTrack() const coverRef = useImage(currentTrack?.track?.artwork ?? '', { onError: () => void 0, })整体思路没问题,但有两个点建议确认:
@roitium/expo-orpheus的useCurrentTrack是否始终返回包含track.artwork字段的对象?如果返回值结构不同(例如直接是 track 而不是带track嵌套),这里会导致封面始终为空或抛异常。- 如果第三方库内部在没有当前曲目时返回
undefined或其它占位值,确保与本地逻辑(无曲目时回退为纯背景色)行为一致。建议在真机/模拟器上实际切换几首歌,确认背景渐变和封面联动正常。
Also applies to: 55-58
src/features/playlist/local/hooks/useLocalPlaylistMenu.ts (1)
9-11:playNext迁移到 Orpheus 队列 API 的实现整体合理,注意确认转换函数签名新的实现:
const playNext = useCallback(async (track: Track) => { try { const oTrack = convertToOrpheusTrack(track) if (oTrack.isErr()) { toastAndLogError('转换 Track 失败', oTrack.error, SCOPE) return } await Orpheus.playNext(oTrack.value) toast.success('添加到下一首播放成功') } catch (error) { toastAndLogError('添加到队列失败', error, SCOPE) } }, [])优点:
- 对转换失败和
playNext异常都有统一的toastAndLogError处理;- 将语义从“本地 store 入队”平滑迁移到 Orpheus 队列,接口更直接。
唯一需要确认的是
convertToOrpheusTrack的签名:
- 如果它是同步函数并返回
Result<OrpheusTrack, Error>,当前用法是正确的;- 如果未来演进为
Promise<Result<...>>,这里需要改成const oTrack = await convertToOrpheusTrack(track)。建议在本地快速测试“下一首播放”是否符合预期(含错误路径)。
Also applies to: 36-48
src/features/playlist/remote/hooks/useRemotePlaylist.ts (1)
6-14: 代码逻辑正确。
playTrack回调正确处理了playNext参数逻辑:当playNext=true时,playNow=false且playNext=true,将曲目插入到当前播放之后;反之则立即播放。依赖数组为空是正确的,因为addToQueue是稳定的模块级导入。src/components/NowPlayingBar.tsx (3)
167-167: 良好的空值处理。为
currentTrack.title添加'未知曲目'作为空值回退是合理的防御性编程实践。
87-87: LGTM!
Orpheus.skipToPrevious()和Orpheus.skipToNext()的调用符合迁移到 Orpheus 的整体模式。Also applies to: 111-111
96-102: The concern about a stale closure withisPlayingis unfounded. The gesture handlers (prevTap,playTap,nextTap) are defined directly in the component body without memoization (lines 81-112), meaning they are recreated on each render and capture the currentisPlayingvalue from that render cycle. When a gesture fires and callsscheduleOnRN(callback, isPlaying), theisPlayingvalue passed is from the current render cycle—not a stale captured value. This pattern is correct for react-native-reanimated architecture and does not present a closure staleness issue.Likely an incorrect or invalid review comment.
src/app/playlist/remote/toview.tsx (1)
92-104: 代码迁移正确。
handlePlayAll正确地从模块导入的addToQueue工具函数替代了之前的 store 方法。依赖数组[tracksData]是正确的,因为addToQueue是稳定的模块级导入,不需要包含在依赖中。src/features/leaderboard/LeaderBoardItem.tsx (2)
35-44: 迁移正确,startFromKey参数略显冗余。使用
useIsCurrentTrackhook 替代 store 的判断是正确的迁移。但当tracks数组只有一个元素且playNow: true时,startFromKey参数是多余的,因为播放器会默认从第一个(也是唯一的)曲目开始播放。不过保留它也不会造成问题,可以视为显式声明意图。
24-24: LGTM!使用
useIsCurrentTrack(item.track.uniqueKey)hook 替代之前从 store 读取currentTrackUniqueKey的方式,符合本次 PR 向 Orpheus 迁移的整体模式。src/features/player/components/PlayerControls.tsx (2)
30-63: 播放控制操作的整体实现良好迁移到 Orpheus API 的播放控制逻辑清晰,触觉反馈保留完好。
void用于标记有意忽略 Promise 返回值是合理的做法。
14-28:repeatModestate management differs fromshuffleModebecauseuseRepeatModehook is not exported by@roitium/expo-orpheusThe library exports
useIsPlayinganduseShuffleModebut notuseRepeatMode. The current implementation usinguseState+useEffectwith AppState listener is the appropriate pattern given the library's available exports. No changes needed.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/modals/player/SleepTimerModal.tsx (1)
26-45: 移除误导性的"暂未实现"提示Line 27 在 useEffect 中显示
toast.error('暂未实现'),但从代码来看睡眠定时器功能已完整实现(包括设置、取消、剩余时间显示等)。此错误提示会:
- 误导用户认为功能不可用,实际上功能正常工作
- 每次
sleepTimerEndAt变化时都会触发(不仅是挂载时),造成重复干扰- 与实际可用的完整 UI 和功能逻辑矛盾
如果功能确实已完全迁移并可用,应直接删除 Line 27;如果存在已知限制或部分功能未完成,请改用具体的提示文案(如"部分功能开发中")并使用
toast.info级别。useEffect(() => { - toast.error('暂未实现') if (sleepTimerEndAt) {
♻️ Duplicate comments (4)
src/components/modals/player/SleepTimerModal.tsx (1)
53-57: 此段的问题已在前一条评论中标注此函数与
handleSetTimer存在相同的错误处理缺失问题,已在 Lines 47-51 的评论中一并指出。src/utils/player.ts (2)
124-195:startFromKey的行为依赖原生层,建议在注释中进一步明确与playNow的关系JSDoc 中写明
startFromKey会“从指定 key 开始播放(并立即开始播放,无视 playNow)”,而实现里:
- 调用
Orpheus.addToEnd(orpheusTracks, startFromKey, clearQueue);- 并且只在
playNow && !startFromKey时手动调用Orpheus.play(),注释说明“原生层已经处理了 startFromKey 的播放逻辑”。也就是说:当传入
startFromKey时,是否“立即播放、无视 playNow”完全取决于 Orpheus 的实现。建议:
- 确认 Orpheus 在带
startFromKey时一定会自动开始播放;- 在 JSDoc 或代码注释中显式写明“依赖原生 Orpheus 行为,如果未来 native 行为变化,这里可能需要同时更新”。
否则,一旦原生实现或接口发生变更,这里会比较难排查行为偏差。
32-48: 响度信息被硬编码为 0,响度归一化可能失效当前
volume始终是{ measured_i: 0, target_i: 0 },原先从track.bilibiliMetadata.bilibiliStreamUrl.volume读取真实响度的逻辑被全部注释掉,这会让 Orpheus 无法进行有效的响度归一化(所有音轨“看上去”都是 0dB)。如果 B 站返回的数据结构里仍然有响度信息,建议恢复按真实值填充
volume(或者明确选择放弃响度归一化并删除相关字段和注释):- let url = '' - const volume = { - measured_i: 0, - target_i: 0, - } + let url = '' + let volume = { + measured_i: 0, + target_i: 0, + } @@ - if (track.source === 'bilibili') { + if (track.source === 'bilibili') { @@ - // volume = { - // measured_i: - // track.bilibiliMetadata.bilibiliStreamUrl.volume?.measured_i ?? 0, - // target_i: track.bilibiliMetadata.bilibiliStreamUrl.volume?.target_i ?? 0, - // } + volume = { + measured_i: + track.bilibiliMetadata.bilibiliStreamUrl?.volume?.measured_i ?? 0, + target_i: + track.bilibiliMetadata.bilibiliStreamUrl?.volume?.target_i ?? 0, + }如果新架构下已经不再维护
bilibiliStreamUrl.volume,也建议同步调整类型与注释,避免“看上去支持响度,但其实永远是 0”这种半残状态。src/components/modals/PlayerQueueModal.tsx (1)
133-149: Orpheus 操作缺少错误处理
Orpheus.skipTo和Orpheus.removeTrack调用可能会抛出异常,但当前实现没有 try-catch 包裹。如果这些操作失败,用户界面将不会给出任何反馈。建议添加错误处理:
const switchTrackHandler = useCallback( async (index: number) => { if (!queue) return if (index === -1) return - await Orpheus.skipTo(index) - void refetch() + try { + await Orpheus.skipTo(index) + await refetch() + } catch (error) { + console.error('切换曲目失败', error) + } }, [queue, refetch], ) const removeTrackHandler = useCallback( async (index: number) => { - await Orpheus.removeTrack(index) - void refetch() + try { + await Orpheus.removeTrack(index) + await refetch() + } catch (error) { + console.error('移除曲目失败', error) + } }, [refetch], )此外,这也解决了之前 review 中提到的
void refetch()可能导致静默失败的问题。
🧹 Nitpick comments (24)
src/components/modals/player/SleepTimerModal.tsx (1)
15-22: 建议适度放宽缓存配置当前
gcTime: 0和staleTime: 0的配置会导致查询数据完全不缓存且始终被视为过期,可能引发不必要的重复请求。考虑到睡眠定时器状态变化频率较低且此弹窗生命周期较短,可以适度放宽(例如staleTime: 1000或更长),以减少不必要的网络请求。src/features/playlist/local/hooks/useLocalPlaylistMenu.ts (1)
44-47: 考虑使 downloadState 参数必需。
downloadState参数是可选的,但在函数体内多处使用(第 86、88、92 行)。如果不传递此参数,下载相关菜单项的行为可能不正确。建议评估是否应该将此参数设为必需:
const menuFunctions = ( item: Track, - downloadState?: DownloadState, + downloadState: DownloadState, ): TrackMenuItem[] => {或者,在函数开头添加默认值处理,确保所有调用路径都能正确工作。
src/features/player/components/PlayerControls.tsx (1)
112-119: 考虑为循环模式切换添加错误处理。循环模式的切换使用了乐观更新(立即调用
setRepeatMode),但对Orpheus.setRepeatMode的调用是 fire-and-forget 的(使用void),如果调用失败,本地状态和实际状态可能不同步。建议添加错误处理:
onPress={() => { void Haptics.performAndroidHapticsAsync( Haptics.AndroidHaptics.Confirm, ) const nextMode = repeatMode === RepeatMode.OFF ? RepeatMode.TRACK : repeatMode === RepeatMode.TRACK ? RepeatMode.QUEUE : RepeatMode.OFF - void Orpheus.setRepeatMode(nextMode) - setRepeatMode(nextMode) + void Orpheus.setRepeatMode(nextMode) + .then(() => setRepeatMode(nextMode)) + .catch((error) => { + // 处理错误,可能需要回滚状态或显示错误提示 + console.error('设置循环模式失败:', error) + }) }}或者,如果 Orpheus 足够可靠,当前的乐观更新方式也是可接受的。
src/hooks/stores/useDownloadManagerStore.ts (1)
2-24: 下载进度事件映射整体合理,仅有命名与语义上的小建议
- 事件转发逻辑清晰,字段映射也和 Orpheus 事件一一对应,看起来可以直接被 UI/Hooks 消费。
eventListner拼写有点怪(少了一个e),如果后续有时间可以顺手改成eventListener以免困惑。percent直接使用event.percentDownloaded,建议确认下该值是 0–1 还是 0–100;如果对外有约定(例如统一用 0–1),可以在ProgressEvent的注释里明确一下,避免误用。src/utils/player.ts (1)
213-261: 完成播放记录的计算逻辑清晰,可以考虑补充边界条件说明这段
finalizeAndRecordCurrentTrack对播放完成度的计算比较严谨:
- 对
position/realDuration做了下限和上限裁剪;- 使用
max(90%, duration - 2s)作为完成阈值,兼顾短曲目和长曲目;- 仅在写入播放记录成功后才刷新排行榜查询并上报播放历史。
建议的微调点:
- 可以在函数注释里简单说明“完成阈值”的策略(90% 或少于 2 秒未播完),方便以后维护者理解为什么是这个数字;
trackService.addPlayRecordFromUniqueKey如果未来可能返回Ok(null)之类“软失败”,当前只用isErr()判断就看不到这种情况,后续如果接口有变更记得同步调整。整体实现本身是靠谱的。
src/hooks/player/useBatchDownloadStatus.ts (1)
1-13: 在ids为空时建议禁用查询,避免不必要的调用Hook 整体很简洁,不过当前即便
ids为空数组,也会发起一次Orpheus.getDownloadStatusByIds([])调用,可能既没意义还存在 API 约束风险。可以加一个
enabled条件,让空列表时直接跳过查询:export function useBatchDownloadStatus(ids: string[]) { return useQuery({ queryKey: ['batchDownloadStatus', ids], queryFn: async () => { return await Orpheus.getDownloadStatusByIds(ids) }, staleTime: 0, gcTime: 0, + enabled: ids.length > 0, }) }src/app/test.tsx (1)
4-195: 测试页使用 Orpheus 的逻辑基本可靠,可补充错误处理与文案微调
haveTrack = useCurrentTrack()直接用于三元表达式作为布尔值是 OK 的(有当前曲目时为对象、无曲目时为null)。- 清理下载缓存这块已经改为调用
Orpheus.removeAllDownloads(),并有 try/catch 和 toast,行为清晰;不过提示文案里仍然提到“数据库记录”,在当前 schema(已删track_downloads)下可能略有误导,可以考虑改成“下载记录及实际文件”等更泛化的描述。- “清空播放器队列”按钮的
onPress={() => Orpheus.clear()}没有处理异步错误,建议与前面下载清理保持一致,显式等待并捕获异常,例如:- onPress={() => Orpheus.clear()} + onPress={async () => { + try { + await Orpheus.clear() + logger.info('清空播放器队列成功') + } catch (error) { + toastAndLogError('清空播放器队列失败', error, 'TestPage') + } + }}src/features/playlist/remote/hooks/useRemotePlaylist.ts (1)
25-31:addToQueue的错误未被处理使用
void忽略了addToQueue的 Promise,如果入队失败,用户将不会收到任何反馈。考虑添加错误处理或使用await。- void addToQueue({ + await addToQueue({ tracks: [track], playNow: !playNext, clearQueue: false, playNext: playNext, startFromKey: track.uniqueKey, - }) + }).catch((e) => { + toastAndLogError('加入队列失败', e, 'UI.Playlist.Remote') + })src/hooks/player/useIsCurrentTrack.ts (2)
7-15: 将fetchTrack移入useEffect或使用useCallback
fetchTrack在组件顶层定义,每次渲染都会重新创建。虽然当前不会导致 bug(因为 effect 只在trackUniqueKey变化时重新执行),但将其移入useEffect内部更清晰,避免潜在的闭包问题。
21-51: 重复的状态更新逻辑可简化初始化获取和事件监听器中的比较逻辑完全相同,可以提取为辅助函数,并简化比较表达式。
useEffect(() => { let isMounted = true + const updateCurrentState = (currentTrack: { id: string } | null) => { + if (isMounted) { + setIsCurrent(currentTrack?.id === trackUniqueKey) + } + } + + const fetchAndUpdate = async () => { + const { currentTrack } = await fetchTrack() + updateCurrentState(currentTrack) + } - fetchTrack() - .then(({ currentTrack }) => { - if (isMounted) { - if (!currentTrack) { - setIsCurrent(false) - return - } - if (currentTrack.id === trackUniqueKey) { - setIsCurrent(true) - } else { - setIsCurrent(false) - } - } - }) - .catch(() => { - // ignore - }) + fetchAndUpdate().catch(() => {}) - const sub = Orpheus.addListener('onTrackStarted', async () => { - const { currentTrack } = await fetchTrack() - if (isMounted) { - if (!currentTrack) { - setIsCurrent(false) - return - } - if (currentTrack.id === trackUniqueKey) { - setIsCurrent(true) - } else { - setIsCurrent(false) - } - } - }) + const sub = Orpheus.addListener('onTrackStarted', () => { + fetchAndUpdate().catch(() => {}) + }) return () => { isMounted = false sub.remove() } }, [trackUniqueKey])src/app/(tabs)/settings.tsx (1)
256-261: 设置切换未等待 Orpheus 调用完成
Orpheus.setRestorePlaybackPositionEnabled的调用没有await,如果用户快速切换,可能导致状态不一致。建议处理异步调用:onValueChange={() => { - Orpheus.setRestorePlaybackPositionEnabled( + void Orpheus.setRestorePlaybackPositionEnabled( !enablePersistCurrentPosition, - ) + ).then(() => { + setEnablePersistCurrentPosition(!enablePersistCurrentPosition) + }) - setEnablePersistCurrentPosition(!enablePersistCurrentPosition) }}或者如果
setRestorePlaybackPositionEnabled是同步的,请忽略此建议。src/hooks/player/useCurrentTrack.ts (2)
10-18: 与useIsCurrentTrack相同的建议:将fetchTrack移入useEffect
fetchTrack函数在组件顶层定义,建议移入useEffect内部以保持一致性和清晰度。
23-47: 重复的 track 解析逻辑可提取为辅助函数初始化和事件处理中的 track 获取、解析、错误处理逻辑完全相同。提取辅助函数可减少代码重复:
useEffect(() => { let isMounted = true + const resolveAndSetTrack = async () => { + const { currentTrack } = await fetchTrack() + if (!isMounted) return + + if (!currentTrack) { + setTrack(null) + return + } + + const internalTrack = await trackService.getTrackByUniqueKey(currentTrack.id) + if (!isMounted) return + + if (internalTrack.isErr()) { + setTrack(null) + toastAndLogError('读取当前曲目信息失败', internalTrack.error, 'Hooks.useCurrentTrack') + return + } + setTrack(internalTrack.value) + } - fetchTrack() - .then(async ({ currentTrack }) => { - // ... 重复逻辑 - }) - .catch(() => {}) + resolveAndSetTrack().catch(() => {}) - const sub = Orpheus.addListener('onTrackStarted', async () => { - // ... 重复逻辑 - }) + const sub = Orpheus.addListener('onTrackStarted', () => { + resolveAndSetTrack().catch(() => {}) + }) return () => { isMounted = false sub.remove() } }, [])Also applies to: 49-70
src/features/player/components/PlayerFunctionalMenu.tsx (1)
27-29: 空字符串 key 可能导致无意义的查询当
currentTrack为null时,会传递空字符串''给useBatchDownloadStatus,这可能导致向 Orpheus 发起无意义的查询请求。建议在
currentTrack存在时才启用查询:const { data: downloadStatus } = useBatchDownloadStatus([ currentTrack?.uniqueKey ?? '', -]) +], { enabled: !!currentTrack })或者在 hook 内部处理空数组/空字符串的情况。
src/hooks/stores/useAppStore.ts (1)
242-247: 日志信息在无 cookie 时可能产生误导当
bilibiliCookie为null时,日志仍会输出"应用启动时自动设置 cookie 到 orpheus",但实际上并未执行任何设置操作。useAppStore.persist.onFinishHydration((state) => { if (state.bilibiliCookie) { Orpheus.setBilibiliCookie(serializeCookieObject(state.bilibiliCookie)) + logger.info('应用启动时自动设置 cookie 到 orpheus') } - logger.info('应用启动时自动设置 cookie 到 orpheus') })src/app/playlist/remote/toview.tsx (1)
110-116: addToQueue 错误未被处理
addToQueue使用void前缀忽略了 Promise 结果。如果队列添加失败,用户不会收到任何反馈。考虑添加错误处理或至少记录日志:
- void addToQueue({ - tracks: [track], - playNow: true, - clearQueue: false, - playNext: false, - }) + addToQueue({ + tracks: [track], + playNow: true, + clearQueue: false, + playNext: false, + }).catch((e) => { + toastAndLogError('添加到播放队列失败', e, 'UI.Playlist.Remote') + })src/features/playlist/local/components/LocalPlaylistHeader.tsx (1)
98-118: 批量下载缺少错误处理
Orpheus.multiDownload使用void忽略了 Promise,如果批量下载失败,用户不会收到任何错误提示。- void Orpheus.multiDownload( + Orpheus.multiDownload( tracksResult.value .filter((item) => item.source === 'bilibili' ? item.bilibiliMetadata.videoIsValid : true, ) .map((t) => { const url = getInternalPlayUri(t) if (!url) return return { id: t.uniqueKey, title: t.title, url: url, artist: t.artist?.name, artwork: t.coverUrl ?? undefined, duration: t.duration, } }) .filter((t) => !!t), - ) + ).catch((e) => { + toastAndLogError('批量下载失败', e, 'UI.Playlist.Local.Header') + })src/app/download.tsx (2)
24-35: 下载任务列表不会自动刷新
staleTime: 0意味着数据总是过期,但只有在组件重新挂载或手动调用refetch时才会重新获取。当下载任务状态变化(完成/失败)时,列表不会自动更新。考虑添加轮询或监听 Orpheus 事件来刷新列表:
const { data: tasks, isPending, isError, error, + refetch, } = useQuery({ queryKey: ['downloadTasks'], queryFn: async () => { return await Orpheus.getUncompletedDownloadTasks() }, staleTime: 0, + refetchInterval: 3000, // 每3秒刷新一次 })或者通过 Orpheus 事件监听来触发
refetch。
37-37: 变量命名可以更清晰
haveTrack实际上是Track | null类型,但命名暗示它是布尔值。建议使用currentTrack更符合其类型。-const haveTrack = useCurrentTrack() +const currentTrack = useCurrentTrack()然后在 line 84 使用
currentTrack作为 truthy 检查。src/features/playlist/local/components/LocalPlaylistItem.tsx (1)
5-5: 类型命名冲突
DownloadState从@roitium/expo-orpheus导入,但src/types/core/downloadManagerStore.ts中也定义了一个名为DownloadState的接口(包含downloads和maxConcurrentDownloads字段)。虽然当前文件使用的是正确的枚举类型,但这种命名冲突可能会在其他地方导致混淆或误用。建议在后续迭代中考虑重命名本地的
DownloadState接口,例如改为DownloadManagerState,以避免与 Orpheus 库的枚举类型混淆。src/features/playlist/local/components/LocalTrackList.tsx (2)
109-109: 变量命名可改进
haveTrack命名暗示是布尔值,但实际上是Track | null类型。建议重命名为currentTrack以更准确地反映其类型和用途。- const haveTrack = useCurrentTrack() + const currentTrack = useCurrentTrack()然后更新第 178 行的使用:
- paddingBottom: haveTrack ? 70 + insets.bottom : insets.bottom, + paddingBottom: currentTrack ? 70 + insets.bottom : insets.bottom,
112-113: 查询键稳定性
ids数组在每次渲染时都会重新创建。由于useBatchDownloadStatus的queryKey包含ids数组,这可能导致在 tracks 引用变化但内容相同时触发不必要的重新查询。考虑使用
useMemo稳定化 ids 数组:- const ids = tracks.map((t) => t.uniqueKey) + const ids = useMemo(() => tracks.map((t) => t.uniqueKey), [tracks])src/features/downloads/DownloadTaskItem.tsx (2)
32-44: 事件监听器依赖数组可优化依赖数组中包含
task.state,这意味着每当状态变化时(例如从 QUEUED 到 DOWNLOADING),事件监听器都会被注销并重新注册。这是不必要的,因为 handler 内部已经处理了状态比较。将
task.state从依赖数组中移除,使用 ref 来访问最新状态:+ const taskStateRef = useRef(task.state) + taskStateRef.current = task.state + useEffect(() => { const handler = (e: ProgressEvent['progress:uniqueKey']) => { sharedProgress.value = e.percent - if (e.state !== task.state) { + if (e.state !== taskStateRef.current) { setTask((task) => ({ ...task, state: e.state })) } } eventListner.on(`progress:${task.id}`, handler) return () => { eventListner.off(`progress:${task.id}`, handler) } - }, [task.id, sharedProgress, task.state, setTask]) + }, [task.id, sharedProgress, setTask])
142-144: 冗余的条件表达式
marginRight: task.state === DownloadState.FAILED ? 0 : 0结果始终为 0,条件判断是多余的。- <View - style={{ marginRight: task.state === DownloadState.FAILED ? 0 : 0 }} - > + <View>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (45)
README.md(1 hunks)app.config.ts(1 hunks)docs/notes.md(1 hunks)drizzle/0012_blushing_human_fly.sql(1 hunks)drizzle/meta/0012_snapshot.json(1 hunks)drizzle/meta/_journal.json(1 hunks)drizzle/migrations.js(2 hunks)index.js(1 hunks)mise.toml(1 hunks)package.json(3 hunks)src/app/(tabs)/settings.tsx(6 hunks)src/app/download.tsx(2 hunks)src/app/playlist/local/[id].tsx(1 hunks)src/app/playlist/remote/toview.tsx(4 hunks)src/app/test.tsx(4 hunks)src/components/NowPlayingBar.tsx(6 hunks)src/components/modals/PlayerQueueModal.tsx(5 hunks)src/components/modals/player/SleepTimerModal.tsx(4 hunks)src/features/downloads/DownloadHeader.tsx(0 hunks)src/features/downloads/DownloadTaskItem.tsx(7 hunks)src/features/player/components/PlayerControls.tsx(4 hunks)src/features/player/components/PlayerFunctionalMenu.tsx(4 hunks)src/features/player/components/PlayerHeader.tsx(3 hunks)src/features/playlist/local/components/LocalPlaylistHeader.tsx(3 hunks)src/features/playlist/local/components/LocalPlaylistItem.tsx(4 hunks)src/features/playlist/local/components/LocalTrackList.tsx(9 hunks)src/features/playlist/local/hooks/useLocalPlaylistMenu.ts(3 hunks)src/features/playlist/local/hooks/useLocalPlaylistPlayer.ts(2 hunks)src/features/playlist/remote/hooks/useRemotePlaylist.ts(2 hunks)src/features/playlist/remote/search-result/hooks/useSearchInteractions.ts(3 hunks)src/hooks/player/useBatchDownloadStatus.ts(1 hunks)src/hooks/player/useCurrentTrack.ts(1 hunks)src/hooks/player/useIsCurrentTrack.ts(1 hunks)src/hooks/stores/useAppStore.ts(4 hunks)src/hooks/stores/useDownloadManagerStore.ts(1 hunks)src/lib/db/schema.ts(0 hunks)src/lib/facades/sync.ts(2 hunks)src/lib/services/artistService.ts(0 hunks)src/lib/services/downloadService.ts(0 hunks)src/lib/services/playlistService.ts(0 hunks)src/lib/services/trackService.ts(0 hunks)src/types/core/appStore.ts(0 hunks)src/types/core/media.ts(0 hunks)src/types/rntp.ts(0 hunks)src/utils/player.ts(3 hunks)
💤 Files with no reviewable changes (9)
- src/features/downloads/DownloadHeader.tsx
- src/lib/db/schema.ts
- src/lib/services/artistService.ts
- src/types/core/media.ts
- src/types/core/appStore.ts
- src/lib/services/playlistService.ts
- src/lib/services/downloadService.ts
- src/types/rntp.ts
- src/lib/services/trackService.ts
✅ Files skipped from review due to trivial changes (1)
- docs/notes.md
🚧 Files skipped from review as they are similar to previous changes (1)
- src/features/playlist/remote/search-result/hooks/useSearchInteractions.ts
🧰 Additional context used
🧬 Code graph analysis (23)
src/hooks/player/useBatchDownloadStatus.ts (1)
src/hooks/stores/usePlayerStore.ts (4)
uniqueKey(826-835)state(401-436)track(689-717)state(830-834)
src/features/playlist/local/components/LocalPlaylistHeader.tsx (3)
src/lib/services/playlistService.ts (1)
playlistService(1040-1040)src/utils/player.ts (1)
getInternalPlayUri(267-267)src/hooks/stores/useModalStore.ts (1)
useModalStore(29-72)
src/components/modals/player/SleepTimerModal.tsx (1)
src/hooks/stores/usePlayerStore.ts (2)
durationInSeconds(66-76)state(935-941)
src/features/player/components/PlayerHeader.tsx (2)
src/hooks/player/useBatchDownloadStatus.ts (1)
useBatchDownloadStatus(4-13)src/types/core/downloadManagerStore.ts (1)
DownloadState(9-12)
src/features/playlist/local/components/LocalPlaylistItem.tsx (3)
src/types/core/downloadManagerStore.ts (1)
DownloadState(9-12)src/hooks/player/useIsCurrentTrack.ts (1)
useIsCurrentTrack(4-60)src/hooks/stores/usePlayerStore.ts (6)
state(401-436)state(830-834)currentTrackUniqueKey(624-650)track(689-717)uniqueKey(826-835)state(254-262)
src/features/playlist/local/components/LocalTrackList.tsx (5)
src/types/core/downloadManagerStore.ts (1)
DownloadState(9-12)src/features/playlist/remote/components/PlaylistItem.tsx (1)
TrackMenuItem(9-13)src/features/playlist/remote/toview/components/Item.tsx (1)
TrackMenuItem(14-18)src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)src/hooks/player/useBatchDownloadStatus.ts (1)
useBatchDownloadStatus(4-13)
index.js (2)
src/utils/player.ts (1)
finalizeAndRecordCurrentTrack(266-266)src/hooks/stores/usePlayerStore.ts (3)
track(689-717)reason(96-224)index(719-824)
src/features/player/components/PlayerFunctionalMenu.tsx (3)
src/hooks/player/useBatchDownloadStatus.ts (1)
useBatchDownloadStatus(4-13)src/utils/player.ts (1)
getInternalPlayUri(267-267)src/types/core/downloadManagerStore.ts (1)
DownloadState(9-12)
src/lib/facades/sync.ts (3)
src/types/core/media.ts (1)
Track(64-64)src/lib/errors/service.ts (1)
createValidationError(70-75)src/lib/services/trackService.ts (5)
newTrack(140-188)db(179-183)track(338-351)db(211-220)db(523-527)
src/hooks/player/useIsCurrentTrack.ts (1)
src/hooks/stores/usePlayerStore.ts (9)
track(354-354)track(689-717)Track(83-88)track(403-405)currentTrackUniqueKey(624-650)track(400-400)state(401-436)span(702-715)uniqueKey(232-307)
src/utils/player.ts (8)
src/types/core/media.ts (1)
Track(64-64)src/lib/errors/thirdparty/bilibili.ts (1)
BilibiliApiError(25-50)src/lib/errors/player.ts (1)
PlayerError(10-17)src/lib/services/trackService.ts (3)
trackService(819-819)formatTrack(72-108)title(506-517)src/lib/db/schema.ts (1)
tracks(51-84)src/utils/log.ts (1)
flatErrorMessage(111-129)src/lib/config/queryClient.ts (1)
queryClient(9-45)src/hooks/queries/db/track.ts (1)
trackKeys(11-32)
src/features/player/components/PlayerControls.tsx (2)
src/types/core/appStore.ts (1)
AppState(29-29)src/hooks/stores/usePlayerStore.ts (6)
activeList(538-563)state(401-436)Sentry(532-565)Sentry(454-530)index(719-824)currentTrackUniqueKey(624-650)
src/features/downloads/DownloadTaskItem.tsx (2)
src/types/core/downloadManagerStore.ts (2)
DownloadTask(1-7)DownloadState(9-12)src/hooks/stores/useDownloadManagerStore.ts (2)
ProgressEvent(5-13)eventListner(14-14)
src/features/playlist/local/hooks/useLocalPlaylistMenu.ts (3)
src/utils/player.ts (2)
convertToOrpheusTrack(265-265)getInternalPlayUri(267-267)src/types/core/downloadManagerStore.ts (1)
DownloadState(9-12)src/features/playlist/local/components/LocalPlaylistItem.tsx (1)
TrackMenuItem(12-17)
src/features/playlist/remote/hooks/useRemotePlaylist.ts (4)
src/types/core/media.ts (1)
BilibiliTrack(30-55)src/lib/facades/sync.ts (1)
syncFacade(703-709)src/utils/log.ts (1)
reportErrorToSentry(138-164)src/hooks/stores/usePlayerStore.ts (9)
addToQueueParams(320-451)state(401-436)checkPlayerReady(339-449)track(689-717)Sentry(454-530)currentTrackUniqueKey(624-650)index(719-824)Sentry(567-583)logger(238-305)
src/app/playlist/local/[id].tsx (1)
src/features/playlist/local/hooks/useLocalPlaylistPlayer.ts (1)
useLocalPlaylistPlayer(13-72)
src/hooks/player/useCurrentTrack.ts (3)
src/types/core/media.ts (1)
Track(64-64)src/lib/services/trackService.ts (1)
trackService(819-819)src/utils/error-handling.ts (1)
toastAndLogError(11-41)
src/features/playlist/local/hooks/useLocalPlaylistPlayer.ts (5)
src/utils/mmkv.ts (1)
storage(7-7)src/lib/services/playlistService.ts (1)
playlistService(1040-1040)src/utils/error-handling.ts (1)
toastAndLogError(11-41)src/lib/db/schema.ts (1)
tracks(51-84)src/hooks/stores/usePlayerStore.ts (6)
addToQueueParams(320-451)state(401-436)index(719-824)track(689-717)currentTrackUniqueKey(624-650)Sentry(532-565)
src/app/(tabs)/settings.tsx (1)
src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)
app.config.ts (2)
expo-plugins/withAndroidGradleProperties.js (1)
config(15-33)expo-plugins/withAndroidPlugin.js (1)
config(5-46)
drizzle/meta/0012_snapshot.json (1)
src/lib/services/trackService.ts (1)
db(211-220)
src/hooks/stores/useDownloadManagerStore.ts (1)
src/types/core/downloadManagerStore.ts (1)
DownloadState(9-12)
src/app/download.tsx (2)
src/types/core/downloadManagerStore.ts (1)
DownloadTask(1-7)src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)
🔇 Additional comments (38)
mise.toml (1)
9-12: 新增构建任务结构清晰,与现有任务保持一致。
buildpreview任务的配置遵循现有的buildprod和builddev任务的结构和模式:环境变量设置恰当,输出路径命名规范,与其他任务保持一致。NODE_ENV 设置为 "development" 符合预发布场景的要求。请确认 EAS 构建系统中已配置 "preview" profile,使该任务能够正确执行。
src/components/modals/player/SleepTimerModal.tsx (2)
3-5: 导入语句符合 Orpheus 迁移需求新增的 toast、Orpheus 和 useQuery 导入支持了从本地 store 向 Orpheus API 的迁移,实现合理。
98-103: 自定义输入的异步处理正确将
onPress改为 async 函数并 awaithandleSetTimer是正确的做法,配合输入验证逻辑合理。假设handleSetTimer的错误处理问题得到修复(见前述评论),此处实现无问题。README.md (1)
35-35: 文档更新准确。播放库的更新已正确记录在技术栈部分,与 PR 的整体迁移保持一致。
app.config.ts (1)
103-104: 确认禁用发布构建优化的原因。将
enableMinifyInReleaseBuilds和enableShrinkResourcesInReleaseBuilds设置为false会显著增加 APK 大小并可能影响性能。请确认:
- 这是 Orpheus 集成所必需的吗?
- 这是临时调试措施还是永久变更?
- 是否有其他解决方案可以在保持优化的同时支持 Orpheus?
如果这是永久变更,建议在 PR 描述或代码注释中记录原因,以便未来维护时参考。
package.json (3)
4-5: 版本号更新合理。主版本号从 1.4.3 升级到 2.0.4 正确反映了这是一个包含破坏性变更的重大更新(迁移到 Orpheus)。
38-38: Verify package availability in npm registry.The package
@roitium/expo-orpheus@^0.4.7was not found in the public npm registry as of December 2025. Confirm whether this is a private package, published to a different registry, or if the package name/version is correct. If it is private, ensure the build environment has appropriate access configured and document the registry source in the project.
99-99: Document the purpose of the react-native-logs fork.The dependency uses a custom fork (v5.6.2) that does not exist in the official upstream (official npm: v5.3.0, official GitHub: v5.5.0). The custom versions v5.6.0, v5.6.1, and v5.6.2 exist only in the bbplayer-app fork and indicate divergence from upstream.
Add comments to
package.jsonor documentation explaining:
- What modifications this fork includes
- Whether upstream contributions are planned
- Why the custom fork is necessary
src/features/player/components/PlayerHeader.tsx (1)
37-39: 下载状态检查逻辑正确。使用 Orpheus 的
DownloadState.COMPLETED枚举值进行状态检查是正确的实现方式。src/lib/facades/sync.ts (1)
84-104: 新方法实现良好。
addTrackToLocal方法的实现符合最佳实践:
- 参数验证清晰(检查 artist 是否存在)
- 使用 ResultAsync 模式进行错误处理
- 正确链接了 artist 和 track 的创建流程
- 错误信息有意义
此方法为单曲本地持久化提供了良好的抽象。
src/features/playlist/local/hooks/useLocalPlaylistMenu.ts (2)
30-42: 播放下一首的实现正确。重构后的
playNext函数正确使用了 Orpheus API:
- 使用
convertToOrpheusTrack进行类型转换- 完善的错误处理和用户反馈
- 正确使用 Result 模式处理转换失败
100-108: 良好的防御性编程。在调用
Orpheus.downloadTrack之前验证内部播放 URI 是否存在,这是一个很好的防御性编程实践,避免了潜在的运行时错误。src/features/player/components/PlayerControls.tsx (3)
16-23: Orpheus 状态集成正确。使用
useIsPlaying、usePlaybackState和useQuery获取播放器状态是正确的实现方式。将shuffleMode的staleTime和gcTime设置为 0 确保了实时同步。
29-39: AppState 监听器实现良好。正确使用
useEffect监听应用状态变化并同步repeatMode,且在清理函数中移除了监听器,避免内存泄漏。
82-90: 随机播放切换实现正确。使用
async/await正确等待 Orpheus API 调用完成,然后通过refetchShuffleMode同步状态,确保了状态一致性。drizzle/0012_blushing_human_fly.sql (1)
1-1: This review comment addresses a non-issue and can be dismissed.The
track_downloadstable drop is not a breaking change affecting users. Both migrations 0011 (table creation) and 0012 (table drop) were introduced together in the initial project commit, with no prior releases. No production users will ever have data in this table when migration 0012 executes—the migrations are applied sequentially on first app startup.Additionally, the table drop is intentional architectural change: the old SQLite-based download tracking (
track_downloads) is being replaced by Orpheus's native download management, which the codebase already uses throughout (Orpheus.downloadTrack, Orpheus.getDownloadStatusByIds, etc.). No data migration is needed because Orpheus maintains its own download state independently.Likely an incorrect or invalid review comment.
index.js (1)
1-22: Orpheus 事件监听整体合理,建议确认事件字段和时间单位
onPlayerError:直接使用error.message和error.code做 toast,非常直观;建议确认 Orpheus 该事件的类型签名是否总是包含这两个字段,否则可在 UI 层对缺失字段做个兜底(例如将整个对象JSON.stringify一下)。onTrackFinished:将event.trackId/event.duration/event.finalPosition直接传给finalizeAndRecordCurrentTrack,逻辑上没问题,但需要确认:
trackId是否就是之前convertToOrpheusTrack里设置的uniqueKey;duration与finalPosition的单位(秒/毫秒)与finalizeAndRecordCurrentTrack中按“秒”计算的逻辑保持一致。这些如果在 Orpheus SDK 文档里已经保证,可以在注释中简单说明一下,方便以后维护。
drizzle/meta/_journal.json (1)
88-95: Drizzle 迁移日志条目看起来一致新增的
idx: 12 / tag: "0012_blushing_human_fly"与前面条目的结构和版本号保持一致,能够正常被drizzle/migrations.js中的journal使用,没有问题。drizzle/migrations.js (1)
15-34: 新迁移m0012注册方式正确
m0012已正确引入并加入migrations映射,形式与之前的迁移保持一致,可以被现有迁移管线正常执行。建议确认对应的
0012_blushing_human_fly.sql与 snapshot/_journal 一致(目前从其它文件看是删除track_downloads表,符合整体改动方向)。drizzle/meta/0012_snapshot.json (1)
1-516: 数据库快照与迁移目标一致该 snapshot 展示的表结构(
artists/bilibili_metadata/local_metadata/playlists/playlist_tracks/tracks)中已经不再包含track_downloads表,和本次新增的0012迁移(删除下载表)目标一致,字段与索引结构也和现有 TypeScript schema 片段对得上,看起来是由 Drizzle 正常生成的快照。src/app/playlist/local/[id].tsx (1)
126-126: 接口变更看起来正确
useLocalPlaylistPlayer现在接收playlistId而非播放列表内容数组,hook 内部负责获取和过滤轨道数据。这是一个更清晰的职责分离。src/components/NowPlayingBar.tsx (3)
79-88: LGTM! Orpheus 集成和状态派生逻辑清晰从本地 store 迁移到 Orpheus hooks(
useIsPlaying、usePlaybackState)的实现正确。finalPlayingIndicator的派生逻辑很好地处理了 BUFFERING 状态,为用户提供加载反馈。
105-111:scheduleOnRN参数传递方式验证这里通过
scheduleOnRN的第二个参数传递isPlaying状态,确保回调执行时使用正确的值。这种模式避免了闭包陷阱,实现正确。
176-176: 良好的空值回退处理为
title添加了'未知曲目'的回退值,与下方 artist 的'未知'回退保持一致,提升了用户体验。src/app/(tabs)/settings.tsx (1)
162-163: The timing concern about synchronous initialization is not applicable here.
Orpheus.restorePlaybackPositionEnabledis a property on a native module wrapper that is guaranteed to be available synchronously after app initialization. The settings component only renders after the root layout completes its setup, so this synchronous read is safe. This pattern is already used consistently throughout the codebase (e.g.,Orpheus.setBilibiliCookie()is called synchronously during store hydration inuseAppStore.ts). No changes needed.Likely an incorrect or invalid review comment.
src/features/player/components/PlayerFunctionalMenu.tsx (1)
134-135: 定时关闭功能已禁用根据 PR 目标,定时停止播放功能尚未完成。当前以 toast 提示用户功能未实现是合理的临时处理。
src/hooks/stores/useAppStore.ts (1)
87-88: Cookie 同步到 Orpheus 的实现正确在状态更新后同步 cookie 到 Orpheus,逻辑顺序正确,确保了 JS 层和原生层的状态一致性。
src/app/playlist/remote/toview.tsx (2)
171-182: "播放全部"按钮的点击事件被禁用
onClickMainButton被注释掉,导致 PlaylistHeader 中的"播放全部"按钮无法响应点击。这似乎是迁移过程中的临时状态。请确认是否需要实现批量播放功能,或者这是计划中的后续工作。
98-108: No issues found.toastAndLogErrordoes not include Sentry reporting — it only displays a toast notification and logs locally. The separatereportErrorToSentrycall is appropriate and not redundant.Likely an incorrect or invalid review comment.
src/features/playlist/local/components/LocalPlaylistHeader.tsx (1)
204-204: LGTM按钮点击处理正确封装。
src/app/download.tsx (1)
41-64: 加载和错误状态处理正确Loading 和 Error 状态的 UI 处理清晰,用户体验良好。
src/features/playlist/local/hooks/useLocalPlaylistPlayer.ts (2)
17-44: LGTM!将 hook 签名从
tracks: Track[]改为playlistId: number的重构是合理的。通过playlistService.getPlaylistTracks获取最新数据可以避免使用过期的 tracks 列表,错误处理也很完善。
24-26: 不存在空值访问风险由于
Track类型是一个有区分的联合类型 (BilibiliTrack | LocalTrack),当item.source === 'bilibili'时,TypeScript 类型系统已经将其类型缩小为BilibiliTrack。BilibiliTrack接口中bilibiliMetadata是必需属性(非可选),且videoIsValid是其必需的布尔属性。因此,条件检查item.source === 'bilibili' ? item.bilibiliMetadata.videoIsValid : true是类型安全的,不存在空值访问风险。src/features/playlist/local/components/LocalPlaylistItem.tsx (2)
57-94: LGTM!
renderDownloadStatus的重构清晰地将下载状态映射到对应的图标和颜色。依赖数组正确地包含了所有使用的值。
53-53: 性能注意事项每个
TrackListItem实例都会调用useIsCurrentTrack,该 hook 会创建独立的 Orpheus 事件监听器。对于较大的播放列表,这可能导致大量并发的事件订阅。建议验证在大型播放列表(如 100+ 曲目)场景下的性能表现。如果存在问题,可以考虑在父组件维护当前曲目状态并通过 props 传递。
src/features/playlist/local/components/LocalTrackList.tsx (1)
205-220: LGTM!菜单项的渲染逻辑正确,
downloadState被正确地传递给trackMenuItems函数以支持基于下载状态的上下文菜单。src/components/modals/PlayerQueueModal.tsx (2)
113-122: LGTM!查询配置合理:
staleTime: 0和gcTime: 0确保队列数据始终是最新的,enabled: isVisible避免在模态框关闭时进行不必要的查询。
27-99: LGTM!
TrackItem组件正确地适配了新的OrpheusTrack类型,使用index参数进行曲目切换和移除操作是符合 Orpheus API 设计的。
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
src/features/player/components/PlayerFunctionalMenu.tsx (1)
89-110:Orpheus.downloadTrack缺少 try/catch:成功 toast 可能误报
这点在旧 commit 的 review 里已经提过,但当前实现仍然会在下载失败时显示“已添加到下载队列”。- await Orpheus.downloadTrack({ - id: currentTrack.uniqueKey, - url: url, - title: currentTrack.title, - artist: currentTrack.artist?.name, - artwork: currentTrack.coverUrl ?? undefined, - duration: currentTrack.duration, - }) - toast.info('已添加到下载队列') + try { + await Orpheus.downloadTrack({ + id: currentTrack.uniqueKey, + url, + title: currentTrack.title, + artist: currentTrack.artist?.name, + artwork: currentTrack.coverUrl ?? undefined, + duration: currentTrack.duration, + }) + toast.info('已添加到下载队列') + } catch (e) { + toast.error('添加到下载队列失败') + }@roitium/expo-orpheus 的 Orpheus.downloadTrack 在失败时是 throw 异常还是返回错误码/Result?是否有“已在队列中”之类的可区分错误?src/components/modals/player/SleepTimerModal.tsx (1)
45-55: 必须添加错误处理和等待 refetch 完成当前实现存在以下严重问题:
- 无错误处理:如果
Orpheus.setSleepTimer或Orpheus.cancelSleepTimer失败,用户不会收到任何提示,弹窗仍会关闭,导致用户误以为操作成功- 状态不一致风险:使用
void refetch()不等待数据更新完成就关闭弹窗,可能导致 UI 显示的状态与实际状态不同步应用以下修改添加错误处理并等待状态同步:
const handleSetTimer = async (minutes: number) => { + try { await Orpheus.setSleepTimer(minutes * 60 * 1000) - void refetch() + await refetch() close('SleepTimer') + } catch (error) { + toast.error('设置定时器失败,请重试') + console.error('Sleep timer error:', error) + } } const handleCancelTimer = async () => { + try { await Orpheus.cancelSleepTimer() - void refetch() + await refetch() close('SleepTimer') + } catch (error) { + toast.error('取消定时器失败,请重试') + console.error('Cancel sleep timer error:', error) + } }注意:需要在文件顶部添加 toast 导入(如果尚未导入)。
🧹 Nitpick comments (5)
package.json (1)
99-99: git+https 依赖react-native-logs:建议在 package.json 中显式指定 commit SHApnpm-lock.yaml 已正确提交并包含该依赖的 commit hash(b534efdc...),提供了可复现性保证。但建议在 package.json 中直接使用 commit SHA 而非 tag 引用,例如:
"react-native-logs": "git+https://github.com/bbplayer-app/react-native-logs.git#b534efdc6022ba04d7b98e4dae0a1f8864cc1fb4"这样可避免 tag 被意外移动或重写的风险,使声明与锁文件一致。仓库已验证可访问,CI 无权限问题。
src/app/(tabs)/settings.tsx (1)
36-36: 变量命名与实际类型不一致。
useCurrentTrack()返回的是Track | null,而变量名haveTrack暗示这是一个布尔值。虽然在第 45 行的条件判断中可以正常工作(利用了 JS 的 truthiness),但命名会让阅读代码的人产生困惑。- const haveTrack = useCurrentTrack() + const currentTrack = useCurrentTrack()并相应更新第 45 行的引用:
- paddingBottom: haveTrack ? 70 : insets.bottom, + paddingBottom: currentTrack ? 70 : insets.bottom,src/features/player/components/PlayerFunctionalMenu.tsx (1)
89-96: 精简 menu 关闭逻辑 + 改善用户提示文案,并让 title 判断更直观
setMenuVisible(false)在!currentTrack分支和后面重复调用;另外“为什么 currentTrack 不存在?”不适合作为面向用户的错误提示。title也建议基于trackId计算,避免空 key。onPress={async () => { - if (!currentTrack) { - setMenuVisible(false) - toast.error('为什么 currentTrack 不存在?') - return - } - setMenuVisible(false) + setMenuVisible(false) + if (!currentTrack) { + toast.error('当前未在播放任何音频') + return + } const url = getInternalPlayUri(currentTrack)title={ - downloadStatus?.[currentTrack?.uniqueKey ?? ''] === DownloadState.COMPLETED + (currentTrack?.uniqueKey && + downloadStatus?.[currentTrack.uniqueKey] === DownloadState.COMPLETED) ? '重新下载音频' : '下载音频' }Also applies to: 111-116
src/components/modals/player/SleepTimerModal.tsx (2)
14-21: 考虑调整缓存策略以平衡实时性与性能当前
gcTime: 0和staleTime: 0的配置会导致组件每次重新挂载或窗口重新聚焦时都触发数据重新获取。由于组件内已有 1 秒间隔的useEffect(Line 27)持续计算并更新剩余时间,UI 的实时性已经得到保障。建议设置适度的
staleTime(如 5000-10000 毫秒),以减少不必要的查询次数,同时保持足够的数据新鲜度。const { data: sleepTimerEndAt, refetch } = useQuery({ queryFn: async () => { return await Orpheus.getSleepTimerEndTime() }, queryKey: ['sleepTimerEndAt'], - gcTime: 0, - staleTime: 0, + staleTime: 5000, // 5秒内认为数据新鲜,避免频繁重新获取 })
96-101: 考虑为无效输入提供用户反馈当前逻辑对无效输入(非数字、负数、零)采用静默忽略策略。建议在输入无效时向用户提供明确反馈,避免用户疑惑为何点击"设置"后无响应。
onPress={async () => { const minutes = parseInt(customMinutes, 10) if (!isNaN(minutes) && minutes > 0) { await handleSetTimer(minutes) + } else { + toast.error('请输入有效的分钟数(大于 0)') } }}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (13)
package.json(3 hunks)src/app/(tabs)/settings.tsx(5 hunks)src/app/playlist/remote/collection/[id].tsx(0 hunks)src/app/playlist/remote/favorite/[id].tsx(0 hunks)src/app/playlist/remote/multipage/[bvid].tsx(0 hunks)src/app/playlist/remote/search-result/fav/[query].tsx(0 hunks)src/app/playlist/remote/search-result/global/[query].tsx(0 hunks)src/app/playlist/remote/toview.tsx(4 hunks)src/app/playlist/remote/uploader/[mid].tsx(0 hunks)src/components/modals/player/SleepTimerModal.tsx(4 hunks)src/features/player/components/PlayerFunctionalMenu.tsx(3 hunks)src/hooks/stores/useAppStore.ts(4 hunks)src/types/core/appStore.ts(0 hunks)
💤 Files with no reviewable changes (7)
- src/app/playlist/remote/multipage/[bvid].tsx
- src/app/playlist/remote/collection/[id].tsx
- src/types/core/appStore.ts
- src/app/playlist/remote/search-result/global/[query].tsx
- src/app/playlist/remote/search-result/fav/[query].tsx
- src/app/playlist/remote/uploader/[mid].tsx
- src/app/playlist/remote/favorite/[id].tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/app/playlist/remote/toview.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
src/components/modals/player/SleepTimerModal.tsx (1)
src/hooks/stores/usePlayerStore.ts (2)
durationInSeconds(66-76)state(935-941)
src/app/(tabs)/settings.tsx (3)
src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-79)src/hooks/stores/usePlayerStore.ts (8)
span(702-715)state(401-436)track(689-717)Sentry(454-530)index(719-824)state(935-941)currentTrackUniqueKey(624-650)track(403-405)src/components/NowPlayingBar.tsx (2)
NowPlayingBar(72-215)state(78-78)
src/features/player/components/PlayerFunctionalMenu.tsx (4)
src/hooks/player/useBatchDownloadStatus.ts (1)
useBatchDownloadStatus(4-13)src/utils/player.ts (1)
getInternalPlayUri(267-267)src/types/core/downloadManagerStore.ts (1)
DownloadState(9-12)src/hooks/stores/usePlayerStore.ts (2)
uniqueKey(826-835)track(689-717)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (9)
package.json (2)
4-5: 版本号与 versionCode 提升:请确认与 Expo/原生配置一致
version / versionCode 的提升没问题,但请同步核对app.config.* / app.json、AndroidversionCode、iOSbuildNumber(若有)一致性,避免商店构建号冲突或 OTA 版本判断异常。
38-38: 引入@roitium/expo-orpheus:请核对 peerDependencies/原生集成要求
这是播放器迁移的关键依赖,建议确认它对 Expo SDK / React Native 版本、是否需要 config plugin、以及 Android(Media3)/iOS 侧的额外配置在本 PR 中都已覆盖。src/app/(tabs)/settings.tsx (2)
162-166: 状态同步依赖于 Orpheus 初始化时机。本地状态从
Orpheus.restorePlaybackPositionEnabled和Orpheus.loudnessNormalizationEnabled同步初始化。如果 Orpheus 模块在组件挂载时尚未完成初始化,或者这些值在其他地方被修改,UI 可能显示过时的状态。如果这些设置只在此页面修改,当前实现是可以接受的。否则,考虑使用
useEffect监听 Orpheus 状态变化以保持同步。
255-260: [rewritten review comment]
[classification tag]src/components/modals/player/SleepTimerModal.tsx (2)
25-43: LGTM!剩余时间计算逻辑正确,正确处理了定时器更新、边界条件和清理工作。
3-4: 导入语句正确,无需修改。这两行导入是代码所需的依赖项,已在
package.json中声明为@roitium/expo-orpheus@^0.5.0。Orpheus.getSleepTimerEndTime()、Orpheus.setSleepTimer()、Orpheus.cancelSleepTimer()等方法在代码中被正常调用(第 16、46、52 行),表明这些 API 在 v0.5.0 中可用且运作正常。导入模式与整个代码库中的其他用法一致。src/hooks/stores/useAppStore.ts (3)
76-107: Orpheus 同步缺少错误处理,且清理 cookie 未同步到原生层Line 86 和 98 的
setBilibiliCookie/updateBilibiliCookie直接调用Orpheus.setBilibiliCookie(...)而不处理其可能的异常,会导致 JS 状态与原生层不一致。同时 line 103-107 的clearBilibiliCookie没有清理 Orpheus,风险更高。需要确认
Orpheus.setBilibiliCookie的 API 契约(返回类型、错误处理方式)后,为这三处添加统一的错误处理,确保状态同步失败时的行为一致。参考src/app/_layout.tsx中已有的 try/catch 模式。
241-246: Hydration 仅在有 cookie 时设置,无 cookie 时不清空;clearBilibiliCookie 也未同步到原生层;日志误导已验证存在多个关键缺陷:
Hydration 不清空:用户登出后
bilibiliCookie为 null,hydration 时未调用 Orpheus API 清空,原生层仍保留旧 cookie,可能继续带 cookie 发请求。clearBilibiliCookie 未同步:该方法(lines 103-107)仅清空本地状态,不调用任何 Orpheus API,用户登出时原生层的 cookie 不会被清空。
日志误导:无论是否存在 cookie,都打印"应用启动时自动设置 cookie 到 orpheus",应根据是否有 cookie 区分。
不对称的 API 调用:
setBilibiliCookie和updateBilibiliCookie都调用Orpheus.setBilibiliCookie(),而clearBilibiliCookie未调用对应的清空方法。建议:
- Hydration 完成后无论是否存在 cookie,都同步一次(存在则 set,不存在则 clear)
clearBilibiliCookie调用对应的 Orpheus API 清空- 日志与实际行为一致
还需确认
Orpheus.setBilibiliCookie('')是否等价于清空;若库提供专门的 clear API(如clearBilibiliCookie()等),应优先使用专用方法。
7-7: 移除不适用的 Web 兼容性检查 - 此项目是 React Native/Expo 应用,不涉及 Web 构建该项目为 Expo 原生应用,仅包含 iOS 和 Android 构建目标(见 package.json),不存在 Web 构建场景。@roitium/expo-orpheus 作为原生模块,在此上下文中使用是安全的,无需平台守护。
建议关注的实际问题:
setBilibiliCookie()和updateBilibiliCookie()中的Orpheus.setBilibiliCookie()调用缺少错误处理clearBilibiliCookie()未同步调用Orpheus.clearBilibiliCookie(),可能导致原生层仍保留旧 cookie
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (5)
src/features/player/components/PlayerFunctionalMenu.tsx (2)
28-31: 下载状态查询逻辑已修复,LGTM!正确处理了
currentTrack为空的情况,避免传递['']触发不必要的原生层调用。
91-120: 错误处理已完善,LGTM!下载流程现在包含完整的错误处理:
- URL 获取失败时显示错误提示
Orpheus.downloadTrack调用包裹在 try-catch 中- 失败时通过
toastAndLogError记录上下文信息代码结构清晰,错误边界处理得当。
src/features/player/components/PlayerHeader.tsx (1)
35-40: 避免用''作为 downloadStatus 索引 key,改为显式判空后再比较建议提取
currentKey并在存在时再做状态比较,避免downloadStatus?.['']这种“看似安全但语义不明”的访问。export function PlayerHeader({ onMorePress, index, }: { onMorePress: () => void index: number }) { const router = useRouter() const currentTrack = useCurrentTrack() + const currentKey = currentTrack?.uniqueKey const { data: downloadStatus } = useBatchDownloadStatus( - currentTrack?.uniqueKey ? [currentTrack.uniqueKey] : [], + currentKey ? [currentKey] : [], ) return ( @@ {index === 1 ? (currentTrack?.title ?? '正在播放') - : downloadStatus?.[currentTrack?.uniqueKey ?? ''] === - DownloadState.COMPLETED + : (currentKey && + downloadStatus?.[currentKey] === DownloadState.COMPLETED) ? '正在播放 (已缓存)' : '正在播放'}src/features/downloads/DownloadTaskItem.tsx (1)
137-148: 错误处理已添加之前的审查指出
Orpheus.downloadTrack和Orpheus.removeDownload缺少错误处理。现在已正确添加了 try-catch 并使用toastAndLogError通知用户。Also applies to: 158-168
src/components/modals/player/SleepTimerModal.tsx (1)
57-61: 缺少错误处理可能导致误导性的成功提示
handleCancelTimer没有 try-catch 错误处理。如果Orpheus.cancelSleepTimer()失败(网络问题、原生层错误等),代码仍会显示"取消定时器成功"的提示并关闭弹窗,但定时器实际上可能仍在运行。这会误导用户,可能导致播放器意外停止。应用以下修复添加错误处理:
const handleCancelTimer = async () => { + try { await Orpheus.cancelSleepTimer() toast.success('取消定时器成功') close('SleepTimer') + } catch (e) { + toastAndLogError('取消定时器失败', e, 'Modal.SleepTimer') + } }更进一步,建议与
handleSetTimer一样,也采用 useMutation 模式以保持一致性并支持跨组件状态同步:+const cancelTimerMutation = useMutation({ + mutationFn: async () => { + await Orpheus.cancelSleepTimer() + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sleepTimerEndAt'] }) + toast.success('取消定时器成功') + close('SleepTimer') + }, + onError: (e) => { + toastAndLogError('取消定时器失败', e, 'Modal.SleepTimer') + }, +}) const handleCancelTimer = async () => { - await Orpheus.cancelSleepTimer() - toast.success('取消定时器成功') - close('SleepTimer') + cancelTimerMutation.mutate() }
🧹 Nitpick comments (3)
src/features/downloads/DownloadTaskItem.tsx (1)
33-45:task.state在依赖数组中会导致不必要的监听器重新注册事件监听器的注册和注销只依赖于
task.id,但task.state被包含在依赖数组中。这意味着每次状态变化时,监听器都会被移除并重新添加,这是不必要的开销。useEffect(() => { const handler = (e: ProgressEvent['progress:uniqueKey']) => { sharedProgress.value = e.percent if (e.state !== task.state) { setTask((task) => ({ ...task, state: e.state })) } } eventListner.on(`progress:${task.id}`, handler) return () => { eventListner.off(`progress:${task.id}`, handler) } - }, [task.id, sharedProgress, task.state, setTask]) + }, [task.id, sharedProgress, setTask])由于
handler内部通过setTask的函数式更新来比较状态,不需要将task.state作为外部依赖。src/hooks/player/useCurrentTrack.ts (1)
52-54: 在事件回调里增加 “已卸载” 短路,避免卸载瞬间仍启动新异步任务
当前依赖fetchAndUpdate()内部的isMounted防护是安全的,但仍会做一次无意义的异步启动。- const sub = Orpheus.addListener('onTrackStarted', () => { - void fetchAndUpdate() - }) + const sub = Orpheus.addListener('onTrackStarted', () => { + if (!isMounted) return + void fetchAndUpdate() + })Also applies to: 56-59
src/components/modals/player/SleepTimerModal.tsx (1)
47-55: 错误处理已添加,但建议使用 react-query mutation 模式相比之前的实现,已添加了 try-catch 错误处理和用户提示,这很好。
不过,当前模式在成功设置定时器后直接关闭弹窗,没有刷新查询数据。虽然弹窗内的陈旧数据不会被看到(因为已关闭),但如果应用的其他部分也查询
sleepTimerEndAt,它们不会自动更新。建议采用 react-query 的标准 mutation 模式,以便自动同步所有查询实例:
+import { useMutation, useQueryClient } from '@tanstack/react-query' const SleepTimerModal = () => { + const queryClient = useQueryClient() const close = useModalStore((state) => state.close) + const setTimerMutation = useMutation({ + mutationFn: async (minutes: number) => { + await Orpheus.setSleepTimer(minutes * 60 * 1000) + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sleepTimerEndAt'] }) + toast.success('设置定时器成功') + close('SleepTimer') + }, + onError: (e) => { + toastAndLogError('设置定时器失败', e, 'Modal.SleepTimer') + }, + }) const handleSetTimer = async (minutes: number) => { - try { - await Orpheus.setSleepTimer(minutes * 60 * 1000) - toast.success('设置定时器成功') - close('SleepTimer') - } catch (e) { - toastAndLogError('设置定时器失败', e, 'Modal.SleepTimer') - } + setTimerMutation.mutate(minutes) }这样可以确保所有使用
['sleepTimerEndAt']查询键的组件都会自动刷新。
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
package.json(3 hunks)src/app/download.tsx(2 hunks)src/components/modals/player/SleepTimerModal.tsx(4 hunks)src/features/downloads/DownloadTaskItem.tsx(7 hunks)src/features/player/components/PlayerFunctionalMenu.tsx(3 hunks)src/features/player/components/PlayerHeader.tsx(3 hunks)src/features/playlist/remote/search-result/hooks/useSearchInteractions.ts(2 hunks)src/hooks/player/useCurrentTrack.ts(1 hunks)src/hooks/player/useIsCurrentTrack.ts(1 hunks)src/hooks/player/useTrackProgress.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- src/hooks/player/useTrackProgress.ts
- src/features/playlist/remote/search-result/hooks/useSearchInteractions.ts
- src/hooks/player/useIsCurrentTrack.ts
- package.json
🧰 Additional context used
🧬 Code graph analysis (4)
src/components/modals/player/SleepTimerModal.tsx (1)
src/hooks/stores/usePlayerStore.ts (6)
durationInSeconds(66-76)logger(674-685)Sentry(668-687)state(935-941)Sentry(454-530)state(679-683)
src/features/player/components/PlayerHeader.tsx (3)
src/hooks/player/useBatchDownloadStatus.ts (1)
useBatchDownloadStatus(4-13)src/types/core/downloadManagerStore.ts (1)
DownloadState(9-12)src/lib/services/trackService.ts (1)
createOrUpdateTrackDownloadRecord(833-860)
src/features/downloads/DownloadTaskItem.tsx (2)
src/types/core/downloadManagerStore.ts (2)
DownloadTask(1-7)DownloadState(9-12)src/hooks/stores/useDownloadManagerStore.ts (2)
ProgressEvent(5-13)eventListner(14-14)
src/app/download.tsx (3)
src/types/core/downloadManagerStore.ts (1)
DownloadTask(1-7)src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-63)src/lib/config/queryClient.ts (1)
queryClient(9-45)
🔇 Additional comments (9)
src/features/player/components/PlayerFunctionalMenu.tsx (1)
121-126: 标题逻辑正确。根据下载状态正确切换按钮文案,已完成时显示"重新下载音频",否则显示"下载音频"。
src/features/player/components/PlayerHeader.tsx (1)
1-6: 无需修改 —— 导入和类型使用正确
useBatchDownloadStatus返回的data属性为Record<string, DownloadState>类型,与@roitium/expo-orpheus中的DownloadState枚举一致。代码中使用downloadStatus?.[currentTrack?.uniqueKey ?? ''] === DownloadState.COMPLETED的模式完全正确。虽然存在命名冲突(
src/types/core/downloadManagerStore.ts也导出同名的DownloadState接口),但此处导入的是@roitium/expo-orpheus中的正确类型,不会造成问题。src/app/download.tsx (1)
74-79: 清除后刷新列表的问题已修复之前的审查指出调用
clearUncompletedDownloadTasks()后列表不会自动刷新。现在已正确添加了queryClient.invalidateQueries()调用。src/features/downloads/DownloadTaskItem.tsx (1)
26-28: 使用useRecyclingState管理任务状态是合适的选择配合 FlashList 的回收机制,使用
useRecyclingState可以在列表项被回收复用时正确重置状态。initTask.id作为依赖项确保了当任务 ID 变化时状态会被重新初始化。src/hooks/player/useCurrentTrack.ts (2)
7-47: 竞态/卸载保护实现到位(requestId + isMounted)
lastRequestIdRef+ 多处 await 后校验,能有效避免快速切歌导致的乱序更新;卸载后也不会再setTrack。
18-28: 所有核心 API 约定已在代码中验证无误,无需修改:
onTrackStarted事件名正确 — 在useCurrentTrack.ts与useIsCurrentTrack.ts中一致使用Orpheus.addListener()返回值包含remove()方法 — 第 58 行成功调用sub.remove()currentTrack.id与uniqueKey语义一致 —src/utils/player.ts的convertToOrpheusTrack()明确设置orpheusTrack.id = track.uniqueKey,保证 Orpheus 返回的currentTrack.id就是trackService.getTrackByUniqueKey()期望的值当前实现可以正常获取
internalTrack,无风险。src/components/modals/player/SleepTimerModal.tsx (3)
2-6: 导入合理,支持新的 Orpheus 集成新增的导入项(toastAndLogError、toast、Orpheus、useQuery)都有明确用途,支持从本地 store 迁移到 Orpheus 原生 API 的架构变更。
102-107: 正确地将自定义输入处理改为异步将按钮的
onPress改为 async 函数并等待handleSetTimer完成,确保了定时器设置操作的正确执行顺序。输入验证逻辑也很恰当。
16-23: useQuery 配置已适配此场景,无需改进当前实现中
sleepTimerEndAt仅在 SleepTimerModal 内部使用,不存在与其他组件共享状态的情况。查询配置gcTime: 0, staleTime: 0搭配handleSetTimer和handleCancelTimer后立即关闭弹窗的逻辑,已经确保下次打开时获取最新数据。无需额外的invalidateQueries调用。
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
src/app/download.tsx (1)
38-38:haveTrack命名可改进
useCurrentTrack()返回Track | null,这里用于布尔判断是正确的,但变量命名haveTrack暗示布尔值。建议改为currentTrack以更准确反映其类型。- const haveTrack = useCurrentTrack() + const currentTrack = useCurrentTrack()并在使用处:
- paddingBottom: haveTrack ? 70 + insets.bottom : insets.bottom, + paddingBottom: currentTrack ? 70 + insets.bottom : insets.bottom,src/features/downloads/DownloadTaskItem.tsx (1)
33-45: useEffect 依赖数组包含task.state可能导致不必要的重订阅当
handler内部调用setTask更新task.state时,由于task.state在依赖数组中,会触发 effect 重新执行并重新订阅事件。虽然功能上正确(因为会先 off 再 on),但会产生不必要的订阅/取消订阅操作。建议移除
task.state依赖,使用 ref 或在 handler 内部获取最新状态:useEffect(() => { const handler = (e: ProgressEvent['progress:uniqueKey']) => { sharedProgress.value = e.percent - if (e.state !== task.state) { - setTask((task) => ({ ...task, state: e.state })) - } + setTask((prevTask) => { + if (e.state !== prevTask.state) { + return { ...prevTask, state: e.state } + } + return prevTask + }) } eventListner.on(`progress:${task.id}`, handler) return () => { eventListner.off(`progress:${task.id}`, handler) } -}, [task.id, sharedProgress, task.state, setTask]) +}, [task.id, sharedProgress, setTask])这样可以避免因
task.state变化导致的重复订阅。src/hooks/player/useBatchDownloadStatus.ts (1)
4-14: 考虑 queryKey 稳定性和缓存策略。当前实现存在两个潜在问题:
queryKey 引用不稳定(Line 6):如果
ids数组在每次渲染时都是新引用,即使内容相同,也会导致缓存失效和不必要的重新获取。建议调用方使用useMemo稳定ids引用,或在 hook 内部对ids进行序列化处理。完全禁用缓存(Lines 10-11):
staleTime: 0和gcTime: 0意味着数据永不缓存,可能导致频繁的 API 调用。根据相关代码片段(如src/hooks/queries/db/track.ts中的usePlayCountLeaderBoardPaginated设置gcTime: 0),完全禁用缓存可能是为了避免加载大量缓存数据导致卡顿。但对于下载状态这种频繁变化的数据,可以考虑设置较短的staleTime(如 1-5 秒)以减少不必要的请求。可以考虑以下改进:
export function useBatchDownloadStatus(ids: string[]) { return useQuery({ - queryKey: ['batchDownloadStatus', ids], + queryKey: ['batchDownloadStatus', ids.join(',')], queryFn: async () => { return await Orpheus.getDownloadStatusByIds(ids) }, - staleTime: 0, + staleTime: 2000, // 2秒内复用缓存 gcTime: 0, enabled: ids.length > 0, }) }src/utils/player.ts (1)
26-30: 清理注释的调试日志。代码中有多处被注释掉的 debug 日志(Lines 26-30 和 57-60)。请决定是删除这些日志还是取消注释。如果这些日志对调试有价值,建议取消注释;如果不再需要,应该删除以保持代码整洁。
Also applies to: 57-60
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/app/download.tsx(1 hunks)src/features/downloads/DownloadTaskItem.tsx(7 hunks)src/hooks/player/useBatchDownloadStatus.ts(1 hunks)src/utils/player.ts(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/features/downloads/DownloadTaskItem.tsx (2)
src/types/core/downloadManagerStore.ts (2)
DownloadTask(1-7)DownloadState(9-12)src/hooks/stores/useDownloadManagerStore.ts (2)
ProgressEvent(5-13)eventListner(14-14)
src/hooks/player/useBatchDownloadStatus.ts (3)
src/hooks/queries/db/track.ts (2)
useTotalPlaybackDuration(66-74)usePlayCountLeaderBoardPaginated(34-64)src/hooks/stores/usePlayerStore.ts (6)
uniqueKey(826-835)state(401-436)checkPlayerReady(339-449)track(689-717)index(719-824)state(830-834)src/hooks/useCheckUpdate.tsx (2)
useCheckUpdate(6-32)__DEV__(8-31)
src/utils/player.ts (7)
src/types/core/media.ts (1)
Track(64-64)src/lib/errors/thirdparty/bilibili.ts (1)
BilibiliApiError(25-50)src/lib/errors/player.ts (1)
PlayerError(10-17)src/lib/services/trackService.ts (2)
trackService(819-819)formatTrack(72-108)src/utils/log.ts (1)
flatErrorMessage(111-129)src/lib/config/queryClient.ts (1)
queryClient(9-45)src/hooks/queries/db/track.ts (1)
trackKeys(11-32)
src/app/download.tsx (4)
src/types/core/downloadManagerStore.ts (1)
DownloadTask(1-7)src/hooks/player/useCurrentTrack.ts (1)
useCurrentTrack(7-63)src/features/downloads/DownloadHeader.tsx (1)
DownloadHeader(12-39)src/lib/config/queryClient.ts (1)
queryClient(9-45)
🔇 Additional comments (16)
src/app/download.tsx (5)
4-9: 导入和类型迁移正确从
@roitium/expo-orpheus导入DownloadTask类型和OrpheusAPI,配合 React Query 的useQuery使用,整体迁移方向正确。
25-36: React Query 配置合理
staleTime: 0确保每次访问页面都会重新获取最新数据,适合下载任务这种需要实时更新的场景。
40-45: Header 提取解决了之前的问题将
header提取为变量并在isPending和isError状态中复用,确保用户在任何状态下都能使用返回按钮。
80-86: 缓存失效逻辑已正确实现
onClearAll现在在清除任务后调用queryClient.invalidateQueries,解决了之前评审中指出的列表不刷新问题。
47-47: 该代码正确,无需修改
DownloadTask从@roitium/expo-orpheus导出,其类型定义包含id属性。虽然本地src/types/core/downloadManagerStore.ts中的DownloadTask使用uniqueKey而非id,但这是两个不同的类型定义,分别服务于不同的功能模块。DownloadTaskItem.tsx中对task.id和task.state的多处使用证实了外部包的DownloadTask包含这些属性。src/features/downloads/DownloadTaskItem.tsx (8)
1-11: 导入结构清晰从
@roitium/expo-orpheus导入DownloadState枚举、OrpheusAPI 和DownloadTask类型,同时保留了事件监听器的导入用于进度更新。
20-28:useRecyclingState用于 FlashList 优化使用
@shopify/flash-list的useRecyclingState是正确的做法,可以在列表项复用时正确重置状态。依赖数组使用initTask.id确保任务变化时状态会重置。
70-83: 状态文本逻辑清晰
getStatusText使用DownloadState枚举进行分支判断,覆盖了所有预期状态并提供了默认处理。
137-148: 错误处理已添加重试下载操作现在包含了 try-catch 和
toastAndLogError调用,解决了之前评审中指出的问题。
151-151: 冗余三元表达式已移除之前评审指出的
marginRight: task.state === DownloadState.FAILED ? 0 : 0冗余代码已被移除,现在是简洁的<View>{icon}</View>。
154-164: 删除任务的错误处理完善删除操作同样添加了 try-catch 处理,保持了与重试操作一致的错误处理模式。
169-169:useMemo依赖数组需要审查
icons的 useMemo 依赖包含task.id、task.state和task.track,但内部还使用了colors.error。当前依赖数组是正确的,但如果task.track是对象引用,可能会导致不必要的重新计算。请确认
task.track的引用稳定性。如果task.track每次渲染都是新对象,可能需要改用task.track?.id或类似的原始值作为依赖。
184-184: Fallback 文本处理得当使用可选链和空值合并运算符
task.track?.title ?? '未知任务'正确处理了 track 可能为空的情况。src/utils/player.ts (3)
68-106: LGTM:播放历史上报重构合理。函数重构为接受
uniqueKey参数并通过trackService查询曲目详情的设计是合理的,符合新的 Orpheus 架构。错误处理完善,逻辑清晰。
185-195: LGTM:播放 URI 生成逻辑正确。函数正确处理了 bilibili(包括多 P 视频)和本地音轨的 URI 生成,对未知来源返回
undefined的处理也很合理。
197-245: LGTM:播放记录最终化逻辑完善。此函数实现了完整的播放记录流程:
- 计算有效播放时长和完成状态(Lines 203-216),完成阈值设置合理(90% 或剩余 2 秒)
- 通过
trackService记录播放数据(Lines 218-233)- 失效排行榜查询缓存(Lines 235-237)
- 异步上报 B 站播放历史并捕获错误(Lines 239-241)
错误处理完善,逻辑清晰。
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/hooks/stores/useAppStore.ts (1)
76-107: clearBilibiliCookie 需要同步清除 Orpheus 侧的 cookie
setBilibiliCookie和updateBilibiliCookie都会推送到 Orpheus,但clearBilibiliCookie目前只清了 Zustand 状态而未清 Orpheus;这会导致登出或切换账号时原生层残留旧 cookie。另外,建议给
setBilibiliCookie和updateBilibiliCookie中的 Orpheus 调用加上 try/catch 保护,防止原生异常导致 action 崩溃。这与其他 Orpheus 调用在整个项目中的错误处理模式一致。clearBilibiliCookie: () => { set((state) => { state.bilibiliCookie = null }) + try { + Orpheus.setBilibiliCookie('') + logger.info('清除 cookie 到 orpheus') + } catch (e) { + logger.error('清除 orpheus cookie 失败', e) + } },src/app/_layout.tsx (1)
96-116: Cookie 的 falsy 检查建议补充空对象判断;且建议在初始化成功后设置global.playerIsReady现有代码只检查
if (!cookie),但bilibiliCookie可能是空对象{}。此时条件为 false(空对象是 truthy),会继续调用Orpheus.setBilibiliCookie(serializeCookieObject({}))传入空字符串。建议参考hasBilibiliCookie()的做法,补充空对象检查。另外,
global.playerIsReady在错误时设为 false,但成功时未设为 true,导致语义不清。建议在初始化成功后补上设置,以明确标记"播放器已完成初始化"的状态。- if (!cookie) { + if (!cookie || Object.keys(cookie).length === 0) { logger.info('没有 bilibili cookie,跳过播放器初始化') return } logger.debug('初始化 orpheus bilibili cookie') Orpheus.setBilibiliCookie(serializeCookieObject(cookie)) + global.playerIsReady = true
♻️ Duplicate comments (1)
src/utils/player.ts (1)
23-62:volume值仍然硬编码为 0,响度归一化功能失效。Lines 33-36 的
volume对象中measured_i和target_i仍被硬编码为 0。这会导致 Orpheus 无法正确执行响度归一化。请确认这是预期行为还是需要从track.bilibiliMetadata获取真实值。
🧹 Nitpick comments (2)
src/utils/player.ts (1)
148-163: 转换失败时静默跳过可能导致用户困惑。当多首曲目中部分转换失败时,仅记录日志而不通知用户。如果大量曲目失败,用户可能不知道为何只添加了部分曲目。
建议在批量添加结束后,若有失败曲目,给用户一个汇总提示:
if (orpheusTracks.length === 0) { return } + const failedCount = tracks.length - orpheusTracks.length + if (failedCount > 0) { + toast.warning(`${failedCount} 首曲目添加失败`) + } if (playNext) {src/features/player/components/PlayerFunctionalMenu.tsx (1)
123-128: 建议复用trackId变量以保持一致性。Line 28 已定义
trackId = currentTrack?.uniqueKey,此处可复用以避免重复逻辑。title={ - downloadStatus?.[currentTrack?.uniqueKey ?? ''] === - DownloadState.COMPLETED + trackId && downloadStatus?.[trackId] === DownloadState.COMPLETED ? '重新下载音频' : '下载音频' }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (8)
app.config.ts(2 hunks)package.json(3 hunks)src/app/_layout.tsx(2 hunks)src/features/player/components/PlayerFunctionalMenu.tsx(3 hunks)src/hooks/player/useCurrentTrack.ts(1 hunks)src/hooks/player/useIsCurrentTrack.ts(1 hunks)src/hooks/stores/useAppStore.ts(3 hunks)src/utils/player.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- app.config.ts
- package.json
🧰 Additional context used
🧬 Code graph analysis (2)
src/features/player/components/PlayerFunctionalMenu.tsx (4)
src/hooks/player/useBatchDownloadStatus.ts (1)
useBatchDownloadStatus(4-14)src/utils/player.ts (1)
getInternalPlayUri(250-250)src/utils/error-handling.ts (1)
toastAndLogError(11-41)src/types/core/downloadManagerStore.ts (1)
DownloadState(9-12)
src/app/_layout.tsx (1)
src/hooks/stores/useAppStore.ts (2)
useAppStore(58-239)serializeCookieObject(40-46)
🔇 Additional comments (6)
src/utils/player.ts (3)
68-106: LGTM!错误处理逻辑完善,包括设置检查、track 查询失败处理和 bilibili 上报失败的日志记录。
184-194: LGTM!URI 生成逻辑清晰,正确处理了 bilibili 多 P 视频和本地文件两种情况。
196-244: LGTM!播放完成指标计算逻辑合理(90% 或最后 2 秒),错误处理完善,使用
void显式标记异步操作的 fire-and-forget 模式符合最佳实践。src/features/player/components/PlayerFunctionalMenu.tsx (2)
28-31: 修复正确!使用
trackId ? [trackId] : []避免了向useBatchDownloadStatus传递['']的问题,且该 hook 内部有enabled: ids.length > 0的保护。
91-122: LGTM!下载流程的错误处理完善,包括 URL 获取失败和下载失败两种情况的处理。使用
toastAndLogError统一了错误日志记录。src/hooks/player/useCurrentTrack.ts (1)
14-55: 该评论不准确。trackService.getTrackByUniqueKey()使用ResultAsync.fromPromise包装 Promise,会将任何 Promise 拒绝转换为Err结果类型返回,而不会抛出未处理的异常。当等待该方法时,总是会获得ResultAsync<Track, ServiceError | DatabaseError>,之后通过第 45 行的internalTrack.isErr()检查来处理错误情况。代码已经通过 Result 模式正确处理了所有可能的错误,无需添加额外的 try/catch。Likely an incorrect or invalid review comment.
| export function useIsCurrentTrack(trackUniqueKey: string) { | ||
| const [isCurrent, setIsCurrent] = useState(false) | ||
| const lastRequestIdRef = useRef(0) | ||
|
|
||
| useEffect(() => { | ||
| let isMounted = true | ||
|
|
||
| const checkCurrentStatus = async () => { | ||
| const currentRequestId = ++lastRequestIdRef.current | ||
| let currentTrack = null | ||
|
|
||
| try { | ||
| currentTrack = await Orpheus.getCurrentTrack() | ||
| } catch (e) { | ||
| if (isMounted && currentRequestId === lastRequestIdRef.current) { | ||
| toastAndLogError('读取当前曲目信息失败', e, 'Hooks.useIsCurrentTrack') | ||
| setIsCurrent(false) | ||
| } | ||
| } | ||
| if (!isMounted || currentRequestId !== lastRequestIdRef.current) { | ||
| return | ||
| } | ||
|
|
||
| const isMatch = currentTrack?.id === trackUniqueKey | ||
| setIsCurrent(isMatch) | ||
| } |
There was a problem hiding this comment.
参数名 trackUniqueKey 与比较字段 currentTrack.id 语义可能不一致,建议对齐命名/类型
这里用 currentTrack?.id === trackUniqueKey 判定,但参数叫 trackUniqueKey,容易让调用方传错(例如传 Track.uniqueKey 而 Orpheus 返回的是另一种 id)。建议至少改名为 trackId / orpheusTrackId,或加注释/类型别名明确两者一致。
-export function useIsCurrentTrack(trackUniqueKey: string) {
+export function useIsCurrentTrack(trackId: string) {
@@
- const isMatch = currentTrack?.id === trackUniqueKey
+ const isMatch = currentTrack?.id === trackId
@@
- }, [trackUniqueKey])
+ }, [trackId])📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function useIsCurrentTrack(trackUniqueKey: string) { | |
| const [isCurrent, setIsCurrent] = useState(false) | |
| const lastRequestIdRef = useRef(0) | |
| useEffect(() => { | |
| let isMounted = true | |
| const checkCurrentStatus = async () => { | |
| const currentRequestId = ++lastRequestIdRef.current | |
| let currentTrack = null | |
| try { | |
| currentTrack = await Orpheus.getCurrentTrack() | |
| } catch (e) { | |
| if (isMounted && currentRequestId === lastRequestIdRef.current) { | |
| toastAndLogError('读取当前曲目信息失败', e, 'Hooks.useIsCurrentTrack') | |
| setIsCurrent(false) | |
| } | |
| } | |
| if (!isMounted || currentRequestId !== lastRequestIdRef.current) { | |
| return | |
| } | |
| const isMatch = currentTrack?.id === trackUniqueKey | |
| setIsCurrent(isMatch) | |
| } | |
| export function useIsCurrentTrack(trackId: string) { | |
| const [isCurrent, setIsCurrent] = useState(false) | |
| const lastRequestIdRef = useRef(0) | |
| useEffect(() => { | |
| let isMounted = true | |
| const checkCurrentStatus = async () => { | |
| const currentRequestId = ++lastRequestIdRef.current | |
| let currentTrack = null | |
| try { | |
| currentTrack = await Orpheus.getCurrentTrack() | |
| } catch (e) { | |
| if (isMounted && currentRequestId === lastRequestIdRef.current) { | |
| toastAndLogError('读取当前曲目信息失败', e, 'Hooks.useIsCurrentTrack') | |
| setIsCurrent(false) | |
| } | |
| } | |
| if (!isMounted || currentRequestId !== lastRequestIdRef.current) { | |
| return | |
| } | |
| const isMatch = currentTrack?.id === trackId | |
| setIsCurrent(isMatch) | |
| } | |
| }, [trackId]) |
🤖 Prompt for AI Agents
In src/hooks/player/useIsCurrentTrack.ts around lines 5 to 30, the hook
parameter name trackUniqueKey may not match the field currentTrack.id returned
by Orpheus which risks caller confusion or wrong comparisons; rename the
parameter to something explicit (e.g., trackId or orpheusTrackId) or create a
clear type alias/comment that documents it, update the function signature and
all call sites accordingly, and/or change the comparison to use the correct
field (e.g., currentTrack?.uniqueKey === trackUniqueKey) so parameter name and
the compared track field are semantically aligned.

Closes #85
Summary by CodeRabbit
New Features
Bug Fixes
Chores
✏️ Tip: You can customize this high-level summary in your review settings.