diff --git a/CHANGELOG.md b/CHANGELOG.md index 1644ac6d..ddde0c0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ - 基于 B 站视频 bgm 识别结果精准搜索歌词 - 切换到 expo-router +### Fixed + +- 一些减少 rerender 次数的优化 +- 使用 [react-native-paper/4807](https://github.com/callstack/react-native-paper/issues/4807) 中提到的 Menu 组件修复方法,移除 patch + ## [1.3.6] - 2025-10-26 ### Added diff --git a/docs/principles.md b/docs/principles.md new file mode 100644 index 00000000..ae02abd1 --- /dev/null +++ b/docs/principles.md @@ -0,0 +1,5 @@ +## 开发规范(及一些 tips) + +### UI 开发 + +- FlashList 使用到的所有 renderItem 应该在函数外定义,并把所有除了 item 之外的依赖放入 extraData 中,并使用 useMemo 包裹 extraData diff --git a/patches/react-native-paper.patch b/patches/react-native-paper.patch deleted file mode 100644 index 35074aed..00000000 --- a/patches/react-native-paper.patch +++ /dev/null @@ -1,63 +0,0 @@ -diff --git a/lib/module/components/Menu/Menu.js b/lib/module/components/Menu/Menu.js -index 46a45f468db95f6a2c150d266de2d2cce0482b1d..15323c16dd891e3b6f7d184316e7989576176fd8 100644 ---- a/lib/module/components/Menu/Menu.js -+++ b/lib/module/components/Menu/Menu.js -@@ -238,10 +238,8 @@ const Menu = ({ - })]).start(({ - finished - }) => { -- if (finished) { - focusFirstDOMNode(menuRef.current); - prevRendered.current = true; -- } - }); - }, [anchor, attachListeners, measureAnchorLayout, theme]); - const hide = React.useCallback(() => { -@@ -257,7 +255,6 @@ const Menu = ({ - }).start(({ - finished - }) => { -- if (finished) { - setMenuLayout({ - width: 0, - height: 0 -@@ -265,7 +262,6 @@ const Menu = ({ - setRendered(false); - prevRendered.current = false; - focusFirstDOMNode(anchorRef.current); -- } - }); - }, [removeListeners, theme]); - const updateVisibility = React.useCallback(async display => { -diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx -index 55922c1fc27003eea00c77c4dbef180f9016b938..1fc035da37c1ca85c6953b11aa1c8138788d7a8e 100644 ---- a/src/components/Menu/Menu.tsx -+++ b/src/components/Menu/Menu.tsx -@@ -359,11 +359,9 @@ const Menu = ({ - easing: EASING, - useNativeDriver: true, - }), -- ]).start(({ finished }) => { -- if (finished) { -+ ]).start(() => { - focusFirstDOMNode(menuRef.current); - prevRendered.current = true; -- } - }); - }, [anchor, attachListeners, measureAnchorLayout, theme]); - -@@ -377,13 +375,11 @@ const Menu = ({ - duration: ANIMATION_DURATION * animation.scale, - easing: EASING, - useNativeDriver: true, -- }).start(({ finished }) => { -- if (finished) { -+ }).start(() => { - setMenuLayout({ width: 0, height: 0 }); - setRendered(false); - prevRendered.current = false; - focusFirstDOMNode(anchorRef.current); -- } - }); - }, [removeListeners, theme]); - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7cf47a18..3f3d5f97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,11 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -patchedDependencies: - react-native-paper: - hash: 6bf0fca413003fb3ebd05efefeaf4290e6a75a6d6ac589187fac82144ac075b4 - path: patches/react-native-paper.patch - importers: .: @@ -243,7 +238,7 @@ importers: version: 6.9.1(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.13)(react@19.1.0))(react@19.1.0) react-native-paper: specifier: ^5.14.5 - version: 5.14.5(patch_hash=6bf0fca413003fb3ebd05efefeaf4290e6a75a6d6ac589187fac82144ac075b4)(react-native-safe-area-context@5.6.1(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.13)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.13)(react@19.1.0))(react@19.1.0) + version: 5.14.5(react-native-safe-area-context@5.6.1(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.13)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.13)(react@19.1.0))(react@19.1.0) react-native-qrcode-svg: specifier: ^6.3.15 version: 6.3.15(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.13)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.13)(react@19.1.0))(react@19.1.0) @@ -14048,7 +14043,7 @@ snapshots: react: 19.1.0 react-native: 0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.13)(react@19.1.0) - react-native-paper@5.14.5(patch_hash=6bf0fca413003fb3ebd05efefeaf4290e6a75a6d6ac589187fac82144ac075b4)(react-native-safe-area-context@5.6.1(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.13)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.13)(react@19.1.0))(react@19.1.0): + react-native-paper@5.14.5(react-native-safe-area-context@5.6.1(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.13)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.13)(react@19.1.0))(react@19.1.0): dependencies: '@callstack/react-theme-provider': 3.0.9(react@19.1.0) color: 3.2.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ec0b3fb9..c2a98a16 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,3 @@ onlyBuiltDependencies: - esbuild - lefthook - unrs-resolver - -patchedDependencies: - react-native-paper: patches/react-native-paper.patch diff --git a/src/app/(tabs)/index.tsx b/src/app/(tabs)/index.tsx index 52884f03..c54845cd 100644 --- a/src/app/(tabs)/index.tsx +++ b/src/app/(tabs)/index.tsx @@ -4,7 +4,7 @@ import SearchSuggestions from '@/features/home/SearchSuggestions' import { usePersonalInformation } from '@/hooks/queries/bilibili/user' import useAppStore from '@/hooks/stores/useAppStore' import { queryClient } from '@/lib/config/queryClient' -import { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' import { matchSearchStrategies, navigateWithSearchStrategy, diff --git a/src/app/(tabs)/settings.tsx b/src/app/(tabs)/settings.tsx index 923720c4..d0a615c0 100644 --- a/src/app/(tabs)/settings.tsx +++ b/src/app/(tabs)/settings.tsx @@ -1,9 +1,9 @@ import NowPlayingBar from '@/components/NowPlayingBar' -import useCurrentTrack from '@/hooks/stores/playerHooks/useCurrentTrack' import useAppStore from '@/hooks/stores/useAppStore' import { useModalStore } from '@/hooks/stores/useModalStore' +import { usePlayerStore } from '@/hooks/stores/usePlayerStore' import { checkForAppUpdate } from '@/lib/services/updateService' -import { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' import toast from '@/utils/toast' import * as Application from 'expo-application' import * as Clipboard from 'expo-clipboard' @@ -24,7 +24,7 @@ const updateTime = Updates.createdAt export default function SettingsPage() { const insets = useSafeAreaInsets() - const currentTrack = useCurrentTrack() + const haveTrack = usePlayerStore((state) => !!state.currentTrackUniqueKey) const colors = useTheme().colors return ( @@ -38,7 +38,7 @@ export default function SettingsPage() { style={{ flex: 1, paddingTop: insets.top + 8, - paddingBottom: currentTrack ? 70 : insets.bottom, + paddingBottom: haveTrack ? 70 : insets.bottom, }} > { + return +} + export default function DownloadPage() { const { colors } = useTheme() const router = useRouter() @@ -23,11 +27,7 @@ export default function DownloadPage() { const start = useDownloadManagerStore((state) => state.startDownload) const clearAll = useDownloadManagerStore((state) => state.clearAll) - const currentTrack = useCurrentTrack() - - const renderItem = useCallback(({ item }: { item: DownloadTask }) => { - return - }, []) + const haveTrack = usePlayerStore((state) => !!state.currentTrackUniqueKey) const keyExtractor = useCallback((item: DownloadTask) => item.uniqueKey, []) @@ -50,7 +50,7 @@ export default function DownloadPage() { renderItem={renderItem} keyExtractor={keyExtractor} contentContainerStyle={{ - paddingBottom: currentTrack ? 70 + insets.bottom : insets.bottom, + paddingBottom: haveTrack ? 70 + insets.bottom : insets.bottom, }} /> diff --git a/src/app/leaderboard.tsx b/src/app/leaderboard.tsx index e701f2db..89e4f9d3 100644 --- a/src/app/leaderboard.tsx +++ b/src/app/leaderboard.tsx @@ -4,7 +4,7 @@ import { usePlayCountLeaderBoardPaginated, useTotalPlaybackDuration, } from '@/hooks/queries/db/track' -import useCurrentTrack from '@/hooks/stores/playerHooks/useCurrentTrack' +import { usePlayerStore } from '@/hooks/stores/usePlayerStore' import type { Track } from '@/types/core/media' import { FlashList } from '@shopify/flash-list' import { useRouter } from 'expo-router' @@ -40,11 +40,24 @@ const formatDurationToWords = (seconds: number) => { return parts.join(' ') } +const renderItem = ({ + item, + index, +}: { + item: LeaderBoardItemData + index: number +}) => ( + +) + export default function LeaderBoardPage() { const { colors } = useTheme() const router = useRouter() const insets = useSafeAreaInsets() - const currentTrack = useCurrentTrack() + const haveTrack = usePlayerStore((state) => !!state.currentTrackUniqueKey) const { data: leaderBoardData, @@ -66,16 +79,6 @@ export default function LeaderBoardPage() { return formatDurationToWords(totalDurationData) }, [totalDurationData, isTotalDurationError]) - const renderItem = useCallback( - ({ item, index }: { item: LeaderBoardItemData; index: number }) => ( - - ), - [], - ) - const keyExtractor = useCallback( (item: LeaderBoardItemData) => item.track.uniqueKey, [], @@ -123,7 +126,7 @@ export default function LeaderBoardPage() { renderItem={renderItem} keyExtractor={keyExtractor} contentContainerStyle={{ - paddingBottom: currentTrack ? 70 + insets.bottom : insets.bottom, + paddingBottom: haveTrack ? 70 + insets.bottom : insets.bottom, }} onEndReached={onEndReached} onEndReachedThreshold={0.8} diff --git a/src/app/player.tsx b/src/app/player.tsx index 64b6a99c..a395d884 100644 --- a/src/app/player.tsx +++ b/src/app/player.tsx @@ -5,7 +5,7 @@ import { PlayerHeader } from '@/features/player/components/PlayerHeader' import Lyrics from '@/features/player/components/PlayerLyrics' import { PlayerSlider } from '@/features/player/components/PlayerSlider' import { TrackInfo } from '@/features/player/components/PlayerTrackInfo' -import useCurrentTrack from '@/hooks/stores/playerHooks/useCurrentTrack' +import useCurrentTrack from '@/hooks/player/useCurrentTrack' import * as Haptics from '@/utils/haptics' import type { BottomSheetMethods } from '@gorhom/bottom-sheet/lib/typescript/types' import { useImage } from 'expo-image' diff --git a/src/app/playlist/local/[id].tsx b/src/app/playlist/local/[id].tsx index 53420ce3..37fd6f58 100644 --- a/src/app/playlist/local/[id].tsx +++ b/src/app/playlist/local/[id].tsx @@ -23,8 +23,8 @@ import { useModalStore } from '@/hooks/stores/useModalStore' import { useDebouncedValue } from '@/hooks/utils/useDebouncedValue' import type { CreateArtistPayload } from '@/types/services/artist' import type { CreateTrackPayload } from '@/types/services/track' +import { toastAndLogError } from '@/utils/error-handling' import * as Haptics from '@/utils/haptics' -import { toastAndLogError } from '@/utils/log' import toast from '@/utils/toast' import { useLocalSearchParams, useRouter } from 'expo-router' import { useCallback, useEffect, useState } from 'react' diff --git a/src/app/test.tsx b/src/app/test.tsx index a5672287..619e488c 100644 --- a/src/app/test.tsx +++ b/src/app/test.tsx @@ -1,11 +1,11 @@ import { alert } from '@/components/modals/AlertModal' import NowPlayingBar from '@/components/NowPlayingBar' -import useCurrentTrack from '@/hooks/stores/playerHooks/useCurrentTrack' import useDownloadManagerStore from '@/hooks/stores/useDownloadManagerStore' import { usePlayerStore } from '@/hooks/stores/usePlayerStore' import { downloadService } from '@/lib/services/downloadService' import lyricService from '@/lib/services/lyricService' -import log, { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' +import log from '@/utils/log' import toast from '@/utils/toast' import * as Updates from 'expo-updates' import { useState } from 'react' @@ -21,7 +21,7 @@ export default function TestPage() { const { isUpdatePending } = Updates.useUpdates() const insets = useSafeAreaInsets() const { colors } = useTheme() - const currentTrack = useCurrentTrack() + const haveTrack = usePlayerStore((state) => !!state.currentTrackUniqueKey) const testCheckUpdate = async () => { try { @@ -149,7 +149,7 @@ export default function TestPage() { > diff --git a/src/components/NowPlayingBar.tsx b/src/components/NowPlayingBar.tsx index fb7c0694..d94c0ddb 100644 --- a/src/components/NowPlayingBar.tsx +++ b/src/components/NowPlayingBar.tsx @@ -1,5 +1,5 @@ import useAnimatedTrackProgress from '@/hooks/player/useAnimatedTrackProgress' -import useCurrentTrack from '@/hooks/stores/playerHooks/useCurrentTrack' +import useCurrentTrack from '@/hooks/player/useCurrentTrack' import { usePlayerStore } from '@/hooks/stores/usePlayerStore' import * as Haptics from '@/utils/haptics' import { Image } from 'expo-image' diff --git a/src/components/common/FunctionalMenu.tsx b/src/components/common/FunctionalMenu.tsx index 227e0c6a..9332d10b 100644 --- a/src/components/common/FunctionalMenu.tsx +++ b/src/components/common/FunctionalMenu.tsx @@ -32,6 +32,7 @@ const FunctionalMenu = memo(function FunctionalMenu({ {...props} onDismiss={onClose} visible={visible} + key={String(visible)} style={{ opacity: showContent ? 1 : 0, }} diff --git a/src/components/modals/PlayerQueueModal.tsx b/src/components/modals/PlayerQueueModal.tsx index a3653551..43c63aee 100644 --- a/src/components/modals/PlayerQueueModal.tsx +++ b/src/components/modals/PlayerQueueModal.tsx @@ -1,6 +1,6 @@ +import useCurrentQueue from '@/hooks/player/useCurrentQueue' +import useCurrentTrack from '@/hooks/player/useCurrentTrack' import usePreventRemove from '@/hooks/router/usePreventRemove' -import useCurrentQueue from '@/hooks/stores/playerHooks/useCurrentQueue' -import useCurrentTrack from '@/hooks/stores/playerHooks/useCurrentTrack' import { usePlayerStore } from '@/hooks/stores/usePlayerStore' import type { Track } from '@/types/core/media' import type { BottomSheetFlatListMethods } from '@gorhom/bottom-sheet' diff --git a/src/components/modals/edit-metadata/editPlaylistMetadataModal.tsx b/src/components/modals/edit-metadata/editPlaylistMetadataModal.tsx index 431bd659..10619de0 100644 --- a/src/components/modals/edit-metadata/editPlaylistMetadataModal.tsx +++ b/src/components/modals/edit-metadata/editPlaylistMetadataModal.tsx @@ -2,7 +2,8 @@ import { useEditPlaylistMetadata } from '@/hooks/mutations/db/playlist' import { useModalStore } from '@/hooks/stores/useModalStore' import { bilibiliFacade } from '@/lib/facades/bilibili' import type { Playlist } from '@/types/core/media' -import log, { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' +import log from '@/utils/log' import toast from '@/utils/toast' import * as DocumentPicker from 'expo-document-picker' import * as FileSystem from 'expo-file-system' diff --git a/src/components/modals/login/CookieLoginModal.tsx b/src/components/modals/login/CookieLoginModal.tsx index 797391d8..23bf5110 100644 --- a/src/components/modals/login/CookieLoginModal.tsx +++ b/src/components/modals/login/CookieLoginModal.tsx @@ -2,7 +2,7 @@ import { favoriteListQueryKeys } from '@/hooks/queries/bilibili/favorite' import { userQueryKeys } from '@/hooks/queries/bilibili/user' import useAppStore, { serializeCookieObject } from '@/hooks/stores/useAppStore' import { useModalStore } from '@/hooks/stores/useModalStore' -import { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' import toast from '@/utils/toast' import { useQueryClient } from '@tanstack/react-query' import { useCallback, useMemo, useState } from 'react' diff --git a/src/components/modals/lyrics/EditLyrics.tsx b/src/components/modals/lyrics/EditLyrics.tsx index 63026240..e97c42b3 100644 --- a/src/components/modals/lyrics/EditLyrics.tsx +++ b/src/components/modals/lyrics/EditLyrics.tsx @@ -3,7 +3,7 @@ import { useModalStore } from '@/hooks/stores/useModalStore' import { queryClient } from '@/lib/config/queryClient' import lyricService from '@/lib/services/lyricService' import type { ParsedLrc } from '@/types/player/lyrics' -import { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' import { mergeLrc, parseLrc } from '@/utils/lyrics' import toast from '@/utils/toast' import { useState } from 'react' diff --git a/src/components/modals/lyrics/ManualSearchLyrics.tsx b/src/components/modals/lyrics/ManualSearchLyrics.tsx index 99b46106..1f57b7ae 100644 --- a/src/components/modals/lyrics/ManualSearchLyrics.tsx +++ b/src/components/modals/lyrics/ManualSearchLyrics.tsx @@ -1,10 +1,11 @@ import { useFetchLyrics } from '@/hooks/mutations/lyrics' import { useManualSearchLyrics } from '@/hooks/queries/lyrics' import { useModalStore } from '@/hooks/stores/useModalStore' +import type { ListRenderItemInfoWithExtraData } from '@/types/flashlist' import type { LyricSearchResult } from '@/types/player/lyrics' import { formatDurationToHHMMSS } from '@/utils/time' import { FlashList } from '@shopify/flash-list' -import { memo, useCallback, useState } from 'react' +import { memo, useCallback, useMemo, useState } from 'react' import { View } from 'react-native' import { ActivityIndicator, @@ -19,6 +20,26 @@ const SOURCE_MAP = { netease: '网易云', } +const renderItem = ({ + item, + extraData, +}: ListRenderItemInfoWithExtraData< + LyricSearchResult[0], + { + isFetchingLyrics: boolean + handlePressItem: (item: LyricSearchResult[0]) => void + } +>) => { + if (!extraData) throw new Error('Extradata 不存在') + return ( + + ) +} + const SearchItem = memo(function SearchItem({ item, onPress, @@ -70,17 +91,8 @@ const ManualSearchLyricsModal = ({ }, [close, fetchLyrics, uniqueKey], ) - - const renderItem = useCallback( - ({ item }: { item: LyricSearchResult[0] }) => { - return ( - - ) - }, + const extraData = useMemo( + () => ({ isFetchingLyrics, handlePressItem }), [handlePressItem, isFetchingLyrics], ) @@ -116,6 +128,7 @@ const ManualSearchLyricsModal = ({ data={searchResult} renderItem={renderItem} keyExtractor={keyExtractor} + extraData={extraData} /> ) } diff --git a/src/components/modals/playlist/BatchAddTracksToLocalPlaylist.tsx b/src/components/modals/playlist/BatchAddTracksToLocalPlaylist.tsx index e74a7c25..850d82df 100644 --- a/src/components/modals/playlist/BatchAddTracksToLocalPlaylist.tsx +++ b/src/components/modals/playlist/BatchAddTracksToLocalPlaylist.tsx @@ -6,10 +6,34 @@ import { useBatchAddTracksToLocalPlaylist } from '@/hooks/mutations/db/playlist' import { usePlaylistLists } from '@/hooks/queries/db/playlist' import { useModalStore } from '@/hooks/stores/useModalStore' import type { Playlist } from '@/types/core/media' +import type { ListRenderItemInfoWithExtraData } from '@/types/flashlist' import type { CreateArtistPayload } from '@/types/services/artist' import type { CreateTrackPayload } from '@/types/services/track' import { FlashList } from '@shopify/flash-list' +const renderPlaylistItem = ({ + item, + extraData, +}: ListRenderItemInfoWithExtraData< + Playlist, + { selectedPlaylistId: number; setSelectedPlaylistId: (id: number) => void } +>) => { + if (!extraData) throw new Error('Extradata 不存在') + const isChecked = extraData.selectedPlaylistId === item.id + const isDisabled = item.type !== 'local' + const setSelectedPlaylistId = extraData.setSelectedPlaylistId + + return ( + !isDisabled && setSelectedPlaylistId(item.id)} + disabled={isDisabled} + /> + ) +} + const BatchAddTracksToLocalPlaylistModal = memo( function AddTracksToLocalPlaylistModal({ payloads, @@ -68,26 +92,16 @@ const BatchAddTracksToLocalPlaylistModal = memo( ) }, [batchAdd, close, isMutating, payloads, selectedPlaylistId]) - const renderPlaylistItem = useCallback( - ({ item }: { item: Playlist }) => { - const isChecked = selectedPlaylistId === item.id - const isDisabled = item.type !== 'local' + const keyExtractor = useCallback((item: Playlist) => item.id.toString(), []) - return ( - !isDisabled && setSelectedPlaylistId(item.id)} - disabled={isDisabled} - /> - ) - }, - [selectedPlaylistId], + const extraData = useMemo( + () => ({ + selectedPlaylistId, + setSelectedPlaylistId, + }), + [selectedPlaylistId, setSelectedPlaylistId], ) - const keyExtractor = useCallback((item: Playlist) => item.id.toString(), []) - const renderContent = () => { if (isLoading) { return ( @@ -120,7 +134,7 @@ const BatchAddTracksToLocalPlaylistModal = memo( data={filteredPlaylists ?? []} renderItem={renderPlaylistItem} keyExtractor={keyExtractor} - extraData={selectedPlaylistId} + extraData={extraData} showsVerticalScrollIndicator={false} ListEmptyComponent={ void + } +>) => { + if (!extraData) throw new Error('Extradata 不存在') + const { checkedPlaylistIds, handleCheckboxPress } = extraData + const isChecked = checkedPlaylistIds.includes(item.id) + const isDisabled = item.type !== 'local' + + return ( + + ) +} + const PlaylistListItem = memo(function PlaylistListItem({ id, title, @@ -136,6 +163,14 @@ const UpdateTrackLocalPlaylistsModal = memo( close, ]) + const extraData = useMemo( + () => ({ + checkedPlaylistIds, + handleCheckboxPress, + }), + [checkedPlaylistIds, handleCheckboxPress], + ) + const handleDismiss = () => { if (isMutating) return close() @@ -146,24 +181,6 @@ const UpdateTrackLocalPlaylistsModal = memo( if (isContainingTrackError) void refetchContainingTrack() } - const renderPlaylistItem = useCallback( - ({ item }: { item: Playlist }) => { - const isChecked = checkedPlaylistIds.includes(item.id) - const isDisabled = item.type !== 'local' - - return ( - - ) - }, - [checkedPlaylistIds, handleCheckboxPress], - ) - const keyExtractor = useCallback((item: Playlist) => item.id.toString(), []) const renderContent = () => { @@ -198,7 +215,7 @@ const UpdateTrackLocalPlaylistsModal = memo( data={filteredPlaylists ?? []} renderItem={renderPlaylistItem} keyExtractor={keyExtractor} - extraData={checkedPlaylistIds} + extraData={extraData} ListEmptyComponent={ ( + +) + const CollectionListComponent = memo(() => { const { colors } = useTheme() - const currentTrack = useCurrentTrack() + const haveTrack = usePlayerStore((state) => !!state.currentTrackUniqueKey) const [refreshing, setRefreshing] = useState(false) const enable = useAppStore((state) => state.hasBilibiliCookie()) @@ -29,12 +33,6 @@ const CollectionListComponent = memo(() => { fetchNextPage, } = useInfiniteCollectionsList(Number(userInfo?.mid)) - const renderCollectionItem = useCallback( - ({ item }: { item: BilibiliCollection }) => ( - - ), - [], - ) const keyExtractor = useCallback( (item: BilibiliCollection) => item.id.toString(), [], @@ -95,7 +93,7 @@ const CollectionListComponent = memo(() => { /> } keyExtractor={keyExtractor} - contentContainerStyle={{ paddingBottom: currentTrack ? 70 : 10 }} + contentContainerStyle={{ paddingBottom: haveTrack ? 70 : 10 }} showsVerticalScrollIndicator={false} onEndReached={hasNextPage ? () => fetchNextPage() : undefined} ListFooterComponent={ diff --git a/src/features/library/favorite/FavoriteFolderList.tsx b/src/features/library/favorite/FavoriteFolderList.tsx index e4fcc024..78d141d4 100644 --- a/src/features/library/favorite/FavoriteFolderList.tsx +++ b/src/features/library/favorite/FavoriteFolderList.tsx @@ -3,8 +3,8 @@ import { DataFetchingPending } from '@/features/library/shared/DataFetchingPendi import TabDisable from '@/features/library/shared/TabDisabled' import { useGetFavoritePlaylists } from '@/hooks/queries/bilibili/favorite' import { usePersonalInformation } from '@/hooks/queries/bilibili/user' -import useCurrentTrack from '@/hooks/stores/playerHooks/useCurrentTrack' import useAppStore from '@/hooks/stores/useAppStore' +import { usePlayerStore } from '@/hooks/stores/usePlayerStore' import type { BilibiliPlaylist } from '@/types/apis/bilibili' import { FlashList } from '@shopify/flash-list' import { useRouter } from 'expo-router' @@ -13,10 +13,14 @@ import { RefreshControl, View } from 'react-native' import { Searchbar, Text, useTheme } from 'react-native-paper' import FavoriteFolderListItem from './FavoriteFolderListItem' +const renderPlaylistItem = ({ item }: { item: BilibiliPlaylist }) => ( + +) + const FavoriteFolderListComponent = memo(() => { const router = useRouter() const { colors } = useTheme() - const currentTrack = useCurrentTrack() + const haveTrack = usePlayerStore((state) => !!state.currentTrackUniqueKey) const [refreshing, setRefreshing] = useState(false) const [query, setQuery] = useState('') const enable = useAppStore((state) => state.hasBilibiliCookie()) @@ -30,12 +34,6 @@ const FavoriteFolderListComponent = memo(() => { isError: playlistsIsError, } = useGetFavoritePlaylists(userInfo?.mid) - const renderPlaylistItem = useCallback( - ({ item }: { item: BilibiliPlaylist }) => ( - - ), - [], - ) const keyExtractor = useCallback( (item: BilibiliPlaylist) => item.id.toString(), [], @@ -110,7 +108,7 @@ const FavoriteFolderListComponent = memo(() => { }} /> ( + +) + const LocalPlaylistListComponent = memo(() => { const { colors } = useTheme() - const currentTrack = useCurrentTrack() + const haveTrack = usePlayerStore((state) => !!state.currentTrackUniqueKey) const [refreshing, setRefreshing] = useState(false) const openModal = useModalStore((state) => state.open) @@ -24,10 +28,6 @@ const LocalPlaylistListComponent = memo(() => { isError: playlistsIsError, } = usePlaylistLists() - const renderPlaylistItem = useCallback( - ({ item }: { item: Playlist }) => , - [], - ) const keyExtractor = useCallback((item: Playlist) => item.id.toString(), []) const onRefresh = async () => { @@ -77,7 +77,7 @@ const LocalPlaylistListComponent = memo(() => { + const MultiPageVideosListComponent = memo(() => { const { colors } = useTheme() - const currentTrack = useCurrentTrack() + const haveTrack = usePlayerStore((state) => !!state.currentTrackUniqueKey) const [refreshing, setRefreshing] = useState(false) const enable = useAppStore((state) => state.hasBilibiliCookie()) @@ -41,12 +47,6 @@ const MultiPageVideosListComponent = memo(() => { playlists?.find((item) => item.title.startsWith('[mp]'))?.id, ) - const renderPlaylistItem = useCallback( - ({ item }: { item: BilibiliFavoriteListContent }) => ( - - ), - [], - ) const keyExtractor = useCallback( (item: BilibiliFavoriteListContent) => item.bvid, [], @@ -109,7 +109,7 @@ const MultiPageVideosListComponent = memo(() => { page.medias ?? []) ?? []} renderItem={renderPlaylistItem} diff --git a/src/features/player/components/PlayerFunctionalMenu.tsx b/src/features/player/components/PlayerFunctionalMenu.tsx index b4028d57..8bd42ae9 100644 --- a/src/features/player/components/PlayerFunctionalMenu.tsx +++ b/src/features/player/components/PlayerFunctionalMenu.tsx @@ -1,5 +1,5 @@ import FunctionalMenu from '@/components/common/FunctionalMenu' -import useCurrentTrack from '@/hooks/stores/playerHooks/useCurrentTrack' +import useCurrentTrack from '@/hooks/player/useCurrentTrack' import useDownloadManagerStore from '@/hooks/stores/useDownloadManagerStore' import { useModalStore } from '@/hooks/stores/useModalStore' import type { Track } from '@/types/core/media' diff --git a/src/features/player/components/PlayerHeader.tsx b/src/features/player/components/PlayerHeader.tsx index 34fe9221..ebee3fd7 100644 --- a/src/features/player/components/PlayerHeader.tsx +++ b/src/features/player/components/PlayerHeader.tsx @@ -1,4 +1,4 @@ -import useCurrentTrack from '@/hooks/stores/playerHooks/useCurrentTrack' +import useCurrentTrack from '@/hooks/player/useCurrentTrack' import { useRouter } from 'expo-router' import { View } from 'react-native' import { IconButton, Text } from 'react-native-paper' diff --git a/src/features/player/components/PlayerLyrics.tsx b/src/features/player/components/PlayerLyrics.tsx index 972a6115..1a363883 100644 --- a/src/features/player/components/PlayerLyrics.tsx +++ b/src/features/player/components/PlayerLyrics.tsx @@ -5,12 +5,20 @@ import { usePlayerStore } from '@/hooks/stores/usePlayerStore' import { queryClient } from '@/lib/config/queryClient' import lyricService from '@/lib/services/lyricService' import type { Track } from '@/types/core/media' +import type { ListRenderItemInfoWithExtraData } from '@/types/flashlist' import type { LyricLine } from '@/types/player/lyrics' -import { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' import type { FlashListRef } from '@shopify/flash-list' import { FlashList } from '@shopify/flash-list' import { LinearGradient } from 'expo-linear-gradient' -import { memo, useCallback, useLayoutEffect, useRef, useState } from 'react' +import { + memo, + useCallback, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react' import { Dimensions, ScrollView, View } from 'react-native' import { RectButton } from 'react-native-gesture-handler' import { @@ -158,6 +166,29 @@ const LyricLineItem = memo(function LyricLineItem({ ) }) +const renderItem = ({ + item, + index, + extraData, +}: ListRenderItemInfoWithExtraData< + LyricLine, + { + currentLyricIndex: number + handleJumpToLyric: (index: number) => void + } +>) => { + if (!extraData) throw new Error('Extradata 不存在') + const { currentLyricIndex, handleJumpToLyric } = extraData + return ( + + ) +} + export default function Lyrics({ onBackPress, track, @@ -239,15 +270,11 @@ export default function Lyrics({ [], ) - const renderItem = useCallback( - ({ item, index }: { item: LyricLine; index: number }) => ( - - ), + const extraData = useMemo( + () => ({ + currentLyricIndex, + handleJumpToLyric, + }), [currentLyricIndex, handleJumpToLyric], ) @@ -352,6 +379,7 @@ export default function Lyrics({ ref={flashListRef} data={lyrics.lyrics} renderItem={renderItem} + extraData={extraData} keyExtractor={keyExtractor} contentContainerStyle={{ justifyContent: 'center', diff --git a/src/features/player/components/PlayerTrackInfo.tsx b/src/features/player/components/PlayerTrackInfo.tsx index 8e4ca985..c3f67ca6 100644 --- a/src/features/player/components/PlayerTrackInfo.tsx +++ b/src/features/player/components/PlayerTrackInfo.tsx @@ -1,6 +1,6 @@ import { useThumbUpVideo } from '@/hooks/mutations/bilibili/video' +import useCurrentTrack from '@/hooks/player/useCurrentTrack' import { useGetVideoIsThumbUp } from '@/hooks/queries/bilibili/video' -import useCurrentTrack from '@/hooks/stores/playerHooks/useCurrentTrack' import { getGradientColors } from '@/utils/color' import type { ImageRef } from 'expo-image' import { Image } from 'expo-image' diff --git a/src/features/playlist/local/components/LocalTrackList.tsx b/src/features/playlist/local/components/LocalTrackList.tsx index 6df6291d..090d06fc 100644 --- a/src/features/playlist/local/components/LocalTrackList.tsx +++ b/src/features/playlist/local/components/LocalTrackList.tsx @@ -1,8 +1,9 @@ import FunctionalMenu from '@/components/common/FunctionalMenu' -import useCurrentTrack from '@/hooks/stores/playerHooks/useCurrentTrack' +import { usePlayerStore } from '@/hooks/stores/usePlayerStore' import type { Playlist, Track } from '@/types/core/media' +import type { ListRenderItemInfoWithExtraData } from '@/types/flashlist' import { FlashList } from '@shopify/flash-list' -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { View } from 'react-native' import { ActivityIndicator, @@ -30,6 +31,52 @@ interface LocalTrackListProps { isFetchingNextPage?: boolean } +const renderItem = ({ + item, + index, + extraData, +}: ListRenderItemInfoWithExtraData< + Track, + { + handleTrackPress: (track: Track) => void + handleMenuPress: (track: Track, anchor: { x: number; y: number }) => void + toggle: (id: number) => void + enterSelectMode: (id: number) => void + selected: Set + selectMode: boolean + playlist: Playlist + } +>) => { + if (!extraData) throw new Error('Extradata 不存在') + const { + handleTrackPress, + handleMenuPress, + toggle, + enterSelectMode, + selected, + selectMode, + playlist, + } = extraData + return ( + handleTrackPress(item)} + onMenuPress={(anchor) => { + handleMenuPress(item, anchor) + }} + disabled={ + item.source === 'bilibili' && !item.bilibiliMetadata.videoIsValid + } + data={item} + playlist={playlist} + toggleSelected={toggle} + isSelected={selected.has(item.id)} + selectMode={selectMode} + enterSelectMode={enterSelectMode} + /> + ) +} + export function LocalTrackList({ tracks, playlist, @@ -44,7 +91,7 @@ export function LocalTrackList({ isFetchingNextPage, hasNextPage, }: LocalTrackListProps) { - const currentTrack = useCurrentTrack() + const haveTrack = usePlayerStore((state) => !!state.currentTrackUniqueKey) const insets = useSafeAreaInsets() const theme = useTheme() @@ -69,52 +116,41 @@ export function LocalTrackList({ setMenuState((prev) => ({ ...prev, visible: false })) }, []) - const renderItem = useCallback( - ({ item, index }: { item: Track; index: number }) => { - return ( - handleTrackPress(item)} - onMenuPress={(anchor) => { - handleMenuPress(item, anchor) - }} - disabled={ - item.source === 'bilibili' && !item.bilibiliMetadata.videoIsValid - } - data={item} - playlist={playlist} - toggleSelected={toggle} - isSelected={selected.has(item.id)} - selectMode={selectMode} - enterSelectMode={enterSelectMode} - /> - ) - }, - [ - enterSelectMode, + const keyExtractor = useCallback((item: Track) => String(item.id), []) + + const extraData = useMemo( + () => ({ + selectMode, + selected, handleTrackPress, + handleMenuPress, + toggle, + enterSelectMode, playlist, + }), + [ selectMode, selected, - toggle, + handleTrackPress, handleMenuPress, + toggle, + enterSelectMode, + playlist, ], ) - const keyExtractor = useCallback((item: Track) => String(item.id), []) - return ( <> } ListHeaderComponent={ListHeaderComponent} keyExtractor={keyExtractor} contentContainerStyle={{ pointerEvents: menuState.visible ? 'none' : 'auto', - paddingBottom: currentTrack ? 70 + insets.bottom : insets.bottom, + paddingBottom: haveTrack ? 70 + insets.bottom : insets.bottom, }} showsVerticalScrollIndicator={false} ListFooterComponent={ diff --git a/src/features/playlist/local/hooks/useLocalPlaylistMenu.ts b/src/features/playlist/local/hooks/useLocalPlaylistMenu.ts index 2ca0ae52..d846f9e3 100644 --- a/src/features/playlist/local/hooks/useLocalPlaylistMenu.ts +++ b/src/features/playlist/local/hooks/useLocalPlaylistMenu.ts @@ -6,7 +6,7 @@ import { usePlayerStore } from '@/hooks/stores/usePlayerStore' import { queryClient } from '@/lib/config/queryClient' import { downloadService } from '@/lib/services/downloadService' import type { Playlist, Track } from '@/types/core/media' -import { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' import toast from '@/utils/toast' import * as Clipboard from 'expo-clipboard' import { useRouter } from 'expo-router' diff --git a/src/features/playlist/local/hooks/useLocalPlaylistPlayer.ts b/src/features/playlist/local/hooks/useLocalPlaylistPlayer.ts index 96e98d21..5b7bbc01 100644 --- a/src/features/playlist/local/hooks/useLocalPlaylistPlayer.ts +++ b/src/features/playlist/local/hooks/useLocalPlaylistPlayer.ts @@ -1,7 +1,7 @@ import { alert } from '@/components/modals/AlertModal' import { usePlayerStore } from '@/hooks/stores/usePlayerStore' import type { Track } from '@/types/core/media' -import { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' import { storage } from '@/utils/mmkv' import { useCallback } from 'react' import type { MMKV } from 'react-native-mmkv' diff --git a/src/features/playlist/remote/components/RemoteTrackList.tsx b/src/features/playlist/remote/components/RemoteTrackList.tsx index 02e9ca07..01c1ee4d 100644 --- a/src/features/playlist/remote/components/RemoteTrackList.tsx +++ b/src/features/playlist/remote/components/RemoteTrackList.tsx @@ -1,9 +1,10 @@ import FunctionalMenu from '@/components/common/FunctionalMenu' -import useCurrentTrack from '@/hooks/stores/playerHooks/useCurrentTrack' +import { usePlayerStore } from '@/hooks/stores/usePlayerStore' import type { BilibiliTrack } from '@/types/core/media' +import type { ListRenderItemInfoWithExtraData } from '@/types/flashlist' import * as Haptics from '@/utils/haptics' import { FlashList } from '@shopify/flash-list' -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { View } from 'react-native' import { ActivityIndicator, @@ -35,6 +36,67 @@ interface TrackListProps { hasNextPage?: boolean } +const renderItem = ({ + item, + index, + extraData, +}: ListRenderItemInfoWithExtraData< + BilibiliTrack, + { + toggle: (id: number) => void + playTrack: (track: BilibiliTrack) => void + handleMenuPress: ( + track: BilibiliTrack, + anchor: { x: number; y: number }, + ) => void + selected: Set + selectMode: boolean + enterSelectMode: (id: number) => void + showItemCover?: boolean + } +>) => { + if (!extraData) throw new Error('Extradata 不存在') + const { + toggle, + playTrack, + handleMenuPress, + selected, + selectMode, + enterSelectMode, + showItemCover, + } = extraData + return ( + playTrack(item)} + onMenuPress={(anchor) => handleMenuPress(item, anchor)} + showCoverImage={showItemCover ?? true} + data={{ + cover: item.coverUrl ?? undefined, + title: item.title, + duration: item.duration, + id: item.id, + artistName: item.artist?.name, + uniqueKey: item.uniqueKey, + }} + toggleSelected={() => { + void Haptics.performAndroidHapticsAsync( + Haptics.AndroidHaptics.Clock_Tick, + ) + toggle(item.id) + }} + isSelected={selected.has(item.id)} + selectMode={selectMode} + enterSelectMode={() => { + void Haptics.performAndroidHapticsAsync( + Haptics.AndroidHaptics.Long_Press, + ) + enterSelectMode(item.id) + }} + /> + ) +} + export function TrackList({ tracks, playTrack, @@ -53,7 +115,7 @@ export function TrackList({ hasNextPage, }: TrackListProps) { const colors = useTheme().colors - const currentTrack = useCurrentTrack() + const haveTrack = usePlayerStore((state) => !!state.currentTrackUniqueKey) const insets = useSafeAreaInsets() const [menuState, setMenuState] = useState<{ @@ -77,59 +139,36 @@ export function TrackList({ setMenuState((prev) => ({ ...prev, visible: false })) }, []) - const renderItem = useCallback( - ({ item, index }: { item: BilibiliTrack; index: number }) => { - return ( - playTrack(item)} - onMenuPress={(anchor) => handleMenuPress(item, anchor)} - showCoverImage={showItemCover ?? true} - data={{ - cover: item.coverUrl ?? undefined, - title: item.title, - duration: item.duration, - id: item.id, - artistName: item.artist?.name, - uniqueKey: item.uniqueKey, - }} - toggleSelected={() => { - void Haptics.performAndroidHapticsAsync( - Haptics.AndroidHaptics.Clock_Tick, - ) - toggle(item.id) - }} - isSelected={selected.has(item.id)} - selectMode={selectMode} - enterSelectMode={() => { - void Haptics.performAndroidHapticsAsync( - Haptics.AndroidHaptics.Long_Press, - ) - enterSelectMode(item.id) - }} - /> - ) - }, - [ - playTrack, - toggle, - selected, + const keyExtractor = useCallback((item: BilibiliTrack) => { + return String(item.id) + }, []) + + const extraData = useMemo( + () => ({ selectMode, + selected, + toggle, + playTrack, + handleMenuPress, enterSelectMode, + showItemCover, + }), + [ + selectMode, + selected, + toggle, + playTrack, handleMenuPress, + enterSelectMode, showItemCover, ], ) - const keyExtractor = useCallback((item: BilibiliTrack) => { - return String(item.id) - }, []) - return ( <> } ListHeaderComponent={ListHeaderComponent} @@ -139,7 +178,7 @@ export function TrackList({ contentContainerStyle={{ // 实现一个在 menu 弹出时,列表不可触摸的效果 pointerEvents: menuState.visible ? 'none' : 'auto', - paddingBottom: currentTrack ? 70 + insets.bottom : insets.bottom, + paddingBottom: haveTrack ? 70 + insets.bottom : insets.bottom, }} onEndReached={onEndReached} ListFooterComponent={ diff --git a/src/features/playlist/remote/hooks/useCheckLinkedToLocalPlaylist.ts b/src/features/playlist/remote/hooks/useCheckLinkedToLocalPlaylist.ts index 192532ad..3b951800 100644 --- a/src/features/playlist/remote/hooks/useCheckLinkedToLocalPlaylist.ts +++ b/src/features/playlist/remote/hooks/useCheckLinkedToLocalPlaylist.ts @@ -1,6 +1,6 @@ import { playlistService } from '@/lib/services/playlistService' import type { Playlist } from '@/types/core/media' -import { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' import { useEffect, useState } from 'react' /** diff --git a/src/hooks/mutations/bilibili/favorite.ts b/src/hooks/mutations/bilibili/favorite.ts index f68a05e8..abf98a5f 100644 --- a/src/hooks/mutations/bilibili/favorite.ts +++ b/src/hooks/mutations/bilibili/favorite.ts @@ -1,7 +1,8 @@ import { favoriteListQueryKeys } from '@/hooks/queries/bilibili/favorite' import { bilibiliApi } from '@/lib/api/bilibili/api' import { BilibiliApiError } from '@/lib/errors/thirdparty/bilibili' -import log, { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' +import log from '@/utils/log' import { returnOrThrowAsync } from '@/utils/neverthrow-utils' import toast from '@/utils/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' diff --git a/src/hooks/mutations/bilibili/video.ts b/src/hooks/mutations/bilibili/video.ts index 0361e137..ffbda341 100644 --- a/src/hooks/mutations/bilibili/video.ts +++ b/src/hooks/mutations/bilibili/video.ts @@ -1,7 +1,7 @@ import { videoDataQueryKeys } from '@/hooks/queries/bilibili/video' import { bilibiliApi } from '@/lib/api/bilibili/api' import { queryClient } from '@/lib/config/queryClient' -import { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' import { returnOrThrowAsync } from '@/utils/neverthrow-utils' import toast from '@/utils/toast' import { useMutation } from '@tanstack/react-query' diff --git a/src/hooks/mutations/db/playlist.ts b/src/hooks/mutations/db/playlist.ts index a9b007fb..b26f982b 100644 --- a/src/hooks/mutations/db/playlist.ts +++ b/src/hooks/mutations/db/playlist.ts @@ -7,7 +7,7 @@ import type { Playlist } from '@/types/core/media' import type { CreateArtistPayload } from '@/types/services/artist' import type { UpdatePlaylistPayload } from '@/types/services/playlist' import type { CreateTrackPayload } from '@/types/services/track' -import { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' import toast from '@/utils/toast' import { useMutation } from '@tanstack/react-query' diff --git a/src/hooks/stores/playerHooks/useCurrentQueue.ts b/src/hooks/player/useCurrentQueue.ts similarity index 100% rename from src/hooks/stores/playerHooks/useCurrentQueue.ts rename to src/hooks/player/useCurrentQueue.ts diff --git a/src/hooks/stores/playerHooks/useCurrentTrack.ts b/src/hooks/player/useCurrentTrack.ts similarity index 100% rename from src/hooks/stores/playerHooks/useCurrentTrack.ts rename to src/hooks/player/useCurrentTrack.ts diff --git a/src/hooks/stores/usePlayerStore.ts b/src/hooks/stores/usePlayerStore.ts index e11f6158..0fe8085d 100644 --- a/src/hooks/stores/usePlayerStore.ts +++ b/src/hooks/stores/usePlayerStore.ts @@ -12,11 +12,8 @@ import type { } from '@/types/core/playerStore' import { ProjectScope } from '@/types/core/scope' import type { RNTPTrack } from '@/types/rntp' -import log, { - flatErrorMessage, - reportErrorToSentry, - toastAndLogError, -} from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' +import log, { flatErrorMessage, reportErrorToSentry } from '@/utils/log' import { zustandStorage } from '@/utils/mmkv' import { checkAndUpdateAudioStream, diff --git a/src/lib/config/queryClient.ts b/src/lib/config/queryClient.ts index e3e2c2b7..8da493e0 100644 --- a/src/lib/config/queryClient.ts +++ b/src/lib/config/queryClient.ts @@ -1,7 +1,7 @@ import { useModalStore } from '@/hooks/stores/useModalStore' import { ThirdPartyError } from '@/lib/errors' import { BilibiliApiError } from '@/lib/errors/thirdparty/bilibili' -import { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' import toast from '@/utils/toast' import * as Sentry from '@sentry/react-native' import { QueryCache, QueryClient } from '@tanstack/react-query' diff --git a/src/lib/player/playerLogic.ts b/src/lib/player/playerLogic.ts index 9b2bef51..73581595 100644 --- a/src/lib/player/playerLogic.ts +++ b/src/lib/player/playerLogic.ts @@ -1,6 +1,7 @@ import { usePlayerStore } from '@/hooks/stores/usePlayerStore' import { ProjectScope } from '@/types/core/scope' -import log, { reportErrorToSentry, toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' +import log, { reportErrorToSentry } from '@/utils/log' import toast from '@/utils/toast' import TrackPlayer, { AppKilledPlaybackBehavior, @@ -14,10 +15,6 @@ const logger = log.extend('Player.Init') const initPlayer = async () => { logger.info('开始初始化播放器') - if (global.playerIsReady) { - logger.warning('播放器已经初始化过了') - return - } await PlayerLogic.preparePlayer() PlayerLogic.setupEventListeners() // 初始化后强制将 RNTP 重复模式设为 Off,循环由我们内部管理 @@ -27,6 +24,7 @@ const initPlayer = async () => { } let isResettingSleepTimer = false +let listenersAttached = false const PlayerLogic = { // 初始化播放器 @@ -74,6 +72,10 @@ const PlayerLogic = { // 设置事件监听器 setupEventListeners(): void { + if (listenersAttached) { + logger.debug('事件监听器已经设置过了,跳过。') + return // 关键!防止重复绑定 + } // 监听播放状态变化 TrackPlayer.addEventListener( Event.PlaybackState, @@ -248,6 +250,8 @@ const PlayerLogic = { }) } }) + + listenersAttached = true }, } diff --git a/src/lib/services/lyricService.ts b/src/lib/services/lyricService.ts index b7ffca53..e93cec38 100644 --- a/src/lib/services/lyricService.ts +++ b/src/lib/services/lyricService.ts @@ -4,7 +4,8 @@ import type { CustomError } from '@/lib/errors' import { DataParsingError, FileSystemError } from '@/lib/errors' import type { BilibiliTrack, Track } from '@/types/core/media' import type { LyricSearchResult, ParsedLrc } from '@/types/player/lyrics' -import log, { toastAndLogError } from '@/utils/log' +import { toastAndLogError } from '@/utils/error-handling' +import log from '@/utils/log' import * as FileSystem from 'expo-file-system' import { errAsync, okAsync, Result, ResultAsync } from 'neverthrow' diff --git a/src/types/flashlist.ts b/src/types/flashlist.ts new file mode 100644 index 00000000..69d48d0d --- /dev/null +++ b/src/types/flashlist.ts @@ -0,0 +1,8 @@ +import type { ListRenderItemInfo } from '@shopify/flash-list' + +export type ListRenderItemInfoWithExtraData = Omit< + ListRenderItemInfo, + 'extraData' +> & { + extraData?: TExtraData +} diff --git a/src/utils/error-handling.ts b/src/utils/error-handling.ts new file mode 100644 index 00000000..4e449b2d --- /dev/null +++ b/src/utils/error-handling.ts @@ -0,0 +1,41 @@ +import { CustomError } from '@/lib/errors' +import log, { flatErrorMessage } from './log' +import toast from './toast' + +/** + * 将错误消息和错误堆栈信息显示在 toast 上,并将错误信息记录到日志中(用于最顶端的调用者消费错误) + * @param error 原始错误对象 + * @param message 需要显示的信息 + * @param scope 日志作用域 + */ +export function toastAndLogError( + message: string, + error: unknown, + scope: string, +) { + if (error instanceof CustomError) { + toast.error(`${message} -- ${error.type}`, { + description: flatErrorMessage(error), + duration: Number.POSITIVE_INFINITY, + }) + log + .extend(scope) + .error(`${message} -- ${error.type}: ${flatErrorMessage(error)}`) + } else if (error instanceof Error) { + toast.error(message, { + description: flatErrorMessage(error), + duration: Number.POSITIVE_INFINITY, + }) + log.extend(scope).error(`${message}: ${flatErrorMessage(error)}`) + } else if (error === undefined) { + toast.error(message, { + duration: Number.POSITIVE_INFINITY, + }) + } else { + toast.error(message, { + description: String(error as unknown), + duration: Number.POSITIVE_INFINITY, + }) + log.extend(scope).error(`${message}`, error) + } +} diff --git a/src/utils/log.ts b/src/utils/log.ts index 1453921b..787a3285 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -9,7 +9,6 @@ import { logger, mapConsoleTransport, } from 'react-native-logs' -import toast from './toast' const isDev = __DEV__ @@ -164,44 +163,6 @@ export function reportErrorToSentry( log.error(`已上报错误到 sentry,id: ${id}`) } -/** - * 将错误消息和错误堆栈信息显示在 toast 上,并将错误信息记录到日志中(用于最顶端的调用者消费错误) - * @param error 原始错误对象 - * @param message 需要显示的信息 - * @param scope 日志作用域 - */ -export function toastAndLogError( - message: string, - error: unknown, - scope: string, -) { - if (error instanceof CustomError) { - toast.error(`${message} -- ${error.type}`, { - description: flatErrorMessage(error), - duration: Number.POSITIVE_INFINITY, - }) - log - .extend(scope) - .error(`${message} -- ${error.type}: ${flatErrorMessage(error)}`) - } else if (error instanceof Error) { - toast.error(message, { - description: flatErrorMessage(error), - duration: Number.POSITIVE_INFINITY, - }) - log.extend(scope).error(`${message}: ${flatErrorMessage(error)}`) - } else if (error === undefined) { - toast.error(message, { - duration: Number.POSITIVE_INFINITY, - }) - } else { - toast.error(message, { - description: String(error as unknown), - duration: Number.POSITIVE_INFINITY, - }) - log.extend(scope).error(`${message}`, error) - } -} - try { new EXPOFS.Directory(EXPOFS.Paths.document, 'logs').create({ intermediates: true, diff --git a/src/utils/lyrics.ts b/src/utils/lyrics.ts index 69e7d901..d2c54279 100644 --- a/src/utils/lyrics.ts +++ b/src/utils/lyrics.ts @@ -1,5 +1,6 @@ import type { ParsedLrc } from '@/types/player/lyrics' -import log, { toastAndLogError } from './log' +import { toastAndLogError } from '@/utils/error-handling' +import log from './log' const logger = log.extend('Utils.Lyrics') diff --git a/src/utils/search.ts b/src/utils/search.ts index 4b93f8fb..0e243d9a 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -1,7 +1,8 @@ import { bilibiliApi } from '@/lib/api/bilibili/api' import { av2bv } from '@/lib/api/bilibili/utils' import type { Router } from 'expo-router' -import log, { toastAndLogError } from './log' +import { toastAndLogError } from './error-handling' +import log from './log' import toast from './toast' const logger = log.extend('Utils.Search')