Full Code of bbplayer-app/BBPlayer for AI

dev 44e0a7422a85 cached
676 files
4.5 MB
1.2M tokens
1914 symbols
1 requests
Download .txt
Showing preview only (5,027K chars total). Download the full file or copy to clipboard to get everything.
Repository: bbplayer-app/BBPlayer
Branch: dev
Commit: 44e0a7422a85
Files: 676
Total size: 4.5 MB

Directory structure:
gitextract_ppffbibi/

├── .agent/
│   ├── rules/
│   │   ├── changelog.md
│   │   └── measure-layout.md
│   └── skills/
│       ├── gesture-handler-3-migration/
│       │   └── SKILL.md
│       └── upgrading-expo/
│           ├── SKILL.md
│           └── references/
│               ├── new-architecture.md
│               ├── react-19.md
│               └── react-compiler.md
├── .agents/
│   └── skills/
│       ├── react-doctor/
│       │   └── SKILL.md
│       └── react-native-ease-refactor/
│           └── SKILL.md
├── .easignore
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── release.yml
│   ├── wiki/
│   │   ├── Home.md
│   │   └── _Sidebar.md
│   └── workflows/
│       ├── build.yml
│       ├── check-lyricon-updates.yml
│       ├── nightly.yml
│       ├── pr-checks.yml
│       ├── update.yml
│       └── wiki.yml
├── .gitignore
├── .gitleaks-baseline.json
├── .gitleaks.toml
├── .npmrc
├── .oxfmtrc.json
├── .oxlintrc.json
├── .sisyphus/
│   ├── boulder.json
│   ├── evidence/
│   │   ├── task-1-complete.txt
│   │   ├── task-2-complete.txt
│   │   ├── task-3-lint-output.txt
│   │   ├── task-4-page-created.txt
│   │   ├── task-5-play-all.txt
│   │   ├── task-6-structure.txt
│   │   └── task-7-complete.txt
│   ├── notepads/
│   │   └── task-5-play-all/
│   │       └── learnings.md
│   └── plans/
│       └── homepage-ui-optimization.md
├── .syncpackrc
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── AGENTS.md
├── LICENSE
├── PRIVACY.md
├── README.md
├── apps/
│   ├── README.md
│   ├── backend/
│   │   ├── .dev.vars.example
│   │   ├── .gitignore
│   │   ├── drizzle.config.ts
│   │   ├── mise.toml
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── db/
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── index.ts
│   │   │   ├── middleware/
│   │   │   │   └── auth.ts
│   │   │   ├── routes/
│   │   │   │   ├── auth.ts
│   │   │   │   ├── me.ts
│   │   │   │   └── playlists.ts
│   │   │   ├── types.ts
│   │   │   └── validators/
│   │   │       ├── auth.ts
│   │   │       └── playlists.ts
│   │   ├── tsconfig.json
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.toml
│   ├── docs/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── docs/
│   │   │   ├── .vitepress/
│   │   │   │   ├── components/
│   │   │   │   │   ├── AppNotExistPage.vue
│   │   │   │   │   ├── SharePlaylistPage.vue
│   │   │   │   │   ├── ShareTrackPage.vue
│   │   │   │   │   └── shared-page.css
│   │   │   │   └── config.mts
│   │   │   ├── SPL.md
│   │   │   ├── app-not-exist.md
│   │   │   ├── guides/
│   │   │   │   ├── comments.md
│   │   │   │   ├── download.md
│   │   │   │   ├── external-playlist.md
│   │   │   │   ├── index.md
│   │   │   │   ├── install.md
│   │   │   │   ├── leaderboard.md
│   │   │   │   ├── lyrics.md
│   │   │   │   ├── player.md
│   │   │   │   ├── playlist.md
│   │   │   │   ├── search.md
│   │   │   │   ├── settings.md
│   │   │   │   └── shared-playlist.md
│   │   │   ├── index.md
│   │   │   ├── public/
│   │   │   │   └── .well-known/
│   │   │   │       └── assetlinks.json
│   │   │   └── share/
│   │   │       ├── playlist.md
│   │   │       └── track.md
│   │   ├── env.d.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── mobile/
│   │   ├── .gitignore
│   │   ├── .maestro/
│   │   │   ├── comments_flow.yaml
│   │   │   ├── common/
│   │   │   │   ├── open_player.yaml
│   │   │   │   └── setup.yaml
│   │   │   ├── playback_flow.yaml
│   │   │   ├── playlist_flow.yaml
│   │   │   ├── search_flow.yaml
│   │   │   └── sync_flow.yaml
│   │   ├── AGENTS.md
│   │   ├── CHANGELOG.md
│   │   ├── README.md
│   │   ├── app.config.ts
│   │   ├── assets/
│   │   │   └── config/
│   │   │       └── google-services/
│   │   │           ├── GoogleService-Info.plist
│   │   │           └── google-services.json
│   │   ├── babel.config.js
│   │   ├── docs/
│   │   │   ├── ARCHITECTURE.md
│   │   │   ├── BEST_PRACTICES.md
│   │   │   ├── CONTRIBUTING.md
│   │   │   ├── Home.md
│   │   │   ├── RELEASE.md
│   │   │   └── TECHNICAL_DEBT.md
│   │   ├── drizzle/
│   │   │   ├── 0000_productive_joystick.sql
│   │   │   ├── 0001_fast_trauma.sql
│   │   │   ├── 0002_groovy_maximus.sql
│   │   │   ├── 0003_glamorous_psylocke.sql
│   │   │   ├── 0004_smiling_beast.sql
│   │   │   ├── 0005_spotty_exiles.sql
│   │   │   ├── 0006_breezy_jigsaw.sql
│   │   │   ├── 0007_legal_thor.sql
│   │   │   ├── 0008_overrated_jimmy_woo.sql
│   │   │   ├── 0009_lethal_marten_broadcloak.sql
│   │   │   ├── 0010_brainy_anita_blake.sql
│   │   │   ├── 0011_grey_echo.sql
│   │   │   ├── 0012_blushing_human_fly.sql
│   │   │   ├── 0013_jittery_randall.sql
│   │   │   ├── 0014_flippant_sebastian_shaw.sql
│   │   │   ├── 0015_flippant_skaar.sql
│   │   │   ├── 0016_cheerful_stark_industries.sql
│   │   │   ├── 0017_rare_lifeguard.sql
│   │   │   ├── 0018_green_dracula.sql
│   │   │   ├── 0019_icy_mandarin.sql
│   │   │   ├── 0020_ambitious_sheva_callister.sql
│   │   │   ├── meta/
│   │   │   │   ├── 0000_snapshot.json
│   │   │   │   ├── 0001_snapshot.json
│   │   │   │   ├── 0002_snapshot.json
│   │   │   │   ├── 0003_snapshot.json
│   │   │   │   ├── 0004_snapshot.json
│   │   │   │   ├── 0005_snapshot.json
│   │   │   │   ├── 0006_snapshot.json
│   │   │   │   ├── 0007_snapshot.json
│   │   │   │   ├── 0008_snapshot.json
│   │   │   │   ├── 0009_snapshot.json
│   │   │   │   ├── 0010_snapshot.json
│   │   │   │   ├── 0011_snapshot.json
│   │   │   │   ├── 0012_snapshot.json
│   │   │   │   ├── 0013_snapshot.json
│   │   │   │   ├── 0014_snapshot.json
│   │   │   │   ├── 0015_snapshot.json
│   │   │   │   ├── 0016_snapshot.json
│   │   │   │   ├── 0017_snapshot.json
│   │   │   │   ├── 0018_snapshot.json
│   │   │   │   ├── 0019_snapshot.json
│   │   │   │   ├── 0020_snapshot.json
│   │   │   │   └── _journal.json
│   │   │   └── migrations.js
│   │   ├── drizzle.config.ts
│   │   ├── eas.json
│   │   ├── expo-plugins/
│   │   │   ├── withAbiFilters.js
│   │   │   ├── withAndroidGradleProperties.js
│   │   │   ├── withAndroidPlugin.js
│   │   │   └── withKotlinSerialization.js
│   │   ├── index.js
│   │   ├── metro.config.js
│   │   ├── mise.toml
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── app/
│   │   │   │   ├── (tabs)/
│   │   │   │   │   ├── _layout.tsx
│   │   │   │   │   ├── index.tsx
│   │   │   │   │   ├── library/
│   │   │   │   │   │   └── [tab].tsx
│   │   │   │   │   └── settings/
│   │   │   │   │       └── index.tsx
│   │   │   │   ├── +native-intent.ts
│   │   │   │   ├── +not-found.tsx
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── comments/
│   │   │   │   │   ├── [bvid].tsx
│   │   │   │   │   └── reply.tsx
│   │   │   │   ├── download.tsx
│   │   │   │   ├── downloaded.tsx
│   │   │   │   ├── history/
│   │   │   │   │   ├── [date].tsx
│   │   │   │   │   └── overall.tsx
│   │   │   │   ├── modal.tsx
│   │   │   │   ├── player.tsx
│   │   │   │   ├── playlist/
│   │   │   │   │   ├── external-sync.tsx
│   │   │   │   │   ├── local/
│   │   │   │   │   │   └── [id].tsx
│   │   │   │   │   ├── recently/
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   └── remote/
│   │   │   │   │       ├── collection/
│   │   │   │   │       │   └── [id].tsx
│   │   │   │   │       ├── favorite/
│   │   │   │   │       │   └── [id].tsx
│   │   │   │   │       ├── multipage/
│   │   │   │   │       │   └── [bvid].tsx
│   │   │   │   │       ├── search-result/
│   │   │   │   │       │   ├── fav/
│   │   │   │   │       │   │   └── [query].tsx
│   │   │   │   │       │   └── global/
│   │   │   │   │       │       └── [query].tsx
│   │   │   │   │       ├── toview.tsx
│   │   │   │   │       └── uploader/
│   │   │   │   │           └── [mid].tsx
│   │   │   │   ├── settings/
│   │   │   │   │   ├── appearance.tsx
│   │   │   │   │   ├── donate.tsx
│   │   │   │   │   ├── download.tsx
│   │   │   │   │   ├── general.tsx
│   │   │   │   │   ├── lyrics.tsx
│   │   │   │   │   └── playback.tsx
│   │   │   │   ├── share/
│   │   │   │   │   └── playlist.tsx
│   │   │   │   └── test.tsx
│   │   │   ├── assets/
│   │   │   │   └── lottie/
│   │   │   │       ├── play-pause.json
│   │   │   │       ├── skip-next.json
│   │   │   │       └── skip-prev.json
│   │   │   ├── components/
│   │   │   │   ├── ErrorBoundary.tsx
│   │   │   │   ├── ModalRegistry.tsx
│   │   │   │   ├── NowPlayingBar.tsx
│   │   │   │   ├── common/
│   │   │   │   │   ├── AnimatedModalOverlay.tsx
│   │   │   │   │   ├── Button.tsx
│   │   │   │   │   ├── CoverWithPlaceHolder.tsx
│   │   │   │   │   ├── FunctionalMenu.tsx
│   │   │   │   │   └── IconButton.tsx
│   │   │   │   ├── modals/
│   │   │   │   │   ├── AlertModal.tsx
│   │   │   │   │   ├── PlayerQueueModal.tsx
│   │   │   │   │   ├── app/
│   │   │   │   │   │   ├── DonationQRModal.tsx
│   │   │   │   │   │   ├── UpdateAppModal.tsx
│   │   │   │   │   │   └── WelcomeModal.tsx
│   │   │   │   │   ├── bilibili/
│   │   │   │   │   │   └── AddVideoToBilibiliFavModal.tsx
│   │   │   │   │   ├── edit-metadata/
│   │   │   │   │   │   ├── editPlaylistMetadataModal.tsx
│   │   │   │   │   │   └── editTrackMetadataModal.tsx
│   │   │   │   │   ├── login/
│   │   │   │   │   │   ├── CookieLoginModal.tsx
│   │   │   │   │   │   ├── PhoneLoginModal.tsx
│   │   │   │   │   │   ├── QRCodeLoginModal.tsx
│   │   │   │   │   │   └── steps/
│   │   │   │   │   │       ├── GeetestVerifyStep.tsx
│   │   │   │   │   │       ├── InputCodeStep.tsx
│   │   │   │   │   │       ├── InputPhoneStep.tsx
│   │   │   │   │   │       └── SuccessStep.tsx
│   │   │   │   │   ├── lyrics/
│   │   │   │   │   │   ├── EditLyrics.tsx
│   │   │   │   │   │   └── ManualSearchLyrics.tsx
│   │   │   │   │   ├── player/
│   │   │   │   │   │   ├── DanmakuSettingsModal.tsx
│   │   │   │   │   │   ├── LyricsSelectionModal.tsx
│   │   │   │   │   │   ├── PlaybackSpeedModal.tsx
│   │   │   │   │   │   ├── SleepTimerModal.tsx
│   │   │   │   │   │   └── SongShareModal.tsx
│   │   │   │   │   ├── playlist/
│   │   │   │   │   │   ├── BatchAddTracksToLocalPlaylist.tsx
│   │   │   │   │   │   ├── CreatePlaylistModal.tsx
│   │   │   │   │   │   ├── DuplicateLocalPlaylistModal.tsx
│   │   │   │   │   │   ├── EnableSharingModal.tsx
│   │   │   │   │   │   ├── FavoriteSyncProgressModal.tsx
│   │   │   │   │   │   ├── InputExternalPlaylistInfo.tsx
│   │   │   │   │   │   ├── ManualMatchExternalSync.tsx
│   │   │   │   │   │   ├── MergePlaylistsModal.tsx
│   │   │   │   │   │   ├── SaveQueueToPlaylistModal.tsx
│   │   │   │   │   │   ├── SubscribeToSharedPlaylistModal.tsx
│   │   │   │   │   │   ├── SyncLocalToBilibiliModal.tsx
│   │   │   │   │   │   └── UpdateTrackLocalPlaylistsModal.tsx
│   │   │   │   │   └── settings/
│   │   │   │   │       ├── CoverDownloadProgressModal.tsx
│   │   │   │   │       └── ExportDownloadsProgressModal.tsx
│   │   │   │   └── providers.tsx
│   │   │   ├── features/
│   │   │   │   ├── comments/
│   │   │   │   │   └── components/
│   │   │   │   │       └── CommentItem.tsx
│   │   │   │   ├── downloads/
│   │   │   │   │   ├── DownloadHeader.tsx
│   │   │   │   │   └── DownloadTaskItem.tsx
│   │   │   │   ├── history/
│   │   │   │   │   └── HistoryListItem.tsx
│   │   │   │   ├── home/
│   │   │   │   │   └── SearchSuggestions.tsx
│   │   │   │   ├── library/
│   │   │   │   │   ├── collection/
│   │   │   │   │   │   ├── CollectionList.tsx
│   │   │   │   │   │   └── CollectionListItem.tsx
│   │   │   │   │   ├── favorite/
│   │   │   │   │   │   ├── FavoriteFolderList.tsx
│   │   │   │   │   │   └── FavoriteFolderListItem.tsx
│   │   │   │   │   ├── local/
│   │   │   │   │   │   ├── LocalPlaylistItem.tsx
│   │   │   │   │   │   └── LocalPlaylistList.tsx
│   │   │   │   │   ├── multipage/
│   │   │   │   │   │   ├── MultiPageVideosItem.tsx
│   │   │   │   │   │   └── MultiPageVideosList.tsx
│   │   │   │   │   ├── shared/
│   │   │   │   │   │   ├── DataFetchingError.tsx
│   │   │   │   │   │   └── TabDisabled.tsx
│   │   │   │   │   └── skeletons/
│   │   │   │   │       └── LibraryTabSkeleton.tsx
│   │   │   │   ├── player/
│   │   │   │   │   ├── components/
│   │   │   │   │   │   ├── BGStreamerShader.ts
│   │   │   │   │   │   ├── LyricsControlOverlay.tsx
│   │   │   │   │   │   ├── PlayerControls.tsx
│   │   │   │   │   │   ├── PlayerFunctionalMenu.tsx
│   │   │   │   │   │   ├── PlayerHeader.tsx
│   │   │   │   │   │   ├── PlayerLyrics.tsx
│   │   │   │   │   │   ├── PlayerMainTab.tsx
│   │   │   │   │   │   ├── PlayerSlider.tsx
│   │   │   │   │   │   ├── PlayerTrackInfo.tsx
│   │   │   │   │   │   ├── SpectrumVisualizer.tsx
│   │   │   │   │   │   ├── danmaku/
│   │   │   │   │   │   │   └── DanmakuView.tsx
│   │   │   │   │   │   ├── lyrics/
│   │   │   │   │   │   │   ├── KaraokeWord.tsx
│   │   │   │   │   │   │   ├── LyricActionSheet.tsx
│   │   │   │   │   │   │   ├── LyricLineItem.tsx
│   │   │   │   │   │   │   └── LyricsOffsetControl.tsx
│   │   │   │   │   │   └── sharing/
│   │   │   │   │   │       ├── LyricsShareCard.tsx
│   │   │   │   │   │       └── SongShareCard.tsx
│   │   │   │   │   └── hooks/
│   │   │   │   │       ├── danmaku/
│   │   │   │   │       │   ├── constants.ts
│   │   │   │   │       │   ├── useDanmakuLoader.ts
│   │   │   │   │       │   └── useDanmakuRender.ts
│   │   │   │   │       ├── useLyricSync.ts
│   │   │   │   │       └── usePlayerHeaderAnimation.ts
│   │   │   │   └── playlist/
│   │   │   │       ├── local/
│   │   │   │       │   ├── components/
│   │   │   │       │   │   ├── LocalPlaylistHeader.tsx
│   │   │   │       │   │   ├── LocalPlaylistItem.tsx
│   │   │   │       │   │   ├── LocalTrackList.tsx
│   │   │   │       │   │   ├── PlaylistError.tsx
│   │   │   │       │   │   ├── SharedPlaylistMembersSheet.tsx
│   │   │   │       │   │   └── SyncFailuresSheet.tsx
│   │   │   │       │   └── hooks/
│   │   │   │       │       ├── useLocalPlaylistMenu.ts
│   │   │   │       │       ├── useLocalPlaylistPlayer.ts
│   │   │   │       │       └── useTrackSelection.ts
│   │   │   │       ├── remote/
│   │   │   │       │   ├── components/
│   │   │   │       │   │   ├── FlashingTrackListItem.tsx
│   │   │   │       │   │   ├── PlaylistError.tsx
│   │   │   │       │   │   ├── PlaylistHeader.tsx
│   │   │   │       │   │   ├── PlaylistItem.tsx
│   │   │   │       │   │   └── RemoteTrackList.tsx
│   │   │   │       │   ├── hooks/
│   │   │   │       │   │   ├── useCheckLinkedToLocalPlaylist.ts
│   │   │   │       │   │   ├── usePlaylistMenu.ts
│   │   │   │       │   │   ├── useRemotePlaylist.ts
│   │   │   │       │   │   └── useTrackSelection.ts
│   │   │   │       │   ├── search-result/
│   │   │   │       │   │   ├── constants.ts
│   │   │   │       │   │   └── hooks/
│   │   │   │       │   │       └── useSearchInteractions.ts
│   │   │   │       │   └── toview/
│   │   │   │       │       └── components/
│   │   │   │       │           ├── Item.tsx
│   │   │   │       │           └── ProgressRing.tsx
│   │   │   │       └── skeletons/
│   │   │   │           └── PlaylistSkeleton.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── analytics/
│   │   │   │   │   └── useFeatureTracking.ts
│   │   │   │   ├── app/
│   │   │   │   │   ├── useCheckUpdate.tsx
│   │   │   │   │   └── useFastMigrations.ts
│   │   │   │   ├── auth/
│   │   │   │   │   ├── useGeetest.ts
│   │   │   │   │   └── usePhoneLogin.ts
│   │   │   │   ├── mutations/
│   │   │   │   │   ├── bilibili/
│   │   │   │   │   │   ├── comments.ts
│   │   │   │   │   │   ├── favorite.ts
│   │   │   │   │   │   └── video.ts
│   │   │   │   │   ├── db/
│   │   │   │   │   │   ├── playlist.ts
│   │   │   │   │   │   └── track.ts
│   │   │   │   │   ├── lyrics/
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   └── orpheus/
│   │   │   │   │       └── index.ts
│   │   │   │   ├── player/
│   │   │   │   │   ├── useCurrentTrack.ts
│   │   │   │   │   ├── useCurrentTrackId.ts
│   │   │   │   │   ├── useIsCurrentTrack.ts
│   │   │   │   │   ├── useLocalCover.ts
│   │   │   │   │   ├── useSmoothProgress.ts
│   │   │   │   │   └── useTrackProgress.ts
│   │   │   │   ├── queries/
│   │   │   │   │   ├── bilibili/
│   │   │   │   │   │   ├── comments.ts
│   │   │   │   │   │   ├── danmaku.ts
│   │   │   │   │   │   ├── favorite.ts
│   │   │   │   │   │   ├── search.ts
│   │   │   │   │   │   ├── user.ts
│   │   │   │   │   │   └── video.ts
│   │   │   │   │   ├── db/
│   │   │   │   │   │   ├── playlist.ts
│   │   │   │   │   │   └── track.ts
│   │   │   │   │   ├── external-playlist/
│   │   │   │   │   │   └── useExternalPlaylist.ts
│   │   │   │   │   ├── lyrics/
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── orpheus/
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── playHistory.ts
│   │   │   │   │   ├── sharedPlaylistAllMembers.ts
│   │   │   │   │   ├── sharedPlaylistMembers.ts
│   │   │   │   │   ├── sharedPlaylistPreview.ts
│   │   │   │   │   └── useRecentPlaylists.ts
│   │   │   │   ├── router/
│   │   │   │   │   ├── useBottomTabBarHeight.ts
│   │   │   │   │   └── usePreventRemove.ts
│   │   │   │   ├── stores/
│   │   │   │   │   ├── useAppStore.ts
│   │   │   │   │   ├── useDownloadManagerStore.ts
│   │   │   │   │   ├── useExternalPlaylistSyncStore.tsx
│   │   │   │   │   ├── useModalStore.ts
│   │   │   │   │   ├── usePlayerStore.ts
│   │   │   │   │   └── useSharedPlaylistMembersStore.ts
│   │   │   │   ├── ui/
│   │   │   │   │   ├── useDoubleTapScrollToTop.ts
│   │   │   │   │   ├── usePlaylistBackgroundColor.ts
│   │   │   │   │   └── useScreenDimensions.ts
│   │   │   │   └── utils/
│   │   │   │       ├── useDebouncedValue.ts
│   │   │   │       ├── useIsActuallyOffline.ts
│   │   │   │       ├── usePreviousState.ts
│   │   │   │       └── useRefreshOnFocus.ts
│   │   │   ├── lib/
│   │   │   │   ├── api/
│   │   │   │   │   ├── bbplayer/
│   │   │   │   │   │   └── client.ts
│   │   │   │   │   ├── bilibili/
│   │   │   │   │   │   ├── api.ts
│   │   │   │   │   │   ├── client.ts
│   │   │   │   │   │   ├── proto/
│   │   │   │   │   │   │   ├── dm.d.ts
│   │   │   │   │   │   │   ├── dm.js
│   │   │   │   │   │   │   └── dm.proto
│   │   │   │   │   │   ├── utils.ts
│   │   │   │   │   │   └── wbi.ts
│   │   │   │   │   ├── kugou/
│   │   │   │   │   │   └── api.ts
│   │   │   │   │   ├── netease/
│   │   │   │   │   │   ├── api.ts
│   │   │   │   │   │   ├── crypto.ts
│   │   │   │   │   │   ├── request.ts
│   │   │   │   │   │   └── utils.ts
│   │   │   │   │   └── qqmusic/
│   │   │   │   │       └── api.ts
│   │   │   │   ├── config/
│   │   │   │   │   ├── queryClient.ts
│   │   │   │   │   └── sentry.ts
│   │   │   │   ├── db/
│   │   │   │   │   ├── db.ts
│   │   │   │   │   └── schema.ts
│   │   │   │   ├── errors/
│   │   │   │   │   ├── facade.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── player.ts
│   │   │   │   │   ├── service.ts
│   │   │   │   │   └── thirdparty/
│   │   │   │   │       ├── bilibili.ts
│   │   │   │   │       └── netease.ts
│   │   │   │   ├── facades/
│   │   │   │   │   ├── bilibili.ts
│   │   │   │   │   ├── playlist.ts
│   │   │   │   │   ├── sharedPlaylist.ts
│   │   │   │   │   ├── syncBilibiliPlaylist.ts
│   │   │   │   │   └── syncExternalPlaylist.ts
│   │   │   │   ├── player/
│   │   │   │   │   ├── PlayerSideEffects.ts
│   │   │   │   │   └── progressListener.ts
│   │   │   │   ├── services/
│   │   │   │   │   ├── analyticsService.ts
│   │   │   │   │   ├── artistService.ts
│   │   │   │   │   ├── externalPlaylistService.ts
│   │   │   │   │   ├── genKey.ts
│   │   │   │   │   ├── lyricService.ts
│   │   │   │   │   ├── playlistService.ts
│   │   │   │   │   ├── syncLocalToBilibiliService.ts
│   │   │   │   │   ├── trackService.ts
│   │   │   │   │   └── updateService.ts
│   │   │   │   ├── theme/
│   │   │   │   │   └── material3Colors.ts
│   │   │   │   ├── utils/
│   │   │   │   │   └── playlistUrlParser.ts
│   │   │   │   └── workers/
│   │   │   │       └── PlaylistSyncWorker.ts
│   │   │   ├── theme/
│   │   │   │   └── dimensions.ts
│   │   │   ├── types/
│   │   │   │   ├── apis/
│   │   │   │   │   ├── baidu.ts
│   │   │   │   │   ├── bilibili.ts
│   │   │   │   │   ├── kugou.ts
│   │   │   │   │   ├── kuwo.ts
│   │   │   │   │   ├── netease.ts
│   │   │   │   │   └── qqmusic.ts
│   │   │   │   ├── core/
│   │   │   │   │   ├── appStore.ts
│   │   │   │   │   ├── downloadManagerStore.ts
│   │   │   │   │   ├── media.ts
│   │   │   │   │   └── scope.ts
│   │   │   │   ├── external_playlist.ts
│   │   │   │   ├── flashlist.ts
│   │   │   │   ├── navigation.ts
│   │   │   │   ├── player/
│   │   │   │   │   └── lyrics.ts
│   │   │   │   ├── services/
│   │   │   │   │   ├── artist.ts
│   │   │   │   │   ├── playlist.ts
│   │   │   │   │   └── track.ts
│   │   │   │   └── storage.ts
│   │   │   └── utils/
│   │   │       ├── __mocks__/
│   │   │       │   └── log.ts
│   │   │       ├── __tests__/
│   │   │       │   ├── set.test.ts
│   │   │       │   ├── sticky-mitt.test.ts
│   │   │       │   └── time.test.ts
│   │   │       ├── color.ts
│   │   │       ├── danmaku.ts
│   │   │       ├── error-handling.ts
│   │   │       ├── haptics.ts
│   │   │       ├── log.ts
│   │   │       ├── lottie.ts
│   │   │       ├── matching.ts
│   │   │       ├── mmkv.ts
│   │   │       ├── network.ts
│   │   │       ├── neverthrow-utils.ts
│   │   │       ├── player.ts
│   │   │       ├── search.ts
│   │   │       ├── set.ts
│   │   │       ├── sticky-mitt.ts
│   │   │       ├── time.ts
│   │   │       └── toast.ts
│   │   └── tsconfig.json
│   └── update-publisher/
│       ├── package.json
│       ├── src/
│       │   └── index.ts
│       └── tsconfig.json
├── commitlint.config.js
├── eslint.config.mjs
├── lefthook.yml
├── package.json
├── packages/
│   ├── eslint-plugin/
│   │   ├── index.js
│   │   ├── package.json
│   │   └── rules/
│   │       └── no-navigate-after-modal-close.js
│   ├── heatmap/
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── components/
│   │   │   │   ├── HeatMapCell.tsx
│   │   │   │   ├── MonthlyHeatMap.tsx
│   │   │   │   └── WeeklyHeatMap.tsx
│   │   │   ├── constants/
│   │   │   │   └── theme.ts
│   │   │   ├── index.ts
│   │   │   ├── types.ts
│   │   │   └── utils/
│   │   │       └── calendar.ts
│   │   └── tsconfig.json
│   ├── image-theme-colors/
│   │   ├── .gitignore
│   │   ├── .npmignore
│   │   ├── README.md
│   │   ├── android/
│   │   │   ├── build.gradle
│   │   │   └── src/
│   │   │       └── main/
│   │   │           ├── AndroidManifest.xml
│   │   │           └── java/
│   │   │               └── expo/
│   │   │                   └── modules/
│   │   │                       └── imagethemecolors/
│   │   │                           └── ExpoImageThemeColorsModule.kt
│   │   ├── example/
│   │   │   ├── .gitignore
│   │   │   ├── App.tsx
│   │   │   ├── app.json
│   │   │   ├── babel.config.js
│   │   │   ├── index.ts
│   │   │   ├── metro.config.js
│   │   │   ├── package.json
│   │   │   └── tsconfig.json
│   │   ├── expo-module.config.json
│   │   ├── ios/
│   │   │   ├── ExpoImageThemeColors.podspec
│   │   │   └── ExpoImageThemeColorsModule.swift
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── ExpoImageThemeColors.types.ts
│   │   │   ├── ExpoImageThemeColorsModule.ts
│   │   │   ├── ImageRef.ts
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── logs/
│   │   ├── .gitignore
│   │   ├── .travis.yml
│   │   ├── CHANGELOG.md
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── demo/
│   │   │   ├── ComponentReadLogsRN.tsx
│   │   │   └── demo.ts
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── index.ts
│   │   │   └── transports/
│   │   │       ├── consoleTransport.ts
│   │   │       ├── crashlyticsTransport.ts
│   │   │       ├── fileAsyncTransport.ts
│   │   │       ├── mapConsoleTransport.ts
│   │   │       └── sentryTransport.ts
│   │   ├── test/
│   │   │   ├── consoleTransport.test.js
│   │   │   └── index.test.js
│   │   └── tsconfig.json
│   ├── native/
│   │   ├── .gitignore
│   │   ├── android/
│   │   │   ├── build.gradle
│   │   │   └── src/
│   │   │       └── main/
│   │   │           ├── AndroidManifest.xml
│   │   │           └── java/
│   │   │               └── expo/
│   │   │                   └── modules/
│   │   │                       └── bbplayernative/
│   │   │                           └── BBPlayerNativeModule.kt
│   │   ├── expo-module.config.json
│   │   ├── package.json
│   │   └── src/
│   │       ├── BBPlayerNative.types.ts
│   │       ├── BBPlayerNativeModule.ts
│   │       └── index.ts
│   ├── orpheus/
│   │   ├── .gitignore
│   │   ├── .lyricon_version
│   │   ├── AGENTS.md
│   │   ├── CHANGELOG.md
│   │   ├── README.md
│   │   ├── android/
│   │   │   ├── build.gradle
│   │   │   └── src/
│   │   │       └── main/
│   │   │           ├── AndroidManifest.xml
│   │   │           ├── aidl/
│   │   │           │   └── io/
│   │   │           │       └── github/
│   │   │           │           └── proify/
│   │   │           │               └── lyricon/
│   │   │           │                   ├── lyric/
│   │   │           │                   │   └── model/
│   │   │           │                   │       └── Song.aidl
│   │   │           │                   └── provider/
│   │   │           │                       ├── IProviderBinder.aidl
│   │   │           │                       ├── IProviderService.aidl
│   │   │           │                       ├── IRemotePlayer.aidl
│   │   │           │                       ├── IRemoteService.aidl
│   │   │           │                       └── ProviderInfo.aidl
│   │   │           ├── java/
│   │   │           │   ├── expo/
│   │   │           │   │   └── modules/
│   │   │           │   │       └── orpheus/
│   │   │           │   │           ├── ExpoOrpheusModule.kt
│   │   │           │   │           ├── OrpheusConfig.kt
│   │   │           │   │           ├── bilibili/
│   │   │           │   │           │   ├── BilibiliApi.kt
│   │   │           │   │           │   ├── BilibiliModels.kt
│   │   │           │   │           │   ├── BilibiliRepository.kt
│   │   │           │   │           │   ├── NetworkModule.kt
│   │   │           │   │           │   └── WbiUtil.kt
│   │   │           │   │           ├── exception/
│   │   │           │   │           │   └── exceptions.kt
│   │   │           │   │           ├── manager/
│   │   │           │   │           │   ├── CachedUriManager.kt
│   │   │           │   │           │   ├── CoverDownloadManager.kt
│   │   │           │   │           │   ├── DownloadCache.kt
│   │   │           │   │           │   ├── FloatingLyricsManager.kt
│   │   │           │   │           │   ├── LyriconBackend.kt
│   │   │           │   │           │   ├── SpectrumManager.kt
│   │   │           │   │           │   ├── StatusBarLyricsBackend.kt
│   │   │           │   │           │   ├── StatusBarLyricsManager.kt
│   │   │           │   │           │   ├── SuperLyricBackend.kt
│   │   │           │   │           │   └── UnifiedLyricsManager.kt
│   │   │           │   │           ├── model/
│   │   │           │   │           │   ├── LyricsModels.kt
│   │   │           │   │           │   └── TrackRecord.kt
│   │   │           │   │           ├── network/
│   │   │           │   │           │   └── OkHttpClientManager.kt
│   │   │           │   │           ├── service/
│   │   │           │   │           │   ├── OrpheusDownloadService.kt
│   │   │           │   │           │   ├── OrpheusHeadlessTaskService.kt
│   │   │           │   │           │   ├── OrpheusMusicService.kt
│   │   │           │   │           │   └── ShuffleManager.kt
│   │   │           │   │           ├── util/
│   │   │           │   │           │   ├── ConvertPlayerError.kt
│   │   │           │   │           │   ├── CustomCommands.kt
│   │   │           │   │           │   ├── DirectoryPickerContract.kt
│   │   │           │   │           │   ├── DownloadUtil.kt
│   │   │           │   │           │   ├── ExportDownloadsHelper.kt
│   │   │           │   │           │   ├── GeneralStorage.kt
│   │   │           │   │           │   ├── GlideBitmapLoader.kt
│   │   │           │   │           │   ├── LoudnessStorage.kt
│   │   │           │   │           │   ├── SleepTimeController.kt
│   │   │           │   │           │   ├── SplConverter.kt
│   │   │           │   │           │   ├── TrackRecordExtension.kt
│   │   │           │   │           │   └── Volume.kt
│   │   │           │   │           └── view/
│   │   │           │   │               └── LyricView.kt
│   │   │           │   └── io/
│   │   │           │       └── github/
│   │   │           │           └── proify/
│   │   │           │               └── lyricon/
│   │   │           │                   ├── lyric/
│   │   │           │                   │   └── model/
│   │   │           │                   │       ├── LyricLine.kt
│   │   │           │                   │       ├── LyricMetadata.kt
│   │   │           │                   │       ├── LyricTiming.kt
│   │   │           │                   │       ├── LyricWord.kt
│   │   │           │                   │       ├── RichLyricLine.kt
│   │   │           │                   │       ├── Song.kt
│   │   │           │                   │       ├── extensions/
│   │   │           │                   │       │   ├── Extensions.kt
│   │   │           │                   │       │   ├── LyricWord.kt
│   │   │           │                   │       │   └── TimingNavigator.kt
│   │   │           │                   │       └── interfaces/
│   │   │           │                   │           ├── DeepCopyable.kt
│   │   │           │                   │           ├── ILyricLine.kt
│   │   │           │                   │           ├── ILyricTiming.kt
│   │   │           │                   │           ├── ILyricWord.kt
│   │   │           │                   │           ├── IRichLyricLine.kt
│   │   │           │                   │           └── Normalize.kt
│   │   │           │                   └── provider/
│   │   │           │                       ├── CachedRemotePlayer.kt
│   │   │           │                       ├── CentralServiceReceiver.kt
│   │   │           │                       ├── ConnectionListener.kt
│   │   │           │                       ├── ConnectionStatus.kt
│   │   │           │                       ├── Extensions.kt
│   │   │           │                       ├── LocalProviderService.kt
│   │   │           │                       ├── LyriconFactory.kt
│   │   │           │                       ├── LyriconProvider.kt
│   │   │           │                       ├── ProviderBinder.kt
│   │   │           │                       ├── ProviderConstants.kt
│   │   │           │                       ├── ProviderInfo.kt
│   │   │           │                       ├── ProviderLogo.kt
│   │   │           │                       ├── ProviderMetadata.kt
│   │   │           │                       ├── ProviderService.kt
│   │   │           │                       ├── RemotePlayer.kt
│   │   │           │                       ├── impl/
│   │   │           │                       │   ├── EmptyProvider.kt
│   │   │           │                       │   ├── LyriconProviderImpl.kt
│   │   │           │                       │   ├── ProviderRemoteEndpoint.kt
│   │   │           │                       │   └── RemotePlayerProxy.kt
│   │   │           │                       └── service/
│   │   │           │                           ├── RemoteService.kt
│   │   │           │                           └── RemoteServiceBinder.kt
│   │   │           └── res/
│   │   │               ├── drawable/
│   │   │               │   ├── baseline_download_24.xml
│   │   │               │   ├── outline_close_24.xml
│   │   │               │   ├── outline_lock_24.xml
│   │   │               │   ├── outline_lyrics_off_24.xml
│   │   │               │   ├── outline_pause_24.xml
│   │   │               │   ├── outline_play_arrow_24.xml
│   │   │               │   ├── outline_repeat_24.xml
│   │   │               │   ├── outline_repeat_off_24.xml
│   │   │               │   ├── outline_repeat_one_24.xml
│   │   │               │   ├── outline_skip_next_24.xml
│   │   │               │   ├── outline_skip_previous_24.xml
│   │   │               │   └── outline_translate_24.xml
│   │   │               └── values/
│   │   │                   └── strings.xml
│   │   ├── docs/
│   │   │   ├── API-Events.md
│   │   │   ├── API-Methods.md
│   │   │   ├── API-Types.md
│   │   │   └── Home.md
│   │   ├── example/
│   │   │   ├── .gitignore
│   │   │   ├── App.tsx
│   │   │   ├── app.json
│   │   │   ├── babel.config.js
│   │   │   ├── index.ts
│   │   │   ├── metro.config.js
│   │   │   ├── package.json
│   │   │   ├── src/
│   │   │   │   ├── components/
│   │   │   │   │   ├── Buttons.tsx
│   │   │   │   │   ├── DebugSection.tsx
│   │   │   │   │   ├── PlayerControls.tsx
│   │   │   │   │   └── SpectrumVisualizer.tsx
│   │   │   │   └── constants.ts
│   │   │   ├── tsconfig.json
│   │   │   └── webpack.config.js
│   │   ├── expo-module.config.json
│   │   ├── ios/
│   │   │   ├── AudioSpectrumAnalyzer.swift
│   │   │   ├── BilibiliApi.swift
│   │   │   ├── ExpoOrpheus.podspec
│   │   │   ├── ExpoOrpheusModule.swift
│   │   │   ├── GeneralStorage.swift
│   │   │   ├── OrpheusDownloadManager.swift
│   │   │   ├── OrpheusModels.swift
│   │   │   ├── OrpheusPlayerManager.swift
│   │   │   ├── OrpheusQueueManager.swift
│   │   │   └── WbiUtil.swift
│   │   ├── mise.toml
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── ExpoOrpheusModule.ts
│   │   │   ├── headless.ts
│   │   │   ├── hooks/
│   │   │   │   ├── index.ts
│   │   │   │   ├── useCurrentTrack.ts
│   │   │   │   ├── useIsPlaying.ts
│   │   │   │   ├── useOrpheus.ts
│   │   │   │   ├── usePlaybackState.ts
│   │   │   │   └── useProgress.ts
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── splash/
│       ├── README.md
│       ├── jest.config.js
│       ├── package.json
│       ├── src/
│       │   ├── __tests__/
│       │   │   └── fixtures/
│       │   │       ├── 687506.json
│       │   │       └── bilibili--BV1Zu411x7mc.json
│       │   ├── converter/
│       │   │   ├── netease.test.ts
│       │   │   └── netease.ts
│       │   ├── index.ts
│       │   ├── parser/
│       │   │   ├── index.test.ts
│       │   │   ├── index.ts
│       │   │   ├── merge.ts
│       │   │   ├── spans.test.ts
│       │   │   └── spans.ts
│       │   ├── types.ts
│       │   └── utils/
│       │       ├── time.test.ts
│       │       └── time.ts
│       └── tsconfig.json
├── patches/
│   ├── react-native-mmkv.patch
│   └── sonner-native@0.23.0.patch
├── pnpm-workspace.yaml
├── rnrepo.config.json
├── scripts/
│   └── update-lyricon.sh
├── skills-lock.json
├── tsconfig.json
└── update.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .agent/rules/changelog.md
================================================
---
trigger: always_on
---

在完成一个任务后,应该去修改 CHANGELOG.md 以反应这个任务的更改。CHANGELOG 应该尽量简洁,一个任务只对应一条记录。


================================================
FILE: .agent/rules/measure-layout.md
================================================
---
description: Best practice for measuring component layout in React Native
---

# Measuring Component Layout

When you need to measure the current layout to apply changes to the overall layout or to make decisions based on precise coordinates (especially relative to the screen/page), use `measure` within `useLayoutEffect`.

This approach ensures you get the most recent values and can apply changes in the same frame, preventing UI flickering.

## Recommended Pattern

```tsx
function AComponent({ children }) {
	const targetRef = React.useRef(null)

	useLayoutEffect(
		() => {
			targetRef.current?.measure((x, y, width, height, pageX, pageY) => {
				// x, y: position relative to parent
				// width, height: dimensions
				// pageX, pageY: absolute position on screen
				// Do something with the measurements
			})
		},
		[
			/* dependencies */
		],
	)

	return <View ref={targetRef}>{children}</View>
}
```

## When to use `onLayout` vs `measure`

- **Use `onLayout`**: When you only need the size (`width`, `height`) or position relative to the parent (`x`, `y`). It is simpler but passive.
- **Use `measure`**: When you need absolute coordinates (`pageX`, `pageY`) or need to trigger logic synchronously after the view is ready to avoid visual jumps.


================================================
FILE: .agent/skills/gesture-handler-3-migration/SKILL.md
================================================
---
name: gesture-handler-3-migration
description: Migrates files containing React Native components which use the React Native Gesture Handler 2 API to Gesture Handler 3.
---

# Migrate to Gesture Handler 3

This skill scans React Native components that use the Gesture Handler builder-based API and updates them to use the new hook-based API. It also updates related types and components to adapt to the new version.

## When to Use

- Updating the usage of components imported from `react-native-gesture-handler`
- Upgrading to Gesture Handler 3
- Migrating to the new hook-based gesture API

## Instructions

Use the instructions below to correctly replace all legacy APIs with the modern ones.

1. Identify all imports from 'react-native-gesture-handler'
2. For each `Gesture.X()` call, replace with corresponding `useXGesture()` hook
3. Replace `Gesture` import with imports for the used hooks
4. Convert builder method chains to configuration objects
5. Update callback names (onStart → onActivate, etc.)
6. Replace composed gestures with relation hooks. Keep rules of hooks in mind
7. Update GestureDetector usage if SVG is involved to Intercepting/Virtual GestureDetector
8. Update usage of compoenent imported from 'react-native-gesture-handler' according to "Legacy components" section

### Migrating gestures

All hook gestures have their counterparts in the builder API: `Gesture.X()` becomes `useXGesture(config)`. The methods are now config object fields with the same name as the relevant builder methods, unless specified otherwise.

The exception to thait is `esture.ForceTouch` which DOES NOT have a counterpart in the hook API.

#### Callback changes

In Gesture Handler 3 some of the callbacks were renamed, namely:

- `onStart` -> `onActivate`
- `onEnd` -> `onDeactivate`
- `onTouchesCancelled` -> `onTouchesCancel`

In the hooks API `onChange` is no longer available. Instead the `*change*` properties were moved to the event available inside `onUpdate`.

All callbacks of a gesture are now using the same type:

- `usePanGesture()` -> `PanGestureEvent`
- `useTapGesture()` -> `TapGestureEvent`
- `useLongPressGesture()` -> `LongPressGestureEvent`
- `useRotationGesture()` -> `RotationGestureEvent`
- `usePinchGesture()` -> `PinchGestureEvent`
- `useFlingGesture()` -> `FlingGestureEvent`
- `useHoverGesture()` -> `HoverGestureEvent`
- `useNativeGesture()` -> `RotationGestureEvent`
- `useManualGesture()` -> `ManualGestureEvent`

The exception to this is touch events:

- `onTouchesDown`
- `onTouchesUp`
- `onTouchesMove`
- `onTouchesCancel`

Where each callback receives `GestureTouchEvent` regardless of the hook used.

#### StateManager

In Gesture Handler 3, `stateManager` is no longer passed to `TouchEvent` callbacks. Instead, you should use the global `GestureStateManager`.

`GestureStateManager` provides methods for imperative state management:

- .begin(handlerTag: number)
- .activate(handlerTag: number)
- .deactivate(handlerTag: number) (.end() in the old API)
- .fail(handlerTag: number)

`handlerTag` can be obtained in two ways:

1. From the gesture object returned by the hook (`gesture.handlerTag`)
2. From the event inside callback (`event.handlerTag`)

Callback definitions CANNOT reference the gesture that's being defined. In this scenario use events to get access to the handler tag.

### Migrating relations

#### Composed gestures

`Gesture.Simultaneous(gesture1, gesture2);` becomes `useSimultaneousGestures(pan1, pan2);`

All relations from the old API and their counterparts in the new one:

- `Gesture.Race()` -> `useCompetingGestures()`
- `Gesture.Simultaneous()` -> `useSimultaneousGestures()`
- `Gesture.Exclusive()` -> `useExclusiveGestures()`

#### Cross components relations properties

Properties used to define cross-components interactions were renamed:

- `.simultaneousWithExternalGesture` -> `simultaneousWith:`
- `.requireExternalGestureToFail` -> `requireToFail:`
- `.blocksExternalGesture` -> `block:`

### GestureDetector

The `GestureDetector` is a key component of `react-native-gesture-handler`. It supports gestures created either using the hooks API or the builder pattern (but those cannot be mixed, it's either or).

Using the same instance of a gesture across multiple Gesture Detectors may result in undefined behavior.

### Integration with Reanimated

Worklets' Babel plugin is setup in a way that automatically marks callbacks passed to gestures in the configuration chain as worklets. This means that you don't need to add a `'worklet';` directive at the beginning of the functions.

This will not be workletized because the callback is defined outside of the gesture object:

```jsx
const callback = () => {
	console.log(_WORKLET)
}

const gesture = useTapGesture({
	onBegin: callback,
})
```

The callback wrapped by any other higher order function will not be workletized:

```jsx
const gesture = useTapGesture({
	onBegin: useCallback(() => {
		console.log(_WORKLET)
	}, []),
})
```

In the above cases, you should add a `"worklet";` directive as the first line of the callback.

### Disabling Reanimated

Gestures created with the hook API have `Reanimated` integration enabled by default (if it's installed), meaning all callbacks are executed on the UI thread.

#### runOnJS

The `runOnJS` property allows you to dynamically control whether callbacks are executed on the JS thread or the UI thread. When set to `true`, callbacks will run on the JS thread. Setting it to `false` will execute them on the UI thread. Default value is `false`.

### Migrating components relying on view hierarchy

Certain components, such as `SVG`, depend on the view hierarchy to function correctly. In Gesture Handler 3, `GestureDetector` disrupts these hierarchies. To resolve this issue, two new detectors have been introduced: `InterceptingGestureDetector` and `VirtualGestureDetector`.

`InterceptingGestureDetector` functions similarly to the `GestureDetector`, but it can also act as a proxy for `VirtualGestureDetector` within its component subtree. Because it can be used solely to establish the context for virtual detectors, the `gesture` property is optional.

`VirtualGestureDetector` is similar to the `GestureDetector` from RNGH2. Because it is not a host component, it does not interfere with the host view hierarchy. This allows you to attach gestures without disrupting functionality that depends on it.

**Warning:** `VirtualGestureDetector` has to be a descendant of `InterceptingGestureDetector`.

#### Migrating SVG

In Gesture Handler 2 it was possible to use `GestureDetector` directly on `SVG`. In Gesture Handler 3, the correct way to interact with `SVG` is to use `InterceptingGestureDetector` and `VirtualGestureDetector`.

### Legacy components

When the code using the component relies on the APIs that are no longer available on the components in Gesture Handler 3 (like `waitFor`, `simultaneousWith`, `blocksHandler`, `onHandlerStateChange`, `onGestureEvent` props), it cannot be easily migrated in isolation. In this case update the imports to the Legacy version of the component, and inform the user that the dependencies need to be migrated first.

If the migration is possible, use the ask questions tool to clarify the user intent unless clearly stated beforehand: should the components be using the new implementation (no `Legacy` prefix when imported), or should they revert to the old implementation (`Legacy` prefix when imported)?

Don't suggest replacing buttons from Gesture Handler with components from React Native and vice versa.

The implementation of buttons has been updated, resolving most button-related issues. They have also been internally rewritten to utilize the new hook API. The legacy JS implementations of button components are still accessible but have been renamed with the prefix `Legacy`, e.g., `RectButton` is now available as `LegacyRectButton`. Those still use the new native component under the hood.

Other components have also been internally rewritten using the new hook API but are exported under their original names, so no changes are necessary on your part. However, if you need to use the previous implementation for any reason, the legacy components are also available and are prefixed with `Legacy`, e.g., `ScrollView` is now available as `LegacyScrollView`.

### Replaced types

Most of the types used in the builder API, like `TapGesture`, are still present in Gesture Handler 3. However, they are now used in new hook API. Types for builder API now have `Legacy` prefix, e.g. `TapGesture` becomes `LegacyTapGesture`.


================================================
FILE: .agent/skills/upgrading-expo/SKILL.md
================================================
---
name: upgrading-expo
description: Guidelines for upgrading Expo SDK versions and fixing dependency issues
version: 1.0.0
license: MIT
---

## References

- ./references/new-architecture.md -- SDK +53: New Architecture migration guide
- ./references/react-19.md -- SDK +54: React 19 changes (useContext → use, Context.Provider → Context, forwardRef removal)
- ./references/react-compiler.md -- SDK +54: React Compiler setup and migration guide

## Step-by-Step Upgrade Process

1. Upgrade Expo and dependencies

```bash
npx expo install expo@latest
npx expo install --fix
```

2. Run diagnostics: `npx expo-doctor`

3. Clear caches and reinstall

```bash
npx expo export -p ios --clear
rm -rf node_modules .expo
watchman watch-del-all
```

## Breaking Changes Checklist

- Check for removed APIs in release notes
- Update import paths for moved modules
- Review native module changes requiring prebuild
- Test all camera, audio, and video features
- Verify navigation still works correctly

## Prebuild for Native Changes

If upgrading requires native changes:

```bash
npx expo prebuild --clean
```

This regenerates the `ios` and `android` directories. Ensure the project is not a bare workflow app before running this command.

## Clear caches for bare workflow

- Clear the cocoapods cache for iOS: `cd ios && pod install --repo-update`
- Clear derived data for Xcode: `npx expo run:ios --no-build-cache`
- Clear the Gradle cache for Android: `cd android && ./gradlew clean`

## Housekeeping

- Review release notes for the target SDK version at https://expo.dev/changelog
- If using Expo SDK 54 or later, ensure react-native-worklets is installed — this is required for react-native-reanimated to work.
- Enable React Compiler in SDK 54+ by adding `"experiments": { "reactCompiler": true }` to app.json — it's stable and recommended
- Delete sdkVersion from `app.json` to let Expo manage it automatically
- Remove implicit packages from `package.json`: `@babel/core`, `babel-preset-expo`, `expo-constants`.
- If the babel.config.js only contains 'babel-preset-expo', delete the file
- If the metro.config.js only contains expo defaults, delete the file

## Deprecated Packages

| Old Package          | Replacement                                          |
| -------------------- | ---------------------------------------------------- |
| `expo-av`            | `expo-audio` and `expo-video`                        |
| `expo-permissions`   | Individual package permission APIs                   |
| `@expo/vector-icons` | `expo-symbols` (for SF Symbols)                      |
| `AsyncStorage`       | `expo-sqlite/localStorage/install`                   |
| `expo-app-loading`   | `expo-splash-screen`                                 |
| expo-linear-gradient | experimental_backgroundImage + CSS gradients in View |

## Removing patches

Check if there are any outdated patches in the `patches/` directory. Remove them if they are no longer needed.

## Postcss

- `autoprefixer` isn't needed in SDK +53.
- Use `postcss.config.mjs` in SDK +53.

## Metro

Remove redundant metro config options:

- resolver.unstable_enablePackageExports is enabled by default in SDK +53.
- `experimentalImportSupport` is enabled by default in SDK +54.
- `EXPO_USE_FAST_RESOLVER=1` is removed in SDK +54.
- cjs and mjs extensions are supported by default in SDK +50.
- Expo webpack is deprecated, migrate to [Expo Router and Metro web](https://docs.expo.dev/router/migrate/from-expo-webpack/).

## New Architecture

The new architecture is enabled by default, the app.json field `"newArchEnabled": true` is no longer needed as it's the default. Expo Go only supports the new architecture as of SDK +53.


================================================
FILE: .agent/skills/upgrading-expo/references/new-architecture.md
================================================
# New Architecture

The New Architecture is enabled by default in Expo SDK 53+. It replaces the legacy bridge with a faster, synchronous communication layer between JavaScript and native code.

## Documentation

Full guide: https://docs.expo.dev/guides/new-architecture/

## What Changed

- **JSI (JavaScript Interface)** — Direct synchronous calls between JS and native
- **Fabric** — New rendering system with concurrent features
- **TurboModules** — Lazy-loaded native modules with type safety

## SDK Compatibility

| SDK Version | New Architecture Status |
| ----------- | ----------------------- |
| SDK 53+     | Enabled by default      |
| SDK 52      | Opt-in via app.json     |
| SDK 51-     | Experimental            |

## Configuration

New Architecture is enabled by default. To explicitly disable (not recommended):

```json
{
	"expo": {
		"newArchEnabled": false
	}
}
```

## Expo Go

Expo Go only supports the New Architecture as of SDK 53. Apps using the old architecture must use development builds.

## Common Migration Issues

### Native Module Compatibility

Some older native modules may not support the New Architecture. Check:

1. Module documentation for New Architecture support
2. GitHub issues for compatibility discussions
3. Consider alternatives if module is unmaintained

### Reanimated

React Native Reanimated requires `react-native-worklets` in SDK 54+:

```bash
npx expo install react-native-worklets
```

### Layout Animations

Some layout animations behave differently. Test thoroughly after upgrading.

## Verifying New Architecture

Check if New Architecture is active:

```tsx
import { Platform } from 'react-native'

// Returns true if Fabric is enabled
const isNewArch = global._IS_FABRIC !== undefined
```

Verify from the command line if the currently running app uses the New Architecture: `bunx xcobra expo eval "_IS_FABRIC"` -> `true`

## Troubleshooting

1. **Clear caches** — `npx expo start --clear`
2. **Clean prebuild** — `npx expo prebuild --clean`
3. **Check native modules** — Ensure all dependencies support New Architecture
4. **Review console warnings** — Legacy modules log compatibility warnings


================================================
FILE: .agent/skills/upgrading-expo/references/react-19.md
================================================
# React 19

React 19 is included in Expo SDK 54. This release simplifies several common patterns.

## Context Changes

### useContext → use

The `use` hook replaces `useContext`:

```tsx
// Before (React 18)
import { useContext } from 'react'
const value = useContext(MyContext)

// After (React 19)
import { use } from 'react'
const value = use(MyContext)
```

- The `use` hook can also read promises, enabling Suspense-based data fetching.
- `use` can be called conditionally, this simplifies components that consume multiple contexts.

### Context.Provider → Context

Context providers no longer need the `.Provider` suffix:

```tsx
// Before (React 18)
<ThemeContext.Provider value={theme}>
  {children}
</ThemeContext.Provider>

// After (React 19)
<ThemeContext value={theme}>
  {children}
</ThemeContext>
```

## ref as a Prop

### Removing forwardRef

Components can now receive `ref` as a regular prop. `forwardRef` is no longer needed:

```tsx
// Before (React 18)
import { forwardRef } from 'react'

const Input = forwardRef<TextInput, Props>((props, ref) => {
	return (
		<TextInput
			ref={ref}
			{...props}
		/>
	)
})

// After (React 19)
function Input({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
	return (
		<TextInput
			ref={ref}
			{...props}
		/>
	)
}
```

### Migration Steps

1. Remove `forwardRef` wrapper
2. Add `ref` to the props destructuring
3. Update the type to include `ref?: React.Ref<T>`

## Other React 19 Features

- **Actions** — Functions that handle async transitions
- **useOptimistic** — Optimistic UI updates
- **useFormStatus** — Form submission state (web)
- **Document Metadata** — Native `<title>` and `<meta>` support (web)

## Cleanup Checklist

When upgrading to SDK 54:

- [ ] Replace `useContext` with `use`
- [ ] Remove `.Provider` from Context components
- [ ] Remove `forwardRef` wrappers, use `ref` prop instead


================================================
FILE: .agent/skills/upgrading-expo/references/react-compiler.md
================================================
# React Compiler

React Compiler is stable in Expo SDK 54 and later. It automatically memoizes components and hooks, eliminating the need for manual `useMemo`, `useCallback`, and `React.memo`.

## Enabling React Compiler

Add to `app.json`:

```json
{
	"expo": {
		"experiments": {
			"reactCompiler": true
		}
	}
}
```

## What React Compiler Does

- Automatically memoizes components and values
- Eliminates unnecessary re-renders
- Removes the need for manual `useMemo` and `useCallback`
- Works with existing code without modifications

## Cleanup After Enabling

Once React Compiler is enabled, you can remove manual memoization:

```tsx
// Before (manual memoization)
const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b])
const memoizedCallback = useCallback(() => doSomething(a), [a])
const MemoizedComponent = React.memo(MyComponent)

// After (React Compiler handles it)
const value = computeExpensive(a, b)
const callback = () => doSomething(a)
// Just use MyComponent directly
```

## Requirements

- Expo SDK 54 or later
- New Architecture enabled (default in SDK 54+)

## Verifying It's Working

React Compiler runs at build time. Check the Metro bundler output for compilation messages. You can also use React DevTools to verify components are being optimized.

## Troubleshooting

If you encounter issues:

1. Ensure New Architecture is enabled
2. Clear Metro cache: `npx expo start --clear`
3. Check for incompatible patterns in your code (rare)

React Compiler is designed to work with idiomatic React code. If it can't safely optimize a component, it skips that component without breaking your app.


================================================
FILE: .agents/skills/react-doctor/SKILL.md
================================================
---
name: react-doctor
description: Diagnose and fix React codebase health issues. Use when reviewing React code, fixing performance problems, auditing security, or improving code quality.
version: 1.0.0
---

# React Doctor

Scans your React codebase for security, performance, correctness, and architecture issues. Outputs a 0-100 score with actionable diagnostics.

## Usage

```bash
npx -y react-doctor@latest . --verbose
```

## Workflow

1. Run the command above at the project root
2. Read every diagnostic with file paths and line numbers
3. Fix issues starting with errors (highest severity)
4. Re-run to verify the score improved

## Rules (47+)

- **Security**: hardcoded secrets in client bundle, eval()
- **State & Effects**: derived state in useEffect, missing cleanup, useState from props, cascading setState
- **Architecture**: components inside components, giant components, inline render functions
- **Performance**: layout property animations, transition-all, large blur values
- **Correctness**: array index as key, conditional rendering bugs
- **Next.js**: missing metadata, client-side fetching for server data, async client components
- **Bundle Size**: barrel imports, full lodash, moment.js, missing code splitting
- **Server**: missing auth in server actions, blocking without after()
- **Accessibility**: missing prefers-reduced-motion
- **Dead Code**: unused files, exports, types

## Score

- **75+**: Great
- **50-74**: Needs work
- **0-49**: Critical


================================================
FILE: .agents/skills/react-native-ease-refactor/SKILL.md
================================================
---
name: react-native-ease-refactor
description: Scan for Animated/Reanimated code and migrate to EaseView
user-invocable: true
---

# react-native-ease refactor

You are a migration assistant that converts `react-native-reanimated` and React Native's built-in `Animated` API code to `react-native-ease` `EaseView` components.

Follow these 6 phases exactly. Do not skip phases or reorder them.

---

## Phase 1: Discovery

Scan the user's project for animation code:

1. Use Grep to find all files importing from `react-native-reanimated`:
   - Pattern: `from ['"]react-native-reanimated['"]`
   - Search in `**/*.{ts,tsx,js,jsx}`

2. Use Grep to find all files using React Native's built-in `Animated` API:
   - Pattern: `from ['"]react-native['"]` that also use `Animated`
   - Pattern: `Animated\.View|Animated\.Text|Animated\.Image|Animated\.Value|Animated\.timing|Animated\.spring`

3. Use Grep to find files already using `react-native-ease` (to avoid re-migrating):
   - Pattern: `from ['"]react-native-ease['"]`

4. Read each file that contains animation code. Build a list of components with their animation patterns.

**Exclude** from scanning:

- `node_modules/`
- `*.test.*` and `*.spec.*` files
- Build output directories (`lib/`, `build/`, `dist/`)

---

## Phase 2: Classification

For each component found, classify as **migratable** or **not migratable**.

### Decision Tree

Apply these checks in order. The first match determines the result:

1. **Uses gesture APIs?** (`Gesture.Pan`, `Gesture.Pinch`, `Gesture.Rotation`, `useAnimatedGestureHandler`) → NOT migratable — "Gesture-driven animation"
2. **Uses scroll handler?** (`useAnimatedScrollHandler`, `onScroll` with `Animated.event`) → NOT migratable — "Scroll-driven animation"
3. **Uses shared element transitions?** (`sharedTransitionTag`) → NOT migratable — "Shared element transition"
4. **Uses `runOnUI` or worklet directives?** → NOT migratable — "Requires worklet runtime"
5. **Uses `withSequence` or `withDelay`?** → NOT migratable — "Animation sequencing not supported"
6. **Uses complex `interpolate()`?** (more than 2 input/output values) → NOT migratable — "Complex interpolation"
7. **Uses `layout={...}` prop?** → NOT migratable — "Layout animation"
8. **Animates unsupported properties?** (anything besides: opacity, translateX, translateY, scale, scaleX, scaleY, rotate, rotateX, rotateY, borderRadius, backgroundColor) → NOT migratable — "Animates unsupported property: `<prop>`"
9. **Uses different transition configs per property?** (e.g., opacity uses 200ms timing, scale uses spring) → NOT migratable — "Per-property transition configs"
10. **Not driven by state?** (animation triggered by gesture/scroll value, not React state) → NOT migratable — "Not state-driven"
11. **Otherwise** → MIGRATABLE

### Migratable Pattern Mapping

Use this table to convert Reanimated/Animated patterns to EaseView:

| Reanimated / Animated Pattern                                                                                             | EaseView Equivalent                                                                                          |
| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `useSharedValue` + `useAnimatedStyle` + `withTiming` for opacity, translate, scale, rotate, borderRadius, backgroundColor | `animate={{ prop: value }}` + `transition={{ type: 'timing', duration, easing }}`                            |
| `withSpring`                                                                                                              | `transition={{ type: 'spring', damping, stiffness, mass }}`                                                  |
| `entering={FadeIn}` / `FadeIn.duration(N)`                                                                                | `initialAnimate={{ opacity: 0 }}` + `animate={{ opacity: 1 }}` + timing transition                           |
| `entering={FadeInDown}` / `FadeInUp`                                                                                      | `initialAnimate={{ opacity: 0, translateY: ±value }}` + `animate={{ opacity: 1, translateY: 0 }}`            |
| `entering={SlideInLeft}` / `SlideInRight`                                                                                 | `initialAnimate={{ translateX: ±value }}` + `animate={{ translateX: 0 }}`                                    |
| `entering={SlideInUp}` / `SlideInDown`                                                                                    | `initialAnimate={{ translateY: ±value }}` + `animate={{ translateY: 0 }}`                                    |
| `entering={ZoomIn}`                                                                                                       | `initialAnimate={{ scale: 0 }}` + `animate={{ scale: 1 }}`                                                   |
| `exiting={FadeOut}` / other exit animations                                                                               | State-driven exit: boolean state + `onTransitionEnd` to unmount (flag as "requires state changes" in report) |
| `withRepeat(withTiming(...), -1, false)`                                                                                  | `transition={{ type: 'timing', ..., loop: 'repeat' }}` + `initialAnimate` for start value                    |
| `withRepeat(withTiming(...), -1, true)`                                                                                   | `transition={{ type: 'timing', ..., loop: 'reverse' }}` + `initialAnimate` for start value                   |
| `Easing.linear`                                                                                                           | `easing: 'linear'`                                                                                           |
| `Easing.ease` / `Easing.inOut(Easing.ease)`                                                                               | `easing: 'easeInOut'`                                                                                        |
| `Easing.in(Easing.ease)`                                                                                                  | `easing: 'easeIn'`                                                                                           |
| `Easing.out(Easing.ease)`                                                                                                 | `easing: 'easeOut'`                                                                                          |
| `Easing.bezier(x1, y1, x2, y2)`                                                                                           | `easing: [x1, y1, x2, y2]`                                                                                   |
| `Animated.Value` + `Animated.timing`                                                                                      | Same `animate` + `transition` pattern — convert to state-driven                                              |
| `Animated.Value` + `Animated.spring`                                                                                      | `animate` + `transition={{ type: 'spring' }}` — convert to state-driven                                      |

### Default Value Mapping

**CRITICAL: Reanimated and EaseView have different defaults. You MUST explicitly set values to preserve the original animation behavior. Do not rely on EaseView defaults matching Reanimated defaults.**

#### `withSpring` → EaseView spring

| Parameter   | Reanimated default | EaseView default | Action                        |
| ----------- | ------------------ | ---------------- | ----------------------------- |
| `damping`   | `10`               | `15`             | **Must set `damping: 10`**    |
| `stiffness` | `100`              | `120`            | **Must set `stiffness: 100`** |
| `mass`      | `1`                | `1`              | Same — omit                   |

If the source code explicitly sets any of these values, carry them over as-is. If the source relies on Reanimated defaults (no explicit value), set the Reanimated default explicitly on the EaseView transition.

Example — bare `withSpring(1)` with no config:

```typescript
// Before (Reanimated)
scale.value = withSpring(1);

// After (EaseView) — must set damping: 10, stiffness: 100 to match
transition={{ type: 'spring', damping: 10, stiffness: 100 }}
```

**Note:** Reanimated v3+ uses duration-based spring by default (`duration: 550`, `dampingRatio: 1`) when no physics params are set. If migrating code that uses `withSpring` without any config, use `damping: 10, stiffness: 100` which matches the physics-based fallback. If the code explicitly sets `dampingRatio`/`duration`, convert using: `damping = dampingRatio * 2 * sqrt(stiffness * mass)`.

#### `withTiming` → EaseView timing

| Parameter  | Reanimated default          | EaseView default      | Action                                             |
| ---------- | --------------------------- | --------------------- | -------------------------------------------------- |
| `duration` | `300`                       | `300`                 | Same — omit                                        |
| `easing`   | `Easing.inOut(Easing.quad)` | `'easeInOut'` (cubic) | **Must set `easing: [0.455, 0.03, 0.515, 0.955]`** |

The easing curves are different! Reanimated's default is quadratic ease-in-out, EaseView's is cubic. Always set the easing explicitly when the source doesn't specify one.

Example — bare `withTiming(1)` with no config:

```typescript
// Before (Reanimated)
opacity.value = withTiming(1);

// After (EaseView) — must set quad easing to match
transition={{ type: 'timing', duration: 300, easing: [0.455, 0.03, 0.515, 0.955] }}
```

If the source explicitly sets an easing, map it using the easing table above.

#### `Animated.timing` (old RN API) → EaseView timing

| Parameter  | RN Animated default         | EaseView default | Action                       |
| ---------- | --------------------------- | ---------------- | ---------------------------- |
| `duration` | `500`                       | `300`            | **Must set `duration: 500`** |
| `easing`   | `Easing.inOut(Easing.ease)` | `'easeInOut'`    | Same curve — omit            |

#### `Animated.spring` (old RN API) → EaseView spring

RN Animated uses `friction`/`tension` by default: `friction: 7, tension: 40`. These map to: `stiffness = tension`, `damping = friction`.

| Parameter           | RN Animated default | EaseView default | Action                       |
| ------------------- | ------------------- | ---------------- | ---------------------------- |
| stiffness (tension) | `40`                | `120`            | **Must set `stiffness: 40`** |
| damping (friction)  | `7`                 | `15`             | **Must set `damping: 7`**    |
| mass                | `1`                 | `1`              | Same — omit                  |

### Unit Conversions

- **Rotation:** Reanimated uses `'45deg'` strings in transforms → EaseView uses `45` (number, degrees). Strip the `'deg'` suffix and parse to number.
- **Translation:** Both use DIPs (density-independent pixels). No conversion needed.
- **Scale:** Both use unitless multipliers. No conversion needed.

---

## Phase 3: Dry-Run Report

**ALWAYS print this report before asking the user to select components. This report must be visible to the user before Phase 4.**

Print a structured report. Do NOT apply any changes yet.

Format:

```
## Migration Report

### Summary
- Files scanned: X
- Components with animations: Y
- Migratable: Z  |  Not migratable: W

### Migratable Components

#### `path/to/file.tsx` — ComponentName
**Current:** Brief description of what the animation does and which API it uses
**Proposed:** What the EaseView equivalent looks like (include exact transition values with mapped defaults)
**Changes:** What will be added/removed/modified
**Note:** (only if applicable) "Requires state changes for exit animation" or other caveats

### Not Migratable (will be skipped)

#### `path/to/file.tsx` — ComponentName
**Reason:** Why it can't be migrated (from decision tree)
```

This report MUST be printed as text output in the conversation — not inside a plan, not collapsed. The user needs to read it before selecting components in Phase 4.

---

## Phase 4: User Confirmation

**CRITICAL: You MUST use the `AskUserQuestion` tool here. Do NOT use plan mode, do NOT use text prompts, do NOT ask inline. Call the `AskUserQuestion` tool directly.**

Call `AskUserQuestion` with these exact parameters:

- `multiSelect`: `true`
- `questions`: a single question object with:
  - `header`: `"Migrate"`
  - `question`: `"Which components should be migrated to EaseView? All are selected — deselect any to skip."`
  - `multiSelect`: `true`
  - `options`: one entry per migratable component, each with:
    - `label`: the component name (e.g., `"AnimatedButton"`)
    - `description`: file path and brief animation description (e.g., `"src/components/animated-button.tsx — spring scale on press"`)

Example tool call for 2 migratable components:

```json
{
	"questions": [
		{
			"header": "Migrate",
			"question": "Which components should be migrated to EaseView? All are selected — deselect any to skip.",
			"multiSelect": true,
			"options": [
				{
					"label": "AnimatedButton",
					"description": "src/components/simple/animated-button.tsx — spring scale on press"
				},
				{
					"label": "Collapsible",
					"description": "src/components/ui/collapsible.tsx — fade-in entering animation"
				}
			]
		}
	]
}
```

**Wait for the user's response before proceeding.** Do not enter plan mode. Do not apply any changes without the user selecting components.

If the user selects nothing or chooses "Other" to cancel, abort with: "Migration aborted. No changes were made."

Only proceed to Phase 5 with the components the user confirmed.

---

## Phase 5: Apply Migrations

For each confirmed component, apply the migration:

### Migration Steps (per component)

1. **Add EaseView import** if not already present:

   ```typescript
   import { EaseView } from 'react-native-ease'
   ```

2. **Replace the animated view:**
   - `Animated.View` → `EaseView`
   - `<Animated.View style={[styles.box, animatedStyle]}>` → `<EaseView style={styles.box} animate={{ ... }} transition={{ ... }}>`

3. **Convert animation hooks to props:**
   - Remove `useSharedValue`, `useAnimatedStyle`, `withTiming`, `withSpring`, `withRepeat` calls
   - Convert their values into `animate`, `initialAnimate`, and `transition` props

4. **Convert entering/exiting animations:**
   - `entering={FadeIn}` → `initialAnimate={{ opacity: 0 }}` on the EaseView + `animate={{ opacity: 1 }}`
   - For `exiting`: introduce a state variable and `onTransitionEnd` callback:

     ```typescript
     const [visible, setVisible] = useState(true);
     const [mounted, setMounted] = useState(true);

     // When triggering exit:
     setVisible(false);

     // On the EaseView:
     {
       mounted && (
         <EaseView
           animate={{ opacity: visible ? 1 : 0 }}
           transition={{ type: 'timing', duration: 300 }}
           onTransitionEnd={({ finished }) => {
             if (finished && !visible) setMounted(false);
           }}
         >
           ...
         </EaseView>
       );
     }
     ```

5. **Clean up imports:**
   - Remove Reanimated imports that are no longer used in the file
   - Keep any Reanimated imports still referenced by non-migrated code in the same file
   - Never remove imports that are still used

6. **Print progress:**
   ```
   [1/N] Migrated ComponentName in path/to/file.tsx
   ```

### Safety Rules

These rules are non-negotiable. Violating them corrupts user code.

1. **When in doubt, skip.** If a pattern is ambiguous or you're not confident in the migration, add it to "Not Migratable" with reason: "Complex pattern — manual review recommended"
2. **Never remove imports still used elsewhere in the file.** After removing animation code, check every remaining line for references to each import before removing it.
3. **Preserve all non-animation logic.** Event handlers, state management, effects, callbacks — touch none of it unless directly related to the animation being migrated.
4. **Preserve component structure and public API.** Props, ref forwarding, exported types — keep them identical.
5. **Handle mixed files correctly.** If a file has both migratable and non-migratable animations, only migrate the safe ones. Keep Reanimated imports if any Reanimated code remains.
6. **Map rotation units correctly.** Reanimated `'45deg'` string → EaseView `45` number. If the source uses radians, convert: `radians * (180 / Math.PI)`.
7. **Map easing presets correctly.** See the mapping table in Phase 2.
8. **Do not introduce TypeScript errors.** Ensure all types are correct after migration. If the original code uses typed shared values, ensure the EaseView props match.

---

## Phase 6: Final Report

After all migrations are applied, print:

```
## Migration Complete

### Changed (X components)
- `path/to/file.tsx` — ComponentName: brief description of what was migrated

### Unchanged (Y components)
- `path/to/file.tsx` — ComponentName: reason skipped

### Next Steps
- Run your app and verify animations visually
- Run your test suite to check for regressions
- If no Reanimated code remains, consider removing `react-native-reanimated` from dependencies
```

---

## EaseView API Reference (for migration accuracy)

### Supported Animatable Properties

All properties in the `animate` prop:

| Property          | Type         | Default         | Notes                                |
| ----------------- | ------------ | --------------- | ------------------------------------ |
| `opacity`         | `number`     | `1`             | 0–1 range                            |
| `translateX`      | `number`     | `0`             | In DIPs (density-independent pixels) |
| `translateY`      | `number`     | `0`             | In DIPs                              |
| `scale`           | `number`     | `1`             | Shorthand for scaleX + scaleY        |
| `scaleX`          | `number`     | `1`             | Overrides scale for X axis           |
| `scaleY`          | `number`     | `1`             | Overrides scale for Y axis           |
| `rotate`          | `number`     | `0`             | Z-axis rotation in degrees           |
| `rotateX`         | `number`     | `0`             | X-axis rotation in degrees (3D)      |
| `rotateY`         | `number`     | `0`             | Y-axis rotation in degrees (3D)      |
| `borderRadius`    | `number`     | `0`             | In pixels                            |
| `backgroundColor` | `ColorValue` | `'transparent'` | Any RN color value                   |

### Transition Types

**Timing:**

```typescript
transition={{
  type: 'timing',
  duration: 300,        // ms, default 300
  easing: 'easeInOut',  // 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | [x1,y1,x2,y2]
  loop: 'repeat',       // 'repeat' | 'reverse' — requires initialAnimate
}}
```

**Spring:**

```typescript
transition={{
  type: 'spring',
  damping: 15,      // default 15
  stiffness: 120,   // default 120
  mass: 1,          // default 1
}}
```

**None (instant):**

```typescript
transition={{ type: 'none' }}
```

### Key Props

- `animate` — target values for animated properties
- `initialAnimate` — starting values (animates to `animate` on mount)
- `transition` — animation config (timing or spring)
- `onTransitionEnd` — callback with `{ finished: boolean }`
- `transformOrigin` — pivot point as `{ x: 0-1, y: 0-1 }`, default center
- `useHardwareLayer` — Android GPU optimization (boolean, default false)

### Important Constraints

- **Loop requires timing** (not spring) and `initialAnimate` must define the start value
- **No per-property transitions** — one transition config applies to all animated properties
- **No animation sequencing** — no equivalent to `withSequence`/`withDelay`
- **No gesture/scroll-driven animations** — EaseView is state-driven only
- **Style/animate conflict** — if a property appears in both `style` and `animate`, the animated value wins


================================================
FILE: .easignore
================================================
# .easignore - Overrides .gitignore for EAS builds

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native - DO NOT IGNORE android/ or ios/ folders in packages/apps
# We intentionally omit 'android/' and 'ios/' and 'apps/**/android' etc so they are INCLUDED.

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

# generated native folders - we want to KEEP these for local builds if they exist
# /ios
# /android
# apps/**/android
# apps/**/ios

temp-builds
.yarn/install-state.gz
**/.vscode/
!/.vscode/
!/.vscode/settings.json
!/.vscode/extensions.json
!/.vscode/tasks.json
!/.vscode/launch.json

.zed
.idea

# Secrets - Explicitly include the real ones
!google-services.real.json
!GoogleService-Info.real.plist
!**/google-services.real.json
!**/GoogleService-Info.real.plist

# Ignore the templates/dummies if necessary, but usually safe to keep


================================================
FILE: .gitattributes
================================================
apps/mobile/src/lib/api/bilibili/proto/*.js linguist-generated


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: '🐞 Bug 报告'
description: '提交 Bug 报告以帮助我们改进 BBPlayer'
title: '[Bug] <title>'
labels: ['bug']
type: Bug
body:
  - type: checkboxes
    attributes:
      label: '问题是否已存在?'
      description: '在提交前,请确保您已经搜索过现有的 issues,确认问题尚未被报告。'
      options:
        - label: '我搜索过了'
          required: true

  - type: input
    id: version
    attributes:
      label: 'App 版本'
      description: '当前你使用的 BBPlayer 版本号(建议优先升级最新版本尝试,我们不会对旧版本做 backport 修复)'
      placeholder: '例如:v1.2.3'
    validations:
      required: true

  - type: textarea
    id: bug-description
    attributes:
      label: '问题描述'
      description: '请清晰地描述该 Bug。如果可以,请附上详细错误日志(可通过设置页面「打开 Debug 日志」按钮开启详细日志后重新操作复现问题,并通过「分享今日运行日志」按钮导出)或截图'
    validations:
      required: true

  - type: textarea
    id: steps-to-reproduce
    attributes:
      label: '复现步骤'
      description: '请提供重现该问题的具体步骤。如果问题较简单,上面「问题描述」中足够描述清楚,可选择不填'
      placeholder: |
        1. 前往 '...'
        2. 点击 '....'
        3. 滚动到 '....'
    validations:
      required: false

  - type: textarea
    id: expected-behavior
    attributes:
      label: '期望行为'
      description: '请描述这里正确的行为应该是什么。'
    validations:
      required: true

  - type: input
    id: device-info
    attributes:
      label: '设备型号 + 操作系统(可选)'
      placeholder: '例如: Xiaomi 10 + Android 14'
    validations:
      required: false


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: '🚀 功能请求'
description: '为项目建议一个新功能或改进'
title: '[Feature] <title>'
labels: ['enhancement']
type: Feature
body:
  - type: checkboxes
    attributes:
      label: '功能请求是否已存在?'
      description: '在提交前,请确保您已经搜索过现有的 issues,确认相关功能请求尚未被提出。'
      options:
        - label: '我搜索过了'
          required: true

  - type: textarea
    id: details
    attributes:
      label: '建议内容'
      description: '详细描述您想要的功能或改进,以及需要该功能的原因。如果可以,请附上截图或使用场景描述来帮助我们理解需求。'
    validations:
      required: true


================================================
FILE: .github/release.yml
================================================
changelog:
  categories:
    - title: '🚀 New Features'
      labels:
        - 'feat'
        - 'feature'
    - title: '🐛 Bug Fixes'
      labels:
        - 'fix'
        - 'bug'
    - title: '📚 Documents'
      labels:
        - 'docs'
        - 'documentation'
    - title: 'Other Changes'
      labels:
        - '*'


================================================
FILE: .github/wiki/Home.md
================================================
# BBPlayer 开源项目 Wiki

> [!TIP]
> 如果您是最终用户,请优先访问 **[BBPlayer 官方文档站点](https://bbplayer.roitium.com)** 以获取安装及使用指南。

---

欢迎查阅 BBPlayer 相关的技术与开发文档。本项目采用 Monorepo 架构,各组件的文档分布如下:

## 📚 快速导航

### 🏁 [BBPlayer 移动端主程序 (App Home)](App-Home)

包含项目架构、贡献指南、发版流程以及开发规范。

### 🎸 [Orpheus 音频模块 (Orpheus Home)](orpheus-Home)

包含核心音频播放器的 API 方法、事件说明及技术方案。


================================================
FILE: .github/wiki/_Sidebar.md
================================================
### [BBPlayer Wiki](Home)

---

#### [移动端应用 (App)](App-Home)

- [贡献指南](CONTRIBUTING)
- [架构设计](ARCHITECTURE)
- [开发规范](BEST_PRACTICES)
- [发版流程](RELEASE)

#### [Orpheus 音频库](orpheus-Home)

- [API 方法](orpheus-API-Methods)
- [数据类型](orpheus-API-Types)
- [事件说明](orpheus-API-Events)

---

- [官网](https://bbplayer.roitium.com)
- [GitHub Repo](https://github.com/bbplayer-app/bbplayer)


================================================
FILE: .github/workflows/build.yml
================================================
name: Build and Release

on:
  workflow_dispatch:
    inputs:
      buildType:
        description: '构建类型'
        required: true
        default: 'prod'
        type: choice
        options:
          - prod
          - dev
          - preview
          - blank-test

  pull_request:
    types: [closed]
    branches:
      - master

env:
  NODE_VERSION: 22.x
  EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
  SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  MENTION_USER: '@roitium'

jobs:
  setup:
    if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
      buildType: ${{ steps.set-vars.outputs.buildType }}
      profile: ${{ steps.set-matrix.outputs.profile }}
      commitSha: ${{ steps.set-vars.outputs.commitSha }}
      prNumber: ${{ steps.set-vars.outputs.prNumber }}
    steps:
      - name: Determine Variables
        id: set-vars
        run: |
          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
            echo "buildType=prod" >> $GITHUB_OUTPUT
            echo "commitSha=${{ github.event.pull_request.merge_commit_sha }}" >> $GITHUB_OUTPUT
            echo "prNumber=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
          else
            echo "buildType=${{ github.event.inputs.buildType }}" >> $GITHUB_OUTPUT
            echo "commitSha=${{ github.sha }}" >> $GITHUB_OUTPUT
            echo "prNumber=" >> $GITHUB_OUTPUT
          fi

      - name: Determine Matrix and Profile
        id: set-matrix
        run: |
          BUILD_TYPE="${{ steps.set-vars.outputs.buildType }}"

          if [[ "$BUILD_TYPE" == "prod" ]]; then
            echo "matrix={\"include\":[{\"arch\":\"arm64-v8a\"},{\"arch\":\"armeabi-v7a\"},{\"arch\":\"x86_64\"},{\"arch\":\"x86\"}]}" >> $GITHUB_OUTPUT
            echo "profile=prod-ci" >> $GITHUB_OUTPUT
          elif [[ "$BUILD_TYPE" == "blank-test" ]]; then
             echo "matrix={\"include\":[{\"arch\":\"arm64-v8a\"}]}" >> $GITHUB_OUTPUT
             echo "profile=blank-test" >> $GITHUB_OUTPUT
          else
            # dev, preview - default to arm64-v8a
            echo "matrix={\"include\":[{\"arch\":\"arm64-v8a\"}]}" >> $GITHUB_OUTPUT
            echo "profile=$BUILD_TYPE" >> $GITHUB_OUTPUT
          fi

  build:
    needs: setup
    runs-on: blacksmith-2vcpu-ubuntu-2404
    strategy:
      fail-fast: false
      matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
    permissions:
      contents: write
      pull-requests: write
      packages: read
    env:
      BUILD_TYPE: ${{ needs.setup.outputs.buildType }}
      EAS_PROFILE: ${{ needs.setup.outputs.profile }}
      ABI_FILTERS: ${{ matrix.arch }}
      EAS_LOCAL_BUILD_SKIP_CLEANUP: 1

    environment: ${{ (github.event_name == 'pull_request' && 'production') || null }}

    steps:
      - name: 🏗 Setup repo
        uses: actions/checkout@v5
        with:
          ref: ${{ needs.setup.outputs.commitSha }}
          fetch-depth: 0 # 获取完整历史记录以计算 commit 数量

      - name: 🤖 Setup PNPM
        uses: pnpm/action-setup@v4

      - name: 🏗 Setup Node
        uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: pnpm

      - name: 🏗 Setup EAS
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ env.EXPO_TOKEN }}
          packager: pnpm

      - name: 📦 Install dependencies
        run: pnpm install

      - name: ⚙️ Prepare Variables
        id: prepare-vars
        run: |
          APP_VERSION=$(node -p "require('./apps/mobile/package.json').version")
          VERSION_CODE=$(git rev-list --count HEAD)

          # e.g. bbplayer-v1.0.0-prod-arm64-v8a
          APK_NAME="bbplayer-v${APP_VERSION}-${{ env.BUILD_TYPE }}-${{ matrix.arch }}"
          MAPPING_NAME="${APK_NAME}-mapping.txt"
          ARTIFACT_DIR="${RUNNER_TEMP}/${APK_NAME}"

          mkdir -p "$ARTIFACT_DIR"

          echo "APK_NAME=${APK_NAME}" >> $GITHUB_ENV
          echo "MAPPING_NAME=${MAPPING_NAME}" >> $GITHUB_ENV
          echo "ARTIFACT_DIR=${ARTIFACT_DIR}" >> $GITHUB_ENV
          echo "APK_TEMP_PATH=${ARTIFACT_DIR}/${APK_NAME}.apk" >> $GITHUB_ENV
          echo "MAPPING_TEMP_PATH=${ARTIFACT_DIR}/${MAPPING_NAME}" >> $GITHUB_ENV
          echo "EAS_LOCAL_BUILD_WORKINGDIR=${RUNNER_TEMP}/eas-local-build-${{ matrix.arch }}" >> $GITHUB_ENV
          echo "VERSION_CODE=${VERSION_CODE}" >> $GITHUB_ENV

          echo "apkName=${APK_NAME}" >> $GITHUB_OUTPUT
          echo "artifactDir=${ARTIFACT_DIR}" >> $GITHUB_OUTPUT

      - name: ⚙️ Prepare Firebase Config
        env:
          FIREBASE_ANDROID_JSON_B64: ${{ secrets.FIREBASE_ANDROID_JSON_B64 }}
        if: ${{ env.FIREBASE_ANDROID_JSON_B64 != '' }}
        run: |
          mkdir -p apps/mobile/assets/config/google-services
          echo "$FIREBASE_ANDROID_JSON_B64" | base64 -d > apps/mobile/assets/config/google-services/google-services.real.json

      - name: 🚀 Build APK
        if: env.BUILD_TYPE != 'blank-test'
        run: cd apps/mobile && eas build --platform android --profile ${{ env.EAS_PROFILE }} --local --no-wait --output="$APK_TEMP_PATH"

      - name: 📦 Collect R8 Mapping
        if: env.BUILD_TYPE != 'blank-test' && env.EAS_PROFILE != 'dev'
        run: |
          SEARCH_ROOTS=()

          if [[ -d "$EAS_LOCAL_BUILD_WORKINGDIR" ]]; then
            SEARCH_ROOTS+=("$EAS_LOCAL_BUILD_WORKINGDIR")
          fi

          if [[ -d apps/mobile/android/app/build/outputs/mapping ]]; then
            SEARCH_ROOTS+=(apps/mobile/android/app/build/outputs/mapping)
          fi

          if [[ ${#SEARCH_ROOTS[@]} -eq 0 ]]; then
            echo "No Android mapping output directories were found."
            echo "Checked EAS local build working directory: $EAS_LOCAL_BUILD_WORKINGDIR"
            exit 1
          fi

          MAPPING_SOURCE=$(find "${SEARCH_ROOTS[@]}" -path "*/release/mapping.txt" -type f -print -quit)

          if [[ -z "$MAPPING_SOURCE" ]]; then
            echo "Expected R8 mapping file was not found."
            printf 'Checked directories:\n'
            printf ' - %s\n' "${SEARCH_ROOTS[@]}"
            exit 1
          fi

          cp "$MAPPING_SOURCE" "$MAPPING_TEMP_PATH"

      - name: 🧪 Create Dummy APK
        if: env.BUILD_TYPE == 'blank-test'
        run: |
          echo "Dummy APK $APK_NAME" > "$APK_TEMP_PATH"

      - name: 🚀 Upload Artifact
        uses: actions/upload-artifact@v5
        with:
          name: ${{ steps.prepare-vars.outputs.apkName }}
          path: ${{ steps.prepare-vars.outputs.artifactDir }}
          if-no-files-found: error

  release:
    needs: [setup, build]
    if: needs.setup.outputs.buildType == 'prod' || needs.setup.outputs.buildType == 'blank-test'
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: 🏗 Setup repo
        uses: actions/checkout@v5
        with:
          ref: ${{ needs.setup.outputs.commitSha }}

      - name: 📥 Download Artifacts
        uses: actions/download-artifact@v4
        with:
          path: dist
          merge-multiple: true

      - name: ⚙️ Prepare Release Variables
        run: |
          APP_VERSION=$(node -p "require('./apps/mobile/package.json').version")
          RELEASE_TAG="v${APP_VERSION}"
          echo "RELEASE_TAG=${RELEASE_TAG}" >> $GITHUB_ENV
          ls -R dist/

      - name: 🎁 Create Release
        run: |
          mapfile -t FILES < <(find dist -type f \( -name "*.apk" -o -name "*-mapping.txt" \) | sort)

          gh release create "$RELEASE_TAG" \
            "${FILES[@]}" \
            --title "$RELEASE_TAG" \
            --draft \
            --notes "auto release by GitHub Actions"

  notify:
    needs: [setup, build]
    if: always() && github.event_name == 'pull_request'
    permissions:
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - name: 💬 Send build status notification
        uses: actions/github-script@v8
        with:
          script: |
            const buildResult = "${{ needs.build.result }}";
            const buildType = "${{ needs.setup.outputs.buildType }}";
            const commitSha = "${{ needs.setup.outputs.commitSha }}";
            const prNumber = "${{ needs.setup.outputs.prNumber }}";
            const runUrl = "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}";
            const mention = "${{ env.MENTION_USER }}";

            const icon = buildResult == 'success' ? '✅' : '❌';
            const title = buildResult == 'success' ? `构建软件包成功` : `构建软件包失败`;

            let body = `
            ## ${icon} ${title} (${buildType})

            ${mention}, 新版触发的构建已经完成

            - **状态:** ${buildResult}
            - **提交:**
            ${commitSha.substring(0, 7)}
            - **详细信息:** [Workflow](${runUrl})
            `;

            if (prNumber) {
              body += `\n- **触发事件:** PR #${prNumber}`;
              try {
                await github.rest.issues.createComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: prNumber,
                  body: body
                });
              } catch (e) {
                console.error(`Failed to comment on PR: ${e.message}.`);
              }
            }


================================================
FILE: .github/workflows/check-lyricon-updates.yml
================================================
name: Check Lyricon Updates

on:
  schedule:
    # 每天凌晨 2 点运行一次 (UTC 时间)
    - cron: '0 2 * * *'
  workflow_dispatch: # 允许手动触发
    inputs:
      force_update:
        description: '强制更新(即使无变化也创建/更新 PR)'
        required: false
        default: 'false'
        type: choice
        options:
          - 'true'
          - 'false'

env:
  ISSUE_TITLE: '🔄 Lyricon Provider 上游代码有更新'
  ISSUE_LABELS: 'dependencies,lyricon'
  PR_BRANCH: 'bot/lyricon-update'
  PR_TITLE: '[Bot] Update Lyricon Provider'
  LYRICON_REPO: 'tomakino/lyricon'

jobs:
  check-updates:
    runs-on: ubuntu-latest
    permissions:
      issues: write
      contents: write
      pull-requests: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Fetch latest commit from Lyricon
        id: fetch_commit
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # 获取 tomakino/lyricon 仓库 master 分支的最新 commit hash
          RESPONSE=$(curl -sL -H "Authorization: Bearer $GITHUB_TOKEN" "https://api.github.com/repos/${{ env.LYRICON_REPO }}/commits/master")
          LATEST_COMMIT=$(echo "$RESPONSE" | jq -r '.sha // empty')

          if [ -z "$LATEST_COMMIT" ] || [ "$LATEST_COMMIT" = "null" ]; then
            echo "Error: Could not fetch latest commit hash. Response was:"
            echo "$RESPONSE"
            exit 1
          fi

          echo "latest_commit=$LATEST_COMMIT" >> $GITHUB_OUTPUT

          # 获取我们当前记录的 commit (如果有的话)
          CURRENT_COMMIT="unknown"
          VERSION_FILE="packages/orpheus/.lyricon_version"
          if [ -f "$VERSION_FILE" ]; then
            CURRENT_COMMIT=$(cat "$VERSION_FILE")
          fi
          echo "current_commit=$CURRENT_COMMIT" >> $GITHUB_OUTPUT

          echo "Current: $CURRENT_COMMIT"
          echo "Latest:  $LATEST_COMMIT"

      - name: Check if there are changes in provider or model directories
        if: steps.fetch_commit.outputs.current_commit != steps.fetch_commit.outputs.latest_commit || github.event.inputs.force_update == 'true'
        id: check_diff
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # 比较我们记录的 commit 和最新的 commit 之间,provider 和 model 目录是否有变动
          if [ "${{ github.event.inputs.force_update }}" = "true" ]; then
            echo "has_changes=true" >> $GITHUB_OUTPUT
            echo "Force update enabled."
            exit 0
          fi

          DIFF_URL="https://api.github.com/repos/${{ env.LYRICON_REPO }}/compare/${{ steps.fetch_commit.outputs.current_commit }}...${{ steps.fetch_commit.outputs.latest_commit }}"

          HAS_CHANGES=$(curl -sL -H "Authorization: Bearer $GITHUB_TOKEN" "$DIFF_URL" | jq -r '(.files // [])[] | select(.filename | test("lyric/bridge/provider/src/main/|lyric/model/src/main/")) | .filename' | wc -l)

          if [ "$HAS_CHANGES" -gt 0 ]; then
            echo "has_changes=true" >> $GITHUB_OUTPUT
            echo "Found changes in relevant directories."
          else
            echo "has_changes=false" >> $GITHUB_OUTPUT
            echo "No changes in provider or model directories."
          fi

      - name: Find or create tracking issue
        if: steps.check_diff.outputs.has_changes == 'true' || steps.fetch_commit.outputs.current_commit == 'unknown'
        id: manage_issue
        uses: actions/github-script@v7
        with:
          script: |
            const issueTitle = '${{ env.ISSUE_TITLE }}';
            const labels = '${{ env.ISSUE_LABELS }}'.split(',');

            // 查找已有的 lyricon issue
            const issues = await github.rest.issues.listForRepo({
              owner: context.repo.owner,
              repo: context.repo.repo,
              state: 'open',
              labels: labels[1] // 'lyricon'
            });

            const existingIssue = issues.data.find(issue => 
              issue.title.includes('Lyricon') && issue.title.includes('更新')
            );

            if (existingIssue) {
              console.log(`Found existing issue: #${existingIssue.number}`);
              core.setOutput('issue_number', existingIssue.number);
              core.setOutput('issue_exists', 'true');
            } else {
              console.log('No existing issue found');
              core.setOutput('issue_exists', 'false');
            }

      - name: Update issue and add comment
        if: steps.manage_issue.outputs.issue_exists == 'true' || steps.manage_issue.outputs.issue_exists == 'false'
        uses: actions/github-script@v7
        with:
          script: |
            const latestCommit = '${{ steps.fetch_commit.outputs.latest_commit }}';
            const currentCommit = '${{ steps.fetch_commit.outputs.current_commit }}';
            const issueNumber = '${{ steps.manage_issue.outputs.issue_number }}';
            const issueExists = '${{ steps.manage_issue.outputs.issue_exists }}' === 'true';
            const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });

            // 构建 issue body
            let body = `## 📢 Lyricon Provider 上游代码更新跟踪\n\n`;
            body += `**检测时间**: ${now}\n\n`;
            body += `### 当前状态\n`;
            body += `- **当前版本**: \`${currentCommit === 'unknown' ? '未知' : currentCommit.substring(0, 7)}\`\n`;
            body += `- **最新版本**: \`${latestCommit.substring(0, 7)}\`\n\n`;

            if (currentCommit !== 'unknown') {
              body += `### 变更详情\n`;
              body += `[查看完整对比](https://github.com/${{ env.LYRICON_REPO }}/compare/${currentCommit}...${latestCommit})\n\n`;
            }

            body += `### 自动更新\n`;
            body += `🤖 已自动创建 Draft PR 进行代码同步,请查看下方的 PR 链接。\n\n`;

            body += `### 手动更新\n`;
            body += `如需手动更新,请运行:\n`;
            body += "\`\`\`bash\n";
            body += `./scripts/update-lyricon.sh ${latestCommit.substring(0, 7)}\n`;
            body += "\`\`\`\n\n";

            body += `---\n`;
            body += `*此 Issue 由 GitHub Actions 自动维护,会在代码同步完成后自动关闭。*`;

            if (issueExists) {
              // 更新现有 issue
              await github.rest.issues.update({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: parseInt(issueNumber),
                body: body
              });
              console.log(`Updated issue #${issueNumber}`);
              
              // 添加评论通知
              let commentBody = `## 🔔 检测到新的更新\n\n`;
              commentBody += `**时间**: ${now}\n`;
              commentBody += `**最新 Commit**: \`${latestCommit.substring(0, 7)}\`\n\n`;
              commentBody += `Issue 描述已更新,请查看最新的变更详情。`;
              
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: parseInt(issueNumber),
                body: commentBody
              });
              console.log(`Added comment to issue #${issueNumber}`);
            } else {
              // 创建新 issue
              const newIssue = await github.rest.issues.create({
                owner: context.repo.owner,
                repo: context.repo.repo,
                title: '${{ env.ISSUE_TITLE }}',
                body: body,
                labels: '${{ env.ISSUE_LABELS }}'.split(',')
              });
              console.log(`Created new issue #${newIssue.data.number}`);
              core.setOutput('issue_number', newIssue.data.number);
            }

      - name: Setup Git
        if: steps.check_diff.outputs.has_changes == 'true' || steps.fetch_commit.outputs.current_commit == 'unknown'
        run: |
          git config --global user.name 'github-actions[bot]'
          git config --global user.email 'github-actions[bot]@users.noreply.github.com'

      - name: Check for existing PR branch
        if: steps.check_diff.outputs.has_changes == 'true' || steps.fetch_commit.outputs.current_commit == 'unknown'
        id: check_branch
        run: |
          BRANCH_NAME="${{ env.PR_BRANCH }}"

          # 检查远程分支是否存在
          if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then
            echo "branch_exists=true" >> $GITHUB_OUTPUT
            echo "Remote branch $BRANCH_NAME exists"
          else
            echo "branch_exists=false" >> $GITHUB_OUTPUT
            echo "Remote branch $BRANCH_NAME does not exist"
          fi

          echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT

      - name: Update existing branch
        if: steps.check_branch.outputs.branch_exists == 'true'
        run: |
          BRANCH_NAME="${{ steps.check_branch.outputs.branch_name }}"
          LATEST_COMMIT="${{ steps.fetch_commit.outputs.latest_commit }}"

          # 获取远程分支
          git fetch origin "$BRANCH_NAME"

          # 检出分支
          git checkout -B "$BRANCH_NAME" origin/"$BRANCH_NAME"

          # 运行更新脚本
          ./scripts/update-lyricon.sh "$LATEST_COMMIT"

          # 提交更改
          git add -A
          if git diff --cached --quiet; then
            echo "No changes to commit"
          else
            git commit -m "chore(orpheus): update lyricon to ${LATEST_COMMIT:0:7}"
            git push origin "$BRANCH_NAME"
            echo "Updated branch $BRANCH_NAME"
          fi

      - name: Create new branch
        if: steps.check_branch.outputs.branch_exists == 'false'
        run: |
          BRANCH_NAME="${{ steps.check_branch.outputs.branch_name }}"
          LATEST_COMMIT="${{ steps.fetch_commit.outputs.latest_commit }}"

          # 创建并切换到新分支
          git checkout -b "$BRANCH_NAME"

          # 运行更新脚本
          ./scripts/update-lyricon.sh "$LATEST_COMMIT"

          # 提交更改
          git add -A
          git commit -m "chore(orpheus): update lyricon to ${LATEST_COMMIT:0:7}"
          git push -u origin "$BRANCH_NAME"
          echo "Created and pushed branch $BRANCH_NAME"

      - name: Find or create Draft PR
        if: steps.check_diff.outputs.has_changes == 'true' || steps.fetch_commit.outputs.current_commit == 'unknown'
        id: manage_pr
        uses: actions/github-script@v7
        with:
          script: |
            const latestCommit = '${{ steps.fetch_commit.outputs.latest_commit }}';
            const currentCommit = '${{ steps.fetch_commit.outputs.current_commit }}';
            const branchName = '${{ steps.check_branch.outputs.branch_name }}';
            const issueNumber = '${{ steps.manage_issue.outputs.issue_number }}' || '${{ steps.manage_issue_issue_number }}';

            // 查找已有的 PR
            const prs = await github.rest.pulls.list({
              owner: context.repo.owner,
              repo: context.repo.repo,
              state: 'open',
              head: `${context.repo.owner}:${branchName}`
            });

            const existingPR = prs.data[0];

            // 构建 PR body
            let body = `## 🤖 自动更新: Lyricon Provider\n\n`;
            body += `此 PR 自动同步上游 [tomakino/lyricon](https://github.com/tomakino/lyricon) 的最新更改。\n\n`;
            body += `### 更新详情\n`;
            body += `- **从**: \`${currentCommit === 'unknown' ? '未知' : currentCommit.substring(0, 7)}\`\n`;
            body += `- **到**: \`${latestCommit.substring(0, 7)}\`\n\n`;

            if (currentCommit !== 'unknown') {
              body += `### 变更摘要\n`;
              body += `[查看完整对比](https://github.com/${{ env.LYRICON_REPO }}/compare/${currentCommit}...${latestCommit})\n\n`;
            }

            body += `### 相关 Issue\n`;
            body += `Closes #${issueNumber}\n\n`;

            body += `---\n`;
            body += `*此 PR 由 GitHub Actions 自动创建和维护。*`;

            if (existingPR) {
              // 更新现有 PR
              await github.rest.pulls.update({
                owner: context.repo.owner,
                repo: context.repo.repo,
                pull_number: existingPR.number,
                body: body
              });
              console.log(`Updated existing PR #${existingPR.number}`);
              core.setOutput('pr_number', existingPR.number);
              core.setOutput('pr_url', existingPR.html_url);
            } else {
              // 创建新 PR
              const newPR = await github.rest.pulls.create({
                owner: context.repo.owner,
                repo: context.repo.repo,
                title: '${{ env.PR_TITLE }}',
                head: branchName,
                base: 'dev',
                body: body,
                draft: true
              });
              console.log(`Created new PR #${newPR.data.number}`);
              core.setOutput('pr_number', newPR.data.number);
              core.setOutput('pr_url', newPR.data.html_url);
              
              // 添加标签
              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: newPR.data.number,
                labels: ['dependencies', 'lyricon', 'automated']
              });
            }

      - name: Summary
        if: always() && (steps.check_diff.outputs.has_changes == 'true' || steps.fetch_commit.outputs.current_commit == 'unknown')
        run: |
          echo "## 📋 工作流执行摘要" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "- **当前 Commit**: ${{ steps.fetch_commit.outputs.current_commit }}" >> $GITHUB_STEP_SUMMARY
          echo "- **最新 Commit**: ${{ steps.fetch_commit.outputs.latest_commit }}" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          if [ "${{ steps.manage_issue.outputs.issue_exists }}" = "true" ]; then
            echo "- **Issue**: #${{ steps.manage_issue.outputs.issue_number }} (已更新)" >> $GITHUB_STEP_SUMMARY
          else
            echo "- **Issue**: #${{ steps.manage_issue.outputs.issue_number }} (新创建)" >> $GITHUB_STEP_SUMMARY
          fi

          echo "- **PR**: ${{ steps.manage_pr.outputs.pr_url }}" >> $GITHUB_STEP_SUMMARY
          echo "- **分支**: ${{ steps.check_branch.outputs.branch_name }}" >> $GITHUB_STEP_SUMMARY


================================================
FILE: .github/workflows/nightly.yml
================================================
name: Nightly Build

on:
  # 支持 Actions 页面手动触发
  workflow_dispatch:

  # 支持 PR 评论触发
  issue_comment:
    types: [created]

env:
  NODE_VERSION: 22.x
  EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
  SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  NIGHTLY_TAG: nightly
  ALLOWED_USER: roitium

jobs:
  check-trigger:
    permissions:
      issues: write
      contents: read
      pull-requests: write
    # 手动触发时直接通过,PR 评论触发时需要检查条件
    if: |
      github.event_name == 'workflow_dispatch' ||
      (github.event_name == 'issue_comment' &&
       github.event.issue.pull_request &&
       github.event.comment.body == '/build-nightly')
    runs-on: ubuntu-latest
    outputs:
      should_build: ${{ steps.check.outputs.should_build }}
      pr_number: ${{ steps.context.outputs.pr_number }}
      head_sha: ${{ steps.context.outputs.head_sha }}
      trigger_type: ${{ steps.context.outputs.trigger_type }}
    steps:
      - name: 🔐 Check user permission
        id: check
        run: |
          if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
            # 手动触发,直接允许
            echo "should_build=true" >> $GITHUB_OUTPUT
            echo "✅ Manual trigger by ${{ github.actor }}"
          elif [[ "${{ github.event.comment.user.login }}" == "${{ env.ALLOWED_USER }}" ]]; then
            echo "should_build=true" >> $GITHUB_OUTPUT
            echo "✅ User ${{ github.event.comment.user.login }} is allowed to trigger nightly build"
          else
            echo "should_build=false" >> $GITHUB_OUTPUT
            echo "❌ User ${{ github.event.comment.user.login }} is not allowed to trigger nightly build"
          fi

      - name: 📝 React to comment
        if: github.event_name == 'issue_comment' && steps.check.outputs.should_build == 'true'
        uses: actions/github-script@v7
        with:
          script: |
            await github.rest.reactions.createForIssueComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              comment_id: context.payload.comment.id,
              content: 'rocket'
            });

      - name: 🔍 Determine build context
        if: steps.check.outputs.should_build == 'true'
        id: context
        uses: actions/github-script@v7
        with:
          script: |
            // 获取默认分支的最新 commit
            const repo = await github.rest.repos.get({
              owner: context.repo.owner,
              repo: context.repo.repo
            });
            const defaultBranch = repo.data.default_branch;

            const ref = await github.rest.git.getRef({
              owner: context.repo.owner,
              repo: context.repo.repo,
              ref: `heads/${defaultBranch}`
            });

            core.setOutput('head_sha', ref.data.object.sha);

            if (context.eventName === 'workflow_dispatch') {
              core.setOutput('pr_number', '');
              core.setOutput('trigger_type', 'manual');
            } else {
              core.setOutput('pr_number', context.issue.number);
              core.setOutput('trigger_type', 'pr_comment');
            }

  build:
    needs: check-trigger
    concurrency:
      group: nightly-build
      cancel-in-progress: true
    if: needs.check-trigger.outputs.should_build == 'true'
    runs-on: blacksmith-2vcpu-ubuntu-2404
    permissions:
      contents: write
      pull-requests: write
      packages: read
    env:
      ABI_FILTERS: arm64-v8a

    steps:
      - name: 🏗 Setup repo
        uses: actions/checkout@v5
        with:
          ref: ${{ needs.check-trigger.outputs.head_sha }}
          fetch-depth: 0 # 获取完整历史记录以计算 commit 数量

      - name: 🤖 Setup PNPM
        uses: pnpm/action-setup@v4

      - name: 🏗 Setup Node
        uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: pnpm

      - name: 🏗 Setup EAS
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ env.EXPO_TOKEN }}
          packager: pnpm

      - name: 📦 Install dependencies
        run: pnpm install

      - name: ⚙️ Prepare Variables
        run: |
          APP_VERSION=$(node -p "require('./apps/mobile/package.json').version")
          COMMIT_SHA="${{ needs.check-trigger.outputs.head_sha }}"
          SHORT_SHA="${COMMIT_SHA:0:7}"
          TIMESTAMP=$(date -u +"%Y%m%d-%H%M%S")
          VERSION_CODE=$(git rev-list --count HEAD)

          APK_NAME="bbplayer-nightly-${SHORT_SHA}"

          echo "APP_VERSION=${APP_VERSION}" >> $GITHUB_ENV
          echo "APK_NAME=${APK_NAME}" >> $GITHUB_ENV
          echo "APK_PATH=${{ runner.temp }}/${APK_NAME}.apk" >> $GITHUB_ENV
          echo "COMMIT_SHA=${COMMIT_SHA}" >> $GITHUB_ENV
          echo "SHORT_SHA=${SHORT_SHA}" >> $GITHUB_ENV
          echo "TIMESTAMP=${TIMESTAMP}" >> $GITHUB_ENV
          echo "VERSION_CODE=${VERSION_CODE}" >> $GITHUB_ENV

          echo "📊 Calculated VERSION_CODE: ${VERSION_CODE}"

      - name: 🚀 Build APK
        run: cd apps/mobile && eas build --platform android --profile prod-v8a --local --no-wait --output=${{ env.APK_PATH }}
        env:
          VERSION_CODE: ${{ env.VERSION_CODE }}

      - name: 📦 Upload Artifact
        uses: actions/upload-artifact@v5
        with:
          name: ${{ env.APK_NAME }}
          path: ${{ env.APK_PATH }}
          if-no-files-found: error

      - name: 🏷️ Update Nightly Release
        run: |
          RELEASE_NOTES="## 🌙 Nightly Build

          **⚠️ 警告:这是自动构建的开发版本,可能不稳定**

          ---

          | 信息 | 值 |
          |------|-----|
          | 📝 Commit | [\`${{ env.SHORT_SHA }}\`](${{ github.server_url }}/${{ github.repository }}/commit/${{ env.COMMIT_SHA }}) |
          | 🕐 构建时间 | ${{ env.TIMESTAMP }} UTC |
          | 📦 架构 | arm64-v8a |"

          # 检查 nightly release 是否存在
          if gh release view ${{ env.NIGHTLY_TAG }} > /dev/null 2>&1; then
            echo "Nightly release exists, updating..."
            
            # 删除所有旧的 APK 附件
            gh release view ${{ env.NIGHTLY_TAG }} --json assets -q '.assets[].name' | while read asset; do
              if [[ "$asset" == *.apk ]]; then
                echo "Deleting old asset: $asset"
                gh release delete-asset ${{ env.NIGHTLY_TAG }} "$asset" --yes || true
              fi
            done
            
            # 上传新的 APK
            gh release upload ${{ env.NIGHTLY_TAG }} "${{ env.APK_PATH }}" --clobber
            
            # 更新 release notes
            gh release edit ${{ env.NIGHTLY_TAG }} \
              --title "Nightly Build" \
              --notes "$RELEASE_NOTES"
          else
            echo "Creating new nightly release..."
            gh release create ${{ env.NIGHTLY_TAG }} \
              "${{ env.APK_PATH }}" \
              --title "Nightly Build" \
              --prerelease \
              --notes "$RELEASE_NOTES"
          fi

      - name: 🔄 Update Nightly Tag
        run: |
          # 强制更新 nightly tag 指向当前构建的 commit
          git tag -f ${{ env.NIGHTLY_TAG }} ${{ env.COMMIT_SHA }}
          git push -f origin ${{ env.NIGHTLY_TAG }}

  notify:
    needs: [check-trigger, build]
    # 只在 PR 评论触发时发送通知
    if: always() && needs.check-trigger.outputs.should_build == 'true' && needs.check-trigger.outputs.trigger_type == 'pr_comment'
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - name: 💬 Send build notification
        uses: actions/github-script@v7
        with:
          script: |
            const buildResult = "${{ needs.build.result }}";
            const prNumber = ${{ needs.check-trigger.outputs.pr_number }};
            const headSha = "${{ needs.check-trigger.outputs.head_sha }}";
            const shortSha = headSha.substring(0, 7);
            const runUrl = "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}";
            const releaseUrl = "${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ env.NIGHTLY_TAG }}";

            let body;
            if (buildResult === 'success') {
              body = `## ✅ Nightly 构建成功

            | 信息 | 值 |
            |------|-----|
            | 📝 Commit | \`${shortSha}\` |
            | 📦 下载 | [Nightly Release](${releaseUrl}) |
            | 🔗 详情 | [Workflow](${runUrl}) |

            APK 已上传到 [Nightly Release](${releaseUrl}) 🚀`;
            } else if (buildResult === 'cancelled') {
              body = `## ⏹️ Nightly 构建已取消

            构建被新的 \`/build-nightly\` 请求取消。

            - **Commit:** \`${shortSha}\`
            - **详情:** [Workflow](${runUrl})`;
            } else {
              body = `## ❌ Nightly 构建失败

            - **Commit:** \`${shortSha}\`
            - **详情:** [Workflow](${runUrl})

            请查看 workflow 日志了解详情。`;
            }

            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: prNumber,
              body: body
            });


================================================
FILE: .github/workflows/pr-checks.yml
================================================
name: 'PR Checks'

on: pull_request

jobs:
  lint:
    name: 🚨 Lint Codebase
    runs-on: ubuntu-latest
    permissions:
      packages: read
    steps:
      - name: 🏗 Setup repo
        uses: actions/checkout@v5

      - name: 🏗 Setup PNPM
        uses: pnpm/action-setup@v4

      - name: 🏗 Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '22.x'
          cache: 'pnpm'

      - name: 📦 Install dependencies
        run: pnpm install

      - name: 🕵️ Check Dependencies
        run: pnpm check:deps

      - name: 🚀 Run Lint
        run: pnpm lint


================================================
FILE: .github/workflows/update.yml
================================================
name: Hot Update
on: workflow_dispatch
jobs:
  update:
    runs-on: ubuntu-latest
    permissions:
      packages: read
    steps:
      - name: 🏗 Setup repo
        uses: actions/checkout@v5

      - name: 🤖 Setup PNPM
        uses: pnpm/action-setup@v4

      - name: 🏗 Setup Node
        uses: actions/setup-node@v6
        with:
          node-version: 22.x
          cache: pnpm

      - name: 🏗 Setup EAS
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
          packager: pnpm

      - name: 📦 Install dependencies
        run: pnpm install

      - name: 🚀 Create update
        run: cd apps/mobile && eas update --auto --platform=android

      - name: 🚀 Submit source map
        run: cd apps/mobile && SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} npx sentry-expo-upload-sourcemaps dist


================================================
FILE: .github/workflows/wiki.yml
================================================
name: Publish wiki
on:
  push:
    branches: [dev]
    paths:
      - apps/mobile/docs/**
      - packages/orpheus/docs/**
      - .github/wiki/**
      - .github/workflows/wiki.yml

concurrency:
  group: publish-wiki
  cancel-in-progress: true

permissions:
  contents: write

jobs:
  publish-wiki:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Prepare Wiki Content
        run: |
          mkdir temp_wiki
          # 1. Copy main entry files from .github/wiki (Home.md, _Sidebar.md)
          cp -r .github/wiki/* temp_wiki/
          # 2. Move apps/mobile/docs/Home.md -> App-Home.md
          cp apps/mobile/docs/Home.md temp_wiki/App-Home.md
          # 3. Copy remaining mobile docs (excluding Home.md)
          find apps/mobile/docs -maxdepth 1 -type f ! -name 'Home.md' -exec cp {} temp_wiki/ \;
          # 4. Copy orpheus docs with orpheus- prefix (flat structure for GitHub Wiki)
          for file in packages/orpheus/docs/*.md; do
            filename=$(basename "$file")
            cp "$file" "temp_wiki/orpheus-$filename"
          done
      - uses: Andrew-Chen-Wang/github-wiki-action@v5
        with:
          path: temp_wiki


================================================
FILE: .gitignore
================================================
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

app-example

# generated native folders
/ios
/android

app-example
temp-builds
.yarn/install-state.gz
**/.vscode/
!/.vscode/
!/.vscode/settings.json
!/.vscode/extensions.json
!/.vscode/tasks.json
!/.vscode/launch.json

.zed
.idea

gha-creds-*.json
.env
apps/**/android
apps/**/ios
# Desloppify
.desloppify/
external-playlist-experiment/


================================================
FILE: .gitleaks-baseline.json
================================================
[
	{
		"RuleID": "generic-api-key",
		"Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.",
		"StartLine": 9,
		"EndLine": 9,
		"StartColumn": 14,
		"EndColumn": 43,
		"Match": "presetKey = '0CoJUm6Qyw8W8jud'",
		"Secret": "0CoJUm6Qyw8W8jud",
		"File": ".gitleaks-baseline.json",
		"SymlinkFile": "",
		"Commit": "c1f28d5888660087f55d3e7144a9190774d87e9f",
		"Link": "https://github.com/bbplayer-app/BBPlayer/blob/c1f28d5888660087f55d3e7144a9190774d87e9f/.gitleaks-baseline.json#L9",
		"Entropy": 3.875,
		"Author": "roitium",
		"Email": "65794453+roitium@users.noreply.github.com",
		"Date": "2026-02-11T11:27:02Z",
		"Message": "chore(root): add gitleaks",
		"Tags": [],
		"Fingerprint": "c1f28d5888660087f55d3e7144a9190774d87e9f:.gitleaks-baseline.json:generic-api-key:9"
	},
	{
		"RuleID": "generic-api-key",
		"Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.",
		"StartLine": 10,
		"EndLine": 10,
		"StartColumn": 5,
		"EndColumn": 31,
		"Match": "Secret\": \"0CoJUm6Qyw8W8jud\"",
		"Secret": "0CoJUm6Qyw8W8jud",
		"File": ".gitleaks-baseline.json",
		"SymlinkFile": "",
		"Commit": "c1f28d5888660087f55d3e7144a9190774d87e9f",
		"Link": "https://github.com/bbplayer-app/BBPlayer/blob/c1f28d5888660087f55d3e7144a9190774d87e9f/.gitleaks-baseline.json#L10",
		"Entropy": 3.875,
		"Author": "roitium",
		"Email": "65794453+roitium@users.noreply.github.com",
		"Date": "2026-02-11T11:27:02Z",
		"Message": "chore(root): add gitleaks",
		"Tags": [],
		"Fingerprint": "c1f28d5888660087f55d3e7144a9190774d87e9f:.gitleaks-baseline.json:generic-api-key:10"
	},
	{
		"RuleID": "generic-api-key",
		"Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.",
		"StartLine": 30,
		"EndLine": 30,
		"StartColumn": 14,
		"EndColumn": 43,
		"Match": "presetKey = '0CoJUm6Qyw8W8jud'",
		"Secret": "0CoJUm6Qyw8W8jud",
		"File": ".gitleaks-baseline.json",
		"SymlinkFile": "",
		"Commit": "c1f28d5888660087f55d3e7144a9190774d87e9f",
		"Link": "https://github.com/bbplayer-app/BBPlayer/blob/c1f28d5888660087f55d3e7144a9190774d87e9f/.gitleaks-baseline.json#L30",
		"Entropy": 3.875,
		"Author": "roitium",
		"Email": "65794453+roitium@users.noreply.github.com",
		"Date": "2026-02-11T11:27:02Z",
		"Message": "chore(root): add gitleaks",
		"Tags": [],
		"Fingerprint": "c1f28d5888660087f55d3e7144a9190774d87e9f:.gitleaks-baseline.json:generic-api-key:30"
	},
	{
		"RuleID": "generic-api-key",
		"Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.",
		"StartLine": 31,
		"EndLine": 31,
		"StartColumn": 5,
		"EndColumn": 31,
		"Match": "Secret\": \"0CoJUm6Qyw8W8jud\"",
		"Secret": "0CoJUm6Qyw8W8jud",
		"File": ".gitleaks-baseline.json",
		"SymlinkFile": "",
		"Commit": "c1f28d5888660087f55d3e7144a9190774d87e9f",
		"Link": "https://github.com/bbplayer-app/BBPlayer/blob/c1f28d5888660087f55d3e7144a9190774d87e9f/.gitleaks-baseline.json#L31",
		"Entropy": 3.875,
		"Author": "roitium",
		"Email": "65794453+roitium@users.noreply.github.com",
		"Date": "2026-02-11T11:27:02Z",
		"Message": "chore(root): add gitleaks",
		"Tags": [],
		"Fingerprint": "c1f28d5888660087f55d3e7144a9190774d87e9f:.gitleaks-baseline.json:generic-api-key:31"
	},
	{
		"RuleID": "sentry-org-token",
		"Description": "Found a Sentry.io Organization Token, risking unauthorized access to error tracking services and sensitive application data.",
		"StartLine": 51,
		"EndLine": 51,
		"StartColumn": 14,
		"EndColumn": 205,
		"Match": "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\"",
		"Secret": "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\"",
		"File": ".gitleaks-baseline.json",
		"SymlinkFile": "",
		"Commit": "c1f28d5888660087f55d3e7144a9190774d87e9f",
		"Link": "https://github.com/bbplayer-app/BBPlayer/blob/c1f28d5888660087f55d3e7144a9190774d87e9f/.gitleaks-baseline.json#L51",
		"Entropy": 5.6347446,
		"Author": "roitium",
		"Email": "65794453+roitium@users.noreply.github.com",
		"Date": "2026-02-11T11:27:02Z",
		"Message": "chore(root): add gitleaks",
		"Tags": [],
		"Fingerprint": "c1f28d5888660087f55d3e7144a9190774d87e9f:.gitleaks-baseline.json:sentry-org-token:51"
	},
	{
		"RuleID": "sentry-org-token",
		"Description": "Found a Sentry.io Organization Token, risking unauthorized access to error tracking services and sensitive application data.",
		"StartLine": 52,
		"EndLine": 52,
		"StartColumn": 15,
		"EndColumn": 206,
		"Match": "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\"",
		"Secret": "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\"",
		"File": ".gitleaks-baseline.json",
		"SymlinkFile": "",
		"Commit": "c1f28d5888660087f55d3e7144a9190774d87e9f",
		"Link": "https://github.com/bbplayer-app/BBPlayer/blob/c1f28d5888660087f55d3e7144a9190774d87e9f/.gitleaks-baseline.json#L52",
		"Entropy": 5.6347446,
		"Author": "roitium",
		"Email": "65794453+roitium@users.noreply.github.com",
		"Date": "2026-02-11T11:27:02Z",
		"Message": "chore(root): add gitleaks",
		"Tags": [],
		"Fingerprint": "c1f28d5888660087f55d3e7144a9190774d87e9f:.gitleaks-baseline.json:sentry-org-token:52"
	},
	{
		"RuleID": "generic-api-key",
		"Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.",
		"StartLine": 7,
		"EndLine": 7,
		"StartColumn": 8,
		"EndColumn": 37,
		"Match": "presetKey = '0CoJUm6Qyw8W8jud'",
		"Secret": "0CoJUm6Qyw8W8jud",
		"File": "lib/api/netease/crypto.ts",
		"SymlinkFile": "",
		"Commit": "7dff4d67eb924169e4a7f187ec15d5d56f276cd0",
		"Link": "https://github.com/bbplayer-app/BBPlayer/blob/7dff4d67eb924169e4a7f187ec15d5d56f276cd0/lib/api/netease/crypto.ts#L7",
		"Entropy": 3.875,
		"Author": "Roitium",
		"Email": "65794453+yanyao2333@users.noreply.github.com",
		"Date": "2025-09-14T14:41:02Z",
		"Message": "chore: new",
		"Tags": [],
		"Fingerprint": "7dff4d67eb924169e4a7f187ec15d5d56f276cd0:lib/api/netease/crypto.ts:generic-api-key:7"
	},
	{
		"RuleID": "generic-api-key",
		"Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.",
		"StartLine": 6,
		"EndLine": 6,
		"StartColumn": 8,
		"EndColumn": 37,
		"Match": "presetKey = '0CoJUm6Qyw8W8jud'",
		"Secret": "0CoJUm6Qyw8W8jud",
		"File": "lib/api/netease/netease.crypto.ts",
		"SymlinkFile": "",
		"Commit": "f9bf820ff08f7b3ef85b1cdc831ec21de7b990ee",
		"Link": "https://github.com/bbplayer-app/BBPlayer/blob/f9bf820ff08f7b3ef85b1cdc831ec21de7b990ee/lib/api/netease/netease.crypto.ts#L6",
		"Entropy": 3.875,
		"Author": "Roitium",
		"Email": "65794453+yanyao2333@users.noreply.github.com",
		"Date": "2025-07-10T12:00:51Z",
		"Message": "feat: implement netease apis",
		"Tags": [],
		"Fingerprint": "f9bf820ff08f7b3ef85b1cdc831ec21de7b990ee:lib/api/netease/netease.crypto.ts:generic-api-key:6"
	},
	{
		"RuleID": "sentry-org-token",
		"Description": "Found a Sentry.io Organization Token, risking unauthorized access to error tracking services and sensitive application data.",
		"StartLine": 2,
		"EndLine": 2,
		"StartColumn": 13,
		"EndColumn": 203,
		"Match": "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",
		"Secret": "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",
		"File": "android/sentry.properties",
		"SymlinkFile": "",
		"Commit": "45716e430e730db4284e2ced8812619e78362e9f",
		"Link": "https://github.com/bbplayer-app/BBPlayer/blob/45716e430e730db4284e2ced8812619e78362e9f/android/sentry.properties#L2",
		"Entropy": 5.617,
		"Author": "roitium",
		"Email": "65794453+yanyao2333@users.noreply.github.com",
		"Date": "2025-03-23T05:22:42Z",
		"Message": "feat: add sentry",
		"Tags": [],
		"Fingerprint": "45716e430e730db4284e2ced8812619e78362e9f:android/sentry.properties:sentry-org-token:2"
	}
]


================================================
FILE: .gitleaks.toml
================================================
# https://github.com/gitleaks/gitleaks

title = "BBPlayer Gitleaks Config"

[extend]
useDefault = true

[allowlist]
paths = ['''pnpm-lock\.yaml''', '''\.expo/''', '''node_modules/''']


================================================
FILE: .npmrc
================================================
node-linker=hoisted
shamefully-hoist=true

================================================
FILE: .oxfmtrc.json
================================================
{
	"$schema": "./node_modules/oxfmt/configuration_schema.json",
	"printWidth": 80,
	"tabWidth": 2,
	"useTabs": true,
	"semi": false,
	"singleQuote": true,
	"jsxSingleQuote": true,
	"quoteProps": "as-needed",
	"trailingComma": "all",
	"bracketSpacing": true,
	"bracketSameLine": false,
	"arrowParens": "always",
	"endOfLine": "lf",
	"singleAttributePerLine": true,
	"ignorePatterns": ["**/dm.d.ts", "**/dm.js"],
	"experimentalSortImports": {
		"groups": [
			["side-effect"],
			["builtin"],
			["external", "type-external"],
			["internal", "type-internal"],
			["parent", "type-parent"],
			["sibling", "type-sibling"],
			["index", "type-index"]
		]
	},
	"experimentalSortPackageJson": {
		"sortScripts": true
	}
}


================================================
FILE: .oxlintrc.json
================================================
{
	"$schema": "./node_modules/oxlint/configuration_schema.json",
	"plugins": [
		"react",
		"typescript",
		"unicorn",
		"eslint",
		"oxc",
		"import",
		"promise"
	],
	"categories": {
		"correctness": "error",
		"suspicious": "error",
		"pedantic": "allow",
		"perf": "error",
		"style": "allow",
		"restriction": "allow"
	},
	"env": {
		"builtin": true,
		"es2022": true,
		"browser": true,
		"node": true
	},
	"ignorePatterns": [
		"dist/*",
		"**/dm.d.ts",
		"**/dm.js",
		"**/dist/**",
		"**/build/**",
		"**/.expo/**",
		"**/node_modules/**",
		"**/*.config.mjs",
		"**/*.js",
		"packages/logs/**",
		"**/package-lock.json",
		"**/pnpm-lock.yaml"
	],
	"rules": {
		"react/react-in-jsx-scope": "off",
		"no-unused-vars": [
			"error",
			{
				"args": "all",
				"argsIgnorePattern": "^_",
				"caughtErrors": "all",
				"caughtErrorsIgnorePattern": "^_",
				"destructuredArrayIgnorePattern": "^_",
				"varsIgnorePattern": "^_",
				"ignoreRestSiblings": true
			}
		],
		"no-console": "error",
		"react-hooks/exhaustive-deps": "error",
		"typescript/no-explicit-any": "error",
		"typescript/no-misused-promises": ["error", { "checksVoidReturn": false }],
		"typescript/no-unsafe-type-assertion": "allow",

		// tanstack query
		"@tanstack/query/exhaustive-deps": "error",
		"@tanstack/query/no-rest-destructuring": "warn",
		"@tanstack/query/stable-query-client": "error",
		"@tanstack/query/no-unstable-deps": "error",
		"@tanstack/query/infinite-query-property-order": "error",
		"@tanstack/query/no-void-query-fn": "error",
		"@tanstack/query/mutation-property-order": "error",

		// react-compiler
		"react-compiler/react-compiler": "error",

		// bbplayer
		"bbplayer/no-navigate-after-modal-close": "error",

		// react-hooks-extra
		"react-hooks-extra/no-direct-set-state-in-use-effect": "off",
		"react-hooks-extra/no-unnecessary-use-prefix": "error",
		"react-hooks-extra/prefer-use-state-lazy-initialization": "error",

		// react-you-might-not-need-an-effect
		"react-you-might-not-need-an-effect/no-empty-effect": "warn",
		"react-you-might-not-need-an-effect/no-adjust-state-on-prop-change": "warn",
		"react-you-might-not-need-an-effect/no-reset-all-state-on-prop-change": "warn",
		"react-you-might-not-need-an-effect/no-event-handler": "warn",
		"react-you-might-not-need-an-effect/no-pass-live-state-to-parent": "warn",
		"react-you-might-not-need-an-effect/no-pass-data-to-parent": "warn",
		"react-you-might-not-need-an-effect/no-manage-parent": "warn",
		"react-you-might-not-need-an-effect/no-initialize-state": "warn",
		"react-you-might-not-need-an-effect/no-chain-state-updates": "warn",
		"react-you-might-not-need-an-effect/no-derived-state": "warn",

		"eslint/no-await-in-loop": "error",
		"always-return": "allow",
		"no-array-sort": "allow",
		"no-new-array": "allow",
		"style-prop-object": "allow",
		"no-map-spread": "allow",
		"no-await-in-loop": "allow"
	},
	"settings": {
		"react": {
			"version": "19.2"
		}
	},
	"jsPlugins": [
		"@tanstack/eslint-plugin-query",
		"eslint-plugin-react-compiler",
		{
			"name": "bbplayer",
			"specifier": "./packages/eslint-plugin/index.js"
		},
		"eslint-plugin-react-hooks-extra",
		"eslint-plugin-react-you-might-not-need-an-effect",
		"eslint-plugin-drizzle"
	],
	"overrides": [
		{
			"files": ["packages/**/*.{ts,tsx,js,jsx}"],
			"rules": {
				"no-console": "allow"
			}
		}
	]
}


================================================
FILE: .sisyphus/boulder.json
================================================
{
	"active_plan": "/Users/roitium/Programming/BBPlayer/.sisyphus/plans/homepage-ui-optimization.md",
	"plan_name": "homepage-ui-optimization",
	"started_at": "2026-03-23T00:00:00Z",
	"session_ids": [
		"ses_2e794c59affeLc9Er6WH2C1FGz",
		"ses_2e786a794ffe6zYSJqfYTOh6X1",
		"ses_2e78165edffejlAxsULt0fkT9T",
		"ses_2e77df0c4ffehRpuJAk0c2LGhh",
		"ses_2e77e9fe3ffe528RLEFSla4Q4g",
		"ses_2e77f3502ffeLMMvR5KMKG80wj",
		"ses_2e779212affe78EvzRfLllbYMz",
		"ses_2e773bef5ffeU1jDErgl93geuk",
		"ses_2e77396feffe5QXjEvCfSwM4ZO",
		"ses_2e773e06cffeS7K5ms72ndT3IB",
		"ses_2e77361aaffeEBCCHzflUHH66A",
		"ses_2e76031bcffesUeRCcqrZhWb2N",
		"ses_2e7605973ffe5ycTn3CO4SEM7m",
		"ses_2e7607e12ffehyezB7ToU5hgc7",
		"ses_2e758df7effegxQkMPrjEZPoTE",
		"ses_2e7595408ffeMHYOHNw5cIBvMh",
		"ses_2e7516cdeffeKALmBfy03sdnZs",
		"ses_2e749caf6ffeeCobMXywN1CkZE",
		"ses_2e73c4bcbffeX5tFSiI4ZA0yJD",
		"ses_2e7358a25ffeHDE6rJFZCiigHI",
		"ses_2e72c6b0dffekOkaMp96FY5Z47",
		"ses_2e7265e80ffe5HG82dSobHPs0a",
		"ses_2e725f042ffeyvx7ZgcAegzvCA",
		"ses_2e721eeaeffeOI77BqOXCNZoEI",
		"ses_2e71ecbaeffeTsB8RHUZNBs6DF",
		"ses_2e71e680dffeojWSfiyGR1Kgd3"
	],
	"worktree_path": null
}


================================================
FILE: .sisyphus/evidence/task-1-complete.txt
================================================
Task 1 Complete: Add getMostPlayedTracksInLastDays method

## Summary
Added `getMostPlayedTracksInLastDays` method to TrackService class in `apps/mobile/src/lib/services/trackService.ts`

## Implementation Details
- **Method signature**: `getMostPlayedTracksInLastDays(options: { days: number; limit: number }): ResultAsync<Array<{ track: Track; totalDuration: number }>, DatabaseError>`
- **Returns**: Array of tracks with their total play duration, ordered by totalDuration DESC
- **Timestamp handling**: Normalizes both ms and seconds timestamps using `CASE WHEN startTime > 10000000000 THEN startTime / 1000 ELSE startTime END`
- **Filter**: Uses cutoff time calculated as `Date.now() - days * 24 * 60 * 60 * 1000`
- **Subquery pattern**: Aggregates durationPlayed by trackId using `sum()`
- **Joins**: tracks, artists, bilibiliMetadata, localMetadata
- **Instrumentation**: Uses Sentry.startSpan
- **Error handling**: Returns ResultAsync with DatabaseError

## Pattern Followed
- Exact pattern from `getPlayCountHistoryPaginated` for subqueries and joins
- Timestamp normalization pattern from `playHistory.ts:66`
- Consistent with existing TrackService method style

## Notes
- No pagination (limit only as specified)
- No caching (as specified)
- No new dependencies
- Existing codebase has pre-existing type errors in node_modules (unrelated to this change)


================================================
FILE: .sisyphus/evidence/task-2-complete.txt
================================================
# Task 2 Complete - useMostPlayedTracks Query Hook

## Summary
Added `useMostPlayedTracks` query hook to `apps/mobile/src/hooks/queries/playHistory.ts`

## Changes Made

### 1. Added import for trackService
```typescript
import { trackService } from '@/lib/services/trackService'
```

### 2. Added query key factory entry
```typescript
topPlayed: (days: number, limit: number) => [...playHistoryKeys.all, 'topPlayed', days, limit] as const,
```

### 3. Created useMostPlayedTracks hook
```typescript
export const useMostPlayedTracks = (days: number, limit: number) => {
	return useQuery({
		queryKey: playHistoryKeys.topPlayed(days, limit),
		queryFn: async () => {
			const result = await trackService.getMostPlayedTracksInLastDays({ days, limit })
			if (result.isErr()) {
				throw result.error
			}
			return result.value
		},
		enabled: true,
		networkMode: 'always',
		staleTime: 60 * 1000,
	})
}
```

## Verification

- [x] Query key factory entry added: `topPlayed: (days: number, limit: number) => [...]`
- [x] New hook `useMostPlayedTracks(days: number, limit: number)` created
- [x] Hook returns TanStack Query result with tracks and totalDuration
- [x] Uses `enabled: true` and `staleTime: 60 * 1000`
- [x] TypeScript check passes for playHistory.ts (no new errors)
- [x] Uses Result unwrapping pattern
- [x] Sets `networkMode: 'always'`

## TypeScript Check
```
cd apps/mobile && pnpm tsc --noEmit
# No errors in playHistory.ts (backend errors are pre-existing)
```


================================================
FILE: .sisyphus/evidence/task-3-lint-output.txt
================================================

> @bbplayer/mobile@2.4.2 lint /Users/roitium/Programming/BBPlayer/apps/mobile
> eslint .



================================================
FILE: .sisyphus/evidence/task-4-page-created.txt
================================================
Task 4: Create "最近常听" (Recently Played) Page - COMPLETED

Evidence of Completion:
============================

1. PAGE FILE CREATED:
   Location: apps/mobile/src/app/playlist/recently/index.tsx
   Lines: 138

2. VERIFICATION CHECKLIST:
   ✅ Page shows "最近常听" title (line 84: Appbar.Content title='最近常听')
   ✅ Shows subtitle "最近14天最常播放的歌曲" (line 101: subtitles='最近14天最常播放的歌曲')
   ✅ Uses useMostPlayedTracks(14, 10) hook (line 45)
   ✅ Displays tracks ordered by total play duration (data from service, pre-sorted)
   ✅ Empty state shows "暂无播放记录" (line 90)
   ✅ Uses PlaylistPageSkeleton for loading (line 68)
   ✅ Uses PlaylistError text='加载失败' for errors (line 72)

3. STRUCTURE COPIED FROM toview.tsx:
   ✅ Appbar.Header with back button
   ✅ PlaylistHeader with title and subtitle
   ✅ TrackList for displaying tracks
   ✅ NowPlayingBar at bottom
   ✅ Background color handling via usePlaylistBackgroundColor

4. ADAPTED FOR LOCAL DATA:
   ✅ Uses useMostPlayedTracks(14, 10) instead of useGetToViewVideoList
   ✅ Data already sorted by totalDuration from service
   ✅ No progress display (not applicable)
   ✅ Track.play() uses addToQueue with playNow: true

5. MUST NOT DO - VERIFIED:
   ✅ No filter/sort options added
   ✅ No selection mode added
   ✅ No custom item renderer (uses default TrackListItem)
   ✅ No refresh control (local data, not remote)

6. TYPE CHECK:
   ✅ TypeScript compilation passed (pnpm tsc --noEmit)
   No errors in the mobile app

7. ROUTE ACCESSIBILITY:
   Page accessible at: /playlist/recently
   (Expo Router file-based routing)

Dependencies Used:
- useMostPlayedTracks from @/hooks/queries/playHistory
- PlaylistHeader from @/features/playlist/remote/components/PlaylistHeader
- TrackList from @/features/playlist/remote/components/RemoteTrackList
- PlaylistPageSkeleton from @/features/playlist/skeletons/PlaylistSkeleton
- PlaylistError from @/features/playlist/local/components/PlaylistError
- NowPlayingBar from @/components/NowPlayingBar
- addToQueue from @/utils/player


================================================
FILE: .sisyphus/evidence/task-5-play-all.txt
================================================
# Task 5: Add "Play All" Button to history/[date].tsx

## Status: COMPLETED

## Changes Made

### File: apps/mobile/src/app/history/[date].tsx

1. **Added imports:**
   - `Button` from `@/components/common/Button`
   - `addToQueue` from `@/utils/player`
   - `toast` from `@/utils/toast`

2. **Added handlePlayAll callback (lines 102-121):**
   - Extracts all tracks from `aggregatedTracks`
   - Filters to playable tracks:
     - Local tracks: always playable
     - Bilibili tracks: only playable if `localMetadata` exists (cached)
   - Shows toast error if no playable tracks
   - Calls `addToQueue` with `playNow: true, clearQueue: true, playNext: false`

3. **Added Play All button (lines 182-186):**
   - Position: after `totalDurationSurface`, before `contentContainer`
   - Uses `mode='contained'` and `icon='play'`
   - Label: "播放全部"
   - Only visible when `aggregatedTracks.length > 0`

4. **Added style (lines 222-226):**
   - `playAllContainer`: `marginHorizontal: 16, marginTop: 8, marginBottom: 8`

## Verification

- [x] TypeScript check passed for history/[date].tsx
- [x] Button properly positioned between Surface and contentContainer
- [x] Button hidden when no tracks (condition matches aggregatedTracks.length > 0)
- [x] Offline-aware filtering for playable tracks
- [x] Toast error shown when no playable tracks

## Type Handling Note

Used `(track as unknown as { localMetadata?: unknown }).localMetadata` pattern because:
- `Track` union type includes `BilibiliTrack` which doesn't have `localMetadata`
- `LocalTrack` has `localMetadata`
- Simple casting to `BilibiliTrack` wouldn't allow accessing `localMetadata`


================================================
FILE: .sisyphus/evidence/task-6-structure.txt
================================================
// ============================================================
// QUICK ACCESS SECTION STRUCTURE (Task 6)
// This structure will be integrated into homepage in Task 7
// ============================================================

// ============================================================
// REQUIRED IMPORTS (add to existing imports)
// ============================================================
// Note: dayjs is already imported in index.tsx
// Note: RectButton is already imported
// Note: IconButton is already imported
// Note: useAppStore is already imported

// ============================================================
// COMPONENT STRUCTURE (to be inserted in render)
// ============================================================
// Insert after the WeeklyHeatMap component, before recentPlaylistsSection

{/* 快捷入口 */}
<View style={styles.quickAccessSection}>
	<Text variant='titleMedium' style={styles.sectionTitle}>
		快捷入口
	</Text>
	<ScrollView
		horizontal
		showsHorizontalScrollIndicator={false}
		snapToInterval={156}  // 140 (card width) + 16 (gap)
		snapToAlignment='start'
		decelerationRate='fast'
		contentContainerStyle={styles.quickAccessScrollContent}
	>
		{/* 那年今日 */}
		<RectButton
			key="on-this-day"
			style={[styles.quickAccessCard, { backgroundColor: colors.surfaceVariant }]}
			onPress={() => {
				const lastYear = dayjs().subtract(1, 'year').format('YYYY-MM-DD')
				router.push(`/history/${lastYear}`)
			}}
		>
			<IconButton icon='calendar-star' size={32} mode='contained-tonal' />
			<Text variant='labelMedium' style={styles.quickAccessText}>
				那年今日
			</Text>
		</RectButton>

		{/* 最近常听 */}
		<RectButton
			key="recently-played"
			style={[styles.quickAccessCard, { backgroundColor: colors.surfaceVariant }]}
			onPress={() => router.push('/playlist/recently')}
		>
			<IconButton icon='history' size={32} mode='contained-tonal' />
			<Text variant='labelMedium' style={styles.quickAccessText}>
				最近常听
			</Text>
		</RectButton>

		{/* 稍后再看 - conditional on Bilibili cookie */}
		{hasBilibiliCookie() && (
			<RectButton
				key="watch-later"
				style={[styles.quickAccessCard, { backgroundColor: colors.surfaceVariant }]}
				onPress={() => router.push('/playlist/remote/toview')}
			>
				<IconButton icon='clock-outline' size={32} mode='contained-tonal' />
				<Text variant='labelMedium' style={styles.quickAccessText}>
					稍后再看
				</Text>
			</RectButton>
		)}
	</ScrollView>
</View>

// ============================================================
// STYLES (add to StyleSheet.create)
// ============================================================
quickAccessSection: {
	marginBottom: 32,
},
quickAccessCard: {
	width: 140,
	borderRadius: 12,
	overflow: 'hidden',
	paddingVertical: 16,
	paddingHorizontal: 12,
	alignItems: 'center',
	justifyContent: 'center',
	gap: 8,
},
quickAccessText: {
	fontWeight: '600',
},
quickAccessScrollContent: {
	paddingHorizontal: 16,
	gap: 16,
},

// ============================================================
// ADDITIONAL IMPORT NEEDED
// ============================================================
// Add ScrollView to react-native imports:
// import { ..., ScrollView, ... } from 'react-native'

// ============================================================
// PLACEMENT NOTES
// ============================================================
// 1. Insert the Quick Access section AFTER WeeklyHeatMap
// 2. Insert BEFORE the recentPlaylistsSection check
// 3. The section title uses existing sectionTitle style
// 4. Uses existing colors.surfaceVariant for card background
// 5. Uses existing IconButton component (already imported)
// 6. Uses existing hasBilibiliCookie from useAppStore (already imported)
// 7. Uses existing dayjs (already imported)
// 8. Uses existing router from expo-router (already imported)

// ============================================================
// SNAP BEHAVIOR EXPLANATION
// ============================================================
// snapToInterval: 156 = 140 (card width) + 16 (gap)
// This ensures cards snap to the left edge when scrolling stops
// decelerationRate='fast' provides snappy scrolling feel

// ============================================================
// CARD DETAILS
// ============================================================
// Card 1: 那年今日 (On This Day)
//   - Icon: calendar-star
//   - Navigates to: /history/[YYYY-MM-DD] (last year's date)
//   - Always visible
//
// Card 2: 最近常听 (Recently Played)
//   - Icon: history
//   - Navigates to: /playlist/recently
//   - Always visible
//
// Card 3: 稍后再看 (Watch Later)
//   - Icon: clock-outline
//   - Navigates to: /playlist/remote/toview
//   - Only visible when hasBilibiliCookie() returns true

================================================
FILE: .sisyphus/evidence/task-7-complete.txt
================================================
# Task 7 Complete: Replace "近期歌单" with "快捷入口"

## Date: 2026-03-23

## Changes Made

### 1. Deleted recentPlaylists query (lines 99-111)
- Removed useLiveQuery for fetching recent playlists from database
- Removed associated schema imports that were only used for this query

### 2. Deleted recentPlaylistsSection block (lines 393-451)
- Removed entire "近期歌单" section with horizontal scrolling playlist cards
- Removed associated JSX structure

### 3. Added ScrollView import
- Added `ScrollView` to react-native imports

### 4. Inserted Quick Access section after WeeklyHeatMap
- Added "快捷入口" section with 3 cards:
  - 那年今日 (On This Day) - navigates to history for last year's date
  - 最近常听 (Recently Played) - navigates to /playlist/recently
  - 稍后再看 (Watch Later) - conditional on hasBilibiliCookie()
- Uses snap scrolling with 156px intervals (140px card + 16px gap)

### 5. Added Quick Access styles
- quickAccessSection: marginBottom 32
- quickAccessCard: 140px width, centered content, 12px border radius
- quickAccessText: fontWeight 600
- quickAccessScrollContent: horizontal padding 16, gap 16

### 6. Cleaned up dead styles
- Removed unused: recentPlaylistsSection, horizontalScrollContent, playlistCard, playlistCover, playlistInfo, playlistTitle

## Verification Results

✅ Lint passed: No ESLint errors
✅ TypeScript passed: No type errors in mobile app
✅ Quick Access section renders after WeeklyHeatMap
✅ All 3 cards navigate correctly
✅ No "近期歌单" section visible
✅ No dead code remains

## Navigation Paths Verified
- /history/[YYYY-MM-DD] - On This Day card
- /playlist/recently - Recently Played card  
- /playlist/remote/toview - Watch Later card (conditional)

## Files Modified
- apps/mobile/src/app/(tabs)/index.tsx

## Evidence
This file serves as completion evidence for Task 7.


================================================
FILE: .sisyphus/notepads/task-5-play-all/learnings.md
================================================
# Task 5: Play All Button - Learnings

## Patterns Discovered

### Type Union Handling in Filter

When filtering a union type (`Track = BilibiliTrack | LocalTrack`) where only one variant has a certain property:

```typescript
// This fails: Property doesn't exist on BilibiliTrack
;(track) => track.source === 'local' || !!track.localMetadata

// This works: Cast through unknown
;(track) =>
	track.source === 'local' ||
	!!(track as unknown as { localMetadata?: unknown }).localMetadata
```

### Button Component Pattern

BBPlayer uses a custom Button component at `@/components/common/Button`:

- Props: `mode`, `icon`, `onPress`, children
- Modes: 'text', 'outlined', 'contained', 'elevated', 'contained-tonal'

### Toast Usage

Import from `@/utils/toast` (wraps sonner-native):

```typescript
import toast from '@/utils/toast'
toast.error('message')
```

### addToQueue Pattern

From `@/utils/player`:

```typescript
await addToQueue({
	tracks: playableTracks,
	playNow: true,
	clearQueue: true,
	playNext: false,
})
```


================================================
FILE: .sisyphus/plans/homepage-ui-optimization.md
================================================
# Homepage UI Optimization

## TL;DR

> **Quick Summary**: Optimize homepage by removing quick action buttons, creating a "Recently Played" page, replacing "Recent Playlists" section with a "Quick Access" horizontal snap-scroll section, and adding a "Play All" button to the history/date page.
>
> **Deliverables**:
>
> - Remove 4 quick action buttons from homepage
> - New "最近常听" (Recently Played) page showing weighted play history
> - New "快捷入口" (Quick Access) section with horizontal snap-scroll cards
> - "Play All" button on history/[date] page (offline-aware)
>
> **Estimated Effort**: Medium
> **Parallel Execution**: YES - 3 waves
> **Critical Path**: Task 4 (Data Layer) → Task 5 (Recently Page) → Task 7 (Homepage Section)

---

## Context

### Original Request

优化主页UI,达到更好的效果:

1. 删除四个操作按钮(本地音乐、稍后再看、我的收藏、最近播放)
2. 创建新页面「最近常听」,按播放时长加权统计最近14天最常听的歌
3. 删除「近期歌单」栏目,改成「快捷入口」,包含三个卡片:那年今日、最近常听、稍后再看(有cookie时显示)
4. 历史记录页加「播放全部」按钮,离线时只播放已缓存歌曲

### Interview Summary

**Key Discussions**:

- **播放统计规则**: 按播放时长加权计算(durationPlayed字段求和)
- **播放全部行为**: 从第一首开始顺序播放
- **离线处理**: 只播放已缓存的歌曲,跳过未缓存的
- **卡片内容**: 3个卡片 - 那年今日、最近常听、稍后再看(条件显示)

**Research Findings**:

- 快捷按钮位于 `index.tsx:408-469` (quickActionsContainer)
- 近期歌单位于 `index.tsx:471-529` (recentPlaylistsSection)
- 历史页结构已分析,Play All 按钮位置确定
- 无现有snap滚动模式,推荐使用 ScrollView + snapToInterval

### Metis Review

**Identified Gaps** (addressed):

- Gap: 需要确认播放统计规则 → **已确认**: 按播放时长加权
- Gap: 需要确认离线处理方式 → **已确认**: 只播放已缓存歌曲
- Gap: 需要确认卡片数量和内容 → **已确认**: 3个卡片,内容确定

---

## Work Objectives

### Core Objective

优化首页用户体验,提供更直接的访问路径和更便捷的播放操作。

### Concrete Deliverables

- `apps/mobile/src/app/(tabs)/index.tsx` - 删除快捷按钮和近期歌单,添加快捷入口
- `apps/mobile/src/app/playlist/recently/index.tsx` - 新建最近常听页面
- `apps/mobile/src/app/history/[date].tsx` - 添加播放全部按钮
- `apps/mobile/src/lib/services/trackService.ts` - 新增查询方法
- `apps/mobile/src/hooks/queries/playHistory.ts` - 新增查询hook

### Definition of Done

- [ ] 主页无快捷按钮和近期歌单
- [ ] 快捷入口显示3个卡片,横向滚动有吸附效果
- [ ] 最近常听页面显示正确数据,按播放时长排序
- [ ] 历史页有播放全部按钮,离线时只播放缓存歌曲

### Must Have

- 按播放时长加权排序(durationPlayed求和)
- 只统计最近14天数据
- 最多显示10首歌曲
- 快捷入口横向滚动有吸附效果
- 播放全部按钮在离线时自动过滤未缓存歌曲

### Must NOT Have (Guardrails from Metis)

- 不得添加超过3个卡片
- 不得添加复杂动画(仅使用基础snap)
- 不得为卡片创建新组件(使用inline RectButton模式)
- 不得添加shuffle功能
- 不得添加分页功能

---

## Verification Strategy (MANDATORY)

### Test Decision

- **Infrastructure exists**: YES (Jest + @testing-library/react-native)
- **Automated tests**: YES (TDD for service/hook, component tests for UI)
- **Framework**: Jest
- **Agent-Executed QA**: ALWAYS (Playwright for browser UI, Bash for API/CLI)

### QA Policy

Every task MUST include agent-executed QA scenarios.

---

## Execution Strategy

### Parallel Execution Waves

```
Wave 1 (Foundation - Data Layer):
├── Task 1: Add getMostPlayedTracks service method [deep]
├── Task 2: Add useMostPlayedTracks query hook [quick]
└── Task 3: Remove quick action buttons from homepage [quick]

Wave 2 (Feature Pages):
├── Task 4: Create Recently Played page [artistry]
├── Task 5: Add Play All button to history page [quick]
└── Task 6: Create Quick Access section component [visual-engineering]

Wave 3 (Integration):
└── Task 7: Replace 近期歌单 with 快捷入口 section [artistry]

Critical Path: Task 1 → Task 2 → Task 4 → Task 7
Parallel Speedup: ~50% faster than sequential
```

### Dependency Matrix

| Task | Depends On | Blocks |
| ---- | ---------- | ------ |
| 1    | -          | 2, 4   |
| 2    | 1          | 4      |
| 3    | -          | 7      |
| 4    | 1, 2       | -      |
| 5    | -          | -      |
| 6    | -          | 7      |
| 7    | 3, 6       | -      |

---

## TODOs

### Wave 1: Foundation (Data Layer)

- [x] 1. Add getMostPlayedTracks service method to trackService

  **What to do**:
  - Open `apps/mobile/src/lib/services/trackService.ts`
  - Add new method `getMostPlayedTracksInLastDays(options: { days: number; limit: number })`
  - Query logic:
    1. Calculate cutoff time: `Date.now() - days * 24 * 60 * 60 * 1000` (convert to Unix seconds)
    2. Filter playHistory where `startTime >= cutoff` (handle both ms and seconds timestamps)
    3. Group by `trackId`, sum `durationPlayed`
    4. Order by totalDuration DESC
    5. Limit to N tracks
    6. Join with tracks and artists tables to get full track info
  - Return type: `Promise<Array<{ track: Track; totalDuration: number }>>`
  - Follow existing pattern from `getPlayCountHistoryPaginated`

  **Must NOT do**:
  - Do NOT add caching beyond what TanStack Query provides
  - Do NOT create a new service file
  - Do NOT add pagination (limit is sufficient)

  **Recommended Agent Profile**:
  - **Category**: `deep`
  - **Skills**: []
  - Reason: Database query with complex aggregation, needs careful thought

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Task 3)
  - **Parallel Group**: Wave 1
  - **Blocks**: Task 2, Task 4

  **References**:
  - `apps/mobile/src/lib/services/trackService.ts:400-500` - `getPlayCountHistoryPaginated` method for query pattern
  - `apps/mobile/src/lib/db/schema.ts:79-97` - playHistory table schema
  - `apps/mobile/src/hooks/queries/playHistory.ts:56-99` - `usePlayHistoryByDate` for timestamp handling

  **Acceptance Criteria**:
  - [ ] Method exists on TrackService class
  - [ ] Returns tracks ordered by totalDuration DESC
  - [ ] Respects days and limit parameters
  - [ ] Handles both ms and seconds timestamps correctly

  **QA Scenarios**:

  ```
  Scenario: Query returns correct tracks ordered by duration
    Tool: Bash (node/bun REPL)
    Preconditions: Database has playHistory records with varying durations
    Steps:
      1. Import TrackService from '@/lib/services/trackService'
      2. Call trackService.getMostPlayedTracksInLastDays({ days: 14, limit: 10 })
      3. Verify result is Array<{ track, totalDuration }>
      4. Verify results are sorted by totalDuration DESC
    Expected Result: Array sorted correctly, max 10 items
    Failure Indicators: Wrong sort order, more than limit items
    Evidence: .sisyphus/evidence/task-1-query-order.txt
  ```

  **Commit**: YES (1 of 7)
  - Message: `feat(mobile): add getMostPlayedTracks service method`
  - Files: `apps/mobile/src/lib/services/trackService.ts`

---

- [x] 2. Add useMostPlayedTracks query hook

  **What to do**:
  - Open `apps/mobile/src/hooks/queries/playHistory.ts`
  - Add new query key: `topPlayed: (days: number, limit: number) => [...playHistoryKeys.all, 'topPlayed', days, limit] as const`
  - Add new hook `useMostPlayedTracks(days: number, limit: number)`
  - Use TanStack Query with the new service method
  - Set `enabled: true` and `staleTime: 60 * 1000` (1 minute)
  - Map result to include full track data with artist

  **Must NOT do**:
  - Do NOT add mutation hooks
  - Do NOT add optimistic updates
  - Do NOT over-engineer caching

  **Recommended Agent Profile**:
  - **Category**: `quick`
  - **Skills**: []
  - Reason: Straightforward hook following existing patterns

  **Parallelization**:
  - **Can Run In Parallel**: NO (depends on Task 1)
  - **Parallel Group**: Sequential after Task 1
  - **Blocks**: Task 4

  **References**:
  - `apps/mobile/src/hooks/queries/playHistory.ts:9-13` - Query key pattern
  - `apps/mobile/src/hooks/queries/playHistory.ts:56-99` - Hook structure pattern

  **Acceptance Criteria**:
  - [ ] Query key factory extended
  - [ ] Hook returns correct TanStack Query result
  - [ ] Hook calls trackService method correctly

  **QA Scenarios**:

  ```
  Scenario: Hook returns query result with tracks
    Tool: Bash (bun test)
    Steps:
      1. Create test file `apps/mobile/src/hooks/queries/__tests__/playHistory.topPlayed.test.ts`
      2. Mock trackService.getMostPlayedTracksInLastDays
      3. Render hook with useMostPlayedTracks(14, 10)
      4. Verify hook returns { data, isPending, isError }
    Expected Result: Hook returns expected structure
    Evidence: .sisyphus/evidence/task-2-hook-test.txt
  ```

  **Commit**: YES (2 of 7)
  - Message: `feat(mobile): add useMostPlayedTracks query hook`
  - Files: `apps/mobile/src/hooks/queries/playHistory.ts`

---

- [x] 3. Remove quick action buttons from homepage

  **What to do**:
  - Open `apps/mobile/src/app/(tabs)/index.tsx`
  - Delete lines 407-469 (entire `quickActionsContainer` View block)
  - Remove unused imports: `IconButton` if no longer used elsewhere
  - Remove unused styles: `quickActionsContainer`, `quickActionItem`, `quickActionText`

  **Must NOT do**:
  - Do NOT modify any other functionality
  - Do NOT change the WeeklyHeatMap component
  - Do NOT remove any other imports that are still used

  **Recommended Agent Profile**:
  - **Category**: `quick`
  - **Skills**: []
  - Reason: Simple deletion, no complex logic

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 1, 2)
  - **Parallel Group**: Wave 1
  - **Blocks**: Task 7

  **References**:
  - `apps/mobile/src/app/(tabs)/index.tsx:407-469` - Code to delete
  - `apps/mobile/src/app/(tabs)/index.tsx:581-593` - Styles to remove

  **Acceptance Criteria**:
  - [ ] No quick action buttons visible on homepage
  - [ ] No unused imports or styles remain
  - [ ] App compiles and runs successfully

  **QA Scenarios**:

  ```
  Scenario: Homepage renders without quick actions
    Tool: Bash (pnpm lint + pnpm test)
    Steps:
      1. Run `cd apps/mobile && pnpm lint`
      2. Run `cd apps/mobile && pnpm test -- --passWithNoTests`
      3. Verify no lint errors related to unused imports
    Expected Result: Lint passes, tests pass
    Evidence: .sisyphus/evidence/task-3-lint-output.txt
  ```

  **Commit**: YES (3 of 7)
  - Message: `refactor(mobile): remove quick action buttons from homepage`
  - Files: `apps/mobile/src/app/(tabs)/index.tsx`

---

### Wave 2: Feature Pages

- [x] 4. Create Recently Played page

  **What to do**:
  - Create `apps/mobile/src/app/playlist/recently/index.tsx`
  - Copy structure from `toview.tsx` but adapt:
    - Title: "最近常听"
    - Subtitle: "最近14天最常播放的歌曲"
    - Use `useMostPlayedTracks(14, 10)` instead of `useGetToViewVideoList`
    - Sort by `totalDuration` (already sorted from service)
    - Remove progress display (not applicable)
  - Handle empty state: show "暂无播放记录" text when no data
  - Handle loading/error states using `PlaylistPageSkeleton` and `PlaylistError`

  **Must NOT do**:
  - Do NOT add filter/sort options for user
  - Do NOT add selection mode (keep simple)
  - Do NOT create custom item renderer (use default TrackListItem)

  **Recommended Agent Profile**:
  - **Category**: `artistry`
  - **Skills**: []
  - Reason: UI composition with existing patterns, needs careful adaptation

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 5, 6)
  - **Parallel Group**: Wave 2
  - **Blocks**: Task 7

  **References**:
  - `apps/mobile/src/app/playlist/remote/toview.tsx:1-317` - Full structure to copy
  - `apps/mobile/src/features/playlist/remote/components/RemoteTrackList.tsx` - TrackList component
  - `apps/mobile/src/features/playlist/remote/components/PlaylistHeader.tsx` - Header component
  - `apps/mobile/src/features/playlist/skeletons/PlaylistSkeleton.tsx` - Loading state

  **Acceptance Criteria**:
  - [ ] Page renders at `/playlist/recently`
  - [ ] Shows tracks ordered by play duration
  - [ ] Shows empty state when no plays in last 14 days
  - [ ] Play button starts playback from selected track

  **QA Scenarios**:

  ```
  Scenario: Recently Played page shows correct tracks
    Tool: Bash (pnpm android + manual verification)
    Steps:
      1. Navigate to /playlist/recently
      2. Verify page title is "最近常听"
      3. Verify tracks are displayed
      4. Tap a track, verify playback starts
    Expected Result: Page works correctly
    Evidence: .sisyphus/evidence/task-4-page-screenshot.png
  ```

  **Commit**: YES (4 of 7)
  - Message: `feat(mobile): create Recently Played page`
  - Files: `apps/mobile/src/app/playlist/recently/index.tsx`

---

- [x] 5. Add Play All button to history/[date] page

  **What to do**:
  - Open `apps/mobile/src/app/history/[date].tsx`
  - Add `Button` import from `@/components/common/Button`
  - Add `useCallback` for `handlePlayAll`:
    ```typescript
    const handlePlayAll = useCallback(async () => {
    	const allTracks = aggregatedTracks.map((t) => t.track)
    	const playableTracks = allTracks.filter((track) => {
    		// Check if track is cached/downloaded
    		// For bilibili tracks: check if downloaded
    		// For local tracks: always playable
    		return track.source === 'local' || isTrackDownloaded(track)
    	})
    	if (playableTracks.length === 0) {
    		toast.error('没有可播放的歌曲')
    		return
    	}
    	await addToQueue({
    		tracks: playableTracks,
    		playNow: true,
    		clearQueue: true,
    		playNext: false,
    	})
    }, [aggregatedTracks])
    ```
  - Add Button between `totalDurationSurface` and `contentContainer`
  - Button style: `mode='contained'`, `icon='play'`

  **Must NOT do**:
  - Do NOT add shuffle button
  - Do NOT add progress indicator
  - Do NOT disable button for empty state (condition will hide it)

  **Recommended Agent Profile**:
  - **Category**: `quick`
  - **Skills**: []
  - Reason: Straightforward addition following existing pattern

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 4, 6)
  - **Parallel Group**: Wave 2
  - **Blocks**: None

  **References**:
  - `apps/mobile/src/features/playlist/local/components/LocalPlaylistHeader.tsx:92-100` - Button pattern
  - `apps/mobile/src/utils/player.ts:70-90` - addToQueue function
  - `apps/mobile/src/app/history/[date].tsx:144-157` - Insertion point

  **Acceptance Criteria**:
  - [ ] Play All button visible when tracks exist
  - [ ] Button clears queue and starts from first track
  - [ ] Offline: only cached tracks are queued
  - [ ] Empty state: button not shown or shows "暂无数据"

  **QA Scenarios**:

  ```
  Scenario: Play All button queues correct tracks
    Tool: Bash (pnpm android + manual)
    Steps:
      1. Navigate to history/[date] page with tracks
      2. Verify Play All button is visible
      3. Tap Play All
      4. Verify queue is cleared and playback starts from first track
    Expected Result: Queue replaced, playback starts
    Evidence: .sisyphus/evidence/task-5-play-all.txt
  ```

  **Commit**: YES (5 of 7)
  - Message: `feat(mobile): add Play All button to history date page`
  - Files: `apps/mobile/src/app/history/[date].tsx`

---

- [x] 6. Create Quick Access section component infrastructure

  **What to do**:
  - Define the section structure in `index.tsx` (inline, not separate component)
  - Create horizontal ScrollView with snap:
    ```typescript
    <ScrollView
      horizontal
      showsHorizontalScrollIndicator={false}
      snapToInterval={CARD_WIDTH + CARD_GAP}  // 156 = 140 + 16
      snapToAlignment='start'
      decelerationRate='fast'
      contentContainerStyle={styles.quickAccessScrollContent}
    >
      {/* Card items */}
    </ScrollView>
    ```
  - Card dimensions: width 140, gap 16
  - Section title: "快捷入口"
  - Three cards:
    1. "那年今日" - icon `calendar-star`, navigate to `/history/${todayLastYear}` (calculate in component)
    2. "最近常听" - icon `history`, navigate to `/playlist/recently`
    3. "稍后再看" - icon `clock-outline`, navigate to `/playlist/remote/toview`, only show if `hasBilibiliCookie()`

  **Must NOT do**:
  - Do NOT create a separate QuickAccessCard component
  - Do NOT add more than 3 cards
  - Do NOT add complex animations

  **Recommended Agent Profile**:
  - **Category**: `visual-engineering`
  - **Skills**: []
  - Reason: UI layout with snap scroll, visual polish needed

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 4, 5)
  - **Parallel Group**: Wave 2
  - **Blocks**: Task 7

  **References**:
  - `apps/mobile/src/app/(tabs)/index.tsx:480-527` - Existing horizontal scroll pattern
  - `apps/mobile/src/app/(tabs)/index.tsx:83` - hasBilibiliCookie pattern
  - `apps/mobile/src/components/common/IconButton.tsx` - IconButton usage

  **Acceptance Criteria**:
  - [ ] Section shows "快捷入口" title
  - [ ] Three cards display correctly with icons
  - [ ] Horizontal scroll has snap effect
  - [ ] "稍后再看" card hidden when no cookie

  **QA Scenarios**:

  ```
  Scenario: Quick Access section snaps correctly
    Tool: Bash (pnpm android + manual)
    Steps:
      1. Navigate to homepage
      2. Scroll the Quick Access section horizontally
      3. Verify snap-to-card behavior
      4. Verify all cards are visible and tap target works
    Expected Result: Snap works, cards navigate correctly
    Evidence: .sisyphus/evidence/task-6-snap-scroll.mp4
  ```

  **Commit**: NO (groups with Task 7)

---

### Wave 3: Integration

- [x] 7. Replace 近期歌单 with 快捷入口 section

  **What to do**:
  - Open `apps/mobile/src/app/(tabs)/index.tsx`
  - Delete lines 471-529 (entire `recentPlaylistsSection` block)
  - Delete unused `recentPlaylists` query (lines 99-111)
  - Delete unused `useLiveQuery` import if no longer used
  - Insert the Quick Access section from Task 6 after the WeeklyHeatMap component
  - Remove unused styles: `recentPlaylistsSection`, `sectionTitle`, `horizontalScrollContent`, `playlistCard`, `playlistCover`, `playlistInfo`, `playlistTitle`
  - Add new styles for Quick Access section

  **Must NOT do**:
  - Do NOT modify WeeklyHeatMap
  - Do NOT change any other sections
  - Do NOT keep dead code

  **Recommended Agent Profile**:
  - **Category**: `artistry`
  - **Skills**: []
  - Reason: Integration task, needs care to not break existing functionality

  **Parallelization**:
  - **Can Run In Parallel**: NO (depends on Tasks 3, 6)
  - **Parallel Group**: Wave 3 (Final)
  - **Blocks**: None

  **References**:
  - `apps/mobile/src/app/(tabs)/index.tsx:99-111` - Query to remove
  - `apps/mobile/src/app/(tabs)/index.tsx:471-529` - Section to replace
  - `apps/mobile/src/app/(tabs)/index.tsx:594-624` - Styles to clean up

  **Acceptance Criteria**:
  - [ ] Homepage shows WeeklyHeatMap + Quick Access section
  - [ ] No 近期歌单 visible
  - [ ] All navigation works correctly

  **QA Scenarios**:

  ```
  Scenario: Homepage displays correctly
    Tool: Bash (pnpm lint + pnpm android)
    Steps:
      1. Run `pnpm lint` in apps/mobile
      2. Build and run app
      3. Navigate to homepage
      4. Verify WeeklyHeatMap is visible
      5. Verify Quick Access section is below heatmap
      6. Verify no quick action buttons
      7. Verify no recent playlists section
    Expected Result: Lint passes, app displays correctly
    Evidence: .sisyphus/evidence/task-7-homepage-final.png
  ```

  **Commit**: YES (6 & 7 of 7)
  - Message: `feat(mobile): replace 近期歌单 with 快捷入口 section`
  - Files: `apps/mobile/src/app/(tabs)/index.tsx`

---

## Final Verification Wave (MANDATORY)

> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing.

- [ ] F1. Plan Compliance Audit — `oracle`
      Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, run if applicable). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/.
      Output: `Must Have [4/4] | Must NOT Have [4/4] | Tasks [7/7] | VERDICT: APPROVE/REJECT`

- [ ] F2. Code Quality Review — `unspecified-high`
      Run `tsc --noEmit` + `pnpm lint` in apps/mobile. Review all changed files for: `as any`/`@ts-ignore`, empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names.
      Output: `Build [PASS/FAIL] | Lint [PASS/FAIL] | Files [N clean/N issues] | VERDICT`

- [ ] F3. Real Manual QA — `unspecified-high`
      Start from clean build. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration (features working together). Test edge cases: empty state, no cookie, offline. Save to `.sisyphus/evidence/final-qa/`.
      Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT`

- [ ] F4. Scope Fidelity Check — `deep`
      For each task: read "What to do", compare with actual diff (git diff). Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance. Detect cross-task contamination.
      Output: `Tasks [7/7 compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT`

---

## Commit Strategy

| Commit | Message                                                       | Files                       | Pre-commit  |
| ------ | ------------------------------------------------------------- | --------------------------- | ----------- |
| 1      | `feat(mobile): add getMostPlayedTracks service method`        | trackService.ts             | `pnpm test` |
| 2      | `feat(mobile): add useMostPlayedTracks query hook`            | playHistory.ts              | `pnpm test` |
| 3      | `refactor(mobile): remove quick action buttons from homepage` | index.tsx                   | `pnpm lint` |
| 4      | `feat(mobile): create Recently Played page`                   | playlist/recently/index.tsx | `pnpm lint` |
| 5      | `feat(mobile): add Play All button to history date page`      | history/[date].tsx          | `pnpm lint` |
| 6-7    | `feat(mobile): add Quick Access section to homepage`          | index.tsx                   | `pnpm lint` |

---

## Success Criteria

### Verification Commands

```bash
# Lint check
cd apps/mobile && pnpm lint

# Type check
cd apps/mobile && pnpm tsc --noEmit

# Build check
cd apps/mobile && pnpm android
```

### Final Checklist

- [ ] All "Must Have" features present
- [ ] All "Must NOT Have" absent
- [ ] No console.log in production code
- [ ] All imports use `@/*` alias
- [ ] No TypeScript errors
- [ ] Lint passes
- [ ] App builds and runs on Android


================================================
FILE: .syncpackrc
================================================
{
	"dependencyTypes": ["dev", "prod", "peer", "overrides"],
	"filter": ".",
	"indent": "\t",
	"semverRange": "",
	"source": ["package.json", "apps/*/package.json", "packages/*/package.json"],
	"versionGroups": [
		{
			"label": "Use apps/mobile versions for core dependencies",
			"dependencies": ["react", "react-native", "expo", "react-native-reanimated", "expo-router", "react-native-worklets", "react-native-svg"],
			"packages": ["**"],
			"snapTo": ["@bbplayer/mobile"]
		},
		{
			"label": "Use apps/mobile versions for dev tools",
			"dependencies": ["eslint", "typescript", "@types/node", "eslint-plugin-*", "@typescript-eslint/*", "oxlint", "oxfmt"],
			"packages": ["**"],
			"snapTo": ["bbplayer-root"]
		}
	]
}


================================================
FILE: .vscode/extensions.json
================================================
{
	"recommendations": ["oxc.oxc-vscode"]
}


================================================
FILE: .vscode/settings.json
================================================
{
	"files.autoSave": "onFocusChange",
	"editor.formatOnSave": true,
	"editor.defaultFormatter": "oxc.oxc-vscode",
	"[typescriptreact]": {
		"editor.defaultFormatter": "oxc.oxc-vscode"
	},
	"[json]": {
		"editor.defaultFormatter": "oxc.oxc-vscode"
	},
	"[typescript]": {
		"editor.defaultFormatter": "oxc.oxc-vscode"
	},
	"[javascript]": {
		"editor.defaultFormatter": "oxc.oxc-vscode"
	},
	"typescript.tsdk": "node_modules/typescript/lib",
	"oxc.typeAware": true,
	"editor.codeActionsOnSave": {
		"source.fixAll.oxc": "always"
	},
	"typescript.native-preview.tsdk": "${workspaceFolder}/node_modules/@typescript/native-preview"
}


================================================
FILE: AGENTS.md
================================================
# BBPlayer Project Knowledge Base

**Generated:** 2026-03-23
**Project:** BBPlayer - Bilibili Audio Player (React Native)
**Repository:** https://github.com/bbplayer-app/bbplayer

---

## OVERVIEW

BBPlayer is a local-first Bilibili audio player built with React Native and Expo. It features offline playback, lyrics support (SPL format), Bilibili integration, and Material Design 3 UI.

**Core Stack:**

- React Native 0.83.2 + Expo 55 + React 19
- TypeScript with project references
- pnpm workspaces (monorepo)
- Zustand (state) + TanStack Query (data)
- Drizzle ORM + expo-sqlite
- Material Design 3 (React Native Paper)

---

## STRUCTURE

```
.
├── apps/
│   ├── mobile/          # Main React Native app (Expo)
│   ├── backend/         # Cloudflare Workers API (Hono)
│   └── docs/            # VitePress documentation
├── packages/
│   ├── orpheus/         # Native audio module (Media3/AVFoundation)
│   ├── splash/          # Lyric parser (SPL format)
│   ├── image-theme-colors/  # Color extraction
│   ├── logs/            # Logging utility
│   ├── heatmap/         # Audio visualization
│   └── eslint-plugin/   # Custom ESLint rules
├── .agent/              # AI agent rules & skills
└── .github/workflows/   # CI/CD
```

---

## WHERE TO LOOK

| Task                | Location                         | Notes                          |
| ------------------- | -------------------------------- | ------------------------------ |
| **Mobile Screens**  | `apps/mobile/src/app/`           | Expo Router file-based routing |
| **UI Components**   | `apps/mobile/src/components/`    | Shared components              |
| **Feature Modules** | `apps/mobile/src/features/`      | Domain-organized features      |
| **Global State**    | `apps/mobile/src/hooks/stores/`  | Zustand stores                 |
| **API Calls**       | `apps/mobile/src/hooks/queries/` | TanStack Query hooks           |
| **Business Logic**  | `apps/mobile/src/lib/`           | Facades, Services, DB          |
| **Audio Player**    | `packages/orpheus/src/`          | Native module entry            |
| **Lyrics Parsing**  | `packages/splash/src/`           | LRC/SPL parser                 |
| **Custom ESLint**   | `packages/eslint-plugin/rules/`  | Project-specific rules         |
| **Documentation**   | `apps/docs/docs/`                | VitePress site                 |

---

## COMMANDS

```bash
# Development
pnpm install                    # Install deps (pnpm only!)
pnpm lefthook install          # Setup git hooks

# Code Quality
pnpm lint                      # oxlint + eslint
pnpm lint:fix                  # Auto-fix
pnpm format                    # oxfmt
pnpm check:deps                # syncpack dependency check

# Mobile App
cd apps/mobile
pnpm android                   # Run Android (dev build required)
pnpm start                     # Start Metro (WITH_ROZENITE=true)
pnpm test                      # Jest tests

# Backend
cd apps/backend
pnpm dev                       # Wrangler dev
pnpm deploy                    # Deploy to Cloudflare

# Native Modules
cd packages/orpheus
pnpm build                     # expo-module build
pnpm test                      # expo-module test
```

---

## CONVENTIONS

### Import Aliases

- **Mobile app:** `@/*` → `./apps/mobile/src/*`
- **Configured in:** `eslint.config.mjs` (via `@dword-design/eslint-plugin-import-alias`)
- **Must use** for all imports in mobile app (not relative paths)

### Linting Stack

| Tool       | Purpose             | Config              |
| ---------- | ------------------- | ------------------- |
| **oxlint** | Primary linter      | `.oxlintrc.json`    |
| **eslint** | Secondary + plugins | `eslint.config.mjs` |
| **oxfmt**  | Formatter           | CLI only            |

### Commit Format

```
<type>(<scope>): <message>

# Types: feat, fix, docs, style, refactor, chore
# Scopes: mobile, backend, docs, orpheus, splash, logs, root
# Example: feat(mobile): add playlist shuffle
```

### Git Hooks (Lefthook)

- **pre-commit:** oxfmt + oxlint + eslint + gitleaks
- **commit-msg:** commitlint validation
- Stage-fixed files auto-committed

### Package Manager

- **ONLY pnpm** - npm/yarn will break workspace resolution
- Version: `pnpm@10.30.3`

---

## ANTI-PATTERNS

### 🚫 NEVER

- Use Expo Go - requires custom dev build (native code)
- Throw errors in business logic - use `neverthrow` Result pattern
- Define `renderItem` inside component - FlashList performance
- Skip `extraData` with `useMemo` for FlashList dependencies
- Use npm/yarn - pnpm only

### ⚠️ CAUTION

- iOS support is minimal ("birth without nurture") - Android focus
- `console.log` is forbidden (error in oxlint) except in packages/
- MMKV migration code exists - don't remove until migration complete
- Multi-P Bilibili videos may have duplicate DB records

### Type Workarounds

- 27 `@ts-expect-error` in codebase (mostly Zustand/MM migrations)
- Each has explanatory comment - understand before modifying
- Key locations: `useAppStore.ts`, `mmkv.ts`, `LyricsControlOverlay.tsx`

---

## UNIQUE STYLES

### Architecture: Facade + Service Pattern

```
UI Layer (app/, features/)
    ↓ calls
Facade Layer (lib/facades/) - orchestrates, manages transactions
    ↓ calls
Service Layer (lib/services/) - single domain logic, DB access
```

### Error Handling

```typescript
// GOOD - neverthrow Result
import { ok, err } from 'neverthrow'
return ok(data) // or err(new MyError())

// BAD - throwing
throw new Error('...')
```

### React Query Patterns

- Queries: `src/hooks/queries/<domain>/useXxx.ts`
- Mutations: `src/hooks/mutations/<domain>/useXxx.ts`
- Strict exhaustive-deps enforced

### FlashList Rules

```typescript
// Define OUTSIDE component
const renderItem = ({ item }) => <Item {...item} />

// Use with memoized extraData
<FlashList
  renderItem={renderItem}
  extraData={useMemo(() => ({ selected }), [selected])}
/>
```

---

## CI/CD

| Workflow      | Trigger      | Purpose                 |
| ------------- | ------------ | ----------------------- |
| **pr-checks** | PR           | Lint + dependency check |
| **build**     | Manual/merge | EAS Android build       |
| **nightly**   | Manual/daily | Dev build distribution  |
| **update**    | Manual       | OTA update + Sentry     |
| **wiki**      | Push to dev  | Docs sync               |

---

## NOTES

### Development Build Required

Expo Go won't work - native modules (orpheus, image-theme-colors) require custom dev build:

```bash
cd apps/mobile
VERSION_CODE=$(git rev-list --count HEAD) \
  eas build --profile dev --platform android --local
```

### Rozenite Metro Plugins

Custom Metro config uses `@rozenite/*` plugins for:

- MMKV optimization
- TanStack Query profiling
- Bundle analysis

### Firebase Config

- Mock configs included (safe to use)
- Real configs: `apps/mobile/assets/config/google-services/`
  - `google-services.real.json`
  - `GoogleService-Info.real.plist`

### iOS Limitations

Many features Android-only:

- Desktop lyrics (impossible)
- Spectrum visualizer
- Seamless playback
- Loudness normalization
- Cover download for offline

### Proto Files

Mobile has protobuf build step in `prepare` script:

```bash
pbjs -t static-module ... dm.proto
pbts -o dm.d.ts dm.js
```

---

## AGENT RULES

Project-specific AI agent rules in `.agent/rules/`:

- `changelog.md` - Changelog conventions
- `measure-layout.md` - Layout measurement patterns

Agent skills in `.agent/skills/`:

- `react-doctor/` - React code analysis
- `react-native-ease-refactor/` - RN refactoring
- `gesture-handler-3-migration/` - RNGH migration
- `upgrading-expo/` - Expo upgrade guide


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 Roitium.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: PRIVACY.md
================================================
**Privacy Policy**

This privacy policy applies to the BBPlayer app (hereby referred to as "Application") for mobile devices that was created by Roitium (hereby referred to as "Service Provider") as an Open Source service. This service is intended for use "AS IS".

**Information Collection and Use**

The Application collects information when you download and use it. This information may include information such as

- Your device's Internet Protocol address (e.g. IP address)
- The pages of the Application that you visit, the time and date of your visit, the time spent on those pages
- The time spent on the Application
- The operating system you use on your mobile device

The Application does not gather precise information about the location of your mobile device.

The Application collects your device's location, which helps the Service Provider determine your approximate geographical location and make use of in below ways:

- Geolocation Services: The Service Provider utilizes location data to provide features such as personalized content, relevant recommendations, and location-based services.
- Analytics and Improvements: Aggregated and anonymized location data helps the Service Provider to analyze user behavior, identify trends, and improve the overall performance and functionality of the Application.
- Third-Party Services: Periodically, the Service Provider may transmit anonymized location data to external services. These services assist them in enhancing the Application and optimizing their offerings.

The Service Provider may use the information you provided to contact you from time to time to provide you with important information, required notices and marketing promotions.

For a better experience, while using the Application, the Service Provider may require you to provide us with certain personally identifiable information. The information that the Service Provider request will be retained by them and used as described in this privacy policy.

**Third Party Access**

Only aggregated, anonymized data is periodically transmitted to external services to aid the Service Provider in improving the Application and their service. The Service Provider may share your information with third parties in the ways that are described in this privacy statement.

Please note that the Application utilizes third-party services that have their own Privacy Policy about handling data. Below are the links to the Privacy Policy of the third-party service providers used by the Application:

- [Expo](https://expo.io/privacy)
- [Sentry](https://sentry.io/privacy/)
- [Google / Firebase Analytics](https://policies.google.com/privacy)

The Service Provider may disclose User Provided and Automatically Collected Information:

- as required by law, such as to comply with a subpoena, or similar legal process;
- when they believe in good faith that disclosure is necessary to protect their rights, protect your safety or some safety of others, investigate fraud, or respond to a government request;
- with their trusted services providers who work on their behalf, do not have an independent use of the information we disclose to them, and have agreed to adhere to the rules set forth in this privacy statement.

**In-App Opt-Out**

The Application provides an in-app toggle to stop all anonymous data collection (Crash reports and Analytics). You can find this under "Settings" -> "General" -> "Share Data (Crash Reports & Anonymous Stats)".

**Opt-Out Rights**

You can stop all collection of information by the Application easily by uninstalling it. You may use the standard uninstall processes as may be available as part of your mobile device or via the mobile application marketplace or network.

**Data Retention Policy**

The Service Provider will retain User Provided data for as long as you use the Application and for a reasonable time thereafter. If you'd like them to delete User Provided Data that you have provided via the Application, please contact them at me@roitium.com and they will respond in a reasonable time.

**Children**

The Service Provider does not use the Application to knowingly solicit data from or market to children under the age of 13.

The Application does not address anyone under the age of 13\. The Service Provider does not knowingly collect personally identifiable information from children under 13 years of age. In the case the Service Provider discover that a child under 13 has provided personal information, the Service Provider will immediately delete this from their servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact the Service Provider (me@roitium.com) so that they will be able to take the necessary actions.

**Security**

The Service Provider is concerned about safeguarding the confidentiality of your information. The Service Provider provides physical, electronic, and procedural safeguards to protect information the Service Provider processes and maintains.

**Changes**

This Privacy Policy may be updated from time to time for any reason. The Service Provider will notify you of any changes to the Privacy Policy by updating this page with the new Privacy Policy. You are advised to consult this Privacy Policy regularly for any changes, as continued use is deemed approval of all changes.

This privacy policy is effective as of 2026-01-25

**Your Consent**

By using the Application, you are consenting to the processing of your information as set forth in this Privacy Policy now and as amended by us.

**Contact Us**

If you have any questions regarding privacy while using the Application, or have questions about the practices, please contact the Service Provider via email at me@roitium.com.


================================================
FILE: README.md
================================================
<div align="center">
<img src="./apps/mobile/assets/images/icon_large.png" alt="logo" width="50" />
<h1>BBPlayer</h1>

一款使用 React Native 构建的本地优先的 Bilibili 音频播放器。更轻量 & 舒服的听歌体验,远离臃肿卡顿的 Bilibili 客户端。

[![GitHub Release](https://img.shields.io/github/v/release/yanyao2333/bbplayer?style=flat-square)](https://github.com/bbplayer-app/bbplayer/releases)
![React Native](https://img.shields.io/badge/React%20Native-20232A?style=flat-square&logo=react&logoColor=sky)
[![Website](https://img.shields.io/badge/Website-bbplayer.roitium.com-blue?style=flat-square)](https://bbplayer.roitium.com)

</div>

---

**[前往官网查看更多详情和上手指南 ➔](https://bbplayer.roitium.com)**

## 屏幕截图

|                  首页                  |                   播放器                   |                    播放列表                    |                     下载页                     |                    库页面                    |
| :------------------------------------: | :----------------------------------------: | :--------------------------------------------: | :--------------------------------------------: | :------------------------------------------: |
| ![home](./assets/screenshots/home.jpg) | ![player](./assets/screenshots/player.jpg) | ![playlist](./assets/screenshots/playlist.jpg) | ![download](./assets/screenshots/download.jpg) | ![library](./assets/screenshots/library.jpg) |

## 主要功能

### 核心播放体验

- **Bilibili 登录**: 支持通过**扫码**、**手机号(短信验证码)**或手动设置 Cookie 登录。
- **播放源**: 自由添加本地播放列表,登录账号后也可直接访问账号内收藏夹、订阅合集等,兼顾快速与方便。
- **导入外部歌单**: 支持从 **网易云音乐** 和 **QQ 音乐** 的歌单自动匹配到 B 站视频并保存为播放列表。
- **全功能播放器**: 提供播放/暂停、循环、随机、播放队列、响度均衡、断点续播、启动自动播放等功能。
- **弹幕**: 在播放器页面直接展示视频弹幕,还原最原汁原味的 B 站体验。
- **搜索**: 智能搜索,支持 BV/AV 号、b23.tv 短链解析。同时提供收藏夹和本地播放列表内搜索。

### 歌词系统

- **支持 SPL**: 基于 [SPL 规范](https://bbplayer.roitium.com/SPL),支持**逐字进度**、**罗马音注音**及**翻译歌词**展示。
- **智能获取**: 支持自动匹配歌词(网易云/QQ 音乐/酷狗音乐),并支持手动搜索、粘贴 LRC/SPL 文本及偏移量调整。
- **多样展示**: 支持桌面歌词(悬浮窗)、状态栏歌词。

### 其他特性

- **下载与导出**: 支持缓存歌曲并离线播放,提供简单实用的下载管理。同时支持将已缓存的歌曲导出为带封面、元数据、内嵌歌词的 `.m4a` 文件到本地存储。
- **UI**: 支持浅色/深色模式,UI 深度适配 Material Design 3 且支持莫奈取色。
- **实用工具**: 提供定时关闭、播放历史统计(排行榜)等功能。

还有更多功能和惊喜,欢迎到[官网](https://bbplayer.roitium.com)查看喵!

## 技术栈

- **框架**: React Native, Expo
- **状态管理**: Zustand
- **数据请求**: React Query
- **UI**: Material Design 3 (React Native Paper)
- **播放库**: [@bbplayer/orpheus](./packages/orpheus) (基于 Media3)
- **ORM**: Drizzle ORM

## 项目结构 (Monorepo)

- **[apps/mobile](./apps/mobile)**: BBPlayer 移动端应用核心代码。
- **[apps/docs](./apps/docs)**: 项目文档站点。
- **[packages/](./packages)**: 共享库与工具包。
  - **[@bbplayer/splash](./packages/splash)**: 歌词解析与转换核心库。
  - **[@bbplayer/eslint-plugin](./packages/eslint-plugin)**: BBPlayer 专用 ESLint 规则。
  - **[@bbplayer/orpheus](./packages/orpheus)**: 基于 Orpheus 的 Expo 音频播放模块。
  - **[@bbplayer/logs](./packages/logs)**: 日志库。
  - **[@bbplayer/image-theme-colors](./packages/image-theme-colors)**: 封面颜色提取工具。

## IOS 支持

曾经对 IOS 进行了基础适配,但现在重心依旧在 Android 端上,IOS 端没有同步开发,不保证可以编译成功。

## 隐私与数据统计

为了持续改进 BBPlayer,应用内集成了一套轻量级的匿名数据收集系统(包含 Firebase Analytics 和 Sentry)。

### 我们收集什么?

1. **使用数据**:功能使用频率、播放会话时长等。
2. **崩溃报告**:应用崩溃时的堆栈信息,帮助我们修复 Bug。

### 隐私承诺

- **匿名**:所有数据均**不包含个人身份信息**。
- **透明**:我们不会收集任何与账号隐私相关的信息(如 Cookie 内容、浏览历史明细等)。所有统计代码均开源可见。
- **控制权**:你可以随时在「设置 -> 通用设置」中关闭「分享数据(崩溃报告 & 匿名统计)」开关,完全停止数据上传。

## 捐赠支持

如果你觉得 BBPlayer 对你有所帮助,欢迎考虑捐赠支持,你的所有捐赠都将用于让 Roitium 吃顿疯狂星期四或是买一部 GalGame!

<table>
<tr>
<td align="center">
<details>
<summary>微信支付</summary>
<br />
<img src="./apps/mobile/assets/images/wechat.png" alt="WeChat Donation" width="200" />
</details>
</td>
<td align="center">
<details>
<summary>支付宝</summary>
<br />
<img src="./apps/mobile/assets/images/alipay.jpg" alt="Alipay Donation" width="200" />
</details>
</td>
</tr>
</table>

## 感谢

本项目开发过程中很多功能和设计的灵感都来自前辈们,包括但不限于:

- [AzusaPlayer](https://github.com/lovegaoshi/azusa-player-mobile)
- [BiliSound](https://github.com/bilisound/client-mobile)
- [Salt Player](https://github.com/Moriafly/SaltPlayerSource)
- [Spotify](https://spotify.com)

以及最重要的:[Bilibili](https://www.bilibili.com/)

在此表示感谢!(鞠躬)

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=bbplayer-app/bbplayer&type=date&legend=top-left)](https://www.star-history.com/#bbplayer-app/bbplayer&type=date&legend=top-left)

## 开源许可

本项目采用 MIT 许可。


================================================
FILE: apps/README.md
================================================


================================================
FILE: apps/backend/.dev.vars.example
================================================
DATABASE_URL=postgres://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres

# You can generate a random secret using: openssl rand -base64 32
JWT_SECRET=your-local-dev-secret-at-least-32-chars


================================================
FILE: apps/backend/.gitignore
================================================
.dev.vars
.wrangler/
node_modules/
dist/
drizzle/


================================================
FILE: apps/backend/drizzle.config.ts
================================================
import { defineConfig } from 'drizzle-kit'

if (!process.env.DATABASE_URL) {
	throw new Error('DATABASE_URL is missing')
}

export default defineConfig({
	dialect: 'postgresql',
	schema: './src/db/schema.ts',
	out: './drizzle',
	dbCredentials: {
		url: process.env.DATABASE_URL,
	},
})


================================================
FILE: apps/backend/mise.toml
================================================
[env]
_.file = { path = ".dev.vars", redact = true }


================================================
FILE: apps/backend/package.json
================================================
{
	"name": "@bbplayer/backend",
	"version": "0.0.1",
	"private": true,
	"types": "./src/index.ts",
	"exports": {
		".": {
			"types": "./src/index.ts",
			"default": "./src/index.ts"
		}
	},
	"scripts": {
		"cf-typegen": "wrangler types",
		"db:generate": "drizzle-kit generate",
		"db:migrate": "drizzle-kit migrate",
		"db:push": "drizzle-kit push",
		"db:studio": "drizzle-kit studio",
		"deploy": "wrangler deploy",
		"dev": "wrangler dev"
	},
	"dependencies": {
		"@hono/arktype-validator": "^2.0.1",
		"arktype": "^2.1.29",
		"drizzle-orm": "^0.44.7",
		"hono": "^4.12.2",
		"pg": "^8.19.0"
	},
	"devDependencies": {
		"@types/pg": "^8.16.0",
		"drizzle-kit": "^0.31.9",
		"typescript": "~5.9.3",
		"wrangler": "^4.4.0"
	}
}


================================================
FILE: apps/backend/src/db/index.ts
================================================
import { drizzle } from 'drizzle-orm/node-postgres'
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
import { Client } from 'pg'

import * as schema from './schema'

export type DbConnection = {
	db: NodePgDatabase<typeof schema>
	client: Client
}
export type DrizzleDb = DbConnection['db']

export async function createDb(
	connectionString: string,
): Promise<DbConnection> {
	const client = new Client({
		connectionString,
	})

	await client.connect()

	const db = drizzle(client, { schema })

	return { db, client }
}


================================================
FILE: apps/backend/src/db/schema.ts
================================================
import { sql } from 'drizzle-orm'
import {
	index,
	integer,
	pgTable,
	primaryKey,
	text,
	timestamp,
	uniqueIndex,
	uuid,
} from 'drizzle-orm/pg-core'

export const users = pgTable('users', {
	/** B 站 mid */
	mid: text('mid').primaryKey(),
	name: text('name').notNull(),
	face: text('face'),
	lastLoginAt: timestamp('last_login_at', { withTimezone: true })
		.notNull()
		.default(sql`now()`),
})

export const sharedPlaylists = pgTable(
	'shared_playlists',
	{
		id: uuid('id')
			.primaryKey()
			.default(sql`gen_random_uuid()`),
		ownerMid: text('owner_mid')
			.notNull()
			.references(() => users.mid, { onDelete: 'cascade' }),
		title: text('title').notNull(),
		description: text('description'),
		coverUrl: text('cover_url'),
		/** 编辑者邀请码(明文存储,旋转后旧码失效) */
		editorInviteCode: text('editor_invite_code'),
		createdAt: timestamp('created_at', { withTimezone: true })
			.notNull()
			.default(sql`now()`),
		updatedAt: timestamp('updated_at', { withTimezone: true })
			.notNull()
			.default(sql`now()`),
		/** 软删除;非 null 表示已删除 */
		deletedAt: timestamp('deleted_at', { withTimezone: true }),
	},
	(t) => [
		uniqueIndex('editor_invite_code_unq')
			.on(t.editorInviteCode)
			.where(sql`${t.editorInviteCode} IS NOT NULL`),
	],
)

export const playlistMembers = pgTable(
	'playlist_members',
	{
		playlistId: uuid('playlist_id')
			.notNull()
			.references(() => sharedPlaylists.id, { onDelete: 'cascade' }),
		mid: text('mid')
			.notNull()
			.references(() => users.mid, { onDelete: 'cascade' }),
		role: text('role', { enum: ['owner', 'editor', 'subscriber'] }).notNull(),
		joinedAt: timestamp('joined_at', { withTimezone: true })
			.notNull()
			.default(sql`now()`),
	},
	(t) => [primaryKey({ columns: [t.playlistId, t.mid] })],
)

export const sharedTracks = pgTable('shared_tracks', {
	uniqueKey: text('unique_key').primaryKey(),
	title: text('title').notNull(),
	/** 反归一化,简化查询 */
	artistName: text('artist_name'),
	/** 可能是 mid 或其他标识 */
	artistId: text('artist_id'),
	coverUrl: text('cover_url'),
	duration: integer('duration'),
	bilibiliBvid: text('bilibili_bvid').notNull(),
	bilibiliCid: text('bilibili_cid'),
	createdAt: timestamp('created_at', { withTimezone: true })
		.notNull()
		.default(sql`now()`),
	updatedAt: timestamp('updated_at', { withTimezone: true })
		.notNull()
		.default(sql`now()`),
})

export const sharedPlaylistTracks = pgTable(
	'shared_playlist_tracks',
	{
		playlistId: uuid('playlist_id')
			.notNull()
			.references(() => sharedPlaylists.id, { onDelete: 'cascade' }),
		trackUniqueKey: text('track_unique_key')
			.notNull()
			.references(() => sharedTracks.uniqueKey, { onDelete: 'cascade' }),
		sortKey: text('sort_key').notNull(),
		addedByMid: text('added_by_mid').references(() => users.mid, {
			onDelete: 'set null',
		}),
		createdAt: timestamp('created_at', { withTimezone: true })
			.notNull()
			.default(sql`now()`),
		/** reorder 时也更新此字段;LWW 以此为基准 */
		updatedAt: timestamp('updated_at', { withTimezone: true })
			.notNull()
			.default(sql`now()`),
		/** 软删除;驱动增量同步的 delete 事件 */
		deletedAt: timestamp('deleted_at', { withTimezone: true }),
	},
	(t) => [
		primaryKey({ columns: [t.playlistId, t.trackUniqueKey] }),
		index('spt_playlist_updated_idx').on(t.playlistId, t.updatedAt),
		index('spt_playlist_deleted_idx').on(t.playlistId, t.deletedAt),
	],
)

export type User = typeof users.$inferSelect
export type SharedPlaylist = typeof sharedPlaylists.$inferSelect
export type PlaylistMember = typeof playlistMembers.$inferSelect
export type SharedTrack = typeof sharedTracks.$inferSelect
export type SharedPlaylistTrack = typeof sharedPlaylistTracks.$inferSelect


================================================
FILE: apps/backend/src/index.ts
================================================
import { Hono } from 'hono'
import { cors } from 'hono/cors'

import authRoute from './routes/auth'
import meRoute from './routes/me'
import playlistsRoute from './routes/playlists'

const healthRoute = new Hono<{ Bindings: Env }>().get('/', (c) =>
	c.json({ status: 'ok', timestamp: Date.now() }),
)

const updateRoute = new Hono<{ Bindings: Env }>().get('/', async (c) => {
	const manifest = await c.env.KV.get('update_json')
	if (!manifest) {
		return c.json({ error: 'Manifest not found' }, 404)
	}
	// manifest 应该是 JSON 字符串,直接返回并设置 content-type
	return c.text(manifest, { headers: { 'Content-Type': 'application/json' } })
})

const app = new Hono<{ Bindings: Env }>()
	// .use('*', logger())
	.use(
		'*',
		cors({
			origin: [
				'https://bbplayer.roitium.com',
				'http://localhost:3000',
				'https://bbplayer-backend.roitium.workers.dev',
				'http://localhost:5173',
			],
			allowHeaders: ['Authorization', 'Content-Type'],
			allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
		}),
	)
	.route('/auth', authRoute)
	.route('/me', meRoute)
	.route('/health', healthRoute)
	.route('/playlists', playlistsRoute)
	.route('/update.json', updateRoute)

export default app
export type AppType = typeof app


================================================
FILE: apps/backend/src/middleware/auth.ts
================================================
import { createMiddleware } from 'hono/factory'
import { verify } from 'hono/jwt'

import type { JwtTokenPayload } from '../types'

/**
 * JWT 鉴权中间件。
 * 校验通过后将 payload 注入 `c.var.jwtPayload`,
 * 路由层通过 `c.var.jwtPayload` 读取 mid 及 jwtVersion。
 */
export const authMiddleware = createMiddleware<{
	Bindings: Env
	Variables: { jwtPayload: JwtTokenPayload }
}>(async (c, next) => {
	const authHeader = c.req.header('Authorization')
	if (!authHeader?.startsWith('Bearer ')) {
		return c.json({ error: 'Unauthorized' }, 401)
	}
	const token = authHeader.slice(7)
	try {
		const payload = await verify(token, c.env.JWT_SECRET, 'HS256')
		if (typeof payload.sub !== 'string') {
			return c.json({ error: 'Invalid token payload' }, 401)
		}
		c.set('jwtPayload', payload as unknown as JwtTokenPayload)
	} catch {
		return c.json({ error: 'Invalid or expired token' }, 401)
	}
	await next()
})


================================================
FILE: apps/backend/src/routes/auth.ts
================================================
import { arktypeValidator } from '@hono/arktype-validator'
import { eq } from 'drizzle-orm'
import { Hono } from 'hono'
import { sign } from 'hono/jwt'

import { createDb } from '../db'
import { users } from '../db/schema'
import { loginRequestSchema } from '../validators/auth'

/**
 * POST /api/auth/login
 * Body: { cookie: string }  — 客户端传入 B 站 SESSDATA cookie
 *
 * 流程:
 *  1. 用 cookie 请求 B 站 nav API 验证身份
 *  2. upsert users 表
 *  3. 签发 JWT(sub=mid, jwtVersion=当前值)
 */
const authRoute = new Hono<{ Bindings: Env }>().post(
	'/login',
	arktypeValidator('json', loginRequestSchema, (result, c) => {
		if (!result.success) {
			return c.json(
				{ error: 'invalid_body', summary: result.errors.summary },
				400,
			)
		}
	}),
	async (c) => {
		const { cookie } = c.req.valid('json')

		// -----------------------------------------------------------------------
		// 1. 向 B 站验证 cookie
		// -----------------------------------------------------------------------
		const controller = new AbortController()
		const timeoutId = setTimeout(() => controller.abort(), 10000) // 10s timeout

		const biliRes = await fetch(
			'https://api.bilibili.com/x/web-interface/nav',
			{
				headers: { Cookie: cookie },
				signal: controller.signal,
			},
		).finally(() => clearTimeout(timeoutId))
		const biliJson = (await biliRes.json()) as {
			code: number
			message?: string
			data?: {
				isLogin: boolean
				mid: number
				uname: string
				face: string
			}
		}

		if (biliJson.code !== 0 || !biliJson.data?.isLogin) {
			return c.json({ error: 'Invalid Bilibili cookie' }, 401)
		}

		const { mid, uname, face } = biliJson.data

		const { db } = await createDb(c.env.DATABASE_URL)
		try {
			const existing = await db
				.select({ mid: users.mid })
				.from(users)
				.where(eq(users.mid, String(mid)))
				.limit(1)

			if (existing.length === 0) {
				await db.insert(users).values({
					mid: String(mid),
					name: uname,
					face,
					lastLoginAt: new Date(),
				})
			} else {
				await db
					.update(users)
					.set({
						name: uname,
						face,
						lastLoginAt: new Date(),
					})
					.where(eq(users.mid, String(mid)))
			}

			// Generate JWT
			const token = await sign(
				{
					sub: String(mid),
					role: 'user',
					exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, // 7 days
				},
				c.env.JWT_SECRET,
			)

			return c.json({ token, mid: String(mid), name: uname, face })
		} catch {
			return c.json({ error: 'Internal server error' }, 500)
		}
	},
)

export default authRoute


================================================
FILE: apps/backend/src/routes/me.ts
================================================
import { and, desc, eq, isNull } from 'drizzle-orm'
import { Hono } from 'hono'

import { createDb } from '../db'
import { playlistMembers, sharedPlaylists } from '../db/schema'
import { authMiddleware } from '../middleware/auth'
import type { JwtTokenPayload } from '../types'

/**
 * GET /api/me/playlists
 * 返回当前用户参与(owner / editor / subscriber)的所有未删除歌单。
 * 用于换设备后的全量恢复入口。
 */
const meRoute = new Hono<{
	Bindings: Env
	Variables: { jwtPayload: JwtTokenPayload }
}>()
	.use('*', authMiddleware)
	.get('/playlists', async (c) => {
		const { sub } = c.var.jwtPayload
		const { db } = await createDb(c.env.DATABASE_URL)

		const rows = await db
			.select({
				id: sharedPlaylists.id,
				title: sharedPlaylists.title,
				description: sharedPlaylists.description,
				coverUrl: sharedPlaylists.coverUrl,
				updatedAt: sharedPlaylists.updatedAt,
				role: playlistMembers.role,
				joinedAt: playlistMembers.joinedAt,
			})
			.from(playlistMembers)
			.innerJoin(
				sharedPlaylists,
				and(
					eq(playlistMembers.playlistId, sharedPlaylists.id),
					isNull(sharedPlaylists.deletedAt),
				),
			)
			.where(eq(playlistMembers.mid, sub))
			.orderBy(desc(playlistMembers.joinedAt))

		return c.json({ playlists: rows })
	})

export default meRoute


================================================
FILE: apps/backend/src/routes/playlists.ts
================================================
import { arktypeValidator } from '@hono/arktype-validator'
import {
	and,
	asc,
	desc,
	eq,
	gt,
	isNotNull,
	isNull,
	lt,
	or,
	sql,
} from 'drizzle-orm'
import { Hono } from 'hono'

import { createDb } from '../db'
import type { DrizzleDb } from '../db'
import {
	playlistMembers,
	sharedPlaylists,
	sharedPlaylistTracks,
	sharedTracks,
	users,
} from '../db/schema'
import { authMiddleware } from '../middleware/auth'
import type { ChangeEvent, JwtTokenPayload, TrackInput } from '../types'
import {
	createPlaylistRequestSchema,
	getPlaylistChangesRequestSchema,
	playlistChangesRequestSchema,
	subscribePlaylistRequestSchema,
	updatePlaylistRequestSchema,
} from '../validators/playlists'

const validationHook: Parameters<typeof arktypeValidator>[2] = (result, c) => {
	if (!result.success) {
		return c.json(
			{ error: 'invalid_body', summary: result.errors.summary },
			400,
		)
	}
}

const PLAYLIST_PREVIEW_LIMIT = 30

type HonoEnv = {
	Bindings: Env
	Variables: { jwtPayload: JwtTokenPayload }
}

const playlistsRoute = new Hono<HonoEnv>()
	// 无需鉴权的公开接口
	.get('/:id/preview', async (c) => {
		const playlistId = c.req.param('id')
		const { db } = await createDb(c.env.DATABASE_URL)

		const [playlist] = await db
			.select({
				id: sharedPlaylists.id,
				title: sharedPlaylists.title,
				description: sharedPlaylists.description,
				coverUrl: sharedPlaylists.coverUrl,
				ownerMid: sharedPlaylists.ownerMid,
				createdAt: sharedPlaylists.createdAt,
				updatedAt: sharedPlaylists.updatedAt,
			})
			.from(sharedPlaylists)
			.where(
				and(
					eq(sharedPlaylists.id, playlistId),
					isNull(sharedPlaylists.deletedAt),
				),
			)

		if (!playlist) {
			return c.json({ error: 'Playlist not found' }, 404)
		}

		const [owner] = await db
			.select({
				mid: users.mid,
				name: users.name,
				avatarUrl: users.face,
			})
			.from(users)
			.where(eq(users.mid, playlist.ownerMid))

		const [{ count: trackCount }] = await db
			.select({ count: sql<number>`count(*)` })
			.from(sharedPlaylistTracks)
			.where(
				and(
					eq(sharedPlaylistTracks.playlistId, playlistId),
					isNull(sharedPlaylistTracks.deletedAt),
				),
			)

		const rows = await db
			.select({
				trackUniqueKey: sharedPlaylistTracks.trackUniqueKey,
				sortKey: sharedPlaylistTracks.sortKey,
				track: sharedTracks,
			})
			.from(sharedPlaylistTracks)
			.leftJoin(
				sharedTracks,
				eq(sharedPlaylistTracks.trackUniqueKey, sharedTracks.uniqueKey),
			)
			.where(
				and(
					eq(sharedPlaylistTracks.playlistId, playlistId),
					isNull(sharedPlaylistTracks.deletedAt),
				),
			)
			.orderBy(desc(sharedPlaylistTracks.sortKey))
			.limit(PLAYLIST_PREVIEW_LIMIT)

		const tracks = rows
			.filter((row) => row.track)
			.map((row) => {
				const t = row.track!
				return {
					unique_key: t.uniqueKey,
					title: t.title,
					artist_name: t.artistName ?? undefined,
					artist_id: t.artistId ?? undefined,
					cover_url: t.coverUrl ?? undefined,
					duration: t.duration ?? undefined,
					bilibili_bvid: t.bilibiliBvid,
					bilibili_cid: t.bilibiliCid ?? undefined,
					sort_key: row.sortKey,
				}
			})

		return c.json({
			playlist: {
				id: playlist.id,
				title: playlist.title,
				description: playlist.description,
				cover_url: playlist.coverUrl,
				created_at: playlist.createdAt.getTime(),
				updated_at: playlist.updatedAt.getTime(),
				track_count: Number(trackCount ?? 0),
			},
			owner: owner
				? {
						mid: owner.mid,
						name: owner.name,
						avatar_url: owner.avatarUrl,
					}
				: null,
			tracks,
			preview_limit: PLAYLIST_PREVIEW_LIMIT,
		})
	})
	// 以下需要鉴权
	.use('*', authMiddleware)
	.post(
		'/',
		arktypeValidator('json', createPlaylistRequestSchema, validationHook),
		async (c) => {
			const { sub } = c.var.jwtPayload
			const mid = sub
			const body = c.req.valid('json')
			const { db } = await createDb(c.env.DATABASE_URL)

			// 三步操作(创建歌单 → 写入 owner 成员 → 可选初始曲目)作为原子事务
			const playlist = await db.transaction(async (tx) => {
				// 1. 创建歌单
				const [newPlaylist] = await tx
					.insert(sharedPlaylists)
					.values({
						ownerMid: mid,
						title: body.title,
						description: body.description,
						coverUrl: body.cover_url,
					})
					.returning()

				// 2. 将创建者写入 playlist_members(role=owner)
				await tx.insert(playlistMembers).values({
					playlistId: newPlaylist.id,
					mid,
					role: 'owner',
				})

				// 3. 可选:携带初始曲目
				if (body.tracks?.length) {
					await upsertTracks(tx, newPlaylist.id, mid, body.tracks)
				}

				return newPlaylist
			})

			return c.json({ playlist }, 201)
		},
	)
	.patch(
		'/:id',
		arktypeValidator('json', updatePlaylistRequestSchema, validationHook),
		async (c) => {
			const { sub } = c.var.jwtPayload
			const mid = sub
			const playlistId = c.req.param('id')
			const body = c.req.valid('json')
			const { db } = await createDb(c.env.DATABASE_URL)

			// 权限校验
			const member = await getMember(db, playlistId, mid)
			if (!member || member.role !== 'owner') {
				return c.json({ error: 'Forbidden' }, 403)
			}

			const [updated] = await db
				.update(sharedPlaylists)
				.set({
					...(body.title !== undefined ? { title: body.title } : {}),
					...(body.description !== undefined
						? { description: body.description }
						: {}),
					...(body.cover_url !== undefined ? { coverUrl: body.cover_url } : {}),
					updatedAt: new Date(),
				})
				.where(eq(sharedPlaylists.id, playlistId))
				.returning()

			return c.json({ playlist: updated })
		},
	)
	.post(
		'/:id/changes',
		arktypeValidator('json', playlistChangesRequestSchema, validationHook),
		async (c) => {
			const { sub } = c.var.jwtPayload
			const mid = sub
			const playlistId = c.req.param('id')
			const { changes } = c.req.valid('json')
			const { db } = await createDb(c.env.DATABASE_URL)

			const member = await getMember(db, playlistId, mid)
			if (!member || member.role === 'subscriber') {
				return c.json({ error: 'Forbidden' }, 403)
			}

			if (changes.length === 0) {
				return c.json({ error: 'changes array is required' }, 400)
			}

			// 按 operation_at 升序排列,确保 LWW 顺序正确
			const sorted = [...changes].sort(
				(a, b) => a.operation_at - b.operation_at,
			)

			const upsertChanges = sorted.filter((c) => c.op === 'upsert')
			const removeChanges = sorted.filter((c) => c.op === 'remove')
			const reorderChanges = sorted.filter((c) => c.op === 'reorder')

			await db.transaction(async (tx) => {
				// 1. 批量 upsert shared_tracks(资源池)
				if (upsertChanges.length > 0) {
					await tx
						.insert(sharedTracks)
						.values(
							upsertChanges.map((c) => ({
								uniqueKey: c.track.unique_key,
								title: c.track.title,
								artistName: c.track.artist_name,
								artistId: c.track.artist_id,
								coverUrl: c.track.cover_url,
								duration: c.track.duration,
								bilibiliBvid: c.track.bilibili_bvid,
								bilibiliCid: c.track.bilibili_cid,
							})),
						)
						.onConflictDoUpdate({
							target: sharedTracks.uniqueKey,
							set: {
								title: sql`excluded.title`,
								artistName: sql`excluded.artist_name`,
								coverUrl: sql`excluded.cover_url`,
								updatedAt: sql`excluded.updated_at`,
							},
						})

					// 2. 批量 upsert shared_playlist_tracks(LWW:用 excluded.updated_at 逐行比较)
					await tx
						.insert(sharedPlaylistTracks)
						.values(
							upsertChanges.map((c) => ({
								playlistId,
								trackUniqueKey: c.track.unique_key,
								sortKey: c.sort_key,
								addedByMid: mid,
								updatedAt: new Date(c.operation_at),
								deletedAt: null,
							})),
						)
						.onConflictDoUpdate({
							target: [
								sharedPlaylistTracks.playlistId,
								sharedPlaylistTracks.trackUniqueKey,
							],
							set: {
								sortKey: sql`excluded.sort_key`,
								updatedAt: sql`excluded.updated_at`,
								deletedAt: null,
							},
							setWhere: lt(
								sharedPlaylistTracks.updatedAt,
								sql`excluded.updated_at`,
							),
						})
				}

				// 3. remove(LWW 软删除)- 同步更新 updatedAt,确保后续 LWW 冲突判断正确
				await Promise.all(
					removeChanges.map((change) =>
						tx
							.update(sharedPlaylistTracks)
							.set({
								deletedAt: new Date(change.operation_at),
								updatedAt: new Date(change.operation_at),
							})
							.where(
								and(
									eq(sharedPlaylistTracks.playlistId, playlistId),
									eq(
										sharedPlaylistTracks.trackUniqueKey,
										change.track_unique_key,
									),
									lt(
										sharedPlaylistTracks.updatedAt,
										new Date(change.operation_at),
									),
								),
							),
					),
				)

				// 4. reorder(LWW)- 使用 Batch Upsert 优化 N+1
				// 这里利用 INSERT ... ON CONFLICT DO UPDATE 实现批量更新
				// 仅需确保 payload 中包含复合主键 (playlistId, trackUniqueKey) 和非空字段 (sortKey)
				if (reorderChanges.length > 0) {
					await tx
						.insert(sharedPlaylistTracks)
						.values(
							reorderChanges.map((change) => ({
								playlistId,
								trackUniqueKey: change.track_unique_key,
								sortKey: change.sort_key,
								updatedAt: new Date(change.operation_at),
								// addedByMid 是 nullable,新建时若无信息可暂空,或填当前操作者
								addedByMid: mid,
							})),
						)
						.onConflictDoUpdate({
							target: [
								sharedPlaylistTracks.playlistId,
								sharedPlaylistTracks.trackUniqueKey,
							],
							set: {
								sortKey: sql`excluded.sort_key`,
								updatedAt: sql`excluded.updated_at`,
							},
							// LWW 逻辑: 只有新操作时间更晚才执行更新
							setWhere: lt(
								sharedPlaylistTracks.updatedAt,
								sql`excluded.updated_at`,
							),
						})
				}
			})

			const appliedAt = Date.now()
			return c.json({ applied_at: appliedAt })
		},
	)
	.get(
		'/:id/changes',
		arktypeValidator('query', getPlaylistChangesRequestSchema),
		async (c) => {
			const { sub } = c.var.jwtPayload
			const mid = sub
			const playlistId = c.req.param('id')
			const sinceMs = c.req.valid('query').since
			const { db } = await createDb(c.env.DATABASE_URL)

			// 先判断歌单是否存在且未被删除
			const [playlist] = await db
				.select()
				.from(sharedPlaylists)
				.where(
					and(
						eq(sharedPlaylists.id, playlistId),
						isNull(sharedPlaylists.deletedAt),
					),
				)
			if (!playlist) {
				return c.json({ error: 'Playlist not found' }, 404)
			}

			// 歌单存在时再校验成员关系
			const member = await getMember(db, playlistId, mid)
			if (!member) {
				return c.json({ error: 'Forbidden' }, 403)
			}

			const sinceDate = new Date(sinceMs)
			const serverTime = Date.now()

			// 元数据变更
			const metadata =
				playlist.updatedAt > sinceDate
					? {
							title: playlist.title,
							description: playlist.description,
							cover_url: playlist.coverUrl,
							updated_at: playlist.updatedAt.getTime(),
						}
					: null

			// 曲目变化(updatedAt 或 deletedAt > since)
			const changedRows = await db
				.select({
					trackUniqueKey: sharedPlaylistTracks.trackUniqueKey,
					sortKey: sharedPlaylistTracks.sortKey,
					updatedAt: sharedPlaylistTracks.updatedAt,
					deletedAt: sharedPlaylistTracks.deletedAt,
					track: sharedTracks,
				})
				.from(sharedPlaylistTracks)
				.leftJoin(
					sharedTracks,
					eq(sharedPlaylistTracks.trackUniqueKey, sharedTracks.uniqueKey),
				)
				.where(
					and(
						eq(sharedPlaylistTracks.playlistId, playlistId),
						or(
							gt(sharedPlaylistTracks.updatedAt, sinceDate),
							and(
								isNotNull(sharedPlaylistTracks.deletedAt),
								gt(sharedPlaylistTracks.deletedAt, sinceDate),
							),
						),
					),
				)

			const tracks: ChangeEvent[] = changedRows.map((row) => {
				if (row.deletedAt && row.deletedAt > sinceDate) {
					return {
						op: 'delete',
						track_unique_key: row.trackUniqueKey,
						deleted_at: row.deletedAt.getTime(),
					}
				}
				const t = row.track!
				return {
					op: 'upsert',
					track: {
						unique_key: t.uniqueKey,
						title: t.title,
						artist_name: t.artistName ?? undefined,
						artist_id: t.artistId ?? undefined,
						cover_url: t.coverUrl ?? undefined,
						duration: t.duration ?? undefined,
						bilibili_bvid: t.bilibiliBvid,
						bilibili_cid: t.bilibiliCid ?? undefined,
					},
					sort_key: row.sortKey,
					updated_at: row.updatedAt.getTime(),
				}
			})

			// 成员列表(仅 owner + editor)
			const members = await db
				.select({
					mid: playlistMembers.mid,
					role: playlistMembers.role,
					name: users.name,
					avatar_url: users.face,
				})
				.from(playlistMembers)
				.innerJoin(users, eq(users.mid, playlistMembers.mid))
				.where(
					and(
						eq(playlistMembers.playlistId, playlistId),
						or(
							eq(playlistMembers.role, 'owner'),
							eq(playlistMembers.role, 'editor'),
						),
					),
				)

			return c.json({
				metadata,
				tracks,
				members: members.map((m) => ({
					...m,
					mid: Number(m.mid),
				})),
				has_more: false,
				server_time: serverTime,
			})
		},
	)
	.post(
		'/:id/subscribe',
		arktypeValidator('json', subscribePlaylistRequestSchema, validationHook),
		async (c) => {
			const { sub } = c.var.jwtPayload
			const mid = sub
			const playlistId = c.req.param('id')
			const body = c.req.valid('json') ?? {}
			const inviteCode =
				typeof body?.invite_code === 'string'
					? body.invite_code.trim()
					: undefined
			const { db } = await createDb(c.env.DATABASE_URL)

			// 歌单必须存在且未删除
			const [playlist] = await db
				.select()
				.from(sharedPlaylists)
				.where(
					and(
						eq(sharedPlaylists.id, playlistId),
						isNull(sharedPlaylists.deletedAt),
					),
				)
			if (!playlist) {
				return c.json({ error: 'Playlist not found' }, 404)
			}

			// 已是成员:owner/editor 直接返回;subscriber 在邀请码匹配时升级
			const existing = await getMember(db, playlistId, mid)
			if (existing) {
				if (existing.role === 'subscriber') {
					const shouldUpgrade =
						inviteCode && playlist.editorInviteCode === inviteCode
					if (shouldUpgrade) {
						await db
							.update(playlistMembers)
							.set({ role: 'editor' })
							.where(
								and(
									eq(playlistMembers.playlistId, playlistId),
									eq(playlistMembers.mid, mid),
								),
							)
						return c.json({
							role: 'editor',
							already_member: true,
							upgraded: true,
						})
					}
				}
				return c.json({ role: existing.role, already_member: true })
			}

			// 新成员:邀请码匹配则授予 editor,否则为 subscriber
			const newRole =
				inviteCode && playlist.editorInviteCode === inviteCode
					? 'editor'
					: 'subscriber'
			await db.insert(playlistMembers).values({
				playlistId,
				mid,
				role: newRole,
			})

			return c.json({ role: newRole, already_member: false }, 201)
		},
	)
	.get('/:id/invite', async (c) => {
		const { sub } = c.var.jwtPayload
		const playlistId = c.req.param('id')
		const { db } = await createDb(c.env.DATABASE_URL)

		const [playlist] = await db
			.select({
				ownerMid: sharedPlaylists.ownerMid,
				editorInviteCode: sharedPlaylists.editorInviteCode,
			})
			.from(sharedPlaylists)
			.where(
				and(
					eq(sharedPlaylists.id, playlistId),
					isNull(sharedPlaylists.deletedAt),
				),
			)

		if (!playlist) {
			return c.json({ error: 'Playlist not found' }, 404)
		}
		if (playlist.ownerMid !== sub) {
			return c.json({ error: 'Forbidden' }, 403)
		}

		return c.json({ editor_invite_code: playlist.editorInviteCode ?? null })
	})
	.post('/:id/invite/rotate', async (c) => {
		const { sub } = c.var.jwtPayload
		const playlistId = c.req.param('id')
		const { db } = await createDb(c.env.DATABASE_URL)

		const [playlist] = await db
			.select({ ownerMid: sharedPlaylists.ownerMid })
			.from(sharedPlaylists)
			.where(
				and(
					eq(sharedPlaylists.id, playlistId),
					isNull(sharedPlaylists.deletedAt),
				),
			)

		if (!playlist) {
			return c.json({ error: 'Playlist not found' }, 404)
		}
		if (playlist.ownerMid !== sub) {
			return c.json({ error: 'Forbidden' }, 403)
		}

		for (let attempt = 0; attempt < MAX_INVITE_ROTATE_ATTEMPTS; attempt++) {
			const newCode = generateInviteCode()
			try {
				await db
					.update(sharedPlaylists)
					.set({ editorInviteCode: newCode })
					.where(eq(sharedPlaylists.id, playlistId))

				return c.json({ editor_invite_code: newCode })
			} catch (err) {
				if (isUniqueConstraintViolation(err)) {
					continue
				}
				throw err
			}
		}

		return c.json({ error: 'Invite code collision, please retry later' }, 503)
	})
	/**
	 * DELETE /playlists/:id
	 * owner 专用:软删除共享歌单(设置 deletedAt)。
	 * 其他成员若再拉取或订阅此歌单会收到 404。
	 */
	.delete('/:id', async (c) => {
		const { sub } = c.var.jwtPayload
		const mid = sub
		const playlistId = c.req.param('id')
		const { db } = await createDb(c.env.DATABASE_URL)

		const member = await getMember(db, playlistId, mid)
		if (!member || member.role !== 'owner') {
			return c.json({ error: 'Forbidden' }, 403)
		}

		await db
			.update(sharedPlaylists)
			.set({ deletedAt: new Date() })
			.where(eq(sharedPlaylists.id, playlistId))

		// 清理成员关系,确保后续请求无法再命中
		await db
			.delete(playlistMembers)
			.where(eq(playlistMembers.playlistId, playlistId))

		return c.json({ deleted: true })
	})
	/**
	 * GET /playlists/:id/members
	 * 获取歌单的所有成员(owner, editor, subscriber)。
	 * 仅 owner 和 editor 有权限调用。
	 */
	.get('/:id/members', async (c) => {
		const { sub } = c.var.jwtPayload
		const mid = sub
		const playlistId = c.req.param('id')
		const { db } = await createDb(c.env.DATABASE_URL)

		const member = await getMember(db, playlistId, mid)
		if (!member || (member.role !== 'owner' && member.role !== 'editor')) {
			return c.json({ error: 'Forbidden' }, 403)
		}

		const members = await db
			.select({
				mid: playlistMembers.mid,
				role: playlistMembers.role,
				name: users.name,
				avatar_url: users.face,
				joined_at: playlistMembers.joinedAt,
			})
			.from(playlistMembers)
			.innerJoin(users, eq(users.mid, playlistMembers.mid))
			.where(eq(playlistMembers.playlistId, playlistId))
			.orderBy(asc(playlistMembers.joinedAt))

		return c.json({
			members: members.map((m) => ({
				...m,
				mid: Number(m.mid),
				joined_at: m.joined_at.getTime(),
			})),
		})
	})

	/**
	 * DELETE /playlists/:id/members/me

	 * subscriber / editor 专用:从 playlist_members 中移除自己,解除与该歌单的关联。
	 * 幂等:若已不是成员,直接返回成功。
	 * owner 不能调用此接口(应使用 DELETE /playlists/:id)。
	 */
	.delete('/:id/members/me', async (c) => {
		const { sub } = c.var.jwtPayload
		const mid = sub
		const playlistId = c.req.param('id')
		const { db } = await createDb(c.env.DATABASE_URL)

		const member = await getMember(db, playlistId, mid)
		if (!member) {
			// 已不是成员,幂等返回成功
			return c.json({ removed: true })
		}
		if (member.role === 'owner') {
			return c.json(
				{ error: 'Owner cannot leave; use DELETE /:id to delete the playlist' },
				400,
			)
		}

		await db
			.delete(playlistMembers)
			.where(
				and(
					eq(playlistMembers.playlistId, playlistId),
					eq(playlistMembers.mid, mid),
				),
			)

		return c.json({ removed: true })
	})

// ---------------------------------------------------------------------------
// 工具函数
// ---------------------------------------------------------------------------
async function getMember(db: DrizzleDb, playlistId: string, mid: string) {
	const [member] = await db
		.select()
		.from(playlistMembers)
		.where(
			and(
				eq(playlistMembers.playlistId, playlistId),
				eq(playlistMembers.mid, mid),
			),
		)
	return member ?? null
}

async function upsertTracks(
	db: DrizzleDb,
	playlistId: string,
	mid: string,
	tracks: Array<{ track: TrackInput; sort_key: string }>,
) {
	await db
		.insert(sharedTracks)
		.values(
			tracks.map(({ track }) => ({
				uniqueKey: track.unique_key,
				title: track.title,
				artistName: track.artist_name,
				artistId: track.artist_id,
				coverUrl: track.cover_url,
				duration: track.duration,
				bilibiliBvid: track.bilibili_bvid,
				bilibiliCid: track.bilibili_cid,
			})),
		)
		.onConflictDoNothing()

	await db
		.insert(sharedPlaylistTracks)
		.values(
			tracks.map(({ track, sort_key }) => ({
				playlistId,
				trackUniqueKey: track.unique_key,
				sortKey: sort_key,
				addedByMid: mid,
			})),
		)
		.onConflictDoNothing()
}

function generateInviteCode(): string {
	const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
	let out = ''
	for (let i = 0; i < 12; i++) {
		const idx = Math.floor(Math.random() * chars.length)
		out += chars[idx]
	}

	return 'BBP-' + out
}

function isUniqueConstraintViolation(err: unknown): boolean {
	if (!err || typeof err !== 'object') return false
	const code = (err as { code?: unknown }).code
	return code === '23505'
}

const MAX_INVITE_ROTATE_ATTEMPTS = 5

export default playlistsRoute


================================================
FILE: apps/backend/src/types.ts
================================================
/** JWT payload 结构 */
export interface JwtTokenPayload {
	sub: string // B 站 mid(text 存储,避免大数精度丢失)
	jwtVersion?: number
	iat?: number
	exp?: number
	role?: string
}

/** POST /api/playlists/:id/changes — 请求体单条变更 */
export type ChangeOperation =
	| {
			op: 'upsert'
			track: TrackInput
			sort_key: string
			operation_at: number
	  }
	| {
			op: 'remove'
			track_unique_key: string
			operation_at: number
	  }
	| {
			op: 'reorder'
			track_unique_key: string
			sort_key: string
			operation_at: number
	  }

export interface TrackInput {
	unique_key: string
	title: string
	artist_name?: string
	artist_id?: string
	cover_url?: string
	duration?: number
	bilibili_bvid: string
	bilibili_cid?: string
}

/** GET /api/playlists/:id/changes — 响应体单条变更 */
export type ChangeEvent =
	| {
			op: 'upsert'
			track: TrackInput
			sort_key: string
			updated_at: number
	  }
	| {
			op: 'delete'
			track_unique_key: string
			deleted_at: number
	  }

export interface PlaylistMemberInfo {
	mid: number
	name: string
	avatar_url?: string | null
	role: 'owner' | 'editor'
}


================================================
FILE: apps/backend/src/validators/auth.ts
================================================
import { type as arkType } from 'arktype'

export const loginRequestSchema = arkType({
	cookie: 'string',
})


================================================
FILE: apps/backend/src/validators/playlists.ts
================================================
import { type as arkType } from 'arktype'

const trackInputSchema = arkType({
	unique_key: 'string',
	title: 'string',
	'artist_name?': 'string',
	'artist_id?': 'string',
	'cover_url?': 'string',
	'duration?': 'number',
	bilibili_bvid: 'string',
	'bilibili_cid?': 'string',
})

const trackWithSortSchema = arkType({
	track: trackInputSchema,
	sort_key: 'string',
})

const upsertChangeSchema = arkType({
	op: "'upsert'",
	track: trackInputSchema,
	sort_key: 'string',
	operation_at: 'number',
})

const removeChangeSchema = arkType({
	op: "'remove'",
	track_unique_key: 'string',
	operation_at: 'number',
})

const reorderChangeSchema = arkType({
	op: "'reorder'",
	track_unique_key: 'string',
	sort_key: 'string',
	operation_at: 'number',
})

const changeOperationSchema = upsertChangeSchema
	.or(removeChangeSchema)
	.or(reorderChangeSchema)

export const createPlaylistRequestSchema = arkType({
	title: 'string',
	'description?': 'string',
	'cover_url?': 'string',
	'tracks?': trackWithSortSchema.array(),
})

export const updatePlaylistRequestSchema = arkType({
	'title?': 'string',
	'description?': 'string',
	'cover_url?': 'string',
})

export const playlistChangesRequestSchema = arkType({
	changes: changeOperationSchema.array(),
})

export const getPlaylistChangesRequestSchema = arkType({
	since: 'string.integer.parse',
})

export const subscribePlaylistRequestSchema = arkType({
	'invite_code?': 'string',
})


================================================
FILE: apps/backend/tsconfig.json
================================================
{
	"compilerOptions": {
		"target": "ESNext",
		"module": "ESNext",
		"moduleResolution": "Bundler",
		"lib": ["ESNext"],
		"strict": true,
		"skipLibCheck": true,
		"noEmit": true
	},
	"include": ["src", "worker-configuration.d.ts"]
}


================================================
FILE: apps/backend/worker-configuration.d.ts
================================================
/* eslint-disable */
// Generated by Wrangler by running `wrangler types` (hash: 88348ad3312309862e1f03e26b965c16)
// Runtime types generated with workerd@1.20260302.0 2025-02-01 nodejs_compat
declare namespace Cloudflare {
	interface GlobalProps {
		mainModule: typeof import('./src/index')
	}
	interface Env {
		KV: KVNamespace
		DATABASE_URL: string
		JWT_SECRET: string
	}
}
interface Env extends Cloudflare.Env {}

// Begin runtime types
/*! *****************************************************************************
Copyright (c) Cloudflare. All rights reserved.
Copyright (c) Microsoft Corporation. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* eslint-disable */
// noinspection JSUnusedGlobalSymbols
declare var onmessage: never
/**
 * The **`DOMException`** interface represents an abnormal event (called an **exception**) that occurs as a result of calling a method or accessing a property of a web API.
 *
 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException)
 */
declare class DOMException extends Error {
	constructor(message?: string, name?: string)
	/**
	 * The **`message`** read-only property of the a message or description associated with the given error name.
	 *
	 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message)
	 */
	readonly message: string
	/**
	 * The **`name`** read-only property of the one of the strings associated with an error name.
	 *
	 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name)
	 */
	readonly name: string
	/**
	 * The **`code`** read-only property of the DOMException interface returns one of the legacy error code constants, or `0` if none match.
	 * @deprecated
	 *
	 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code)
	 */
	readonly code: number
	static readonly INDEX_SIZE_ERR: number
	static readonly DOMSTRING_SIZE_ERR: number
	static readonly HIERARCHY_REQUEST_ERR: number
	static readonly WRONG_DOCUMENT_ERR: number
	static readonly INVALID_CHARACTER_ERR: number
	static readonly NO_DATA_ALLOWED_ERR: number
	static readonly NO_MODIFICATION_ALLOWED_ERR: number
	static readonly NOT_FOUND_ERR: number
	static readonly NOT_SUPPORTED_ERR: number
	static readonly INUSE_ATTRIBUTE_ERR: number
	static readonly INVALID_STATE_ERR: number
	static readonly SYNTAX_ERR: number
	static readonly INVALID_MODIFICATION_ERR: number
	static readonly NAMESPACE_ERR: number
	static readonly INVALID_ACCESS_ERR: number
	static readonly VALIDATION_ERR: number
	static readonly TYPE_MISMATCH_ERR: number
	static readonly SECURITY_ERR: number
	static readonly NETWORK_ERR: number
	static readonly ABORT_ERR: number
	static readonly URL_MISMATCH_ERR: number
	static readonly QUOTA_EXCEEDED_ERR: number
	static readonly TIMEOUT_ERR: number
	static readonly INVALID_NODE_TYPE_ERR: number
	static readonly DATA_CLONE_ERR: number
	get stack(): any
	set stack(value: any)
}
type WorkerGlobalScopeEventMap = {
	fetch: FetchEvent
	scheduled: ScheduledEvent
	queue: QueueEvent
	unhandledrejection: PromiseRejectionEvent
	rejectionhandled: PromiseRejectionEvent
}
declare abstract class WorkerGlobalScope extends EventTarget<WorkerGlobalScopeEventMap> {
	EventTarget: typeof EventTarget
}
/* The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). *
 * The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox).
 *
 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console)
 */
interface Console {
	'assert'(condition?: boolean, ...data: any[]): void
	/**
	 * The **`console.clear()`** static method clears the console if possible.
	 *
	 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static)
	 */
	clear(): void
	/**
	 * The **`console.count()`** static method logs the number of times that this particular call to `count()` has been called.
	 *
	 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static)
	 */
	count(label?: string): void
	/**
	 * The **`console.countReset()`** static method resets counter used with console/count_static.
	 *
	 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static)
	 */
	countReset(label?: string): void
	/**
	 * The **`console.debug()`** static method outputs a message to the console at the 'debug' log level.
	 *
	 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static)
	 */
	debug(...data: any[]): void
	/**
	 *
Download .txt
gitextract_ppffbibi/

├── .agent/
│   ├── rules/
│   │   ├── changelog.md
│   │   └── measure-layout.md
│   └── skills/
│       ├── gesture-handler-3-migration/
│       │   └── SKILL.md
│       └── upgrading-expo/
│           ├── SKILL.md
│           └── references/
│               ├── new-architecture.md
│               ├── react-19.md
│               └── react-compiler.md
├── .agents/
│   └── skills/
│       ├── react-doctor/
│       │   └── SKILL.md
│       └── react-native-ease-refactor/
│           └── SKILL.md
├── .easignore
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── release.yml
│   ├── wiki/
│   │   ├── Home.md
│   │   └── _Sidebar.md
│   └── workflows/
│       ├── build.yml
│       ├── check-lyricon-updates.yml
│       ├── nightly.yml
│       ├── pr-checks.yml
│       ├── update.yml
│       └── wiki.yml
├── .gitignore
├── .gitleaks-baseline.json
├── .gitleaks.toml
├── .npmrc
├── .oxfmtrc.json
├── .oxlintrc.json
├── .sisyphus/
│   ├── boulder.json
│   ├── evidence/
│   │   ├── task-1-complete.txt
│   │   ├── task-2-complete.txt
│   │   ├── task-3-lint-output.txt
│   │   ├── task-4-page-created.txt
│   │   ├── task-5-play-all.txt
│   │   ├── task-6-structure.txt
│   │   └── task-7-complete.txt
│   ├── notepads/
│   │   └── task-5-play-all/
│   │       └── learnings.md
│   └── plans/
│       └── homepage-ui-optimization.md
├── .syncpackrc
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── AGENTS.md
├── LICENSE
├── PRIVACY.md
├── README.md
├── apps/
│   ├── README.md
│   ├── backend/
│   │   ├── .dev.vars.example
│   │   ├── .gitignore
│   │   ├── drizzle.config.ts
│   │   ├── mise.toml
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── db/
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── index.ts
│   │   │   ├── middleware/
│   │   │   │   └── auth.ts
│   │   │   ├── routes/
│   │   │   │   ├── auth.ts
│   │   │   │   ├── me.ts
│   │   │   │   └── playlists.ts
│   │   │   ├── types.ts
│   │   │   └── validators/
│   │   │       ├── auth.ts
│   │   │       └── playlists.ts
│   │   ├── tsconfig.json
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.toml
│   ├── docs/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── docs/
│   │   │   ├── .vitepress/
│   │   │   │   ├── components/
│   │   │   │   │   ├── AppNotExistPage.vue
│   │   │   │   │   ├── SharePlaylistPage.vue
│   │   │   │   │   ├── ShareTrackPage.vue
│   │   │   │   │   └── shared-page.css
│   │   │   │   └── config.mts
│   │   │   ├── SPL.md
│   │   │   ├── app-not-exist.md
│   │   │   ├── guides/
│   │   │   │   ├── comments.md
│   │   │   │   ├── download.md
│   │   │   │   ├── external-playlist.md
│   │   │   │   ├── index.md
│   │   │   │   ├── install.md
│   │   │   │   ├── leaderboard.md
│   │   │   │   ├── lyrics.md
│   │   │   │   ├── player.md
│   │   │   │   ├── playlist.md
│   │   │   │   ├── search.md
│   │   │   │   ├── settings.md
│   │   │   │   └── shared-playlist.md
│   │   │   ├── index.md
│   │   │   ├── public/
│   │   │   │   └── .well-known/
│   │   │   │       └── assetlinks.json
│   │   │   └── share/
│   │   │       ├── playlist.md
│   │   │       └── track.md
│   │   ├── env.d.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── mobile/
│   │   ├── .gitignore
│   │   ├── .maestro/
│   │   │   ├── comments_flow.yaml
│   │   │   ├── common/
│   │   │   │   ├── open_player.yaml
│   │   │   │   └── setup.yaml
│   │   │   ├── playback_flow.yaml
│   │   │   ├── playlist_flow.yaml
│   │   │   ├── search_flow.yaml
│   │   │   └── sync_flow.yaml
│   │   ├── AGENTS.md
│   │   ├── CHANGELOG.md
│   │   ├── README.md
│   │   ├── app.config.ts
│   │   ├── assets/
│   │   │   └── config/
│   │   │       └── google-services/
│   │   │           ├── GoogleService-Info.plist
│   │   │           └── google-services.json
│   │   ├── babel.config.js
│   │   ├── docs/
│   │   │   ├── ARCHITECTURE.md
│   │   │   ├── BEST_PRACTICES.md
│   │   │   ├── CONTRIBUTING.md
│   │   │   ├── Home.md
│   │   │   ├── RELEASE.md
│   │   │   └── TECHNICAL_DEBT.md
│   │   ├── drizzle/
│   │   │   ├── 0000_productive_joystick.sql
│   │   │   ├── 0001_fast_trauma.sql
│   │   │   ├── 0002_groovy_maximus.sql
│   │   │   ├── 0003_glamorous_psylocke.sql
│   │   │   ├── 0004_smiling_beast.sql
│   │   │   ├── 0005_spotty_exiles.sql
│   │   │   ├── 0006_breezy_jigsaw.sql
│   │   │   ├── 0007_legal_thor.sql
│   │   │   ├── 0008_overrated_jimmy_woo.sql
│   │   │   ├── 0009_lethal_marten_broadcloak.sql
│   │   │   ├── 0010_brainy_anita_blake.sql
│   │   │   ├── 0011_grey_echo.sql
│   │   │   ├── 0012_blushing_human_fly.sql
│   │   │   ├── 0013_jittery_randall.sql
│   │   │   ├── 0014_flippant_sebastian_shaw.sql
│   │   │   ├── 0015_flippant_skaar.sql
│   │   │   ├── 0016_cheerful_stark_industries.sql
│   │   │   ├── 0017_rare_lifeguard.sql
│   │   │   ├── 0018_green_dracula.sql
│   │   │   ├── 0019_icy_mandarin.sql
│   │   │   ├── 0020_ambitious_sheva_callister.sql
│   │   │   ├── meta/
│   │   │   │   ├── 0000_snapshot.json
│   │   │   │   ├── 0001_snapshot.json
│   │   │   │   ├── 0002_snapshot.json
│   │   │   │   ├── 0003_snapshot.json
│   │   │   │   ├── 0004_snapshot.json
│   │   │   │   ├── 0005_snapshot.json
│   │   │   │   ├── 0006_snapshot.json
│   │   │   │   ├── 0007_snapshot.json
│   │   │   │   ├── 0008_snapshot.json
│   │   │   │   ├── 0009_snapshot.json
│   │   │   │   ├── 0010_snapshot.json
│   │   │   │   ├── 0011_snapshot.json
│   │   │   │   ├── 0012_snapshot.json
│   │   │   │   ├── 0013_snapshot.json
│   │   │   │   ├── 0014_snapshot.json
│   │   │   │   ├── 0015_snapshot.json
│   │   │   │   ├── 0016_snapshot.json
│   │   │   │   ├── 0017_snapshot.json
│   │   │   │   ├── 0018_snapshot.json
│   │   │   │   ├── 0019_snapshot.json
│   │   │   │   ├── 0020_snapshot.json
│   │   │   │   └── _journal.json
│   │   │   └── migrations.js
│   │   ├── drizzle.config.ts
│   │   ├── eas.json
│   │   ├── expo-plugins/
│   │   │   ├── withAbiFilters.js
│   │   │   ├── withAndroidGradleProperties.js
│   │   │   ├── withAndroidPlugin.js
│   │   │   └── withKotlinSerialization.js
│   │   ├── index.js
│   │   ├── metro.config.js
│   │   ├── mise.toml
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── app/
│   │   │   │   ├── (tabs)/
│   │   │   │   │   ├── _layout.tsx
│   │   │   │   │   ├── index.tsx
│   │   │   │   │   ├── library/
│   │   │   │   │   │   └── [tab].tsx
│   │   │   │   │   └── settings/
│   │   │   │   │       └── index.tsx
│   │   │   │   ├── +native-intent.ts
│   │   │   │   ├── +not-found.tsx
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── comments/
│   │   │   │   │   ├── [bvid].tsx
│   │   │   │   │   └── reply.tsx
│   │   │   │   ├── download.tsx
│   │   │   │   ├── downloaded.tsx
│   │   │   │   ├── history/
│   │   │   │   │   ├── [date].tsx
│   │   │   │   │   └── overall.tsx
│   │   │   │   ├── modal.tsx
│   │   │   │   ├── player.tsx
│   │   │   │   ├── playlist/
│   │   │   │   │   ├── external-sync.tsx
│   │   │   │   │   ├── local/
│   │   │   │   │   │   └── [id].tsx
│   │   │   │   │   ├── recently/
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   └── remote/
│   │   │   │   │       ├── collection/
│   │   │   │   │       │   └── [id].tsx
│   │   │   │   │       ├── favorite/
│   │   │   │   │       │   └── [id].tsx
│   │   │   │   │       ├── multipage/
│   │   │   │   │       │   └── [bvid].tsx
│   │   │   │   │       ├── search-result/
│   │   │   │   │       │   ├── fav/
│   │   │   │   │       │   │   └── [query].tsx
│   │   │   │   │       │   └── global/
│   │   │   │   │       │       └── [query].tsx
│   │   │   │   │       ├── toview.tsx
│   │   │   │   │       └── uploader/
│   │   │   │   │           └── [mid].tsx
│   │   │   │   ├── settings/
│   │   │   │   │   ├── appearance.tsx
│   │   │   │   │   ├── donate.tsx
│   │   │   │   │   ├── download.tsx
│   │   │   │   │   ├── general.tsx
│   │   │   │   │   ├── lyrics.tsx
│   │   │   │   │   └── playback.tsx
│   │   │   │   ├── share/
│   │   │   │   │   └── playlist.tsx
│   │   │   │   └── test.tsx
│   │   │   ├── assets/
│   │   │   │   └── lottie/
│   │   │   │       ├── play-pause.json
│   │   │   │       ├── skip-next.json
│   │   │   │       └── skip-prev.json
│   │   │   ├── components/
│   │   │   │   ├── ErrorBoundary.tsx
│   │   │   │   ├── ModalRegistry.tsx
│   │   │   │   ├── NowPlayingBar.tsx
│   │   │   │   ├── common/
│   │   │   │   │   ├── AnimatedModalOverlay.tsx
│   │   │   │   │   ├── Button.tsx
│   │   │   │   │   ├── CoverWithPlaceHolder.tsx
│   │   │   │   │   ├── FunctionalMenu.tsx
│   │   │   │   │   └── IconButton.tsx
│   │   │   │   ├── modals/
│   │   │   │   │   ├── AlertModal.tsx
│   │   │   │   │   ├── PlayerQueueModal.tsx
│   │   │   │   │   ├── app/
│   │   │   │   │   │   ├── DonationQRModal.tsx
│   │   │   │   │   │   ├── UpdateAppModal.tsx
│   │   │   │   │   │   └── WelcomeModal.tsx
│   │   │   │   │   ├── bilibili/
│   │   │   │   │   │   └── AddVideoToBilibiliFavModal.tsx
│   │   │   │   │   ├── edit-metadata/
│   │   │   │   │   │   ├── editPlaylistMetadataModal.tsx
│   │   │   │   │   │   └── editTrackMetadataModal.tsx
│   │   │   │   │   ├── login/
│   │   │   │   │   │   ├── CookieLoginModal.tsx
│   │   │   │   │   │   ├── PhoneLoginModal.tsx
│   │   │   │   │   │   ├── QRCodeLoginModal.tsx
│   │   │   │   │   │   └── steps/
│   │   │   │   │   │       ├── GeetestVerifyStep.tsx
│   │   │   │   │   │       ├── InputCodeStep.tsx
│   │   │   │   │   │       ├── InputPhoneStep.tsx
│   │   │   │   │   │       └── SuccessStep.tsx
│   │   │   │   │   ├── lyrics/
│   │   │   │   │   │   ├── EditLyrics.tsx
│   │   │   │   │   │   └── ManualSearchLyrics.tsx
│   │   │   │   │   ├── player/
│   │   │   │   │   │   ├── DanmakuSettingsModal.tsx
│   │   │   │   │   │   ├── LyricsSelectionModal.tsx
│   │   │   │   │   │   ├── PlaybackSpeedModal.tsx
│   │   │   │   │   │   ├── SleepTimerModal.tsx
│   │   │   │   │   │   └── SongShareModal.tsx
│   │   │   │   │   ├── playlist/
│   │   │   │   │   │   ├── BatchAddTracksToLocalPlaylist.tsx
│   │   │   │   │   │   ├── CreatePlaylistModal.tsx
│   │   │   │   │   │   ├── DuplicateLocalPlaylistModal.tsx
│   │   │   │   │   │   ├── EnableSharingModal.tsx
│   │   │   │   │   │   ├── FavoriteSyncProgressModal.tsx
│   │   │   │   │   │   ├── InputExternalPlaylistInfo.tsx
│   │   │   │   │   │   ├── ManualMatchExternalSync.tsx
│   │   │   │   │   │   ├── MergePlaylistsModal.tsx
│   │   │   │   │   │   ├── SaveQueueToPlaylistModal.tsx
│   │   │   │   │   │   ├── SubscribeToSharedPlaylistModal.tsx
│   │   │   │   │   │   ├── SyncLocalToBilibiliModal.tsx
│   │   │   │   │   │   └── UpdateTrackLocalPlaylistsModal.tsx
│   │   │   │   │   └── settings/
│   │   │   │   │       ├── CoverDownloadProgressModal.tsx
│   │   │   │   │       └── ExportDownloadsProgressModal.tsx
│   │   │   │   └── providers.tsx
│   │   │   ├── features/
│   │   │   │   ├── comments/
│   │   │   │   │   └── components/
│   │   │   │   │       └── CommentItem.tsx
│   │   │   │   ├── downloads/
│   │   │   │   │   ├── DownloadHeader.tsx
│   │   │   │   │   └── DownloadTaskItem.tsx
│   │   │   │   ├── history/
│   │   │   │   │   └── HistoryListItem.tsx
│   │   │   │   ├── home/
│   │   │   │   │   └── SearchSuggestions.tsx
│   │   │   │   ├── library/
│   │   │   │   │   ├── collection/
│   │   │   │   │   │   ├── CollectionList.tsx
│   │   │   │   │   │   └── CollectionListItem.tsx
│   │   │   │   │   ├── favorite/
│   │   │   │   │   │   ├── FavoriteFolderList.tsx
│   │   │   │   │   │   └── FavoriteFolderListItem.tsx
│   │   │   │   │   ├── local/
│   │   │   │   │   │   ├── LocalPlaylistItem.tsx
│   │   │   │   │   │   └── LocalPlaylistList.tsx
│   │   │   │   │   ├── multipage/
│   │   │   │   │   │   ├── MultiPageVideosItem.tsx
│   │   │   │   │   │   └── MultiPageVideosList.tsx
│   │   │   │   │   ├── shared/
│   │   │   │   │   │   ├── DataFetchingError.tsx
│   │   │   │   │   │   └── TabDisabled.tsx
│   │   │   │   │   └── skeletons/
│   │   │   │   │       └── LibraryTabSkeleton.tsx
│   │   │   │   ├── player/
│   │   │   │   │   ├── components/
│   │   │   │   │   │   ├── BGStreamerShader.ts
│   │   │   │   │   │   ├── LyricsControlOverlay.tsx
│   │   │   │   │   │   ├── PlayerControls.tsx
│   │   │   │   │   │   ├── PlayerFunctionalMenu.tsx
│   │   │   │   │   │   ├── PlayerHeader.tsx
│   │   │   │   │   │   ├── PlayerLyrics.tsx
│   │   │   │   │   │   ├── PlayerMainTab.tsx
│   │   │   │   │   │   ├── PlayerSlider.tsx
│   │   │   │   │   │   ├── PlayerTrackInfo.tsx
│   │   │   │   │   │   ├── SpectrumVisualizer.tsx
│   │   │   │   │   │   ├── danmaku/
│   │   │   │   │   │   │   └── DanmakuView.tsx
│   │   │   │   │   │   ├── lyrics/
│   │   │   │   │   │   │   ├── KaraokeWord.tsx
│   │   │   │   │   │   │   ├── LyricActionSheet.tsx
│   │   │   │   │   │   │   ├── LyricLineItem.tsx
│   │   │   │   │   │   │   └── LyricsOffsetControl.tsx
│   │   │   │   │   │   └── sharing/
│   │   │   │   │   │       ├── LyricsShareCard.tsx
│   │   │   │   │   │       └── SongShareCard.tsx
│   │   │   │   │   └── hooks/
│   │   │   │   │       ├── danmaku/
│   │   │   │   │       │   ├── constants.ts
│   │   │   │   │       │   ├── useDanmakuLoader.ts
│   │   │   │   │       │   └── useDanmakuRender.ts
│   │   │   │   │       ├── useLyricSync.ts
│   │   │   │   │       └── usePlayerHeaderAnimation.ts
│   │   │   │   └── playlist/
│   │   │   │       ├── local/
│   │   │   │       │   ├── components/
│   │   │   │       │   │   ├── LocalPlaylistHeader.tsx
│   │   │   │       │   │   ├── LocalPlaylistItem.tsx
│   │   │   │       │   │   ├── LocalTrackList.tsx
│   │   │   │       │   │   ├── PlaylistError.tsx
│   │   │   │       │   │   ├── SharedPlaylistMembersSheet.tsx
│   │   │   │       │   │   └── SyncFailuresSheet.tsx
│   │   │   │       │   └── hooks/
│   │   │   │       │       ├── useLocalPlaylistMenu.ts
│   │   │   │       │       ├── useLocalPlaylistPlayer.ts
│   │   │   │       │       └── useTrackSelection.ts
│   │   │   │       ├── remote/
│   │   │   │       │   ├── components/
│   │   │   │       │   │   ├── FlashingTrackListItem.tsx
│   │   │   │       │   │   ├── PlaylistError.tsx
│   │   │   │       │   │   ├── PlaylistHeader.tsx
│   │   │   │       │   │   ├── PlaylistItem.tsx
│   │   │   │       │   │   └── RemoteTrackList.tsx
│   │   │   │       │   ├── hooks/
│   │   │   │       │   │   ├── useCheckLinkedToLocalPlaylist.ts
│   │   │   │       │   │   ├── usePlaylistMenu.ts
│   │   │   │       │   │   ├── useRemotePlaylist.ts
│   │   │   │       │   │   └── useTrackSelection.ts
│   │   │   │       │   ├── search-result/
│   │   │   │       │   │   ├── constants.ts
│   │   │   │       │   │   └── hooks/
│   │   │   │       │   │       └── useSearchInteractions.ts
│   │   │   │       │   └── toview/
│   │   │   │       │       └── components/
│   │   │   │       │           ├── Item.tsx
│   │   │   │       │           └── ProgressRing.tsx
│   │   │   │       └── skeletons/
│   │   │   │           └── PlaylistSkeleton.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── analytics/
│   │   │   │   │   └── useFeatureTracking.ts
│   │   │   │   ├── app/
│   │   │   │   │   ├── useCheckUpdate.tsx
│   │   │   │   │   └── useFastMigrations.ts
│   │   │   │   ├── auth/
│   │   │   │   │   ├── useGeetest.ts
│   │   │   │   │   └── usePhoneLogin.ts
│   │   │   │   ├── mutations/
│   │   │   │   │   ├── bilibili/
│   │   │   │   │   │   ├── comments.ts
│   │   │   │   │   │   ├── favorite.ts
│   │   │   │   │   │   └── video.ts
│   │   │   │   │   ├── db/
│   │   │   │   │   │   ├── playlist.ts
│   │   │   │   │   │   └── track.ts
│   │   │   │   │   ├── lyrics/
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   └── orpheus/
│   │   │   │   │       └── index.ts
│   │   │   │   ├── player/
│   │   │   │   │   ├── useCurrentTrack.ts
│   │   │   │   │   ├── useCurrentTrackId.ts
│   │   │   │   │   ├── useIsCurrentTrack.ts
│   │   │   │   │   ├── useLocalCover.ts
│   │   │   │   │   ├── useSmoothProgress.ts
│   │   │   │   │   └── useTrackProgress.ts
│   │   │   │   ├── queries/
│   │   │   │   │   ├── bilibili/
│   │   │   │   │   │   ├── comments.ts
│   │   │   │   │   │   ├── danmaku.ts
│   │   │   │   │   │   ├── favorite.ts
│   │   │   │   │   │   ├── search.ts
│   │   │   │   │   │   ├── user.ts
│   │   │   │   │   │   └── video.ts
│   │   │   │   │   ├── db/
│   │   │   │   │   │   ├── playlist.ts
│   │   │   │   │   │   └── track.ts
│   │   │   │   │   ├── external-playlist/
│   │   │   │   │   │   └── useExternalPlaylist.ts
│   │   │   │   │   ├── lyrics/
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── orpheus/
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── playHistory.ts
│   │   │   │   │   ├── sharedPlaylistAllMembers.ts
│   │   │   │   │   ├── sharedPlaylistMembers.ts
│   │   │   │   │   ├── sharedPlaylistPreview.ts
│   │   │   │   │   └── useRecentPlaylists.ts
│   │   │   │   ├── router/
│   │   │   │   │   ├── useBottomTabBarHeight.ts
│   │   │   │   │   └── usePreventRemove.ts
│   │   │   │   ├── stores/
│   │   │   │   │   ├── useAppStore.ts
│   │   │   │   │   ├── useDownloadManagerStore.ts
│   │   │   │   │   ├── useExternalPlaylistSyncStore.tsx
│   │   │   │   │   ├── useModalStore.ts
│   │   │   │   │   ├── usePlayerStore.ts
│   │   │   │   │   └── useSharedPlaylistMembersStore.ts
│   │   │   │   ├── ui/
│   │   │   │   │   ├── useDoubleTapScrollToTop.ts
│   │   │   │   │   ├── usePlaylistBackgroundColor.ts
│   │   │   │   │   └── useScreenDimensions.ts
│   │   │   │   └── utils/
│   │   │   │       ├── useDebouncedValue.ts
│   │   │   │       ├── useIsActuallyOffline.ts
│   │   │   │       ├── usePreviousState.ts
│   │   │   │       └── useRefreshOnFocus.ts
│   │   │   ├── lib/
│   │   │   │   ├── api/
│   │   │   │   │   ├── bbplayer/
│   │   │   │   │   │   └── client.ts
│   │   │   │   │   ├── bilibili/
│   │   │   │   │   │   ├── api.ts
│   │   │   │   │   │   ├── client.ts
│   │   │   │   │   │   ├── proto/
│   │   │   │   │   │   │   ├── dm.d.ts
│   │   │   │   │   │   │   ├── dm.js
│   │   │   │   │   │   │   └── dm.proto
│   │   │   │   │   │   ├── utils.ts
│   │   │   │   │   │   └── wbi.ts
│   │   │   │   │   ├── kugou/
│   │   │   │   │   │   └── api.ts
│   │   │   │   │   ├── netease/
│   │   │   │   │   │   ├── api.ts
│   │   │   │   │   │   ├── crypto.ts
│   │   │   │   │   │   ├── request.ts
│   │   │   │   │   │   └── utils.ts
│   │   │   │   │   └── qqmusic/
│   │   │   │   │       └── api.ts
│   │   │   │   ├── config/
│   │   │   │   │   ├── queryClient.ts
│   │   │   │   │   └── sentry.ts
│   │   │   │   ├── db/
│   │   │   │   │   ├── db.ts
│   │   │   │   │   └── schema.ts
│   │   │   │   ├── errors/
│   │   │   │   │   ├── facade.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── player.ts
│   │   │   │   │   ├── service.ts
│   │   │   │   │   └── thirdparty/
│   │   │   │   │       ├── bilibili.ts
│   │   │   │   │       └── netease.ts
│   │   │   │   ├── facades/
│   │   │   │   │   ├── bilibili.ts
│   │   │   │   │   ├── playlist.ts
│   │   │   │   │   ├── sharedPlaylist.ts
│   │   │   │   │   ├── syncBilibiliPlaylist.ts
│   │   │   │   │   └── syncExternalPlaylist.ts
│   │   │   │   ├── player/
│   │   │   │   │   ├── PlayerSideEffects.ts
│   │   │   │   │   └── progressListener.ts
│   │   │   │   ├── services/
│   │   │   │   │   ├── analyticsService.ts
│   │   │   │   │   ├── artistService.ts
│   │   │   │   │   ├── externalPlaylistService.ts
│   │   │   │   │   ├── genKey.ts
│   │   │   │   │   ├── lyricService.ts
│   │   │   │   │   ├── playlistService.ts
│   │   │   │   │   ├── syncLocalToBilibiliService.ts
│   │   │   │   │   ├── trackService.ts
│   │   │   │   │   └── updateService.ts
│   │   │   │   ├── theme/
│   │   │   │   │   └── material3Colors.ts
│   │   │   │   ├── utils/
│   │   │   │   │   └── playlistUrlParser.ts
│   │   │   │   └── workers/
│   │   │   │       └── PlaylistSyncWorker.ts
│   │   │   ├── theme/
│   │   │   │   └── dimensions.ts
│   │   │   ├── types/
│   │   │   │   ├── apis/
│   │   │   │   │   ├── baidu.ts
│   │   │   │   │   ├── bilibili.ts
│   │   │   │   │   ├── kugou.ts
│   │   │   │   │   ├── kuwo.ts
│   │   │   │   │   ├── netease.ts
│   │   │   │   │   └── qqmusic.ts
│   │   │   │   ├── core/
│   │   │   │   │   ├── appStore.ts
│   │   │   │   │   ├── downloadManagerStore.ts
│   │   │   │   │   ├── media.ts
│   │   │   │   │   └── scope.ts
│   │   │   │   ├── external_playlist.ts
│   │   │   │   ├── flashlist.ts
│   │   │   │   ├── navigation.ts
│   │   │   │   ├── player/
│   │   │   │   │   └── lyrics.ts
│   │   │   │   ├── services/
│   │   │   │   │   ├── artist.ts
│   │   │   │   │   ├── playlist.ts
│   │   │   │   │   └── track.ts
│   │   │   │   └── storage.ts
│   │   │   └── utils/
│   │   │       ├── __mocks__/
│   │   │       │   └── log.ts
│   │   │       ├── __tests__/
│   │   │       │   ├── set.test.ts
│   │   │       │   ├── sticky-mitt.test.ts
│   │   │       │   └── time.test.ts
│   │   │       ├── color.ts
│   │   │       ├── danmaku.ts
│   │   │       ├── error-handling.ts
│   │   │       ├── haptics.ts
│   │   │       ├── log.ts
│   │   │       ├── lottie.ts
│   │   │       ├── matching.ts
│   │   │       ├── mmkv.ts
│   │   │       ├── network.ts
│   │   │       ├── neverthrow-utils.ts
│   │   │       ├── player.ts
│   │   │       ├── search.ts
│   │   │       ├── set.ts
│   │   │       ├── sticky-mitt.ts
│   │   │       ├── time.ts
│   │   │       └── toast.ts
│   │   └── tsconfig.json
│   └── update-publisher/
│       ├── package.json
│       ├── src/
│       │   └── index.ts
│       └── tsconfig.json
├── commitlint.config.js
├── eslint.config.mjs
├── lefthook.yml
├── package.json
├── packages/
│   ├── eslint-plugin/
│   │   ├── index.js
│   │   ├── package.json
│   │   └── rules/
│   │       └── no-navigate-after-modal-close.js
│   ├── heatmap/
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── components/
│   │   │   │   ├── HeatMapCell.tsx
│   │   │   │   ├── MonthlyHeatMap.tsx
│   │   │   │   └── WeeklyHeatMap.tsx
│   │   │   ├── constants/
│   │   │   │   └── theme.ts
│   │   │   ├── index.ts
│   │   │   ├── types.ts
│   │   │   └── utils/
│   │   │       └── calendar.ts
│   │   └── tsconfig.json
│   ├── image-theme-colors/
│   │   ├── .gitignore
│   │   ├── .npmignore
│   │   ├── README.md
│   │   ├── android/
│   │   │   ├── build.gradle
│   │   │   └── src/
│   │   │       └── main/
│   │   │           ├── AndroidManifest.xml
│   │   │           └── java/
│   │   │               └── expo/
│   │   │                   └── modules/
│   │   │                       └── imagethemecolors/
│   │   │                           └── ExpoImageThemeColorsModule.kt
│   │   ├── example/
│   │   │   ├── .gitignore
│   │   │   ├── App.tsx
│   │   │   ├── app.json
│   │   │   ├── babel.config.js
│   │   │   ├── index.ts
│   │   │   ├── metro.config.js
│   │   │   ├── package.json
│   │   │   └── tsconfig.json
│   │   ├── expo-module.config.json
│   │   ├── ios/
│   │   │   ├── ExpoImageThemeColors.podspec
│   │   │   └── ExpoImageThemeColorsModule.swift
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── ExpoImageThemeColors.types.ts
│   │   │   ├── ExpoImageThemeColorsModule.ts
│   │   │   ├── ImageRef.ts
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── logs/
│   │   ├── .gitignore
│   │   ├── .travis.yml
│   │   ├── CHANGELOG.md
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── demo/
│   │   │   ├── ComponentReadLogsRN.tsx
│   │   │   └── demo.ts
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── index.ts
│   │   │   └── transports/
│   │   │       ├── consoleTransport.ts
│   │   │       ├── crashlyticsTransport.ts
│   │   │       ├── fileAsyncTransport.ts
│   │   │       ├── mapConsoleTransport.ts
│   │   │       └── sentryTransport.ts
│   │   ├── test/
│   │   │   ├── consoleTransport.test.js
│   │   │   └── index.test.js
│   │   └── tsconfig.json
│   ├── native/
│   │   ├── .gitignore
│   │   ├── android/
│   │   │   ├── build.gradle
│   │   │   └── src/
│   │   │       └── main/
│   │   │           ├── AndroidManifest.xml
│   │   │           └── java/
│   │   │               └── expo/
│   │   │                   └── modules/
│   │   │                       └── bbplayernative/
│   │   │                           └── BBPlayerNativeModule.kt
│   │   ├── expo-module.config.json
│   │   ├── package.json
│   │   └── src/
│   │       ├── BBPlayerNative.types.ts
│   │       ├── BBPlayerNativeModule.ts
│   │       └── index.ts
│   ├── orpheus/
│   │   ├── .gitignore
│   │   ├── .lyricon_version
│   │   ├── AGENTS.md
│   │   ├── CHANGELOG.md
│   │   ├── README.md
│   │   ├── android/
│   │   │   ├── build.gradle
│   │   │   └── src/
│   │   │       └── main/
│   │   │           ├── AndroidManifest.xml
│   │   │           ├── aidl/
│   │   │           │   └── io/
│   │   │           │       └── github/
│   │   │           │           └── proify/
│   │   │           │               └── lyricon/
│   │   │           │                   ├── lyric/
│   │   │           │                   │   └── model/
│   │   │           │                   │       └── Song.aidl
│   │   │           │                   └── provider/
│   │   │           │                       ├── IProviderBinder.aidl
│   │   │           │                       ├── IProviderService.aidl
│   │   │           │                       ├── IRemotePlayer.aidl
│   │   │           │                       ├── IRemoteService.aidl
│   │   │           │                       └── ProviderInfo.aidl
│   │   │           ├── java/
│   │   │           │   ├── expo/
│   │   │           │   │   └── modules/
│   │   │           │   │       └── orpheus/
│   │   │           │   │           ├── ExpoOrpheusModule.kt
│   │   │           │   │           ├── OrpheusConfig.kt
│   │   │           │   │           ├── bilibili/
│   │   │           │   │           │   ├── BilibiliApi.kt
│   │   │           │   │           │   ├── BilibiliModels.kt
│   │   │           │   │           │   ├── BilibiliRepository.kt
│   │   │           │   │           │   ├── NetworkModule.kt
│   │   │           │   │           │   └── WbiUtil.kt
│   │   │           │   │           ├── exception/
│   │   │           │   │           │   └── exceptions.kt
│   │   │           │   │           ├── manager/
│   │   │           │   │           │   ├── CachedUriManager.kt
│   │   │           │   │           │   ├── CoverDownloadManager.kt
│   │   │           │   │           │   ├── DownloadCache.kt
│   │   │           │   │           │   ├── FloatingLyricsManager.kt
│   │   │           │   │           │   ├── LyriconBackend.kt
│   │   │           │   │           │   ├── SpectrumManager.kt
│   │   │           │   │           │   ├── StatusBarLyricsBackend.kt
│   │   │           │   │           │   ├── StatusBarLyricsManager.kt
│   │   │           │   │           │   ├── SuperLyricBackend.kt
│   │   │           │   │           │   └── UnifiedLyricsManager.kt
│   │   │           │   │           ├── model/
│   │   │           │   │           │   ├── LyricsModels.kt
│   │   │           │   │           │   └── TrackRecord.kt
│   │   │           │   │           ├── network/
│   │   │           │   │           │   └── OkHttpClientManager.kt
│   │   │           │   │           ├── service/
│   │   │           │   │           │   ├── OrpheusDownloadService.kt
│   │   │           │   │           │   ├── OrpheusHeadlessTaskService.kt
│   │   │           │   │           │   ├── OrpheusMusicService.kt
│   │   │           │   │           │   └── ShuffleManager.kt
│   │   │           │   │           ├── util/
│   │   │           │   │           │   ├── ConvertPlayerError.kt
│   │   │           │   │           │   ├── CustomCommands.kt
│   │   │           │   │           │   ├── DirectoryPickerContract.kt
│   │   │           │   │           │   ├── DownloadUtil.kt
│   │   │           │   │           │   ├── ExportDownloadsHelper.kt
│   │   │           │   │           │   ├── GeneralStorage.kt
│   │   │           │   │           │   ├── GlideBitmapLoader.kt
│   │   │           │   │           │   ├── LoudnessStorage.kt
│   │   │           │   │           │   ├── SleepTimeController.kt
│   │   │           │   │           │   ├── SplConverter.kt
│   │   │           │   │           │   ├── TrackRecordExtension.kt
│   │   │           │   │           │   └── Volume.kt
│   │   │           │   │           └── view/
│   │   │           │   │               └── LyricView.kt
│   │   │           │   └── io/
│   │   │           │       └── github/
│   │   │           │           └── proify/
│   │   │           │               └── lyricon/
│   │   │           │                   ├── lyric/
│   │   │           │                   │   └── model/
│   │   │           │                   │       ├── LyricLine.kt
│   │   │           │                   │       ├── LyricMetadata.kt
│   │   │           │                   │       ├── LyricTiming.kt
│   │   │           │                   │       ├── LyricWord.kt
│   │   │           │                   │       ├── RichLyricLine.kt
│   │   │           │                   │       ├── Song.kt
│   │   │           │                   │       ├── extensions/
│   │   │           │                   │       │   ├── Extensions.kt
│   │   │           │                   │       │   ├── LyricWord.kt
│   │   │           │                   │       │   └── TimingNavigator.kt
│   │   │           │                   │       └── interfaces/
│   │   │           │                   │           ├── DeepCopyable.kt
│   │   │           │                   │           ├── ILyricLine.kt
│   │   │           │                   │           ├── ILyricTiming.kt
│   │   │           │                   │           ├── ILyricWord.kt
│   │   │           │                   │           ├── IRichLyricLine.kt
│   │   │           │                   │           └── Normalize.kt
│   │   │           │                   └── provider/
│   │   │           │                       ├── CachedRemotePlayer.kt
│   │   │           │                       ├── CentralServiceReceiver.kt
│   │   │           │                       ├── ConnectionListener.kt
│   │   │           │                       ├── ConnectionStatus.kt
│   │   │           │                       ├── Extensions.kt
│   │   │           │                       ├── LocalProviderService.kt
│   │   │           │                       ├── LyriconFactory.kt
│   │   │           │                       ├── LyriconProvider.kt
│   │   │           │                       ├── ProviderBinder.kt
│   │   │           │                       ├── ProviderConstants.kt
│   │   │           │                       ├── ProviderInfo.kt
│   │   │           │                       ├── ProviderLogo.kt
│   │   │           │                       ├── ProviderMetadata.kt
│   │   │           │                       ├── ProviderService.kt
│   │   │           │                       ├── RemotePlayer.kt
│   │   │           │                       ├── impl/
│   │   │           │                       │   ├── EmptyProvider.kt
│   │   │           │                       │   ├── LyriconProviderImpl.kt
│   │   │           │                       │   ├── ProviderRemoteEndpoint.kt
│   │   │           │                       │   └── RemotePlayerProxy.kt
│   │   │           │                       └── service/
│   │   │           │                           ├── RemoteService.kt
│   │   │           │                           └── RemoteServiceBinder.kt
│   │   │           └── res/
│   │   │               ├── drawable/
│   │   │               │   ├── baseline_download_24.xml
│   │   │               │   ├── outline_close_24.xml
│   │   │               │   ├── outline_lock_24.xml
│   │   │               │   ├── outline_lyrics_off_24.xml
│   │   │               │   ├── outline_pause_24.xml
│   │   │               │   ├── outline_play_arrow_24.xml
│   │   │               │   ├── outline_repeat_24.xml
│   │   │               │   ├── outline_repeat_off_24.xml
│   │   │               │   ├── outline_repeat_one_24.xml
│   │   │               │   ├── outline_skip_next_24.xml
│   │   │               │   ├── outline_skip_previous_24.xml
│   │   │               │   └── outline_translate_24.xml
│   │   │               └── values/
│   │   │                   └── strings.xml
│   │   ├── docs/
│   │   │   ├── API-Events.md
│   │   │   ├── API-Methods.md
│   │   │   ├── API-Types.md
│   │   │   └── Home.md
│   │   ├── example/
│   │   │   ├── .gitignore
│   │   │   ├── App.tsx
│   │   │   ├── app.json
│   │   │   ├── babel.config.js
│   │   │   ├── index.ts
│   │   │   ├── metro.config.js
│   │   │   ├── package.json
│   │   │   ├── src/
│   │   │   │   ├── components/
│   │   │   │   │   ├── Buttons.tsx
│   │   │   │   │   ├── DebugSection.tsx
│   │   │   │   │   ├── PlayerControls.tsx
│   │   │   │   │   └── SpectrumVisualizer.tsx
│   │   │   │   └── constants.ts
│   │   │   ├── tsconfig.json
│   │   │   └── webpack.config.js
│   │   ├── expo-module.config.json
│   │   ├── ios/
│   │   │   ├── AudioSpectrumAnalyzer.swift
│   │   │   ├── BilibiliApi.swift
│   │   │   ├── ExpoOrpheus.podspec
│   │   │   ├── ExpoOrpheusModule.swift
│   │   │   ├── GeneralStorage.swift
│   │   │   ├── OrpheusDownloadManager.swift
│   │   │   ├── OrpheusModels.swift
│   │   │   ├── OrpheusPlayerManager.swift
│   │   │   ├── OrpheusQueueManager.swift
│   │   │   └── WbiUtil.swift
│   │   ├── mise.toml
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── ExpoOrpheusModule.ts
│   │   │   ├── headless.ts
│   │   │   ├── hooks/
│   │   │   │   ├── index.ts
│   │   │   │   ├── useCurrentTrack.ts
│   │   │   │   ├── useIsPlaying.ts
│   │   │   │   ├── useOrpheus.ts
│   │   │   │   ├── usePlaybackState.ts
│   │   │   │   └── useProgress.ts
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── splash/
│       ├── README.md
│       ├── jest.config.js
│       ├── package.json
│       ├── src/
│       │   ├── __tests__/
│       │   │   └── fixtures/
│       │   │       ├── 687506.json
│       │   │       └── bilibili--BV1Zu411x7mc.json
│       │   ├── converter/
│       │   │   ├── netease.test.ts
│       │   │   └── netease.ts
│       │   ├── index.ts
│       │   ├── parser/
│       │   │   ├── index.test.ts
│       │   │   ├── index.ts
│       │   │   ├── merge.ts
│       │   │   ├── spans.test.ts
│       │   │   └── spans.ts
│       │   ├── types.ts
│       │   └── utils/
│       │       ├── time.test.ts
│       │       └── time.ts
│       └── tsconfig.json
├── patches/
│   ├── react-native-mmkv.patch
│   └── sonner-native@0.23.0.patch
├── pnpm-workspace.yaml
├── rnrepo.config.json
├── scripts/
│   └── update-lyricon.sh
├── skills-lock.json
├── tsconfig.json
└── update.json
Download .txt
SYMBOL INDEX (1914 symbols across 285 files)

FILE: apps/backend/src/db/index.ts
  type DbConnection (line 7) | type DbConnection = {
  type DrizzleDb (line 11) | type DrizzleDb = DbConnection['db']
  function createDb (line 13) | async function createDb(

FILE: apps/backend/src/db/schema.ts
  type User (line 119) | type User = typeof users.$inferSelect
  type SharedPlaylist (line 120) | type SharedPlaylist = typeof sharedPlaylists.$inferSelect
  type PlaylistMember (line 121) | type PlaylistMember = typeof playlistMembers.$inferSelect
  type SharedTrack (line 122) | type SharedTrack = typeof sharedTracks.$inferSelect
  type SharedPlaylistTrack (line 123) | type SharedPlaylistTrack = typeof sharedPlaylistTracks.$inferSelect

FILE: apps/backend/src/index.ts
  type AppType (line 43) | type AppType = typeof app

FILE: apps/backend/src/routes/playlists.ts
  constant PLAYLIST_PREVIEW_LIMIT (line 44) | const PLAYLIST_PREVIEW_LIMIT = 30
  type HonoEnv (line 46) | type HonoEnv = {
  function getMember (line 749) | async function getMember(db: DrizzleDb, playlistId: string, mid: string) {
  function upsertTracks (line 762) | async function upsertTracks(
  function generateInviteCode (line 797) | function generateInviteCode(): string {
  function isUniqueConstraintViolation (line 808) | function isUniqueConstraintViolation(err: unknown): boolean {
  constant MAX_INVITE_ROTATE_ATTEMPTS (line 814) | const MAX_INVITE_ROTATE_ATTEMPTS = 5

FILE: apps/backend/src/types.ts
  type JwtTokenPayload (line 2) | interface JwtTokenPayload {
  type ChangeOperation (line 11) | type ChangeOperation =
  type TrackInput (line 30) | interface TrackInput {
  type ChangeEvent (line 42) | type ChangeEvent =
  type PlaylistMemberInfo (line 55) | interface PlaylistMemberInfo {

FILE: apps/backend/worker-configuration.d.ts
  type GlobalProps (line 5) | interface GlobalProps {
  type Env (line 8) | interface Env {
  type Env (line 14) | interface Env extends Cloudflare.Env {}
  class DOMException (line 39) | class DOMException extends Error {
  type WorkerGlobalScopeEventMap (line 88) | type WorkerGlobalScopeEventMap = {
  type Console (line 103) | interface Console {
  type BufferSource (line 216) | type BufferSource = ArrayBufferView | ArrayBuffer
  type TypedArray (line 217) | type TypedArray =
  class CompileError (line 230) | class CompileError extends Error {
  class RuntimeError (line 233) | class RuntimeError extends Error {
  type ValueType (line 236) | type ValueType =
  type GlobalDescriptor (line 244) | interface GlobalDescriptor {
  class Global (line 248) | class Global {
  type ImportValue (line 253) | type ImportValue = ExportValue | number
  type ModuleImports (line 254) | type ModuleImports = Record<string, ImportValue>
  type Imports (line 255) | type Imports = Record<string, ModuleImports>
  type ExportValue (line 256) | type ExportValue = Function | Global | Memory | Table
  type Exports (line 257) | type Exports = Record<string, ExportValue>
  class Instance (line 258) | class Instance {
  type MemoryDescriptor (line 262) | interface MemoryDescriptor {
  class Memory (line 267) | class Memory {
  type ImportExportKind (line 272) | type ImportExportKind = 'function' | 'global' | 'memory' | 'table'
  type ModuleExportDescriptor (line 273) | interface ModuleExportDescriptor {
  type ModuleImportDescriptor (line 277) | interface ModuleImportDescriptor {
  type TableKind (line 287) | type TableKind = 'anyfunc' | 'externref'
  type TableDescriptor (line 288) | interface TableDescriptor {
  class Table (line 293) | class Table {
  type ServiceWorkerGlobalScope (line 309) | interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
  type TestController (line 490) | interface TestController {}
  type ExecutionContext (line 491) | interface ExecutionContext<Props = unknown> {
  type ExportedHandlerFetchHandler (line 496) | type ExportedHandlerFetchHandler<Env = unknown, CfHostMetadata = unknown...
  type ExportedHandlerTailHandler (line 501) | type ExportedHandlerTailHandler<Env = unknown> = (
  type ExportedHandlerTraceHandler (line 506) | type ExportedHandlerTraceHandler<Env = unknown> = (
  type ExportedHandlerTailStreamHandler (line 511) | type ExportedHandlerTailStreamHandler<Env = unknown> = (
  type ExportedHandlerScheduledHandler (line 516) | type ExportedHandlerScheduledHandler<Env = unknown> = (
  type ExportedHandlerQueueHandler (line 521) | type ExportedHandlerQueueHandler<Env = unknown, Message = unknown> = (
  type ExportedHandlerTestHandler (line 526) | type ExportedHandlerTestHandler<Env = unknown> = (
  type ExportedHandler (line 531) | interface ExportedHandler<
  type StructuredSerializeOptions (line 545) | interface StructuredSerializeOptions {
  type AlarmInvocationInfo (line 553) | interface AlarmInvocationInfo {
  type Cloudflare (line 557) | interface Cloudflare {
  type DurableObject (line 560) | interface DurableObject {
  type DurableObjectStub (line 575) | type DurableObjectStub<
  type DurableObjectId (line 584) | interface DurableObjectId {
  type DurableObjectJurisdiction (line 609) | type DurableObjectJurisdiction = 'eu' | 'fedramp' | 'fedramp-high'
  type DurableObjectNamespaceNewUniqueIdOptions (line 610) | interface DurableObjectNamespaceNewUniqueIdOptions {
  type DurableObjectLocationHint (line 613) | type DurableObjectLocationHint =
  type DurableObjectRoutingMode (line 623) | type DurableObjectRoutingMode = 'primary-only'
  type DurableObjectNamespaceGetDurableObjectOptions (line 624) | interface DurableObjectNamespaceGetDurableObjectOptions {
  type DurableObjectClass (line 628) | interface DurableObjectClass<
  type DurableObjectState (line 631) | interface DurableObjectState<Props = unknown> {
  type DurableObjectTransaction (line 648) | interface DurableObjectTransaction {
  type DurableObjectStorage (line 677) | interface DurableObjectStorage {
  type DurableObjectListOptions (line 716) | interface DurableObjectListOptions {
  type DurableObjectGetOptions (line 726) | interface DurableObjectGetOptions {
  type DurableObjectGetAlarmOptions (line 730) | interface DurableObjectGetAlarmOptions {
  type DurableObjectPutOptions (line 733) | interface DurableObjectPutOptions {
  type DurableObjectSetAlarmOptions (line 738) | interface DurableObjectSetAlarmOptions {
  class WebSocketRequestResponsePair (line 742) | class WebSocketRequestResponsePair {
  type AnalyticsEngineDataset (line 747) | interface AnalyticsEngineDataset {
  type AnalyticsEngineDataPoint (line 750) | interface AnalyticsEngineDataPoint {
  class Event (line 760) | class Event {
  type EventInit (line 879) | interface EventInit {
  type EventListener (line 884) | type EventListener<EventType extends Event = Event> = (event: EventType)...
  type EventListenerObject (line 885) | interface EventListenerObject<EventType extends Event = Event> {
  type EventListenerOrEventListenerObject (line 888) | type EventListenerOrEventListenerObject<EventType extends Event = Event> =
  class EventTarget (line 896) | class EventTarget<
  type EventTargetEventListenerOptions (line 927) | interface EventTargetEventListenerOptions {
  type EventTargetAddEventListenerOptions (line 930) | interface EventTargetAddEventListenerOptions {
  type EventTargetHandlerObject (line 936) | interface EventTargetHandlerObject {
  class AbortController (line 944) | class AbortController {
  type Scheduler (line 1006) | interface Scheduler {
  type SchedulerWaitOptions (line 1009) | interface SchedulerWaitOptions {
  class CustomEvent (line 1030) | class CustomEvent<T = any> extends Event {
  type CustomEventCustomEventInit (line 1039) | interface CustomEventCustomEventInit {
  class Blob (line 1050) | class Blob {
  type BlobOptions (line 1098) | interface BlobOptions {
  class File (line 1106) | class File extends Blob {
  type FileOptions (line 1125) | interface FileOptions {
  type CacheQueryOptions (line 1162) | interface CacheQueryOptions {
  type CryptoKeyPair (line 1381) | interface CryptoKeyPair {
  type JsonWebKey (line 1385) | interface JsonWebKey {
  type RsaOtherPrimesInfo (line 1405) | interface RsaOtherPrimesInfo {
  type SubtleCryptoDeriveKeyAlgorithm (line 1410) | interface SubtleCryptoDeriveKeyAlgorithm {
  type SubtleCryptoEncryptAlgorithm (line 1418) | interface SubtleCryptoEncryptAlgorithm {
  type SubtleCryptoGenerateKeyAlgorithm (line 1427) | interface SubtleCryptoGenerateKeyAlgorithm {
  type SubtleCryptoHashAlgorithm (line 1435) | interface SubtleCryptoHashAlgorithm {
  type SubtleCryptoImportKeyAlgorithm (line 1438) | interface SubtleCryptoImportKeyAlgorithm {
  type SubtleCryptoSignAlgorithm (line 1445) | interface SubtleCryptoSignAlgorithm {
  type CryptoKeyKeyAlgorithm (line 1451) | interface CryptoKeyKeyAlgorithm {
  type CryptoKeyAesKeyAlgorithm (line 1454) | interface CryptoKeyAesKeyAlgorithm {
  type CryptoKeyHmacKeyAlgorithm (line 1458) | interface CryptoKeyHmacKeyAlgorithm {
  type CryptoKeyRsaKeyAlgorithm (line 1463) | interface CryptoKeyRsaKeyAlgorithm {
  type CryptoKeyEllipticKeyAlgorithm (line 1469) | interface CryptoKeyEllipticKeyAlgorithm {
  type CryptoKeyArbitraryKeyAlgorithm (line 1473) | interface CryptoKeyArbitraryKeyAlgorithm {
  class DigestStream (line 1479) | class DigestStream extends WritableStream<
  class TextDecoder (line 1491) | class TextDecoder {
  class TextEncoder (line 1511) | class TextEncoder {
  type TextDecoderConstructorOptions (line 1527) | interface TextDecoderConstructorOptions {
  type TextDecoderDecodeOptions (line 1531) | interface TextDecoderDecodeOptions {
  type TextEncoderEncodeIntoResult (line 1534) | interface TextEncoderEncodeIntoResult {
  class ErrorEvent (line 1543) | class ErrorEvent extends Event {
  type ErrorEventErrorEventInit (line 1576) | interface ErrorEventErrorEventInit {
  class MessageEvent (line 1588) | class MessageEvent extends Event {
  type MessageEventInit (line 1621) | interface MessageEventInit {
  class FormData (line 1648) | class FormData {
  type ContentOptions (line 1727) | interface ContentOptions {
  class HTMLRewriter (line 1730) | class HTMLRewriter {
  type HTMLRewriterElementContentHandlers (line 1739) | interface HTMLRewriterElementContentHandlers {
  type HTMLRewriterDocumentContentHandlers (line 1744) | interface HTMLRewriterDocumentContentHandlers {
  type Doctype (line 1750) | interface Doctype {
  type Element (line 1755) | interface Element {
  type EndTag (line 1792) | interface EndTag {
  type Comment (line 1804) | interface Comment {
  type Text (line 1812) | interface Text {
  type DocumentEnd (line 1830) | interface DocumentEnd {
  type HeadersInit (line 1853) | type HeadersInit = Headers | Iterable<Iterable<string>> | Record<string,...
  class Headers (line 1859) | class Headers {
  type BodyInit (line 1910) | type BodyInit =
  type Response (line 1953) | interface Response extends Body {
  type ResponseInit (line 2005) | interface ResponseInit {
  type RequestInfo (line 2013) | type RequestInfo<CfHostMetadata = unknown, Cf = CfProperties<CfHostMetad...
  type Request (line 2033) | interface Request<
  type RequestInit (line 2094) | interface RequestInit<Cf = CfProperties> {
  type Service (line 2113) | type Service<
  type Fetcher (line 2126) | type Fetcher<
  type KVNamespaceListKey (line 2135) | interface KVNamespaceListKey<Metadata, Key extends string = string> {
  type KVNamespaceListResult (line 2140) | type KVNamespaceListResult<Metadata, Key extends string = string> =
  type KVNamespace (line 2152) | interface KVNamespace<Key extends string = string> {
  type KVNamespaceListOptions (line 2264) | interface KVNamespaceListOptions {
  type KVNamespaceGetOptions (line 2269) | interface KVNamespaceGetOptions<Type> {
  type KVNamespacePutOptions (line 2273) | interface KVNamespacePutOptions {
  type KVNamespaceGetWithMetadataResult (line 2278) | interface KVNamespaceGetWithMetadataResult<Value, Metadata> {
  type QueueContentType (line 2283) | type QueueContentType = 'text' | 'bytes' | 'json' | 'v8'
  type Queue (line 2284) | interface Queue<Body = unknown> {
  type QueueSendOptions (line 2291) | interface QueueSendOptions {
  type QueueSendBatchOptions (line 2295) | interface QueueSendBatchOptions {
  type MessageSendRequest (line 2298) | interface MessageSendRequest<Body = unknown> {
  type QueueRetryOptions (line 2303) | interface QueueRetryOptions {
  type Message (line 2306) | interface Message<Body = unknown> {
  type QueueEvent (line 2314) | interface QueueEvent<Body = unknown> extends ExtendableEvent {
  type MessageBatch (line 2320) | interface MessageBatch<Body = unknown> {
  type R2Error (line 2326) | interface R2Error extends Error {
  type R2ListOptions (line 2333) | interface R2ListOptions {
  type R2MultipartUpload (line 2382) | interface R2MultipartUpload {
  type R2UploadedPart (line 2393) | interface R2UploadedPart {
  type R2ObjectBody (line 2412) | interface R2ObjectBody extends R2Object {
  type R2Range (line 2421) | type R2Range =
  type R2Conditional (line 2433) | interface R2Conditional {
  type R2GetOptions (line 2440) | interface R2GetOptions {
  type R2PutOptions (line 2445) | interface R2PutOptions {
  type R2MultipartOptions (line 2457) | interface R2MultipartOptions {
  type R2Checksums (line 2463) | interface R2Checksums {
  type R2StringChecksums (line 2471) | interface R2StringChecksums {
  type R2HTTPMetadata (line 2478) | interface R2HTTPMetadata {
  type R2Objects (line 2486) | type R2Objects = {
  type R2UploadPartOptions (line 2498) | interface R2UploadPartOptions {
  type ScheduledController (line 2506) | interface ScheduledController {
  type QueuingStrategy (line 2511) | interface QueuingStrategy<T = any> {
  type UnderlyingSink (line 2515) | interface UnderlyingSink<W = any> {
  type UnderlyingByteSource (line 2525) | interface UnderlyingByteSource {
  type UnderlyingSource (line 2532) | interface UnderlyingSource<R = any> {
  type Transformer (line 2543) | interface Transformer<I = any, O = any> {
  type StreamPipeOptions (line 2559) | interface StreamPipeOptions {
  type ReadableStreamReadResult (line 2582) | type ReadableStreamReadResult<R = any> =
  type ReadableStream (line 2596) | interface ReadableStream<R = any> {
  class ReadableStreamDefaultReader (line 2671) | class ReadableStreamDefaultReader<R = any> {
  class ReadableStreamBYOBReader (line 2693) | class ReadableStreamBYOBReader {
  type ReadableStreamBYOBReaderReadableStreamBYOBReaderReadOptions (line 2714) | interface ReadableStreamBYOBReaderReadableStreamBYOBReaderReadOptions {
  type ReadableStreamGetReaderOptions (line 2717) | interface ReadableStreamGetReaderOptions {
  type ReadableWritablePair (line 2869) | interface ReadableWritablePair<R = any, W = any> {
  class WritableStream (line 2883) | class WritableStream<W = any> {
  class WritableStreamDefaultWriter (line 2918) | class WritableStreamDefaultWriter<W = any> {
  class TransformStream (line 2968) | class TransformStream<I = any, O = any> {
  class FixedLengthStream (line 2987) | class FixedLengthStream extends IdentityTransformStream {
  class IdentityTransformStream (line 2993) | class IdentityTransformStream extends TransformStream<
  type IdentityTransformStreamQueuingStrategy (line 2999) | interface IdentityTransformStreamQueuingStrategy {
  type ReadableStreamValuesOptions (line 3002) | interface ReadableStreamValuesOptions {
  class CompressionStream (line 3010) | class CompressionStream extends TransformStream<
  class DecompressionStream (line 3021) | class DecompressionStream extends TransformStream<
  class TextEncoderStream (line 3032) | class TextEncoderStream extends TransformStream<string, Uint8Array> {
  class TextDecoderStream (line 3041) | class TextDecoderStream extends TransformStream<
  type TextDecoderStreamTextDecoderStreamInit (line 3050) | interface TextDecoderStreamTextDecoderStreamInit {
  class ByteLengthQueuingStrategy (line 3059) | class ByteLengthQueuingStrategy implements QueuingStrategy<ArrayBufferVi...
  class CountQueuingStrategy (line 3075) | class CountQueuingStrategy implements QueuingStrategy {
  type QueuingStrategyInit (line 3086) | interface QueuingStrategyInit {
  type ScriptVersion (line 3094) | interface ScriptVersion {
  type TraceItem (line 3103) | interface TraceItem {
  type TraceItemAlarmEventInfo (line 3133) | interface TraceItemAlarmEventInfo {
  type TraceItemCustomEventInfo (line 3136) | interface TraceItemCustomEventInfo {}
  type TraceItemScheduledEventInfo (line 3137) | interface TraceItemScheduledEventInfo {
  type TraceItemQueueEventInfo (line 3141) | interface TraceItemQueueEventInfo {
  type TraceItemEmailEventInfo (line 3145) | interface TraceItemEmailEventInfo {
  type TraceItemTailEventInfo (line 3150) | interface TraceItemTailEventInfo {
  type TraceItemTailEventInfoTailItem (line 3153) | interface TraceItemTailEventInfoTailItem {
  type TraceItemFetchEventInfo (line 3156) | interface TraceItemFetchEventInfo {
  type TraceItemFetchEventInfoRequest (line 3160) | interface TraceItemFetchEventInfoRequest {
  type TraceItemFetchEventInfoResponse (line 3167) | interface TraceItemFetchEventInfoResponse {
  type TraceItemJsRpcEventInfo (line 3170) | interface TraceItemJsRpcEventInfo {
  type TraceItemHibernatableWebSocketEventInfo (line 3173) | interface TraceItemHibernatableWebSocketEventInfo {
  type TraceItemHibernatableWebSocketEventInfoMessage (line 3179) | interface TraceItemHibernatableWebSocketEventInfoMessage {
  type TraceItemHibernatableWebSocketEventInfoClose (line 3182) | interface TraceItemHibernatableWebSocketEventInfoClose {
  type TraceItemHibernatableWebSocketEventInfoError (line 3187) | interface TraceItemHibernatableWebSocketEventInfoError {
  type TraceLog (line 3190) | interface TraceLog {
  type TraceException (line 3195) | interface TraceException {
  type TraceDiagnosticChannelEvent (line 3201) | interface TraceDiagnosticChannelEvent {
  type TraceMetrics (line 3206) | interface TraceMetrics {
  type UnsafeTraceMetrics (line 3210) | interface UnsafeTraceMetrics {
  class URL (line 3218) | class URL {
  class URLSearchParams (line 3390) | class URLSearchParams {
  class URLPattern (line 3461) | class URLPattern {
  type URLPatternInit (line 3481) | interface URLPatternInit {
  type URLPatternComponentResult (line 3492) | interface URLPatternComponentResult {
  type URLPatternResult (line 3496) | interface URLPatternResult {
  type URLPatternOptions (line 3507) | interface URLPatternOptions {
  class CloseEvent (line 3515) | class CloseEvent extends Event {
  type CloseEventInit (line 3536) | interface CloseEventInit {
  type WebSocketEventMap (line 3541) | type WebSocketEventMap = {
  type WebSocket (line 3569) | interface WebSocket extends EventTarget<WebSocketEventMap> {
  type SqlStorage (line 3616) | interface SqlStorage {
  type SqlStorageValue (line 3626) | type SqlStorageValue = ArrayBuffer | string | number | null
  type Socket (line 3647) | interface Socket {
  type SocketOptions (line 3657) | interface SocketOptions {
  type SocketAddress (line 3662) | interface SocketAddress {
  type TlsOptions (line 3666) | interface TlsOptions {
  type SocketInfo (line 3669) | interface SocketInfo {
  class EventSource (line 3678) | class EventSource extends EventTarget {
  type EventSourceEventSourceInit (line 3721) | interface EventSourceEventSourceInit {
  type Container (line 3725) | interface Container {
  type ContainerStartupOptions (line 3734) | interface ContainerStartupOptions {
  type MessagePortPostMessageOptions (line 3767) | interface MessagePortPostMessageOptions {
  type LoopbackForExport (line 3770) | type LoopbackForExport<
  type LoopbackServiceStub (line 3782) | type LoopbackServiceStub<
  type LoopbackDurableObjectClass (line 3788) | type LoopbackDurableObjectClass<
  type SyncKvStorage (line 3794) | interface SyncKvStorage {
  type SyncKvListOptions (line 3800) | interface SyncKvListOptions {
  type WorkerStub (line 3808) | interface WorkerStub {
  type WorkerStubEntrypointOptions (line 3814) | interface WorkerStubEntrypointOptions {
  type WorkerLoader (line 3817) | interface WorkerLoader {
  type WorkerLoaderModule (line 3823) | interface WorkerLoaderModule {
  type WorkerLoaderWorkerCode (line 3832) | interface WorkerLoaderWorkerCode {
  type AiSearchInternalError (line 3856) | interface AiSearchInternalError extends Error {}
  type AiSearchNotFoundError (line 3857) | interface AiSearchNotFoundError extends Error {}
  type AiSearchNameNotSetError (line 3858) | interface AiSearchNameNotSetError extends Error {}
  type ComparisonFilter (line 3860) | type ComparisonFilter = {
  type CompoundFilter (line 3865) | type CompoundFilter = {
  type AiSearchSearchRequest (line 3870) | type AiSearchSearchRequest = {
  type AiSearchChatCompletionsRequest (line 3904) | type AiSearchChatCompletionsRequest = {
  type AiSearchSearchResponse (line 3937) | type AiSearchSearchResponse = {
  type AiSearchListResponse (line 3958) | type AiSearchListResponse = Array<{
  type AiSearchConfig (line 3969) | type AiSearchConfig = {
  type AiSearchInstance (line 3985) | type AiSearchInstance = {
  type AiImageClassificationInput (line 4033) | type AiImageClassificationInput = {
  type AiImageClassificationOutput (line 4036) | type AiImageClassificationOutput = {
  type AiImageToTextInput (line 4044) | type AiImageToTextInput = {
  type AiImageToTextOutput (line 4058) | type AiImageToTextOutput = {
  type AiImageTextToTextInput (line 4065) | type AiImageTextToTextInput = {
  type AiImageTextToTextOutput (line 4080) | type AiImageTextToTextOutput = {
  type AiMultimodalEmbeddingsInput (line 4087) | type AiMultimodalEmbeddingsInput = {
  type AiIMultimodalEmbeddingsOutput (line 4091) | type AiIMultimodalEmbeddingsOutput = {
  type AiObjectDetectionInput (line 4099) | type AiObjectDetectionInput = {
  type AiObjectDetectionOutput (line 4102) | type AiObjectDetectionOutput = {
  type AiSentenceSimilarityInput (line 4110) | type AiSentenceSimilarityInput = {
  type AiSentenceSimilarityOutput (line 4114) | type AiSentenceSimilarityOutput = number[]
  type AiAutomaticSpeechRecognitionInput (line 4119) | type AiAutomaticSpeechRecognitionInput = {
  type AiAutomaticSpeechRecognitionOutput (line 4122) | type AiAutomaticSpeechRecognitionOutput = {
  type AiSummarizationInput (line 4135) | type AiSummarizationInput = {
  type AiSummarizationOutput (line 4139) | type AiSummarizationOutput = {
  type AiTextClassificationInput (line 4146) | type AiTextClassificationInput = {
  type AiTextClassificationOutput (line 4149) | type AiTextClassificationOutput = {
  type AiTextEmbeddingsInput (line 4157) | type AiTextEmbeddingsInput = {
  type AiTextEmbeddingsOutput (line 4160) | type AiTextEmbeddingsOutput = {
  type RoleScopedChatInput (line 4168) | type RoleScopedChatInput = {
  type AiTextGenerationToolLegacyInput (line 4178) | type AiTextGenerationToolLegacyInput = {
  type AiTextGenerationToolInput (line 4192) | type AiTextGenerationToolInput = {
  type AiTextGenerationFunctionsInput (line 4209) | type AiTextGenerationFunctionsInput = {
  type AiTextGenerationResponseFormat (line 4213) | type AiTextGenerationResponseFormat = {
  type AiTextGenerationInput (line 4217) | type AiTextGenerationInput = {
  type AiTextGenerationToolLegacyOutput (line 4237) | type AiTextGenerationToolLegacyOutput = {
  type AiTextGenerationToolOutput (line 4241) | type AiTextGenerationToolOutput = {
  type UsageTags (line 4249) | type UsageTags = {
  type AiTextGenerationOutput (line 4254) | type AiTextGenerationOutput = {
  type AiTextToSpeechInput (line 4263) | type AiTextToSpeechInput = {
  type AiTextToSpeechOutput (line 4267) | type AiTextToSpeechOutput =
  type AiTextToImageInput (line 4276) | type AiTextToImageInput = {
  type AiTextToImageOutput (line 4289) | type AiTextToImageOutput = ReadableStream<Uint8Array>
  type AiTranslationInput (line 4294) | type AiTranslationInput = {
  type AiTranslationOutput (line 4299) | type AiTranslationOutput = {
  type ResponsesInput (line 4316) | type ResponsesInput = {
  type ResponsesOutput (line 4338) | type ResponsesOutput = {
  type EasyInputMessage (line 4363) | type EasyInputMessage = {
  type ResponsesFunctionTool (line 4368) | type ResponsesFunctionTool = {
  type ResponseIncompleteDetails (line 4377) | type ResponseIncompleteDetails = {
  type ResponsePrompt (line 4380) | type ResponsePrompt = {
  type Reasoning (line 4387) | type Reasoning = {
  type ResponseContent (line 4392) | type ResponseContent =
  type ResponseContentReasoningText (line 4398) | type ResponseContentReasoningText = {
  type ResponseConversationParam (line 4402) | type ResponseConversationParam = {
  type ResponseCreatedEvent (line 4405) | type ResponseCreatedEvent = {
  type ResponseCustomToolCallOutput (line 4410) | type ResponseCustomToolCallOutput = {
  type ResponseError (line 4416) | type ResponseError = {
  type ResponseErrorEvent (line 4438) | type ResponseErrorEvent = {
  type ResponseFailedEvent (line 4445) | type ResponseFailedEvent = {
  type ResponseFormatText (line 4450) | type ResponseFormatText = {
  type ResponseFormatJSONObject (line 4453) | type ResponseFormatJSONObject = {
  type ResponseFormatTextConfig (line 4456) | type ResponseFormatTextConfig =
  type ResponseFormatTextJSONSchemaConfig (line 4460) | type ResponseFormatTextJSONSchemaConfig = {
  type ResponseFunctionCallArgumentsDeltaEvent (line 4469) | type ResponseFunctionCallArgumentsDeltaEvent = {
  type ResponseFunctionCallArgumentsDoneEvent (line 4476) | type ResponseFunctionCallArgumentsDoneEvent = {
  type ResponseFunctionCallOutputItem (line 4484) | type ResponseFunctionCallOutputItem =
  type ResponseFunctionCallOutputItemList (line 4487) | type ResponseFunctionCallOutputItemList = Array<ResponseFunctionCallOutp...
  type ResponseFunctionToolCall (line 4488) | type ResponseFunctionToolCall = {
  type ResponseFunctionToolCallItem (line 4496) | interface ResponseFunctionToolCallItem extends ResponseFunctionToolCall {
  type ResponseFunctionToolCallOutputItem (line 4499) | type ResponseFunctionToolCallOutputItem = {
  type ResponseIncludable (line 4506) | type ResponseIncludable =
  type ResponseIncompleteEvent (line 4509) | type ResponseIncompleteEvent = {
  type ResponseInput (line 4514) | type ResponseInput = Array<ResponseInputItem>
  type ResponseInputContent (line 4515) | type ResponseInputContent = ResponseInputText | ResponseInputImage
  type ResponseInputImage (line 4516) | type ResponseInputImage = {
  type ResponseInputImageContent (line 4524) | type ResponseInputImageContent = {
  type ResponseInputItem (line 4532) | type ResponseInputItem =
  type ResponseInputItemFunctionCallOutput (line 4539) | type ResponseInputItemFunctionCallOutput = {
  type ResponseInputItemMessage (line 4546) | type ResponseInputItemMessage = {
  type ResponseInputMessageContentList (line 4552) | type ResponseInputMessageContentList = Array<ResponseInputContent>
  type ResponseInputMessageItem (line 4553) | type ResponseInputMessageItem = {
  type ResponseInputText (line 4560) | type ResponseInputText = {
  type ResponseInputTextContent (line 4564) | type ResponseInputTextContent = {
  type ResponseItem (line 4568) | type ResponseItem =
  type ResponseOutputItem (line 4573) | type ResponseOutputItem =
  type ResponseOutputItemAddedEvent (line 4577) | type ResponseOutputItemAddedEvent = {
  type ResponseOutputItemDoneEvent (line 4583) | type ResponseOutputItemDoneEvent = {
  type ResponseOutputMessage (line 4589) | type ResponseOutputMessage = {
  type ResponseOutputRefusal (line 4596) | type ResponseOutputRefusal = {
  type ResponseOutputText (line 4600) | type ResponseOutputText = {
  type ResponseReasoningItem (line 4605) | type ResponseReasoningItem = {
  type ResponseReasoningSummaryItem (line 4613) | type ResponseReasoningSummaryItem = {
  type ResponseReasoningContentItem (line 4617) | type ResponseReasoningContentItem = {
  type ResponseReasoningTextDeltaEvent (line 4621) | type ResponseReasoningTextDeltaEvent = {
  type ResponseReasoningTextDoneEvent (line 4629) | type ResponseReasoningTextDoneEvent = {
  type ResponseRefusalDeltaEvent (line 4637) | type ResponseRefusalDeltaEvent = {
  type ResponseRefusalDoneEvent (line 4645) | type ResponseRefusalDoneEvent = {
  type ResponseStatus (line 4653) | type ResponseStatus =
  type ResponseStreamEvent (line 4660) | type ResponseStreamEvent =
  type ResponseCompletedEvent (line 4676) | type ResponseCompletedEvent = {
  type ResponseTextConfig (line 4681) | type ResponseTextConfig = {
  type ResponseTextDeltaEvent (line 4685) | type ResponseTextDeltaEvent = {
  type ResponseTextDoneEvent (line 4694) | type ResponseTextDoneEvent = {
  type Logprob (line 4703) | type Logprob = {
  type TopLogprob (line 4708) | type TopLogprob = {
  type ResponseUsage (line 4712) | type ResponseUsage = {
  type Tool (line 4717) | type Tool = ResponsesFunctionTool
  type ToolChoiceFunction (line 4718) | type ToolChoiceFunction = {
  type ToolChoiceOptions (line 4722) | type ToolChoiceOptions = 'none'
  type ReasoningEffort (line 4723) | type ReasoningEffort = 'minimal' | 'low' | 'medium' | 'high' | null
  type StreamOptions (line 4724) | type StreamOptions = {
  type Ai_Cf_Baai_Bge_Base_En_V1_5_Input (line 4727) | type Ai_Cf_Baai_Bge_Base_En_V1_5_Input =
  type Ai_Cf_Baai_Bge_Base_En_V1_5_Output (line 4747) | type Ai_Cf_Baai_Bge_Base_En_V1_5_Output =
  type Ai_Cf_Baai_Bge_Base_En_V1_5_AsyncResponse (line 4760) | interface Ai_Cf_Baai_Bge_Base_En_V1_5_AsyncResponse {
  type Ai_Cf_Openai_Whisper_Input (line 4770) | type Ai_Cf_Openai_Whisper_Input =
  type Ai_Cf_Openai_Whisper_Output (line 4778) | interface Ai_Cf_Openai_Whisper_Output {
  type Ai_Cf_Meta_M2M100_1_2B_Input (line 4801) | type Ai_Cf_Meta_M2M100_1_2B_Input =
  type Ai_Cf_Meta_M2M100_1_2B_Output (line 4835) | type Ai_Cf_Meta_M2M100_1_2B_Output =
  type Ai_Cf_Meta_M2M100_1_2B_AsyncResponse (line 4843) | interface Ai_Cf_Meta_M2M100_1_2B_AsyncResponse {
  type Ai_Cf_Baai_Bge_Small_En_V1_5_Input (line 4853) | type Ai_Cf_Baai_Bge_Small_En_V1_5_Input =
  type Ai_Cf_Baai_Bge_Small_En_V1_5_Output (line 4873) | type Ai_Cf_Baai_Bge_Small_En_V1_5_Output =
  type Ai_Cf_Baai_Bge_Small_En_V1_5_AsyncResponse (line 4886) | interface Ai_Cf_Baai_Bge_Small_En_V1_5_AsyncResponse {
  type Ai_Cf_Baai_Bge_Large_En_V1_5_Input (line 4896) | type Ai_Cf_Baai_Bge_Large_En_V1_5_Input =
  type Ai_Cf_Baai_Bge_Large_En_V1_5_Output (line 4916) | type Ai_Cf_Baai_Bge_Large_En_V1_5_Output =
  type Ai_Cf_Baai_Bge_Large_En_V1_5_AsyncResponse (line 4929) | interface Ai_Cf_Baai_Bge_Large_En_V1_5_AsyncResponse {
  type Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input (line 4939) | type Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input =
  type Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output (line 4980) | interface Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output {
  type Ai_Cf_Openai_Whisper_Tiny_En_Input (line 4987) | type Ai_Cf_Openai_Whisper_Tiny_En_Input =
  type Ai_Cf_Openai_Whisper_Tiny_En_Output (line 4995) | interface Ai_Cf_Openai_Whisper_Tiny_En_Output {
  type Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input (line 5018) | interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input {
  type Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output (line 5044) | interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output {
  type Ai_Cf_Baai_Bge_M3_Input (line 5124) | type Ai_Cf_Baai_Bge_M3_Input =
  type Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts (line 5136) | interface Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts {
  type Ai_Cf_Baai_Bge_M3_Input_Embedding (line 5155) | interface Ai_Cf_Baai_Bge_M3_Input_Embedding {
  type Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts_1 (line 5162) | interface Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts_1 {
  type Ai_Cf_Baai_Bge_M3_Input_Embedding_1 (line 5181) | interface Ai_Cf_Baai_Bge_M3_Input_Embedding_1 {
  type Ai_Cf_Baai_Bge_M3_Output (line 5188) | type Ai_Cf_Baai_Bge_M3_Output =
  type Ai_Cf_Baai_Bge_M3_Ouput_Query (line 5193) | interface Ai_Cf_Baai_Bge_M3_Ouput_Query {
  type Ai_Cf_Baai_Bge_M3_Output_EmbeddingFor_Contexts (line 5205) | interface Ai_Cf_Baai_Bge_M3_Output_EmbeddingFor_Contexts {
  type Ai_Cf_Baai_Bge_M3_Ouput_Embedding (line 5213) | interface Ai_Cf_Baai_Bge_M3_Ouput_Embedding {
  type Ai_Cf_Baai_Bge_M3_AsyncResponse (line 5224) | interface Ai_Cf_Baai_Bge_M3_AsyncResponse {
  type Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input (line 5234) | interface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input {
  type Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output (line 5244) | interface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output {
  type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input (line 5254) | type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input =
  type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Prompt (line 5257) | interface Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Prompt {
  type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Messages (line 5308) | interface Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Messages {
  type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output (line 5482) | type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output = {
  type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input (line 5505) | type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input =
  type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt (line 5509) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt {
  type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode (line 5560) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode {
  type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages (line 5564) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages {
  type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_1 (line 5714) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_1 {
  type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Async_Batch (line 5718) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Async_Batch {
  type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_2 (line 5763) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_2 {
  type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output (line 5767) | type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output =
  type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_AsyncResponse (line 5806) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_AsyncResponse {
  type Ai_Cf_Meta_Llama_Guard_3_8B_Input (line 5816) | interface Ai_Cf_Meta_Llama_Guard_3_8B_Input {
  type Ai_Cf_Meta_Llama_Guard_3_8B_Output (line 5848) | interface Ai_Cf_Meta_Llama_Guard_3_8B_Output {
  type Ai_Cf_Baai_Bge_Reranker_Base_Input (line 5883) | interface Ai_Cf_Baai_Bge_Reranker_Base_Input {
  type Ai_Cf_Baai_Bge_Reranker_Base_Output (line 5901) | interface Ai_Cf_Baai_Bge_Reranker_Base_Output {
  type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input (line 5917) | type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input =
  type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Prompt (line 5920) | interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Prompt {
  type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode (line 5971) | interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode {
  type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Messages (line 5975) | interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Messages {
  type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode_1 (line 6125) | interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode_1 {
  type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output (line 6129) | type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output = {
  type Ai_Cf_Qwen_Qwq_32B_Input (line 6169) | type Ai_Cf_Qwen_Qwq_32B_Input =
  type Ai_Cf_Qwen_Qwq_32B_Prompt (line 6172) | interface Ai_Cf_Qwen_Qwq_32B_Prompt {
  type Ai_Cf_Qwen_Qwq_32B_Messages (line 6222) | interface Ai_Cf_Qwen_Qwq_32B_Messages {
  type Ai_Cf_Qwen_Qwq_32B_Output (line 6403) | type Ai_Cf_Qwen_Qwq_32B_Output = {
  type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input (line 6443) | type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input =
  type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Prompt (line 6446) | interface Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Prompt {
  type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Messages (line 6496) | interface Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Messages {
  type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output (line 6677) | type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output = {
  type Ai_Cf_Google_Gemma_3_12B_It_Input (line 6717) | type Ai_Cf_Google_Gemma_3_12B_It_Input =
  type Ai_Cf_Google_Gemma_3_12B_It_Prompt (line 6720) | interface Ai_Cf_Google_Gemma_3_12B_It_Prompt {
  type Ai_Cf_Google_Gemma_3_12B_It_Messages (line 6770) | interface Ai_Cf_Google_Gemma_3_12B_It_Messages {
  type Ai_Cf_Google_Gemma_3_12B_It_Output (line 6934) | type Ai_Cf_Google_Gemma_3_12B_It_Output = {
  type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input (line 6974) | type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input =
  type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt (line 6978) | interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt {
  type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode (line 7029) | interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode {
  type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages (line 7033) | interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages {
  type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Async_Batch (line 7215) | interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Async_Batch {
  type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt_Inner (line 7221) | interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt_Inner {
  type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages_Inner (line 7272) | interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages_Inner {
  type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output (line 7454) | type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output = {
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Input (line 7507) | type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Input =
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt (line 7511) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt {
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode (line 7562) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode {
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages (line 7566) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages {
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_1 (line 7716) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_1 {
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Async_Batch (line 7720) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Async_Batch {
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt_1 (line 7726) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt_1 {
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_2 (line 7777) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_2 {
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages_1 (line 7781) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages_1 {
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_3 (line 7931) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_3 {
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Output (line 7935) | type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Output =
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Chat_Completion_Response (line 7940) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Chat_Completion_Response {
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Text_Completion_Response (line 8040) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Text_Completion_Response {
  type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_AsyncResponse (line 8104) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_AsyncResponse {
  type Ai_Cf_Deepgram_Nova_3_Input (line 8114) | interface Ai_Cf_Deepgram_Nova_3_Input {
  type Ai_Cf_Deepgram_Nova_3_Output (line 8268) | interface Ai_Cf_Deepgram_Nova_3_Output {
  type Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Input (line 8305) | interface Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Input {
  type Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Output (line 8314) | interface Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Output {
  type Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Input (line 8322) | type Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Input =
  type Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output (line 8346) | interface Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output {
  type Ai_Cf_Leonardo_Phoenix_1_0_Input (line 8368) | interface Ai_Cf_Leonardo_Phoenix_1_0_Input {
  type Ai_Cf_Leonardo_Phoenix_1_0_Output (line 8401) | type Ai_Cf_Leonardo_Phoenix_1_0_Output = string
  type Ai_Cf_Leonardo_Lucid_Origin_Input (line 8406) | interface Ai_Cf_Leonardo_Lucid_Origin_Input {
  type Ai_Cf_Leonardo_Lucid_Origin_Output (line 8436) | interface Ai_Cf_Leonardo_Lucid_Origin_Output {
  type Ai_Cf_Deepgram_Aura_1_Input (line 8446) | interface Ai_Cf_Deepgram_Aura_1_Input {
  type Ai_Cf_Deepgram_Aura_1_Output (line 8487) | type Ai_Cf_Deepgram_Aura_1_Output = string
  type Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Input (line 8492) | interface Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Input {
  type Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Output (line 8536) | interface Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Output {
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Input (line 8546) | type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Input =
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt (line 8550) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt {
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode (line 8601) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode {
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages (line 8605) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages {
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_1 (line 8755) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_1 {
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Async_Batch (line 8759) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Async_Batch {
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt_1 (line 8765) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt_1 {
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_2 (line 8816) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_2 {
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages_1 (line 8820) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages_1 {
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_3 (line 8970) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_3 {
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Output (line 8974) | type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Output =
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Chat_Completion_Response (line 8979) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Chat_Completion_Res...
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Text_Completion_Response (line 9079) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Text_Completion_Res...
  type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_AsyncResponse (line 9143) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_AsyncResponse {
  type Ai_Cf_Pfnet_Plamo_Embedding_1B_Input (line 9153) | interface Ai_Cf_Pfnet_Plamo_Embedding_1B_Input {
  type Ai_Cf_Pfnet_Plamo_Embedding_1B_Output (line 9159) | interface Ai_Cf_Pfnet_Plamo_Embedding_1B_Output {
  type Ai_Cf_Deepgram_Flux_Input (line 9176) | interface Ai_Cf_Deepgram_Flux_Input {
  type Ai_Cf_Deepgram_Flux_Output (line 9213) | interface Ai_Cf_Deepgram_Flux_Output {
  type Ai_Cf_Deepgram_Aura_2_En_Input (line 9269) | interface Ai_Cf_Deepgram_Aura_2_En_Input {
  type Ai_Cf_Deepgram_Aura_2_En_Output (line 9338) | type Ai_Cf_Deepgram_Aura_2_En_Output = string
  type Ai_Cf_Deepgram_Aura_2_Es_Input (line 9343) | interface Ai_Cf_Deepgram_Aura_2_Es_Input {
  type Ai_Cf_Deepgram_Aura_2_Es_Output (line 9382) | type Ai_Cf_Deepgram_Aura_2_Es_Output = string
  type AiModels (line 9387) | interface AiModels {
  type AiOptions (line 9473) | type AiOptions = {
  type AiModelsSearchParams (line 9498) | type AiModelsSearchParams = {
  type AiModelsSearchObject (line 9507) | type AiModelsSearchObject = {
  type InferenceUpstreamError (line 9523) | interface InferenceUpstreamError extends Error {}
  type AiInternalError (line 9524) | interface AiInternalError extends Error {}
  type AiModelListType (line 9525) | type AiModelListType = Record<string, any>
  type GatewayRetries (line 9606) | type GatewayRetries = {
  type GatewayOptions (line 9611) | type GatewayOptions = {
  type UniversalGatewayOptions (line 9622) | type UniversalGatewayOptions = Exclude<GatewayOptions, 'id'> & {
  type AiGatewayPatchLog (line 9628) | type AiGatewayPatchLog = {
  type AiGatewayLog (line 9633) | type AiGatewayLog = {
  type AIGatewayProviders (line 9660) | type AIGatewayProviders =
  type AIGatewayHeaders (line 9681) | type AIGatewayHeaders = {
  type AIGatewayUniversalRequest (line 9707) | type AIGatewayUniversalRequest = {
  type AiGatewayInternalError (line 9713) | interface AiGatewayInternalError extends Error {}
  type AiGatewayLogNotFound (line 9714) | interface AiGatewayLogNotFound extends Error {}
  type AutoRAGInternalError (line 9731) | interface AutoRAGInternalError extends Error {}
  type AutoRAGNotFoundError (line 9736) | interface AutoRAGNotFoundError extends Error {}
  type AutoRAGUnauthorizedError (line 9740) | interface AutoRAGUnauthorizedError extends Error {}
  type AutoRAGNameNotSetError (line 9745) | interface AutoRAGNameNotSetError extends Error {}
  type AutoRagSearchRequest (line 9751) | type AutoRagSearchRequest = {
  type AutoRagAiSearchRequest (line 9770) | type AutoRagAiSearchRequest = AutoRagSearchRequest & {
  type AutoRagAiSearchRequestStreaming (line 9779) | type AutoRagAiSearchRequestStreaming = Omit<
  type AutoRagSearchResponse (line 9790) | type AutoRagSearchResponse = {
  type AutoRagListResponse (line 9811) | type AutoRagListResponse = {
  type AutoRagAiSearchResponse (line 9824) | type AutoRagAiSearchResponse = AutoRagSearchResponse & {
  type BasicImageTransformations (line 9869) | interface BasicImageTransformations {
  type BasicImageTransformationsGravityCoordinates (line 9945) | interface BasicImageTransformationsGravityCoordinates {
  type RequestInitCfProperties (line 9959) | interface RequestInitCfProperties extends Record<string, unknown> {
  type RequestInitCfPropertiesImageDraw (line 10010) | interface RequestInitCfPropertiesImageDraw extends BasicImageTransformat...
  type RequestInitCfPropertiesImage (line 10047) | interface RequestInitCfPropertiesImage extends BasicImageTransformations {
  type RequestInitCfPropertiesImageMinify (line 10212) | interface RequestInitCfPropertiesImageMinify {
  type RequestInitCfPropertiesR2 (line 10217) | interface RequestInitCfPropertiesR2 {
  type IncomingRequestCfProperties (line 10226) | type IncomingRequestCfProperties<HostMetadata = unknown> =
  type IncomingRequestCfPropertiesBase (line 10232) | interface IncomingRequestCfPropertiesBase extends Record<string, unknown> {
  type IncomingRequestCfPropertiesBotManagementBase (line 10310) | interface IncomingRequestCfPropertiesBotManagementBase {
  type IncomingRequestCfPropertiesBotManagement (line 10337) | interface IncomingRequestCfPropertiesBotManagement {
  type IncomingRequestCfPropertiesBotManagementEnterprise (line 10349) | interface IncomingRequestCfPropertiesBotManagementEnterprise extends Inc...
  type IncomingRequestCfPropertiesCloudflareForSaaSEnterprise (line 10361) | interface IncomingRequestCfPropertiesCloudflareForSaaSEnterprise<HostMet...
  type IncomingRequestCfPropertiesCloudflareAccessOrApiShield (line 10370) | interface IncomingRequestCfPropertiesCloudflareAccessOrApiShield {
  type IncomingRequestCfPropertiesExportedAuthenticatorMetadata (line 10392) | interface IncomingRequestCfPropertiesExportedAuthenticatorMetadata {
  type IncomingRequestCfPropertiesGeographicInformation (line 10421) | interface IncomingRequestCfPropertiesGeographicInformation {
  type IncomingRequestCfPropertiesTLSClientAuth (line 10498) | interface IncomingRequestCfPropertiesTLSClientAuth {
  type IncomingRequestCfPropertiesTLSClientAuthPlaceholder (line 10591) | interface IncomingRequestCfPropertiesTLSClientAuthPlaceholder {
  type CertVerificationStatus (line 10611) | type CertVerificationStatus =
  type IncomingRequestCfPropertiesEdgeRequestKeepAliveStatus (line 10629) | type IncomingRequestCfPropertiesEdgeRequestKeepAliveStatus =
  type Iso3166Alpha2Code (line 10637) | type Iso3166Alpha2Code =
  type ContinentCode (line 10888) | type ContinentCode = 'AF' | 'AN' | 'AS' | 'EU' | 'NA' | 'OC' | 'SA'
  type CfProperties (line 10889) | type CfProperties<HostMetadata = unknown> =
  type D1Meta (line 10892) | interface D1Meta {
  type D1Response (line 10924) | interface D1Response {
  type D1Result (line 10929) | type D1Result<T = unknown> = D1Response & {
  type D1ExecResult (line 10932) | interface D1ExecResult {
  type D1SessionConstraint (line 10936) | type D1SessionConstraint =
  type D1SessionBookmark (line 10945) | type D1SessionBookmark = string
  type Disposable (line 10991) | interface Disposable {}
  type EmailSendResult (line 10995) | interface EmailSendResult {
  type EmailMessage (line 11004) | interface EmailMessage {
  type ForwardableEmailMessage (line 11017) | interface ForwardableEmailMessage extends EmailMessage {
  type EmailAttachment (line 11051) | type EmailAttachment =
  type EmailAddress (line 11067) | interface EmailAddress {
  type SendEmail (line 11074) | interface SendEmail {
  type EmailExportedHandler (line 11092) | type EmailExportedHandler<Env = unknown> = (
  type HelloWorldBinding (line 11107) | interface HelloWorldBinding {
  type Hyperdrive (line 11120) | interface Hyperdrive {
  type ImageInfoResponse (line 11170) | type ImageInfoResponse =
  type ImageTransform (line 11180) | type ImageTransform = {
  type ImageDrawOptions (line 11237) | type ImageDrawOptions = {
  type ImageInputOptions (line 11245) | type ImageInputOptions = {
  type ImageOutputOptions (line 11248) | type ImageOutputOptions = {
  type ImagesBinding (line 11261) | interface ImagesBinding {
  type ImageTransformer (line 11281) | interface ImageTransformer {
  type ImageTransformationOutputOptions (line 11305) | type ImageTransformationOutputOptions = {
  type ImageTransformationResult (line 11308) | interface ImageTransformationResult {
  type ImagesError (line 11322) | interface ImagesError extends Error {
  type MediaBinding (line 11331) | interface MediaBinding {
  type MediaTransformer (line 11343) | interface MediaTransformer {
  type MediaTransformationGenerator (line 11363) | interface MediaTransformationGenerator {
  type MediaTransformationResult (line 11375) | interface MediaTransformationResult {
  type MediaTransformationInputOptions (line 11396) | type MediaTransformationInputOptions = {
  type MediaTransformationOutputOptions (line 11408) | type MediaTransformationOutputOptions = {
  type MediaError (line 11436) | interface MediaError extends Error {
  type NodeStyleServer (line 11442) | interface NodeStyleServer {
  type Params (line 11452) | type Params<P extends string = any> = Record<P, string | string[]>
  type EventContext (line 11453) | type EventContext<Env, P extends string, Data> = {
  type PagesFunction (line 11467) | type PagesFunction<
  type EventPluginContext (line 11472) | type EventPluginContext<Env, P extends string, Data, PluginArgs> = {
  type PagesPluginFunction (line 11487) | type PagesPluginFunction<
  type PipelineRecord (line 11519) | type PipelineRecord = Record<string, unknown>
  type PipelineBatchMetadata (line 11520) | type PipelineBatchMetadata = {
  type Pipeline (line 11524) | interface Pipeline<T extends PipelineRecord = PipelineRecord> {
  type PubSubMessage (line 11537) | interface PubSubMessage {
  type JsonWebKeyWithKid (line 11563) | interface JsonWebKeyWithKid extends JsonWebKey {
  type RateLimitOptions (line 11567) | interface RateLimitOptions {
  type RateLimitOutcome (line 11570) | interface RateLimitOutcome {
  type RateLimit (line 11573) | interface RateLimit {
  type RpcTargetBranded (line 11594) | interface RpcTargetBranded {
  type WorkerEntrypointBranded (line 11597) | interface WorkerEntrypointBranded {
  type DurableObjectBranded (line 11600) | interface DurableObjectBranded {
  type WorkflowEntrypointBranded (line 11603) | interface WorkflowEntrypointBranded {
  type EntrypointBranded (line 11606) | type EntrypointBranded =
  type Stubable (line 11611) | type Stubable = RpcTargetBranded | ((...args: any[]) => any)
  type Serializable (line 11616) | type Serializable<T> =
  type StubBase (line 11635) | interface StubBase<T extends Stubable> extends Disposable {
  type Stub (line 11639) | type Stub<T extends Stubable> = Provider<T> & StubBase<T>
  type BaseType (line 11641) | type BaseType =
  type Stubify (line 11662) | type Stubify<T> = T extends Stubable ? Stub<T> : T extends Map<infer K, ...
  type Unstubify (line 11671) | type Unstubify<T> = T extends StubBase<infer V> ? V : T extends Map<infe...
  type UnstubifyAll (line 11676) | type UnstubifyAll<A extends any[]> = {
  type MaybeProvider (line 11681) | type MaybeProvider<T> = T extends object ? Provider<T> : unknown
  type MaybeDisposable (line 11682) | type MaybeDisposable<T> = T extends object ? Disposable : unknown
  type Result (line 11691) | type Result<R> = R extends Stubable ? Promise<Stub<R>> & Provider<R> : R...
  type MethodOrProperty (line 11697) | type MethodOrProperty<V> = V extends (...args: infer P) => infer R
  type MaybeCallableProvider (line 11702) | type MaybeCallableProvider<T> = T extends (...args: any[]) => any
  type Provider (line 11708) | type Provider<
  type Env (line 11726) | interface Env {}
  type GlobalProps (line 11745) | interface GlobalProps {}
  type GlobalProp (line 11748) | type GlobalProp<K extends string, Default> = K extends keyof GlobalProps
  type MainModule (line 11753) | type MainModule = GlobalProp<'mainModule', {}>
  type Exports (line 11755) | type Exports = {
  type RpcStub (line 11769) | type RpcStub<T extends Rpc.Stubable> = Rpc.Stub<T>
  type WorkflowDurationLabel (line 11818) | type WorkflowDurationLabel =
  type WorkflowSleepDuration (line 11826) | type WorkflowSleepDuration =
  type WorkflowDelayDuration (line 11829) | type WorkflowDelayDuration = WorkflowSleepDuration
  type WorkflowTimeoutDuration (line 11830) | type WorkflowTimeoutDuration = WorkflowSleepDuration
  type WorkflowRetentionDuration (line 11831) | type WorkflowRetentionDuration = WorkflowSleepDuration
  type WorkflowBackoff (line 11832) | type WorkflowBackoff = 'constant' | 'linear' | 'exponential'
  type WorkflowStepConfig (line 11833) | type WorkflowStepConfig = {
  type WorkflowEvent (line 11841) | type WorkflowEvent<T> = {
  type WorkflowStepEvent (line 11846) | type WorkflowStepEvent<T> = {
  type WorkflowInstanceStatus (line 11871) | type WorkflowInstanceStatus =
  type SecretsStoreSecret (line 11907) | interface SecretsStoreSecret {
  type MarkdownDocument (line 11921) | type MarkdownDocument = {
  type ConversionResponse (line 11925) | type ConversionResponse =
  type ImageConversionOptions (line 11941) | type ImageConversionOptions = {
  type EmbeddedImageConversionOptions (line 11944) | type EmbeddedImageConversionOptions = ImageConversionOptions & {
  type ConversionOptions (line 11948) | type ConversionOptions = {
  type ConversionRequestOptions (line 11964) | type ConversionRequestOptions = {
  type SupportedFileFormat (line 11969) | type SupportedFileFormat = {
  type Header (line 11985) | interface Header {
  type FetchEventInfo (line 11989) | interface FetchEventInfo {
  type JsRpcEventInfo (line 11996) | interface JsRpcEventInfo {
  type ScheduledEventInfo (line 11999) | interface ScheduledEventInfo {
  type AlarmEventInfo (line 12004) | interface AlarmEventInfo {
  type QueueEventInfo (line 12008) | interface QueueEventInfo {
  type EmailEventInfo (line 12013) | interface EmailEventInfo {
  type TraceEventInfo (line 12019) | interface TraceEventInfo {
  type HibernatableWebSocketEventInfoMessage (line 12023) | interface HibernatableWebSocketEventInfoMessage {
  type HibernatableWebSocketEventInfoError (line 12026) | interface HibernatableWebSocketEventInfoError {
  type HibernatableWebSocketEventInfoClose (line 12029) | interface HibernatableWebSocketEventInfoClose {
  type HibernatableWebSocketEventInfo (line 12034) | interface HibernatableWebSocketEventInfo {
  type CustomEventInfo (line 12041) | interface CustomEventInfo {
  type FetchResponseInfo (line 12044) | interface FetchResponseInfo {
  type EventOutcome (line 12048) | type EventOutcome =
  type ScriptVersion (line 12060) | interface ScriptVersion {
  type Onset (line 12065) | interface Onset {
  type Outcome (line 12087) | interface Outcome {
  type SpanOpen (line 12093) | interface SpanOpen {
  type SpanClose (line 12100) | interface SpanClose {
  type DiagnosticChannelEvent (line 12104) | interface DiagnosticChannelEvent {
  type Exception (line 12109) | interface Exception {
  type Log (line 12115) | interface Log {
  type DroppedEventsDiagnostic (line 12120) | interface DroppedEventsDiagnostic {
  type StreamDiagnostic (line 12124) | interface StreamDiagnostic {
  type Return (line 12133) | interface Return {
  type Attribute (line 12137) | interface Attribute {
  type Attributes (line 12149) | interface Attributes {
  type EventType (line 12153) | type EventType =
  type SpanContext (line 12165) | interface SpanContext {
  type TailEvent (line 12180) | interface TailEvent<Event extends EventType> {
  type TailEventHandler (line 12190) | type TailEventHandler<Event extends EventType = EventType> = (
  type TailEventHandlerObject (line 12193) | type TailEventHandlerObject = {
  type TailEventHandlerType (line 12203) | type TailEventHandlerType = TailEventHandler | TailEventHandlerObject
  type VectorizeVectorMetadataValue (line 12211) | type VectorizeVectorMetadataValue = string | number | boolean | string[]
  type VectorizeVectorMetadata (line 12215) | type VectorizeVectorMetadata =
  type VectorFloatArray (line 12218) | type VectorFloatArray = Float32Array | Float64Array
  type VectorizeError (line 12219) | interface VectorizeError {
  type VectorizeVectorMetadataFilterOp (line 12228) | type VectorizeVectorMetadataFilterOp =
  type VectorizeVectorMetadataFilterCollectionOp (line 12235) | type VectorizeVectorMetadataFilterCollectionOp = '$in' | '$nin'
  type VectorizeVectorMetadataFilter (line 12239) | type VectorizeVectorMetadataFilter = {
  type VectorizeDistanceMetric (line 12260) | type VectorizeDistanceMetric = 'euclidean' | 'cosine' | 'dot-product'
  type VectorizeMetadataRetrievalLevel (line 12270) | type VectorizeMetadataRetrievalLevel = 'all' | 'indexed' | 'none'
  type VectorizeQueryOptions (line 12271) | interface VectorizeQueryOptions {
  type VectorizeIndexConfig (line 12281) | type VectorizeIndexConfig =
  type VectorizeIndexDetails (line 12295) | interface VectorizeIndexDetails {
  type VectorizeIndexInfo (line 12310) | interface VectorizeIndexInfo {
  type VectorizeVector (line 12323) | interface VectorizeVector {
  type VectorizeMatch (line 12336) | type VectorizeMatch = Pick<Partial<VectorizeVector>, 'values'> &
  type VectorizeMatches (line 12344) | interface VectorizeMatches {
  type VectorizeVectorMutation (line 12355) | interface VectorizeVectorMutation {
  type VectorizeAsyncMutation (line 12365) | interface VectorizeAsyncMutation {
  type WorkerVersionMetadata (line 12476) | type WorkerVersionMetadata = {
  type DynamicDispatchLimits (line 12484) | interface DynamicDispatchLimits {
  type DynamicDispatchOptions (line 12494) | interface DynamicDispatchOptions {
  type DispatchNamespace (line 12506) | interface DispatchNamespace {
  class NonRetryableError (line 12527) | class NonRetryableError extends Error {
  type WorkflowDurationLabel (line 12556) | type WorkflowDurationLabel =
  type WorkflowSleepDuration (line 12564) | type WorkflowSleepDuration =
  type WorkflowRetentionDuration (line 12567) | type WorkflowRetentionDuration = WorkflowSleepDuration
  type WorkflowInstanceCreateOptions (line 12568) | interface WorkflowInstanceCreateOptions<PARAMS = unknown> {
  type InstanceStatus (line 12586) | type InstanceStatus = {
  type WorkflowError (line 12603) | interface WorkflowError {

FILE: apps/mobile/app.config.ts
  constant IS_DEV (line 9) | const IS_DEV = process.env.APP_VARIANT === 'development'
  constant IS_PREVIEW (line 10) | const IS_PREVIEW = process.env.APP_VARIANT === 'preview'

FILE: apps/mobile/babel.config.js
  method logEvent (line 17) | logEvent(filename, event) {

FILE: apps/mobile/drizzle/0000_productive_joystick.sql
  type `artists` (line 1) | CREATE TABLE `artists` (
  type `playlist_tracks` (line 9) | CREATE TABLE `playlist_tracks` (
  type `playlists` (line 18) | CREATE TABLE `playlists` (
  type `search_history` (line 31) | CREATE TABLE `search_history` (
  type `query_unq` (line 37) | CREATE UNIQUE INDEX `query_unq` ON `search_history` (`query`)
  type `tracks` (line 38) | CREATE TABLE `tracks` (

FILE: apps/mobile/drizzle/0002_groovy_maximus.sql
  type `bilibili_metadata` (line 1) | CREATE TABLE `bilibili_metadata` (
  type `local_metadata` (line 10) | CREATE TABLE `local_metadata` (
  type `__new_artists` (line 18) | CREATE TABLE `__new_artists` (
  type `source_remote_id_unq` (line 32) | CREATE UNIQUE INDEX `source_remote_id_unq` ON `artists` (`source`,`remot...
  type `__new_playlists` (line 33) | CREATE TABLE `__new_playlists` (

FILE: apps/mobile/drizzle/0004_smiling_beast.sql
  type `bilibili_metadata_bvid_cid_idx` (line 1) | CREATE INDEX `bilibili_metadata_bvid_cid_idx` ON `bilibili_metadata` (`b...

FILE: apps/mobile/drizzle/0005_spotty_exiles.sql
  type `__new_playlist_tracks` (line 2) | CREATE TABLE `__new_playlist_tracks` (
  type `playlist_tracks_playlist_idx` (line 16) | CREATE INDEX `playlist_tracks_playlist_idx` ON `playlist_tracks` (`playl...
  type `playlist_tracks_track_idx` (line 17) | CREATE INDEX `playlist_tracks_track_idx` ON `playlist_tracks` (`track_id`)
  type `__new_tracks` (line 18) | CREATE TABLE `__new_tracks` (
  type `tracks_unique_key_unique` (line 34) | CREATE UNIQUE INDEX `tracks_unique_key_unique` ON `tracks` (`unique_key`)
  type `tracks_artist_idx` (line 35) | CREATE INDEX `tracks_artist_idx` ON `tracks` (`artist_id`)
  type `tracks_title_idx` (line 36) | CREATE INDEX `tracks_title_idx` ON `tracks` (`title`)
  type `tracks_source_idx` (line 37) | CREATE INDEX `tracks_source_idx` ON `tracks` (`source`)
  type `artists_name_idx` (line 38) | CREATE INDEX `artists_name_idx` ON `artists` (`name`)
  type `playlists_title_idx` (line 39) | CREATE INDEX `playlists_title_idx` ON `playlists` (`title`)
  type `playlists_type_idx` (line 40) | CREATE INDEX `playlists_type_idx` ON `playlists` (`type`)
  type `playlists_author_idx` (line 41) | CREATE INDEX `playlists_author_idx` ON `playlists` (`author_id`)

FILE: apps/mobile/drizzle/0009_lethal_marten_broadcloak.sql
  type `__new_artists` (line 2) | CREATE TABLE `__new_artists` (
  type `source_remote_id_unq` (line 22) | CREATE UNIQUE INDEX `source_remote_id_unq` ON `artists` (`source`,`remot...
  type `local_artist_unq` (line 23) | CREATE UNIQUE INDEX `local_artist_unq` ON `artists` (`name`) WHERE sourc...
  type `artists_name_idx` (line 24) | CREATE INDEX `artists_name_idx` ON `artists` (`name`)

FILE: apps/mobile/drizzle/0011_grey_echo.sql
  type `track_downloads` (line 1) | CREATE TABLE `track_downloads` (
  type `track_downloads_track_idx` (line 9) | CREATE INDEX `track_downloads_track_idx` ON `track_downloads` (`track_id`)

FILE: apps/mobile/drizzle/0013_jittery_randall.sql
  type `playlist_tracks_sort_key_idx` (line 2) | CREATE INDEX `playlist_tracks_sort_key_idx` ON `playlist_tracks` (`playl...

FILE: apps/mobile/drizzle/0015_flippant_skaar.sql
  type `playlist_sync_queue` (line 1) | CREATE TABLE `playlist_sync_queue` (

FILE: apps/mobile/drizzle/0017_rare_lifeguard.sql
  type `playlist_sync_queue_status_idx` (line 1) | CREATE INDEX `playlist_sync_queue_status_idx` ON `playlist_sync_queue` (...
  type `playlist_sync_queue_playlist_id_idx` (line 2) | CREATE INDEX `playlist_sync_queue_playlist_id_idx` ON `playlist_sync_que...
  type `playlists_share_id_idx` (line 3) | CREATE INDEX `playlists_share_id_idx` ON `playlists` (`share_id`)

FILE: apps/mobile/drizzle/0018_green_dracula.sql
  type `play_history` (line 1) | CREATE TABLE `play_history` (
  type `play_history_track_idx` (line 11) | CREATE INDEX `play_history_track_idx` ON `play_history` (`track_id`)
  type `play_history_start_time_idx` (line 12) | CREATE INDEX `play_history_start_time_idx` ON `play_history` (`start_time`)

FILE: apps/mobile/drizzle/0019_icy_mandarin.sql
  type `dynamic_playlist_sources` (line 1) | CREATE TABLE `dynamic_playlist_sources` (
  type `dynamic_playlist_sources_playlist_idx` (line 11) | CREATE INDEX `dynamic_playlist_sources_playlist_idx` ON `dynamic_playlis...
  type `dynamic_playlist_sources_source_idx` (line 12) | CREATE INDEX `dynamic_playlist_sources_source_idx` ON `dynamic_playlist_...

FILE: apps/mobile/drizzle/0020_ambitious_sheva_callister.sql
  type `dynamic_playlist_sources_playlist_position_idx` (line 1) | CREATE INDEX `dynamic_playlist_sources_playlist_position_idx` ON `dynami...

FILE: apps/mobile/expo-plugins/withAndroidGradleProperties.js
  constant GRADLE_XMX (line 3) | const GRADLE_XMX = process.env.GRADLE_XMX || '4g'
  constant KOTLIN_XMX (line 4) | const KOTLIN_XMX = process.env.KOTLIN_XMX || '2g'
  constant WORKERS_MAX (line 5) | const WORKERS_MAX =

FILE: apps/mobile/src/app/(tabs)/_layout.tsx
  type nonNullableIcon (line 23) | interface nonNullableIcon {
  function TabLayout (line 32) | function TabLayout() {

FILE: apps/mobile/src/app/(tabs)/index.tsx
  constant SEARCH_HISTORY_KEY (line 59) | const SEARCH_HISTORY_KEY = 'bilibili_search_history'
  constant MAX_SEARCH_HISTORY (line 60) | const MAX_SEARCH_HISTORY = 10
  function HomePage (line 71) | function HomePage() {

FILE: apps/mobile/src/app/(tabs)/library/[tab].tsx
  type Tabs (line 30) | enum Tabs {
  function Library (line 37) | function Library() {

FILE: apps/mobile/src/app/(tabs)/settings/index.tsx
  function SettingsPage (line 19) | function SettingsPage() {

FILE: apps/mobile/src/app/+native-intent.ts
  function redirectSystemPath (line 3) | function redirectSystemPath({

FILE: apps/mobile/src/app/_layout.tsx
  function onAppStateChange (line 50) | function onAppStateChange(status: AppStateStatus) {

FILE: apps/mobile/src/app/comments/[bvid].tsx
  function CommentsPage (line 30) | function CommentsPage() {

FILE: apps/mobile/src/app/comments/reply.tsx
  function ReplyCommentsPage (line 26) | function ReplyCommentsPage() {

FILE: apps/mobile/src/app/download.tsx
  function DownloadPage (line 24) | function DownloadPage() {

FILE: apps/mobile/src/app/downloaded.tsx
  constant PUBLIC_MUSIC_EXPORT_URI (line 53) | const PUBLIC_MUSIC_EXPORT_URI = 'orpheus://public-music'
  type DownloadedItemExtraData (line 55) | interface DownloadedItemExtraData {
  function renderDownloadedItem (line 64) | function renderDownloadedItem({
  function DownloadedItem (line 87) | function DownloadedItem({
  function DownloadedPage (line 224) | function DownloadedPage() {

FILE: apps/mobile/src/app/history/[date].tsx
  type HistoryItemData (line 21) | interface HistoryItemData {
  function DateHistoryPage (line 55) | function DateHistoryPage() {

FILE: apps/mobile/src/app/history/overall.tsx
  type HistoryItemData (line 23) | interface HistoryItemData {
  function OverallHistoryPage (line 57) | function OverallHistoryPage() {

FILE: apps/mobile/src/app/modal.tsx
  function ModalHost (line 10) | function ModalHost() {

FILE: apps/mobile/src/app/player.tsx
  type PageScrollEvent (line 48) | interface PageScrollEvent {
  function usePageScrollHandler (line 53) | function usePageScrollHandler(
  function PlayerPage (line 77) | function PlayerPage() {

FILE: apps/mobile/src/app/playlist/external-sync.tsx
  function ExternalPlaylistSyncPage (line 577) | function ExternalPlaylistSyncPage() {

FILE: apps/mobile/src/app/playlist/local/[id].tsx
  constant SEARCHBAR_HEIGHT (line 67) | const SEARCHBAR_HEIGHT = 72
  constant SCOPE (line 68) | const SCOPE = 'UI.Playlist.Local'
  constant SELECT_MODE_ITEM_HEIGHT (line 70) | const SELECT_MODE_ITEM_HEIGHT = 69
  constant EDGE_ZONE (line 73) | const EDGE_ZONE = 80
  constant SCROLL_SPEED (line 75) | const SCROLL_SPEED = 8
  function LocalPlaylistPage (line 115) | function LocalPlaylistPage() {

FILE: apps/mobile/src/app/playlist/recently/index.tsx
  function RecentlyPlayedPage (line 19) | function RecentlyPlayedPage() {

FILE: apps/mobile/src/app/playlist/remote/collection/[id].tsx
  function CollectionPage (line 55) | function CollectionPage() {

FILE: apps/mobile/src/app/playlist/remote/favorite/[id].tsx
  function FavoritePage (line 55) | function FavoritePage() {

FILE: apps/mobile/src/app/playlist/remote/multipage/[bvid].tsx
  function MultipagePage (line 68) | function MultipagePage() {

FILE: apps/mobile/src/app/playlist/remote/search-result/fav/[query].tsx
  function SearchResultsPage (line 53) | function SearchResultsPage() {

FILE: apps/mobile/src/app/playlist/remote/search-result/global/[query].tsx
  function SearchResultsPage (line 49) | function SearchResultsPage() {

FILE: apps/mobile/src/app/playlist/remote/toview.tsx
  function ToViewPage (line 72) | function ToViewPage() {

FILE: apps/mobile/src/app/playlist/remote/uploader/[mid].tsx
  constant SEARCHBAR_HEIGHT (line 39) | const SEARCHBAR_HEIGHT = 72
  function UploaderPage (line 72) | function UploaderPage() {

FILE: apps/mobile/src/app/settings/appearance.tsx
  function AppearanceSettingsPage (line 20) | function AppearanceSettingsPage() {

FILE: apps/mobile/src/app/settings/donate.tsx
  function DonateSettingsPage (line 10) | function DonateSettingsPage() {

FILE: apps/mobile/src/app/settings/download.tsx
  constant DOWNLOAD_PARALLEL_OPTIONS (line 13) | const DOWNLOAD_PARALLEL_OPTIONS = [
  function DownloadSettingsPage (line 20) | function DownloadSettingsPage() {

FILE: apps/mobile/src/app/settings/general.tsx
  function GeneralSettingsPage (line 19) | function GeneralSettingsPage() {
  function performShareLog (line 197) | async function performShareLog(

FILE: apps/mobile/src/app/settings/lyrics.tsx
  function LyricsSettingsPage (line 19) | function LyricsSettingsPage() {

FILE: apps/mobile/src/app/settings/playback.tsx
  function PlaybackSettingsPage (line 14) | function PlaybackSettingsPage() {

FILE: apps/mobile/src/app/share/playlist.tsx
  function SharedPlaylistPreviewPage (line 78) | function SharedPlaylistPreviewPage() {

FILE: apps/mobile/src/app/test.tsx
  function TestPage (line 30) | function TestPage() {

FILE: apps/mobile/src/components/ErrorBoundary.tsx
  function GlobalErrorFallback (line 6) | function GlobalErrorFallback({

FILE: apps/mobile/src/components/ModalRegistry.tsx
  type ModalComponent (line 78) | type ModalComponent<K extends ModalKey> = ComponentType<ModalPropsMap[K]...

FILE: apps/mobile/src/components/common/AnimatedModalOverlay.tsx
  type Props (line 13) | interface Props {
  function AnimatedModalOverlay (line 22) | function AnimatedModalOverlay({

FILE: apps/mobile/src/components/common/Button.tsx
  type ButtonMode (line 16) | type ButtonMode =
  type ButtonProps (line 23) | interface ButtonProps {

FILE: apps/mobile/src/components/common/CoverWithPlaceHolder.tsx
  type CoverWithPlaceHolderProps (line 15) | interface CoverWithPlaceHolderProps {

FILE: apps/mobile/src/components/common/FunctionalMenu.tsx
  type FunctionalMenuProps (line 8) | type FunctionalMenuProps = PropsWithChildren<Parameters<typeof Menu>[0]>

FILE: apps/mobile/src/components/common/IconButton.tsx
  type IconButtonMode (line 9) | type IconButtonMode = 'outlined' | 'contained' | 'contained-tonal'
  type IconButtonProps (line 11) | interface IconButtonProps {

FILE: apps/mobile/src/components/modals/AlertModal.tsx
  type AlertButton (line 7) | interface AlertButton {
  type AlertOptions (line 12) | interface AlertOptions {
  type AlertModalProps (line 16) | interface AlertModalProps {
  function AlertModal (line 23) | function AlertModal({
  function alert (line 79) | function alert(

FILE: apps/mobile/src/components/modals/PlayerQueueModal.tsx
  type PlayerQueueModalProps (line 105) | interface PlayerQueueModalProps extends TrueSheetProps {
  function PlayerQueueModal (line 110) | function PlayerQueueModal({

FILE: apps/mobile/src/components/modals/app/DonationQRModal.tsx
  constant WECHAT_QR (line 15) | const WECHAT_QR = require('../../../../assets/images/wechat.png')
  constant ALIPAY_QR (line 17) | const ALIPAY_QR = require('../../../../assets/images/alipay.jpg')
  type DonationType (line 19) | type DonationType = 'wechat' | 'alipay'
  function DonationQRModal (line 21) | function DonationQRModal({

FILE: apps/mobile/src/components/modals/app/UpdateAppModal.tsx
  type UpdateModalProps (line 19) | interface UpdateModalProps {
  function UpdateAppModal (line 28) | function UpdateAppModal({

FILE: apps/mobile/src/components/modals/app/WelcomeModal.tsx
  function Step0 (line 25) | function Step0() {
  function Step1 (line 43) | function Step1({
  function WelcomeModal (line 99) | function WelcomeModal() {

FILE: apps/mobile/src/components/modals/edit-metadata/editPlaylistMetadataModal.tsx
  function EditPlaylistMetadataModal (line 19) | function EditPlaylistMetadataModal({

FILE: apps/mobile/src/components/modals/edit-metadata/editTrackMetadataModal.tsx
  function EditTrackMetadataModal (line 11) | function EditTrackMetadataModal({ track }: { track: Track }) {

FILE: apps/mobile/src/components/modals/login/CookieLoginModal.tsx
  function CookieLoginModal (line 14) | function CookieLoginModal() {

FILE: apps/mobile/src/components/modals/login/PhoneLoginModal.tsx
  function PhoneLoginModal (line 8) | function PhoneLoginModal() {

FILE: apps/mobile/src/components/modals/login/QRCodeLoginModal.tsx
  type Status (line 20) | type Status =
  type State (line 28) | interface State {
  type Action (line 35) | type Action =
  function reducer (line 53) | function reducer(state: State, action: Action): State {

FILE: apps/mobile/src/components/modals/login/steps/GeetestVerifyStep.tsx
  type Props (line 9) | interface Props {
  function buildGeetestHtml (line 16) | function buildGeetestHtml(gt: string, challenge: string): string {
  function GeetestVerifyStep (line 75) | function GeetestVerifyStep({

FILE: apps/mobile/src/components/modals/login/steps/InputCodeStep.tsx
  type Props (line 6) | interface Props {
  function InputCodeStep (line 17) | function InputCodeStep({

FILE: apps/mobile/src/components/modals/login/steps/InputPhoneStep.tsx
  type Props (line 6) | interface Props {
  function InputPhoneStep (line 16) | function InputPhoneStep({

FILE: apps/mobile/src/components/modals/login/steps/SuccessStep.tsx
  function SuccessStep (line 4) | function SuccessStep() {

FILE: apps/mobile/src/components/modals/lyrics/EditLyrics.tsx
  function EditLyricsModal (line 18) | function EditLyricsModal({

FILE: apps/mobile/src/components/modals/lyrics/ManualSearchLyrics.tsx
  constant SOURCE_MAP (line 20) | const SOURCE_MAP = {

FILE: apps/mobile/src/components/modals/player/LyricsSelectionModal.tsx
  function performShare (line 82) | async function performShare(

FILE: apps/mobile/src/components/modals/player/PlaybackSpeedModal.tsx
  constant PRESET_SPEEDS (line 11) | const PRESET_SPEEDS = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0]

FILE: apps/mobile/src/components/modals/player/SleepTimerModal.tsx
  constant PRESET_DURATIONS (line 13) | const PRESET_DURATIONS = [15, 30, 45, 60] // in minutes

FILE: apps/mobile/src/components/modals/player/SongShareModal.tsx
  function performShare (line 21) | async function performShare(

FILE: apps/mobile/src/components/modals/playlist/CreatePlaylistModal.tsx
  function CreatePlaylistModal (line 14) | function CreatePlaylistModal({

FILE: apps/mobile/src/components/modals/playlist/DuplicateLocalPlaylistModal.tsx
  function DuplicateLocalPlaylistModal (line 10) | function DuplicateLocalPlaylistModal({

FILE: apps/mobile/src/components/modals/playlist/EnableSharingModal.tsx
  constant SHARE_BASE_URL (line 17) | const SHARE_BASE_URL = 'https://bbplayer.roitium.com/share/playlist'
  function EnableSharingModal (line 19) | function EnableSharingModal({

FILE: apps/mobile/src/components/modals/playlist/ManualMatchExternalSync.tsx
  function ManualMatchExternalSync (line 88) | function ManualMatchExternalSync({

FILE: apps/mobile/src/components/modals/playlist/MergePlaylistsModal.tsx
  type RenderExtraData (line 55) | type RenderExtraData = {
  function MergePlaylistsModal (line 74) | function MergePlaylistsModal() {

FILE: apps/mobile/src/components/modals/playlist/SaveQueueToPlaylistModal.tsx
  function SaveQueueToPlaylistModal (line 17) | function SaveQueueToPlaylistModal({

FILE: apps/mobile/src/components/modals/playlist/SubscribeToSharedPlaylistModal.tsx
  constant UUID_RE (line 9) | const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f...
  function parseShareLink (line 12) | function parseShareLink(input: string): {
  function SubscribeToSharedPlaylistModal (line 35) | function SubscribeToSharedPlaylistModal() {

FILE: apps/mobile/src/components/modals/playlist/SyncLocalToBilibiliModal.tsx
  type SyncLocalToBilibiliModalProps (line 19) | interface SyncLocalToBilibiliModalProps {
  type Step (line 23) | type Step =
  type RemoteFolder (line 32) | interface RemoteFolder {
  type DiffResult (line 37) | interface DiffResult {
  type State (line 42) | interface State {
  type Action (line 52) | type Action =
  function reducer (line 72) | function reducer(state: State, action: Action): State {
  function SyncLocalToBilibiliModal (line 95) | function SyncLocalToBilibiliModal({

FILE: apps/mobile/src/components/modals/settings/CoverDownloadProgressModal.tsx
  type ProgressState (line 10) | interface ProgressState {

FILE: apps/mobile/src/components/modals/settings/ExportDownloadsProgressModal.tsx
  type Stage (line 22) | type Stage = 'config' | 'exporting' | 'completed' | 'error'
  type ProgressState (line 24) | interface ProgressState {
  type ExportDownloadsProgressModalProps (line 32) | interface ExportDownloadsProgressModalProps {
  constant PREVIEW_VALUES (line 38) | const PREVIEW_VALUES: Record<string, string> = {
  constant VARIABLE_KEYS (line 46) | const VARIABLE_KEYS = Object.keys(PREVIEW_VALUES)
  function buildPreviewFilename (line 48) | function buildPreviewFilename(pattern: string): string {
  function patternHasVariable (line 58) | function patternHasVariable(pattern: string): boolean {
  function startExport (line 109) | function startExport() {
  function dismiss (line 173) | function dismiss() {

FILE: apps/mobile/src/components/providers.tsx
  function AppProviders (line 21) | function AppProviders({ children }: { children: ReactNode }) {

FILE: apps/mobile/src/features/comments/components/CommentItem.tsx
  type CommentItemProps (line 15) | interface CommentItemProps {
  function CommentItem (line 21) | function CommentItem({ item, onReplyPress, bvid }: CommentItemProps) {

FILE: apps/mobile/src/features/downloads/DownloadHeader.tsx
  type DownloadHeaderProps (line 6) | interface DownloadHeaderProps {
  function DownloadHeader (line 16) | function DownloadHeader({

FILE: apps/mobile/src/features/history/HistoryListItem.tsx
  type HistoryListItemProps (line 14) | interface HistoryListItemProps {

FILE: apps/mobile/src/features/home/SearchSuggestions.tsx
  type SearchSuggestionsProps (line 29) | interface SearchSuggestionsProps {
  type SearchHistoryItem (line 39) | interface SearchHistoryItem {
  function parseEmTags (line 50) | function parseEmTags(text: string | undefined) {
  constant MARGIN_HORIZONTAL (line 74) | const MARGIN_HORIZONTAL = 16
  constant MARGIN_TOP (line 75) | const MARGIN_TOP = 12
  constant MARGIN_BOTTOM (line 76) | const MARGIN_BOTTOM = 12
  function SearchSuggestions (line 78) | function SearchSuggestions({

FILE: apps/mobile/src/features/library/shared/DataFetchingError.tsx
  type DataFetchingErrorProps (line 6) | interface DataFetchingErrorProps {
  function DataFetchingError (line 11) | function DataFetchingError({

FILE: apps/mobile/src/features/library/shared/TabDisabled.tsx
  function TabDisable (line 7) | function TabDisable() {

FILE: apps/mobile/src/features/library/skeletons/LibraryTabSkeleton.tsx
  function LibraryListItemSkeleton (line 10) | function LibraryListItemSkeleton() {
  function LocalPlaylistListSkeleton (line 60) | function LocalPlaylistListSkeleton() {
  function FavoriteFolderListSkeleton (line 101) | function FavoriteFolderListSkeleton() {
  function CollectionListSkeleton (line 141) | function CollectionListSkeleton() {
  function LibraryTabSkeleton (line 175) | function LibraryTabSkeleton() {

FILE: apps/mobile/src/features/player/components/BGStreamerShader.ts
  constant GLSL_SHADER_SOURCE (line 8) | const GLSL_SHADER_SOURCE = `

FILE: apps/mobile/src/features/player/components/LyricsControlOverlay.tsx
  constant OVERLAY_HEIGHT (line 24) | const OVERLAY_HEIGHT = windowHeight * 0.4
  constant AUTO_HIDE_DELAY (line 25) | const AUTO_HIDE_DELAY = 3000
  type LyricsControlOverlayProps (line 27) | interface LyricsControlOverlayProps {

FILE: apps/mobile/src/features/player/components/PlayerControls.tsx
  type MainPlaybackControlsProps (line 30) | interface MainPlaybackControlsProps {
  function MainPlaybackControls (line 39) | function MainPlaybackControls({
  function PlayerControls (line 241) | function PlayerControls({ onOpenQueue }: { onOpenQueue: () => void }) {

FILE: apps/mobile/src/features/player/components/PlayerFunctionalMenu.tsx
  function HighFreqButton (line 25) | function HighFreqButton({
  function PlayerFunctionalMenu (line 75) | function PlayerFunctionalMenu({

FILE: apps/mobile/src/features/player/components/PlayerHeader.tsx
  function PlayerHeader (line 12) | function PlayerHeader({

FILE: apps/mobile/src/features/player/components/PlayerLyrics.tsx
  constant SCROLL_DIRECTION_THRESHOLD (line 38) | const SCROLL_DIRECTION_THRESHOLD = 8

FILE: apps/mobile/src/features/player/components/PlayerMainTab.tsx
  type PlayerMainTabProps (line 16) | interface PlayerMainTabProps {

FILE: apps/mobile/src/features/player/components/PlayerSlider.tsx
  constant THUMB_SIZE (line 21) | const THUMB_SIZE = 12
  function TextWithAnimation (line 23) | function TextWithAnimation({
  type PlayerSliderProps (line 89) | interface PlayerSliderProps {
  function PlayerSlider (line 93) | function PlayerSlider({ onInteraction }: PlayerSliderProps = {}) {

FILE: apps/mobile/src/features/player/components/PlayerTrackInfo.tsx
  constant COVER_SIZE_RECT (line 30) | const COVER_SIZE_RECT = screenWidth - 80
  constant COVER_SIZE_CIRCLE (line 31) | const COVER_SIZE_CIRCLE = screenWidth - 120
  function TrackInfo (line 33) | function TrackInfo({

FILE: apps/mobile/src/features/player/components/SpectrumVisualizer.tsx
  type SpectrumVisualizerProps (line 10) | interface SpectrumVisualizerProps {
  constant BAR_COUNT (line 16) | const BAR_COUNT = 60
  constant MAX_BAR_HEIGHT (line 17) | const MAX_BAR_HEIGHT = 36
  constant SMOOTHING_FACTOR (line 18) | const SMOOTHING_FACTOR = 0.3
  constant GAP (line 19) | const GAP = 4

FILE: apps/mobile/src/features/player/components/danmaku/DanmakuView.tsx
  type DanmakuViewProps (line 16) | interface DanmakuViewProps {

FILE: apps/mobile/src/features/player/components/lyrics/KaraokeWord.tsx
  type KaraokeWordProps (line 17) | interface KaraokeWordProps {

FILE: apps/mobile/src/features/player/components/lyrics/LyricActionSheet.tsx
  type Props (line 5) | interface Props {
  function LyricActionSheet (line 16) | function LyricActionSheet({

FILE: apps/mobile/src/features/player/components/lyrics/LyricLineItem.tsx
  type LyricLineItemProps (line 19) | interface LyricLineItemProps {

FILE: apps/mobile/src/features/player/components/lyrics/LyricsOffsetControl.tsx
  type LyricsOffsetControlProps (line 6) | interface LyricsOffsetControlProps {

FILE: apps/mobile/src/features/player/components/sharing/LyricsShareCard.tsx
  type LyricsShareCardProps (line 10) | interface LyricsShareCardProps {

FILE: apps/mobile/src/features/player/components/sharing/SongShareCard.tsx
  type SongShareCardProps (line 9) | interface SongShareCardProps {

FILE: apps/mobile/src/features/player/hooks/danmaku/constants.ts
  constant CONFIG (line 1) | const CONFIG = {

FILE: apps/mobile/src/features/player/hooks/danmaku/useDanmakuLoader.ts
  constant PRELOAD_DISTANCE_MS (line 17) | const PRELOAD_DISTANCE_MS = 1000 * 60
  constant SEGMENT_DURATION_MS (line 18) | const SEGMENT_DURATION_MS = 1000 * 60 * 6
  constant BASE_RETRY_DELAY (line 19) | const BASE_RETRY_DELAY = 1000
  constant MAX_RETRY_DELAY (line 20) | const MAX_RETRY_DELAY = 1000 * 60 * 5
  function useDanmakuLoader (line 24) | function useDanmakuLoader(

FILE: apps/mobile/src/features/player/hooks/danmaku/useDanmakuRender.ts
  type ActiveBullet (line 21) | interface ActiveBullet {
  function binarySearch (line 31) | function binarySearch(data: BilibiliDanmakuItem[], targetTime: number): ...
  function findBestScrollTrack (line 59) | function findBestScrollTrack(tracks: number[], width: number) {

FILE: apps/mobile/src/features/player/hooks/useLyricSync.ts
  function useLyricSync (line 8) | function useLyricSync(

FILE: apps/mobile/src/features/player/hooks/usePlayerHeaderAnimation.ts
  function usePlayerHeaderAnimation (line 8) | function usePlayerHeaderAnimation(

FILE: apps/mobile/src/features/playlist/local/components/LocalPlaylistHeader.tsx
  type PlaylistHeaderProps (line 29) | interface PlaylistHeaderProps {
  type SubtitlePieces (line 42) | interface SubtitlePieces {
  function buildSubtitlePieces (line 51) | function buildSubtitlePieces(

FILE: apps/mobile/src/features/playlist/local/components/LocalPlaylistItem.tsx
  type TrackMenuItem (line 22) | interface TrackMenuItem {
  type TrackListItemProps (line 30) | interface TrackListItemProps {

FILE: apps/mobile/src/features/playlist/local/components/LocalTrackList.tsx
  type LocalTrackListProps (line 34) | interface LocalTrackListProps extends Omit<
  function LocalTrackList (line 230) | function LocalTrackList({

FILE: apps/mobile/src/features/playlist/local/components/PlaylistError.tsx
  type PlaylistErrorProps (line 6) | interface PlaylistErrorProps {
  function PlaylistError (line 11) | function PlaylistError({

FILE: apps/mobile/src/features/playlist/local/components/SharedPlaylistMembersSheet.tsx
  type Props (line 11) | interface Props {

FILE: apps/mobile/src/features/playlist/local/components/SyncFailuresSheet.tsx
  constant SCOPE (line 19) | const SCOPE = 'SyncFailuresSheet'
  constant OPERATION_INFO (line 21) | const OPERATION_INFO = {
  constant DEV_MOCK_ROWS (line 44) | const DEV_MOCK_ROWS: (typeof schema.playlistSyncQueue.$inferSelect)[] = ...
  type Props (line 55) | interface Props {

FILE: apps/mobile/src/features/playlist/local/hooks/useLocalPlaylistMenu.ts
  constant SCOPE (line 14) | const SCOPE = 'UI.Playlist.Local.Menu'
  type LocalPlaylistMenuProps (line 16) | interface LocalPlaylistMenuProps {
  function useLocalPlaylistMenu (line 24) | function useLocalPlaylistMenu({

FILE: apps/mobile/src/features/playlist/local/hooks/useLocalPlaylistPlayer.ts
  constant SCOPE (line 16) | const SCOPE = 'UI.Playlist.Local.Player'
  function useLocalPlaylistPlayer (line 18) | function useLocalPlaylistPlayer(

FILE: apps/mobile/src/features/playlist/local/hooks/useTrackSelection.ts
  function useTrackSelection (line 5) | function useTrackSelection<T = number>() {

FILE: apps/mobile/src/features/playlist/remote/components/FlashingTrackListItem.tsx
  type TrackListItemProps (line 14) | type TrackListItemProps = ComponentProps<typeof TrackListItem>
  type FlashingTrackListItemProps (line 16) | interface FlashingTrackListItemProps extends TrackListItemProps {
  function FlashingTrackListItem (line 20) | function FlashingTrackListItem({

FILE: apps/mobile/src/features/playlist/remote/components/PlaylistError.tsx
  type PlaylistErrorProps (line 6) | interface PlaylistErrorProps {
  function PlaylistError (line 11) | function PlaylistError({

FILE: apps/mobile/src/features/playlist/remote/components/PlaylistHeader.tsx
  type PlaylistHeaderProps (line 14) | interface PlaylistHeaderProps {

FILE: apps/mobile/src/features/playlist/remote/components/PlaylistItem.tsx
  type TrackMenuItem (line 15) | interface TrackMenuItem {
  type TrackNecessaryData (line 27) | interface TrackNecessaryData {
  type TrackListItemProps (line 38) | interface TrackListItemProps {

FILE: apps/mobile/src/features/playlist/remote/components/RemoteTrackList.tsx
  type TrackListProps (line 30) | interface TrackListProps extends Omit<
  type ExtraData (line 76) | interface ExtraData {
  function TrackList (line 132) | function TrackList({

FILE: apps/mobile/src/features/playlist/remote/hooks/useCheckLinkedToLocalPlaylist.ts
  function useCheckLinkedToPlaylist (line 13) | function useCheckLinkedToPlaylist(

FILE: apps/mobile/src/features/playlist/remote/hooks/usePlaylistMenu.ts
  function usePlaylistMenu (line 8) | function usePlaylistMenu(

FILE: apps/mobile/src/features/playlist/remote/hooks/useRemotePlaylist.ts
  function useRemotePlaylist (line 9) | function useRemotePlaylist() {

FILE: apps/mobile/src/features/playlist/remote/hooks/useTrackSelection.ts
  function useTrackSelection (line 5) | function useTrackSelection() {

FILE: apps/mobile/src/features/playlist/remote/search-result/constants.ts
  constant MULTIPAGE_VIDEO_KEYWORDS (line 1) | const MULTIPAGE_VIDEO_KEYWORDS = [

FILE: apps/mobile/src/features/playlist/remote/search-result/hooks/useSearchInteractions.ts
  function useSearchInteractions (line 13) | function useSearchInteractions() {

FILE: apps/mobile/src/features/playlist/remote/toview/components/Item.tsx
  type TrackMenuItem (line 20) | interface TrackMenuItem {
  type TrackNecessaryData (line 32) | interface TrackNecessaryData {
  type TrackListItemProps (line 42) | interface TrackListItemProps {

FILE: apps/mobile/src/features/playlist/remote/toview/components/ProgressRing.tsx
  type TrackNecessaryData (line 6) | interface TrackNecessaryData {
  constant RING_RADIUS (line 17) | const RING_RADIUS = 10
  constant RING_STROKE (line 18) | const RING_STROKE = 2.5
  constant RING_SIZE (line 19) | const RING_SIZE = (RING_RADIUS + RING_STROKE) * 2
  constant RING_CENTER (line 20) | const RING_CENTER = RING_RADIUS + RING_STROKE
  constant RING_CIRCUMFERENCE (line 21) | const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS
  type ProgressRingProps (line 23) | interface ProgressRingProps {

FILE: apps/mobile/src/features/playlist/skeletons/PlaylistSkeleton.tsx
  function PlaylistPageSkeleton (line 8) | function PlaylistPageSkeleton() {
  function PlaylistTrackListSkeleton (line 34) | function PlaylistTrackListSkeleton() {
  function PlaylistHeaderSkeleton (line 59) | function PlaylistHeaderSkeleton() {
  function TrackListItemSkeleton (line 147) | function TrackListItemSkeleton() {

FILE: apps/mobile/src/hooks/analytics/useFeatureTracking.ts
  function useFeatureTracking (line 11) | function useFeatureTracking() {

FILE: apps/mobile/src/hooks/app/useCheckUpdate.tsx
  function useCheckUpdate (line 7) | function useCheckUpdate() {

FILE: apps/mobile/src/hooks/app/useFastMigrations.ts
  constant SCHEMA_VERSION_KEY (line 11) | const SCHEMA_VERSION_KEY = 'db_schema_version'
  constant SORT_KEY_MIGRATED_V2_KEY (line 13) | const SORT_KEY_MIGRATED_V2_KEY = 'sort_key_migrated_v2' // gitleaks:allow
  constant SORT_KEY_MIGRATED_V3_KEY (line 14) | const SORT_KEY_MIGRATED_V3_KEY = 'sort_key_migrated_v3' // gitleaks:allow
  constant PLAY_HISTORY_MIGRATED_V1_KEY (line 15) | const PLAY_HISTORY_MIGRATED_V1_KEY = 'play_history_migrated_v1' // gitle...
  type MigrationConfig (line 17) | interface MigrationConfig {
  type State (line 24) | interface State {
  type Action (line 29) | type Action =
  function migrateSortKeysV2 (line 34) | function migrateSortKeysV2(): void {
  function migrateSortKeysV3 (line 111) | function migrateSortKeysV3(): void {
  function migratePlayHistory (line 165) | function migratePlayHistory(): void {

FILE: apps/mobile/src/hooks/auth/useGeetest.ts
  type CaptchaParams (line 8) | interface CaptchaParams {
  type UseGeetestProps (line 16) | interface UseGeetestProps {
  function useGeetest (line 23) | function useGeetest({

FILE: apps/mobile/src/hooks/auth/usePhoneLogin.ts
  type Step (line 16) | type Step = 'input_phone' | 'geetest_verify' | 'input_code' | 'success'
  type CaptchaParams (line 18) | interface CaptchaParams {
  constant COUNTRY_CODE (line 26) | const COUNTRY_CODE = '86'
  method validate (line 30) | validate(v: string): string {
  method validate (line 38) | validate(v: string): string {
  type LoginStatus (line 47) | type LoginStatus = 'idle' | 'loading' | 'success'
  type LoginState (line 49) | interface LoginState {
  type LoginAction (line 60) | type LoginAction =
  function loginReducer (line 84) | function loginReducer(state: LoginState, action: LoginAction): LoginState {
  function usePhoneLogin (line 136) | function usePhoneLogin() {

FILE: apps/mobile/src/hooks/mutations/db/playlist.ts
  function ensureBBPlayerToken (line 22) | async function ensureBBPlayerToken(): Promise<void> {
  constant SCOPE (line 45) | const SCOPE = 'Mutation.DB.Playlist'

FILE: apps/mobile/src/hooks/mutations/orpheus/index.ts
  function useRemoveDownloadsMutation (line 12) | function useRemoveDownloadsMutation() {

FILE: apps/mobile/src/hooks/player/useCurrentTrack.ts
  function useCurrentTrack (line 5) | function useCurrentTrack() {

FILE: apps/mobile/src/hooks/player/useCurrentTrackId.ts
  function useCurrentTrackId (line 3) | function useCurrentTrackId() {

FILE: apps/mobile/src/hooks/player/useIsCurrentTrack.ts
  function useIsCurrentTrack (line 5) | function useIsCurrentTrack(trackUniqueKey: string) {

FILE: apps/mobile/src/hooks/player/useLocalCover.ts
  function resolveTrackCover (line 8) | function resolveTrackCover(

FILE: apps/mobile/src/hooks/player/useSmoothProgress.ts
  function useSmoothProgress (line 13) | function useSmoothProgress(background = false) {

FILE: apps/mobile/src/hooks/player/useTrackProgress.ts
  type Progress (line 7) | interface Progress {
  constant INITIAL (line 13) | const INITIAL: Progress = { position: 0, duration: 0, buffered: 0 }
  function useTrackProgress (line 19) | function useTrackProgress(background = false) {

FILE: apps/mobile/src/hooks/queries/bilibili/comments.ts
  function useComments (line 14) | function useComments(bvid: string, mode = 3) {
  function useReplyComments (line 31) | function useReplyComments(bvid: string, rpid: number) {

FILE: apps/mobile/src/hooks/queries/bilibili/danmaku.ts
  function fetchDanmakuSegmentQuery (line 11) | async function fetchDanmakuSegmentQuery(

FILE: apps/mobile/src/hooks/queries/db/track.ts
  function usePlayCountHistoryPaginated (line 32) | function usePlayCountHistoryPaginated(
  function useTotalPlaybackDuration (line 64) | function useTotalPlaybackDuration(onlyCompleted = true) {

FILE: apps/mobile/src/hooks/queries/orpheus/index.ts
  function useBatchDownloadStatus (line 25) | function useBatchDownloadStatus(ids: string[]) {
  function useShuffleMode (line 37) | function useShuffleMode() {
  function useDownloadTasks (line 46) | function useDownloadTasks() {
  function useAllDownloads (line 56) | function useAllDownloads() {
  function usePlayerQueue (line 66) | function usePlayerQueue(enabled: boolean = true) {
  function useSleepTimerEndTime (line 79) | function useSleepTimerEndTime() {

FILE: apps/mobile/src/hooks/queries/sharedPlaylistAllMembers.ts
  type SharedPlaylistAllMember (line 5) | type SharedPlaylistAllMember = {
  function useSharedPlaylistAllMembers (line 13) | function useSharedPlaylistAllMembers(shareId?: string | null) {

FILE: apps/mobile/src/hooks/queries/sharedPlaylistMembers.ts
  function useSharedPlaylistMembers (line 8) | function useSharedPlaylistMembers(

FILE: apps/mobile/src/hooks/queries/useRecentPlaylists.ts
  function useRecentPlaylists (line 7) | function useRecentPlaylists() {

FILE: apps/mobile/src/hooks/router/useBottomTabBarHeight.ts
  function useBottomTabBarHeight (line 4) | function useBottomTabBarHeight() {

FILE: apps/mobile/src/hooks/router/usePreventRemove.ts
  function usePreventRemove (line 5) | function usePreventRemove(

FILE: apps/mobile/src/hooks/stores/useAppStore.ts
  constant OLD_KEYS (line 83) | const OLD_KEYS: Record<string, StorageKey> = {

FILE: apps/mobile/src/hooks/stores/useDownloadManagerStore.ts
  type ProgressEvent (line 6) | type ProgressEvent = Record<

FILE: apps/mobile/src/hooks/stores/useExternalPlaylistSyncStore.tsx
  type SyncState (line 6) | interface SyncState {
  type SyncStore (line 16) | type SyncStore = ReturnType<typeof createExternalPlaylistSyncStore>
  function useExternalPlaylistSyncStoreApi (line 51) | function useExternalPlaylistSyncStoreApi() {
  function useExternalPlaylistSyncStore (line 61) | function useExternalPlaylistSyncStore<T>(

FILE: apps/mobile/src/hooks/stores/useModalStore.ts
  type ModalState (line 10) | interface ModalState {

FILE: apps/mobile/src/hooks/stores/usePlayerStore.ts
  type PlayerState (line 11) | interface PlayerState {

FILE: apps/mobile/src/hooks/stores/useSharedPlaylistMembersStore.ts
  type SharedPlaylistMember (line 7) | type SharedPlaylistMember = {
  constant EMPTY (line 14) | const EMPTY: SharedPlaylistMember[] = []
  type SharedPlaylistMembersState (line 16) | interface SharedPlaylistMembersState {

FILE: apps/mobile/src/hooks/ui/useDoubleTapScrollToTop.ts
  function useDoubleTapScrollToTop (line 6) | function useDoubleTapScrollToTop<T>(

FILE: apps/mobile/src/hooks/ui/usePlaylistBackgroundColor.ts
  function getDominantColor (line 10) | function getDominantColor(
  function computeLightenedColor (line 22) | function computeLightenedColor(
  type PlaylistBackgroundColorResult (line 33) | interface PlaylistBackgroundColorResult {
  function usePlaylistBackgroundColor (line 41) | function usePlaylistBackgroundColor(

FILE: apps/mobile/src/hooks/ui/useScreenDimensions.ts
  function useScreenDimensions (line 4) | function useScreenDimensions() {

FILE: apps/mobile/src/hooks/utils/useDebouncedValue.ts
  function useDebouncedValue (line 3) | function useDebouncedValue<T>(value: T, delay = 300) {

FILE: apps/mobile/src/hooks/utils/usePreviousState.ts
  function usePreviousState (line 3) | function usePreviousState<T>(value: T) {

FILE: apps/mobile/src/hooks/utils/useRefreshOnFocus.ts
  function useRefreshOnFocus (line 4) | function useRefreshOnFocus<T>(refetch: () => Promise<T>) {

FILE: apps/mobile/src/lib/api/bbplayer/client.ts
  constant BASE_URL (line 6) | const BASE_URL =

FILE: apps/mobile/src/lib/api/bilibili/api.ts
  constant PASSPORT_UA (line 47) | const PASSPORT_UA =
  class BilibiliApi (line 53) | class BilibiliApi {
    method getHistory (line 57) | getHistory(): ResultAsync<BilibiliHistoryVideo[], BilibiliApiError> {
    method getPopularVideos (line 67) | getPopularVideos(
    method getFavoritePlaylists (line 80) | getFavoritePlaylists(
    method createFavoriteFolder (line 96) | createFavoriteFolder(
    method getSegDanmaku (line 124) | getSegDanmaku(
    method searchVideos (line 187) | searchVideos(
    method getHotSearches (line 224) | getHotSearches(): ResultAsync<BilibiliHotSearch[], BilibiliApiError> {
    method getSearchSuggestions (line 237) | getSearchSuggestions(
    method getAudioStream (line 308) | getAudioStream(
    method getPageList (line 440) | getPageList(
    method getUserInfo (line 454) | getUserInfo(): ResultAsync<BilibiliUserInfo, BilibiliApiError> {
    method getOtherUserInfo (line 461) | getOtherUserInfo(mid: number) {
    method getFavoriteListContents (line 477) | getFavoriteListContents(
    method searchFavoriteListContents (line 495) | searchFavoriteListContents(
    method getFavoriteListAllContents (line 519) | getFavoriteListAllContents(
    method getVideoDetails (line 532) | getVideoDetails(
    method batchDeleteFavoriteListContents (line 546) | batchDeleteFavoriteListContents(
    method getCollectionsList (line 561) | getCollectionsList(
    method getCollectionAllContents (line 589) | getCollectionAllContents(
    method dealFavoriteForOneVideo (line 605) | dealFavoriteForOneVideo(
    method getTargetVideoFavoriteStatus (line 629) | getTargetVideoFavoriteStatus(
    method reportPlaybackHistory (line 654) | reportPlaybackHistory(
    method getUserUploadedVideos (line 673) | getUserUploadedVideos(
    method getComments (line 698) | getComments(
    method getReplyComments (line 719) | getReplyComments(
    method likeComment (line 743) | likeComment(
    method getLoginQrCode (line 760) | getLoginQrCode(): ResultAsync<
    method pollQrCodeLoginStatus (line 774) | pollQrCodeLoginStatus(
    method getB23ResolvedUrl (line 848) | getB23ResolvedUrl(b23Url: string): ResultAsync<string, BilibiliApiErro...
    method checkVideoIsThumbUp (line 910) | checkVideoIsThumbUp(bvid: string) {
    method thumbUpVideo (line 922) | thumbUpVideo(bvid: string, like: boolean): ResultAsync<0, BilibiliApiE...
    method getWebPlayerInfo (line 947) | getWebPlayerInfo(
    method getToViewVideoList (line 966) | getToViewVideoList(): ResultAsync<BilibiliToViewVideoList, BilibiliApi...
    method deleteToViewVideo (line 979) | deleteToViewVideo(
    method clearToViewVideoList (line 1014) | clearToViewVideoList(): ResultAsync<undefined, BilibiliApiError> {
    method getPhoneLoginCaptchaToken (line 1023) | getPhoneLoginCaptchaToken(): ResultAsync<
    method sendPhoneLoginSms (line 1083) | sendPhoneLoginSms(
    method loginWithPhoneSmsCode (line 1159) | loginWithPhoneSmsCode(

FILE: apps/mobile/src/lib/api/bilibili/client.ts
  type ReqResponse (line 8) | interface ReqResponse<T> {
  class ApiClient (line 14) | class ApiClient {
    method get (line 111) | get<T>(
    method getBuffer (line 140) | getBuffer(
    method post (line 214) | post<T>(
    method postWithCsrf (line 242) | public postWithCsrf<T>(

FILE: apps/mobile/src/lib/api/bilibili/proto/dm.d.ts
  class DM (line 19) | class DM extends $protobuf.rpc.Service {
  type DmSegMobileCallback (line 130) | type DmSegMobileCallback = (error: (Error|null), response?: bilibili.com...
  type DmViewCallback (line 137) | type DmViewCallback = (error: (Error|null), response?: bilibili.communit...
  type DmPlayerConfigCallback (line 144) | type DmPlayerConfigCallback = (error: (Error|null), response?: bilibili....
  type DmSegOttCallback (line 151) | type DmSegOttCallback = (error: (Error|null), response?: bilibili.commun...
  type DmSegSDKCallback (line 158) | type DmSegSDKCallback = (error: (Error|null), response?: bilibili.commun...
  type DmExpoReportCallback (line 165) | type DmExpoReportCallback = (error: (Error|null), response?: bilibili.co...
  type IAvatar (line 169) | interface IAvatar {
  class Avatar (line 182) | class Avatar implements IAvatar {
  type AvatarType (line 278) | enum AvatarType {
  type IBubble (line 284) | interface IBubble {
  class Bubble (line 294) | class Bubble implements IBubble {
  type BubbleType (line 387) | enum BubbleType {
  type IBubbleV2 (line 394) | interface IBubbleV2 {
  class BubbleV2 (line 413) | class BubbleV2 implements IBubbleV2 {
  type IButton (line 515) | interface IButton {
  class Button (line 525) | class Button implements IButton {
  type IBuzzwordConfig (line 618) | interface IBuzzwordConfig {
  class BuzzwordConfig (line 625) | class BuzzwordConfig implements IBuzzwordConfig {
  type IBuzzwordShowConfig (line 715) | interface IBuzzwordShowConfig {
  class BuzzwordShowConfig (line 737) | class BuzzwordShowConfig implements IBuzzwordShowConfig {
  type ICheckBox (line 842) | interface ICheckBox {
  class CheckBox (line 858) | class CheckBox implements ICheckBox {
  type CheckboxType (line 957) | enum CheckboxType {
  type ICheckBoxV2 (line 964) | interface ICheckBoxV2 {
  class CheckBoxV2 (line 977) | class CheckBoxV2 implements ICheckBoxV2 {
  type IClickButton (line 1073) | interface IClickButton {
  class ClickButton (line 1098) | class ClickButton implements IClickButton {
  type IClickButtonV2 (line 1206) | interface IClickButtonV2 {
  class ClickButtonV2 (line 1234) | class ClickButtonV2 implements IClickButtonV2 {
  type ICommandDm (line 1345) | interface ICommandDm {
  class CommandDm (line 1379) | class CommandDm implements ICommandDm {
  type IDanmakuAIFlag (line 1496) | interface IDanmakuAIFlag {
  class DanmakuAIFlag (line 1503) | class DanmakuAIFlag implements IDanmakuAIFlag {
  type IDanmakuElem (line 1593) | interface IDanmakuElem {
  class DanmakuElem (line 1642) | class DanmakuElem implements IDanmakuElem {
  type IDanmakuFlag (line 1774) | interface IDanmakuFlag {
  class DanmakuFlag (line 1784) | class DanmakuFlag implements IDanmakuFlag {
  type IDanmakuFlagConfig (line 1877) | interface IDanmakuFlagConfig {
  class DanmakuFlagConfig (line 1890) | class DanmakuFlagConfig implements IDanmakuFlagConfig {
  type IDanmuDefaultPlayerConfig (line 1986) | interface IDanmuDefaultPlayerConfig {
  class DanmuDefaultPlayerConfig (line 2041) | class DanmuDefaultPlayerConfig implements IDanmuDefaultPlayerConfig {
  type IDanmuPlayerConfig (line 2179) | interface IDanmuPlayerConfig {
  class DanmuPlayerConfig (line 2249) | class DanmuPlayerConfig implements IDanmuPlayerConfig {
  type IDanmuPlayerConfigPanel (line 2402) | interface IDanmuPlayerConfigPanel {
  class DanmuPlayerConfigPanel (line 2409) | class DanmuPlayerConfigPanel implements IDanmuPlayerConfigPanel {
  type IDanmuPlayerDynamicConfig (line 2499) | interface IDanmuPlayerDynamicConfig {
  class DanmuPlayerDynamicConfig (line 2509) | class DanmuPlayerDynamicConfig implements IDanmuPlayerDynamicConfig {
  type IDanmuPlayerViewConfig (line 2602) | interface IDanmuPlayerViewConfig {
  class DanmuPlayerViewConfig (line 2618) | class DanmuPlayerViewConfig implements IDanmuPlayerViewConfig {
  type IDanmuWebPlayerConfig (line 2717) | interface IDanmuWebPlayerConfig {
  class DanmuWebPlayerConfig (line 2790) | class DanmuWebPlayerConfig implements IDanmuWebPlayerConfig {
  type DMAttrBit (line 2946) | enum DMAttrBit {
  type IDmColorful (line 2953) | interface IDmColorful {
  class DmColorful (line 2963) | class DmColorful implements IDmColorful {
  type DmColorfulType (line 3056) | enum DmColorfulType {
  type IDmExpoReportReq (line 3062) | interface IDmExpoReportReq {
  class DmExpoReportReq (line 3075) | class DmExpoReportReq implements IDmExpoReportReq {
  type IDmExpoReportRes (line 3171) | interface IDmExpoReportRes {
  class DmExpoReportRes (line 3175) | class DmExpoReportRes implements IDmExpoReportRes {
  type IDmPlayerConfigReq (line 3262) | interface IDmPlayerConfigReq {
  class DmPlayerConfigReq (line 3326) | class DmPlayerConfigReq implements IDmPlayerConfigReq {
  type IDmSegConfig (line 3473) | interface IDmSegConfig {
  class DmSegConfig (line 3483) | class DmSegConfig implements IDmSegConfig {
  type IDmSegMobileReply (line 3576) | interface IDmSegMobileReply {
  class DmSegMobileReply (line 3592) | class DmSegMobileReply implements IDmSegMobileReply {
  type IDmSegMobileReq (line 3691) | interface IDmSegMobileReq {
  class DmSegMobileReq (line 3722) | class DmSegMobileReq implements IDmSegMobileReq {
  type IDmSegOttReply (line 3836) | interface IDmSegOttReply {
  class DmSegOttReply (line 3846) | class DmSegOttReply implements IDmSegOttReply {
  type IDmSegOttReq (line 3939) | interface IDmSegOttReq {
  class DmSegOttReq (line 3955) | class DmSegOttReq implements IDmSegOttReq {
  type IDmSegSDKReply (line 4054) | interface IDmSegSDKReply {
  class DmSegSDKReply (line 4064) | class DmSegSDKReply implements IDmSegSDKReply {
  type IDmSegSDKReq (line 4157) | interface IDmSegSDKReq {
  class DmSegSDKReq (line 4173) | class DmSegSDKReq implements IDmSegSDKReq {
  type IDmViewReply (line 4272) | interface IDmViewReply {
  class DmViewReply (line 4333) | class DmViewReply implements IDmViewReply {
  type IDmViewReq (line 4477) | interface IDmViewReq {
  class DmViewReq (line 4496) | class DmViewReq implements IDmViewReq {
  type IDmWebViewReply (line 4598) | interface IDmWebViewReply {
  class DmWebViewReply (line 4644) | class DmWebViewReply implements IDmWebViewReply {
  type IExpoReport (line 4773) | interface IExpoReport {
  class ExpoReport (line 4780) | class ExpoReport implements IExpoReport {
  type ExposureType (line 4870) | enum ExposureType {
  type IExpression (line 4876) | interface IExpression {
  class Expression (line 4889) | class Expression implements IExpression {
  type IExpressions (line 4985) | interface IExpressions {
  class Expressions (line 4992) | class Expressions implements IExpressions {
  type IInlinePlayerDanmakuSwitch (line 5082) | interface IInlinePlayerDanmakuSwitch {
  class InlinePlayerDanmakuSwitch (line 5089) | class InlinePlayerDanmakuSwitch implements IInlinePlayerDanmakuSwitch {
  type ILabel (line 5179) | interface ILabel {
  class Label (line 5189) | class Label implements ILabel {
  type ILabelV2 (line 5282) | interface ILabelV2 {
  class LabelV2 (line 5298) | class LabelV2 implements ILabelV2 {
  type IPeriod (line 5397) | interface IPeriod {
  class Period (line 5407) | class Period implements IPeriod {
  type IPlayerDanmakuAiRecommendedLevel (line 5500) | interface IPlayerDanmakuAiRecommendedLevel {
  class PlayerDanmakuAiRecommendedLevel (line 5507) | class PlayerDanmakuAiRecommendedLevel implements IPlayerDanmakuAiRecomme...
  type IPlayerDanmakuAiRecommendedLevelV2 (line 5597) | interface IPlayerDanmakuAiRecommendedLevelV2 {
  class PlayerDanmakuAiRecommendedLevelV2 (line 5604) | class PlayerDanmakuAiRecommendedLevelV2 implements IPlayerDanmakuAiRecom...
  type IPlayerDanmakuAiRecommendedSwitch (line 5694) | interface IPlayerDanmakuAiRecommendedSwitch {
  class PlayerDanmakuAiRecommendedSwitch (line 5701) | class PlayerDanmakuAiRecommendedSwitch implements IPlayerDanmakuAiRecomm...
  type IPlayerDanmakuBlockbottom (line 5791) | interface IPlayerDanmakuBlockbottom {
  class PlayerDanmakuBlockbottom (line 5798) | class PlayerDanmakuBlockbottom implements IPlayerDanmakuBlockbottom {
  type IPlayerDanmakuBlockcolorful (line 5888) | interface IPlayerDanmakuBlockcolorful {
  class PlayerDanmakuBlockcolorful (line 5895) | class PlayerDanmakuBlockcolorful implements IPlayerDanmakuBlockcolorful {
  type IPlayerDanmakuBlockrepeat (line 5985) | interface IPlayerDanmakuBlockrepeat {
  class PlayerDanmakuBlockrepeat (line 5992) | class PlayerDanmakuBlockrepeat implements IPlayerDanmakuBlockrepeat {
  type IPlayerDanmakuBlockscroll (line 6082) | interface IPlayerDanmakuBlockscroll {
  class PlayerDanmakuBlockscroll (line 6089) | class PlayerDanmakuBlockscroll implements IPlayerDanmakuBlockscroll {
  type IPlayerDanmakuBlockspecial (line 6179) | interface IPlayerDanmakuBlockspecial {
  class PlayerDanmakuBlockspecial (line 6186) | class PlayerDanmakuBlockspecial implements IPlayerDanmakuBlockspecial {
  type IPlayerDanmakuBlocktop (line 6276) | interface IPlayerDanmakuBlocktop {
  class PlayerDanmakuBlocktop (line 6283) | class PlayerDanmakuBlocktop implements IPlayerDanmakuBlocktop {
  type IPlayerDanmakuDomain (line 6373) | interface IPlayerDanmakuDomain {
  class PlayerDanmakuDomain (line 6380) | class PlayerDanmakuDomain implements IPlayerDanmakuDomain {
  type IPlayerDanmakuEnableblocklist (line 6470) | interface IPlayerDanmakuEnableblocklist {
  class PlayerDanmakuEnableblocklist (line 6477) | class PlayerDanmakuEnableblocklist implements IPlayerDanmakuEnableblockl...
  type IPlayerDanmakuOpacity (line 6567) | interface IPlayerDanmakuOpacity {
  class PlayerDanmakuOpacity (line 6574) | class PlayerDanmakuOpacity implements IPlayerDanmakuOpacity {
  type IPlayerDanmakuScalingfactor (line 6664) | interface IPlayerDanmakuScalingfactor {
  class PlayerDanmakuScalingfactor (line 6671) | class PlayerDanmakuScalingfactor implements IPlayerDanmakuScalingfactor {
  type IPlayerDanmakuSeniorModeSwitch (line 6761) | interface IPlayerDanmakuSeniorModeSwitch {
  class PlayerDanmakuSeniorModeSwitch (line 6768) | class PlayerDanmakuSeniorModeSwitch implements IPlayerDanmakuSeniorModeS...
  type IPlayerDanmakuSpeed (line 6858) | interface IPlayerDanmakuSpeed {
  class PlayerDanmakuSpeed (line 6865) | class PlayerDanmakuSpeed implements IPlayerDanmakuSpeed {
  type IPlayerDanmakuSwitch (line 6955) | interface IPlayerDanmakuSwitch {
  class PlayerDanmakuSwitch (line 6965) | class PlayerDanmakuSwitch implements IPlayerDanmakuSwitch {
  type IPlayerDanmakuSwitchSave (line 7058) | interface IPlayerDanmakuSwitchSave {
  class PlayerDanmakuSwitchSave (line 7065) | class PlayerDanmakuSwitchSave implements IPlayerDanmakuSwitchSave {
  type IPlayerDanmakuUseDefaultConfig (line 7155) | interface IPlayerDanmakuUseDefaultConfig {
  class PlayerDanmakuUseDefaultConfig (line 7162) | class PlayerDanmakuUseDefaultConfig implements IPlayerDanmakuUseDefaultC...
  type IPostPanel (line 7252) | interface IPostPanel {
  class PostPanel (line 7283) | class PostPanel implements IPostPanel {
  type PostPanelBizType (line 7397) | enum PostPanelBizType {
  type IPostPanelV2 (line 7407) | interface IPostPanelV2 {
  class PostPanelV2 (line 7441) | class PostPanelV2 implements IPostPanelV2 {
  type PostStatus (line 7558) | enum PostStatus {
  type RenderType (line 7564) | enum RenderType {
  type IResponse (line 7571) | interface IResponse {
  class Response (line 7581) | class Response implements IResponse {
  type SubtitleAiStatus (line 7674) | enum SubtitleAiStatus {
  type SubtitleAiType (line 7681) | enum SubtitleAiType {
  type ISubtitleItem (line 7687) | interface ISubtitleItem {
  class SubtitleItem (line 7721) | class SubtitleItem implements ISubtitleItem {
  type SubtitleType (line 7838) | enum SubtitleType {
  type ITextInput (line 7844) | interface ITextInput {
  class TextInput (line 7872) | class TextInput implements ITextInput {
  type ITextInputV2 (line 7983) | interface ITextInputV2 {
  class TextInputV2 (line 8005) | class TextInputV2 implements ITextInputV2 {
  type IToast (line 8110) | interface IToast {
  class Toast (line 8126) | class Toast implements IToast {
  type IToastButtonV2 (line 8225) | interface IToastButtonV2 {
  class ToastButtonV2 (line 8235) | class ToastButtonV2 implements IToastButtonV2 {
  type ToastFunctionType (line 8328) | enum ToastFunctionType {
  type IToastV2 (line 8334) | interface IToastV2 {
  class ToastV2 (line 8347) | class ToastV2 implements IToastV2 {
  type IUserInfo (line 8443) | interface IUserInfo {
  class UserInfo (line 8465) | class UserInfo implements IUserInfo {
  type IVideoMask (line 8570) | interface IVideoMask {
  class VideoMask (line 8589) | class VideoMask implements IVideoMask {
  type IVideoSubtitle (line 8691) | interface IVideoSubtitle {
  class VideoSubtitle (line 8704) | class VideoSubtitle implements IVideoSubtitle {

FILE: apps/mobile/src/lib/api/bilibili/proto/dm.js
  function DM (line 69) | function DM(rpcImpl, requestDelimited, responseDelimited) {
  function Avatar (line 309) | function Avatar(properties) {
  function Bubble (line 593) | function Bubble(properties) {
  function BubbleV2 (line 841) | function BubbleV2(properties) {
  function Button (line 1179) | function Button(properties) {
  function BuzzwordConfig (line 1407) | function BuzzwordConfig(properties) {
  function BuzzwordShowConfig (line 1638) | function BuzzwordShowConfig(properties) {
  function CheckBox (line 1985) | function CheckBox(properties) {
  function CheckBoxV2 (line 2299) | function CheckBoxV2(properties) {
  function ClickButton (line 2555) | function ClickButton(properties) {
  function ClickButtonV2 (line 2995) | function ClickButtonV2(properties) {
  function CommandDm (line 3430) | function CommandDm(properties) {
  function DanmakuAIFlag (line 3862) | function DanmakuAIFlag(properties) {
  function DanmakuElem (line 4102) | function DanmakuElem(properties) {
  function DanmakuFlag (line 4664) | function DanmakuFlag(properties) {
  function DanmakuFlagConfig (line 4908) | function DanmakuFlagConfig(properties) {
  function DanmuDefaultPlayerConfig (line 5174) | function DanmuDefaultPlayerConfig(properties) {
  function DanmuPlayerConfig (line 5792) | function DanmuPlayerConfig(properties) {
  function DanmuPlayerConfigPanel (line 6499) | function DanmuPlayerConfigPanel(properties) {
  function DanmuPlayerDynamicConfig (line 6705) | function DanmuPlayerDynamicConfig(properties) {
  function DanmuPlayerViewConfig (line 6936) | function DanmuPlayerViewConfig(properties) {
  function DanmuWebPlayerConfig (line 7267) | function DanmuWebPlayerConfig(properties) {
  function DmColorful (line 8013) | function DmColorful(properties) {
  function DmExpoReportReq (line 8276) | function DmExpoReportReq(properties) {
  function DmExpoReportRes (line 8539) | function DmExpoReportRes(properties) {
  function DmPlayerConfigReq (line 8736) | function DmPlayerConfigReq(properties) {
  function DmSegConfig (line 9470) | function DmSegConfig(properties) {
  function DmSegMobileReply (line 9729) | function DmSegMobileReply(properties) {
  function DmSegMobileReq (line 10058) | function DmSegMobileReq(properties) {
  function DmSegOttReply (line 10511) | function DmSegOttReply(properties) {
  function DmSegOttReq (line 10763) | function DmSegOttReq(properties) {
  function DmSegSDKReply (line 11078) | function DmSegSDKReply(properties) {
  function DmSegSDKReq (line 11330) | function DmSegSDKReq(properties) {
  function DmViewReply (line 11662) | function DmViewReply(properties) {
  function DmViewReq (line 12411) | function DmViewReq(properties) {
  function DmWebViewReply (line 12746) | function DmWebViewReply(properties) {
  function ExpoReport (line 13380) | function ExpoReport(properties) {
  function Expression (line 13601) | function Expression(properties) {
  function Expressions (line 13889) | function Expressions(properties) {
  function InlinePlayerDanmakuSwitch (line 14115) | function InlinePlayerDanmakuSwitch(properties) {
  function Label (line 14321) | function Label(properties) {
  function LabelV2 (line 14568) | function LabelV2(properties) {
  function Period (line 14858) | function Period(properties) {
  function PlayerDanmakuAiRecommendedLevel (line 15114) | function PlayerDanmakuAiRecommendedLevel(properties) {
  function PlayerDanmakuAiRecommendedLevelV2 (line 15319) | function PlayerDanmakuAiRecommendedLevelV2(properties) {
  function PlayerDanmakuAiRecommendedSwitch (line 15524) | function PlayerDanmakuAiRecommendedSwitch(properties) {
  function PlayerDanmakuBlockbottom (line 15729) | function PlayerDanmakuBlockbottom(properties) {
  function PlayerDanmakuBlockcolorful (line 15934) | function PlayerDanmakuBlockcolorful(properties) {
  function PlayerDanmakuBlockrepeat (line 16139) | function PlayerDanmakuBlockrepeat(properties) {
  function PlayerDanmakuBlockscroll (line 16344) | function PlayerDanmakuBlockscroll(properties) {
  function PlayerDanmakuBlockspecial (line 16549) | function PlayerDanmakuBlockspecial(properties) {
  function PlayerDanmakuBlocktop (line 16754) | function PlayerDanmakuBlocktop(properties) {
  function PlayerDanmakuDomain (line 16959) | function PlayerDanmakuDomain(properties) {
  function PlayerDanmakuEnableblocklist (line 17164) | function PlayerDanmakuEnableblocklist(properties) {
  function PlayerDanmakuOpacity (line 17369) | function PlayerDanmakuOpacity(properties) {
  function PlayerDanmakuScalingfactor (line 17574) | function PlayerDanmakuScalingfactor(properties) {
  function PlayerDanmakuSeniorModeSwitch (line 17779) | function PlayerDanmakuSeniorModeSwitch(properties) {
  function PlayerDanmakuSpeed (line 17984) | function PlayerDanmakuSpeed(properties) {
  function PlayerDanmakuSwitch (line 18190) | function PlayerDanmakuSwitch(properties) {
  function PlayerDanmakuSwitchSave (line 18418) | function PlayerDanmakuSwitchSave(properties) {
  function PlayerDanmakuUseDefaultConfig (line 18623) | function PlayerDanmakuUseDefaultConfig(properties) {
  function PostPanel (line 18836) | function PostPanel(properties) {
  function PostPanelV2 (line 19364) | function PostPanelV2(properties) {
  function Response (line 19857) | function Response(properties) {
  function SubtitleItem (line 20124) | function SubtitleItem(properties) {
  function TextInput (line 20630) | function TextInput(properties) {
  function TextInputV2 (line 21098) | function TextInputV2(properties) {
  function Toast (line 21496) | function Toast(properties) {
  function ToastButtonV2 (line 21774) | function ToastButtonV2(properties) {
  function ToastV2 (line 22018) | function ToastV2(properties) {
  function UserInfo (line 22278) | function UserInfo(properties) {
  function VideoMask (line 22612) | function VideoMask(properties) {
  function VideoSubtitle (line 22936) | function VideoSubtitle(properties) {

FILE: apps/mobile/src/lib/api/bilibili/utils.ts
  function bv2av (line 11) | function bv2av(bvid: string): number {
  function av2bv (line 33) | function av2bv(avid: number | bigint): string {
  function getCsrfToken (line 54) | function getCsrfToken(): Result<string, BilibiliApiError> {

FILE: apps/mobile/src/lib/api/bilibili/wbi.ts
  function encWbi (line 27) | function encWbi(
  function isSameDayAsToday (line 53) | function isSameDayAsToday(timestamp: number) {
  type WbiKeys (line 70) | interface WbiKeys {
  function getWbiKeysFromStorage (line 76) | function getWbiKeysFromStorage() {
  function getWbiKeys (line 90) | function getWbiKeys(): ResultAsync<
  function getWbiEncodedParams (line 125) | function getWbiEncodedParams(

FILE: apps/mobile/src/lib/api/kugou/api.ts
  class KugouApi (line 17) | class KugouApi {
    method getHeaders (line 18) | private getHeaders() {
    method search (line 25) | search(
    method getLyrics (line 68) | getLyrics(id: string, signal?: AbortSignal): ResultAsync<string, Error> {
    method parseLyrics (line 117) | parseLyrics(content: string): LyricProviderResponseData {
    method searchBestMatchedLyrics (line 125) | searchBestMatchedLyrics(

FILE: apps/mobile/src/lib/api/netease/api.ts
  type SearchParams (line 19) | interface SearchParams {
  class NeteaseApi (line 26) | class NeteaseApi {
    method getLyrics (line 27) | getLyrics(
    method search (line 63) | search(
    method parseLyrics (line 100) | public parseLyrics(
    method searchBestMatchedLyrics (line 121) | public searchBestMatchedLyrics(
    method getPlaylist (line 151) | getPlaylist(

FILE: apps/mobile/src/lib/api/netease/request.ts
  type AppConfig (line 14) | interface AppConfig {
  constant APP_CONF (line 20) | const APP_CONF: AppConfig = {
  type RequestOptions (line 36) | interface RequestOptions {
  type RequestPayload (line 45) | interface RequestPayload {
  type FetchResult (line 118) | interface FetchResult<TReturnBody> {

FILE: apps/mobile/src/lib/api/netease/utils.ts
  function cookieToJson (line 2) | function cookieToJson(cookie: string): Record<string, string> {
  function cookieObjToString (line 12) | function cookieObjToString(
  function toBoolean (line 21) | function toBoolean(value: unknown): boolean {
  type Query (line 25) | interface Query {
  function createOption (line 34) | function createOption(

FILE: apps/mobile/src/lib/api/qqmusic/api.ts
  class QQMusicApi (line 17) | class QQMusicApi {
    method search (line 24) | public search(
    method getLyrics (line 87) | public getLyrics(
    method parseLyrics (line 114) | public parseLyrics(
    method searchBestMatchedLyrics (line 132) | public searchBestMatchedLyrics(
    method getPlaylist (line 176) | public getPlaylist(id: string): ResultAsync<QQMusicPlaylistResponse, E...

FILE: apps/mobile/src/lib/config/sentry.ts
  function initializeSentry (line 45) | function initializeSentry() {

FILE: apps/mobile/src/lib/errors/facade.ts
  type FacadeErrorType (line 3) | type FacadeErrorType =
  class FacadeError (line 31) | class FacadeError extends BaseFacadeError {
    method constructor (line 32) | constructor(
  function createSyncTaskAlreadyRunningError (line 40) | function createSyncTaskAlreadyRunningError(cause?: unknown) {
  function createFacadeError (line 47) | function createFacadeError(

FILE: apps/mobile/src/lib/errors/index.ts
  class CustomError (line 1) | class CustomError extends Error {
    method constructor (line 4) | constructor(
  class ServiceError (line 15) | class ServiceError extends CustomError {}
  class FacadeError (line 17) | class FacadeError extends CustomError {}
  class UIError (line 19) | class UIError extends CustomError {}
  class ThirdPartyError (line 21) | class ThirdPartyError extends CustomError {
    method constructor (line 25) | constructor(
  class DatabaseError (line 36) | class DatabaseError extends CustomError {}
  class DataParsingError (line 37) | class DataParsingError extends CustomError {}
  class FileSystemError (line 38) | class FileSystemError extends CustomError {}
  class LrcParseError (line 39) | class LrcParseError extends CustomError {
    method constructor (line 40) | constructor(message: string) {
  class LyricNotFoundError (line 45) | class LyricNotFoundError extends CustomError {
    method constructor (line 46) | constructor(message: string, opts?: { cause?: unknown }) {

FILE: apps/mobile/src/lib/errors/player.ts
  type PlayerErrorType (line 8) | type PlayerErrorType = 'UnknownSource' | 'AudioUrlNotFound'
  class PlayerError (line 10) | class PlayerError extends UIError {
    method constructor (line 11) | constructor(
  function createPlayerError (line 19) | function createPlayerError(

FILE: apps/mobile/src/lib/errors/service.ts
  type ServiceErrorType (line 2) | type ServiceErrorType =
  function createServiceError (line 14) | function createServiceError(
  function createTrackNotFound (line 26) | function createTrackNotFound(trackId: number | string, cause?: unknown) {
  function createArtistNotFound (line 33) | function createArtistNotFound(
  function createPlaylistNotFound (line 43) | function createPlaylistNotFound(
  function createTrackNotInPlaylist (line 54) | function createTrackNotInPlaylist(
  function createValidationError (line 69) | function createValidationError(
  function createNotImplementedError (line 76) | function createNotImplementedError(message = '未实现', cause?: unknown) {
  function createPlaylistAlreadyExists (line 80) | function createPlaylistAlreadyExists(title: string, cause?: unknown) {

FILE: apps/mobile/src/lib/errors/thirdparty/bilibili.ts
  type BilibiliApiErrorType (line 3) | type BilibiliApiErrorType =
  type BilibiliApiErrorDetails (line 12) | interface BilibiliApiErrorDetails {
  type BilibiliErrorData (line 20) | interface BilibiliErrorData {
  class BilibiliApiError (line 25) | class BilibiliApiError extends ThirdPartyError {
    method constructor (line 28) | constructor({

FILE: apps/mobile/src/lib/errors/thirdparty/netease.ts
  type NeteaseApiErrorType (line 3) | type NeteaseApiErrorType =
  type NeteaseApiErrorDetails (line 8) | interface NeteaseApiErrorDetails {
  type NeteaseErrorData (line 16) | interface NeteaseErrorData {
  class NeteaseApiError (line 21) | class NeteaseApiError extends ThirdPartyError {
    method constructor (line 24) | constructor({

FILE: apps/mobile/src/lib/facades/bilibili.ts
  class BilibiliFacade (line 11) | class BilibiliFacade {
    method constructor (line 12) | constructor(private readonly bilibiliApi: typeof BilibiliApiService) {}
    method fetchRemotePlaylistMetadata (line 14) | public async fetchRemotePlaylistMetadata(

FILE: apps/mobile/src/lib/facades/playlist.ts
  type BbplayerApiClient (line 24) | type BbplayerApiClient = typeof bbplayerApi
  class PlaylistFacade (line 28) | class PlaylistFacade {
    method constructor (line 29) | constructor(
    method duplicatePlaylist (line 43) | public async duplicatePlaylist(playlistId: number, name: string) {
    method mergePlaylists (line 118) | public async mergePlaylists(sourcePlaylistIds: number[], title: string) {
    method updateTrackLocalPlaylists (line 180) | public async updateTrackLocalPlaylists(params: {
    method batchAddTracksToLocalPlaylist (line 321) | public async batchAddTracksToLocalPlaylist(
    method saveQueueAsPlaylist (line 414) | public async saveQueueAsPlaylist(name: string, uniqueKeys: string[]) {
    method removeTracksFromPlaylist (line 477) | public async removeTracksFromPlaylist(
    method reorderLocalPlaylistTrack (line 533) | public async reorderLocalPlaylistTrack(
    method updatePlaylistMetadata (line 591) | public async updatePlaylistMetadata(
    method deletePlaylist (line 657) | public async deletePlaylist(playlistId: number) {
    method enqueueSync (line 768) | private async enqueueSync(

FILE: apps/mobile/src/lib/facades/sharedPlaylist.ts
  type Tx (line 32) | type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
  type SharedPlaylistPreview (line 36) | interface SharedPlaylistPreview {
  class SharedPlaylistFacade (line 65) | class SharedPlaylistFacade {
    method constructor (line 66) | constructor(
    method enableSharing (line 82) | public enableSharing(
    method subscribeToPlaylist (line 203) | public subscribeToPlaylist(params: {
    method getPreview (line 311) | public getPreview(
    method restoreFromCloud (line 389) | public restoreFromCloud(): ResultAsync<
    method pullChanges (line 514) | public pullChanges(
    method unsubscribeFromPlaylist (line 620) | public unsubscribeFromPlaylist(
    method rotateEditorInviteCode (line 681) | public rotateEditorInviteCode(
    method getEditorInviteCode (line 710) | public getEditorInviteCode(
    method _applyPullResponse (line 740) | private async _applyPullResponse(

FILE: apps/mobile/src/lib/facades/syncBilibiliPlaylist.ts
  type FavoriteSyncProgress (line 32) | interface FavoriteSyncProgress {
  class SyncBilibiliPlaylistFacade (line 48) | class SyncBilibiliPlaylistFacade {
    method constructor (line 50) | constructor(
    method addTrackFromBilibiliApi (line 64) | public addTrackFromBilibiliApi(
    method addTrackToLocal (line 104) | public addTrackToLocal(track: Track) {
    method syncCollection (line 127) | public syncCollection(
    method syncMultiPageVideo (line 269) | public syncMultiPageVideo(
    method syncFavorite (line 381) | public async syncFavorite(
    method sync (line 766) | public sync(
    method dbInstance (line 790) | public get dbInstance() {

FILE: apps/mobile/src/lib/facades/syncExternalPlaylist.ts
  class SyncExternalPlaylistFacade (line 23) | class SyncExternalPlaylistFacade {
    method constructor (line 24) | constructor(
    method saveMatchedPlaylist (line 36) | public saveMatchedPlaylist(

FILE: apps/mobile/src/lib/player/PlayerSideEffects.ts
  class PlayerSideEffects (line 18) | class PlayerSideEffects {
    method initialize (line 21) | public initialize() {
    method registerHeadlessTask (line 44) | private registerHeadlessTask() {
    method getPlayerErrorInfo (line 69) | private async getPlayerErrorInfo(
    method toSentryError (line 180) | private toSentryError(event: PlaybackErrorEvent): Error {
    method setupErrorHandler (line 195) | private setupErrorHandler() {

FILE: apps/mobile/src/lib/player/progressListener.ts
  type Events (line 5) | interface Events {

FILE: apps/mobile/src/lib/services/analyticsService.ts
  type PlayerAction (line 13) | type PlayerAction =
  type PlayerQueueAction (line 20) | type PlayerQueueAction = 'open_queue' | 'play_item'
  type PlaylistSyncAction (line 21) | type PlaylistSyncAction = 'sync_bilibili' | 'sync_external'
  class AnalyticsService (line 23) | class AnalyticsService {
    method safeLogEvent (line 24) | private async safeLogEvent(name: string, params?: Record<string, unkno...
    method logPlayerAction (line 33) | public async logPlayerAction(
    method logPlayerQueueAction (line 43) | public async logPlayerQueueAction(action: PlayerQueueAction) {
    method logPlaylistSync (line 49) | public async logPlaylistSync(
    method logSearch (line 61) | public async logSearch(type: 'global' | 'fav') {
    method logScreenView (line 67) | public async logScreenView(
    method setUserProperty (line 84) | public async setUserProperty(name: string, value: string) {
    method logPlaybackSession (line 98) | public async logPlaybackSession(durationSeconds: number) {
    method setAnalyticsCollectionEnabled (line 104) | public async setAnalyticsCollectionEnabled(enabled: boolean) {
    method logAppInfo (line 109) | public async logAppInfo(version: string, buildVersion: string) {

FILE: apps/mobile/src/lib/services/artistService.ts
  type Tx (line 23) | type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
  type DBLike (line 24) | type DBLike = ExpoSQLiteDatabase<typeof schema> | Tx
  class ArtistService (line 26) | class ArtistService {
    method constructor (line 27) | constructor(
    method withDB (line 37) | public withDB(conn: DBLike) {
    method createArtist (line 46) | public createArtist(
    method findOrCreateArtist (line 74) | public findOrCreateArtist(
    method updateArtist (line 136) | public updateArtist(
    method deleteArtist (line 188) | public deleteArtist(
    method getArtistTracks (line 231) | public getArtistTracks(
    method getAllArtists (line 270) | public getAllArtists(): ResultAsync<
    method getArtistById (line 287) | public getArtistById(
    method findOrCreateManyRemoteArtists (line 312) | public findOrCreateManyRemoteArtists(

FILE: apps/mobile/src/lib/services/externalPlaylistService.ts
  constant MIN_DELAY (line 16) | const MIN_DELAY = 1200 // 防封号延迟 (ms)
  constant BLACKLIST_ZONES (line 17) | const BLACKLIST_ZONES = new Set([26, 29, 31, 201, 238]) // 黑名单分区 (音MAD, ...
  constant PRIORITY_ZONES (line 18) | const PRIORITY_ZONES = new Set([193, 130, 267]) // 优先分区 (MV, 音乐综合, 电台)
  type MatchCandidate (line 22) | interface MatchCandidate {
  type MatchResult (line 27) | interface MatchResult {
  class ExternalPlaylistService (line 32) | class ExternalPlaylistService {
    method fetchExternalPlaylist (line 33) | public fetchExternalPlaylist(
    method matchExternalPlaylist (line 123) | public matchExternalPlaylist(
    method findBestMatchSimple (line 214) | private findBestMatchSimple(
    method findBestMatch (line 241) | private findBestMatch(
    method rankScore (line 260) | private rankScore(

FILE: apps/mobile/src/lib/services/genKey.ts
  function generateUniqueTrackKey (line 11) | function generateUniqueTrackKey(

FILE: apps/mobile/src/lib/services/lyricService.ts
  type oldLyricFileType (line 28) | type oldLyricFileType =
  class LyricService (line 34) | class LyricService {
    method constructor (line 35) | constructor(
    method cleanKeyword (line 45) | private cleanKeyword(keyword: string): string {
    method getBestMatchedLyrics (line 71) | public getBestMatchedLyrics(
    method smartFetchLyrics (line 173) | public smartFetchLyrics(
    method skipLyric (line 271) | public skipLyric(
    method processAndSaveLyrics (line 284) | private processAndSaveLyrics(
    method saveLyricsToFile (line 297) | public saveLyricsToFile(
    method fetchLyrics (line 331) | public fetchLyrics(
    method migrateFromOldFormat (line 387) | public async migrateFromOldFormat() {
    method getPreciseMusicNameOnBilibiliVideo (line 476) | public async getPreciseMusicNameOnBilibiliVideo(
    method clearAllLyrics (line 505) | public clearAllLyrics(): Result<true, unknown> {
    method pushLyricsToOverlays (line 529) | public pushLyricsToOverlays(trackId: string) {
    method preloadNextTrackLyrics (line 628) | public async preloadNextTrackLyrics() {

FILE: apps/mobile/src/lib/services/playlistService.ts
  type Tx (line 28) | type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
  type DBLike (line 29) | type DBLike = ExpoSQLiteDatabase<typeof schema> | Tx
  type PlaylistTrackRow (line 30) | type PlaylistTrackRow = typeof schema.playlistTracks.$inferSelect & {
  type DynamicPlaylistTrackSqlRow (line 37) | type DynamicPlaylistTrackSqlRow = {
  type DynamicPlaylistStats (line 68) | type DynamicPlaylistStats = {
  class PlaylistService (line 77) | class PlaylistService {
    method constructor (line 78) | constructor(
    method withDB (line 88) | withDB(conn: DBLike) {
    method parseDynamicCursor (line 92) | private parseDynamicCursor(cursor?: {
    method mapDynamicPlaylistTrackRow (line 114) | private mapDynamicPlaylistTrackRow(
    method dynamicPlaylistRowsCte (line 167) | private dynamicPlaylistRowsCte(playlistId: number) {
    method queryDynamicPlaylistTrackRows (line 193) | private async queryDynamicPlaylistTrackRows({
    method getDynamicPlaylistStats (line 287) | private async getDynamicPlaylistStats(
    method getDynamicPlaylistCounts (line 319) | private async getDynamicPlaylistCounts(playlistIds: number[]) {
    method createPlaylist (line 361) | public createPlaylist(
    method updatePlaylistMetadata (line 408) | public updatePlaylistMetadata(
    method deletePlaylist (line 487) | public deletePlaylist(
    method addManyTracksToLocalPlaylist (line 529) | public addManyTracksToLocalPlaylist(
    method batchRemoveTracksFromLocalPlaylist (line 626) | public batchRemoveTracksFromLocalPlaylist(
    method reorderSingleLocalPlaylistTrack (line 712) | public reorderSingleLocalPlaylistTrack(
    method getPlaylistTracks (line 779) | public getPlaylistTracks(
    method getAllPlaylists (line 842) | public getAllPlaylists(): ResultAsync<
    method getPlaylistMetadata (line 885) | public getPlaylistMetadata(playlistId: number): ResultAsync<
    method findOrCreateRemotePlaylist (line 957) | public findOrCreateRemotePlaylist(
    method replacePlaylistAllTracks (line 1021) | public replacePlaylistAllTracks(
    method findPlaylistByTypeAndRemoteId (line 1080) | public findPlaylistByTypeAndRemoteId(
    method getPlaylistById (line 1110) | public getPlaylistById(playlistId: number) {
    method getLocalPlaylistsContainingTrackByUniqueKey (line 1129) | public getLocalPlaylistsContainingTrackByUniqueKey(
    method getLocalPlaylistsContainingTrackById (line 1172) | public getLocalPlaylistsContainingTrackById(
    method searchPlaylists (line 1205) | public searchPlaylists(query: string): ResultAsync<
    method searchTrackInPlaylist (line 1252) | public searchTrackInPlaylist(
    method getPlaylistTracksPaginated (line 1346) | public getPlaylistTracksPaginated(options: {
    method findPlaylistByShareId (line 1496) | public findPlaylistByShareId(
    method getSharedPlaylists (line 1515) | public getSharedPlaylists(): ResultAsync<

FILE: apps/mobile/src/lib/services/syncLocalToBilibiliService.ts
  class SyncLocalToBilibiliService (line 12) | class SyncLocalToBilibiliService {
    method findRemotePlaylistByName (line 16) | findRemotePlaylistByName(
    method createRemotePlaylist (line 34) | createRemotePlaylist(
    method calculateSyncDiff (line 48) | async calculateSyncDiff(
    method executeBatchAdd (line 95) | async executeBatchAdd(
    method executeBatchRemove (line 153) | async executeBatchRemove(

FILE: apps/mobile/src/lib/services/trackService.ts
  type Tx (line 35) | type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
  type DBLike (line 36) | type DBLike = ExpoSQLiteDatabase<typeof schema> | Tx
  type SelectTrackBase (line 37) | type SelectTrackBase = typeof schema.tracks.$inferSelect
  type SelectTrackWithMetadata (line 38) | type SelectTrackWithMetadata = SelectTrackBase & {
  class TrackService (line 44) | class TrackService {
    method constructor (line 45) | constructor(private readonly db: DBLike) {}
    method withDB (line 52) | withDB(conn: DBLike) {
    method formatTrack (line 61) | public formatTrack(
    method _createTrack (line 103) | private _createTrack(
    method updateTrack (line 193) | public updateTrack(
    method getTrackById (line 228) | public getTrackById(
    method deleteTrack (line 260) | public deleteTrack(
    method addPlayRecordFromTrackId (line 289) | public addPlayRecordFromTrackId(
    method addPlayRecordFromUniqueKey (line 317) | public addPlayRecordFromUniqueKey(
    method getTrackByBilibiliMetadata (line 350) | public getTrackByBilibiliMetadata(
    method findOrCreateTrack (line 401) | public findOrCreateTrack(
    method findOrCreateManyTracks (line 457) | public findOrCreateManyTracks(
    method findTrackIdsByUniqueKeys (line 603) | public findTrackIdsByUniqueKeys(
    method getPlayCountHistoryPaginated (line 645) | public getPlayCountHistoryPaginated(options: {
    method getTotalPlaybackDuration (line 775) | public getTotalPlaybackDuration(options?: {
    method getTrackByUniqueKey (line 834) | public getTrackByUniqueKey(
    method getMostPlayedTracksInLastDays (line 869) | public getMostPlayedTracksInLastDays(options: {

FILE: apps/mobile/src/lib/services/updateService.ts
  type ReleaseInfo (line 6) | interface ReleaseInfo {
  type UpdateDownloads (line 15) | interface UpdateDownloads {
  type UpdateManifest (line 19) | interface UpdateManifest {
  function fetchLatestRelease (line 59) | async function fetchLatestRelease(): Promise<
  function checkForAppUpdate (line 113) | async function checkForAppUpdate(): Promise<

FILE: apps/mobile/src/lib/theme/material3Colors.ts
  function buildMaterial3PaperColors (line 9) | function buildMaterial3PaperColors(

FILE: apps/mobile/src/lib/workers/PlaylistSyncWorker.ts
  type QueueRow (line 11) | type QueueRow = typeof schema.playlistSyncQueue.$inferSelect
  type TrackMeta (line 13) | type TrackMeta = {
  class PlaylistSyncWorker (line 29) | class PlaylistSyncWorker {
    method triggerSync (line 33) | triggerSync() {
    method recoverStuckRows (line 43) | async recoverStuckRows(): Promise<void> {
    method syncAllPlaylists (line 66) | private async syncAllPlaylists(): Promise<void> {
    method syncSinglePlaylist (line 96) | private async syncSinglePlaylist(playlistId: number): Promise<void> {
    method pushTrackChanges (line 155) | private async pushTrackChanges(
    method collectTrackIds (line 224) | private collectTrackIds(rows: QueueRow[]): Set<number> {
    method mapTrackChangesToApi (line 243) | private mapTrackChangesToApi(
    method pushMetadataChanges (line 350) | private async pushMetadataChanges(
    method fetchTrackMetadata (line 388) | private async fetchTrackMetadata(
    method parsePayload (line 451) | private parsePayload(payload: unknown): Record<string, unknown> {
    method toMillis (line 465) | private toMillis(value: unknown): number {
    method markRows (line 475) | private async markRows(
    method deleteRows (line 487) | private async deleteRows(ids: number[]): Promise<void> {

FILE: apps/mobile/src/theme/dimensions.ts
  constant LIST_ITEM_COVER_SIZE (line 1) | const LIST_ITEM_COVER_SIZE = 48
  constant LIST_ITEM_BORDER_RADIUS (line 2) | const LIST_ITEM_BORDER_RADIUS = 12
  constant SQUIRCLE_RADIUS_RATIO (line 4) | const SQUIRCLE_RADIUS_RATIO = 0.22
  constant SQUIRCLE_CORNER_SMOOTHING (line 6) | const SQUIRCLE_CORNER_SMOOTHING = 0.6
  constant LIST_ITEM_HEIGHT (line 8) | const LIST_ITEM_HEIGHT = 64

FILE: apps/mobile/src/types/apis/baidu.ts
  type BaiduSearchResponse (line 1) | interface BaiduSearchResponse {
  type BaiduLyricResponse (line 19) | interface BaiduLyricResponse {

FILE: apps/mobile/src/types/apis/bilibili.ts
  type BilibiliAudioStreamParams (line 4) | interface BilibiliAudioStreamParams {
  type BilibiliAudioStreamResponse (line 15) | interface BilibiliAudioStreamResponse {
  type BilibiliHistoryVideo (line 66) | interface BilibiliHistoryVideo {
  type BilibiliVideoDetails (line 83) | interface BilibiliVideoDetails {
  type BilibiliVideoDetailsPage (line 103) | interface BilibiliVideoDetailsPage {
  type BilibiliPlaylist (line 112) | interface BilibiliPlaylist {
  type BilibiliSearchVideo (line 122) | interface BilibiliSearchVideo {
  type BilibiliHotSearch (line 137) | interface BilibiliHotSearch {
  type BilibiliUserInfo (line 145) | interface BilibiliUserInfo {
  type BilibiliFavoriteListContent (line 155) | interface BilibiliFavoriteListContent {
  type BilibiliFavoriteListContents (line 175) | interface BilibiliFavoriteListContents {
  type BilibiliFavoriteListAllContents (line 196) | type BilibiliFavoriteListAllContents = {
  type BilibiliCollection (line 205) | interface BilibiliCollection {
  type BilibiliCollectionContent (line 224) | interface BilibiliCollectionContent {
  type BilibiliCollectionInfo (line 255) | interface BilibiliCollectionInfo {
  type BilibiliMediaItemInCollection (line 276) | interface BilibiliMediaItemInCollection {
  type BilibiliCollectionAllContents (line 298) | interface BilibiliCollectionAllContents {
  type BilibiliMultipageVideo (line 306) | interface BilibiliMultipageVideo {
  type BilibiliDealFavoriteForOneVideoResponse (line 317) | interface BilibiliDealFavoriteForOneVideoResponse {
  type BilibiliUserUploadedVideosResponse (line 327) | interface BilibiliUserUploadedVideosResponse {
  type BilibiliQrCodeLoginStatus (line 346) | enum BilibiliQrCodeLoginStatus {
  type BilibiliCaptchaTokenData (line 356) | interface BilibiliCaptchaTokenData {
  type BilibiliSmsSendData (line 370) | interface BilibiliSmsSendData {
  type BilibiliSmsLoginData (line 377) | interface BilibiliSmsLoginData {
  type BilibiliSearchSuggestionItem (line 396) | interface BilibiliSearchSuggestionItem {
  type BilibiliWebPlayerInfo (line 405) | interface BilibiliWebPlayerInfo {
  type BilibiliToViewVideoList (line 413) | interface BilibiliToViewVideoList {
  type BilibiliCommentMember (line 436) | interface BilibiliCommentMember {
  type BilibiliCommentContent (line 451) | interface BilibiliCommentContent {
  type BilibiliCommentItem (line 469) | interface BilibiliCommentItem {
  type BilibiliCommentsResponse (line 503) | interface BilibiliCommentsResponse {
  type BilibiliReplyCommentsResponse (line 525) | interface BilibiliReplyCommentsResponse {
  type BilibiliDanmakuItem (line 538) | interface BilibiliDanmakuItem {

FILE: apps/mobile/src/types/apis/kugou.ts
  type KugouSearchResponse (line 1) | interface KugouSearchResponse {
  type KugouLyricSearchResponse (line 16) | interface KugouLyricSearchResponse {
  type KugouLyricDownloadResponse (line 28) | interface KugouLyricDownloadResponse {

FILE: apps/mobile/src/types/apis/kuwo.ts
  type KuwoSearchResponse (line 1) | interface KuwoSearchResponse {
  type KuwoLyricResponse (line 23) | interface KuwoLyricResponse {

FILE: apps/mobile/src/types/apis/netease.ts
  type NeteasePlaylistResponse (line 1) | interface NeteasePlaylistResponse {
  type NeteasePlaylist (line 6) | interface NeteasePlaylist {
  type NeteaseCreator (line 24) | interface NeteaseCreator {
  type NeteaseSong (line 33) | interface NeteaseSong {
  type NeteaseArtist (line 43) | interface NeteaseArtist {
  type NeteaseAlbum (line 50) | interface NeteaseAlbum {
  type NeteaseLyricResponse (line 57) | interface NeteaseLyricResponse {
  type NeteaseSearchResponse (line 90) | interface NeteaseSearchResponse {

FILE: apps/mobile/src/types/apis/qqmusic.ts
  type QQMusicSearchResponse (line 1) | interface QQMusicSearchResponse {
  type QQMusicSong (line 31) | interface QQMusicSong {
  type QQMusicLyricResponse (line 65) | interface QQMusicLyricResponse {
  type QQMusicPlaylistResponse (line 73) | interface QQMusicPlaylistResponse {
  type QQMusicPlaylist (line 80) | interface QQMusicPlaylist {

FILE: apps/mobile/src/types/core/appStore.ts
  type Settings (line 3) | interface Settings {
  type BilibiliUserSummary (line 18) | interface BilibiliUserSummary {
  type AppState (line 25) | interface AppState {

FILE: apps/mobile/src/types/core/downloadManagerStore.ts
  type DownloadTask (line 1) | interface DownloadTask {
  type DownloadState (line 9) | interface DownloadState {
  type DownloadActions (line 14) | interface DownloadActions {

FILE: apps/mobile/src/types/core/media.ts
  type Artist (line 1) | interface Artist {
  type PlayRecord (line 12) | interface PlayRecord {
  type BaseTrack (line 18) | interface BaseTrack {
  type BilibiliTrack (line 30) | interface BilibiliTrack extends BaseTrack {
  type LocalTrack (line 58) | interface LocalTrack extends BaseTrack {
  type Track (line 65) | type Track = BilibiliTrack | LocalTrack
  type Playlist (line 67) | interface Playlist {

FILE: apps/mobile/src/types/core/scope.ts
  type ProjectScope (line 4) | enum ProjectScope {

FILE: apps/mobile/src/types/external_playlist.ts
  type GenericTrack (line 1) | interface GenericTrack {
  type GenericPlaylist (line 10) | interface GenericPlaylist {

FILE: apps/mobile/src/types/flashlist.ts
  type ListRenderItemInfoWithExtraData (line 3) | type ListRenderItemInfoWithExtraData<TItem, TExtraData> = Omit<
  type SelectionState (line 13) | interface SelectionState {

FILE: apps/mobile/src/types/navigation.ts
  type ModalPropsMap (line 9) | interface ModalPropsMap {
  type ModalKey (line 55) | type ModalKey = keyof ModalPropsMap
  type ModalInstance (line 56) | interface ModalInstance<K extends ModalKey = ModalKey> {

FILE: apps/mobile/src/types/player/lyrics.ts
  type Tags (line 1) | type Tags = Record<string, string>
  type OldLyricLine (line 3) | interface OldLyricLine {
  type ParsedLrc (line 18) | interface ParsedLrc {
  type LyricSearchResult (line 26) | type LyricSearchResult = (
  type LyricFileData (line 50) | interface LyricFileData {
  type LyricProviderResponseData (line 77) | type LyricProviderResponseData = Omit<

FILE: apps/mobile/src/types/services/artist.ts
  type CreateArtistPayload (line 1) | interface CreateArtistPayload {
  type UpdateArtistPayload (line 9) | interface UpdateArtistPayload {

FILE: apps/mobile/src/types/services/playlist.ts
  type SharedPlaylistRole (line 1) | type SharedPlaylistRole = 'owner' | 'editor' | 'subscriber'
  type CreatePlaylistPayload (line 3) | interface CreatePlaylistPayload {
  type UpdatePlaylistPayload (line 15) | interface UpdatePlaylistPayload {
  type ReorderLocalPlaylistTrackPayload (line 25) | interface ReorderLocalPlaylistTrackPayload {

FILE: apps/mobile/src/types/services/track.ts
  type BilibiliMetadataPayload (line 1) | interface BilibiliMetadataPayload {
  type LocalMetadataPayload (line 9) | interface LocalMetadataPayload {
  type CreateTrackPayloadBase (line 13) | interface CreateTrackPayloadBase {
  type CreateBilibiliTrackPayload (line 20) | interface CreateBilibiliTrackPayload extends CreateTrackPayloadBase {
  type CreateLocalTrackPayload (line 25) | interface CreateLocalTrackPayload extends CreateTrackPayloadBase {
  type CreateTrackPayload (line 30) | type CreateTrackPayload =
  type UpdateTrackPayloadBase (line 45) | interface UpdateTrackPayloadBase {
  type UpdateBilibiliTrackPayload (line 53) | interface UpdateBilibiliTrackPayload extends UpdateTrackPayloadBase {
  type UpdateLocalTrackPayload (line 58) | interface UpdateLocalTrackPayload extends UpdateTrackPayloadBase {
  type UpdateTrackPayload (line 63) | type UpdateTrackPayload =
  type TrackSourceData (line 67) | type TrackSourceData =

FILE: apps/mobile/src/types/storage.ts
  type AppStorageSchema (line 1) | interface AppStorageSchema {
  type StorageKey (line 27) | type StorageKey = keyof AppStorageSchema
  type KeysForType (line 29) | type KeysForType<Schema, T> = {
  type BooleanKeys (line 33) | type BooleanKeys<Schema> = KeysForType<Schema, boolean>
  type StringKeys (line 34) | type StringKeys<Schema> = KeysForType<Schema, string>
  type NumberKeys (line 35) | type NumberKeys<Schema> = KeysForType<Schema, number>
  type BufferKeys (line 36) | type BufferKeys<Schema> = KeysForType<Schema, ArrayBuffer | ArrayBufferL...
  type TypedNativeMMKV (line 41) | interface TypedNativeMMKV<Schema> {
  type Listener (line 121) | interface Listener {
  type TypedMMKVInterface (line 125) | interface TypedMMKVInterface extends TypedNativeMMKV<AppStorageSchema> {

FILE: apps/mobile/src/utils/__tests__/sticky-mitt.test.ts
  type Events (line 5) | interface Events {

FILE: apps/mobile/src/utils/color.ts
  type RGBColor (line 1) | interface RGBColor {
  function hslToRgb (line 23) | function hslToRgb(h: number, s: number, l: number): RGBColor {
  function stringToHashCode (line 48) | function stringToHashCode(str: string): number {
  type GradientColors (line 62) | interface GradientColors {
  function getGradientColors (line 73) | function getGradientColors(name: string, isDarkMode: boolean) {
  function hexToHsl (line 99) | function hexToHsl(hex: string): { h: number; s: number; l: number } {
  function hslToString (line 137) | function hslToString(h: number, s: number, l: number): string {

FILE: apps/mobile/src/utils/danmaku.ts
  function cleanDanmaku (line 10) | function cleanDanmaku(

FILE: apps/mobile/src/utils/error-handling.ts
  function toastAndLogError (line 12) | function toastAndLogError(

FILE: apps/mobile/src/utils/log.ts
  function cleanOldLogFiles (line 53) | function cleanOldLogFiles(keepDays = 7): Result<number, Error> {
  function flatErrorMessage (line 108) | function flatErrorMessage(
  function reportErrorToSentry (line 147) | function reportErrorToSentry(

FILE: apps/mobile/src/utils/lottie.ts
  function tintLottieSource (line 6) | function tintLottieSource(

FILE: apps/mobile/src/utils/matching.ts
  function gaussian (line 7) | function gaussian(x: number, sigma: number): number {
  function lcs (line 17) | function lcs(s1: string, s2: string): number {
  function lcsScore (line 43) | function lcsScore(s1: string, s2: string): number {
  function cleanString (line 55) | function cleanString(str: string): string {

FILE: apps/mobile/src/utils/neverthrow-utils.ts
  function returnOrThrowAsync (line 8) | async function returnOrThrowAsync<T, E>(
  function wrapResultAsyncFunction (line 30) | function wrapResultAsyncFunction<A extends unknown[], T, E>(

FILE: apps/mobile/src/utils/player.ts
  function convertToOrpheusTrack (line 25) | function convertToOrpheusTrack(
  function reportPlaybackHistory (line 65) | async function reportPlaybackHistory(
  function addToQueue (line 136) | async function addToQueue({
  function getInternalPlayUri (line 204) | function getInternalPlayUri(track: Track) {
  function finalizeAndRecordCurrentTrack (line 216) | async function finalizeAndRecordCurrentTrack(

FILE: apps/mobile/src/utils/search.ts
  constant BV_REGEX (line 12) | const BV_REGEX = /(?<![A-Za-z0-9])(bv[0-9A-Za-z]{10})(?![A-Za-z0-9])/i
  constant AV_REGEX (line 13) | const AV_REGEX = /(?<![A-Za-z0-9])av(\d+)(?![A-Za-z0-9])/i
  constant SPACE_REGEX (line 14) | const SPACE_REGEX = /^\/space\/(\d+)(?:\/|$)/i
  type SearchStrategy (line 24) | type SearchStrategy =
  function matchSearchStrategies (line 40) | async function matchSearchStrategies(
  function navigateWithSearchStrategy (line 233) | function navigateWithSearchStrategy(

FILE: apps/mobile/src/utils/set.ts
  function diffSets (line 7) | function diffSets<T>(

FILE: apps/mobile/src/utils/sticky-mitt.ts
  function createStickyEmitter (line 13) | function createStickyEmitter<Events extends Record<string, any>>() {

FILE: apps/mobile/src/utils/time.ts
  function formatRelativeTime (line 15) | function formatRelativeTime(date: Date | string | number): string {
  function parseDurationString (line 51) | function parseDurationString(durationStr: string): number {

FILE: apps/mobile/src/utils/toast.ts
  type Options (line 5) | interface Options {

FILE: apps/update-publisher/src/index.ts
  type GitHubAsset (line 19) | interface GitHubAsset {
  type GitHubRelease (line 24) | interface GitHubRelease {
  type UpdateManifest (line 35) | interface UpdateManifest {
  constant REPO_ROOT (line 46) | const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../....
  constant DEFAULT_REPO (line 47) | const DEFAULT_REPO = 'bbplayer-app/BBPlayer'
  constant UPDATE_KEY (line 48) | const UPDATE_KEY = 'update_json'
  constant ANDROID_ABIS (line 49) | const ANDROID_ABIS = ['arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86'] as const
  function main (line 51) | async function main() {
  function fetchRecentReleases (line 88) | async function fetchRecentReleases(repo: string): Promise<GitHubRelease[...
  function parseRelease (line 114) | function parseRelease(value: unknown): GitHubRelease {
  function parseAsset (line 133) | function parseAsset(value: unknown): GitHubAsset {
  function requireString (line 147) | function requireString(value: unknown, field: string): string {
  function selectRelease (line 154) | async function selectRelease(
  function formatReleaseLabel (line 182) | function formatReleaseLabel(release: GitHubRelease): string {
  function createManifest (line 191) | function createManifest(release: GitHubRelease): UpdateManifest {
  function collectAndroidDownloads (line 206) | function collectAndroidDownloads(
  function inferAndroidAbi (line 218) | function inferAndroidAbi(fileName: string): string | null {
  function normalizeVersion (line 227) | function normalizeVersion(tag: string): string {
  function parseMarkdownListItems (line 231) | function parseMarkdownListItems(markdown: string): string[] | undefined {
  function writeTempManifest (line 242) | async function writeTempManifest(manifest: UpdateManifest): Promise<stri...
  function openInVSCode (line 250) | async function openInVSCode(path: string): Promise<void> {
  function readManifest (line 257) | async function readManifest(path: string): Promise<UpdateManifest> {
  function validateManifest (line 264) | function validateManifest(value: unknown): asserts value is UpdateManife...
  function printManifestSummary (line 290) | function printManifestSummary(manifest: UpdateManifest) {
  function publishToWorkersKv (line 306) | async function publishToWorkersKv(path: string) {
  function run (line 328) | async function run(

FILE: packages/eslint-plugin/rules/no-navigate-after-modal-close.js
  method create (line 39) | create(context) {

FILE: packages/heatmap/src/components/HeatMapCell.tsx
  type HeatMapCellProps (line 4) | interface HeatMapCellProps {

FILE: packages/heatmap/src/constants/theme.ts
  constant DEFAULT_LIGHT_THEME (line 3) | const DEFAULT_LIGHT_THEME: Required<HeatMapColor> = {
  constant DEFAULT_DARK_THEME (line 16) | const DEFAULT_DARK_THEME: Required<HeatMapColor> = {

FILE: packages/heatmap/src/types.ts
  type Day (line 3) | type Day = 0 | 1 | 2 | 3 | 4 | 5 | 6
  type HeatMapDailyProps (line 5) | type HeatMapDailyProps = {
  type HeatMapWeeklyProps (line 9) | type HeatMapWeeklyProps = {
  type HeatMapScheme (line 14) | type HeatMapScheme = 'light' | 'dark'
  type HeatMapColor (line 16) | type HeatMapColor = {
  type HeatMapThemeProps (line 24) | type HeatMapThemeProps = HeatMapColor & {
  type HeatMapDimensionsProps (line 30) | type HeatMapDimensionsProps = {
  type HeatMapStyle (line 40) | type HeatMapStyle = {
  type HeatMapControllerProps (line 45) | type HeatMapControllerProps = {
  type HeatMapFormatterProps (line 56) | type HeatMapFormatterProps = {
  type HeatMapDatetimeProps (line 63) | type HeatMapDatetimeProps = {
  type HeatMapActionsProps (line 69) | type HeatMapActionsProps = {
  type HeatMapProps (line 80) | type HeatMapProps = HeatMapDailyProps &

FILE: packages/heatmap/src/utils/calendar.ts
  function getDaysInRange (line 10) | function getDaysInRange(startDate: Date, endDate: Date): Date[] {
  function getMonthlyData (line 24) | function getMonthlyData(startDate: Date, endDate: Date) {
  function getWeeklyData (line 49) | function getWeeklyData(
  function countData (line 72) | function countData(
  function getLevel (line 88) | function getLevel(
  function getColor (line 104) | function getColor(

FILE: packages/image-theme-colors/example/App.tsx
  function App (line 13) | function App() {

FILE: packages/image-theme-colors/src/ExpoImageThemeColors.types.ts
  type ColorInfo (line 4) | interface ColorInfo {
  type SwatchName (line 18) | type SwatchName =
  type PaletteSwatches (line 27) | type PaletteSwatches = Record<SwatchName, ColorInfo | null>
  type ExtractedPalette (line 29) | type ExtractedPalette = {

FILE: packages/image-theme-colors/src/ExpoImageThemeColorsModule.ts
  class ExpoImageThemeColorsModule (line 6) | class ExpoImageThemeColorsModule extends NativeModule {

FILE: packages/image-theme-colors/src/ImageRef.ts
  class ImageRef (line 6) | class ImageRef extends SharedRef<'image'> {

FILE: packages/logs/src/index.ts
  type transportFunctionType (line 88) | type transportFunctionType<T extends object> = (props: {
  type levelsType (line 96) | type levelsType = { [key: string]: number }
  type logMethodType (line 98) | type logMethodType = (
  type levelLogMethodType (line 103) | type levelLogMethodType = (...msgs: any[]) => boolean
  type extendedLogType (line 105) | type extendedLogType = { [key: string]: levelLogMethodType | any }
  type ExtractOptions (line 107) | type ExtractOptions<T> = T extends transportFunctionType<infer U> ? U : ...
  type MergeTransportOptions (line 109) | type MergeTransportOptions<T> = T extends (infer U)[]
  type UnionToIntersection (line 113) | type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) e...
  type configLoggerType (line 119) | type configLoggerType<
  type OptionsWithConsoleFunc (line 178) | type OptionsWithConsoleFunc = {
  class logs (line 183) | class logs<
    method constructor (line 212) | constructor(config: Required<configLoggerType<T, K>>) {
  type defLvlType (line 586) | type defLvlType = 'debug' | 'info' | 'warn' | 'error'
  type levelMethods (line 601) | type levelMethods<levels extends string> = {
  type loggerType (line 605) | type loggerType = levelMethods<Y>
  type extendMethods (line 607) | type extendMethods = {

FILE: packages/logs/src/transports/consoleTransport.ts
  type Color (line 25) | type Color = keyof typeof availableColors
  type ConsoleTransportOptions (line 27) | type ConsoleTransportOptions = {

FILE: packages/logs/src/transports/crashlyticsTransport.ts
  type CrashlyticsTransportOption (line 3) | type CrashlyticsTransportOption = {

FILE: packages/logs/src/transports/fileAsyncTransport.ts
  type RNFS (line 3) | type RNFS = {
  type EXPOFS (line 13) | type EXPOFS = {
  type EXPONEXTFS (line 28) | type EXPONEXTFS = {
  type EXPOqueueitem (line 44) | interface EXPOqueueitem {
  type EXPONEXTFSqueueitem (line 53) | interface EXPONEXTFSqueueitem {
  type FileAsyncTransportOptions (line 203) | interface FileAsyncTransportOptions {

FILE: packages/logs/src/transports/mapConsoleTransport.ts
  type ConsoleMethod (line 3) | type ConsoleMethod = 'log' | 'warn' | 'error' | 'info' | (string & {})
  type LogLevel (line 4) | type LogLevel = string
  type MapConsoleTransportOptions (line 6) | type MapConsoleTransportOptions = {

FILE: packages/logs/src/transports/sentryTransport.ts
  type SentryTransportOptions (line 3) | type SentryTransportOptions = {

FILE: packages/logs/test/consoleTransport.test.js
  function testFunc (line 25) | function testFunc() {

FILE: packages/native/src/BBPlayerNative.types.ts
  type AppUpdateDownloadOptions (line 1) | interface AppUpdateDownloadOptions {
  type AppUpdateInstallResult (line 8) | interface AppUpdateInstallResult {

FILE: packages/native/src/BBPlayerNativeModule.ts
  class BBPlayerNativeModule (line 9) | class BBPlayerNativeModule extends NativeModule {

FILE: packages/orpheus/example/App.tsx
  function OrpheusTestScreen (line 15) | function OrpheusTestScreen() {

FILE: packages/orpheus/example/src/components/Buttons.tsx
  type ControlButtonProps (line 4) | interface ControlButtonProps {
  type ButtonProps (line 18) | interface ButtonProps {

FILE: packages/orpheus/example/src/components/DebugSection.tsx
  type DebugSectionProps (line 9) | interface DebugSectionProps {

FILE: packages/orpheus/example/src/components/PlayerControls.tsx
  type PlayerControlsProps (line 12) | interface PlayerControlsProps {

FILE: packages/orpheus/example/src/components/SpectrumVisualizer.tsx
  constant BAR_COUNT (line 5) | const BAR_COUNT = 32
  constant FFT_SIZE (line 6) | const FFT_SIZE = 1024 // Buffer size we might pull, but we only show 32 ...

FILE: packages/orpheus/example/src/constants.ts
  constant TEST_TRACKS (line 3) | const TEST_TRACKS: Track[] = [

FILE: packages/orpheus/src/ExpoOrpheusModule.ts
  type PlaybackState (line 3) | enum PlaybackState {
  type RepeatMode (line 10) | enum RepeatMode {
  type TransitionReason (line 16) | enum TransitionReason {
  type Track (line 23) | interface Track {
  type LyricSpan (line 32) | interface LyricSpan {
  type LyricLine (line 39) | interface LyricLine {
  type LyricsData (line 48) | interface LyricsData {
  type LyricConsumer (line 53) | type LyricConsumer = 'desktop' | 'statusBar' | 'car'
  type AndroidPlaybackErrorEvent (line 55) | interface AndroidPlaybackErrorEvent {
  type IosPlaybackErrorEvent (line 66) | interface IosPlaybackErrorEvent {
  type PlaybackErrorEvent (line 71) | type PlaybackErrorEvent =
  type OrpheusEvents (line 75) | type OrpheusEvents = {
  type OrpheusHeadlessTrackStartedEvent (line 109) | interface OrpheusHeadlessTrackStartedEvent {
  type OrpheusHeadlessTrackFinishedEvent (line 115) | interface OrpheusHeadlessTrackFinishedEvent {
  type OrpheusHeadlessTrackPausedEvent (line 122) | interface OrpheusHeadlessTrackPausedEvent {
  type OrpheusHeadlessTrackResumedEvent (line 126) | interface OrpheusHeadlessTrackResumedEvent {
  type OrpheusHeadlessRequestClearLyricsEvent (line 130) | interface OrpheusHeadlessRequestClearLyricsEvent {
  type OrpheusHeadlessEvent (line 135) | type OrpheusHeadlessEvent =
  class NativeOrpheusModule (line 143) | class NativeOrpheusModule extends NativeModule<OrpheusEvents> {
  type PublicOrpheusModule (line 227) | type PublicOrpheusModule = Omit<NativeOrpheusModule, 'setLyricsInternal'...
  constant SPECTRUM_SIZE (line 246) | const SPECTRUM_SIZE = 512
  type DownloadState (line 248) | enum DownloadState {
  type DownloadTask (line 258) | interface DownloadTask {

FILE: packages/orpheus/src/headless.ts
  constant ORPHEUS_HEADLESS_TASK (line 5) | const ORPHEUS_HEADLESS_TASK = 'OrpheusHeadlessTask'
  function registerOrpheusHeadlessTask (line 7) | function registerOrpheusHeadlessTask(

FILE: packages/orpheus/src/hooks/useCurrentTrack.ts
  function useCurrentTrack (line 5) | function useCurrentTrack() {

FILE: packages/orpheus/src/hooks/useIsPlaying.ts
  function useIsPlaying (line 5) | function useIsPlaying() {

FILE: packages/orpheus/src/hooks/useOrpheus.ts
  function useOrpheus (line 6) | function useOrpheus() {

FILE: packages/orpheus/src/hooks/usePlaybackState.ts
  function usePlaybackState (line 5) | function usePlaybackState() {

FILE: packages/orpheus/src/hooks/useProgress.ts
  type OrpheusSubscription (line 6) | type OrpheusSubscription = ReturnType<typeof Orpheus.addListener>
  function useProgress (line 8) | function useProgress() {

FILE: packages/splash/src/converter/netease.ts
  type YrcLine (line 1) | interface YrcLine {
  function formatSplTime (line 6) | function formatSplTime(ms: number): string {
  function parseYrc (line 22) | function parseYrc(yrcContent: string): string {

FILE: packages/splash/src/parser/index.ts
  function parseSpl (line 14) | function parseSpl(lrcContent: string): SplLyricData {
  function verify (line 140) | function verify(

FILE: packages/splash/src/parser/merge.ts
  type MultiLyricsInput (line 5) | interface MultiLyricsInput {
  function isMatch (line 14) | function isMatch(mainLines: LyricLine[], secondaryLines: LyricLine[]): b...
  function parseAndMergeLyrics (line 28) | function parseAndMergeLyrics(input: MultiLyricsInput): LyricLine[] {

FILE: packages/splash/src/parser/spans.ts
  function parseSpans (line 12) | function parseSpans(

FILE: packages/splash/src/types.ts
  type LyricSpan (line 4) | interface LyricSpan {
  type LyricLine (line 18) | interface LyricLine {
  type SplLyricData (line 40) | interface SplLyricData {
  type RawLine (line 50) | interface RawLine {
  class SplParseError (line 59) | class SplParseError extends Error {
    method constructor (line 60) | constructor(

FILE: packages/splash/src/utils/time.ts
  function parseTimeTag (line 13) | function parseTimeTag(timeStr: string): number {
Condensed preview — 676 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (5,347K chars).
[
  {
    "path": ".agent/rules/changelog.md",
    "chars": 97,
    "preview": "---\ntrigger: always_on\n---\n\n在完成一个任务后,应该去修改 CHANGELOG.md 以反应这个任务的更改。CHANGELOG 应该尽量简洁,一个任务只对应一条记录。\n"
  },
  {
    "path": ".agent/rules/measure-layout.md",
    "chars": 1266,
    "preview": "---\ndescription: Best practice for measuring component layout in React Native\n---\n\n# Measuring Component Layout\n\nWhen yo"
  },
  {
    "path": ".agent/skills/gesture-handler-3-migration/SKILL.md",
    "chars": 8569,
    "preview": "---\nname: gesture-handler-3-migration\ndescription: Migrates files containing React Native components which use the React"
  },
  {
    "path": ".agent/skills/upgrading-expo/SKILL.md",
    "chars": 3693,
    "preview": "---\nname: upgrading-expo\ndescription: Guidelines for upgrading Expo SDK versions and fixing dependency issues\nversion: 1"
  },
  {
    "path": ".agent/skills/upgrading-expo/references/new-architecture.md",
    "chars": 2157,
    "preview": "# New Architecture\n\nThe New Architecture is enabled by default in Expo SDK 53+. It replaces the legacy bridge with a fas"
  },
  {
    "path": ".agent/skills/upgrading-expo/references/react-19.md",
    "chars": 1883,
    "preview": "# React 19\n\nReact 19 is included in Expo SDK 54. This release simplifies several common patterns.\n\n## Context Changes\n\n#"
  },
  {
    "path": ".agent/skills/upgrading-expo/references/react-compiler.md",
    "chars": 1631,
    "preview": "# React Compiler\n\nReact Compiler is stable in Expo SDK 54 and later. It automatically memoizes components and hooks, eli"
  },
  {
    "path": ".agents/skills/react-doctor/SKILL.md",
    "chars": 1481,
    "preview": "---\nname: react-doctor\ndescription: Diagnose and fix React codebase health issues. Use when reviewing React code, fixing"
  },
  {
    "path": ".agents/skills/react-native-ease-refactor/SKILL.md",
    "chars": 20411,
    "preview": "---\nname: react-native-ease-refactor\ndescription: Scan for Animated/Reanimated code and migrate to EaseView\nuser-invocab"
  },
  {
    "path": ".easignore",
    "chars": 986,
    "preview": "# .easignore - Overrides .gitignore for EAS builds\n\n# dependencies\nnode_modules/\n\n# Expo\n.expo/\ndist/\nweb-build/\nexpo-en"
  },
  {
    "path": ".gitattributes",
    "chars": 63,
    "preview": "apps/mobile/src/lib/api/bilibili/proto/*.js linguist-generated\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 1339,
    "preview": "name: '🐞 Bug 报告'\ndescription: '提交 Bug 报告以帮助我们改进 BBPlayer'\ntitle: '[Bug] <title>'\nlabels: ['bug']\ntype: Bug\nbody:\n  - typ"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 488,
    "preview": "name: '🚀 功能请求'\ndescription: '为项目建议一个新功能或改进'\ntitle: '[Feature] <title>'\nlabels: ['enhancement']\ntype: Feature\nbody:\n  - t"
  },
  {
    "path": ".github/release.yml",
    "chars": 320,
    "preview": "changelog:\n  categories:\n    - title: '🚀 New Features'\n      labels:\n        - 'feat'\n        - 'feature'\n    - title: '"
  },
  {
    "path": ".github/wiki/Home.md",
    "chars": 336,
    "preview": "# BBPlayer 开源项目 Wiki\n\n> [!TIP]\n> 如果您是最终用户,请优先访问 **[BBPlayer 官方文档站点](https://bbplayer.roitium.com)** 以获取安装及使用指南。\n\n---\n\n欢迎"
  },
  {
    "path": ".github/wiki/_Sidebar.md",
    "chars": 376,
    "preview": "### [BBPlayer Wiki](Home)\n\n---\n\n#### [移动端应用 (App)](App-Home)\n\n- [贡献指南](CONTRIBUTING)\n- [架构设计](ARCHITECTURE)\n- [开发规范](BES"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 9456,
    "preview": "name: Build and Release\n\non:\n  workflow_dispatch:\n    inputs:\n      buildType:\n        description: '构建类型'\n        requi"
  },
  {
    "path": ".github/workflows/check-lyricon-updates.yml",
    "chars": 14068,
    "preview": "name: Check Lyricon Updates\n\non:\n  schedule:\n    # 每天凌晨 2 点运行一次 (UTC 时间)\n    - cron: '0 2 * * *'\n  workflow_dispatch: # "
  },
  {
    "path": ".github/workflows/nightly.yml",
    "chars": 9034,
    "preview": "name: Nightly Build\n\non:\n  # 支持 Actions 页面手动触发\n  workflow_dispatch:\n\n  # 支持 PR 评论触发\n  issue_comment:\n    types: [created"
  },
  {
    "path": ".github/workflows/pr-checks.yml",
    "chars": 592,
    "preview": "name: 'PR Checks'\n\non: pull_request\n\njobs:\n  lint:\n    name: 🚨 Lint Codebase\n    runs-on: ubuntu-latest\n    permissions:"
  },
  {
    "path": ".github/workflows/update.yml",
    "chars": 883,
    "preview": "name: Hot Update\non: workflow_dispatch\njobs:\n  update:\n    runs-on: ubuntu-latest\n    permissions:\n      packages: read\n"
  },
  {
    "path": ".github/workflows/wiki.yml",
    "chars": 1188,
    "preview": "name: Publish wiki\non:\n  push:\n    branches: [dev]\n    paths:\n      - apps/mobile/docs/**\n      - packages/orpheus/docs/"
  },
  {
    "path": ".gitignore",
    "chars": 736,
    "preview": "# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files\n\n# dependencies\nnode_modules"
  },
  {
    "path": ".gitleaks-baseline.json",
    "chars": 8738,
    "preview": "[\n\t{\n\t\t\"RuleID\": \"generic-api-key\",\n\t\t\"Description\": \"Detected a Generic API Key, potentially exposing access to various"
  },
  {
    "path": ".gitleaks.toml",
    "chars": 184,
    "preview": "# https://github.com/gitleaks/gitleaks\n\ntitle = \"BBPlayer Gitleaks Config\"\n\n[extend]\nuseDefault = true\n\n[allowlist]\npath"
  },
  {
    "path": ".npmrc",
    "chars": 41,
    "preview": "node-linker=hoisted\nshamefully-hoist=true"
  },
  {
    "path": ".oxfmtrc.json",
    "chars": 717,
    "preview": "{\n\t\"$schema\": \"./node_modules/oxfmt/configuration_schema.json\",\n\t\"printWidth\": 80,\n\t\"tabWidth\": 2,\n\t\"useTabs\": true,\n\t\"s"
  },
  {
    "path": ".oxlintrc.json",
    "chars": 3369,
    "preview": "{\n\t\"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n\t\"plugins\": [\n\t\t\"react\",\n\t\t\"typescript\",\n\t\t\"unicorn\",\n\t"
  },
  {
    "path": ".sisyphus/boulder.json",
    "chars": 1164,
    "preview": "{\n\t\"active_plan\": \"/Users/roitium/Programming/BBPlayer/.sisyphus/plans/homepage-ui-optimization.md\",\n\t\"plan_name\": \"home"
  },
  {
    "path": ".sisyphus/evidence/task-1-complete.txt",
    "chars": 1366,
    "preview": "Task 1 Complete: Add getMostPlayedTracksInLastDays method\n\n## Summary\nAdded `getMostPlayedTracksInLastDays` method to Tr"
  },
  {
    "path": ".sisyphus/evidence/task-2-complete.txt",
    "chars": 1479,
    "preview": "# Task 2 Complete - useMostPlayedTracks Query Hook\n\n## Summary\nAdded `useMostPlayedTracks` query hook to `apps/mobile/sr"
  },
  {
    "path": ".sisyphus/evidence/task-3-lint-output.txt",
    "chars": 91,
    "preview": "\n> @bbplayer/mobile@2.4.2 lint /Users/roitium/Programming/BBPlayer/apps/mobile\n> eslint .\n\n"
  },
  {
    "path": ".sisyphus/evidence/task-4-page-created.txt",
    "chars": 2013,
    "preview": "Task 4: Create \"最近常听\" (Recently Played) Page - COMPLETED\n\nEvidence of Completion:\n============================\n\n1. PAGE "
  },
  {
    "path": ".sisyphus/evidence/task-5-play-all.txt",
    "chars": 1639,
    "preview": "# Task 5: Add \"Play All\" Button to history/[date].tsx\n\n## Status: COMPLETED\n\n## Changes Made\n\n### File: apps/mobile/src/"
  },
  {
    "path": ".sisyphus/evidence/task-6-structure.txt",
    "chars": 4729,
    "preview": "// ============================================================\n// QUICK ACCESS SECTION STRUCTURE (Task 6)\n// This struc"
  },
  {
    "path": ".sisyphus/evidence/task-7-complete.txt",
    "chars": 1803,
    "preview": "# Task 7 Complete: Replace \"近期歌单\" with \"快捷入口\"\n\n## Date: 2026-03-23\n\n## Changes Made\n\n### 1. Deleted recentPlaylists quer"
  },
  {
    "path": ".sisyphus/notepads/task-5-play-all/learnings.md",
    "chars": 1027,
    "preview": "# Task 5: Play All Button - Learnings\n\n## Patterns Discovered\n\n### Type Union Handling in Filter\n\nWhen filtering a union"
  },
  {
    "path": ".sisyphus/plans/homepage-ui-optimization.md",
    "chars": 21948,
    "preview": "# Homepage UI Optimization\n\n## TL;DR\n\n> **Quick Summary**: Optimize homepage by removing quick action buttons, creating "
  },
  {
    "path": ".syncpackrc",
    "chars": 724,
    "preview": "{\n\t\"dependencyTypes\": [\"dev\", \"prod\", \"peer\", \"overrides\"],\n\t\"filter\": \".\",\n\t\"indent\": \"\\t\",\n\t\"semverRange\": \"\",\n\t\"sourc"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 43,
    "preview": "{\n\t\"recommendations\": [\"oxc.oxc-vscode\"]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 629,
    "preview": "{\n\t\"files.autoSave\": \"onFocusChange\",\n\t\"editor.formatOnSave\": true,\n\t\"editor.defaultFormatter\": \"oxc.oxc-vscode\",\n\t\"[typ"
  },
  {
    "path": "AGENTS.md",
    "chars": 7624,
    "preview": "# BBPlayer Project Knowledge Base\n\n**Generated:** 2026-03-23\n**Project:** BBPlayer - Bilibili Audio Player (React Native"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2025 Roitium.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "PRIVACY.md",
    "chars": 5768,
    "preview": "**Privacy Policy**\n\nThis privacy policy applies to the BBPlayer app (hereby referred to as \"Application\") for mobile dev"
  },
  {
    "path": "README.md",
    "chars": 4248,
    "preview": "<div align=\"center\">\n<img src=\"./apps/mobile/assets/images/icon_large.png\" alt=\"logo\" width=\"50\" />\n<h1>BBPlayer</h1>\n\n一"
  },
  {
    "path": "apps/README.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "apps/backend/.dev.vars.example",
    "chars": 225,
    "preview": "DATABASE_URL=postgres://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres\n\n# You can ge"
  },
  {
    "path": "apps/backend/.gitignore",
    "chars": 50,
    "preview": ".dev.vars\n.wrangler/\nnode_modules/\ndist/\ndrizzle/\n"
  },
  {
    "path": "apps/backend/drizzle.config.ts",
    "chars": 286,
    "preview": "import { defineConfig } from 'drizzle-kit'\n\nif (!process.env.DATABASE_URL) {\n\tthrow new Error('DATABASE_URL is missing')"
  },
  {
    "path": "apps/backend/mise.toml",
    "chars": 53,
    "preview": "[env]\n_.file = { path = \".dev.vars\", redact = true }\n"
  },
  {
    "path": "apps/backend/package.json",
    "chars": 731,
    "preview": "{\n\t\"name\": \"@bbplayer/backend\",\n\t\"version\": \"0.0.1\",\n\t\"private\": true,\n\t\"types\": \"./src/index.ts\",\n\t\"exports\": {\n\t\t\".\": "
  },
  {
    "path": "apps/backend/src/db/index.ts",
    "chars": 538,
    "preview": "import { drizzle } from 'drizzle-orm/node-postgres'\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres'\nimpo"
  },
  {
    "path": "apps/backend/src/db/schema.ts",
    "chars": 3640,
    "preview": "import { sql } from 'drizzle-orm'\nimport {\n\tindex,\n\tinteger,\n\tpgTable,\n\tprimaryKey,\n\ttext,\n\ttimestamp,\n\tuniqueIndex,\n\tuu"
  },
  {
    "path": "apps/backend/src/index.ts",
    "chars": 1230,
    "preview": "import { Hono } from 'hono'\nimport { cors } from 'hono/cors'\n\nimport authRoute from './routes/auth'\nimport meRoute from "
  },
  {
    "path": "apps/backend/src/middleware/auth.ts",
    "chars": 882,
    "preview": "import { createMiddleware } from 'hono/factory'\nimport { verify } from 'hono/jwt'\n\nimport type { JwtTokenPayload } from "
  },
  {
    "path": "apps/backend/src/routes/auth.ts",
    "chars": 2529,
    "preview": "import { arktypeValidator } from '@hono/arktype-validator'\nimport { eq } from 'drizzle-orm'\nimport { Hono } from 'hono'\n"
  },
  {
    "path": "apps/backend/src/routes/me.ts",
    "chars": 1256,
    "preview": "import { and, desc, eq, isNull } from 'drizzle-orm'\nimport { Hono } from 'hono'\n\nimport { createDb } from '../db'\nimport"
  },
  {
    "path": "apps/backend/src/routes/playlists.ts",
    "chars": 20566,
    "preview": "import { arktypeValidator } from '@hono/arktype-validator'\nimport {\n\tand,\n\tasc,\n\tdesc,\n\teq,\n\tgt,\n\tisNotNull,\n\tisNull,\n\tl"
  },
  {
    "path": "apps/backend/src/types.ts",
    "chars": 1070,
    "preview": "/** JWT payload 结构 */\nexport interface JwtTokenPayload {\n\tsub: string // B 站 mid(text 存储,避免大数精度丢失)\n\tjwtVersion?: number\n"
  },
  {
    "path": "apps/backend/src/validators/auth.ts",
    "chars": 109,
    "preview": "import { type as arkType } from 'arktype'\n\nexport const loginRequestSchema = arkType({\n\tcookie: 'string',\n})\n"
  },
  {
    "path": "apps/backend/src/validators/playlists.ts",
    "chars": 1421,
    "preview": "import { type as arkType } from 'arktype'\n\nconst trackInputSchema = arkType({\n\tunique_key: 'string',\n\ttitle: 'string',\n\t"
  },
  {
    "path": "apps/backend/tsconfig.json",
    "chars": 236,
    "preview": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"ESNext\",\n\t\t\"module\": \"ESNext\",\n\t\t\"moduleResolution\": \"Bundler\",\n\t\t\"lib\": [\"ESNext\"]"
  },
  {
    "path": "apps/backend/worker-configuration.d.ts",
    "chars": 384838,
    "preview": "/* eslint-disable */\n// Generated by Wrangler by running `wrangler types` (hash: 88348ad3312309862e1f03e26b965c16)\n// Ru"
  },
  {
    "path": "apps/backend/wrangler.toml",
    "chars": 432,
    "preview": "# JWT_SECRET 和 DATABASE_URL 通过 `wrangler secret put` 设置\nname = \"bbplayer-backend\"\nmain = \"src/index.ts\"\ncompatibility_da"
  },
  {
    "path": "apps/docs/.gitignore",
    "chars": 77,
    "preview": ".vitepress/cache\ndocs/.vitepress/cache\nnode_modules\ndist\ndocs/.vitepress/dist"
  },
  {
    "path": "apps/docs/README.md",
    "chars": 246,
    "preview": "# @bbplayer/docs\n\nBBPlayer 官方文档站源代码。\n\n## 简介\n\n此目录包含基于 VitePress 构建的 BBPlayer 文档站点。它负责向用户提供安装、使用、歌词配置等全方位的指南。\n\n## 文档内容\n\n- "
  },
  {
    "path": "apps/docs/docs/.vitepress/components/AppNotExistPage.vue",
    "chars": 2806,
    "preview": "<script setup lang=\"ts\">\nimport { onMounted, ref } from 'vue'\nimport { Download, Github, RefreshCw } from 'lucide-vue-ne"
  },
  {
    "path": "apps/docs/docs/.vitepress/components/SharePlaylistPage.vue",
    "chars": 15278,
    "preview": "<script setup lang=\"ts\">\nimport { ref, onMounted, computed } from 'vue'\nimport {\n\tListMusic,\n\tPlay,\n\tAlertCircle,\n\tExter"
  },
  {
    "path": "apps/docs/docs/.vitepress/components/ShareTrackPage.vue",
    "chars": 4545,
    "preview": "<script setup lang=\"ts\">\nimport { ref, onMounted, computed } from 'vue'\nimport { Play, Tv2, AlertCircle, Music2, Externa"
  },
  {
    "path": "apps/docs/docs/.vitepress/components/shared-page.css",
    "chars": 3952,
    "preview": "/* shared-page.css — BBPlayer 共享页面设计系统 */\n\n/* ── Design tokens ─────────────────────────────────────────────────────── *"
  },
  {
    "path": "apps/docs/docs/.vitepress/config.mts",
    "chars": 1308,
    "preview": "import { defineConfig } from 'vitepress'\n\n// https://vitepress.dev/reference/site-config\nexport default defineConfig({\n\t"
  },
  {
    "path": "apps/docs/docs/SPL.md",
    "chars": 3144,
    "preview": "---\ntitle: SPL 歌词规范\neditLink: true\n---\n\n> [!NOTE]\n> 本文档转载自 [Moriafly Official | SPL 格式语法标准](https://moriafly.com/standar"
  },
  {
    "path": "apps/docs/docs/app-not-exist.md",
    "chars": 157,
    "preview": "---\nlayout: false\ntitle: 未检测到应用\n---\n\n<script setup>\nimport AppNotExistPage from './.vitepress/components/AppNotExistPage"
  },
  {
    "path": "apps/docs/docs/guides/comments.md",
    "chars": 312,
    "preview": "---\ntitle: 评论区\neditLink: true\n---\n\n# 评论区\n\nBBPlayer 支持查看 B 站视频对应的评论区,让你在听歌的同时也能看看大家的评论。\n\n## 访问方式\n\n在播放界面页面,点击 **评论** 图标(气泡"
  },
  {
    "path": "apps/docs/docs/guides/download.md",
    "chars": 829,
    "preview": "---\ntitle: 下载与导出\neditLink: true\n---\n\n### 下载\n\n我也给 BBPlayer 简单搓了个下载功能:\n\n- **缓存整个歌单**:在播放列表页面点「:arrow_down:」按钮。(是增量下载,已经下载过"
  },
  {
    "path": "apps/docs/docs/guides/external-playlist.md",
    "chars": 1025,
    "preview": "# 导入外部歌单\n\nBBPlayer 支持将 **网易云音乐** 和 **QQ 音乐** 的歌单导入到本地。通过 Bilibili 的搜索功能,会自动为你匹配歌单中的每一首歌曲。\n\n## 支持的平台\n\n- 网易云音乐\n- QQ 音乐\n\n##"
  },
  {
    "path": "apps/docs/docs/guides/index.md",
    "chars": 119,
    "preview": "---\ntitle: 指南\neditLink: true\n---\n\n# 指南\n\n虽然 BBPlayer 始终把**「简单易用」**作为开发目标,但受限于作者水平,可能仍具有一点上手门槛。你可以通过阅读本指南,快速上手 BBPlayer。\n"
  },
  {
    "path": "apps/docs/docs/guides/install.md",
    "chars": 246,
    "preview": "---\ntitle: 安装\neditLink: true\n---\n\n# 安装\n\n~~由于开发者没有 Mac 电脑,完全无法开发 iOS 端~~,故目前只有 Android 端。\n\n目前正在开发 IOS 版本,但推进较慢,别抱期待,,,\n\nB"
  },
  {
    "path": "apps/docs/docs/guides/leaderboard.md",
    "chars": 317,
    "preview": "---\ntitle: 排行榜\neditLink: true\n---\n\n# 排行榜\n\n既然我们记录了你的播放数据,那么生成一个排行榜自然是理所应当的事情。\n\n## 播放统计\n\nBBPlayer 会根据你的本地播放记录,统计出你听歌最多的曲目和"
  },
  {
    "path": "apps/docs/docs/guides/lyrics.md",
    "chars": 2640,
    "preview": "---\ntitle: 歌词\neditLink: true\n---\n\n# 歌词\n\nBBPlayer 拥有一个功能完善的歌词系统。除了基础的展示外,还支持多种进阶特性。\n\n## 获取歌词\n\n- **智能获取**:播放歌曲时,BBPlayer 会"
  },
  {
    "path": "apps/docs/docs/guides/player.md",
    "chars": 888,
    "preview": "---\ntitle: 播放器功能\neditLink: true\n---\n\n# 播放器功能\n\n除了基本的播放控制,BBPlayer 的播放器页面还藏着一些实用的小功能。\n\n## 定时关闭\n\n睡前听歌神器。\n\n### 设置方法\n\n在播放器页面,"
  },
  {
    "path": "apps/docs/docs/guides/playlist.md",
    "chars": 1632,
    "preview": "---\ntitle: 歌单\neditLink: true\n---\n\n# 歌单\n\nBBPlayer 同时支持在线和本地两种模式,所以歌单系统设计的有那么一点点绕。不过我觉得读完这篇指南,或许就差不多了。\n\n大体上看,歌单分为两大类:**在线歌"
  },
  {
    "path": "apps/docs/docs/guides/search.md",
    "chars": 896,
    "preview": "---\ntitle: 搜索\neditLink: true\n---\n\n# 搜索\n\n## 搜索框\n\n打开软件,便可以看到 BBPlayer 的搜索框。它被设计为支持识别多种链接、规则的聚合搜索器,具体来说,他支持以下这几种类型:\n\n1. `BV"
  },
  {
    "path": "apps/docs/docs/guides/settings.md",
    "chars": 545,
    "preview": "---\ntitle: 设置与个性化\neditLink: true\n---\n\n# 设置与个性化\n\n每个人都有自己的听歌习惯,BBPlayer 提供了丰富的设置选项来满足你的需求。\n\n## 播放设置\n\n在 **设置 -> 播放设置** 中,你可"
  },
  {
    "path": "apps/docs/docs/guides/shared-playlist.md",
    "chars": 1257,
    "preview": "# 共享歌单与协同编辑\n\nBBPlayer 提供了强大的共享歌单与协同编辑功能,让你可以与朋友一起分享和管理音乐。\n\n> [!CAUTION]\n> 该功能目前处于早期阶段,不会导致你的数据损坏,但可能会存在一些体验问题,请酌情考虑~ \\\n>"
  },
  {
    "path": "apps/docs/docs/index.md",
    "chars": 1158,
    "preview": "---\nlayout: home\n\nhero:\n  name: 'BBPlayer'\n  text: '更纯粹的 BiliBili 听歌方式'\n  tagline: '轻量、开源、不妥协。让 B 站音频回归它应有的样子。'\n  action"
  },
  {
    "path": "apps/docs/docs/public/.well-known/assetlinks.json",
    "chars": 918,
    "preview": "[\n\t{\n\t\t\"relation\": [\"delegate_permission/common.handle_all_urls\"],\n\t\t\"target\": {\n\t\t\t\"namespace\": \"android_app\",\n\t\t\t\"pack"
  },
  {
    "path": "apps/docs/docs/share/playlist.md",
    "chars": 162,
    "preview": "---\nlayout: false\ntitle: 共享歌单\n---\n\n<script setup>\nimport SharePlaylistPage from '../.vitepress/components/SharePlaylistP"
  },
  {
    "path": "apps/docs/docs/share/track.md",
    "chars": 153,
    "preview": "---\nlayout: false\ntitle: 分享曲目\n---\n\n<script setup>\nimport ShareTrackPage from '../.vitepress/components/ShareTrackPage.vu"
  },
  {
    "path": "apps/docs/env.d.ts",
    "chars": 293,
    "preview": "/// <reference types=\"vitepress/client\" />\n\ndeclare module '*.vue' {\n\timport type { DefineComponent } from 'vue'\n\t// oxl"
  },
  {
    "path": "apps/docs/package.json",
    "chars": 278,
    "preview": "{\n\t\"name\": \"@bbplayer/docs\",\n\t\"scripts\": {\n\t\t\"docs:build\": \"vitepress build docs\",\n\t\t\"docs:dev\": \"vitepress dev docs\",\n\t"
  },
  {
    "path": "apps/docs/tsconfig.json",
    "chars": 473,
    "preview": "{\n\t\"extends\": \"../../tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"target\": \"ESNext\",\n\t\t\"module\": \"ESNext\",\n\t\t\"moduleResoluti"
  },
  {
    "path": "apps/mobile/.gitignore",
    "chars": 409,
    "preview": "\n# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb\n# The following patterns were generated by expo-cli"
  },
  {
    "path": "apps/mobile/.maestro/comments_flow.yaml",
    "chars": 910,
    "preview": "appId: com.roitium.bbplayer\nenv:\n  COOKIE: ${COOKIE}\n---\n- runFlow: common/setup.yaml\n\n# 7. Comments & Image Viewer\n- ta"
  },
  {
    "path": "apps/mobile/.maestro/common/open_player.yaml",
    "chars": 211,
    "preview": "appId: com.roitium.bbplayer\n---\n- runFlow:\n    when:\n      notVisible:\n        id: 'player-cover'\n    commands:\n      - "
  },
  {
    "path": "apps/mobile/.maestro/common/setup.yaml",
    "chars": 1234,
    "preview": "appId: com.roitium.bbplayer\nenv:\n  COOKIE: ${COOKIE}\n---\n- launchApp:\n    clearState: false\n- waitForAnimationToEnd:\n   "
  },
  {
    "path": "apps/mobile/.maestro/playback_flow.yaml",
    "chars": 591,
    "preview": "appId: com.roitium.bbplayer\nenv:\n  COOKIE: ${COOKIE}\n---\n# Pre-requisite: Play something. We reuse search_flow for this."
  },
  {
    "path": "apps/mobile/.maestro/playlist_flow.yaml",
    "chars": 584,
    "preview": "appId: com.roitium.bbplayer\nenv:\n  COOKIE: ${COOKIE}\n---\n- runFlow: common/setup.yaml\n\n# 6. Local Playlist Management\n- "
  },
  {
    "path": "apps/mobile/.maestro/search_flow.yaml",
    "chars": 696,
    "preview": "appId: com.roitium.bbplayer\nenv:\n  COOKIE: ${COOKIE}\n---\n- runFlow: common/setup.yaml\n\n# 4. Search & Play\n- tapOn:\n    i"
  },
  {
    "path": "apps/mobile/.maestro/sync_flow.yaml",
    "chars": 896,
    "preview": "appId: com.roitium.bbplayer\nenv:\n  COOKIE: ${COOKIE}\n---\n- runFlow: common/setup.yaml\n\n# 3. Library & Favorites Sync\n- t"
  },
  {
    "path": "apps/mobile/AGENTS.md",
    "chars": 7858,
    "preview": "# BBPlayer Mobile App\n\n**Location:** `apps/mobile/`\n**Type:** React Native (Expo) Application\n\n---\n\n## OVERVIEW\n\nMain BB"
  },
  {
    "path": "apps/mobile/CHANGELOG.md",
    "chars": 12285,
    "preview": "# Changelog\n\n项目的所有显著更改都将记录在这个文件中。\n\n项目的 CHANGELOG 格式符合 [Keep a Changelog],\n且版本号遵循 [Semantic Versioning]。 ~~(然而,事实上遵循的是 [P"
  },
  {
    "path": "apps/mobile/README.md",
    "chars": 226,
    "preview": "# @bbplayer/mobile\n\nBBPlayer 移动端应用的主程序。\n\n## 简介\n\n此目录包含 BBPlayer 移动端应用的核心源代码。基于 React Native 和 Expo 构建,旨在提供从 Bilibili 同步音频"
  },
  {
    "path": "apps/mobile/app.config.ts",
    "chars": 7541,
    "preview": "import { execSync } from 'child_process'\nimport fs from 'fs'\nimport path from 'path'\n\nimport type { ConfigContext, ExpoC"
  },
  {
    "path": "apps/mobile/assets/config/google-services/GoogleService-Info.plist",
    "chars": 869,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "apps/mobile/assets/config/google-services/google-services.json",
    "chars": 1402,
    "preview": "{\n\t\"project_info\": {\n\t\t\"project_number\": \"123456789012\",\n\t\t\"project_id\": \"bbplayer-mock\",\n\t\t\"storage_bucket\": \"bbplayer-"
  },
  {
    "path": "apps/mobile/babel.config.js",
    "chars": 908,
    "preview": "export default (api) => {\n\tapi.cache(true)\n\treturn {\n\t\tpresets: [['babel-preset-expo']],\n\t\tenv: {\n\t\t\tproduction: {\n\t\t\t\tp"
  },
  {
    "path": "apps/mobile/docs/ARCHITECTURE.md",
    "chars": 2869,
    "preview": "# 项目架构指南\n\nBBPlayer 是一个基于 **React Native (Expo)** 的现代化移动端应用。本文档详细介绍了项目的整体架构、目录结构以及核心开发模式。\n\n## 🛠 技术栈概览\n\n- **核心框架**: [Expo]"
  },
  {
    "path": "apps/mobile/docs/BEST_PRACTICES.md",
    "chars": 669,
    "preview": "# 开发规范与最佳实践\n\n## 🎨 UI 开发规范\n\n### FlashList 性能优化\n\n项目中大量使用了 `FlashList` 进行列表渲染。为了保证滚动性能,请严格遵守以下规范:\n\n1.  **renderItem 定义**: `"
  },
  {
    "path": "apps/mobile/docs/CONTRIBUTING.md",
    "chars": 2526,
    "preview": "# 贡献指南 (Contributing Guide)\n\n欢迎来到 BBPlayer 项目!我们非常感谢你对开源社区的贡献。在开始之前,请花一点时间阅读以下指南,这将帮助你更高效地参与开发。\n\n## 🚀 快速开始 (Getting Star"
  },
  {
    "path": "apps/mobile/docs/Home.md",
    "chars": 573,
    "preview": "# BBPlayer 开发文档\n\n> [!NOTE]\n> 如果您是最终用户,请访问 [BBPlayer 官网](https://bbplayer.roitium.com) 获取使用说明。\n\n---\n\n欢迎查阅 BBPlayer 开发文档。这"
  },
  {
    "path": "apps/mobile/docs/RELEASE.md",
    "chars": 874,
    "preview": "# 发版流程 (Release Process)\n\n本文档描述了 BBPlayer 的版本发布流程。\n\n## 1. 准备版本\n\n- **更新 `package.json`**:同步修改 `version` 字段与 `android.vers"
  },
  {
    "path": "apps/mobile/docs/TECHNICAL_DEBT.md",
    "chars": 812,
    "preview": "# 技术债与路线图 (Technical Debt & Roadmap)\n\n本文档记录了项目当前已知的设计缺陷、待改进项以及长期的技术规划。\n\n## ⚠️ 错误处理与日志 (Error Handling & Logging)\n\n### 现状"
  },
  {
    "path": "apps/mobile/drizzle/0000_productive_joystick.sql",
    "chars": 1726,
    "preview": "CREATE TABLE `artists` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`name` text NOT NULL,\n\t`avatar_url` text,\n\t`signature` tex"
  },
  {
    "path": "apps/mobile/drizzle/0001_fast_trauma.sql",
    "chars": 39,
    "preview": "ALTER TABLE `tracks` ADD `source` text;"
  },
  {
    "path": "apps/mobile/drizzle/0002_groovy_maximus.sql",
    "chars": 2506,
    "preview": "CREATE TABLE `bilibili_metadata` (\n\t`track_id` integer PRIMARY KEY NOT NULL,\n\t`bvid` text NOT NULL,\n\t`cid` integer,\n\t`is"
  },
  {
    "path": "apps/mobile/drizzle/0003_glamorous_psylocke.sql",
    "chars": 56,
    "preview": "ALTER TABLE `bilibili_metadata` DROP COLUMN `create_at`;"
  },
  {
    "path": "apps/mobile/drizzle/0004_smiling_beast.sql",
    "chars": 84,
    "preview": "CREATE INDEX `bilibili_metadata_bvid_cid_idx` ON `bilibili_metadata` (`bvid`,`cid`);"
  },
  {
    "path": "apps/mobile/drizzle/0005_spotty_exiles.sql",
    "chars": 2597,
    "preview": "PRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_playlist_tracks` (\n\t`playlist_id` integer NOT NULL,"
  },
  {
    "path": "apps/mobile/drizzle/0006_breezy_jigsaw.sql",
    "chars": 81,
    "preview": "ALTER TABLE `bilibili_metadata` RENAME COLUMN \"is_multi_part\" TO \"is_multi_page\";"
  },
  {
    "path": "apps/mobile/drizzle/0007_legal_thor.sql",
    "chars": 138,
    "preview": "ALTER TABLE `tracks` ADD `play_history` text DEFAULT '[]';--> statement-breakpoint\nALTER TABLE `tracks` DROP COLUMN `pla"
  },
  {
    "path": "apps/mobile/drizzle/0008_overrated_jimmy_woo.sql",
    "chars": 83,
    "preview": "ALTER TABLE `bilibili_metadata` ADD `video_is_valid` integer DEFAULT true NOT NULL;"
  },
  {
    "path": "apps/mobile/drizzle/0009_lethal_marten_broadcloak.sql",
    "chars": 1491,
    "preview": "PRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_artists` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT "
  },
  {
    "path": "apps/mobile/drizzle/0010_brainy_anita_blake.sql",
    "chars": 60,
    "preview": "ALTER TABLE `bilibili_metadata` ADD `main_track_title` text;"
  },
  {
    "path": "apps/mobile/drizzle/0011_grey_echo.sql",
    "chars": 378,
    "preview": "CREATE TABLE `track_downloads` (\n\t`track_id` integer PRIMARY KEY NOT NULL,\n\t`downloadedAt` integer DEFAULT (unixepoch() "
  },
  {
    "path": "apps/mobile/drizzle/0012_blushing_human_fly.sql",
    "chars": 29,
    "preview": "DROP TABLE `track_downloads`;"
  },
  {
    "path": "apps/mobile/drizzle/0013_jittery_randall.sql",
    "chars": 187,
    "preview": "ALTER TABLE `playlist_tracks` ADD `sort_key` text NOT NULL DEFAULT '';--> statement-breakpoint\nCREATE INDEX `playlist_tr"
  },
  {
    "path": "apps/mobile/drizzle/0014_flippant_sebastian_shaw.sql",
    "chars": 42,
    "preview": "DROP INDEX `playlist_tracks_playlist_idx`;"
  },
  {
    "path": "apps/mobile/drizzle/0015_flippant_skaar.sql",
    "chars": 754,
    "preview": "CREATE TABLE `playlist_sync_queue` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,\n\t`playlist_id` integer NOT NULL,\n"
  },
  {
    "path": "apps/mobile/drizzle/0016_cheerful_stark_industries.sql",
    "chars": 234,
    "preview": "ALTER TABLE `playlist_sync_queue` DROP COLUMN `attempts`;--> statement-breakpoint\nALTER TABLE `playlist_sync_queue` DROP"
  },
  {
    "path": "apps/mobile/drizzle/0017_rare_lifeguard.sql",
    "chars": 290,
    "preview": "CREATE INDEX `playlist_sync_queue_status_idx` ON `playlist_sync_queue` (`status`);--> statement-breakpoint\nCREATE INDEX "
  },
  {
    "path": "apps/mobile/drizzle/0018_green_dracula.sql",
    "chars": 560,
    "preview": "CREATE TABLE `play_history` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,\n\t`track_id` integer NOT NULL,\n\t`start_ti"
  },
  {
    "path": "apps/mobile/drizzle/0019_icy_mandarin.sql",
    "chars": 713,
    "preview": "CREATE TABLE `dynamic_playlist_sources` (\n\t`playlist_id` integer NOT NULL,\n\t`source_playlist_id` integer NOT NULL,\n\t`pos"
  },
  {
    "path": "apps/mobile/drizzle/0020_ambitious_sheva_callister.sql",
    "chars": 120,
    "preview": "CREATE INDEX `dynamic_playlist_sources_playlist_position_idx` ON `dynamic_playlist_sources` (`playlist_id`,`position`);\n"
  },
  {
    "path": "apps/mobile/drizzle/meta/0000_snapshot.json",
    "chars": 7299,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"81739eb6-6932-4793-94af-5772e1e0f6cd\",\n\t\"prevId\": \"00000000-0000-0000-0"
  },
  {
    "path": "apps/mobile/drizzle/meta/0001_snapshot.json",
    "chars": 7443,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"75da2b11-e2bf-414d-b2f1-494b79899231\",\n\t\"prevId\": \"81739eb6-6932-4793-9"
  },
  {
    "path": "apps/mobile/drizzle/meta/0002_snapshot.json",
    "chars": 8858,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"ab9b4fb1-8f85-4c45-a234-a3c016493c63\",\n\t\"prevId\": \"75da2b11-e2bf-414d-b"
  },
  {
    "path": "apps/mobile/drizzle/meta/0003_snapshot.json",
    "chars": 8706,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"825a53df-1145-43a3-ba0f-968f42511a8c\",\n\t\"prevId\": \"ab9b4fb1-8f85-4c45-a"
  },
  {
    "path": "apps/mobile/drizzle/meta/0004_snapshot.json",
    "chars": 8859,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"81f73733-963f-402e-9201-378310e220a6\",\n\t\"prevId\": \"825a53df-1145-43a3-b"
  },
  {
    "path": "apps/mobile/drizzle/meta/0005_snapshot.json",
    "chars": 10494,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"2573ead5-558c-479c-920b-1a065ade1c0e\",\n\t\"prevId\": \"81f73733-963f-402e-9"
  },
  {
    "path": "apps/mobile/drizzle/meta/0006_snapshot.json",
    "chars": 10585,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"a45b56db-b386-49b7-83e3-e863d9199e82\",\n\t\"prevId\": \"2573ead5-558c-479c-9"
  },
  {
    "path": "apps/mobile/drizzle/meta/0007_snapshot.json",
    "chars": 10480,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"ec2725a8-34d0-4437-93e4-78d4a033b08a\",\n\t\"prevId\": \"a45b56db-b386-49b7-8"
  },
  {
    "path": "apps/mobile/drizzle/meta/0008_snapshot.json",
    "chars": 10664,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"7e7942c7-ccfa-4b6c-a846-4ea356ae7dee\",\n\t\"prevId\": \"ec2725a8-34d0-4437-9"
  },
  {
    "path": "apps/mobile/drizzle/meta/0009_snapshot.json",
    "chars": 11654,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"3da47614-a62f-4173-961a-e8b238af0dae\",\n\t\"prevId\": \"7e7942c7-ccfa-4b6c-a"
  },
  {
    "path": "apps/mobile/drizzle/meta/0010_snapshot.json",
    "chars": 11818,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"6c17f4d1-bd70-4052-9a24-250d42de868d\",\n\t\"prevId\": \"3da47614-a62f-4173-9"
  },
  {
    "path": "apps/mobile/drizzle/meta/0011_snapshot.json",
    "chars": 13096,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"f20429b4-0237-45df-8274-6d3a282d16a8\",\n\t\"prevId\": \"6c17f4d1-bd70-4052-9"
  },
  {
    "path": "apps/mobile/drizzle/meta/0012_snapshot.json",
    "chars": 11818,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"1c3c1015-206b-4997-90f4-04dfe6f6f2a8\",\n\t\"prevId\": \"f20429b4-0237-45df-8"
  },
  {
    "path": "apps/mobile/drizzle/meta/0013_snapshot.json",
    "chars": 11979,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"89873c64-ec04-4f62-a78e-38796ad6b8be\",\n\t\"prevId\": \"1c3c1015-206b-4997-9"
  },
  {
    "path": "apps/mobile/drizzle/meta/0014_snapshot.json",
    "chars": 11833,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"a3add204-1a72-44d6-aef0-9168b5e7d4cf\",\n\t\"prevId\": \"89873c64-ec04-4f62-a"
  },
  {
    "path": "apps/mobile/drizzle/meta/0015_snapshot.json",
    "chars": 14481,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"a639464c-4977-4ba6-a9d7-7f60542a2f8e\",\n\t\"prevId\": \"a3add204-1a72-44d6-a"
  },
  {
    "path": "apps/mobile/drizzle/meta/0016_snapshot.json",
    "chars": 13987,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"2c3abf30-f1cc-49ea-96f1-3b725e5398dd\",\n\t\"prevId\": \"a639464c-4977-4ba6-a"
  },
  {
    "path": "apps/mobile/drizzle/meta/0017_snapshot.json",
    "chars": 14426,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"31911814-f083-419a-b906-aa508f89b7b5\",\n\t\"prevId\": \"2c3abf30-f1cc-49ea-9"
  },
  {
    "path": "apps/mobile/drizzle/meta/0018_snapshot.json",
    "chars": 15953,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"d574920c-fa44-4436-8d85-72401b2c03b9\",\n\t\"prevId\": \"31911814-f083-419a-b"
  },
  {
    "path": "apps/mobile/drizzle/meta/0019_snapshot.json",
    "chars": 18076,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"b7a69c01-9852-4c0d-bcbd-9b6735aebe3f\",\n\t\"prevId\": \"d574920c-fa44-4436-8"
  },
  {
    "path": "apps/mobile/drizzle/meta/0020_snapshot.json",
    "chars": 18270,
    "preview": "{\n\t\"version\": \"6\",\n\t\"dialect\": \"sqlite\",\n\t\"id\": \"fb480ae8-7103-416c-8090-0c473fb06f10\",\n\t\"prevId\": \"b7a69c01-9852-4c0d-b"
  },
  {
    "path": "apps/mobile/drizzle/meta/_journal.json",
    "chars": 2699,
    "preview": "{\n\t\"version\": \"7\",\n\t\"dialect\": \"sqlite\",\n\t\"entries\": [\n\t\t{\n\t\t\t\"idx\": 0,\n\t\t\t\"version\": \"6\",\n\t\t\t\"when\": 1752671021496,\n\t\t\t"
  },
  {
    "path": "apps/mobile/drizzle/migrations.js",
    "chars": 1404,
    "preview": "// This file is required for Expo/React Native SQLite migrations - https://orm.drizzle.team/quick-sqlite/expo\n\nimport m0"
  },
  {
    "path": "apps/mobile/drizzle.config.ts",
    "chars": 169,
    "preview": "import type { Config } from 'drizzle-kit'\nexport default {\n\tschema: './src/lib/db/schema.ts',\n\tout: './drizzle',\n\tdialec"
  },
  {
    "path": "apps/mobile/eas.json",
    "chars": 1348,
    "preview": "{\n\t\"cli\": {\n\t\t\"version\": \">= 13.4.2\",\n\t\t\"appVersionSource\": \"local\"\n\t},\n\t\"build\": {\n\t\t\"dev\": {\n\t\t\t\"developmentClient\": t"
  },
  {
    "path": "apps/mobile/expo-plugins/withAbiFilters.js",
    "chars": 1223,
    "preview": "const {\n\twithGradleProperties,\n\twithAppBuildGradle,\n} = require('expo/config-plugins')\n\nconst withAbiFilters = (config, "
  },
  {
    "path": "apps/mobile/expo-plugins/withAndroidGradleProperties.js",
    "chars": 978,
    "preview": "const { withGradleProperties } = require('expo/config-plugins')\n\nconst GRADLE_XMX = process.env.GRADLE_XMX || '4g'\nconst"
  },
  {
    "path": "apps/mobile/expo-plugins/withAndroidPlugin.js",
    "chars": 915,
    "preview": "const configPlugins = require('expo/config-plugins')\n\nconst { withAndroidManifest, withStringsXml } = configPlugins\n\ncon"
  },
  {
    "path": "apps/mobile/expo-plugins/withKotlinSerialization.js",
    "chars": 658,
    "preview": "const { withProjectBuildGradle } = require('expo/config-plugins')\n\nconst withKotlinSerialization = (config) => {\n\treturn"
  },
  {
    "path": "apps/mobile/index.js",
    "chars": 131,
    "preview": "import { playerSideEffects } from './src/lib/player/PlayerSideEffects'\n\nplayerSideEffects.initialize()\n\nimport 'expo-rou"
  },
  {
    "path": "apps/mobile/metro.config.js",
    "chars": 956,
    "preview": "/* oxlint-disable @typescript-eslint/no-require-imports */\nconst path = require('path')\nconst { withRozenite } = require"
  },
  {
    "path": "apps/mobile/mise.toml",
    "chars": 970,
    "preview": "[tasks.buildprod]\ndescription = \"Build BBPlayer production release\"\nenv = { NODE_ENV = \"production\", BUILD_MODE = \"v8a\","
  },
  {
    "path": "apps/mobile/package.json",
    "chars": 5896,
    "preview": "{\n\t\"name\": \"@bbplayer/mobile\",\n\t\"version\": \"2.4.6\",\n\t\"private\": true,\n\t\"main\": \"index.js\",\n\t\"scripts\": {\n\t\t\"android\": \"W"
  },
  {
    "path": "apps/mobile/src/app/(tabs)/_layout.tsx",
    "chars": 1800,
    "preview": "import type {\n\tNativeBottomTabNavigationEventMap,\n\tNativeBottomTabNavigationOptions,\n} from '@bottom-tabs/react-navigati"
  },
  {
    "path": "apps/mobile/src/app/(tabs)/index.tsx",
    "chars": 16868,
    "preview": "import { WeeklyHeatMap } from '@bbplayer/heatmap'\nimport type { TrueSheet } from '@lodev09/react-native-true-sheet'\nimpo"
  },
  {
    "path": "apps/mobile/src/app/(tabs)/library/[tab].tsx",
    "chars": 4818,
    "preview": "import Icon from '@react-native-vector-icons/material-design-icons'\nimport { useFocusEffect, useLocalSearchParams, useRo"
  },
  {
    "path": "apps/mobile/src/app/(tabs)/settings/index.tsx",
    "chars": 5960,
    "preview": "import * as Application from 'expo-application'\nimport * as Clipboard from 'expo-clipboard'\nimport { useRouter } from 'e"
  },
  {
    "path": "apps/mobile/src/app/+native-intent.ts",
    "chars": 922,
    "preview": "import log from '@/utils/log'\n\nexport function redirectSystemPath({\n\tpath,\n\tinitial,\n}: {\n\tpath: string\n\tinitial: boolea"
  },
  {
    "path": "apps/mobile/src/app/+not-found.tsx",
    "chars": 911,
    "preview": "import { useRouter } from 'expo-router'\nimport { Button, StyleSheet, Text, View } from 'react-native'\n\nconst NotFoundScr"
  },
  {
    "path": "apps/mobile/src/app/_layout.tsx",
    "chars": 9017,
    "preview": "import { Orpheus } from '@bbplayer/orpheus'\nimport {\n\tfetch as fetchNetInfo,\n\taddEventListener as addNetInfoEventListene"
  },
  {
    "path": "apps/mobile/src/app/comments/[bvid].tsx",
    "chars": 3035,
    "preview": "import { FlashList } from '@shopify/flash-list'\nimport { useLocalSearchParams, useRouter } from 'expo-router'\nimport { u"
  },
  {
    "path": "apps/mobile/src/app/comments/reply.tsx",
    "chars": 3225,
    "preview": "import { FlashList } from '@shopify/flash-list'\nimport { useLocalSearchParams, useRouter } from 'expo-router'\nimport { u"
  },
  {
    "path": "apps/mobile/src/app/download.tsx",
    "chars": 3393,
    "preview": "import { DownloadState, Orpheus, type DownloadTask } from '@bbplayer/orpheus'\nimport { FlashList } from '@shopify/flash-"
  },
  {
    "path": "apps/mobile/src/app/downloaded.tsx",
    "chars": 15791,
    "preview": "import { DownloadState, Orpheus, type DownloadTask } from '@bbplayer/orpheus'\nimport type { TrueSheet as TrueSheetType }"
  },
  {
    "path": "apps/mobile/src/app/history/[date].tsx",
    "chars": 4732,
    "preview": "import { FlashList } from '@shopify/flash-list'\nimport dayjs from 'dayjs'\nimport { useLocalSearchParams, useRouter } fro"
  },
  {
    "path": "apps/mobile/src/app/history/overall.tsx",
    "chars": 5087,
    "preview": "import { FlashList } from '@shopify/flash-list'\nimport { useRouter } from 'expo-router'\nimport { useCallback, useMemo } "
  },
  {
    "path": "apps/mobile/src/app/modal.tsx",
    "chars": 2227,
    "preview": "import { useRouter } from 'expo-router'\nimport { Suspense, useEffect, useState } from 'react'\nimport { ActivityIndicator"
  },
  {
    "path": "apps/mobile/src/app/player.tsx",
    "chars": 8965,
    "preview": "import ImageThemeColors from '@bbplayer/image-theme-colors'\nimport type { TrueSheet } from '@lodev09/react-native-true-s"
  },
  {
    "path": "apps/mobile/src/app/playlist/external-sync.tsx",
    "chars": 15023,
    "preview": "import { FlashList } from '@shopify/flash-list'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { useLocal"
  },
  {
    "path": "apps/mobile/src/app/playlist/local/[id].tsx",
    "chars": 25679,
    "preview": "import { DownloadState, Orpheus } from '@bbplayer/orpheus'\nimport type { TrueSheet } from '@lodev09/react-native-true-sh"
  },
  {
    "path": "apps/mobile/src/app/playlist/recently/index.tsx",
    "chars": 4086,
    "preview": "import { useRouter } from 'expo-router'\nimport { useCallback, useMemo } from 'react'\nimport { StyleSheet, View } from 'r"
  },
  {
    "path": "apps/mobile/src/app/playlist/remote/collection/[id].tsx",
    "chars": 6941,
    "preview": "import { useImage } from 'expo-image'\nimport { useLocalSearchParams, useRouter } from 'expo-router'\nimport { useCallback"
  },
  {
    "path": "apps/mobile/src/app/playlist/remote/favorite/[id].tsx",
    "chars": 7113,
    "preview": "import { useImage } from 'expo-image'\nimport { useLocalSearchParams, useRouter } from 'expo-router'\nimport { useCallback"
  },
  {
    "path": "apps/mobile/src/app/playlist/remote/multipage/[bvid].tsx",
    "chars": 9387,
    "preview": "import type { FlashListRef } from '@shopify/flash-list'\nimport { useImage } from 'expo-image'\nimport { useLocalSearchPar"
  },
  {
    "path": "apps/mobile/src/app/playlist/remote/search-result/fav/[query].tsx",
    "chars": 6182,
    "preview": "import { useLocalSearchParams, useRouter } from 'expo-router'\nimport { useMemo, useState } from 'react'\nimport { Refresh"
  },
  {
    "path": "apps/mobile/src/app/playlist/remote/search-result/global/[query].tsx",
    "chars": 5968,
    "preview": "import { useLocalSearchParams, useRouter } from 'expo-router'\nimport { decode } from 'he'\nimport { useMemo, useEffect, u"
  },
  {
    "path": "apps/mobile/src/app/playlist/remote/toview.tsx",
    "chars": 8872,
    "preview": "import { useImage } from 'expo-image'\nimport { useRouter } from 'expo-router'\nimport { useCallback, useMemo, useState } "
  },
  {
    "path": "apps/mobile/src/app/playlist/remote/uploader/[mid].tsx",
    "chars": 8897,
    "preview": "import { useImage } from 'expo-image'\nimport { useLocalSearchParams, useRouter } from 'expo-router'\nimport { useEffect, "
  },
  {
    "path": "apps/mobile/src/app/settings/appearance.tsx",
    "chars": 5581,
    "preview": "import { useRouter } from 'expo-router'\nimport { useState } from 'react'\nimport {\n\tPermissionsAndroid,\n\tPlatform,\n\tScrol"
  },
  {
    "path": "apps/mobile/src/app/settings/donate.tsx",
    "chars": 2457,
    "preview": "import { useRouter } from 'expo-router'\nimport { ScrollView, StyleSheet, View } from 'react-native'\nimport { Appbar, Lis"
  },
  {
    "path": "apps/mobile/src/app/settings/download.tsx",
    "chars": 3148,
    "preview": "import { useRouter } from 'expo-router'\nimport { useState } from 'react'\nimport { ScrollView, StyleSheet, View } from 'r"
  },
  {
    "path": "apps/mobile/src/app/settings/general.tsx",
    "chars": 6444,
    "preview": "import * as FileSystem from 'expo-file-system'\nimport { Image } from 'expo-image'\nimport { useRouter } from 'expo-router"
  },
  {
    "path": "apps/mobile/src/app/settings/lyrics.tsx",
    "chars": 12030,
    "preview": "import { Orpheus } from '@bbplayer/orpheus'\nimport { useFocusEffect, useRouter } from 'expo-router'\nimport * as WebBrows"
  },
  {
    "path": "apps/mobile/src/app/settings/playback.tsx",
    "chars": 3565,
    "preview": "import { Orpheus } from '@bbplayer/orpheus'\nimport { useRouter } from 'expo-router'\nimport { useState } from 'react'\nimp"
  },
  {
    "path": "apps/mobile/src/app/share/playlist.tsx",
    "chars": 10924,
    "preview": "import * as Clipboard from 'expo-clipboard'\nimport { useImage } from 'expo-image'\nimport { useLocalSearchParams, useRout"
  },
  {
    "path": "apps/mobile/src/app/test.tsx",
    "chars": 12610,
    "preview": "import { Orpheus } from '@bbplayer/orpheus'\nimport { TrueSheet } from '@lodev09/react-native-true-sheet'\nimport dayjs fr"
  }
]

// ... and 476 more files (download for full content)

About this extraction

This page contains the full source code of the bbplayer-app/BBPlayer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 676 files (4.5 MB), approximately 1.2M tokens, and a symbol index with 1914 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!