├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── music-player-master.iml └── vcs.xml ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── 1024.png ├── LICENSE ├── README.md ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── debug.keystore │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── music │ │ │ └── player │ │ │ └── gyc │ │ │ ├── MainActivity.kt │ │ │ └── MainApplication.kt │ │ └── res │ │ ├── drawable-hdpi │ │ └── splashscreen_image.png │ │ ├── drawable-mdpi │ │ └── splashscreen_image.png │ │ ├── drawable-xhdpi │ │ └── splashscreen_image.png │ │ ├── drawable-xxhdpi │ │ └── splashscreen_image.png │ │ ├── drawable-xxxhdpi │ │ └── splashscreen_image.png │ │ ├── drawable │ │ ├── rn_edit_text_material.xml │ │ └── splashscreen.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── colors.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── app.json ├── assets ├── .fake-audio.mp3.md5 ├── 1024.png ├── 144.png ├── adaptive-icon.png ├── data │ └── library.json ├── fake-audio.mp3 ├── fake-audio.mp3.md5 ├── favicon.png ├── icon.png ├── local.png ├── local1.png ├── splash.png ├── unknown_artist.png └── unknown_track.png ├── babel.config.js ├── ios ├── .gitignore ├── .xcode.env ├── CyMusic.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── CyMusic.xcscheme ├── CyMusic.xcworkspace │ └── contents.xcworkspacedata ├── CyMusic │ ├── AppDelegate.h │ ├── AppDelegate.mm │ ├── CyMusic-Bridging-Header.h │ ├── CyMusic.entitlements │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── App-Icon-1024x1024@1x.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── SplashScreen.imageset │ │ │ ├── Contents.json │ │ │ └── image.png │ │ └── SplashScreenBackground.imageset │ │ │ ├── Contents.json │ │ │ └── image.png │ ├── Info.plist │ ├── SplashScreen.storyboard │ ├── Supporting │ │ └── Expo.plist │ ├── main.m │ └── noop-file.swift ├── Podfile ├── Podfile.lock ├── Podfile.properties.json └── ShareExtension │ ├── MainInterface.storyboard │ ├── PrivacyInfo.xcprivacy │ ├── ShareExtension-Info.plist │ ├── ShareExtension.entitlements │ └── ShareViewController.swift ├── package-lock.json ├── package.json ├── patches └── xcode+3.0.1.patch ├── readme.jpg ├── src.zip ├── src ├── app │ ├── (modals) │ │ ├── [name].tsx │ │ ├── addToPlaylist.tsx │ │ ├── importPlayList.tsx │ │ ├── logScreen.tsx │ │ ├── playList.tsx │ │ ├── settingModal.tsx │ │ └── shareintent.tsx │ ├── (tabs) │ │ ├── (songs) │ │ │ ├── _layout.tsx │ │ │ └── index.tsx │ │ ├── _layout.tsx │ │ ├── favorites │ │ │ ├── [name].tsx │ │ │ ├── _layout.tsx │ │ │ ├── favoriteMusic.tsx │ │ │ ├── index.tsx │ │ │ └── localMusic.tsx │ │ ├── radio │ │ │ ├── [name].tsx │ │ │ ├── _layout.tsx │ │ │ └── index.tsx │ │ └── search │ │ │ ├── [name].tsx │ │ │ ├── _layout.tsx │ │ │ └── index.tsx │ ├── [...unmatched].tsx │ ├── _layout.tsx │ └── player.tsx ├── components │ ├── AddPlayListButton.tsx │ ├── ArtistTracksList.tsx │ ├── FloatingPlayer.tsx │ ├── GlobalButton.tsx │ ├── LogScreen.tsx │ ├── MovingText.tsx │ ├── NowPlayList.tsx │ ├── PlayerControls.tsx │ ├── PlayerProgressbar.tsx │ ├── PlayerRepeatToggle.tsx │ ├── PlayerVolumeBar.tsx │ ├── PlaylistListItem.tsx │ ├── PlaylistTracksList.tsx │ ├── PlaylistsList.tsx │ ├── PlaylistsListModal.tsx │ ├── QueueControls.tsx │ ├── RadioList.tsx │ ├── RadioListItem.tsx │ ├── SearchList.tsx │ ├── SearchListItem.tsx │ ├── ShowPlayerListToggle.tsx │ ├── SingerTracksList.tsx │ ├── TrackShortcutsMenu.tsx │ ├── TracksList.tsx │ ├── TracksListItem.tsx │ ├── lyric │ │ ├── index.tsx │ │ └── lyricItem.tsx │ └── utils │ │ ├── StopPropagation.tsx │ │ ├── common.ts │ │ ├── index.ts │ │ ├── message.ts │ │ ├── musicSdk │ │ ├── api-source-info.ts │ │ ├── api-source.js │ │ ├── index.js │ │ ├── options.js │ │ ├── tx │ │ │ ├── api-ikun.js │ │ │ ├── comment.js │ │ │ ├── hotSearch.js │ │ │ ├── index.js │ │ │ ├── leaderboard.js │ │ │ ├── lyric.js │ │ │ ├── musicInfo.js │ │ │ ├── musicSearch.js │ │ │ ├── songList.js │ │ │ └── tipSearch.js │ │ ├── utils.js │ │ └── xm.js │ │ ├── nativeModules │ │ ├── cache.ts │ │ ├── crypto.ts │ │ ├── userApi.ts │ │ └── utils.ts │ │ └── request.js ├── constants │ ├── commonConst.ts │ ├── constant.ts │ ├── images.ts │ ├── layout.ts │ ├── playbackService.ts │ ├── tokens.ts │ └── trackPlayerEventListener.tsx ├── helpers │ ├── errors │ │ └── MusicError.ts │ ├── filter.ts │ ├── logger.ts │ ├── lyricManager.ts │ ├── miscellaneous.ts │ ├── searchAll.ts │ ├── storage.ts │ ├── trackPlayerIndex.ts │ ├── types.ts │ ├── userApi │ │ ├── getMusicSource.ts │ │ ├── ikun-music-source.js │ │ ├── qq-music-api.js │ │ ├── request.js │ │ ├── user-api-preload.js │ │ └── xiaoqiu.js │ └── userApiHelper.ts ├── hooks │ ├── useDelayFalsy.ts │ ├── useLastActiveTrack.tsx │ ├── useLogTrackPlayerState.tsx │ ├── useNavigationSearch.tsx │ ├── usePlayerBackground.tsx │ ├── useSetupTrackPlayer.tsx │ ├── useTrackPlayerFavorite.tsx │ ├── useTrackPlayerRepeatMode.tsx │ └── useTrackPlayerVolume.tsx ├── locales │ ├── en.json │ └── zh.json ├── store │ ├── PersistStatus.ts │ ├── config.ts │ ├── getOrCreateMMKV.ts │ ├── library.tsx │ ├── mediaExtra.ts │ ├── pathConst.ts │ ├── playList.ts │ ├── queue.tsx │ ├── trackViewList.ts │ └── usePlayerStore.ts ├── styles │ └── index.ts ├── types │ ├── album.d.ts │ ├── app.d.ts │ ├── app_setting.d.ts │ ├── artist.d.ts │ ├── common.d.ts │ ├── config_files.d.ts │ ├── declarations.d.ts │ ├── dislike_list.d.ts │ ├── dislike_list_sync.d.ts │ ├── download_list.d.ts │ ├── index.d.ts │ ├── list.d.ts │ ├── list_sync.d.ts │ ├── lyric.d.ts │ ├── music.d.ts │ ├── musicSheet.d.ts │ ├── musicSheetGroup.d.ts │ ├── player.d.ts │ ├── plugin.d.ts │ ├── shims.d.ts │ ├── sync.d.ts │ ├── sync_common.d.ts │ ├── theme.d.ts │ ├── user_api.d.ts │ └── utils.d.ts └── utils │ ├── delay.ts │ ├── i18n.ts │ ├── lrcParser.ts │ ├── mediaIndexMap.ts │ ├── mediaItem.ts │ ├── musicInfo │ ├── Buffer.js │ ├── MusicInfo.d.ts │ ├── MusicInfo.js │ ├── MusicInfoResponse.js │ └── index.js │ ├── rpx.ts │ ├── safeParse.ts │ ├── stateMapper.ts │ ├── timeformat.ts │ ├── timingClose.ts │ ├── trackUtils.ts │ └── utils.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore all node_modules 2 | 3 | **/node_modules/** 4 | 5 | # Autogenerated files 6 | 7 | **/.expo/** 8 | **/.next/** 9 | **/__generated__/** 10 | **/build/** 11 | 12 | # Git submodules 13 | 14 | /react-native-lab/react-native/** 15 | /docs/react-native-website/** 16 | 17 | # Other 18 | 19 | **/android/** 20 | **/assets/** 21 | **/bin/** 22 | **/fastlane/** 23 | **/ios/** 24 | **/kotlin/providers/** 25 | **/vendored/** 26 | /docs/public/static/** 27 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:react/recommended", 11 | "plugin:react-hooks/recommended" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "ecmaVersion": 12, 19 | "sourceType": "module" 20 | }, 21 | "plugins": ["@typescript-eslint", "react", "react-hooks"], 22 | "rules": { 23 | "import/order": "off", 24 | "@typescript-eslint/no-explicit-any": "off", 25 | "react/react-in-jsx-scope": "off", 26 | "react/display-name": "off", 27 | "@typescript-eslint/no-unused-vars": "warn" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 38 | # The following patterns were generated by expo-cli 39 | 40 | expo-env.d.ts 41 | third_party_url.ts 42 | # @end expo-cli 43 | # SpecStory explanation file 44 | .specstory/ 45 | # SpecStory explanation file 46 | .specstory/.what-is-this.md 47 | .cursorindexingignore 48 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/music-player-master.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | ios/ 3 | android/ 4 | assets/ 5 | .expo/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "bracketSpacing": true, 5 | "trailingComma": "all", 6 | "printWidth": 100, 7 | "tabWidth": 2, 8 | "useTabs": true, 9 | "endOfLine": "lf" 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": "always", 6 | "source.organizeImports": "always" 7 | }, 8 | "editor.wordWrap": "on", 9 | "svg.preview.background": "custom", 10 | "files.associations": { 11 | "__bit_reference": "cpp", 12 | "__hash_table": "cpp", 13 | "__split_buffer": "cpp", 14 | "__tree": "cpp", 15 | "array": "cpp", 16 | "deque": "cpp", 17 | "hash_map": "cpp", 18 | "hash_set": "cpp", 19 | "initializer_list": "cpp", 20 | "ios": "cpp", 21 | "list": "cpp", 22 | "map": "cpp", 23 | "set": "cpp", 24 | "string": "cpp", 25 | "string_view": "cpp", 26 | "unordered_map": "cpp", 27 | "unordered_set": "cpp", 28 | "valarray": "cpp", 29 | "vector": "cpp", 30 | "*.ipp": "cpp" 31 | }, 32 | "cSpell.words": ["abslist", "flac", "hilight", "TARGETID"] 33 | } 34 | -------------------------------------------------------------------------------- /1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/1024.png -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Android/IntelliJ 6 | # 7 | build/ 8 | .idea 9 | .gradle 10 | local.properties 11 | *.iml 12 | *.hprof 13 | 14 | # Bundle artifacts 15 | *.jsbundle 16 | -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/debug.keystore -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # react-native-reanimated 11 | -keep class com.swmansion.reanimated.** { *; } 12 | -keep class com.facebook.react.turbomodule.** { *; } 13 | 14 | # Add any project specific keep options here: 15 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/music/player/gyc/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.music.player.gyc 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | 6 | import com.facebook.react.ReactActivity 7 | import com.facebook.react.ReactActivityDelegate 8 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 9 | import com.facebook.react.defaults.DefaultReactActivityDelegate 10 | 11 | import expo.modules.ReactActivityDelegateWrapper 12 | 13 | class MainActivity : ReactActivity() { 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | // Set the theme to AppTheme BEFORE onCreate to support 16 | // coloring the background, status bar, and navigation bar. 17 | // This is required for expo-splash-screen. 18 | setTheme(R.style.AppTheme); 19 | super.onCreate(null) 20 | } 21 | 22 | /** 23 | * Returns the name of the main component registered from JavaScript. This is used to schedule 24 | * rendering of the component. 25 | */ 26 | override fun getMainComponentName(): String = "main" 27 | 28 | /** 29 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 30 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 31 | */ 32 | override fun createReactActivityDelegate(): ReactActivityDelegate { 33 | return ReactActivityDelegateWrapper( 34 | this, 35 | BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, 36 | object : DefaultReactActivityDelegate( 37 | this, 38 | mainComponentName, 39 | fabricEnabled 40 | ){}) 41 | } 42 | 43 | /** 44 | * Align the back button behavior with Android S 45 | * where moving root activities to background instead of finishing activities. 46 | * @see onBackPressed 47 | */ 48 | override fun invokeDefaultOnBackPressed() { 49 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { 50 | if (!moveTaskToBack(false)) { 51 | // For non-root activities, use the default implementation to finish them. 52 | super.invokeDefaultOnBackPressed() 53 | } 54 | return 55 | } 56 | 57 | // Use the default back button implementation on Android S 58 | // because it's doing more than [Activity.moveTaskToBack] in fact. 59 | super.invokeDefaultOnBackPressed() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/music/player/gyc/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.music.player.gyc 2 | 3 | import android.app.Application 4 | import android.content.res.Configuration 5 | import androidx.annotation.NonNull 6 | 7 | import com.facebook.react.PackageList 8 | import com.facebook.react.ReactApplication 9 | import com.facebook.react.ReactNativeHost 10 | import com.facebook.react.ReactPackage 11 | import com.facebook.react.ReactHost 12 | import com.facebook.react.config.ReactFeatureFlags 13 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 14 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 15 | import com.facebook.react.defaults.DefaultReactNativeHost 16 | import com.facebook.react.flipper.ReactNativeFlipper 17 | import com.facebook.soloader.SoLoader 18 | 19 | import expo.modules.ApplicationLifecycleDispatcher 20 | import expo.modules.ReactNativeHostWrapper 21 | 22 | class MainApplication : Application(), ReactApplication { 23 | 24 | override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper( 25 | this, 26 | object : DefaultReactNativeHost(this) { 27 | override fun getPackages(): List { 28 | // Packages that cannot be autolinked yet can be added manually here, for example: 29 | // packages.add(new MyReactNativePackage()); 30 | return PackageList(this).packages 31 | } 32 | 33 | override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" 34 | 35 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 36 | 37 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 38 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 39 | } 40 | ) 41 | 42 | override val reactHost: ReactHost 43 | get() = getDefaultReactHost(this.applicationContext, reactNativeHost) 44 | 45 | override fun onCreate() { 46 | super.onCreate() 47 | SoLoader.init(this, false) 48 | if (!BuildConfig.REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS) { 49 | ReactFeatureFlags.unstable_useRuntimeSchedulerAlways = false 50 | } 51 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 52 | // If you opted-in for the New Architecture, we load the native entry point for this app. 53 | load() 54 | } 55 | if (BuildConfig.DEBUG) { 56 | ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager) 57 | } 58 | ApplicationLifecycleDispatcher.onApplicationCreate(this) 59 | } 60 | 61 | override fun onConfigurationChanged(newConfig: Configuration) { 62 | super.onConfigurationChanged(newConfig) 63 | ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/drawable-hdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/drawable-mdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splashscreen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #000 3 | #000 4 | #023c69 5 | #000 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CyMusic 3 | cover 4 | false 5 | undefined 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | 17 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0' 6 | minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '23') 7 | compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '34') 8 | targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34') 9 | kotlinVersion = findProperty('android.kotlinVersion') ?: '1.8.10' 10 | 11 | ndkVersion = "25.1.8937393" 12 | } 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | dependencies { 18 | classpath('com.android.tools.build:gradle') 19 | classpath('com.facebook.react:react-native-gradle-plugin') 20 | } 21 | } 22 | 23 | apply plugin: "com.facebook.react.rootproject" 24 | 25 | allprojects { 26 | repositories { 27 | maven { 28 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 29 | url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android')) 30 | } 31 | maven { 32 | // Android JSC is installed from npm 33 | url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist')) 34 | } 35 | 36 | google() 37 | mavenCentral() 38 | maven { url 'https://www.jitpack.io' } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | 25 | # Automatically convert third-party libraries to use AndroidX 26 | android.enableJetifier=true 27 | 28 | # Use this property to specify which architecture you want to build. 29 | # You can also override it from the CLI using 30 | # ./gradlew -PreactNativeArchitectures=x86_64 31 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 32 | 33 | # Use this property to enable support to the new architecture. 34 | # This will allow you to use TurboModules and the Fabric render in 35 | # your application. You should enable this flag either if you want 36 | # to write custom TurboModules/Fabric components OR use libraries that 37 | # are providing them. 38 | newArchEnabled=false 39 | 40 | # Use this property to enable or disable the Hermes JS engine. 41 | # If set to false, you will be using JSC instead. 42 | hermesEnabled=true 43 | 44 | # Enable GIF support in React Native images (~200 B increase) 45 | expo.gif.enabled=true 46 | # Enable webp support in React Native images (~85 KB increase) 47 | expo.webp.enabled=true 48 | # Enable animated webp support (~3.4 MB increase) 49 | # Disabled by default because iOS doesn't support animated webp 50 | expo.webp.animated=false 51 | 52 | # Enable network inspector 53 | EX_DEV_CLIENT_NETWORK_INSPECTOR=true 54 | 55 | # Use legacy packaging to compress native libraries in the resulting APK. 56 | expo.useLegacyPackaging=false 57 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'CyMusic' 2 | 3 | dependencyResolutionManagement { 4 | versionCatalogs { 5 | reactAndroidLibs { 6 | from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../gradle/libs.versions.toml"))) 7 | } 8 | } 9 | } 10 | 11 | apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); 12 | useExpoModules() 13 | 14 | apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); 15 | applyNativeModulesSettingsGradle(settings) 16 | 17 | include ':app' 18 | includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile()) 19 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "CyMusic", 4 | "slug": "CyMusic", 5 | "version": "1.1.7", 6 | "orientation": "portrait", 7 | "icon": "./assets/1024.png", 8 | "userInterfaceStyle": "dark", 9 | "scheme": "cymusic", 10 | "splash": { 11 | "image": "./assets/splash.png", 12 | "resizeMode": "cover", 13 | "backgroundColor": "#000" 14 | }, 15 | "assetBundlePatterns": ["**/*"], 16 | "ios": { 17 | "supportsTablet": true, 18 | "bundleIdentifier": "com.music.player.gyc", 19 | "usesIcloudStorage": false, 20 | "infoPlist": { 21 | "UIBackgroundModes": ["audio"], 22 | "UIFileSharingEnabled": true, 23 | "LSSupportsOpeningDocumentsInPlace": true, 24 | "NSAppTransportSecurity": { 25 | "NSAllowsArbitraryLoads": true 26 | }, 27 | "CADisableMinimumFrameDurationOnPhone": true 28 | }, 29 | "splash": { 30 | "image": "./assets/splash.png", 31 | "resizeMode": "cover", 32 | "backgroundColor": "#000" 33 | } 34 | }, 35 | "android": { 36 | "adaptiveIcon": { 37 | "foregroundImage": "./assets/adaptive-icon.png", 38 | "backgroundColor": "#000" 39 | }, 40 | "package": "com.music.player.gyc" 41 | }, 42 | "web": { 43 | "favicon": "./assets/favicon.png" 44 | }, 45 | "plugins": [ 46 | "expo-router", 47 | "expo-localization", 48 | [ 49 | "expo-share-intent", 50 | { 51 | "iosActivationRules": { 52 | "NSExtensionActivationSupportsFileWithMaxCount": 1, 53 | "NSExtensionActivationSupportsWebURLWithMaxCount": 1, 54 | "NSExtensionActivationSupportsText": 1 55 | } 56 | } 57 | ] 58 | ], 59 | "experiments": { 60 | "typedRoutes": true, 61 | "tsconfigPaths": true 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /assets/.fake-audio.mp3.md5: -------------------------------------------------------------------------------- 1 | e94a71b468d32b9a382de98138a14817e94a71b468d32b9a382de98138a14817 -------------------------------------------------------------------------------- /assets/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/assets/1024.png -------------------------------------------------------------------------------- /assets/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/assets/144.png -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/fake-audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/assets/fake-audio.mp3 -------------------------------------------------------------------------------- /assets/fake-audio.mp3.md5: -------------------------------------------------------------------------------- 1 | e94a71b468d32b9a382de98138a14817e94a71b468d32b9a382de98138a14817 -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/assets/icon.png -------------------------------------------------------------------------------- /assets/local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/assets/local.png -------------------------------------------------------------------------------- /assets/local1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/assets/local1.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/assets/splash.png -------------------------------------------------------------------------------- /assets/unknown_artist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/assets/unknown_artist.png -------------------------------------------------------------------------------- /assets/unknown_track.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/assets/unknown_track.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true) 3 | return { 4 | presets: ['babel-preset-expo'], 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | .xcode.env.local 25 | 26 | # Bundle artifacts 27 | *.jsbundle 28 | 29 | # CocoaPods 30 | /Pods/ 31 | -------------------------------------------------------------------------------- /ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /ios/CyMusic.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/CyMusic/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface AppDelegate : EXAppDelegateWrapper 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /ios/CyMusic/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | 6 | @implementation AppDelegate 7 | 8 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 9 | { 10 | self.moduleName = @"main"; 11 | 12 | // You can add your custom initial props in the dictionary below. 13 | // They will be passed down to the ViewController used by React Native. 14 | self.initialProps = @{}; 15 | 16 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 17 | } 18 | 19 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 20 | { 21 | return [self getBundleURL]; 22 | } 23 | 24 | - (NSURL *)getBundleURL 25 | { 26 | #if DEBUG 27 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; 28 | #else 29 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 30 | #endif 31 | } 32 | 33 | // Linking API 34 | - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { 35 | return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; 36 | } 37 | 38 | // Universal Links 39 | - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { 40 | BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 41 | return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; 42 | } 43 | 44 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 45 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken 46 | { 47 | return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; 48 | } 49 | 50 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 51 | - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error 52 | { 53 | return [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; 54 | } 55 | 56 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 57 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler 58 | { 59 | return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /ios/CyMusic/CyMusic-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /ios/CyMusic/CyMusic.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.com.music.player.gyc 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/CyMusic/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/ios/CyMusic/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/CyMusic/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "App-Icon-1024x1024@1x.png", 5 | "idiom": "universal", 6 | "platform": "ios", 7 | "size": "1024x1024" 8 | } 9 | ], 10 | "info": { 11 | "version": 1, 12 | "author": "expo" 13 | } 14 | } -------------------------------------------------------------------------------- /ios/CyMusic/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "expo" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/CyMusic/Images.xcassets/SplashScreen.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "version": 1, 19 | "author": "expo" 20 | } 21 | } -------------------------------------------------------------------------------- /ios/CyMusic/Images.xcassets/SplashScreen.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/ios/CyMusic/Images.xcassets/SplashScreen.imageset/image.png -------------------------------------------------------------------------------- /ios/CyMusic/Images.xcassets/SplashScreenBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "version": 1, 19 | "author": "expo" 20 | } 21 | } -------------------------------------------------------------------------------- /ios/CyMusic/Images.xcassets/SplashScreenBackground.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/ios/CyMusic/Images.xcassets/SplashScreenBackground.imageset/image.png -------------------------------------------------------------------------------- /ios/CyMusic/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | CyMusic 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | 1.1.7 23 | CFBundleSignature 24 | ???? 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleURLSchemes 29 | 30 | cymusic 31 | com.music.player.gyc 32 | 33 | 34 | 35 | CFBundleURLSchemes 36 | 37 | exp+cymusic 38 | 39 | 40 | 41 | CFBundleVersion 42 | 1 43 | LSRequiresIPhoneOS 44 | 45 | LSSupportsOpeningDocumentsInPlace 46 | 47 | NSAppTransportSecurity 48 | 49 | NSAllowsArbitraryLoads 50 | 51 | 52 | NSCameraUsageDescription 53 | Allow $(PRODUCT_NAME) to access your camera 54 | NSMicrophoneUsageDescription 55 | Allow $(PRODUCT_NAME) to access your microphone 56 | NSPhotoLibraryUsageDescription 57 | Allow $(PRODUCT_NAME) to access your photos 58 | NSUserActivityTypes 59 | 60 | $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route 61 | 62 | UIBackgroundModes 63 | 64 | audio 65 | 66 | UIFileSharingEnabled 67 | 68 | UILaunchStoryboardName 69 | SplashScreen 70 | UIRequiredDeviceCapabilities 71 | 72 | armv7 73 | 74 | UIRequiresFullScreen 75 | 76 | UIStatusBarStyle 77 | UIStatusBarStyleDefault 78 | UISupportedInterfaceOrientations 79 | 80 | UIInterfaceOrientationPortrait 81 | UIInterfaceOrientationPortraitUpsideDown 82 | 83 | UISupportedInterfaceOrientations~ipad 84 | 85 | UIInterfaceOrientationPortrait 86 | UIInterfaceOrientationPortraitUpsideDown 87 | UIInterfaceOrientationLandscapeLeft 88 | UIInterfaceOrientationLandscapeRight 89 | 90 | UIUserInterfaceStyle 91 | Dark 92 | UIViewControllerBasedStatusBarAppearance 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /ios/CyMusic/Supporting/Expo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EXUpdatesCheckOnLaunch 6 | ALWAYS 7 | EXUpdatesEnabled 8 | 9 | EXUpdatesLaunchWaitMs 10 | 0 11 | EXUpdatesSDKVersion 12 | 50.0.0 13 | 14 | -------------------------------------------------------------------------------- /ios/CyMusic/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /ios/CyMusic/noop-file.swift: -------------------------------------------------------------------------------- 1 | // 2 | // @generated 3 | // A blank Swift file must be created for native modules with Swift files to work correctly. 4 | // 5 | -------------------------------------------------------------------------------- /ios/Podfile.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo.jsEngine": "hermes", 3 | "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true" 4 | } 5 | -------------------------------------------------------------------------------- /ios/ShareExtension/MainInterface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/ShareExtension/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryUserDefaults 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | CA92.1 13 | 14 | 15 | 16 | NSPrivacyCollectedDataTypes 17 | 18 | NSPrivacyTracking 19 | 20 | 21 | -------------------------------------------------------------------------------- /ios/ShareExtension/ShareExtension-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | NSExtensionActivationRule 10 | 11 | NSExtensionActivationSupportsFileWithMaxCount 12 | 1 13 | 14 | 15 | NSExtensionMainStoryboard 16 | MainInterface 17 | NSExtensionPointIdentifier 18 | com.apple.share-services 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ios/ShareExtension/ShareExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.com.music.player.gyc 8 | 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "music-player", 3 | "version": "1.0.3", 4 | "main": "expo-router/entry", 5 | "scripts": { 6 | "android": "expo run:android", 7 | "ios": "expo run:ios", 8 | "lint": "eslint .", 9 | "postinstall": "patch-package" 10 | }, 11 | "dependencies": { 12 | "@react-native-async-storage/async-storage": "^1.23.1", 13 | "@react-native-menu/menu": "^0.9.1", 14 | "axios": "^1.7.2", 15 | "crypto-js": "^4.2.0", 16 | "expo": "~50.0.14", 17 | "expo-blur": "~12.9.2", 18 | "expo-constants": "~15.4.5", 19 | "expo-dev-client": "~3.3.11", 20 | "expo-document-picker": "^12.0.2", 21 | "expo-haptics": "~12.8.1", 22 | "expo-image-picker": "~14.7.1", 23 | "expo-keep-awake": "~12.8.2", 24 | "expo-linear-gradient": "~12.7.2", 25 | "expo-linking": "~6.2.2", 26 | "expo-localization": "~14.8.4", 27 | "expo-music-info-2": "^2.0.0", 28 | "expo-router": "~3.4.8", 29 | "expo-share-intent": "^1.9.0", 30 | "expo-status-bar": "~1.11.1", 31 | "i18n-js": "^4.4.3", 32 | "iconv-lite": "^0.6.3", 33 | "immer": "^10.1.1", 34 | "lodash.shuffle": "^4.2.0", 35 | "patch-package": "^8.0.0", 36 | "react": "18.2.0", 37 | "react-native": "0.73.6", 38 | "react-native-awesome-slider": "^2.5.1", 39 | "react-native-background-timer": "^2.4.1", 40 | "react-native-fast-image": "^8.6.3", 41 | "react-native-fs": "^2.20.0", 42 | "react-native-gesture-handler": "~2.18.0", 43 | "react-native-image-colors": "^2.4.0", 44 | "react-native-loader-kit": "^2.0.8", 45 | "react-native-lyric": "^1.0.2", 46 | "react-native-mmkv": "^2.12.2", 47 | "react-native-quick-base64": "^2.1.2", 48 | "react-native-quick-crypto": "^0.7.0", 49 | "react-native-quick-md5": "^3.0.6", 50 | "react-native-reanimated": "~3.6.2", 51 | "react-native-safe-area-context": "4.8.2", 52 | "react-native-screens": "~3.29.0", 53 | "react-native-simple-crypto": "^0.2.15", 54 | "react-native-toast-message": "^2.2.1", 55 | "react-native-track-player": "^4.1.1", 56 | "react-native-volume-manager": "^1.10.0", 57 | "ts-pattern": "^5.0.8", 58 | "zustand": "^4.5.2" 59 | }, 60 | "devDependencies": { 61 | "@babel/core": "^7.20.0", 62 | "@types/lodash.shuffle": "^4.2.9", 63 | "@types/react": "~18.2.0", 64 | "@typescript-eslint/eslint-plugin": "^7.4.0", 65 | "@typescript-eslint/parser": "^7.4.0", 66 | "eslint": "^8.57.0", 67 | "eslint-plugin-react": "^7.34.1", 68 | "eslint-plugin-react-hooks": "^4.6.0", 69 | "prettier": "^3.2.5", 70 | "typescript": "^5.1.3" 71 | }, 72 | "private": true 73 | } 74 | -------------------------------------------------------------------------------- /patches/xcode+3.0.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/xcode/lib/pbxProject.js b/node_modules/xcode/lib/pbxProject.js 2 | index 068548a..b478056 100644 3 | --- a/node_modules/xcode/lib/pbxProject.js 4 | +++ b/node_modules/xcode/lib/pbxProject.js 5 | @@ -1678,8 +1678,7 @@ function correctForFrameworksPath(file, project) { 6 | 7 | function correctForPath(file, project, group) { 8 | var r_group_dir = new RegExp('^' + group + '[\\\\/]'); 9 | - 10 | - if (project.pbxGroupByName(group).path) 11 | + if (project.pbxGroupByName(group)&&project.pbxGroupByName(group).path) 12 | file.path = file.path.replace(r_group_dir, ''); 13 | 14 | return file; 15 | -------------------------------------------------------------------------------- /readme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/readme.jpg -------------------------------------------------------------------------------- /src.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/src.zip -------------------------------------------------------------------------------- /src/app/(modals)/[name].tsx: -------------------------------------------------------------------------------- 1 | import { SingerTracksList } from '@/components/SingerTracksList' 2 | import { colors, screenPadding } from '@/constants/tokens' 3 | import { logInfo } from '@/helpers/logger' 4 | import { getAlbumSongList, getSingerDetail } from '@/helpers/userApi/getMusicSource' 5 | import { defaultStyles } from '@/styles' 6 | import { useLocalSearchParams, usePathname } from 'expo-router' 7 | import React, { useEffect, useState } from 'react' 8 | import { ActivityIndicator, ScrollView, View } from 'react-native' 9 | import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' 10 | import { Track } from 'react-native-track-player' 11 | import ShareIntent from './shareintent' 12 | // 专辑页面or歌手页面 13 | const SingerListScreen = () => { 14 | const pathname = usePathname() 15 | logInfo('pathname', pathname) 16 | 17 | const { name: playlistName, album } = useLocalSearchParams<{ name: string; album?: string }>() 18 | const isAlbum = !!album 19 | logInfo('album', album) 20 | 21 | const [singerListDetail, setSingerListDetail] = useState<{ musicList: Track[] } | null>(null) 22 | const [loading, setLoading] = useState(true) 23 | 24 | useEffect(() => { 25 | const fetchSingerListDetail = async () => { 26 | let detail 27 | if (isAlbum) { 28 | detail = await getAlbumSongList(playlistName) 29 | // console.log('detail', detail) 30 | // console.log('playlistName', playlistName) 31 | } else { 32 | detail = await getSingerDetail(playlistName) 33 | } 34 | 35 | setSingerListDetail(detail) 36 | 37 | setLoading(false) 38 | } 39 | fetchSingerListDetail() 40 | }, []) 41 | 42 | if (pathname.includes('cymusic')) { 43 | return 44 | } 45 | 46 | console.log('album', album) 47 | 48 | if (loading) { 49 | return ( 50 | 58 | 59 | 60 | ) 61 | } 62 | const DismissPlayerSymbol = () => { 63 | const { top } = useSafeAreaInsets() 64 | 65 | return ( 66 | 76 | 86 | 87 | ) 88 | } 89 | 90 | return ( 91 | 92 | 93 | 97 | 98 | 99 | 100 | ) 101 | } 102 | 103 | export default SingerListScreen 104 | -------------------------------------------------------------------------------- /src/app/(modals)/addToPlaylist.tsx: -------------------------------------------------------------------------------- 1 | import { PlaylistsListModal } from '@/components/PlaylistsListModal' 2 | import { screenPadding } from '@/constants/tokens' 3 | import myTrackPlayer from '@/helpers/trackPlayerIndex' 4 | import { useFavorites } from '@/store/library' 5 | import { defaultStyles } from '@/styles' 6 | import { useHeaderHeight } from '@react-navigation/elements' 7 | import { useLocalSearchParams, useRouter } from 'expo-router' 8 | import { Alert, StyleSheet } from 'react-native' 9 | import { SafeAreaView } from 'react-native-safe-area-context' 10 | import { Track } from 'react-native-track-player' 11 | 12 | const AddToPlaylistModal = () => { 13 | const router = useRouter() 14 | const params = useLocalSearchParams() 15 | 16 | const track: IMusic.IMusicItem = { 17 | title: params.title as string, 18 | album: params.album as string, 19 | artwork: params.artwork as string, 20 | artist: params.artist as string, 21 | id: params.id as string, 22 | url: (params.url as string) || 'Unknown', 23 | platform: (params.platform as string) || 'tx', 24 | duration: typeof params.duration === 'string' ? parseInt(params.duration, 10) : 0, 25 | } 26 | const headerHeight = useHeaderHeight() 27 | 28 | const { favorites, toggleTrackFavorite } = useFavorites() 29 | // track was not found 30 | if (!track) { 31 | return null 32 | } 33 | 34 | const handlePlaylistPress = async (playlist: IMusic.PlayList) => { 35 | // console.log('playlist', playlist) 36 | if (playlist.id === 'favorites') { 37 | if (favorites.find((item) => item.id === track.id)) { 38 | console.log('已收藏') 39 | } else { 40 | toggleTrackFavorite(track as Track) 41 | } 42 | } else { 43 | myTrackPlayer.addSongToStoredPlayList(playlist, track) 44 | } 45 | // should close the modal 46 | router.dismiss() 47 | Alert.alert('成功', '添加成功') 48 | 49 | // if the current queue is the playlist we're adding to, add the track at the end of the queue 50 | // if (activeQueueId?.startsWith(playlist.name)) { 51 | // await TrackPlayer.add(track) 52 | // } 53 | } 54 | 55 | return ( 56 | 57 | 58 | 59 | ) 60 | } 61 | 62 | const styles = StyleSheet.create({ 63 | modalContainer: { 64 | ...defaultStyles.container, 65 | paddingHorizontal: screenPadding.horizontal, 66 | }, 67 | }) 68 | 69 | export default AddToPlaylistModal 70 | -------------------------------------------------------------------------------- /src/app/(modals)/logScreen.tsx: -------------------------------------------------------------------------------- 1 | import LogScreen from '@/components/LogScreen' 2 | import React from 'react' 3 | 4 | const LogScreenModal = () => { 5 | return 6 | } 7 | 8 | export default LogScreenModal 9 | -------------------------------------------------------------------------------- /src/app/(modals)/playList.tsx: -------------------------------------------------------------------------------- 1 | import { NowPlayList } from '@/components/NowPlayList' 2 | import { colors, screenPadding } from '@/constants/tokens' 3 | import { usePlayList } from '@/store/playList' 4 | import { defaultStyles } from '@/styles' 5 | import { useHeaderHeight } from '@react-navigation/elements' 6 | import React from 'react' 7 | import { StyleSheet } from 'react-native' 8 | import { SafeAreaView } from 'react-native-safe-area-context' 9 | import { Track } from 'react-native-track-player' 10 | 11 | const PlayListScreen = () => { 12 | const headerHeight = useHeaderHeight() 13 | const tracks = usePlayList() 14 | 15 | return ( 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | const styles = StyleSheet.create({ 23 | modalContainer: { 24 | flex: 1, 25 | paddingHorizontal: screenPadding.horizontal, 26 | backgroundColor: defaultStyles.container.backgroundColor, // 设置默认背景颜色 27 | }, 28 | header: { 29 | fontSize: 28, 30 | fontWeight: 'bold', 31 | padding: 0, 32 | paddingBottom: 20, 33 | paddingTop: 0, 34 | color: colors.text, 35 | }, 36 | }) 37 | 38 | export default PlayListScreen 39 | -------------------------------------------------------------------------------- /src/app/(tabs)/(songs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import GlobalButton from '@/components/GlobalButton' 2 | import { StackScreenWithSearchBar } from '@/constants/layout' 3 | import { defaultStyles } from '@/styles' 4 | import i18n, { nowLanguage } from '@/utils/i18n' 5 | import { Stack } from 'expo-router' 6 | import { View } from 'react-native' 7 | const SongsScreenLayout = () => { 8 | const language = nowLanguage.useValue() 9 | return ( 10 | 11 | 12 | , 18 | }} 19 | /> 20 | 21 | 22 | ) 23 | } 24 | 25 | export default SongsScreenLayout 26 | -------------------------------------------------------------------------------- /src/app/(tabs)/(songs)/index.tsx: -------------------------------------------------------------------------------- 1 | import { TracksList } from '@/components/TracksList' 2 | import { screenPadding } from '@/constants/tokens' 3 | import { trackTitleFilter } from '@/helpers/filter' 4 | import { generateTracksListId } from '@/helpers/miscellaneous' 5 | import { songsNumsToLoadStore } from '@/helpers/trackPlayerIndex' 6 | import { useNavigationSearch } from '@/hooks/useNavigationSearch' 7 | import { useLibraryStore, useTracks, useTracksLoading } from '@/store/library' 8 | import { defaultStyles } from '@/styles' 9 | import i18n from '@/utils/i18n' 10 | import { useMemo } from 'react' 11 | import { ActivityIndicator, ScrollView, View } from 'react-native' 12 | const SongsScreen = () => { 13 | const search = useNavigationSearch({ 14 | searchBarOptions: { 15 | placeholder: i18n.t('find.inSongs'), 16 | cancelButtonText: i18n.t('find.cancel'), 17 | }, 18 | }) 19 | 20 | const tracks = useTracks() 21 | const songsNumsToLoad = songsNumsToLoadStore.useValue() 22 | const isLoading = useTracksLoading() // 添加加载状态 23 | const { fetchTracks } = useLibraryStore() 24 | const filteredTracks = useMemo(() => { 25 | if (!search) return tracks 26 | return tracks.filter(trackTitleFilter(search)) 27 | }, [search, tracks]) 28 | const handleLoadMore = () => { 29 | fetchTracks() 30 | } 31 | 32 | if (!tracks.length && isLoading) { 33 | return ( 34 | 35 | 36 | 37 | ) 38 | } 39 | return ( 40 | 41 | { 45 | const { layoutMeasurement, contentOffset, contentSize } = nativeEvent 46 | const paddingToBottom = 20 47 | const isCloseToBottom = 48 | layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom 49 | 50 | if (isCloseToBottom) { 51 | handleLoadMore() 52 | } 53 | }} 54 | scrollEventThrottle={400} 55 | > 56 | 62 | {/* {isLoading && tracks.length > 0 && ( 63 | 70 | 71 | 72 | )} */} 73 | 74 | 75 | ) 76 | } 77 | 78 | export default SongsScreen 79 | -------------------------------------------------------------------------------- /src/app/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { FloatingPlayer } from '@/components/FloatingPlayer' 2 | import { colors, fontSize } from '@/constants/tokens' 3 | import i18n, { nowLanguage } from '@/utils/i18n' 4 | import { FontAwesome, Ionicons, MaterialCommunityIcons } from '@expo/vector-icons' 5 | import { BlurView } from 'expo-blur' 6 | import { Tabs } from 'expo-router' 7 | import React from 'react' 8 | import { StyleSheet } from 'react-native' 9 | const TabsNavigation = () => { 10 | const language = nowLanguage.useValue() 11 | return ( 12 | <> 13 | ( 29 | 38 | ), 39 | }} 40 | > 41 | ( 46 | 47 | ), 48 | }} 49 | /> 50 | , 55 | }} 56 | /> 57 | , //当你定义 tabBarIcon 时,React Navigation 会自动传递一些参数给你,其中包括 color、focused 和 size。这些参数会根据当前 Tab 的选中状态和主题来动态变化。 62 | }} 63 | /> 64 | ( 69 | 70 | ), 71 | }} 72 | /> 73 | 74 | 75 | 83 | 84 | ) 85 | } 86 | 87 | export default TabsNavigation 88 | -------------------------------------------------------------------------------- /src/app/(tabs)/favorites/[name].tsx: -------------------------------------------------------------------------------- 1 | import { PlaylistTracksList } from '@/components/PlaylistTracksList' 2 | import { screenPadding } from '@/constants/tokens' 3 | import myTrackPlayer, { playListsStore } from '@/helpers/trackPlayerIndex' 4 | import { Playlist } from '@/helpers/types' 5 | 6 | import { defaultStyles } from '@/styles' 7 | import { Redirect, useLocalSearchParams } from 'expo-router' 8 | import React, { useCallback, useMemo } from 'react' 9 | import { ScrollView, View } from 'react-native' 10 | import { Track } from 'react-native-track-player' 11 | 12 | const PlaylistScreen = () => { 13 | const { name: playlistID } = useLocalSearchParams<{ name: string }>() 14 | const playlists = playListsStore.useValue() as Playlist[] | null 15 | 16 | const playlist = useMemo(() => { 17 | return playlists?.find((p) => p.id === playlistID) 18 | }, [playlistID, playlists]) 19 | 20 | const songs = useMemo(() => { 21 | return playlist?.songs || [] 22 | }, [playlist]) 23 | 24 | const handleDeleteTrack = useCallback( 25 | (trackId: string) => { 26 | myTrackPlayer.deleteSongFromStoredPlayList(playlist as Playlist, trackId) 27 | }, 28 | [playlistID], 29 | ) 30 | 31 | if (!playlist) { 32 | console.warn(`Playlist ${playlistID} was not found!`) 33 | return 34 | } 35 | 36 | return ( 37 | 38 | 42 | 48 | 49 | 50 | ) 51 | } 52 | 53 | export default PlaylistScreen 54 | -------------------------------------------------------------------------------- /src/app/(tabs)/favorites/_layout.tsx: -------------------------------------------------------------------------------- 1 | import AddPlayListButton from '@/components/AddPlayListButton' 2 | import { StackScreenWithSearchBar } from '@/constants/layout' 3 | import { colors } from '@/constants/tokens' 4 | import { defaultStyles } from '@/styles' 5 | import i18n, { nowLanguage } from '@/utils/i18n' 6 | import { Stack } from 'expo-router' 7 | import { View } from 'react-native' 8 | const FavoritesScreenLayout = () => { 9 | const language = nowLanguage.useValue() 10 | return ( 11 | 12 | 13 | , 19 | }} 20 | /> 21 | 32 | 43 | 54 | 55 | 56 | ) 57 | } 58 | 59 | export default FavoritesScreenLayout 60 | -------------------------------------------------------------------------------- /src/app/(tabs)/favorites/favoriteMusic.tsx: -------------------------------------------------------------------------------- 1 | import { PlaylistTracksList } from '@/components/PlaylistTracksList' 2 | import { screenPadding } from '@/constants/tokens' 3 | import { Playlist } from '@/helpers/types' 4 | import { useFavorites } from '@/store/library' 5 | import { defaultStyles } from '@/styles' 6 | import i18n from '@/utils/i18n' 7 | import React, { useMemo } from 'react' 8 | import { ScrollView, View } from 'react-native' 9 | import { Track } from 'react-native-track-player' 10 | const FavoriteMusicScreen = () => { 11 | // const search = useNavigationSearch({ 12 | // searchBarOptions: { 13 | // placeholder: 'Find in favorites', 14 | // }, 15 | // }) 16 | 17 | const { favorites } = useFavorites() 18 | const playListItem = { 19 | name: 'Favorites', 20 | id: 'favorites', 21 | tracks: [], 22 | title: i18n.t('appTab.favoritesSongs'), 23 | coverImg: 'https://y.qq.com/mediastyle/global/img/cover_like.png?max_age=2592000', 24 | description: i18n.t('appTab.favoritesSongs'), 25 | } 26 | const playLists = [playListItem] 27 | const filteredFavoritesTracks = useMemo(() => { 28 | // if (!search) return favorites as Track[] 29 | 30 | return favorites as Track[] 31 | }, [favorites]) 32 | 33 | return ( 34 | 35 | 39 | 40 | 41 | 42 | ) 43 | } 44 | export default FavoriteMusicScreen 45 | -------------------------------------------------------------------------------- /src/app/(tabs)/favorites/index.tsx: -------------------------------------------------------------------------------- 1 | import localImage from '@/assets/local.png' 2 | import { PlaylistsList } from '@/components/PlaylistsList' 3 | import { screenPadding } from '@/constants/tokens' 4 | import { playListsStore } from '@/helpers/trackPlayerIndex' 5 | import { Playlist } from '@/helpers/types' 6 | import { useNavigationSearch } from '@/hooks/useNavigationSearch' 7 | import { defaultStyles } from '@/styles' 8 | import i18n from '@/utils/i18n' 9 | import { router } from 'expo-router' 10 | import { useMemo } from 'react' 11 | import { Image, ScrollView, View } from 'react-native' 12 | 13 | const FavoritesScreen = () => { 14 | const search = useNavigationSearch({ 15 | searchBarOptions: { 16 | placeholder: i18n.t('find.inFavorites'), 17 | cancelButtonText: i18n.t('find.cancel'), 18 | }, 19 | }) 20 | 21 | const favoritePlayListItem = { 22 | name: 'Favorites', 23 | id: 'favorites', 24 | tracks: [], 25 | title: i18n.t('appTab.favoritesSongs'), 26 | coverImg: 'https://y.qq.com/mediastyle/global/img/cover_like.png?max_age=2592000', 27 | description: i18n.t('appTab.favoritesSongs'), 28 | } 29 | 30 | const localPlayListItem = { 31 | name: 'Local', 32 | id: 'local', 33 | tracks: [], 34 | title: i18n.t('appTab.localOrCachedSongs'), 35 | coverImg: Image.resolveAssetSource(localImage).uri, 36 | description: i18n.t('appTab.localOrCachedSongs'), 37 | } 38 | const storedPlayLists = playListsStore.useValue() || [] 39 | const playLists = [favoritePlayListItem, localPlayListItem, ...storedPlayLists] 40 | 41 | const filteredPlayLists = useMemo(() => { 42 | if (!search) return playLists as Playlist[] 43 | 44 | return playLists.filter((playlist: Playlist) => 45 | playlist.name.toLowerCase().includes(search.toLowerCase()), 46 | ) as Playlist[] 47 | }, [search, playLists, storedPlayLists]) 48 | const handlePlaylistPress = (playlist: Playlist) => { 49 | if (playlist.name == 'Favorites') { 50 | router.push(`/(tabs)/favorites/favoriteMusic`) 51 | } else if (playlist.name == 'Local') { 52 | router.push(`/(tabs)/favorites/localMusic`) 53 | } else { 54 | router.push(`/(tabs)/favorites/${playlist.id}`) 55 | } 56 | } 57 | return ( 58 | 59 | 65 | 70 | 71 | 72 | ) 73 | } 74 | 75 | export default FavoritesScreen 76 | -------------------------------------------------------------------------------- /src/app/(tabs)/radio/[name].tsx: -------------------------------------------------------------------------------- 1 | import { PlaylistTracksList } from '@/components/PlaylistTracksList' 2 | import { colors, screenPadding } from '@/constants/tokens' 3 | import { getTopListDetail } from '@/helpers/userApi/getMusicSource' 4 | import { usePlaylists } from '@/store/library' 5 | import { defaultStyles } from '@/styles' 6 | import { Redirect, useLocalSearchParams } from 'expo-router' 7 | import React, { useEffect, useState } from 'react' 8 | import { ActivityIndicator, ScrollView, View } from 'react-native' 9 | import { Track } from 'react-native-track-player' 10 | 11 | const RadioListScreen = () => { 12 | const { name: playlistName } = useLocalSearchParams<{ name: string }>() 13 | const { playlists } = usePlaylists() 14 | const [topListDetail, setTopListDetail] = useState<{ musicList: Track[] } | null>(null) 15 | const [loading, setLoading] = useState(true) 16 | 17 | const playlist = playlists.find((playlist) => playlist.title === playlistName) 18 | 19 | useEffect(() => { 20 | const fetchTopListDetail = async () => { 21 | // console.log(playlistName) 22 | if (!playlist) { 23 | console.warn(`Playlist ${playlistName} was not found!`) 24 | setLoading(false) 25 | return 26 | } 27 | 28 | const detail = await getTopListDetail(playlist) 29 | setTopListDetail(detail) 30 | // console.log(JSON.stringify(detail)); 31 | setLoading(false) 32 | } 33 | fetchTopListDetail() 34 | }, []) 35 | 36 | if (loading) { 37 | return ( 38 | 46 | 47 | 48 | ) 49 | } 50 | 51 | if (!playlist || !topListDetail) { 52 | return 53 | } 54 | 55 | return ( 56 | 57 | 61 | 62 | 63 | 64 | ) 65 | } 66 | 67 | export default RadioListScreen 68 | -------------------------------------------------------------------------------- /src/app/(tabs)/radio/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { StackScreenWithSearchBar } from '@/constants/layout' 2 | import { colors } from '@/constants/tokens' 3 | import { defaultStyles } from '@/styles' 4 | import i18n, { nowLanguage } from '@/utils/i18n' 5 | import { Stack } from 'expo-router' 6 | import { View } from 'react-native' 7 | const RadiolistsScreenLayout = () => { 8 | const language = nowLanguage.useValue() 9 | return ( 10 | 11 | 12 | 19 | 30 | 31 | 32 | ) 33 | } 34 | 35 | export default RadiolistsScreenLayout 36 | -------------------------------------------------------------------------------- /src/app/(tabs)/radio/index.tsx: -------------------------------------------------------------------------------- 1 | import { RadioList } from '@/components/RadioList' 2 | import { screenPadding } from '@/constants/tokens' 3 | import { playlistNameFilter } from '@/helpers/filter' 4 | import { Playlist } from '@/helpers/types' 5 | import { useNavigationSearch } from '@/hooks/useNavigationSearch' 6 | import { usePlaylists } from '@/store/library' 7 | import { defaultStyles } from '@/styles' 8 | import i18n from '@/utils/i18n' 9 | import { useRouter } from 'expo-router' 10 | import { useMemo } from 'react' 11 | import { ScrollView, View } from 'react-native' 12 | const RadiolistsScreen = () => { 13 | const router = useRouter() 14 | 15 | const search = useNavigationSearch({ 16 | searchBarOptions: { 17 | placeholder: i18n.t('find.inRadio'), 18 | cancelButtonText: i18n.t('find.cancel'), 19 | }, 20 | }) 21 | 22 | const { playlists, setPlayList } = usePlaylists() 23 | const filteredPlayLists = useMemo(() => { 24 | if (!search) return playlists 25 | return playlists.filter(playlistNameFilter(search)) 26 | }, [search, playlists]) 27 | 28 | const handlePlaylistPress = (playlist: Playlist) => { 29 | router.push(`/(tabs)/radio/${playlist.title}`) 30 | } 31 | 32 | return ( 33 | 34 | 40 | 45 | 46 | 47 | ) 48 | } 49 | 50 | export default RadiolistsScreen 51 | -------------------------------------------------------------------------------- /src/app/(tabs)/search/[name].tsx: -------------------------------------------------------------------------------- 1 | import { screenPadding } from '@/constants/tokens' 2 | import { usePlaylists } from '@/store/library' 3 | import { defaultStyles } from '@/styles' 4 | import { Redirect, useLocalSearchParams } from 'expo-router' 5 | import { ScrollView, View } from 'react-native' 6 | 7 | const PlaylistScreen = () => { 8 | const { name: playlistName } = useLocalSearchParams<{ name: string }>() 9 | 10 | const { playlists } = usePlaylists() 11 | 12 | const playlist = playlists.find((playlist) => playlist.name === playlistName) 13 | 14 | if (!playlist) { 15 | console.warn(`song ${playlistName} was not found!`) 16 | 17 | return 18 | } 19 | 20 | return ( 21 | 22 | 26 | 27 | ) 28 | } 29 | 30 | export default PlaylistScreen 31 | -------------------------------------------------------------------------------- /src/app/(tabs)/search/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { StackScreenWithSearchBar } from '@/constants/layout' 2 | import { colors } from '@/constants/tokens' 3 | import { defaultStyles } from '@/styles' 4 | import i18n, { nowLanguage } from '@/utils/i18n' 5 | import { Stack } from 'expo-router' 6 | import { View } from 'react-native' 7 | const PlaylistsScreenLayout = () => { 8 | const language = nowLanguage.useValue() 9 | return ( 10 | 11 | 12 | 19 | 20 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default PlaylistsScreenLayout 37 | -------------------------------------------------------------------------------- /src/app/[...unmatched].tsx: -------------------------------------------------------------------------------- 1 | import { Redirect } from 'expo-router' 2 | 3 | export default function Unmatched() { 4 | // 重定向到主页 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /src/components/AddPlayListButton.tsx: -------------------------------------------------------------------------------- 1 | // src/components/GlobalButton.tsx 2 | import { router } from 'expo-router' 3 | import React from 'react' 4 | import { Button, StyleSheet, View } from 'react-native' 5 | import { MaterialCommunityIcons } from '@expo/vector-icons' 6 | import { colors } from '@/constants/tokens' 7 | 8 | const addPlayListButton = () => { 9 | 10 | const showPlayList = () => { 11 | router.navigate('/(modals)/importPlayList') 12 | } 13 | 14 | return ( 15 | 16 | 23 | 24 | ) 25 | } 26 | 27 | const styles = StyleSheet.create({ 28 | container: {}, 29 | }) 30 | 31 | export default addPlayListButton 32 | -------------------------------------------------------------------------------- /src/components/ArtistTracksList.tsx: -------------------------------------------------------------------------------- 1 | import { unknownArtistImageUri } from '@/constants/images' 2 | import { fontSize } from '@/constants/tokens' 3 | import { trackTitleFilter } from '@/helpers/filter' 4 | import { generateTracksListId } from '@/helpers/miscellaneous' 5 | import { Artist } from '@/helpers/types' 6 | import { useNavigationSearch } from '@/hooks/useNavigationSearch' 7 | import { defaultStyles } from '@/styles' 8 | import i18n from '@/utils/i18n' 9 | import { useMemo } from 'react' 10 | import { StyleSheet, Text, View } from 'react-native' 11 | import FastImage from 'react-native-fast-image' 12 | import { QueueControls } from './QueueControls' 13 | import { TracksList } from './TracksList' 14 | export const ArtistTracksList = ({ artist }: { artist: Artist }) => { 15 | const search = useNavigationSearch({ 16 | searchBarOptions: { 17 | hideWhenScrolling: true, 18 | placeholder: i18n.t('find.inSongs'), 19 | cancelButtonText: i18n.t('find.cancel'), 20 | }, 21 | }) 22 | 23 | const filteredArtistTracks = useMemo(() => { 24 | return artist.tracks.filter(trackTitleFilter(search)) 25 | }, [artist.tracks, search]) 26 | 27 | return ( 28 | 35 | 36 | 43 | 44 | 45 | 46 | {artist.name} 47 | 48 | 49 | {search.length === 0 && ( 50 | 51 | )} 52 | 53 | } 54 | tracks={filteredArtistTracks} 55 | /> 56 | ) 57 | } 58 | 59 | const styles = StyleSheet.create({ 60 | artistHeaderContainer: { 61 | flex: 1, 62 | marginBottom: 32, 63 | }, 64 | artworkImageContainer: { 65 | flexDirection: 'row', 66 | justifyContent: 'center', 67 | height: 200, 68 | }, 69 | artistImage: { 70 | width: '60%', 71 | height: '100%', 72 | resizeMode: 'cover', 73 | borderRadius: 128, 74 | }, 75 | artistNameText: { 76 | ...defaultStyles.text, 77 | marginTop: 22, 78 | textAlign: 'center', 79 | fontSize: fontSize.lg, 80 | fontWeight: '800', 81 | }, 82 | }) 83 | -------------------------------------------------------------------------------- /src/components/FloatingPlayer.tsx: -------------------------------------------------------------------------------- 1 | import { PlayPauseButton, SkipToNextButton } from '@/components/PlayerControls' 2 | import { unknownTrackImageUri } from '@/constants/images' 3 | import { useLastActiveTrack } from '@/hooks/useLastActiveTrack' 4 | import { defaultStyles } from '@/styles' 5 | import { useRouter } from 'expo-router' 6 | import { StyleSheet, TouchableOpacity, View, ViewProps } from 'react-native' 7 | import FastImage from 'react-native-fast-image' 8 | import { useActiveTrack } from 'react-native-track-player' 9 | import { MovingText } from './MovingText' 10 | 11 | export const FloatingPlayer = ({ style }: ViewProps) => { 12 | const router = useRouter() 13 | 14 | const activeTrack = useActiveTrack() 15 | const lastActiveTrack = useLastActiveTrack() 16 | 17 | const displayedTrack = activeTrack ?? lastActiveTrack 18 | 19 | const handlePress = () => { 20 | router.navigate('/player') 21 | } 22 | 23 | if (!displayedTrack) return null 24 | 25 | return ( 26 | 27 | <> 28 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | {/**/} 45 | 46 | 47 | 48 | 49 | 50 | ) 51 | } 52 | 53 | const styles = StyleSheet.create({ 54 | container: { 55 | flexDirection: 'row', 56 | alignItems: 'center', 57 | backgroundColor: '#252525', 58 | padding: 8, 59 | borderRadius: 12, 60 | paddingVertical: 10, 61 | }, 62 | trackArtworkImage: { 63 | width: 40, 64 | height: 40, 65 | borderRadius: 8, 66 | }, 67 | trackTitleContainer: { 68 | flex: 1, 69 | overflow: 'hidden', 70 | marginLeft: 10, 71 | }, 72 | trackTitle: { 73 | ...defaultStyles.text, 74 | fontSize: 18, 75 | fontWeight: '600', 76 | paddingLeft: 10, 77 | }, 78 | trackControlsContainer: { 79 | flexDirection: 'row', 80 | alignItems: 'center', 81 | columnGap: 20, 82 | marginRight: 16, 83 | paddingLeft: 16, 84 | }, 85 | }) 86 | -------------------------------------------------------------------------------- /src/components/GlobalButton.tsx: -------------------------------------------------------------------------------- 1 | // src/components/GlobalButton.tsx 2 | import { router } from 'expo-router' 3 | import React from 'react' 4 | import { Button, StyleSheet, View } from 'react-native' 5 | import { MaterialCommunityIcons } from '@expo/vector-icons' 6 | import { colors } from '@/constants/tokens' 7 | 8 | const GlobalButton = () => { 9 | 10 | const showPlayList = () => { 11 | router.navigate('/(modals)/settingModal') 12 | } 13 | 14 | return ( 15 | 16 | 23 | 24 | ) 25 | } 26 | 27 | const styles = StyleSheet.create({ 28 | container: {}, 29 | }) 30 | 31 | export default GlobalButton 32 | -------------------------------------------------------------------------------- /src/components/MovingText.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import Animated, { 3 | Easing, 4 | StyleProps, 5 | cancelAnimation, 6 | useAnimatedStyle, 7 | useSharedValue, 8 | withDelay, 9 | withRepeat, 10 | withTiming, 11 | } from 'react-native-reanimated' 12 | 13 | export type MovingTextProps = { 14 | text: string 15 | animationThreshold: number 16 | style?: StyleProps 17 | } 18 | 19 | export const MovingText = ({ text, animationThreshold, style }: MovingTextProps) => { 20 | const translateX = useSharedValue(0) 21 | const shouldAnimate = text.length >= animationThreshold 22 | 23 | const textWidth = text.length * 3 24 | 25 | useEffect(() => { 26 | if (!shouldAnimate) return 27 | 28 | translateX.value = withDelay( 29 | 1000, 30 | withRepeat( 31 | withTiming(-textWidth, { 32 | duration: 5000, 33 | easing: Easing.linear, 34 | }), 35 | -1, 36 | true, 37 | ), 38 | ) 39 | 40 | return () => { 41 | cancelAnimation(translateX) 42 | translateX.value = 0//useEffect 钩子函数的返回值是一个清理函数(cleanup function),它在以下情况下执行:组件卸载时。依赖项(dependency array)中的某个值在重新渲染时发生变化时。 43 | } 44 | }, [translateX, text, animationThreshold, shouldAnimate, textWidth]) 45 | 46 | const animatedStyle = useAnimatedStyle(() => { 47 | return { 48 | transform: [{ translateX: translateX.value }], 49 | } 50 | }) 51 | 52 | return ( 53 | 64 | {text} 65 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /src/components/PlayerControls.tsx: -------------------------------------------------------------------------------- 1 | import { colors } from '@/constants/tokens' 2 | import { FontAwesome6 } from '@expo/vector-icons' 3 | import { StyleSheet, TouchableOpacity, View, ViewStyle } from 'react-native' 4 | import TrackPlayer, { useIsPlaying } from 'react-native-track-player' 5 | 6 | import { useLibraryStore } from '@/store/library' 7 | import myTrackPlayer from '@/helpers/trackPlayerIndex' 8 | 9 | type PlayerControlsProps = { 10 | style?: ViewStyle 11 | } 12 | 13 | type PlayerButtonProps = { 14 | style?: ViewStyle 15 | iconSize?: number 16 | } 17 | 18 | export const PlayerControls = ({ style }: PlayerControlsProps) => { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export const PlayPauseButton = ({ style, iconSize = 48 }: PlayerButtonProps) => { 33 | const { playing } = useIsPlaying() 34 | 35 | return ( 36 | 37 | 41 | 42 | 43 | 44 | ) 45 | } 46 | 47 | 48 | 49 | export const SkipToNextButton = ({ iconSize = 30 }: PlayerButtonProps) => { 50 | const { tracks, fetchTracks } = useLibraryStore((state) => ({ 51 | tracks: state.tracks, 52 | fetchTracks: state.fetchTracks, 53 | })) 54 | 55 | return ( 56 | 57 | 58 | 59 | ) 60 | } 61 | 62 | export const SkipToPreviousButton = ({ iconSize = 30 }: PlayerButtonProps) => { 63 | 64 | 65 | return ( 66 | 67 | 68 | 69 | ) 70 | } 71 | 72 | const styles = StyleSheet.create({ 73 | container: { 74 | width: '100%', 75 | }, 76 | row: { 77 | flexDirection: 'row', 78 | justifyContent: 'space-evenly', 79 | alignItems: 'center', 80 | }, 81 | }) 82 | -------------------------------------------------------------------------------- /src/components/PlayerRepeatToggle.tsx: -------------------------------------------------------------------------------- 1 | import { colors } from '@/constants/tokens' 2 | import { useTrackPlayerRepeatMode } from '@/hooks/useTrackPlayerRepeatMode' 3 | import { MaterialCommunityIcons } from '@expo/vector-icons' 4 | import { ComponentProps } from 'react' 5 | import { RepeatMode } from 'react-native-track-player' 6 | import { match } from 'ts-pattern' 7 | import myTrackPlayer, { MusicRepeatMode, repeatModeStore } from '@/helpers/trackPlayerIndex' 8 | import { GlobalState } from '@/utils/stateMapper' 9 | 10 | type IconProps = Omit, 'name'> 11 | type IconName = ComponentProps['name'] 12 | 13 | // const repeatOrder = [RepeatMode.Off, RepeatMode.Track, RepeatMode.Queue] as const 14 | 15 | export const PlayerRepeatToggle = ({ ...iconProps }: IconProps) => { 16 | 17 | 18 | const toggleRepeatMode = () => { 19 | myTrackPlayer.toggleRepeatMode() 20 | } 21 | 22 | const icon = match(myTrackPlayer.getRepeatMode()) 23 | .returnType() 24 | .with(MusicRepeatMode.SHUFFLE, () => 'shuffle') 25 | .with(MusicRepeatMode.SINGLE, () => 'repeat-once') 26 | .with(MusicRepeatMode.QUEUE, () => 'repeat') 27 | .otherwise(() => 'repeat-off') 28 | 29 | return ( 30 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/components/PlayerVolumeBar.tsx: -------------------------------------------------------------------------------- 1 | import { colors } from '@/constants/tokens' 2 | import { utilsStyles } from '@/styles' 3 | import { Ionicons } from '@expo/vector-icons' 4 | import React, { useEffect, useState } from 'react' 5 | import { View, ViewProps } from 'react-native' 6 | import { Slider } from 'react-native-awesome-slider' 7 | import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated' 8 | import { VolumeManager } from 'react-native-volume-manager' 9 | 10 | const NORMAL_HEIGHT = 4 11 | const EXPANDED_HEIGHT = 4 12 | 13 | export const PlayerVolumeBar = ({ style }: ViewProps) => { 14 | const [volume, setVolume] = useState(0) 15 | const progress = useSharedValue(0) 16 | const min = useSharedValue(0) 17 | const max = useSharedValue(1) 18 | const isSliding = useSharedValue(false) 19 | 20 | const [trackColor, setTrackColor] = useState(colors.maximumTrackTintColor) 21 | 22 | useEffect(() => { 23 | const getInitialVolume = async () => { 24 | await VolumeManager.showNativeVolumeUI({ enabled: true }) 25 | const initialVolume = await VolumeManager.getVolume() 26 | setVolume(initialVolume.volume) 27 | progress.value = initialVolume.volume 28 | } 29 | getInitialVolume() 30 | 31 | const volumeListener = VolumeManager.addVolumeListener((result) => { 32 | setVolume(result.volume) 33 | progress.value = result.volume 34 | }) 35 | 36 | return () => { 37 | volumeListener.remove() 38 | } 39 | }, []) 40 | 41 | const animatedSliderStyle = useAnimatedStyle(() => { 42 | return { 43 | height: withSpring(isSliding.value ? EXPANDED_HEIGHT : NORMAL_HEIGHT), 44 | transform: [{ scaleY: withSpring(isSliding.value ? 2 : 1) }], 45 | } 46 | }) 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 | 56 | { 61 | isSliding.value = true 62 | setTrackColor('#fff') 63 | }} 64 | onSlidingComplete={() => { 65 | isSliding.value = false 66 | setTrackColor(colors.maximumTrackTintColor) 67 | }} 68 | onValueChange={async (value) => { 69 | await VolumeManager.showNativeVolumeUI({ enabled: true }) 70 | await VolumeManager.setVolume(value, { 71 | type: 'system', // default: "music" (Android only) 72 | showUI: true, // default: false (suppress native UI volume toast for iOS & Android) 73 | playSound: false, // default: false (Android only) 74 | }) 75 | }} 76 | renderBubble={() => null} 77 | theme={{ 78 | minimumTrackTintColor: trackColor, 79 | maximumTrackTintColor: colors.maximumTrackTintColor, 80 | }} 81 | thumbWidth={0} 82 | maximumValue={max} 83 | /> 84 | 85 | 86 | 87 | 88 | 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /src/components/PlaylistListItem.tsx: -------------------------------------------------------------------------------- 1 | import { colors } from '@/constants/tokens' 2 | import { Playlist } from '@/helpers/types' 3 | import { defaultStyles } from '@/styles' 4 | import { AntDesign } from '@expo/vector-icons' 5 | import { StyleSheet, Text, TouchableHighlight, TouchableHighlightProps, View } from 'react-native' 6 | import FastImage from 'react-native-fast-image' 7 | 8 | type PlaylistListItemProps = { 9 | playlist: Playlist 10 | } & TouchableHighlightProps 11 | 12 | export const PlaylistListItem = ({ playlist, ...props }: PlaylistListItemProps) => { 13 | return ( 14 | 15 | 16 | 17 | 24 | 25 | 26 | 34 | 35 | {playlist.title} 36 | 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | const styles = StyleSheet.create({ 46 | playlistItemContainer: { 47 | flexDirection: 'row', 48 | columnGap: 14, 49 | alignItems: 'center', 50 | paddingRight: 90, 51 | }, 52 | playlistArtworkImage: { 53 | borderRadius: 8, 54 | width: 70, 55 | height: 70, 56 | }, 57 | playlistNameText: { 58 | ...defaultStyles.text, 59 | fontSize: 17, 60 | fontWeight: '600', 61 | maxWidth: '80%', 62 | }, 63 | }) 64 | -------------------------------------------------------------------------------- /src/components/PlaylistsList.tsx: -------------------------------------------------------------------------------- 1 | import { PlaylistListItem } from '@/components/PlaylistListItem' 2 | import { unknownTrackImageUri } from '@/constants/images' 3 | import myTrackPlayer from '@/helpers/trackPlayerIndex' 4 | import { Playlist } from '@/helpers/types' 5 | import { useNavigationSearch } from '@/hooks/useNavigationSearch' 6 | import { utilsStyles } from '@/styles' 7 | import i18n from '@/utils/i18n' 8 | import { useMemo } from 'react' 9 | import { Alert, FlatList, FlatListProps, Text, View } from 'react-native' 10 | import FastImage from 'react-native-fast-image' 11 | type PlaylistsListProps = { 12 | playlists: Playlist[] 13 | onPlaylistPress: (playlist: Playlist) => void 14 | } & Partial> 15 | 16 | const ItemDivider = () => ( 17 | 18 | ) 19 | 20 | export const PlaylistsList = ({ 21 | playlists, 22 | onPlaylistPress: handlePlaylistPress, 23 | ...flatListProps 24 | }: PlaylistsListProps) => { 25 | const search = useNavigationSearch({ 26 | searchBarOptions: { 27 | placeholder: i18n.t('find.inPlaylist'), 28 | cancelButtonText: i18n.t('find.cancel'), 29 | }, 30 | }) 31 | 32 | const filteredPlaylist = useMemo(() => { 33 | return playlists 34 | }, [playlists, search]) 35 | 36 | const showDeleteAlert = (playlist: Playlist) => { 37 | Alert.alert('删除歌单', `确定要删除这个歌单吗 "${playlist.name}"?`, [ 38 | { text: '取消', style: 'cancel' }, 39 | { 40 | text: '删除', 41 | style: 'destructive', 42 | onPress: async () => { 43 | try { 44 | const result = await myTrackPlayer.deletePlayLists(playlist.id) 45 | if (result === 'success') { 46 | // 删除成功 47 | // 可以在这里添加一些成功的反馈,比如显示一个成功的提示 48 | Alert.alert('成功', '歌单删除成功') 49 | } else { 50 | // 删除失败,显示错误信息 51 | Alert.alert('错误', result) 52 | } 53 | } catch (error) { 54 | // 处理可能发生的错误 55 | Alert.alert('错误', 'An error occurred while deleting the playlist') 56 | } 57 | }, 58 | }, 59 | ]) 60 | } 61 | return ( 62 | 68 | No playlist found 69 | 70 | 74 | 75 | } 76 | data={playlists} 77 | renderItem={({ item: playlist }) => ( 78 | handlePlaylistPress(playlist)} 81 | onLongPress={() => showDeleteAlert(playlist)} 82 | /> 83 | )} 84 | {...flatListProps} 85 | /> 86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /src/components/PlaylistsListModal.tsx: -------------------------------------------------------------------------------- 1 | import { PlaylistListItem } from '@/components/PlaylistListItem' 2 | import { unknownTrackImageUri } from '@/constants/images' 3 | import { playListsStore } from '@/helpers/trackPlayerIndex' 4 | import { Playlist } from '@/helpers/types' 5 | import { useNavigationSearch } from '@/hooks/useNavigationSearch' 6 | import { utilsStyles } from '@/styles' 7 | import i18n from '@/utils/i18n' 8 | import { useMemo } from 'react' 9 | import { FlatList, FlatListProps, Text, View } from 'react-native' 10 | import FastImage from 'react-native-fast-image' 11 | type PlaylistsListProps = { 12 | onPlaylistPress: (playlist: IMusic.PlayList) => void 13 | } & Partial> 14 | 15 | const ItemDivider = () => ( 16 | 17 | ) 18 | 19 | export const PlaylistsListModal = ({ 20 | onPlaylistPress: handlePlaylistPress, 21 | ...flatListProps 22 | }: PlaylistsListProps) => { 23 | const search = useNavigationSearch({ 24 | searchBarOptions: { 25 | placeholder: i18n.t('find.inPlaylist'), 26 | cancelButtonText: i18n.t('find.cancel'), 27 | }, 28 | }) 29 | const favoritePlayListItem = useMemo( 30 | () => ({ 31 | name: 'Favorites', 32 | id: 'favorites', 33 | tracks: [], 34 | title: '喜欢的歌曲', 35 | coverImg: 'https://y.qq.com/mediastyle/global/img/cover_like.png?max_age=2592000', 36 | description: '喜欢的歌曲', 37 | }), 38 | [], 39 | ) 40 | const storedPlayLists = playListsStore.useValue() || [] 41 | const filteredPlayLists = useMemo(() => { 42 | const playLists = [favoritePlayListItem, ...storedPlayLists] 43 | 44 | if (!search) return playLists 45 | 46 | return playLists.filter((playlist: Playlist) => 47 | playlist.name.toLowerCase().includes(search.toLowerCase()), 48 | ) 49 | }, [search, favoritePlayListItem, storedPlayLists]) 50 | return ( 51 | 57 | No playlist found 58 | 59 | 63 | 64 | } 65 | data={filteredPlayLists} 66 | renderItem={({ item: playlist }) => ( 67 | handlePlaylistPress(playlist as IMusic.PlayList)} 70 | /> 71 | )} 72 | {...flatListProps} 73 | /> 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /src/components/RadioList.tsx: -------------------------------------------------------------------------------- 1 | import { RadioListItem } from '@/components/RadioListItem' 2 | import { unknownTrackImageUri } from '@/constants/images' 3 | import { Playlist } from '@/helpers/types' 4 | import { useNavigationSearch } from '@/hooks/useNavigationSearch' 5 | import { utilsStyles } from '@/styles' 6 | import i18n from '@/utils/i18n' 7 | import { useMemo } from 'react' 8 | import { FlatList, FlatListProps, Text, View } from 'react-native' 9 | import FastImage from 'react-native-fast-image' 10 | type PlaylistsListProps = { 11 | playlists: Playlist[] 12 | onPlaylistPress: (playlist: Playlist) => void 13 | } & Partial> 14 | 15 | const ItemDivider = () => ( 16 | 17 | ) 18 | 19 | export const RadioList = ({ 20 | playlists, 21 | onPlaylistPress: handlePlaylistPress, 22 | ...flatListProps 23 | }: PlaylistsListProps) => { 24 | const search = useNavigationSearch({ 25 | searchBarOptions: { 26 | placeholder: i18n.t('find.inPlaylist'), 27 | cancelButtonText: i18n.t('find.cancel'), 28 | }, 29 | }) 30 | 31 | const filteredPlaylist = useMemo(() => { 32 | return playlists 33 | }, [playlists, search]) 34 | 35 | return ( 36 | 42 | No playlist found 43 | 44 | 48 | 49 | } 50 | data={playlists} 51 | renderItem={({ item: playlist }) => ( 52 | handlePlaylistPress(playlist)} /> 53 | )} 54 | {...flatListProps} 55 | /> 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/components/RadioListItem.tsx: -------------------------------------------------------------------------------- 1 | import { colors } from '@/constants/tokens' 2 | import { Playlist } from '@/helpers/types' 3 | import { defaultStyles } from '@/styles' 4 | import { AntDesign } from '@expo/vector-icons' 5 | import { StyleSheet, Text, TouchableHighlight, TouchableHighlightProps, View } from 'react-native' 6 | import FastImage from 'react-native-fast-image' 7 | 8 | type PlaylistListItemProps = { 9 | playlist: Playlist 10 | } & TouchableHighlightProps 11 | 12 | export const RadioListItem = ({ playlist, ...props }: PlaylistListItemProps) => { 13 | return ( 14 | 15 | 16 | 17 | 24 | 25 | 26 | 34 | 35 | {playlist.title} 36 | 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | const styles = StyleSheet.create({ 46 | playlistItemContainer: { 47 | flexDirection: 'row', 48 | columnGap: 14, 49 | alignItems: 'center', 50 | paddingRight: 90, 51 | }, 52 | playlistArtworkImage: { 53 | borderRadius: 8, 54 | width: 70, 55 | height: 70, 56 | }, 57 | playlistNameText: { 58 | ...defaultStyles.text, 59 | fontSize: 17, 60 | fontWeight: '600', 61 | maxWidth: '80%', 62 | }, 63 | }) 64 | -------------------------------------------------------------------------------- /src/components/ShowPlayerListToggle.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Modal, View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native'; 3 | import { colors } from '@/constants/tokens'; 4 | import { MaterialCommunityIcons } from '@expo/vector-icons'; 5 | import { ComponentProps } from 'react'; 6 | import { useLibraryStore } from '@/store/library' 7 | import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context' 8 | import { router } from 'expo-router' 9 | 10 | type IconProps = Omit, 'name'>; 11 | 12 | export const ShowPlayerListToggle = ({ ...iconProps }: IconProps) => { 13 | const [modalVisible, setModalVisible] = useState(false); 14 | const nowPlaylist = useLibraryStore((state) => state.tracks); // 获取播放列表数据 15 | 16 | const showPlayList = () => { 17 | 18 | router.navigate('/(modals)/playList') 19 | 20 | }; 21 | 22 | const hidePlayList = () => { 23 | setModalVisible(false); 24 | }; 25 | 26 | return ( 27 | <> 28 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | const styles = StyleSheet.create({ 41 | modalOverlay: { 42 | flex: 1, 43 | backgroundColor: 'transparent', 44 | justifyContent: 'center', 45 | alignItems: 'center', 46 | borderRadius: 12, 47 | paddingVertical: 10, 48 | padding: 10, 49 | marginTop: 20, 50 | opacity: 0.75, 51 | }, 52 | modalContent: { 53 | width: '90%', 54 | 55 | backgroundColor: 'white', 56 | borderRadius: 10, 57 | padding:50, 58 | alignItems: 'center', 59 | }, 60 | modalTitle: { 61 | fontSize: 18, 62 | fontWeight: 'bold', 63 | marginBottom: 10, 64 | }, 65 | listItem: { 66 | padding: 10, 67 | borderBottomWidth: 1, 68 | borderBottomColor: '#ccc', 69 | }, 70 | trackTitle: { 71 | fontSize: 16, 72 | }, 73 | trackArtist: { 74 | fontSize: 14, 75 | color: 'gray', 76 | }, 77 | closeButton: { 78 | marginTop: 20, 79 | padding: 10, 80 | backgroundColor: colors.primary, 81 | borderRadius: 5, 82 | }, 83 | closeButtonText: { 84 | color: 'black', 85 | fontWeight: 'bold', 86 | }, 87 | }); 88 | -------------------------------------------------------------------------------- /src/components/lyric/lyricItem.tsx: -------------------------------------------------------------------------------- 1 | import { colors } from '@/constants/tokens' 2 | import rpx from '@/utils/rpx' 3 | import * as Haptics from 'expo-haptics' 4 | import React, { memo, useCallback, useRef } from 'react' 5 | import { Animated, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native' 6 | interface ILyricItemComponentProps { 7 | // 行号 8 | index?: number 9 | // 显示 10 | light?: boolean 11 | // 高亮 12 | highlight?: boolean 13 | // 文本 14 | text?: string 15 | // 字体大小 16 | fontSize?: number 17 | onPress: () => Promise 18 | onLayout?: (index: number, height: number) => void 19 | } 20 | 21 | function _LyricItemComponent(props: ILyricItemComponentProps) { 22 | const { light, highlight, text, onLayout, index, fontSize, onPress } = props 23 | const animatedOpacity = useRef(new Animated.Value(0)).current 24 | 25 | const handlePress = useCallback(() => { 26 | // 触发震动 27 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) 28 | 29 | // 显示背景 30 | Animated.sequence([ 31 | Animated.timing(animatedOpacity, { 32 | toValue: 1, 33 | duration: 600, 34 | useNativeDriver: true, 35 | }), 36 | Animated.timing(animatedOpacity, { 37 | toValue: 0, 38 | duration: 200, 39 | useNativeDriver: true, 40 | }), 41 | ]).start() 42 | 43 | // 调用原来的 onPress 44 | onPress() 45 | }, [onPress, animatedOpacity]) 46 | 47 | return ( 48 | 49 | 50 | 58 | { 60 | if (index !== undefined) { 61 | onLayout?.(index, nativeEvent.layout.height) 62 | } 63 | }} 64 | style={[ 65 | lyricStyles.item, 66 | { 67 | fontSize: fontSize || rpx(28), 68 | }, 69 | highlight 70 | ? [ 71 | lyricStyles.highlightItem, 72 | { 73 | color: colors.primary, 74 | }, 75 | ] 76 | : null, 77 | // light ? lyricStyles.draggingItem : null, 78 | ]} 79 | > 80 | {text} 81 | 82 | 83 | 84 | ) 85 | } 86 | // 歌词 87 | const LyricItemComponent = memo( 88 | _LyricItemComponent, 89 | (prev, curr) => 90 | prev.light === curr.light && 91 | prev.highlight === curr.highlight && 92 | prev.text === curr.text && 93 | prev.index === curr.index && 94 | prev.fontSize === curr.fontSize, 95 | ) 96 | 97 | export default LyricItemComponent 98 | 99 | const lyricStyles = StyleSheet.create({ 100 | highlightItem: { 101 | opacity: 1, 102 | }, 103 | item: { 104 | color: 'white', 105 | opacity: 0.6, 106 | paddingHorizontal: rpx(64), 107 | paddingVertical: rpx(24), 108 | width: '100%', 109 | textAlign: 'center', 110 | textAlignVertical: 'center', 111 | }, 112 | draggingItem: { 113 | opacity: 1, 114 | color: 'white', 115 | }, 116 | background: { 117 | position: 'absolute', 118 | top: 0, 119 | left: 0, 120 | right: 0, 121 | bottom: 0, 122 | backgroundColor: 'rgba(255, 255, 255, 0.2)', 123 | borderRadius: rpx(10), 124 | }, 125 | }) 126 | -------------------------------------------------------------------------------- /src/components/utils/StopPropagation.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react' 2 | import { View } from 'react-native' 3 | 4 | export const StopPropagation = ({ children }: PropsWithChildren) => { 5 | return ( 6 | true} 8 | onTouchEnd={(e) => e.stopPropagation()} 9 | style={{ 10 | flex: 1, 11 | flexDirection: 'row', 12 | alignItems: 'flex-end', 13 | justifyContent: 'center', 14 | }} 15 | > 16 | {children} 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/utils/message.ts: -------------------------------------------------------------------------------- 1 | export const requestMsg = { 2 | fail: '请求异常😮,可以多试几次,若还是不行就换一首吧。。。', 3 | unachievable: '哦No😱...接口无法访问了!', 4 | timeout: '请求超时', 5 | // unachievable: '哦No😱...接口无法访问了!已帮你切换到临时接口,重试下看能不能播放吧~', 6 | notConnectNetwork: '无法连接到服务器', 7 | cancelRequest: '取消http请求', 8 | tooManyRequests: '服务器繁忙', 9 | } as const 10 | -------------------------------------------------------------------------------- /src/components/utils/musicSdk/api-source-info.ts: -------------------------------------------------------------------------------- 1 | // Support qualitys: 128k 320k flac wav 2 | 3 | const sources: Array<{ 4 | id: string 5 | name: string 6 | disabled: boolean 7 | supportQualitys: Partial> 8 | }> = [ 9 | ] 10 | 11 | export default sources 12 | -------------------------------------------------------------------------------- /src/components/utils/musicSdk/api-source.js: -------------------------------------------------------------------------------- 1 | import apiSourceInfo from './api-source-info' 2 | 3 | // import temp_api_kw from './kw/api-temp' 4 | // import test_api_kg from './kg/api-test' 5 | // import test_api_kw from './kw/api-test' 6 | // import test_api_tx from './tx/api-test' 7 | // import test_api_wy from './wy/api-test' 8 | // import test_api_mg from './mg/api-test' 9 | 10 | // import direct_api_kg from './kg/api-direct' 11 | // import direct_api_kw from './kw/api-direct' 12 | // import direct_api_tx from './tx/api-direct' 13 | // import direct_api_wy from './wy/api-direct' 14 | // import direct_api_mg from './mg/api-direct' 15 | 16 | 17 | 18 | const apiList = { 19 | // temp_api_kw, 20 | // // test_api_bd: require('./bd/api-test'), 21 | // test_api_kg, 22 | // test_api_kw, 23 | // test_api_tx, 24 | // test_api_wy, 25 | // test_api_mg, 26 | // direct_api_kg, 27 | // direct_api_kw, 28 | // direct_api_tx, 29 | // direct_api_wy, 30 | // direct_api_mg, 31 | // test_api_tx: require('./tx/api-test'), 32 | // test_api_wy: require('./wy/api-test'), 33 | // test_api_xm: require('./xm/api-test'), 34 | } 35 | const supportQuality = {} 36 | 37 | for (const api of apiSourceInfo) { 38 | supportQuality[api.id] = api.supportQualitys 39 | // for (const source of Object.keys(api.supportQualitys)) { 40 | // const path = `./${source}/api-${api.id}` 41 | // console.log(path) 42 | // apiList[`${api.id}_api_${source}`] = path 43 | // } 44 | } 45 | 46 | // const getAPI = source => apiList[`${settingState.setting['common.apiSource']}_api_${source}`] 47 | 48 | const apis = source => { 49 | // if (/^user_api/.test(settingState.setting['common.apiSource'])) return global.lx.apis[source] 50 | // const api = getAPI(source) 51 | // if (api) return api 52 | // throw new Error('Api is not found') 53 | return global.lx.apis[source] 54 | } 55 | 56 | export { apis, supportQuality } 57 | -------------------------------------------------------------------------------- /src/components/utils/musicSdk/options.js: -------------------------------------------------------------------------------- 1 | export const bHh = '624868746c' 2 | 3 | export const headers = { 4 | 'User-Agent': 'lx-music mobile request', 5 | [bHh]: [bHh], 6 | } 7 | 8 | export const timeout = 4000 9 | -------------------------------------------------------------------------------- /src/components/utils/musicSdk/tx/api-ikun.js: -------------------------------------------------------------------------------- 1 | import { requestMsg } from '../../message' 2 | import { httpFetch } from '../../request' 3 | import { headers, timeout } from '../options' 4 | import { getMediaSource } from '@/helpers/userApi/xiaoqiu' 5 | 6 | const api_ikun = { 7 | getMusicUrl(songInfo, type) { 8 | console.log('ikun>????s') 9 | const requestObj = httpFetch(`http://110.42.111.49:1314/url/tx/${songInfo.id}/${type}`, { 10 | method: 'get', 11 | timeout, 12 | headers, 13 | family: 4, 14 | }) 15 | requestObj.promise = requestObj.promise.then(async ({ body }) => { 16 | // console.log(body.data) 17 | if (!body.data||(typeof body.data === 'string' && body.data.includes('error') )) 18 | { 19 | const resp = await getMediaSource(songInfo, '128k') 20 | console.log('获取成功:' + resp) 21 | return Promise.resolve({ type, url: resp.url }) 22 | } 23 | console.log('获取成功:' + body.data) 24 | return body.code === 0 25 | ? Promise.resolve({ type, url: body.data }) 26 | : Promise.reject(new Error(requestMsg.fail)) 27 | }) 28 | 29 | return requestObj.promise 30 | }, 31 | getPic(songInfo) { 32 | return { 33 | promise: Promise.resolve( 34 | `https://y.gtimg.cn/music/photo_new/T002R500x500M000${songInfo.albumId}.jpg`, 35 | ), 36 | } 37 | }, 38 | } 39 | 40 | export default api_ikun 41 | -------------------------------------------------------------------------------- /src/components/utils/musicSdk/tx/hotSearch.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | 3 | export default { 4 | _requestObj: null, 5 | async getList(retryNum = 0) { 6 | if (this._requestObj) this._requestObj.cancelHttp() 7 | if (retryNum > 2) return Promise.reject(new Error('try max num')) 8 | 9 | // const _requestObj = httpFetch('https://c.y.qq.com/splcloud/fcgi-bin/gethotkey.fcg', { 10 | // method: 'get', 11 | // headers: { 12 | // Referer: 'https://y.qq.com/portal/player.html', 13 | // }, 14 | // }) 15 | const _requestObj = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { 16 | method: 'post', 17 | body: { 18 | comm: { 19 | ct: '19', 20 | cv: '1803', 21 | guid: '0', 22 | patch: '118', 23 | psrf_access_token_expiresAt: 0, 24 | psrf_qqaccess_token: '', 25 | psrf_qqopenid: '', 26 | psrf_qqunionid: '', 27 | tmeAppID: 'qqmusic', 28 | tmeLoginType: 0, 29 | uin: '0', 30 | wid: '0', 31 | }, 32 | hotkey: { 33 | method: 'GetHotkeyForQQMusicPC', 34 | module: 'tencent_musicsoso_hotkey.HotkeyService', 35 | param: { 36 | search_id: '', 37 | uin: 0, 38 | }, 39 | }, 40 | }, 41 | headers: { 42 | Referer: 'https://y.qq.com/portal/player.html', 43 | }, 44 | }) 45 | const { body, statusCode } = await _requestObj.promise 46 | // console.log(body) 47 | if (statusCode != 200 || body.code !== 0) throw new Error('获取热搜词失败') 48 | // console.log(body) 49 | return { source: 'tx', list: this.filterList(body.hotkey.data.vec_hotkey) } 50 | }, 51 | filterList(rawList) { 52 | return rawList.map(item => item.query) 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /src/components/utils/musicSdk/tx/index.js: -------------------------------------------------------------------------------- 1 | import leaderboard from './leaderboard' 2 | import lyric from './lyric' 3 | import songList from './songList' 4 | import musicSearch from './musicSearch' 5 | import { apis } from '../api-source' 6 | import hotSearch from './hotSearch' 7 | import comment from './comment' 8 | // import tipSearch from './tipSearch' 9 | 10 | const tx = { 11 | // tipSearch, 12 | leaderboard, 13 | songList, 14 | musicSearch, 15 | hotSearch, 16 | comment, 17 | 18 | getMusicUrl(songInfo, type) { 19 | return apis('tx').getMusicUrl(songInfo, type) 20 | }, 21 | getLyric(songInfo) { 22 | // let singer = songInfo.singer.indexOf('、') > -1 ? songInfo.singer.split('、')[0] : songInfo.singer 23 | return lyric.getLyric(songInfo.songmid) 24 | }, 25 | getPic(songInfo) { 26 | return apis('tx').getPic(songInfo) 27 | }, 28 | getMusicDetailPageUrl(songInfo) { 29 | return `https://y.qq.com/n/yqq/song/${songInfo.songmid}.html` 30 | }, 31 | } 32 | 33 | export default tx 34 | -------------------------------------------------------------------------------- /src/components/utils/musicSdk/tx/lyric.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { b64DecodeUnicode, decodeName } from '../../index' 3 | 4 | export default { 5 | regexps: { 6 | matchLrc: /.+"lyric":"([\w=+/]*)".+/, 7 | }, 8 | getLyric(songmid) { 9 | const requestObj = httpFetch(`https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg?songmid=${songmid}&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&platform=yqq`, { 10 | headers: { 11 | Referer: 'https://y.qq.com/portal/player.html', 12 | }, 13 | }) 14 | requestObj.promise = requestObj.promise.then(({ body }) => { 15 | if (body.code != 0 || !body.lyric) return Promise.reject(new Error('Get lyric failed')) 16 | return { 17 | lyric: decodeName(b64DecodeUnicode(body.lyric)), 18 | tlyric: decodeName(b64DecodeUnicode(body.trans)), 19 | } 20 | }) 21 | return requestObj 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /src/components/utils/musicSdk/tx/musicInfo.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { formatPlayTime, sizeFormate } from '../../index' 3 | 4 | const getSinger = (singers) => { 5 | let arr = [] 6 | singers.forEach(singer => { 7 | arr.push(singer.name) 8 | }) 9 | return arr.join('、') 10 | } 11 | 12 | export default (songmid) => { 13 | const requestObj = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { 14 | method: 'post', 15 | headers: { 16 | 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', 17 | }, 18 | body: { 19 | comm: { 20 | ct: '19', 21 | cv: '1859', 22 | uin: '0', 23 | }, 24 | req: { 25 | module: 'music.pf_song_detail_svr', 26 | method: 'get_song_detail_yqq', 27 | param: { 28 | song_type: 0, 29 | song_mid: songmid, 30 | }, 31 | }, 32 | }, 33 | }) 34 | return requestObj.promise.then(({ body }) => { 35 | // console.log(body) 36 | if (body.code != 0 || body.req.code != 0) return Promise.reject(new Error('获取歌曲信息失败')) 37 | const item = body.req.data.track_info 38 | if (!item.file?.media_mid) return null 39 | 40 | let types = [] 41 | let _types = {} 42 | const file = item.file 43 | if (file.size_128mp3 != 0) { 44 | let size = sizeFormate(file.size_128mp3) 45 | types.push({ type: '128k', size }) 46 | _types['128k'] = { 47 | size, 48 | } 49 | } 50 | if (file.size_320mp3 !== 0) { 51 | let size = sizeFormate(file.size_320mp3) 52 | types.push({ type: '320k', size }) 53 | _types['320k'] = { 54 | size, 55 | } 56 | } 57 | if (file.size_flac !== 0) { 58 | let size = sizeFormate(file.size_flac) 59 | types.push({ type: 'flac', size }) 60 | _types.flac = { 61 | size, 62 | } 63 | } 64 | if (file.size_hires !== 0) { 65 | let size = sizeFormate(file.size_hires) 66 | types.push({ type: 'flac24bit', size }) 67 | _types.flac24bit = { 68 | size, 69 | } 70 | } 71 | // types.reverse() 72 | let albumId = '' 73 | let albumName = '' 74 | if (item.album) { 75 | albumName = item.album.name 76 | albumId = item.album.mid 77 | } 78 | return { 79 | singer: getSinger(item.singer), 80 | name: item.title, 81 | albumName, 82 | albumId, 83 | source: 'tx', 84 | interval: formatPlayTime(item.interval), 85 | songId: item.id, 86 | albumMid: item.album?.mid ?? '', 87 | strMediaMid: item.file.media_mid, 88 | songmid: item.mid, 89 | img: (albumId === '' || albumId === '空') 90 | ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : '' 91 | : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg`, 92 | types, 93 | _types, 94 | typeUrl: {}, 95 | } 96 | }) 97 | } 98 | 99 | -------------------------------------------------------------------------------- /src/components/utils/musicSdk/tx/tipSearch.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | 3 | export default { 4 | // regExps: { 5 | // relWord: /RELWORD=(.+)/, 6 | // }, 7 | requestObj: null, 8 | tipSearch(str) { 9 | this.cancelTipSearch() 10 | this.requestObj = httpFetch(`https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg?is_xml=0&format=json&key=${encodeURIComponent(str)}&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0`, { 11 | headers: { 12 | Referer: 'https://y.qq.com/portal/player.html', 13 | }, 14 | }) 15 | return this.requestObj.promise.then(({ statusCode, body }) => { 16 | if (statusCode != 200 || body.code != 0) return Promise.reject(new Error('请求失败')) 17 | return body.data 18 | }) 19 | }, 20 | handleResult(rawData) { 21 | return rawData.map(info => `${info.name} - ${info.singer}`) 22 | }, 23 | cancelTipSearch() { 24 | if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp() 25 | }, 26 | async search(str) { 27 | return this.tipSearch(str).then(result => this.handleResult(result.song.itemlist)) 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /src/components/utils/musicSdk/utils.js: -------------------------------------------------------------------------------- 1 | import { stringMd5 } from 'react-native-quick-md5' 2 | import { decodeName } from '../common' 3 | 4 | /** 5 | * 获取音乐音质 6 | * @param {*} info 7 | * @param {*} type 8 | */ 9 | 10 | export const QUALITYS = ['flac24bit', 'flac', 'wav', 'ape', '320k', '192k', '128k'] 11 | export const getMusicType = (info, type) => { 12 | const list = global.lx.qualityList[info.source] 13 | if (!list) return '128k' 14 | if (!list.includes(type)) type = list[list.length - 1] 15 | const rangeType = QUALITYS.slice(QUALITYS.indexOf(type)) 16 | for (const type of rangeType) { 17 | if (info._types[type]) return type 18 | } 19 | return '128k' 20 | } 21 | 22 | export const toMD5 = str => stringMd5(str) 23 | 24 | 25 | /** 26 | * 格式化歌手 27 | * @param singers 歌手数组 28 | * @param nameKey 歌手名键值 29 | * @param join 歌手分割字符 30 | */ 31 | export const formatSingerName = (singers, nameKey = 'name', join = '、') => { 32 | if (Array.isArray(singers)) { 33 | const singer = [] 34 | singers.forEach(item => { 35 | let name = item[nameKey] 36 | if (!name) return 37 | singer.push(name) 38 | }) 39 | return decodeName(singer.join(join)) 40 | } 41 | return decodeName(String(singers ?? '')) 42 | } 43 | -------------------------------------------------------------------------------- /src/components/utils/musicSdk/xm.js: -------------------------------------------------------------------------------- 1 | // import { apis } from '../api-source' 2 | // import leaderboard from './leaderboard' 3 | // import songList from './songList' 4 | // import musicSearch from './musicSearch' 5 | // import pic from './pic' 6 | // import lyric from './lyric' 7 | // import hotSearch from './hotSearch' 8 | // import comment from './comment' 9 | // import musicInfo from './musicInfo' 10 | // import { closeVerifyModal } from './util' 11 | 12 | const xm = { 13 | // songList, 14 | // musicSearch, 15 | // leaderboard, 16 | // hotSearch, 17 | // closeVerifyModal, 18 | comment: { 19 | getComment() { 20 | return Promise.reject(new Error('fail')) 21 | }, 22 | getHotComment() { 23 | return Promise.reject(new Error('fail')) 24 | }, 25 | }, 26 | getMusicUrl(songInfo, type) { 27 | return { 28 | promise: Promise.reject(new Error('fail')), 29 | } 30 | // return apis('xm').getMusicUrl(songInfo, type) 31 | }, 32 | getLyric(songInfo) { 33 | return { 34 | promise: Promise.reject(new Error('fail')), 35 | } 36 | // return lyric.getLyric(songInfo) 37 | }, 38 | getPic(songInfo) { 39 | return Promise.reject(new Error('fail')) 40 | // return pic.getPic(songInfo) 41 | }, 42 | // getMusicDetailPageUrl(songInfo) { 43 | // if (songInfo.songStringId) return `https://www.xiami.com/song/${songInfo.songStringId}` 44 | 45 | // musicInfo.getMusicInfo(songInfo).then(({ data }) => { 46 | // songInfo.songStringId = data.songStringId 47 | // }) 48 | // return `https://www.xiami.com/song/${songInfo.songmid}` 49 | // }, 50 | // init() { 51 | // getToken() 52 | // }, 53 | } 54 | 55 | export default xm 56 | -------------------------------------------------------------------------------- /src/components/utils/nativeModules/cache.ts: -------------------------------------------------------------------------------- 1 | import { NativeModules } from 'react-native' 2 | 3 | const { CacheModule } = NativeModules 4 | 5 | export const getAppCacheSize = async(): Promise => CacheModule.getAppCacheSize().then((size: number) => Math.trunc(size)) 6 | export const clearAppCache = CacheModule.clearAppCache as () => Promise 7 | -------------------------------------------------------------------------------- /src/components/utils/nativeModules/userApi.ts: -------------------------------------------------------------------------------- 1 | import { NativeEventEmitter, NativeModules } from 'react-native' 2 | 3 | const { UserApiModule } = NativeModules 4 | 5 | let loadScriptInfo: LX.UserApi.UserApiInfo | null = null 6 | export const loadScript = (info: LX.UserApi.UserApiInfo & { script: string }) => { 7 | loadScriptInfo = info 8 | UserApiModule.loadScript({ 9 | id: info.id, 10 | name: info.name, 11 | description: info.description, 12 | version: info.version ?? '', 13 | author: info.author ?? '', 14 | homepage: info.homepage ?? '', 15 | script: info.script, 16 | }) 17 | } 18 | 19 | export interface SendResponseParams { 20 | requestKey: string 21 | error: string | null 22 | response: { 23 | statusCode: number 24 | statusMessage: string 25 | headers: Record 26 | body: any 27 | } | null 28 | } 29 | export interface SendActions { 30 | request: LX.UserApi.UserApiRequestParams 31 | response: SendResponseParams 32 | } 33 | export const sendAction = (action: T, data: SendActions[T]) => { 34 | UserApiModule.sendAction(action, JSON.stringify(data)) 35 | } 36 | 37 | // export const clearAppCache = CacheModule.clearAppCache as () => Promise 38 | 39 | export interface InitParams { 40 | status: boolean 41 | errorMessage: string 42 | info: LX.UserApi.UserApiInfo 43 | } 44 | 45 | export interface ResponseParams { 46 | status: boolean 47 | errorMessage?: string 48 | requestKey: string 49 | result: any 50 | } 51 | export interface UpdateInfoParams { 52 | name: string 53 | log: string 54 | updateUrl: string 55 | } 56 | export interface RequestParams { 57 | requestKey: string 58 | url: string 59 | options: { 60 | method: string 61 | data: any 62 | timeout: number 63 | headers: any 64 | binary: boolean 65 | } 66 | } 67 | export type CancelRequestParams = string 68 | 69 | export interface Actions { 70 | init: InitParams 71 | request: RequestParams 72 | cancelRequest: CancelRequestParams 73 | response: ResponseParams 74 | showUpdateAlert: UpdateInfoParams 75 | log: string 76 | } 77 | export type ActionsEvent = { [K in keyof Actions]: { action: K, data: Actions[K] } }[keyof Actions] 78 | 79 | export const onScriptAction = (handler: (event: ActionsEvent) => void): () => void => { 80 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 81 | const eventEmitter = new NativeEventEmitter(UserApiModule) 82 | const eventListener = eventEmitter.addListener('api-action', event => { 83 | if (event.data) event.data = JSON.parse(event.data as string) 84 | if (event.action == 'init') { 85 | if (event.data.info) event.data.info = { ...loadScriptInfo, ...event.data.info } 86 | else event.data.info = { ...loadScriptInfo } 87 | } else if (event.action == 'showUpdateAlert') { 88 | if (!loadScriptInfo?.allowShowUpdateAlert) return 89 | } 90 | handler(event as ActionsEvent) 91 | }) 92 | 93 | return () => { 94 | eventListener.remove() 95 | } 96 | } 97 | 98 | export const destroy = () => { 99 | UserApiModule.destroy() 100 | } 101 | -------------------------------------------------------------------------------- /src/constants/commonConst.ts: -------------------------------------------------------------------------------- 1 | import Animated, {Easing} from 'react-native-reanimated'; 2 | 3 | export const internalSymbolKey = Symbol.for('$'); 4 | // 加入播放列表/歌单的时间 5 | export const timeStampSymbol = Symbol.for('time-stamp'); 6 | // 加入播放列表的辅助顺序 7 | export const sortIndexSymbol = Symbol.for('sort-index'); 8 | export const internalSerializeKey = '$'; 9 | export const localMusicSheetId = 'local-music-sheet'; 10 | export const musicHistorySheetId = 'history-music-sheet'; 11 | 12 | export const localPluginPlatform = '本地'; 13 | export const localPluginHash = 'local-plugin-hash'; 14 | 15 | export const internalFakeSoundKey = 'fake-key'; 16 | 17 | const emptyFunction = () => {}; 18 | Object.freeze(emptyFunction); 19 | export {emptyFunction}; 20 | 21 | export enum RequestStateCode { 22 | /** 空闲 */ 23 | IDLE = 0b00000000, 24 | PENDING_FIRST_PAGE = 0b00000010, 25 | LOADING = 0b00000010, 26 | /** 检索中 */ 27 | PENDING_REST_PAGE = 0b00000011, 28 | /** 部分结束 */ 29 | PARTLY_DONE = 0b00000100, 30 | /** 全部结束 */ 31 | FINISHED = 0b0001000, 32 | /** 出错了 */ 33 | ERROR = 0b10000000, 34 | } 35 | 36 | export const StorageKeys = { 37 | /** @deprecated */ 38 | MediaMetaKeys: 'media-meta-keys', 39 | PluginMetaKey: 'plugin-meta', 40 | MediaCache: 'media-cache', 41 | LocalMusicSheet: 'local-music-sheet', 42 | }; 43 | 44 | export const CacheControl = { 45 | Cache: 'cache', 46 | NoCache: 'no-cache', 47 | NoStore: 'no-store', 48 | }; 49 | 50 | export const supportLocalMediaType = [ 51 | '.mp3', 52 | '.flac', 53 | '.wma', 54 | '.wav', 55 | '.m4a', 56 | '.ogg', 57 | '.acc', 58 | '.aac', 59 | '.ape', 60 | '.opus', 61 | ]; 62 | 63 | /** 全局事件 */ 64 | export enum EDeviceEvents { 65 | /** 刷新歌词 */ 66 | REFRESH_LYRIC = 'refresh-lyric', 67 | } 68 | 69 | const ANIMATION_EASING: Animated.EasingFunction = Easing.out(Easing.exp); 70 | const ANIMATION_DURATION = 150; 71 | 72 | const animationFast = { 73 | duration: ANIMATION_DURATION, 74 | easing: ANIMATION_EASING, 75 | }; 76 | 77 | const animationNormal = { 78 | duration: 250, 79 | easing: ANIMATION_EASING, 80 | }; 81 | 82 | const animationSlow = { 83 | duration: 500, 84 | easing: ANIMATION_EASING, 85 | }; 86 | 87 | export const timingConfig = { 88 | animationFast, 89 | animationNormal, 90 | animationSlow, 91 | }; 92 | -------------------------------------------------------------------------------- /src/constants/images.ts: -------------------------------------------------------------------------------- 1 | import unknownArtistImage from '@/assets/unknown_artist.png' 2 | import unknownTrackImage from '@/assets/unknown_track.png' 3 | import { Image } from 'react-native' 4 | import { SoundAsset } from '@/constants/constant' 5 | 6 | export const unknownTrackImageUri = Image.resolveAssetSource(unknownTrackImage).uri //在 React Native 中,URI 常用于标识图片、视频、音频等资源。例如,在你提供的代码中,Image.resolveAssetSource 返回的 URI 可以用于设置图片的 source 属性。 7 | export const unknownArtistImageUri = Image.resolveAssetSource(unknownArtistImage).uri 8 | export const fakeAudioMp3Uri=Image.resolveAssetSource(SoundAsset.fakeAudio).uri; 9 | -------------------------------------------------------------------------------- /src/constants/layout.ts: -------------------------------------------------------------------------------- 1 | import { NativeStackNavigationOptions } from '@react-navigation/native-stack' 2 | import { colors } from './tokens' 3 | 4 | export const StackScreenWithSearchBar: NativeStackNavigationOptions = { 5 | headerLargeTitle: true, 6 | headerLargeStyle: { 7 | backgroundColor: colors.background, 8 | }, 9 | headerLargeTitleStyle: { 10 | color: colors.text, 11 | }, 12 | headerTintColor: colors.text, 13 | headerTransparent: true, 14 | headerBlurEffect: 'prominent', 15 | headerShadowVisible: false, 16 | } 17 | -------------------------------------------------------------------------------- /src/constants/playbackService.ts: -------------------------------------------------------------------------------- 1 | import myTrackPlayer from '@/helpers/trackPlayerIndex' 2 | import TrackPlayer, { Event } from 'react-native-track-player' 3 | 4 | export const playbackService = async () => { 5 | TrackPlayer.addEventListener(Event.RemotePlay, () => { 6 | TrackPlayer.play() 7 | }) 8 | 9 | TrackPlayer.addEventListener(Event.RemotePause, () => { 10 | TrackPlayer.pause() 11 | }) 12 | 13 | TrackPlayer.addEventListener(Event.RemoteStop, () => { 14 | TrackPlayer.stop() 15 | }) 16 | 17 | TrackPlayer.addEventListener(Event.RemoteNext, () => { 18 | myTrackPlayer.skipToNext() 19 | }) 20 | 21 | TrackPlayer.addEventListener(Event.RemotePrevious, () => { 22 | myTrackPlayer.skipToPrevious() 23 | }) 24 | 25 | TrackPlayer.addEventListener(Event.RemoteSeek, (event) => { 26 | const position = event.position 27 | TrackPlayer.seekTo(position) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/constants/tokens.ts: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | primary: '#fc3c44', 3 | background: '#000', 4 | text: '#fff', 5 | textMuted: '#9ca3af', 6 | icon: '#fff', 7 | maximumTrackTintColor: 'rgba(255,255,255,0.4)', 8 | minimumTrackTintColor: 'rgba(255,255,255,0.6)', 9 | loading: 'rgba(255,255,255,0.6)', 10 | } 11 | 12 | export const fontSize = { 13 | xs: 12, 14 | sm: 16, 15 | base: 20, 16 | lg: 24, 17 | } 18 | 19 | export const screenPadding = { 20 | horizontal: 24, 21 | } 22 | -------------------------------------------------------------------------------- /src/constants/trackPlayerEventListener.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyc-12/Cymusic/26474d6517ff87044acb3c755ea5c99ca0db625c/src/constants/trackPlayerEventListener.tsx -------------------------------------------------------------------------------- /src/helpers/errors/MusicError.ts: -------------------------------------------------------------------------------- 1 | export class MusicError extends Error { 2 | constructor( 3 | message: string, 4 | public code: string, 5 | public details?: any, 6 | ) { 7 | super(message) 8 | this.name = 'MusicError' 9 | } 10 | } 11 | 12 | export class NetworkError extends MusicError { 13 | constructor(message: string, details?: any) { 14 | super(message, 'NETWORK_ERROR', details) 15 | this.name = 'NetworkError' 16 | } 17 | } 18 | 19 | export class APIError extends MusicError { 20 | constructor(message: string, details?: any) { 21 | super(message, 'API_ERROR', details) 22 | this.name = 'APIError' 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/helpers/filter.ts: -------------------------------------------------------------------------------- 1 | import { Artist, Playlist } from './types' 2 | 3 | export const trackTitleFilter = (title: string) => (track: any) => 4 | track.title?.toLowerCase().includes(title.toLowerCase()) 5 | 6 | export const artistNameFilter = (name: string) => (artist: Artist) => 7 | artist.name.toLowerCase().includes(name.toLowerCase()) 8 | 9 | export const playlistNameFilter = (name: string) => (playlist: Playlist) => 10 | playlist.title.toLowerCase().includes(name.toLowerCase()) 11 | -------------------------------------------------------------------------------- /src/helpers/logger.ts: -------------------------------------------------------------------------------- 1 | import { createStore, useStore } from 'zustand' 2 | 3 | type LogLevel = 'INFO' | 'WARN' | 'ERROR' 4 | 5 | type LogEntry = { 6 | timestamp: string 7 | level: LogLevel 8 | message: string 9 | details?: any[] 10 | } 11 | 12 | interface LoggerState { 13 | logs: LogEntry[] 14 | addLog: (level: LogLevel, ...args: any[]) => void 15 | clearLogs: () => void 16 | } 17 | 18 | const stringifyArg = (arg: any): string => { 19 | if (typeof arg === 'string') return arg 20 | try { 21 | return JSON.stringify(arg, null, 2) 22 | } catch (error) { 23 | return `[Unstringifiable Object]: ${Object.prototype.toString.call(arg)}` 24 | } 25 | } 26 | 27 | export const useLogger = createStore((set) => ({ 28 | logs: [], 29 | addLog: (level, ...args) => 30 | set((state) => { 31 | const message = args.map(stringifyArg).join(' ') 32 | const newLog = { 33 | timestamp: new Date().toISOString(), 34 | level, 35 | message, 36 | details: args.length > 1 ? args : undefined, 37 | } 38 | 39 | // 在控制台打印日志 40 | console.log(`[${newLog.timestamp}] [${level}] ${message}`) 41 | 42 | return { 43 | logs: [ 44 | ...state.logs, 45 | { 46 | timestamp: new Date().toISOString(), 47 | level, 48 | message, 49 | details: args.length > 1 ? args : undefined, 50 | }, 51 | ], 52 | } 53 | }), 54 | clearLogs: () => set({ logs: [] }), 55 | })) 56 | 57 | const createLogFunction = 58 | (level: LogLevel) => 59 | (...args: any[]) => { 60 | useLogger.getState().addLog(level, ...args) 61 | } 62 | 63 | export const logInfo = createLogFunction('INFO') 64 | export const logWarn = createLogFunction('WARN') 65 | export const logError = createLogFunction('ERROR') 66 | 67 | export const useLoggerHook = () => useStore(useLogger) 68 | -------------------------------------------------------------------------------- /src/helpers/miscellaneous.ts: -------------------------------------------------------------------------------- 1 | export const formatSecondsToMinutes = (seconds: number) => { 2 | const minutes = Math.floor(seconds / 60) 3 | const remainingSeconds = Math.floor(seconds % 60) 4 | 5 | const formattedMinutes = String(minutes).padStart(2, '0') 6 | const formattedSeconds = String(remainingSeconds).padStart(2, '0') 7 | 8 | return `${formattedMinutes}:${formattedSeconds}` 9 | } 10 | 11 | export const generateTracksListId = (trackListName: string, search?: string) => { 12 | return `${trackListName}${`-${search}` || ''}` 13 | } 14 | -------------------------------------------------------------------------------- /src/helpers/searchAll.ts: -------------------------------------------------------------------------------- 1 | // helpers/searchAll.ts 2 | 3 | import { searchArtist, searchMusic } from '@/helpers/userApi/xiaoqiu' 4 | import { Track } from 'react-native-track-player' 5 | 6 | const PAGE_SIZE = 20 7 | 8 | type SearchType = 'songs' | 'artists' 9 | 10 | const searchAll = async ( 11 | searchText: string, 12 | page: number = 1, 13 | type: SearchType = 'songs', 14 | ): Promise<{ data: Track[]; hasMore: boolean }> => { 15 | console.log('search text+++', searchText, 'page:', page, 'type:', type) 16 | 17 | let result 18 | if (type === 'songs') { 19 | console.log('search song') 20 | result = await searchMusic(searchText, page, PAGE_SIZE) 21 | } else { 22 | console.log('search artist') 23 | result = await searchArtist(searchText, page) 24 | // console.log('search result', result) 25 | // Transform artist results to Track format 26 | result.data = result.data.map((artist) => ({ 27 | id: artist.id, 28 | title: artist.name, 29 | artist: artist.name, 30 | artwork: artist.avatar, 31 | isArtist: true, 32 | })) as Track[] 33 | } 34 | 35 | const hasMore = result.data.length === PAGE_SIZE 36 | 37 | return { 38 | data: result.data as Track[], 39 | hasMore, 40 | } 41 | } 42 | 43 | export default searchAll 44 | -------------------------------------------------------------------------------- /src/helpers/types.ts: -------------------------------------------------------------------------------- 1 | import { Track } from 'react-native-track-player' 2 | 3 | export type Playlist = { 4 | name: string 5 | tracks: Track[] 6 | artworkPreview: string 7 | singerImg: string 8 | coverImg: string 9 | period: string 10 | title: string 11 | description: string 12 | artwork: string 13 | id: string 14 | /** 平台 */ 15 | platform: string 16 | /** 作者 */ 17 | artist: string 18 | songs: IMusic.IMusicItem[] 19 | } 20 | 21 | export type Artist = { 22 | name: string 23 | tracks: Track[] 24 | singerImg: string 25 | } 26 | 27 | export enum MusicRepeatMode { 28 | /** 随机播放 */ 29 | SHUFFLE = 'SHUFFLE', 30 | /** 列表循环 */ 31 | QUEUE = 'QUEUE', 32 | /** 单曲循环 */ 33 | SINGLE = 'SINGLE', 34 | } 35 | 36 | export type TrackWithPlaylist = Track & { playlist?: string[]; platform?: string } 37 | -------------------------------------------------------------------------------- /src/helpers/userApiHelper.ts: -------------------------------------------------------------------------------- 1 | import { getData } from 'ajv/lib/compile/validate' 2 | import { saveDataMultiple } from '@/helpers/storage' 3 | import { storageDataPrefix } from '@/constants/constant' 4 | 5 | const INFO_NAMES = { 6 | name: 24, 7 | description: 36, 8 | author: 56, 9 | homepage: 1024, 10 | version: 36, 11 | } as const 12 | type INFO_NAMES_Type = typeof INFO_NAMES 13 | 14 | const userApis: LX.UserApi.UserApiInfo[] = [] 15 | const userApiPrefix = storageDataPrefix.userApi 16 | 17 | 18 | export const addUserApi = async(script: string): Promise => { 19 | const result = /^\/\*[\S|\s]+?\*\//.exec(script); 20 | if (!result) throw new Error('user_api_add_failed_tip'); 21 | 22 | const scriptInfo = matchInfo(result[0]); 23 | 24 | scriptInfo.name ||= `user_api_${new Date().toLocaleString()}`; 25 | const apiInfo = { 26 | id: `user_api_${Math.random().toString().substring(2, 5)}_${Date.now()}`, 27 | ...scriptInfo, 28 | script, 29 | allowShowUpdateAlert: true, 30 | }; 31 | userApis.length = 0; 32 | 33 | userApis.push(apiInfo); 34 | await saveDataMultiple([ 35 | [userApiPrefix, userApis], 36 | [`${userApiPrefix}${apiInfo.id}`, script], 37 | ]); 38 | return apiInfo; 39 | }; 40 | 41 | const matchInfo = (scriptInfo: string) => { 42 | const infoArr = scriptInfo.split(/\r?\n/) 43 | const rxp = /^\s?\*\s?@(\w+)\s(.+)$/ 44 | const infos: Partial> = {} 45 | for (const info of infoArr) { 46 | const result = rxp.exec(info) 47 | if (!result) continue 48 | const key = result[1] as keyof typeof INFO_NAMES 49 | if (INFO_NAMES[key] == null) continue 50 | infos[key] = result[2].trim() 51 | } 52 | 53 | for (const [key, len] of Object.entries(INFO_NAMES) as Array<{ [K in keyof INFO_NAMES_Type]: [K, INFO_NAMES_Type[K]] }[keyof INFO_NAMES_Type]>) { 54 | infos[key] ||= '' 55 | if (infos[key] == null) infos[key] = '' 56 | else if (infos[key]!.length > len) infos[key] = infos[key]!.substring(0, len) + '...' 57 | } 58 | 59 | return infos as Record 60 | } -------------------------------------------------------------------------------- /src/hooks/useDelayFalsy.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react' 2 | 3 | export default function useDelayFalsy(init?: T, ms: number = 0) { 4 | const [_state, _setState] = useState(init) 5 | const timer = useRef() 6 | 7 | function setState(st: T) { 8 | if (st === undefined || st === null || st === false) { 9 | timer.current && clearTimeout(timer.current) 10 | timer.current = setTimeout(() => { 11 | _setState(st) 12 | timer.current = undefined 13 | }, ms) 14 | return 15 | } 16 | timer.current && clearTimeout(timer.current) 17 | timer.current = undefined 18 | _setState(st) 19 | } 20 | 21 | return [_state, setState, _setState] as [ 22 | ...ReturnType>, 23 | ReturnType>[1], 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useLastActiveTrack.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Track, useActiveTrack } from 'react-native-track-player' 3 | 4 | export const useLastActiveTrack = () => { 5 | const activeTrack = useActiveTrack() 6 | const [lastActiveTrack, setLastActiveTrack] = useState() 7 | 8 | useEffect(() => { 9 | if (!activeTrack) return 10 | 11 | setLastActiveTrack(activeTrack) 12 | }, [activeTrack]) 13 | 14 | return lastActiveTrack 15 | } 16 | -------------------------------------------------------------------------------- /src/hooks/useLogTrackPlayerState.tsx: -------------------------------------------------------------------------------- 1 | import { Event, useTrackPlayerEvents } from 'react-native-track-player' 2 | 3 | const events = [ 4 | Event.PlaybackState, 5 | Event.PlaybackError, 6 | Event.PlaybackQueueEnded, 7 | Event.PlaybackActiveTrackChanged, 8 | Event.PlaybackPlayWhenReadyChanged, 9 | Event.PlaybackTrackChanged, 10 | Event.PlaybackProgressUpdated, 11 | ] 12 | 13 | export const useLogTrackPlayerState = () => { 14 | useTrackPlayerEvents(events, async (event) => { 15 | if (event.type === Event.PlaybackError) { 16 | console.warn('An error occurred: ', event) 17 | } 18 | 19 | if (event.type === Event.PlaybackState) { 20 | console.log('Playback state: ', event.state) 21 | } else if (event.type === Event.PlaybackQueueEnded) { 22 | console.log(' PlaybackQueueEnded: ', event.track) 23 | } else if (event.type === Event.PlaybackPlayWhenReadyChanged) { 24 | console.log('Ready ?:', event.playWhenReady) 25 | } 26 | // else if (event.type === Event.PlaybackProgressUpdated) { 27 | // const currentPosition = event.position 28 | // const currentLyric = 29 | // LyricManager.getLyricState().lyricParser?.getPosition(currentPosition).lrc 30 | // console.log('PlaybackProgressUpdated: ', currentLyric) 31 | // LyricManager.setCurrentLyric(currentLyric || null) 32 | // // LyricManager.refreshLyric() 33 | // } 34 | else { 35 | // console.log('Track other type:', event.type) 36 | } 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/hooks/useNavigationSearch.tsx: -------------------------------------------------------------------------------- 1 | import { colors } from '@/constants/tokens' 2 | import { nowLanguage } from '@/utils/i18n' 3 | import { useNavigation } from 'expo-router' 4 | import { debounce } from 'lodash' 5 | import { useCallback, useLayoutEffect, useState } from 'react' 6 | import { SearchBarProps } from 'react-native-screens' 7 | 8 | const defaultSearchOptions: SearchBarProps = { 9 | tintColor: colors.primary, 10 | hideWhenScrolling: false, 11 | } 12 | 13 | export const useNavigationSearch = ({ 14 | searchBarOptions, 15 | onFocus, 16 | onBlur, 17 | onCancel, 18 | }: { 19 | searchBarOptions?: SearchBarProps 20 | onFocus?: () => void 21 | onBlur?: () => void 22 | onCancel?: () => void 23 | }) => { 24 | const [search, setSearch] = useState('') 25 | 26 | const navigation = useNavigation() 27 | const language = nowLanguage.useValue() 28 | 29 | const debouncedSetSearch = useCallback( 30 | debounce((text) => { 31 | setSearch(text) 32 | }, 400), 33 | [], 34 | ) 35 | 36 | const handleOnChangeText: SearchBarProps['onChangeText'] = ({ nativeEvent: { text } }) => { 37 | debouncedSetSearch(text) 38 | } 39 | 40 | useLayoutEffect(() => { 41 | navigation.setOptions({ 42 | headerSearchBarOptions: { 43 | ...defaultSearchOptions, 44 | ...searchBarOptions, 45 | onChangeText: handleOnChangeText, 46 | onFocus: onFocus, 47 | onBlur: onBlur, 48 | onCancelButtonPress: (e) => { 49 | onCancel?.() 50 | searchBarOptions?.onCancelButtonPress?.(e) 51 | }, 52 | }, 53 | }) 54 | }, [navigation, searchBarOptions, onFocus, onBlur, onCancel]) 55 | 56 | return search 57 | } 58 | -------------------------------------------------------------------------------- /src/hooks/usePlayerBackground.tsx: -------------------------------------------------------------------------------- 1 | import { colors } from '@/constants/tokens' 2 | import { useEffect, useState } from 'react' 3 | import { getColors } from 'react-native-image-colors' 4 | import { IOSImageColors } from 'react-native-image-colors/build/types' 5 | 6 | export const usePlayerBackground = (imageUrl: string) => { 7 | const [imageColors, setImageColors] = useState(null) 8 | 9 | useEffect(() => { 10 | getColors(imageUrl, { 11 | fallback: colors.background, 12 | cache: true, 13 | key: imageUrl, 14 | }).then((colors) => setImageColors(colors as IOSImageColors)) 15 | }, [imageUrl]) 16 | 17 | return { imageColors } 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/useSetupTrackPlayer.tsx: -------------------------------------------------------------------------------- 1 | import myTrackPlayer from '@/helpers/trackPlayerIndex' 2 | import { useEffect, useRef } from 'react' 3 | import TrackPlayer, { Capability, RatingType, RepeatMode } from 'react-native-track-player' 4 | 5 | const setupPlayer = async () => { 6 | await TrackPlayer.setupPlayer({}) 7 | 8 | await TrackPlayer.updateOptions({ 9 | ratingType: RatingType.Heart, 10 | capabilities: [ 11 | Capability.Play, 12 | Capability.Pause, 13 | Capability.SkipToNext, 14 | Capability.SkipToPrevious, 15 | Capability.Stop, 16 | Capability.SeekTo, 17 | ], 18 | progressUpdateEventInterval: 1, 19 | }) 20 | 21 | await TrackPlayer.setVolume(1) // 默认音量1 22 | await TrackPlayer.setRepeatMode(RepeatMode.Queue) 23 | } 24 | 25 | export const useSetupTrackPlayer = ({ onLoad }: { onLoad?: () => void }) => { 26 | //useSetupTrackPlayer 这个自定义 Hook 用于初始化音乐播放器,并确保它只初始化一次。 27 | const isInitialized = useRef(false) //是一个 React Hook,用于持有可变的对象,这些对象在组件的生命周期内保持不变。使用 useRef 创建一个引用 isInitialized,初始值为 false。它用于跟踪播放器是否已经初始化。 28 | 29 | useEffect(() => { 30 | //是一个 React Hook,用于在函数组件中执行副作用(如数据获取、订阅等)。 31 | if (isInitialized.current) return 32 | 33 | setupPlayer() 34 | .then(async () => { 35 | await myTrackPlayer.setupTrackPlayer() 36 | isInitialized.current = true 37 | onLoad?.() 38 | }) 39 | .catch((error) => { 40 | isInitialized.current = false 41 | console.error(error) 42 | }) 43 | }, [onLoad]) 44 | } 45 | -------------------------------------------------------------------------------- /src/hooks/useTrackPlayerFavorite.tsx: -------------------------------------------------------------------------------- 1 | import { useFavorites } from '@/store/library' 2 | import { useCallback } from 'react' 3 | import TrackPlayer, { useActiveTrack } from 'react-native-track-player' 4 | 5 | export const useTrackPlayerFavorite = () => { 6 | const activeTrack = useActiveTrack() 7 | 8 | const { favorites, toggleTrackFavorite } = useFavorites() 9 | 10 | const isFavorite = favorites.find((track) => track.id === activeTrack?.id)?.id === activeTrack?.id 11 | 12 | // 我们正在更新轨道播放器内部状态和应用程序内部状态 13 | const toggleFavorite = useCallback(async () => { 14 | // const id = await TrackPlayer.getActiveTrackIndex() 15 | // 16 | // if (id == null) return 17 | 18 | // 更新轨道播放器内部状态 19 | // await TrackPlayer.updateMetadataForTrack(id, { 20 | // rating: isFavorite ? 0 : 1, 21 | // }) 22 | 23 | // 更新应用内部状态 24 | if (activeTrack) { 25 | toggleTrackFavorite(activeTrack) 26 | } 27 | }, [isFavorite, toggleTrackFavorite, activeTrack]) 28 | 29 | return { isFavorite, toggleFavorite } 30 | } 31 | -------------------------------------------------------------------------------- /src/hooks/useTrackPlayerRepeatMode.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react' 2 | import TrackPlayer, { RepeatMode } from 'react-native-track-player' 3 | 4 | export const useTrackPlayerRepeatMode = () => { 5 | const [repeatMode, setRepeatMode] = useState() 6 | 7 | const changeRepeatMode = useCallback(async (repeatMode: RepeatMode) => { 8 | await TrackPlayer.setRepeatMode(repeatMode) 9 | 10 | setRepeatMode(repeatMode) 11 | }, []) 12 | 13 | useEffect(() => { 14 | TrackPlayer.getRepeatMode().then(setRepeatMode) 15 | }, []) 16 | 17 | return { repeatMode, changeRepeatMode } 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/useTrackPlayerVolume.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react' 2 | import TrackPlayer from 'react-native-track-player' 3 | 4 | export const useTrackPlayerVolume = () => { 5 | const [volume, setVolume] = useState(1) 6 | 7 | const getVolume = useCallback(async () => { 8 | const currentVolume = await TrackPlayer.getVolume() 9 | setVolume(currentVolume) 10 | }, []) 11 | 12 | const updateVolume = useCallback(async (newVolume: number) => { 13 | if (newVolume < 0 || newVolume > 1) return 14 | 15 | setVolume(newVolume) 16 | 17 | await TrackPlayer.setVolume(newVolume) 18 | }, []) 19 | 20 | useEffect(() => { 21 | TrackPlayer.setVolume(1).then(() => getVolume()) 22 | }, [getVolume]) 23 | 24 | return { volume, updateVolume } 25 | } 26 | -------------------------------------------------------------------------------- /src/store/PersistStatus.ts: -------------------------------------------------------------------------------- 1 | import getOrCreateMMKV from '@/store/getOrCreateMMKV' 2 | import safeParse from '@/utils/safeParse' 3 | import { useEffect, useState } from 'react' 4 | 5 | const PersistConfig = { 6 | PersistStatus: 'appPersistStatus', 7 | } 8 | 9 | interface IPersistConfig { 10 | 'music.musicItem': IMusic.IMusicItem //当前播放 11 | 'music.progress': number 12 | 'music.repeatMode': string 13 | //播放列表 14 | 'music.play-list': IMusic.IMusicItem[] 15 | 'music.favorites': IMusic.IMusicItem[] 16 | 'music.rate': number 17 | 'music.quality': IMusic.IQualityKey 18 | 'app.skipVersion': string 19 | 'app.pluginUpdateTime': number 20 | 'lyric.showTranslation': boolean 21 | 'lyric.detailFontSize': number 22 | 'app.logo': 'Default' | 'Logo1' 23 | //歌单 24 | 'music.playLists': IMusic.PlayList[] 25 | //音源 26 | 'music.musicApi': IMusic.MusicApi[] 27 | //当前选择的音源 28 | 'music.selectedMusicApi': IMusic.MusicApi 29 | //已导入的本地音乐 30 | 'music.importedLocalMusic': IMusic.IMusicItem[] 31 | 'music.autoCacheLocal': boolean 32 | 'app.language': string 33 | 'music.isCachedIconVisible': boolean 34 | 'music.songsNumsToLoad': number 35 | } 36 | 37 | function set(key: K, value: IPersistConfig[K] | undefined) { 38 | const store = getOrCreateMMKV(PersistConfig.PersistStatus) 39 | if (value === undefined) { 40 | store.delete(key) 41 | } else { 42 | store.set(key, JSON.stringify(value)) 43 | } 44 | } 45 | 46 | function get(key: K): IPersistConfig[K] | null { 47 | const store = getOrCreateMMKV(PersistConfig.PersistStatus) 48 | const raw = store.getString(key) 49 | if (raw) { 50 | return safeParse(raw) as IPersistConfig[K] 51 | } 52 | return null 53 | } 54 | 55 | function useValue( 56 | key: K, 57 | defaultValue?: IPersistConfig[K], 58 | ): IPersistConfig[K] | null { 59 | const [state, setState] = useState(get(key) ?? defaultValue ?? null) 60 | 61 | useEffect(() => { 62 | const store = getOrCreateMMKV(PersistConfig.PersistStatus) 63 | const sub = store.addOnValueChangedListener((changedKey) => { 64 | if (key === changedKey) { 65 | setState(get(key)) 66 | } 67 | }) 68 | 69 | return () => { 70 | sub.remove() 71 | } 72 | }, [key]) 73 | 74 | return state 75 | } 76 | 77 | const PersistStatus = { 78 | get, 79 | set, 80 | useValue, 81 | } 82 | 83 | export default PersistStatus 84 | -------------------------------------------------------------------------------- /src/store/getOrCreateMMKV.ts: -------------------------------------------------------------------------------- 1 | 2 | import {MMKV} from 'react-native-mmkv'; 3 | import pathConst from '@/store/pathConst' 4 | 5 | const _mmkvCache: Record = {}; 6 | 7 | global.mmkv = _mmkvCache; 8 | 9 | // Internal Method 10 | const getOrCreateMMKV = (dbName: string, cachePath = false) => { 11 | if (_mmkvCache[dbName]) { 12 | return _mmkvCache[dbName]; 13 | } 14 | 15 | const newStore = new MMKV({ 16 | id: dbName, 17 | path: cachePath ? pathConst.mmkvCachePath : pathConst.mmkvPath, 18 | }); 19 | 20 | _mmkvCache[dbName] = newStore; 21 | return newStore; 22 | }; 23 | 24 | export default getOrCreateMMKV; 25 | -------------------------------------------------------------------------------- /src/store/mediaExtra.ts: -------------------------------------------------------------------------------- 1 | 2 | import safeParse from '@/utils/safeParse'; 3 | import getOrCreateMMKV from '@/store/getOrCreateMMKV' 4 | 5 | // Internal Method 6 | const getPluginStore = (pluginName: string) => { 7 | return getOrCreateMMKV(`MediaExtra.${pluginName}`); 8 | }; 9 | 10 | /** 获取meta信息 */ 11 | const get = (mediaItem: ICommon.IMediaBase) => { 12 | if (mediaItem.platform && mediaItem.id) { 13 | const meta = getPluginStore(mediaItem.platform).getString( 14 | `${mediaItem.id}`, 15 | ); 16 | if (!meta) { 17 | return null; 18 | } 19 | 20 | return safeParse(meta); 21 | } 22 | 23 | return null; 24 | }; 25 | 26 | /** 设置meta信息 */ 27 | const set = (mediaItem: ICommon.IMediaBase, meta: ICommon.IMediaMeta) => { 28 | if (mediaItem.platform && mediaItem.id) { 29 | const store = getPluginStore(mediaItem.platform); 30 | store.set(mediaItem.id, JSON.stringify(meta)); 31 | return true; 32 | } 33 | 34 | return false; 35 | }; 36 | 37 | /** 更新meta信息 */ 38 | const update = ( 39 | mediaItem: ICommon.IMediaBase, 40 | meta: Partial, 41 | ) => { 42 | if (mediaItem.platform && mediaItem.id) { 43 | const store = getPluginStore(mediaItem.platform); 44 | const originalMeta = get(mediaItem); 45 | 46 | store.set( 47 | `${mediaItem.id}`, 48 | JSON.stringify({ 49 | ...(originalMeta || {}), 50 | ...meta, 51 | }), 52 | ); 53 | return true; 54 | } 55 | 56 | return false; 57 | }; 58 | 59 | /** 删除meta信息 */ 60 | const remove = (mediaItem: ICommon.IMediaBase) => { 61 | if (mediaItem.platform && mediaItem.id) { 62 | const store = getPluginStore(mediaItem.platform); 63 | store.delete(`${mediaItem.id}`); 64 | return true; 65 | } 66 | 67 | return false; 68 | }; 69 | 70 | const removeAll = (pluginName: string) => { 71 | const store = getPluginStore(pluginName); 72 | store.clearAll(); 73 | }; 74 | 75 | const MediaExtra = { 76 | get: get, 77 | set: set, 78 | update: update, 79 | remove: remove, 80 | removeAll: removeAll, 81 | }; 82 | 83 | export default MediaExtra; 84 | -------------------------------------------------------------------------------- /src/store/pathConst.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native' 2 | import RNFS, { CachesDirectoryPath } from 'react-native-fs' 3 | 4 | // 基础路径设置 5 | export const basePath = 6 | Platform.OS === 'android' 7 | ? RNFS.ExternalDirectoryPath // Android 存储路径 8 | : RNFS.DocumentDirectoryPath // iOS 存储路径 9 | 10 | // 导出路径配置 11 | export default { 12 | basePath, 13 | pluginPath: `${basePath}/plugins/`, // 插件路径 14 | logPath: `${basePath}/log/`, // 日志路径 15 | dataPath: `${basePath}/data/`, // 数据路径 16 | cachePath: `${basePath}/cache/`, // 缓存路径 17 | musicCachePath: `${CachesDirectoryPath}/TrackPlayer`, // 音乐缓存路径 18 | imageCachePath: `${CachesDirectoryPath}/image_manager_disk_cache`, // 图片缓存路径 19 | lrcCachePath: `${basePath}/cache/lrc/`, // 歌词缓存路径 20 | downloadPath: `${basePath}/download/`, // 下载路径 21 | downloadMusicPath: `${basePath}/download/music/`, // 音乐下载路径 22 | mmkvPath: `${basePath}/mmkv`, // MMKV 存储路径 23 | mmkvCachePath: `${basePath}/cache/mmkv`, // MMKV 缓存路径 24 | } 25 | -------------------------------------------------------------------------------- /src/store/playList.ts: -------------------------------------------------------------------------------- 1 | 2 | import {GlobalState} from '@/utils/stateMapper'; 3 | import PersistStatus from '@/store/PersistStatus' 4 | 5 | /** 音乐队列 */ 6 | const playListStore = new GlobalState([]); 7 | 8 | /** 下标映射 */ 9 | let playListIndexMap: Record> = {}; 10 | 11 | /** 12 | * 设置播放队列 13 | * @param newPlayList 新的播放队列 14 | */ 15 | export function setPlayList( 16 | newPlayList: IMusic.IMusicItem[], 17 | shouldSave = true, 18 | ) { 19 | playListStore.setValue(newPlayList); 20 | const newIndexMap: Record> = {}; 21 | newPlayList.forEach((item, index) => { 22 | // 映射中不存在 23 | if (!newIndexMap[item.platform]) { 24 | newIndexMap[item.platform] = { 25 | [item.id]: index, 26 | }; 27 | } else { 28 | // 修改映射 29 | newIndexMap[item.platform][item.id] = index; 30 | } 31 | }); 32 | playListIndexMap = newIndexMap; 33 | if (shouldSave) { 34 | PersistStatus.set('music.playList', newPlayList); 35 | } 36 | } 37 | 38 | /** 39 | * 获取当前的播放队列 40 | */ 41 | export const getPlayList = playListStore.getValue; 42 | 43 | /** 44 | * hook 45 | */ 46 | export const usePlayList = playListStore.useValue; 47 | 48 | /** 49 | * 寻找歌曲在播放列表中的下标 50 | * @param musicItem 音乐 51 | * @returns 下标 52 | */ 53 | export function getMusicIndex(musicItem?: IMusic.IMusicItem | null) { 54 | if (!musicItem) { 55 | return -1; 56 | } 57 | return playListIndexMap[musicItem.platform]?.[musicItem.id] ?? -1; 58 | } 59 | 60 | /** 61 | * 歌曲是否在播放队列中 62 | * @param musicItem 音乐 63 | * @returns 是否在播放队列中 64 | */ 65 | export function isInPlayList(musicItem?: IMusic.IMusicItem | null) { 66 | if (!musicItem) { 67 | return false; 68 | } 69 | 70 | return playListIndexMap[musicItem.platform]?.[musicItem.id] > -1; 71 | } 72 | 73 | /** 74 | * 获取第i个位置的歌曲 75 | * @param index 下标 76 | */ 77 | export function getPlayListMusicAt(index: number): IMusic.IMusicItem | null { 78 | const playList = playListStore.getValue(); 79 | 80 | const len = playList.length; 81 | if (len === 0) { 82 | return null; 83 | } 84 | 85 | return playList[(index + len) % len]; 86 | } 87 | 88 | /** 89 | * 播放队列是否为空 90 | * @returns 91 | */ 92 | export function isPlayListEmpty() { 93 | return playListStore.getValue().length === 0; 94 | } 95 | -------------------------------------------------------------------------------- /src/store/queue.tsx: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | type QueueStore = { 4 | activeQueueId: string | null 5 | setActiveQueueId: (id: string) => void 6 | } 7 | 8 | export const useQueueStore = create()((set) => ({ 9 | activeQueueId: null, 10 | setActiveQueueId: (id) => set({ activeQueueId: id }), 11 | })) 12 | 13 | export const useQueue = () => useQueueStore((state) => state) 14 | -------------------------------------------------------------------------------- /src/store/trackViewList.ts: -------------------------------------------------------------------------------- 1 | 2 | import {GlobalState} from '@/utils/stateMapper'; 3 | import PersistStatus from '@/store/PersistStatus' 4 | 5 | /** 音乐队列 */ 6 | const trackViewList = new GlobalState([]); 7 | 8 | /** 下标映射 */ 9 | let playListIndexMap: Record> = {}; 10 | 11 | /** 12 | * 设置播放队列 13 | * @param newPlayList 新的播放队列 14 | */ 15 | export function setTrackViewList( 16 | newPlayList: IMusic.IMusicItem[], 17 | shouldSave = true, 18 | ) { 19 | trackViewList.setValue(newPlayList); 20 | const newIndexMap: Record> = {}; 21 | newPlayList.forEach((item, index) => { 22 | // 映射中不存在 23 | if (!newIndexMap[item.platform]) { 24 | newIndexMap[item.platform] = { 25 | [item.id]: index, 26 | }; 27 | } else { 28 | // 修改映射 29 | newIndexMap[item.platform][item.id] = index; 30 | } 31 | }); 32 | playListIndexMap = newIndexMap; 33 | if (shouldSave) { 34 | PersistStatus.set('music.playList', newPlayList); 35 | } 36 | } 37 | 38 | /** 39 | * 获取当前的播放队列 40 | */ 41 | export const getTrackViewList = trackViewList.getValue; 42 | 43 | /** 44 | * hook 45 | */ 46 | export const useTrackViewList = trackViewList.useValue; 47 | 48 | /** 49 | * 寻找歌曲在播放列表中的下标 50 | * @param musicItem 音乐 51 | * @returns 下标 52 | */ 53 | export function getTrackViewListIndex(musicItem?: IMusic.IMusicItem | null) { 54 | if (!musicItem) { 55 | return -1; 56 | } 57 | return playListIndexMap[musicItem.platform]?.[musicItem.id] ?? -1; 58 | } 59 | 60 | /** 61 | * 歌曲是否在播放队列中 62 | * @param musicItem 音乐 63 | * @returns 是否在播放队列中 64 | */ 65 | export function isInTrackViewList(musicItem?: IMusic.IMusicItem | null) { 66 | if (!musicItem) { 67 | return false; 68 | } 69 | 70 | return playListIndexMap[musicItem.platform]?.[musicItem.id] > -1; 71 | } 72 | 73 | /** 74 | * 获取第i个位置的歌曲 75 | * @param index 下标 76 | */ 77 | export function getTrackViewListMusicAt(index: number): IMusic.IMusicItem | null { 78 | const playList = trackViewList.getValue(); 79 | const len = playList.length; 80 | if (len === 0) { 81 | return null; 82 | } 83 | 84 | return playList[(index + len) % len]; 85 | } 86 | 87 | /** 88 | * 播放队列是否为空 89 | * @returns 90 | */ 91 | export function isTrackViewListEmpty() { 92 | return trackViewList.getValue().length === 0; 93 | } 94 | -------------------------------------------------------------------------------- /src/store/usePlayerStore.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'zustand/vanilla'; 2 | import { create } from 'zustand'; 3 | import { Track, useActiveTrack } from 'react-native-track-player'; 4 | 5 | interface PlayerState { 6 | isLoading: boolean; 7 | isInitialized: boolean; 8 | prevTrack: Track | null; 9 | activeTrack: Track | null; 10 | setLoading: (isLoading: boolean) => void; 11 | setInitialized: (isInitialized: boolean) => void; 12 | setPrevTrack: (prevTrack: Track | null) => void; 13 | setActiveTrack: (activeTrack: Track | null) => void; 14 | } 15 | 16 | const usePlayerStore = create((set) => ({ 17 | isLoading: false, 18 | isInitialized: false, 19 | prevTrack: null, 20 | activeTrack: null, 21 | setLoading: (isLoading) => set({ isLoading }), 22 | setInitialized: (isInitialized) => set({ isInitialized }), 23 | setPrevTrack: (prevTrack) => set({ prevTrack }), 24 | setActiveTrack: (activeTrack) => set({ activeTrack }), 25 | })); 26 | 27 | export default usePlayerStore; 28 | -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import { colors, fontSize } from '@/constants/tokens' 2 | import { StyleSheet } from 'react-native' 3 | 4 | export const defaultStyles = StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | backgroundColor: colors.background, 8 | }, 9 | text: { 10 | fontSize: fontSize.base, 11 | color: colors.text, 12 | }, 13 | }) 14 | 15 | export const utilsStyles = StyleSheet.create({ 16 | centeredRow: { 17 | flexDirection: 'row', 18 | justifyContent: 'center', 19 | alignItems: 'center', 20 | }, 21 | rightRow: { 22 | flexDirection: 'row', 23 | justifyContent: 'flex-end', 24 | alignItems: 'center', 25 | }, 26 | slider: { 27 | height: 7, 28 | borderRadius: 16, 29 | }, 30 | itemSeparator: { 31 | borderColor: colors.textMuted, 32 | borderWidth: StyleSheet.hairlineWidth, 33 | opacity: 0.3, 34 | }, 35 | emptyContentText: { 36 | ...defaultStyles.text, 37 | color: colors.textMuted, 38 | textAlign: 'center', 39 | marginTop: 20, 40 | }, 41 | emptyContentImage: { 42 | width: 200, 43 | height: 200, 44 | alignSelf: 'center', 45 | marginTop: 40, 46 | opacity: 0.3, 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /src/types/album.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace IAlbum { 2 | export interface IAlbumItemBase extends ICommon.IMediaBase { 3 | artwork?: string; 4 | title: string; 5 | date?: string; 6 | artist?: string; 7 | description: string; 8 | /** 专辑内有多少作品 */ 9 | worksNum?: number; 10 | } 11 | 12 | export interface IAlbumItem extends IAlbumItemBase { 13 | musicList: IMusic.IMusicItem[]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/types/app.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | import type { AppEventTypes } from '@/event/appEvent' 3 | import type { ListEventTypes } from '@/event/listEvent' 4 | import type { DislikeEventTypes } from '@/event/dislikeEvent' 5 | import type { StateEventTypes } from '@/event/stateEvent' 6 | import type { I18n } from '@/lang/i18n' 7 | import type { Buffer as _Buffer } from 'buffer' 8 | import type { SettingScreenIds } from '@/screens/Home/Views/Setting' 9 | 10 | // interface Process { 11 | // env: { 12 | // NODE_ENV: 'development' | 'production' 13 | // } 14 | // versions: { 15 | // app: string 16 | // } 17 | // } 18 | interface GlobalData { 19 | fontSize: number 20 | gettingUrlId: string 21 | 22 | // event_app: AppType 23 | // event_list: ListType 24 | 25 | playerStatus: { 26 | isInitialized: boolean 27 | isRegisteredService: boolean 28 | isIniting: boolean 29 | } 30 | restorePlayInfo: LX.Player.SavedPlayInfo | null 31 | isScreenKeepAwake: boolean 32 | isPlayedStop: boolean 33 | isEnableSyncLog: boolean 34 | isEnableUserApiLog: boolean 35 | playerTrackId: string 36 | 37 | qualityList: LX.QualityList 38 | apis: Partial 39 | apiInitPromise: [Promise, boolean, (success: boolean) => void] 40 | 41 | jumpMyListPosition: boolean 42 | 43 | settingActiveId: SettingScreenIds 44 | 45 | /** 46 | * 首页是否正在滚动中,用于防止意外误触播放歌曲 47 | */ 48 | homePagerIdle: boolean 49 | 50 | // windowInfo: { 51 | // screenW: number 52 | // screenH: number 53 | // fontScale: number 54 | // pixelRatio: number 55 | // screenPxW: number 56 | // screenPxH: number 57 | // } 58 | 59 | // syncKeyInfo: LX.Sync.KeyInfo 60 | } 61 | 62 | 63 | declare global { 64 | var isDev: boolean 65 | var lx: GlobalData 66 | var i18n: I18n 67 | var app_event: AppEventTypes 68 | var list_event: ListEventTypes 69 | var dislike_event: DislikeEventTypes 70 | var state_event: StateEventTypes 71 | 72 | var Buffer: typeof _Buffer 73 | 74 | module NodeJS { 75 | interface ProcessVersions { 76 | app: string 77 | } 78 | } 79 | // var process: Process 80 | } 81 | -------------------------------------------------------------------------------- /src/types/artist.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace IArtist { 2 | export interface IArtistItemBase extends ICommon.IMediaBase { 3 | name: string; 4 | id: string; 5 | fans?: number; 6 | description?: string; 7 | platform: string; 8 | avatar: string; 9 | worksNum: number; 10 | } 11 | export interface IArtistItem extends IArtistItemBase { 12 | musicList: IMusic.IMusicItemBase; 13 | albumList: IAlbum.IAlbumItemBase; 14 | [k: string]: any; 15 | } 16 | 17 | export type ArtistMediaType = IArtist.ArtistMediaType; 18 | } 19 | -------------------------------------------------------------------------------- /src/types/common.d.ts: -------------------------------------------------------------------------------- 1 | // import './app_setting' 2 | 3 | declare namespace LX { 4 | type OnlineSource = 'kw' | 'kg' | 'tx' | 'wy' | 'mg' 5 | type Source = OnlineSource | 'local' 6 | type Quality = '128k' | '320k' | 'flac' | 'flac24bit' | '192k' | 'ape' | 'wav' 7 | type QualityList = Partial> 8 | 9 | type ShareType = 'system' | 'clipboard' 10 | 11 | type UpdateStatus = 'downloaded' | 'downloading' | 'error' | 'checking' | 'idle' 12 | interface VersionInfo { 13 | version: string 14 | desc: string 15 | } 16 | } 17 | declare namespace ICommon { 18 | /** 支持搜索的媒体类型 */ 19 | export type SupportMediaType = 20 | | 'music' 21 | | 'album' 22 | | 'artist' 23 | | 'sheet' 24 | | 'lyric'; 25 | 26 | /** 媒体定义 */ 27 | export type SupportMediaItemBase = { 28 | music: IMusic.IMusicItemBase; 29 | album: IAlbum.IAlbumItemBase; 30 | artist: IArtist.IArtistItemBase; 31 | sheet: IMusic.IMusicSheetItemBase; 32 | lyric: ILyric.ILyricItem; 33 | }; 34 | 35 | export type IUnique = { 36 | id: string; 37 | [k: string | symbol]: any; 38 | }; 39 | 40 | export type IMediaBase = { 41 | id: string; 42 | platform: string; 43 | $?: any; 44 | [k: symbol]: any; 45 | [k: string]: any; 46 | }; 47 | 48 | /** 一些额外信息 */ 49 | export type IMediaMeta = { 50 | /** 关联歌词信息 */ 51 | associatedLrc?: IMediaBase; 52 | /** 是否下载过 TODO: 删去 */ 53 | downloaded?: boolean; 54 | /** 本地下载路径 */ 55 | localPath?: string; 56 | /** 补充的音乐信息 */ 57 | mediaItem?: Partial; 58 | /** 歌词偏移 */ 59 | lyricOffset?: number; 60 | 61 | lrc?: string; 62 | associatedLrc?: IMediaBase; 63 | headers?: Record; 64 | url?: string; 65 | id?: string; 66 | platform?: string; 67 | qualities?: IMusic.IQuality; 68 | $?: { 69 | local?: { 70 | localLrc?: string; 71 | [k: string]: any; 72 | }; 73 | [k: string]: any; 74 | }; 75 | [k: string]: any; 76 | [k: symbol]: any; 77 | }; 78 | 79 | export type WithMusicList = T & { 80 | musicList?: IMusic.IMusicItem[]; 81 | }; 82 | 83 | export type PaginationResponse = { 84 | isEnd?: boolean; 85 | data?: T[]; 86 | }; 87 | 88 | export interface IPoint { 89 | x: number; 90 | y: number; 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /src/types/config_files.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace LX { 2 | namespace ConfigFile { 3 | interface MyListInfoPart { 4 | type: 'playListPart_v2' 5 | data: LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull | LX.List.UserListInfoFull 6 | } 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/types/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import React from 'react'; 3 | import {SvgProps} from 'react-native-svg'; 4 | const content: React.FC; 5 | export default content; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/dislike_list.d.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | declare namespace LX { 4 | namespace Dislike { 5 | // interface ListItemMusicText { 6 | // id?: string 7 | // // type: 'music' 8 | // name: string | null 9 | // singer: string | null 10 | // } 11 | // interface ListItemMusic { 12 | // id?: number 13 | // type: 'musicId' 14 | // musicId: string 15 | // meta: LX.Music.MusicInfo 16 | // } 17 | // type ListItem = ListItemMusicText 18 | // type ListItem = string 19 | // type ListItem = ListItemMusic | ListItemMusicText 20 | 21 | interface DislikeMusicInfo { 22 | name: string 23 | singer: string 24 | } 25 | 26 | type DislikeRules = string 27 | 28 | interface DislikeInfo { 29 | // musicIds: Set 30 | names: Set 31 | musicNames: Set 32 | singerNames: Set 33 | // list: LX.Dislike.ListItem[] 34 | rules: DislikeRules 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/types/dislike_list_sync.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace LX { 2 | 3 | namespace Sync { 4 | namespace Dislike { 5 | interface ListInfo { 6 | lastSyncDate?: number 7 | snapshotKey: string 8 | } 9 | 10 | interface SyncActionBase { 11 | action: A 12 | } 13 | interface SyncActionData extends SyncActionBase { 14 | data: D 15 | } 16 | type SyncAction = D extends undefined ? SyncActionBase : SyncActionData 17 | type ActionList = SyncAction<'dislike_data_overwrite', LX.Dislike.DislikeRules> 18 | | SyncAction<'dislike_music_add', LX.Dislike.DislikeMusicInfo[]> 19 | | SyncAction<'dislike_music_clear'> 20 | 21 | type SyncMode = 'merge_local_remote' 22 | | 'merge_remote_local' 23 | | 'overwrite_local_remote' 24 | | 'overwrite_remote_local' 25 | // | 'none' 26 | | 'cancel' 27 | } 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/types/download_list.d.ts: -------------------------------------------------------------------------------- 1 | 2 | // interface DownloadList { 3 | 4 | // } 5 | 6 | 7 | declare namespace LX { 8 | namespace Download { 9 | type DownloadTaskStatus = 'run' 10 | | 'waiting' 11 | | 'pause' 12 | | 'error' 13 | | 'completed' 14 | 15 | type FileExt = 'mp3' | 'flac' | 'wav' | 'ape' 16 | 17 | interface ProgressInfo { 18 | progress: number 19 | speed: string 20 | downloaded: number 21 | total: number 22 | } 23 | 24 | interface DownloadTaskActionBase { 25 | action: A 26 | } 27 | interface DownloadTaskActionData extends DownloadTaskActionBase { 28 | data: D 29 | } 30 | type DownloadTaskAction = D extends undefined ? DownloadTaskActionBase : DownloadTaskActionData 31 | 32 | type DownloadTaskActions = DownloadTaskAction<'start'> 33 | | DownloadTaskAction<'complete'> 34 | | DownloadTaskAction<'refreshUrl'> 35 | | DownloadTaskAction<'statusText', string> 36 | | DownloadTaskAction<'progress', ProgressInfo> 37 | | DownloadTaskAction<'error', { 38 | error?: string 39 | message?: string 40 | }> 41 | 42 | interface ListItem { 43 | id: string 44 | isComplate: boolean 45 | status: DownloadTaskStatus 46 | statusText: string 47 | downloaded: number 48 | total: number 49 | progress: number 50 | speed: string 51 | metadata: { 52 | musicInfo: LX.Music.MusicInfoOnline 53 | url: string | null 54 | quality: LX.Quality 55 | ext: FileExt 56 | fileName: string 57 | filePath: string 58 | } 59 | } 60 | 61 | interface saveDownloadMusicInfo { 62 | list: ListItem[] 63 | addMusicLocationType: LX.AddMusicLocationType 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' 2 | declare module '*.jpg' 3 | declare let platform 4 | -------------------------------------------------------------------------------- /src/types/list_sync.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace LX { 2 | 3 | namespace Sync { 4 | namespace List { 5 | interface ListInfo { 6 | lastSyncDate?: number 7 | snapshotKey: string 8 | } 9 | 10 | interface SyncActionBase { 11 | action: A 12 | } 13 | interface SyncActionData extends SyncActionBase { 14 | data: D 15 | } 16 | type SyncAction = D extends undefined ? SyncActionBase : SyncActionData 17 | type ActionList = SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite> 18 | | SyncAction<'list_create', LX.List.ListActionAdd> 19 | | SyncAction<'list_remove', LX.List.ListActionRemove> 20 | | SyncAction<'list_update', LX.List.ListActionUpdate> 21 | | SyncAction<'list_update_position', LX.List.ListActionUpdatePosition> 22 | | SyncAction<'list_music_add', LX.List.ListActionMusicAdd> 23 | | SyncAction<'list_music_move', LX.List.ListActionMusicMove> 24 | | SyncAction<'list_music_remove', LX.List.ListActionMusicRemove> 25 | | SyncAction<'list_music_update', LX.List.ListActionMusicUpdate> 26 | | SyncAction<'list_music_update_position', LX.List.ListActionMusicUpdatePosition> 27 | | SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite> 28 | | SyncAction<'list_music_clear', LX.List.ListActionMusicClear> 29 | 30 | type ListData = Omit 31 | type SyncMode = 'merge_local_remote' 32 | | 'merge_remote_local' 33 | | 'overwrite_local_remote' 34 | | 'overwrite_remote_local' 35 | | 'overwrite_local_remote_full' 36 | | 'overwrite_remote_local_full' 37 | // | 'none' 38 | | 'cancel' 39 | } 40 | 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/types/lyric.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace ILyric { 2 | export interface ILyricItem extends IMusic.IMusicItem { 3 | /** 歌词(无时间戳) */ 4 | rawLrcTxt?: string; 5 | } 6 | 7 | export interface ILyricSource { 8 | /** @deprecated 歌词url */ 9 | lrc?: string; 10 | /** 纯文本格式歌词 */ 11 | rawLrc?: string; 12 | /** 纯文本格式的翻译 */ 13 | translation?: string; 14 | } 15 | 16 | export interface IParsedLrcItem { 17 | /** 时间 s */ 18 | time: number; 19 | /** 歌词 */ 20 | lrc: string; 21 | /** 下标 */ 22 | index?: number; 23 | } 24 | 25 | export type IParsedLrc = IParsedLrcItem[]; 26 | } 27 | -------------------------------------------------------------------------------- /src/types/musicSheet.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace IMusic { 2 | export interface IMusicSheetItemBase { 3 | /** 封面图 */ 4 | coverImg?: string; 5 | artwork?: string; 6 | /** 标题 */ 7 | title?: string; 8 | /** 作者 */ 9 | artist?: string; 10 | /** 歌单id */ 11 | id: string; 12 | /** 描述 */ 13 | description?: string; 14 | /** 作品总数 */ 15 | worksNum?: number; 16 | platform?: string; 17 | } 18 | /** 歌单项 */ 19 | export interface IMusicSheetItem extends IMusicSheetItemBase { 20 | musicList: Array; 21 | } 22 | 23 | export type IMusicSheet = Array; 24 | } 25 | -------------------------------------------------------------------------------- /src/types/musicSheetGroup.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace IMusic { 2 | /** 歌单项 */ 3 | export interface IMusicSheetGroupItem { 4 | title?: string; 5 | data: Array; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/types/player.d.ts: -------------------------------------------------------------------------------- 1 | import type { Track as RNTrack } from 'react-native-track-player' 2 | 3 | declare global { 4 | namespace LX { 5 | namespace Player { 6 | interface MusicInfo { 7 | id: string | null 8 | pic: string | null | undefined 9 | lrc: string | null 10 | tlrc: string | null 11 | rlrc: string | null 12 | lxlrc: string | null 13 | rawlrc: string | null 14 | // url: string | null 15 | name: string 16 | singer: string 17 | album: string 18 | } 19 | 20 | interface LyricInfo extends LX.Music.LyricInfo { 21 | rawlrcInfo: LX.Music.LyricInfo 22 | } 23 | 24 | type PlayMusic = LX.Music.MusicInfo | LX.Download.ListItem 25 | 26 | type PlayMusicInfo = Readonly<{ 27 | /** 28 | * 当前播放歌曲的列表 id 29 | */ 30 | musicInfo: PlayMusic 31 | /** 32 | * 当前播放歌曲的列表 id 33 | */ 34 | listId: string 35 | /** 36 | * 是否属于 “稍后播放” 37 | */ 38 | isTempPlay: boolean 39 | }> 40 | 41 | interface PlayInfo { 42 | /** 43 | * 当前正在播放歌曲 index 44 | */ 45 | playIndex: number 46 | /** 47 | * 播放器的播放列表 id 48 | */ 49 | playerListId: string | null 50 | /** 51 | * 播放器播放歌曲 index 52 | */ 53 | playerPlayIndex: number 54 | } 55 | 56 | interface TempPlayListItem { 57 | /** 58 | * 播放列表id 59 | */ 60 | listId: string 61 | /** 62 | * 歌曲信息 63 | */ 64 | musicInfo: PlayMusic 65 | /** 66 | * 是否添加到列表顶部 67 | */ 68 | isTop?: boolean 69 | } 70 | 71 | interface SavedPlayInfo { 72 | time: number 73 | maxTime: number 74 | listId: string 75 | index: number 76 | } 77 | 78 | interface Track extends RNTrack { 79 | musicId: string 80 | // original: PlayMusic 81 | // quality: LX.Quality 82 | } 83 | 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/types/shims.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module 'crypto' { 3 | import crypto from 'react-native-quick-crypto' 4 | export default crypto 5 | } 6 | -------------------------------------------------------------------------------- /src/types/sync.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace LX { 3 | namespace Sync { 4 | interface Status { 5 | status: boolean 6 | message: string 7 | } 8 | 9 | interface KeyInfo { 10 | clientId: string 11 | key: string 12 | serverName: string 13 | } 14 | 15 | interface Socket extends WebSocket { 16 | isReady: boolean 17 | data: { 18 | keyInfo: KeyInfo 19 | urlInfo: UrlInfo 20 | } 21 | moduleReadys: { 22 | list: boolean 23 | dislike: boolean 24 | } 25 | 26 | onClose: (handler: (err: Error) => (void | Promise)) => () => void 27 | 28 | remote: LX.Sync.ServerSyncActions 29 | remoteQueueList: LX.Sync.ServerSyncListActions 30 | remoteQueueDislike: LX.Sync.ServerSyncDislikeActions 31 | } 32 | 33 | 34 | interface ModeTypes { 35 | list: LX.Sync.List.SyncMode 36 | dislike: LX.Sync.Dislike.SyncMode 37 | } 38 | 39 | type ModeType = { [K in keyof ModeTypes]: { type: K, mode: ModeTypes[K] } }[keyof ModeTypes] 40 | 41 | interface UrlInfo { 42 | wsProtocol: string 43 | httpProtocol: string 44 | hostPath: string 45 | href: string 46 | } 47 | 48 | interface ListConfig { 49 | skipSnapshot: boolean 50 | } 51 | interface DislikeConfig { 52 | skipSnapshot: boolean 53 | } 54 | type ServerType = 'desktop-app' | 'server' 55 | interface EnabledFeatures { 56 | list?: false | ListConfig 57 | dislike?: false | DislikeConfig 58 | } 59 | type SupportedFeatures = Partial<{ [k in keyof EnabledFeatures]: number }> 60 | } 61 | } 62 | } 63 | 64 | export {} 65 | -------------------------------------------------------------------------------- /src/types/sync_common.d.ts: -------------------------------------------------------------------------------- 1 | type WarpSyncHandlerActions = { 2 | [K in keyof Actions]: (...args: [Socket, ...Parameters]) => ReturnType 3 | } 4 | 5 | declare namespace LX { 6 | namespace Sync { 7 | type ServerSyncActions = WarpPromiseRecord<{ 8 | onFeatureChanged: (feature: EnabledFeatures) => void 9 | }> 10 | type ServerSyncHandlerActions = WarpSyncHandlerActions 11 | 12 | type ServerSyncListActions = WarpPromiseRecord<{ 13 | onListSyncAction: (action: LX.Sync.List.ActionList) => void 14 | }> 15 | type ServerSyncHandlerListActions = WarpSyncHandlerActions 16 | 17 | type ServerSyncDislikeActions = WarpPromiseRecord<{ 18 | onDislikeSyncAction: (action: LX.Sync.Dislike.ActionList) => void 19 | }> 20 | type ServerSyncHandlerDislikeActions = WarpSyncHandlerActions 21 | 22 | type ClientSyncActions = WarpPromiseRecord<{ 23 | getEnabledFeatures: (serverType: ServerType, supportedFeatures: SupportedFeatures) => EnabledFeatures 24 | finished: () => void 25 | }> 26 | type ClientSyncHandlerActions = WarpSyncHandlerActions 27 | 28 | type ClientSyncListActions = WarpPromiseRecord<{ 29 | onListSyncAction: (action: LX.Sync.List.ActionList) => void 30 | list_sync_get_md5: () => string 31 | list_sync_get_sync_mode: () => LX.Sync.List.SyncMode 32 | list_sync_get_list_data: () => LX.Sync.List.ListData 33 | list_sync_set_list_data: (data: LX.Sync.List.ListData) => void 34 | list_sync_finished: () => void 35 | }> 36 | type ClientSyncHandlerListActions = WarpSyncHandlerActions 37 | 38 | type ClientSyncDislikeActions = WarpPromiseRecord<{ 39 | onDislikeSyncAction: (action: LX.Sync.Dislike.ActionList) => void 40 | dislike_sync_get_md5: () => string 41 | dislike_sync_get_sync_mode: () => LX.Sync.Dislike.SyncMode 42 | dislike_sync_get_list_data: () => LX.Dislike.DislikeRules 43 | dislike_sync_set_list_data: (data: LX.Dislike.DislikeRules) => void 44 | dislike_sync_finished: () => void 45 | }> 46 | type ClientSyncHandlerDislikeActions = WarpSyncHandlerActions 47 | } 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/types/user_api.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace LX { 2 | namespace UserApi { 3 | type UserApiSourceInfoType = 'music' 4 | type UserApiSourceInfoActions = 'musicUrl' | 'lyric' | 'pic' 5 | 6 | interface UserApiSourceInfo { 7 | name: string 8 | type: UserApiSourceInfoType 9 | actions: UserApiSourceInfoActions[] 10 | qualitys: LX.Quality[] 11 | } 12 | 13 | type UserApiSources = Record 14 | 15 | 16 | interface UserApiInfo { 17 | id: string 18 | name: string 19 | description: string 20 | // script: string 21 | allowShowUpdateAlert: boolean 22 | author: string 23 | homepage: string 24 | version: string 25 | sources?: UserApiSources 26 | } 27 | 28 | interface UserApiStatus { 29 | status: boolean 30 | message?: string 31 | apiInfo?: UserApiInfo 32 | } 33 | 34 | interface UserApiUpdateInfo { 35 | name: string 36 | description: string 37 | log: string 38 | updateUrl?: string 39 | } 40 | 41 | interface UserApiRequestParams { 42 | requestKey: string 43 | data: any 44 | } 45 | interface UserApiRequestParams { 46 | requestKey: string 47 | data: any 48 | } 49 | type UserApiRequestCancelParams = string 50 | type UserApiSetApiParams = string 51 | 52 | interface UserApiSetAllowUpdateAlertParams { 53 | id: string 54 | enable: boolean 55 | } 56 | 57 | interface ImportUserApi { 58 | apiInfo: UserApiInfo 59 | apiList: UserApiInfo[] 60 | } 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/types/utils.d.ts: -------------------------------------------------------------------------------- 1 | type MakeOptional = Omit & Partial> 2 | 3 | type DeepPartial = { 4 | [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; 5 | } 6 | 7 | type Modify = Omit & R 8 | 9 | type MakeArrayItemReadOnly = { [K in keyof T]: Readonly } 10 | 11 | type ForwardRefFn =

(p: React.PropsWithChildren

& React.RefAttributes) => React.ReactNode | null 12 | 13 | // type UndefinedOrNever = undefined 14 | type Actions = { 15 | [U in T as U['action']]: 'data' extends keyof U ? U['data'] : undefined 16 | } 17 | 18 | type WarpPromiseValue = T extends ((...args: infer P) => Promise) 19 | ? ((...args: P) => Promise) 20 | : T extends ((...args: infer P2) => infer R2) 21 | ? ((...args: P2) => Promise) 22 | : Promise 23 | 24 | type WarpPromiseRecord> = { 25 | [K in keyof T]: WarpPromiseValue 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/delay.ts: -------------------------------------------------------------------------------- 1 | import BackgroundTimer from 'react-native-background-timer'; 2 | 3 | export default function (millsecond: number) { 4 | return new Promise(resolve => { 5 | BackgroundTimer.setTimeout(() => { 6 | resolve(); 7 | }, millsecond); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import en from '@/locales/en.json' 2 | import zh from '@/locales/zh.json' 3 | import PersistStatus from '@/store/PersistStatus' 4 | import { getLocales } from 'expo-localization' 5 | import { I18n } from 'i18n-js' 6 | import { GlobalState } from './stateMapper' 7 | 8 | const translations = { 9 | en, 10 | zh, 11 | } 12 | 13 | const i18n = new I18n(translations) 14 | 15 | i18n.enableFallback = true 16 | export const nowLanguage = new GlobalState('zh') 17 | 18 | export const setI18nConfig = () => { 19 | const languageCode = PersistStatus.get('app.language') || getLocales()[0].languageCode || 'zh' 20 | nowLanguage.setValue(languageCode) 21 | i18n.locale = languageCode 22 | } 23 | 24 | export const changeLanguage = (languageCode: string) => { 25 | PersistStatus.set('app.language', languageCode) 26 | i18n.locale = languageCode 27 | nowLanguage.setValue(languageCode) 28 | } 29 | 30 | export default i18n 31 | -------------------------------------------------------------------------------- /src/utils/mediaIndexMap.ts: -------------------------------------------------------------------------------- 1 | interface IIndexMap { 2 | getIndexMap: () => Record>; 3 | getIndex: (musicItem: ICommon.IMediaBase) => number; 4 | has: (mediaItem: ICommon.IMediaBase) => boolean; 5 | } 6 | 7 | export function createMediaIndexMap( 8 | mediaItems: ICommon.IMediaBase[], 9 | ): IIndexMap { 10 | const indexMap: Record> = {}; 11 | 12 | mediaItems.forEach((item, index) => { 13 | // 映射中不存在 14 | if (!indexMap[item.platform]) { 15 | indexMap[item.platform] = { 16 | [item.id]: index, 17 | }; 18 | } else { 19 | // 修改映射 20 | indexMap[item.platform][item.id] = index; 21 | } 22 | }); 23 | 24 | function getIndexMap() { 25 | return indexMap; 26 | } 27 | 28 | function getIndex(mediaItem: ICommon.IMediaBase) { 29 | if (!mediaItem) { 30 | return -1; 31 | } 32 | return indexMap[mediaItem.platform]?.[mediaItem.id] ?? -1; 33 | } 34 | 35 | function has(mediaItem: ICommon.IMediaBase) { 36 | if (!mediaItem) { 37 | return false; 38 | } 39 | 40 | return indexMap[mediaItem.platform]?.[mediaItem.id] > -1; 41 | } 42 | 43 | return { 44 | getIndexMap, 45 | getIndex, 46 | has, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/mediaItem.ts: -------------------------------------------------------------------------------- 1 | import { internalSerializeKey, sortIndexSymbol, timeStampSymbol } from '@/constants/commonConst' 2 | import MediaMeta from '@/store/mediaExtra' 3 | 4 | /** 获取mediakey */ 5 | export function getMediaKey(mediaItem: ICommon.IMediaBase) { 6 | return `${mediaItem.platform}@${mediaItem.id}` 7 | } 8 | 9 | /** 比较两media是否相同 */ 10 | export function isSameMediaItem( 11 | a: ICommon.IMediaBase | null | undefined, 12 | b: ICommon.IMediaBase | null | undefined, 13 | ) { 14 | return a && b && a.id == b.id && a.platform === b.platform 15 | } 16 | 17 | /** 查找是否存在 */ 18 | export function includesMedia( 19 | a: ICommon.IMediaBase[] | null | undefined, 20 | b: ICommon.IMediaBase | null | undefined, 21 | ) { 22 | if (!a || !b) { 23 | return false 24 | } 25 | return a.findIndex((_) => isSameMediaItem(_, b)) !== -1 26 | } 27 | 28 | /** 获取复位的mediaItem */ 29 | // export function resetMediaItem>( 30 | // mediaItem: T, 31 | // platform?: string, 32 | // newObj?: boolean, 33 | // ): T { 34 | // // 本地音乐不做处理 35 | // if ( 36 | // mediaItem.platform === localPluginPlatform || 37 | // platform === localPluginPlatform 38 | // ) { 39 | // return newObj ? {...mediaItem} : mediaItem; 40 | // } 41 | // if (!newObj) { 42 | // mediaItem.platform = platform ?? mediaItem.platform; 43 | // mediaItem[internalSerializeKey] = undefined; 44 | // return mediaItem; 45 | // } else { 46 | // return produce(mediaItem, _ => { 47 | // _.platform = platform ?? mediaItem.platform; 48 | // _[internalSerializeKey] = undefined; 49 | // }); 50 | // } 51 | // } 52 | 53 | export function mergeProps( 54 | mediaItem: ICommon.IMediaBase, 55 | props: Record | undefined, 56 | anotherProps?: Record | undefined | null, 57 | ) { 58 | return props 59 | ? { 60 | ...mediaItem, 61 | ...props, 62 | ...(anotherProps ?? {}), 63 | id: mediaItem.id, 64 | platform: mediaItem.platform, 65 | } 66 | : mediaItem 67 | } 68 | 69 | export enum InternalDataType { 70 | LOCALPATH = 'localPath', 71 | } 72 | 73 | export function trimInternalData(mediaItem: ICommon.IMediaBase | null | undefined) { 74 | if (!mediaItem) { 75 | return undefined 76 | } 77 | return { 78 | ...mediaItem, 79 | [internalSerializeKey]: undefined, 80 | } 81 | } 82 | 83 | /** 关联歌词 */ 84 | export async function associateLrc(musicItem: ICommon.IMediaBase, linkto: ICommon.IMediaBase) { 85 | if (!musicItem || !linkto) { 86 | throw new Error('') 87 | } 88 | 89 | // 如果相同直接断链 90 | MediaMeta.update(musicItem, { 91 | associatedLrc: isSameMediaItem(musicItem, linkto) ? undefined : linkto, 92 | }) 93 | } 94 | 95 | export function sortByTimestampAndIndex(array: any[], newArray = false) { 96 | if (newArray) { 97 | array = [...array] 98 | } 99 | return array.sort((a, b) => { 100 | const ts = a[timeStampSymbol] - b[timeStampSymbol] 101 | if (ts !== 0) { 102 | return ts 103 | } 104 | return a[sortIndexSymbol] - b[sortIndexSymbol] 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /src/utils/musicInfo/Buffer.js: -------------------------------------------------------------------------------- 1 | class Buffer { 2 | constructor() { 3 | this.cursor = 0; 4 | this.size = 0; 5 | this.data = null; 6 | } 7 | 8 | finished() { 9 | return this.cursor >= this.size; 10 | } 11 | 12 | getByte() { 13 | return this.data[this.cursor++]; 14 | } 15 | 16 | move(length) { 17 | let start = this.cursor; 18 | this.cursor = this.cursor + length > this.size ? this.size : this.cursor + length; 19 | let end = this.cursor; 20 | return end - start; 21 | } 22 | 23 | setData(data) { 24 | this.size = data.length; 25 | this.data = data; 26 | this.cursor = 0; 27 | } 28 | } 29 | 30 | export default Buffer; -------------------------------------------------------------------------------- /src/utils/musicInfo/MusicInfo.d.ts: -------------------------------------------------------------------------------- 1 | declare class MusicInfo { 2 | static getMusicInfoAsync(fileUri: string, options?: MusicInfoOptions): Promise 3 | } 4 | 5 | export declare class MusicInfoOptions { 6 | title?: boolean 7 | artist?: boolean 8 | album?: boolean 9 | genre?: boolean 10 | picture?: boolean 11 | } 12 | 13 | export declare class MusicInfoResponse { 14 | title?: string 15 | artist?: string 16 | album?: string 17 | genre?: string 18 | picture?: Picture 19 | } 20 | 21 | export declare class Picture { 22 | description: string 23 | pictureData: string 24 | } 25 | export default MusicInfo 26 | -------------------------------------------------------------------------------- /src/utils/musicInfo/MusicInfoResponse.js: -------------------------------------------------------------------------------- 1 | // src/utils/musicInfo/MusicInfoResponse.js 2 | 3 | class MusicInfoResponse { 4 | constructor() { 5 | this.title = ''; 6 | this.artist = ''; 7 | this.album = ''; 8 | this.genre = ''; 9 | this.picture = null; 10 | } 11 | } 12 | 13 | export default MusicInfoResponse; -------------------------------------------------------------------------------- /src/utils/musicInfo/index.js: -------------------------------------------------------------------------------- 1 | import MusicInfo from './MusicInfo'; 2 | export default MusicInfo; -------------------------------------------------------------------------------- /src/utils/rpx.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native' 2 | 3 | const windowWidth = Dimensions.get('window').width 4 | const windowHeight = Dimensions.get('window').height 5 | const minWindowEdge = Math.min(windowHeight, windowWidth) 6 | const maxWindowEdge = Math.max(windowHeight, windowWidth) 7 | 8 | export default function (rpx: number) { 9 | return (rpx / 750) * minWindowEdge 10 | } 11 | 12 | export function vh(pct: number) { 13 | return (pct / 100) * Dimensions.get('window').height 14 | } 15 | 16 | export function vw(pct: number) { 17 | return (pct / 100) * Dimensions.get('window').width 18 | } 19 | 20 | export function vmin(pct: number) { 21 | return (pct / 100) * minWindowEdge 22 | } 23 | 24 | export function vmax(pct: number) { 25 | return (pct / 100) * maxWindowEdge 26 | } 27 | 28 | export function sh(pct: number) { 29 | return (pct / 100) * Dimensions.get('screen').height 30 | } 31 | 32 | export function sw(pct: number) { 33 | return (pct / 100) * Dimensions.get('screen').width 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/safeParse.ts: -------------------------------------------------------------------------------- 1 | export default function (raw?: string) { 2 | try { 3 | if (!raw) { 4 | return null; 5 | } 6 | return JSON.parse(raw) as T; 7 | } catch { 8 | return null; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/stateMapper.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export default class StateMapper { 4 | private getFun: () => T 5 | private cbs: Set = new Set([]) 6 | constructor(getFun: () => T) { 7 | this.getFun = getFun 8 | } 9 | 10 | notify = () => { 11 | this.cbs.forEach((_) => _?.()) 12 | } 13 | 14 | useMappedState = () => { 15 | const [_state, _setState] = useState(this.getFun) 16 | const updateState = () => { 17 | _setState(this.getFun()) 18 | } 19 | useEffect(() => { 20 | this.cbs.add(updateState) 21 | return () => { 22 | this.cbs.delete(updateState) 23 | } 24 | }, []) 25 | return _state 26 | } 27 | } 28 | 29 | type UpdateFunc = (prev: T) => T 30 | 31 | export class GlobalState { 32 | private value: T 33 | private stateMapper: StateMapper 34 | 35 | constructor(initValue: T) { 36 | this.value = initValue 37 | this.stateMapper = new StateMapper(this.getValue) 38 | } 39 | 40 | public getValue = () => { 41 | return this.value 42 | } 43 | 44 | public useValue = () => { 45 | return this.stateMapper.useMappedState() 46 | } 47 | 48 | public setValue = (value: T | UpdateFunc) => { 49 | let newValue: T 50 | if (typeof value === 'function') { 51 | newValue = (value as UpdateFunc)(this.value) 52 | } else { 53 | newValue = value 54 | } 55 | 56 | this.value = newValue 57 | this.stateMapper.notify() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/utils/timeformat.ts: -------------------------------------------------------------------------------- 1 | export default function (time: number) { 2 | time = Math.round(time); 3 | if (time < 60) { 4 | return `00:${time.toFixed(0).padStart(2, '0')}`; 5 | } 6 | const sec = Math.floor(time % 60); 7 | time = Math.floor(time / 60); 8 | const min = time % 60; 9 | time = Math.floor(time / 60); 10 | const formatted = `${min.toString().padStart(2, '0')}:${sec 11 | .toFixed(0) 12 | .padStart(2, '0')}`; 13 | if (time === 0) { 14 | return formatted; 15 | } 16 | 17 | return `${time}:${formatted}`; 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/timingClose.ts: -------------------------------------------------------------------------------- 1 | import myTrackPlayer from '@/helpers/trackPlayerIndex' 2 | 3 | import StateMapper from '@/utils/stateMapper' 4 | import { useEffect, useRef, useState } from 'react' 5 | import BackgroundTimer from 'react-native-background-timer' 6 | 7 | import { logInfo } from '@/helpers/logger' 8 | import { NativeModule, NativeModules } from 'react-native' 9 | 10 | interface INativeUtils extends NativeModule { 11 | exitApp: () => void 12 | checkStoragePermission: () => Promise 13 | requestStoragePermission: () => void 14 | } 15 | 16 | const NativeUtils = NativeModules.NativeUtils 17 | let deadline: number | null = null 18 | const stateMapper = new StateMapper(() => deadline) 19 | // let closeAfterPlayEnd = false; 20 | // const closeAfterPlayEndStateMapper = new StateMapper(() => closeAfterPlayEnd); 21 | let timerId: any 22 | 23 | function setTimingClose(_deadline: number | null) { 24 | deadline = _deadline 25 | stateMapper.notify() 26 | timerId && BackgroundTimer.clearTimeout(timerId) 27 | if (_deadline) { 28 | logInfo('将在', (_deadline - Date.now()) / 1000 / 60, '分钟后暂停播放') 29 | timerId = BackgroundTimer.setTimeout(async () => { 30 | // todo: 播完整首歌再关闭 31 | await myTrackPlayer.pause() 32 | // NativeUtils.exitApp() 33 | // if(closeAfterPlayEnd) { 34 | // myTrackPlayer.addEventListener() 35 | // } else { 36 | // // 立即关闭 37 | // NativeUtils.exitApp(); 38 | // } 39 | }, _deadline - Date.now()) 40 | } else { 41 | timerId = null 42 | } 43 | } 44 | 45 | function useTimingClose() { 46 | const _deadline = stateMapper.useMappedState() 47 | const [countDown, setCountDown] = useState(deadline ? deadline - Date.now() : null) 48 | const intervalRef = useRef() 49 | 50 | useEffect(() => { 51 | // deadline改变时,更新定时器 52 | // 清除原有的定时器 53 | intervalRef.current && clearInterval(intervalRef.current) 54 | intervalRef.current = null 55 | 56 | // 清空定时 57 | if (!_deadline || _deadline <= Date.now()) { 58 | setCountDown(null) 59 | return 60 | } else { 61 | // 更新倒计时 62 | setCountDown(Math.max(_deadline - Date.now(), 0) / 1000) 63 | intervalRef.current = setInterval(() => { 64 | setCountDown(Math.max(_deadline - Date.now(), 0) / 1000) 65 | }, 1000) 66 | } 67 | }, [_deadline]) 68 | 69 | return countDown 70 | } 71 | 72 | export { setTimingClose, useTimingClose } 73 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": false, 5 | "target": "ES6", 6 | "module": "commonjs", 7 | "moduleResolution": "Node", 8 | "jsx": "react-native", 9 | "lib": ["dom", "esnext"], 10 | "noEmit": true, 11 | "allowSyntheticDefaultImports": true, 12 | "skipLibCheck": true, 13 | "resolveJsonModule": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "@/*": ["./src/*"], 17 | "@/assets/*": ["./assets/*"] 18 | } 19 | }, 20 | "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"], 21 | "exclude": ["node_modules"] 22 | } 23 | --------------------------------------------------------------------------------