├── .bundle └── config ├── .commitlintrc.json ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report_zh.yaml │ ├── config.yml │ └── feature_request_zh.yaml └── workflows │ └── autobuild.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .imgs ├── artist-detail.jpg ├── basic-setting.jpg ├── main.jpg ├── search-in-sheet.jpg ├── song-cover.jpg ├── song-lrc.jpg ├── song-sheet.jpg └── theme-setting.jpg ├── .prettierrc.js ├── .watchmanconfig ├── Gemfile ├── LICENSE ├── android ├── app │ ├── build.gradle │ ├── debug.keystore │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── ic_launcher-playstore.png │ │ ├── java │ │ └── fun │ │ │ └── upup │ │ │ └── musicfree │ │ │ ├── MainActivity.kt │ │ │ ├── MainApplication.kt │ │ │ ├── lyricUtil │ │ │ ├── LyricUtilModule.kt │ │ │ ├── LyricUtilPackage.kt │ │ │ └── LyricView.kt │ │ │ ├── mp3Util │ │ │ ├── Mp3UtilModule.kt │ │ │ └── Mp3UtilPackage.kt │ │ │ └── utils │ │ │ ├── UtilsModule.kt │ │ │ └── UtilsPackage.kt │ │ └── res │ │ ├── drawable │ │ ├── rn_edit_text_material.xml │ │ ├── splashscreen.xml │ │ └── splashscreen_image.png │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ └── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── app.json ├── babel.config.js ├── changelog.md ├── generator └── generate-assets.mjs ├── index.js ├── ios ├── .xcode.env ├── MusicFree.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── MusicFreeNew.xcscheme ├── MusicFree │ ├── AppDelegate.h │ ├── AppDelegate.mm │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ ├── LaunchScreen.storyboard │ ├── PrivacyInfo.xcprivacy │ └── main.m ├── MusicFreeTests │ ├── Info.plist │ └── MusicFreeNewTests.m └── Podfile ├── jest.config.js ├── metro.config.js ├── package.json ├── readme.md ├── release └── version.json ├── src ├── assets │ ├── icons │ │ ├── alarm-outline.svg │ │ ├── album-outline.svg │ │ ├── archive-box-x-mark.svg │ │ ├── arrow-down-tray.svg │ │ ├── arrow-left.svg │ │ ├── arrow-long-left.svg │ │ ├── arrow-path.svg │ │ ├── arrow-right-end-on-rectangle.svg │ │ ├── arrow-up-tray.svg │ │ ├── arrow-uturn-left.svg │ │ ├── arrows-left-right.svg │ │ ├── bars-3.svg │ │ ├── bookmark-square.svg │ │ ├── chat-bubble-oval-left-ellipsis.svg │ │ ├── check-circle-outline.svg │ │ ├── check-circle.svg │ │ ├── check.svg │ │ ├── circle-stack.svg │ │ ├── clock-outline.svg │ │ ├── code-bracket-square.svg │ │ ├── cog-8-tooth.svg │ │ ├── document-outline.svg │ │ ├── ellipsis-vertical.svg │ │ ├── exclamation-circle.svg │ │ ├── fire-outline.svg │ │ ├── fire.svg │ │ ├── folder-music-outline.svg │ │ ├── folder-outline.svg │ │ ├── folder-plus.svg │ │ ├── font-size.svg │ │ ├── hand-thumb-up.svg │ │ ├── heart-outline.svg │ │ ├── heart.svg │ │ ├── home-outline.svg │ │ ├── identification.svg │ │ ├── inbox-arrow-down.svg │ │ ├── information-circle.svg │ │ ├── javascript.svg │ │ ├── link-slash.svg │ │ ├── link.svg │ │ ├── lyric.svg │ │ ├── magnifying-glass.svg │ │ ├── minus.svg │ │ ├── motion-play.svg │ │ ├── musical-note.svg │ │ ├── pause-circle-outline.svg │ │ ├── pause.svg │ │ ├── pencil-outline.svg │ │ ├── pencil-square.svg │ │ ├── play-circle-outline.svg │ │ ├── play-circle.svg │ │ ├── play.svg │ │ ├── playlist.svg │ │ ├── plus.svg │ │ ├── power-outline.svg │ │ ├── repeat-song-1.svg │ │ ├── repeat-song.svg │ │ ├── share.svg │ │ ├── shield-keyhole-outline.svg │ │ ├── shuffle.svg │ │ ├── skip-left.svg │ │ ├── skip-right.svg │ │ ├── sort-outline.svg │ │ ├── t-shirt-outline.svg │ │ ├── translation.svg │ │ ├── trash-outline.svg │ │ ├── trophy.svg │ │ ├── user.svg │ │ └── x-mark.svg │ ├── imgs │ │ ├── 100x.png │ │ ├── 125x.png │ │ ├── 150x.png │ │ ├── 175x.png │ │ ├── 200x.png │ │ ├── 50x.png │ │ ├── 75x.png │ │ ├── add-image.png │ │ ├── add.png │ │ ├── album-default.jpeg │ │ ├── author.jpg │ │ ├── high-quality.png │ │ ├── logo-transparent.png │ │ ├── logo.png │ │ ├── low-quality.png │ │ ├── standard-quality.png │ │ ├── super-quality.png │ │ ├── transparent-bg.png │ │ └── wechat_channel.jpg │ └── sounds │ │ └── fake-audio.mp3 ├── components │ ├── base │ │ ├── SortableFlatList.tsx │ │ ├── appBar.tsx │ │ ├── button.tsx │ │ ├── checkbox.tsx │ │ ├── chip.tsx │ │ ├── colorBlock.tsx │ │ ├── divider.tsx │ │ ├── empty.tsx │ │ ├── fab.tsx │ │ ├── fastImage.tsx │ │ ├── horizontalSafeAreaView.tsx │ │ ├── icon.tsx │ │ ├── iconButton.tsx │ │ ├── iconTextButton.tsx │ │ ├── image.tsx │ │ ├── imageBtn.tsx │ │ ├── input.tsx │ │ ├── linkText.tsx │ │ ├── listItem.tsx │ │ ├── listLoading.tsx │ │ ├── listReachEnd.tsx │ │ ├── loading.tsx │ │ ├── noPlugin.tsx │ │ ├── pageBackground.tsx │ │ ├── paragraph.tsx │ │ ├── playAllBar.tsx │ │ ├── portal.tsx │ │ ├── statusBar.tsx │ │ ├── switch.tsx │ │ ├── tag.tsx │ │ ├── textButton.tsx │ │ ├── themeText.tsx │ │ ├── toast.tsx │ │ ├── typeTag.tsx │ │ └── verticalSafeAreaView.tsx │ ├── debug │ │ └── index.tsx │ ├── dialogs │ │ ├── components │ │ │ ├── base │ │ │ │ └── index.tsx │ │ │ ├── checkStorage.tsx │ │ │ ├── downloadDialog.tsx │ │ │ ├── editSheetDetail.tsx │ │ │ ├── index.ts │ │ │ ├── loadingDialog.tsx │ │ │ ├── radioDialog.tsx │ │ │ ├── simpleDialog.tsx │ │ │ └── subscribePluginDialog.tsx │ │ ├── index.tsx │ │ └── useDialog.ts │ ├── mediaItem │ │ ├── LyricItem.tsx │ │ ├── albumItem.tsx │ │ ├── musicItem.tsx │ │ ├── sheetItem.tsx │ │ ├── titleAndTag.tsx │ │ └── topListItem.tsx │ ├── musicBar │ │ ├── index.tsx │ │ └── musicInfo.tsx │ ├── musicList │ │ └── index.tsx │ ├── musicSheetPage │ │ ├── components │ │ │ ├── header.tsx │ │ │ ├── navBar.tsx │ │ │ └── sheetMusicList.tsx │ │ └── index.tsx │ └── panels │ │ ├── base │ │ ├── panelBase.tsx │ │ ├── panelFullscreen.tsx │ │ └── panelHeader.tsx │ │ ├── index.tsx │ │ ├── types │ │ ├── addToMusicSheet.tsx │ │ ├── associateLrc.tsx │ │ ├── colorPicker.tsx │ │ ├── createMusicSheet.tsx │ │ ├── editMusicSheetInfo.tsx │ │ ├── imageViewer.tsx │ │ ├── importMusicSheet.tsx │ │ ├── index.ts │ │ ├── musicComment │ │ │ ├── comment.tsx │ │ │ ├── index.tsx │ │ │ └── useComments.ts │ │ ├── musicItemLyricOptions.tsx │ │ ├── musicItemOptions.tsx │ │ ├── musicQuality.tsx │ │ ├── playList │ │ │ ├── body.tsx │ │ │ ├── header.tsx │ │ │ └── index.tsx │ │ ├── playRate.tsx │ │ ├── searchLrc │ │ │ ├── LyricList.tsx │ │ │ ├── index.tsx │ │ │ ├── searchResultStore.ts │ │ │ └── useSearchLrc.ts │ │ ├── setFontSize.tsx │ │ ├── setLyricOffset.tsx │ │ ├── setUserVariables.tsx │ │ ├── sheetTags.tsx │ │ ├── simpleInput.tsx │ │ ├── simpleSelect.tsx │ │ └── timingClose.tsx │ │ └── usePanel.ts ├── constants │ ├── assetsConst.ts │ ├── commonConst.ts │ ├── globalStyle.ts │ ├── pathConst.ts │ ├── repeatModeConst.ts │ ├── strings.ts │ └── uiConst.ts ├── core │ ├── appMeta.ts │ ├── backup.ts │ ├── config.ts │ ├── download.ts │ ├── localMusicSheet.ts │ ├── lyricManager.ts │ ├── mediaCache.ts │ ├── mediaExtra.ts │ ├── musicHistory.ts │ ├── musicSheet │ │ ├── atoms.ts │ │ ├── ee.ts │ │ ├── index.ts │ │ ├── migrate.ts │ │ ├── sortedMusicList.ts │ │ └── storage.ts │ ├── network.ts │ ├── persistStatus.ts │ ├── pluginManager.ts │ ├── pluginMeta.ts │ ├── router │ │ ├── index.ts │ │ └── routes.tsx │ ├── theme.ts │ └── trackPlayer │ │ ├── common.ts │ │ ├── index.ts │ │ └── internal │ │ └── playList.ts ├── entry │ ├── bootstrap.ts │ ├── index.tsx │ └── useBootstrap.tsx ├── hooks │ ├── useCheckUpdate.ts │ ├── useColors.ts │ ├── useDelayFalsy.ts │ ├── useHardwareBack.ts │ ├── useLogRerender.ts │ ├── useMounted.ts │ ├── useOnceEffect.ts │ ├── useOrientation.ts │ ├── usePrimaryColor.ts │ └── useTextColor.ts ├── lib │ └── react-native-vdebug │ │ ├── index.js │ │ └── src │ │ ├── event.js │ │ ├── hoc.js │ │ ├── log.js │ │ ├── network.js │ │ ├── storage.js │ │ └── tool.js ├── native │ ├── lyricUtil │ │ └── index.ts │ ├── mp3Util │ │ └── index.ts │ └── utils │ │ └── index.ts ├── pages │ ├── albumDetail │ │ ├── hooks │ │ │ └── useAlbumMusicList.ts │ │ └── index.tsx │ ├── artistDetail │ │ ├── components │ │ │ ├── body.tsx │ │ │ ├── content │ │ │ │ ├── albumContentItem.tsx │ │ │ │ ├── index.ts │ │ │ │ └── musicContentItem.tsx │ │ │ ├── header.tsx │ │ │ └── resultList.tsx │ │ ├── hooks │ │ │ └── useQuery.ts │ │ ├── index.tsx │ │ └── store │ │ │ └── atoms.ts │ ├── downloading │ │ ├── downloadingList.tsx │ │ └── index.tsx │ ├── fileSelector │ │ ├── fileItem.tsx │ │ └── index.tsx │ ├── history │ │ └── index.tsx │ ├── home │ │ ├── components │ │ │ ├── ActionButton.tsx │ │ │ ├── drawer │ │ │ │ └── index.tsx │ │ │ ├── homeBody │ │ │ │ ├── index.tsx │ │ │ │ ├── operations.tsx │ │ │ │ └── sheets.tsx │ │ │ ├── homeBodyHorizontal │ │ │ │ ├── index.tsx │ │ │ │ └── operations.tsx │ │ │ ├── navBar.tsx │ │ │ └── operations │ │ │ │ └── index.tsx │ │ └── index.tsx │ ├── localMusic │ │ ├── index.tsx │ │ └── mainPage │ │ │ ├── index.tsx │ │ │ └── localMusicList.tsx │ ├── musicDetail │ │ ├── components │ │ │ ├── background.tsx │ │ │ ├── bottom │ │ │ │ ├── index.tsx │ │ │ │ ├── playControl.tsx │ │ │ │ └── seekBar.tsx │ │ │ ├── content │ │ │ │ ├── albumCover │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── operations.tsx │ │ │ │ ├── heartIcon │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── lyric │ │ │ │ │ ├── draggingTime.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── lyricItem.tsx │ │ │ │ │ └── lyricOperations.tsx │ │ │ └── navBar.tsx │ │ └── index.tsx │ ├── musicListEditor │ │ ├── components │ │ │ ├── body.tsx │ │ │ ├── bottom.tsx │ │ │ └── musicList.tsx │ │ ├── index.tsx │ │ └── store │ │ │ └── atom.ts │ ├── permissions │ │ └── index.tsx │ ├── pluginSheetDetail │ │ ├── hooks │ │ │ └── usePluginSheetMusicList.ts │ │ └── index.tsx │ ├── recommendSheets │ │ ├── components │ │ │ └── body │ │ │ │ ├── index.tsx │ │ │ │ ├── sheetBody.tsx │ │ │ │ └── sheetList.tsx │ │ ├── hooks │ │ │ ├── useRecommendListTags.ts │ │ │ └── useRecommendSheets.ts │ │ └── index.tsx │ ├── searchMusicList │ │ ├── index.tsx │ │ └── searchResult.tsx │ ├── searchPage │ │ ├── common │ │ │ └── historySearch.ts │ │ ├── components │ │ │ ├── historyPanel.tsx │ │ │ ├── navBar.tsx │ │ │ └── resultPanel │ │ │ │ ├── index.tsx │ │ │ │ ├── resultSubPanel.tsx │ │ │ │ ├── resultWrapper.tsx │ │ │ │ └── results │ │ │ │ ├── albumResultItem.tsx │ │ │ │ ├── artistResultItem.tsx │ │ │ │ ├── defaultResults.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── musicResultItem.tsx │ │ │ │ └── musicSheetResultItem.tsx │ │ ├── hooks │ │ │ └── useSearch.ts │ │ ├── index.tsx │ │ └── store │ │ │ └── atoms.ts │ ├── setCustomTheme │ │ ├── body.tsx │ │ └── index.tsx │ ├── setting │ │ ├── index.tsx │ │ └── settingTypes │ │ │ ├── aboutSetting.tsx │ │ │ ├── backupSetting.tsx │ │ │ ├── basicSetting.tsx │ │ │ ├── index.ts │ │ │ ├── pluginSetting │ │ │ ├── components │ │ │ │ └── pluginItem.tsx │ │ │ ├── index.tsx │ │ │ └── views │ │ │ │ ├── pluginList.tsx │ │ │ │ ├── pluginSort.tsx │ │ │ │ └── pluginSubscribe.tsx │ │ │ └── themeSetting │ │ │ ├── background.tsx │ │ │ ├── index.tsx │ │ │ ├── logoCard.tsx │ │ │ ├── mode.tsx │ │ │ └── themeCard.tsx │ ├── sheetDetail │ │ ├── components │ │ │ ├── header.tsx │ │ │ ├── navBar.tsx │ │ │ └── sheetMusicList.tsx │ │ └── index.tsx │ ├── topList │ │ ├── components │ │ │ ├── boardPanel.tsx │ │ │ ├── boardPanelWrapper.tsx │ │ │ └── topListBody.tsx │ │ ├── hooks │ │ │ └── useGetTopList.ts │ │ ├── index.tsx │ │ └── store │ │ │ └── atoms.ts │ └── topListDetail │ │ ├── hooks │ │ └── useTopListDetail.ts │ │ └── index.tsx ├── service │ └── index.ts ├── types │ ├── album.d.ts │ ├── artist.d.ts │ ├── common.d.ts │ ├── declarations.d.ts │ ├── lyric.d.ts │ ├── media.d.ts │ ├── music.d.ts │ ├── musicSheet.d.ts │ ├── musicSheetGroup.d.ts │ └── plugin.d.ts └── utils │ ├── asyncLock.ts │ ├── base64.ts │ ├── checkUpdate.ts │ ├── colorUtil.ts │ ├── delay.ts │ ├── eventBus.ts │ ├── fileUtils.ts │ ├── getOrCreateMMKV.ts │ ├── getSimilarMusic.ts │ ├── getUrlExt.ts │ ├── log.ts │ ├── lrcParser.ts │ ├── mediaIndexMap.ts │ ├── mediaItem.ts │ ├── minDistance.ts │ ├── musicIsPaused.ts │ ├── notImplementedFunction.ts │ ├── openUrl.ts │ ├── perfLogger.ts │ ├── qualities.ts │ ├── rpx.ts │ ├── safeParse.ts │ ├── safeStringify.ts │ ├── sleep.ts │ ├── stateMapper.ts │ ├── storage.ts │ ├── timeformat.ts │ ├── timingClose.ts │ ├── toast.ts │ └── trackUtils.ts ├── tsconfig.json └── yarn.lock /.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": { 4 | "type-enum": [ 5 | 2, 6 | "always", 7 | [ 8 | "ci", 9 | "chore", 10 | "docs", 11 | "feat", 12 | "fix", 13 | "perf", 14 | "refactor", 15 | "revert", 16 | "style" 17 | ] 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/lib/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@react-native', 'prettier'], 4 | overrides: [ 5 | { 6 | files: ['*.ts', '*.tsx'], 7 | rules: { 8 | '@typescript-eslint/no-shadow': 'warn', 9 | 'no-shadow': 'off', 10 | 'no-undef': 'off', 11 | 'react-hooks/exhaustive-deps': 'warn', 12 | }, 13 | }, 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_zh.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 反馈问题 3 | description: 问题反馈模板 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: "## 不要在此仓库提和具体插件有关的问题!!!" 9 | 10 | - type: checkboxes 11 | attributes: 12 | label: 提问题之前,请先确认 13 | description: "请勾选以下确认项" 14 | options: 15 | - label: "已经阅读过Q&A (https://musicfree.catcat.work/qa/mobile.html)" 16 | required: true 17 | - label: "要提出的问题与插件功能无关(类似某个插件搜索结果不全、ip被封禁等请找对应插件作者,在此仓库下提具体插件的问题将会被直接关闭)" 18 | required: true 19 | - label: "不与其他已有issue重复" 20 | required: true 21 | 22 | - type: textarea 23 | id: system_info 24 | attributes: 25 | label: 系统信息 26 | description: "请填写以下系统信息" 27 | placeholder: | 28 | 软件版本: 29 | 系统版本: 30 | 设备型号: 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | id: problem_description 36 | attributes: 37 | label: 问题描述 38 | description: "请详细描述问题现象及预期正确行为" 39 | placeholder: "例如:当执行XX操作时,出现XX现象,预期应该XX..." 40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | id: reproduction_steps 45 | attributes: 46 | label: 复现步骤 47 | description: "请按顺序描述复现步骤" 48 | placeholder: | 49 | 1. 打开应用 50 | 2. 点击XX按钮 51 | 3. ... 52 | validations: 53 | required: true 54 | 55 | - type: textarea 56 | id: screenshots_logs 57 | attributes: 58 | label: 截图 & 日志 59 | description: "请粘贴截图链接或错误日志(可拖放文件直接上传截图)" 60 | placeholder: "错误日志示例:\n[2023-01-01 12:00] ERROR: xxxx" 61 | validations: 62 | required: false 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 讨论区 4 | url: https://github.com/maotoumao/MusicFree/discussions 5 | about: 在这里讨论或寻求帮助 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_zh.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 提交新需求 3 | description: 新功能需求模板 4 | labels: ["feature"] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: 提需求之前,请先确认 9 | description: "请检查以下确认项" 10 | options: 11 | - label: "已经阅读过Q&A (https://musicfree.catcat.work/qa/mobile.html)" 12 | required: true 13 | - label: "新需求不是仅仅满足个人口味的需求" 14 | required: true 15 | - label: "新需求不与其他已有issue重复" 16 | required: true 17 | - label: "我可以在代码、测试上提供帮助" 18 | required: false 19 | 20 | - type: textarea 21 | id: feature_description 22 | attributes: 23 | label: 需求描述 24 | description: "请详细说明需求背景、使用场景和预期效果" 25 | placeholder: | 26 | 例如: 27 | - 当前存在的痛点是什么? 28 | - 希望如何解决这个问题? 29 | - 预期的使用体验是怎样的? 30 | validations: 31 | required: true 32 | 33 | - type: textarea 34 | id: attachments 35 | attributes: 36 | label: 附件信息(可选) 37 | description: "可拖放上传示意图/设计稿" 38 | placeholder: "设计稿说明或云文档链接..." 39 | validations: 40 | required: false 41 | -------------------------------------------------------------------------------- /.github/workflows/autobuild.yml: -------------------------------------------------------------------------------- 1 | name: 自动构建 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | build-android: 10 | if: "github.event.pull_request.merged == true && startsWith(github.event.pull_request.title, 'release: ')" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - env: 14 | REQBODY: ${{github.event.pull_request.body}} 15 | run: | 16 | echo "${REQBODY}" 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | 20 | - name: Setup Env 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | - name: Install Pkgs 25 | run: npm ci 26 | - name: Get Version 27 | run: node -p -e '`VERSION=${require("./package.json").version}`' >> $GITHUB_ENV 28 | - name: Build 29 | run: | 30 | cd ./android 31 | ./gradlew assembleRelease 32 | - name: Generate Changelog 33 | run: echo github.event_name 34 | manual: 35 | runs-on: ubuntu-latest 36 | if: "github.event_name == 'workflow_dispatch'" 37 | steps: 38 | - env: 39 | REQBODY: ${{github.event.pull_request.body}} 40 | run: | 41 | echo "${REQBODY}" 42 | - name: Checkout 43 | uses: actions/checkout@v3 44 | 45 | - name: Setup Env 46 | uses: actions/setup-node@v3 47 | with: 48 | node-version: 16 49 | - name: Install Pkgs 50 | run: npm ci 51 | - name: Get Version 52 | run: node -p -e '`VERSION=${require("./package.json").version}`' >> $GITHUB_ENV 53 | - name: Build 54 | run: | 55 | cd ./android 56 | ./gradlew assembleRelease 57 | 58 | -------------------------------------------------------------------------------- /.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 | **/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | .cxx/ 34 | *.keystore 35 | !debug.keystore 36 | 37 | # node.js 38 | # 39 | node_modules/ 40 | npm-debug.log 41 | yarn-error.log 42 | 43 | # fastlane 44 | # 45 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 46 | # screenshots whenever they are needed. 47 | # For more information about the recommended setup visit: 48 | # https://docs.fastlane.tools/best-practices/source-control/ 49 | 50 | **/fastlane/report.xml 51 | **/fastlane/Preview.html 52 | **/fastlane/screenshots 53 | **/fastlane/test_output 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # Ruby / CocoaPods 59 | **/Pods/ 60 | /vendor/bundle/ 61 | 62 | # Temporary files created by Metro to check the health of the file watcher 63 | .metro-health-check* 64 | 65 | # testing 66 | /coverage 67 | 68 | # Yarn 69 | .yarn/* 70 | !.yarn/patches 71 | !.yarn/plugins 72 | !.yarn/releases 73 | !.yarn/sdks 74 | !.yarn/versions 75 | 76 | 77 | keystore.properties 78 | .VSCodeCounter/ 79 | 80 | .vscode/ 81 | tmp/ 82 | scripts/ 83 | 84 | *.log 85 | # Expo 86 | .expo 87 | dist/ 88 | web-build/ -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npm run commit-lint 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint-staged 2 | -------------------------------------------------------------------------------- /.imgs/artist-detail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/.imgs/artist-detail.jpg -------------------------------------------------------------------------------- /.imgs/basic-setting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/.imgs/basic-setting.jpg -------------------------------------------------------------------------------- /.imgs/main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/.imgs/main.jpg -------------------------------------------------------------------------------- /.imgs/search-in-sheet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/.imgs/search-in-sheet.jpg -------------------------------------------------------------------------------- /.imgs/song-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/.imgs/song-cover.jpg -------------------------------------------------------------------------------- /.imgs/song-lrc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/.imgs/song-lrc.jpg -------------------------------------------------------------------------------- /.imgs/song-sheet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/.imgs/song-sheet.jpg -------------------------------------------------------------------------------- /.imgs/theme-setting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/.imgs/theme-setting.jpg -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSameLine: true, 4 | bracketSpacing: false, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | tabWidth: 4, 8 | useTabs: false, 9 | endOfLine: "auto" 10 | }; 11 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures. 7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' 8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' 9 | gem 'xcodeproj', '< 1.26.0' 10 | -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/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 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /android/app/src/main/java/fun/upup/musicfree/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package `fun`.upup.musicfree 2 | import expo.modules.ReactActivityDelegateWrapper 3 | 4 | import com.facebook.react.ReactActivity 5 | import com.facebook.react.ReactActivityDelegate 6 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 7 | import com.facebook.react.defaults.DefaultReactActivityDelegate 8 | import android.os.Bundle 9 | 10 | class MainActivity : ReactActivity() { 11 | 12 | /** 13 | * Returns the name of the main component registered from JavaScript. This is used to schedule 14 | * rendering of the component. 15 | */ 16 | override fun getMainComponentName(): String = "MusicFree" 17 | 18 | /** 19 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 20 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 21 | */ 22 | override fun createReactActivityDelegate(): ReactActivityDelegate = 23 | ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)) 24 | 25 | // https://reactnavigation.org/docs/getting-started/#installing-dependencies-into-a-bare-react-native-project 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(null); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/app/src/main/java/fun/upup/musicfree/lyricUtil/LyricUtilPackage.kt: -------------------------------------------------------------------------------- 1 | package `fun`.upup.musicfree.lyricUtil 2 | 3 | import android.view.View 4 | import com.facebook.react.ReactPackage 5 | import com.facebook.react.bridge.NativeModule 6 | import com.facebook.react.bridge.ReactApplicationContext 7 | import com.facebook.react.uimanager.ReactShadowNode 8 | import com.facebook.react.uimanager.ViewManager 9 | 10 | class LyricUtilPackage : ReactPackage { 11 | 12 | override fun createViewManagers( 13 | reactContext: ReactApplicationContext 14 | ): MutableList>> = mutableListOf() 15 | 16 | override fun createNativeModules( 17 | reactContext: ReactApplicationContext 18 | ): MutableList = listOf(LyricUtilModule(reactContext)).toMutableList() 19 | } -------------------------------------------------------------------------------- /android/app/src/main/java/fun/upup/musicfree/mp3Util/Mp3UtilPackage.kt: -------------------------------------------------------------------------------- 1 | package `fun`.upup.musicfree.mp3Util 2 | 3 | import android.view.View 4 | import com.facebook.react.ReactPackage 5 | import com.facebook.react.bridge.NativeModule 6 | import com.facebook.react.bridge.ReactApplicationContext 7 | import com.facebook.react.uimanager.ReactShadowNode 8 | import com.facebook.react.uimanager.ViewManager 9 | 10 | class Mp3UtilPackage : ReactPackage { 11 | 12 | override fun createViewManagers( 13 | reactContext: ReactApplicationContext 14 | ): MutableList>> = mutableListOf() 15 | 16 | override fun createNativeModules( 17 | reactContext: ReactApplicationContext 18 | ): MutableList = listOf(Mp3UtilModule(reactContext)).toMutableList() 19 | } -------------------------------------------------------------------------------- /android/app/src/main/java/fun/upup/musicfree/utils/UtilsPackage.kt: -------------------------------------------------------------------------------- 1 | package `fun`.upup.musicfree.utils 2 | 3 | import android.view.View 4 | import com.facebook.react.ReactPackage 5 | import com.facebook.react.bridge.NativeModule 6 | import com.facebook.react.bridge.ReactApplicationContext 7 | import com.facebook.react.uimanager.ReactShadowNode 8 | import com.facebook.react.uimanager.ViewManager 9 | 10 | class UtilsPackage : ReactPackage { 11 | 12 | override fun createViewManagers( 13 | reactContext: ReactApplicationContext 14 | ): MutableList>> = mutableListOf() 15 | 16 | override fun createNativeModules( 17 | reactContext: ReactApplicationContext 18 | ): MutableList = listOf(UtilsModule(reactContext)).toMutableList() 19 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splashscreen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/drawable/splashscreen_image.png -------------------------------------------------------------------------------- /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.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #27282C 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #27282C 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MusicFree 3 | true 4 | 5 | musicfree_temporary_channel 6 | 7 | musicfree_temporary_channel 8 | MusicFree 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | buildToolsVersion = "35.0.0" 4 | minSdkVersion = 24 5 | compileSdkVersion = 35 6 | targetSdkVersion = 30 7 | ndkVersion = "26.1.10909125" 8 | kotlinVersion = "1.9.24" 9 | } 10 | repositories { 11 | maven { url 'https://maven.aliyun.com/repository/public' } 12 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } 13 | google() 14 | mavenCentral() 15 | maven { url 'https://jitpack.io' } 16 | } 17 | dependencies { 18 | classpath("com.android.tools.build:gradle") 19 | classpath("com.facebook.react:react-native-gradle-plugin") 20 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 21 | } 22 | } 23 | apply plugin: "com.facebook.react.rootproject" 24 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/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.10.2-all.zip 4 | distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.10.2-all.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } 2 | plugins { id("com.facebook.react.settings") } 3 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> 4 | def command = [ 5 | 'node', 6 | '--no-warnings', 7 | '--eval', 8 | 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', 9 | 'react-native-config', 10 | '--json', 11 | '--platform', 12 | 'android' 13 | ].toList() 14 | ex.autolinkLibrariesFromCommand(command) 15 | } 16 | rootProject.name = 'MusicFree' 17 | include ':app' 18 | includeBuild('../node_modules/@react-native/gradle-plugin') 19 | 20 | apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle") 21 | useExpoModules() 22 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MusicFree", 3 | "displayName": "MusicFree", 4 | "expo": { 5 | "plugins": [ 6 | [ 7 | "expo-splash-screen", 8 | { 9 | "backgroundColor": "#27282C", 10 | "image": "./android/app/src/main/res/drawable/splashscreen_image.png", 11 | "imageWidth": 200 12 | } 13 | ] 14 | ], 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['babel-preset-expo'], 3 | plugins: [ 4 | [ 5 | 'module-resolver', 6 | { 7 | root: ['./'], 8 | alias: { 9 | '^@/(.+)': './src/\\1', 10 | }, 11 | }, 12 | ], 13 | 'react-native-reanimated/plugin', 14 | ], 15 | env: { 16 | production: { 17 | plugins: ['transform-remove-console'], 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import {name as appName} from './app.json'; 7 | import TrackPlayer from 'react-native-track-player'; 8 | import Pages from '@/entry'; 9 | 10 | AppRegistry.registerComponent(appName, () => Pages); 11 | TrackPlayer.registerPlaybackService(() => require('./src/service/index')); 12 | -------------------------------------------------------------------------------- /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/MusicFree/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface AppDelegate : EXAppDelegateWrapper 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /ios/MusicFree/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 8 | { 9 | self.moduleName = @"MusicFree"; 10 | // You can add your custom initial props in the dictionary below. 11 | // They will be passed down to the ViewController used by React Native. 12 | self.initialProps = @{}; 13 | 14 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 15 | } 16 | 17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 18 | { 19 | return [self bundleURL]; 20 | } 21 | 22 | - (NSURL *)bundleURL 23 | { 24 | #if DEBUG 25 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; 26 | #else 27 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 28 | #endif 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /ios/MusicFree/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ios/MusicFree/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/MusicFree/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryUserDefaults 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | CA92.1 21 | 22 | 23 | 24 | NSPrivacyAccessedAPIType 25 | NSPrivacyAccessedAPICategorySystemBootTime 26 | NSPrivacyAccessedAPITypeReasons 27 | 28 | 35F9.1 29 | 30 | 31 | 32 | NSPrivacyCollectedDataTypes 33 | 34 | NSPrivacyTracking 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/MusicFree/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ios/MusicFreeTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | }; 4 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const {getDefaultConfig} = require('expo/metro-config'); 2 | const {mergeConfig} = require('@react-native/metro-config'); 3 | 4 | /** 5 | * Reference: https://github.com/software-mansion/react-native-svg/blob/main/USAGE.md 6 | */ 7 | const defaultConfig = getDefaultConfig(__dirname); 8 | const {assetExts, sourceExts} = defaultConfig.resolver; 9 | /** 10 | * Metro configuration 11 | * https://reactnative.dev/docs/metro 12 | * 13 | * @type {import('metro-config').MetroConfig} 14 | */ 15 | const config = { 16 | transformer: { 17 | babelTransformerPath: require.resolve('react-native-svg-transformer'), 18 | }, 19 | resolver: { 20 | assetExts: assetExts.filter(ext => ext !== 'svg'), 21 | sourceExts: [...sourceExts, 'svg'], 22 | }, 23 | }; 24 | 25 | module.exports = mergeConfig(getDefaultConfig(__dirname), config); 26 | -------------------------------------------------------------------------------- /release/version.json: -------------------------------------------------------------------------------- 1 | {"version":"0.5.1","changeLog":[ 2 | "1. 【修复】修复插件开关点击无效的问题", 3 | "2. 【修复】修复开屏图片消失的问题", 4 | "3. 【优化】增加新建歌单名称的长度限制", 5 | "4. 【优化】优化插件安装失败的提示样式" 6 | ],"download":["https://r0rvr854dd1.feishu.cn/drive/folder/KLqKfWOA3lx8MKdo8xNcYpR8n7t"]} 7 | -------------------------------------------------------------------------------- /src/assets/icons/alarm-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/album-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/archive-box-x-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-down-tray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-long-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-path.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-right-end-on-rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-up-tray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-uturn-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/arrows-left-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/bars-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/bookmark-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/chat-bubble-oval-left-ellipsis.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/check-circle-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/check-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/circle-stack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/clock-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/code-bracket-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/cog-8-tooth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/document-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/ellipsis-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/exclamation-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/fire-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/fire.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/folder-music-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/folder-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/folder-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/font-size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/hand-thumb-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/heart-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/home-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/identification.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/inbox-arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/information-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/javascript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/link-slash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/lyric.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/magnifying-glass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/motion-play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/musical-note.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/pause-circle-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/pencil-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/pencil-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/play-circle-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/play-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/playlist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/power-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/repeat-song-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/repeat-song.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/shield-keyhole-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/shuffle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/skip-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/skip-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/sort-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/t-shirt-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/translation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/trash-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/trophy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/x-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/imgs/100x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/100x.png -------------------------------------------------------------------------------- /src/assets/imgs/125x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/125x.png -------------------------------------------------------------------------------- /src/assets/imgs/150x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/150x.png -------------------------------------------------------------------------------- /src/assets/imgs/175x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/175x.png -------------------------------------------------------------------------------- /src/assets/imgs/200x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/200x.png -------------------------------------------------------------------------------- /src/assets/imgs/50x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/50x.png -------------------------------------------------------------------------------- /src/assets/imgs/75x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/75x.png -------------------------------------------------------------------------------- /src/assets/imgs/add-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/add-image.png -------------------------------------------------------------------------------- /src/assets/imgs/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/add.png -------------------------------------------------------------------------------- /src/assets/imgs/album-default.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/album-default.jpeg -------------------------------------------------------------------------------- /src/assets/imgs/author.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/author.jpg -------------------------------------------------------------------------------- /src/assets/imgs/high-quality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/high-quality.png -------------------------------------------------------------------------------- /src/assets/imgs/logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/logo-transparent.png -------------------------------------------------------------------------------- /src/assets/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/logo.png -------------------------------------------------------------------------------- /src/assets/imgs/low-quality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/low-quality.png -------------------------------------------------------------------------------- /src/assets/imgs/standard-quality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/standard-quality.png -------------------------------------------------------------------------------- /src/assets/imgs/super-quality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/super-quality.png -------------------------------------------------------------------------------- /src/assets/imgs/transparent-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/transparent-bg.png -------------------------------------------------------------------------------- /src/assets/imgs/wechat_channel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/imgs/wechat_channel.jpg -------------------------------------------------------------------------------- /src/assets/sounds/fake-audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maotoumao/MusicFree/206b3656c006e1f84b8fa7f4d82fb53fe859b6bf/src/assets/sounds/fake-audio.mp3 -------------------------------------------------------------------------------- /src/components/base/button.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | GestureResponderEvent, 3 | StyleProp, 4 | StyleSheet, 5 | TouchableOpacity, 6 | ViewStyle, 7 | } from 'react-native'; 8 | import useColors from '@/hooks/useColors.ts'; 9 | import ThemeText from '@/components/base/themeText.tsx'; 10 | import React from 'react'; 11 | import rpx from '@/utils/rpx.ts'; 12 | 13 | export function Button(props: { 14 | type?: 'normal' | 'primary'; 15 | text: string; 16 | style?: StyleProp; 17 | onPress?: (evt: GestureResponderEvent) => void; 18 | }) { 19 | const {type = 'normal', text, style, onPress} = props; 20 | const colors = useColors(); 21 | 22 | return ( 23 | 34 | 35 | {text} 36 | 37 | 38 | ); 39 | } 40 | 41 | const styles = StyleSheet.create({ 42 | bottomBtn: { 43 | borderRadius: rpx(8), 44 | flexShrink: 0, 45 | justifyContent: 'center', 46 | alignItems: 'center', 47 | height: rpx(72), 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/base/colorBlock.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Image, StyleSheet, View} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import {ImgAsset} from '@/constants/assetsConst'; 5 | 6 | interface IColorBlockProps { 7 | color: string; 8 | } 9 | export default function ColorBlock(props: IColorBlockProps) { 10 | const {color} = props; 11 | 12 | return ( 13 | 14 | 19 | 27 | 28 | ); 29 | } 30 | 31 | const styles = StyleSheet.create({ 32 | showBar: { 33 | width: rpx(76), 34 | height: rpx(50), 35 | borderWidth: 1, 36 | borderStyle: 'solid', 37 | borderColor: '#ccc', 38 | }, 39 | showBarContent: { 40 | width: '100%', 41 | height: '100%', 42 | position: 'absolute', 43 | left: 0, 44 | top: 0, 45 | }, 46 | transparentBg: { 47 | position: 'absolute', 48 | zIndex: -1, 49 | width: '100%', 50 | height: '100%', 51 | left: 0, 52 | top: 0, 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /src/components/base/divider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'; 3 | import useColors from '@/hooks/useColors'; 4 | 5 | interface IDividerProps { 6 | vertical?: boolean; 7 | style?: StyleProp; 8 | } 9 | export default function Divider(props: IDividerProps) { 10 | const {vertical, style} = props; 11 | const colors = useColors(); 12 | 13 | return ( 14 | 23 | ); 24 | } 25 | 26 | const css = StyleSheet.create({ 27 | divider: { 28 | width: '100%', 29 | height: 1, 30 | }, 31 | dividerVertical: { 32 | height: '100%', 33 | width: 1, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/base/empty.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import ThemeText from './themeText'; 5 | 6 | interface IEmptyProps { 7 | content?: string; 8 | } 9 | export default function Empty(props: IEmptyProps) { 10 | return ( 11 | 12 | 13 | {props?.content ?? '什么都没有呀~'} 14 | 15 | 16 | ); 17 | } 18 | 19 | const style = StyleSheet.create({ 20 | wrapper: { 21 | width: '100%', 22 | flex: 1, 23 | minHeight: rpx(300), 24 | justifyContent: 'center', 25 | alignItems: 'center', 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/base/fab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Pressable, StyleSheet} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import useColors from '@/hooks/useColors'; 5 | import {iconSizeConst} from '@/constants/uiConst'; 6 | import Icon, {IIconName} from '@/components/base/icon.tsx'; 7 | 8 | interface IFabProps { 9 | icon?: IIconName; 10 | onPress?: () => void; 11 | } 12 | export default function Fab(props: IFabProps) { 13 | const {icon, onPress} = props; 14 | 15 | const colors = useColors(); 16 | 17 | return ( 18 | 27 | {icon ? ( 28 | 33 | ) : null} 34 | 35 | ); 36 | } 37 | 38 | const styles = StyleSheet.create({ 39 | container: { 40 | width: rpx(108), 41 | height: rpx(108), 42 | borderRadius: rpx(54), 43 | position: 'absolute', 44 | zIndex: 10010, 45 | right: rpx(36), 46 | bottom: rpx(72), 47 | justifyContent: 'center', 48 | alignItems: 'center', 49 | shadowOffset: { 50 | width: 0, 51 | height: 5, 52 | }, 53 | shadowOpacity: 0.34, 54 | shadowRadius: 6.27, 55 | 56 | elevation: 10, 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /src/components/base/fastImage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import FastImage, {FastImageProps} from 'react-native-fast-image'; 3 | 4 | interface IFastImageProps { 5 | style: FastImageProps['style']; 6 | defaultSource?: FastImageProps['defaultSource']; 7 | emptySrc?: number; 8 | uri?: string; 9 | } 10 | export default function (props: IFastImageProps) { 11 | const {style, emptySrc, uri, defaultSource} = props ?? {}; 12 | const [isError, setIsError] = useState(false); 13 | const source = uri 14 | ? { 15 | uri, 16 | } 17 | : emptySrc; 18 | 19 | useEffect(() => { 20 | setIsError(false); 21 | }, [uri]); 22 | return ( 23 | { 27 | setIsError(true); 28 | }} 29 | defaultSource={defaultSource} 30 | /> 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/base/horizontalSafeAreaView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleProp, ViewStyle} from 'react-native'; 3 | import {SafeAreaView} from 'react-native-safe-area-context'; 4 | 5 | interface IHorizontalSafeAreaViewProps { 6 | mode?: 'margin' | 'padding'; 7 | children: JSX.Element | JSX.Element[]; 8 | style?: StyleProp; 9 | } 10 | export default function HorizontalSafeAreaView( 11 | props: IHorizontalSafeAreaViewProps, 12 | ) { 13 | const {children, style, mode} = props; 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/base/iconTextButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleProp, StyleSheet, ViewStyle} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import ThemeText from './themeText'; 5 | import {iconSizeConst} from '@/constants/uiConst'; 6 | import useColors from '@/hooks/useColors'; 7 | import {TouchableOpacity} from 'react-native-gesture-handler'; 8 | import Icon, {IIconName} from '@/components/base/icon.tsx'; 9 | 10 | interface IProps { 11 | icon: IIconName; 12 | onPress?: () => void; 13 | containerStyle?: StyleProp; 14 | children?: string; 15 | } 16 | export default function (props: IProps) { 17 | const {icon, children, onPress, containerStyle} = props; 18 | const colors = useColors(); 19 | 20 | return ( 21 | 25 | 26 | 27 | {children} 28 | 29 | 30 | ); 31 | } 32 | 33 | const style = StyleSheet.create({ 34 | container: { 35 | flexDirection: 'row', 36 | alignItems: 'center', 37 | paddingHorizontal: rpx(16), 38 | paddingVertical: rpx(8), 39 | }, 40 | text: { 41 | marginLeft: rpx(8), 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/components/base/image.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Image, ImageProps} from 'react-native'; 3 | 4 | interface IImageProps extends ImageProps { 5 | uri?: string | null; 6 | emptySrc?: any; 7 | } 8 | export default function (props: Omit) { 9 | const {uri, emptySrc} = props; 10 | const source = uri 11 | ? { 12 | uri, 13 | } 14 | : emptySrc; 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/base/imageBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import Image from './image'; 5 | import {ImgAsset} from '@/constants/assetsConst'; 6 | import ThemeText from './themeText'; 7 | 8 | interface IImageBtnProps { 9 | uri?: string; 10 | title?: string; 11 | onPress?: () => void; 12 | style?: StyleProp; 13 | } 14 | export default function ImageBtn(props: IImageBtnProps) { 15 | const {onPress, uri, title, style: _style} = props ?? {}; 16 | return ( 17 | 21 | 26 | 30 | {title ?? ''} 31 | 32 | 33 | ); 34 | } 35 | 36 | const style = StyleSheet.create({ 37 | wrapper: { 38 | width: rpx(210), 39 | height: rpx(290), 40 | flexGrow: 0, 41 | flexShrink: 0, 42 | }, 43 | image: { 44 | width: rpx(210), 45 | height: rpx(210), 46 | borderRadius: rpx(12), 47 | marginBottom: rpx(16), 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/base/input.tsx: -------------------------------------------------------------------------------- 1 | import useColors from '@/hooks/useColors'; 2 | import rpx from '@/utils/rpx'; 3 | import Color from 'color'; 4 | import React from 'react'; 5 | import {StyleSheet, TextInput, TextInputProps} from 'react-native'; 6 | 7 | interface IInputProps extends TextInputProps { 8 | fontColor?: string; 9 | hasHorizontalPadding?: boolean; 10 | } 11 | 12 | export default function Input(props: IInputProps) { 13 | const {fontColor, hasHorizontalPadding = true} = props; 14 | const colors = useColors(); 15 | 16 | const currentColor = fontColor ?? colors.text; 17 | 18 | const defaultStyle = { 19 | color: currentColor, 20 | }; 21 | 22 | return ( 23 | 34 | ); 35 | } 36 | 37 | const styles = StyleSheet.create({ 38 | container: { 39 | paddingVertical: 0, 40 | paddingHorizontal: rpx(24), 41 | }, 42 | containerWithoutPadding: { 43 | padding: 0, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /src/components/base/linkText.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {GestureResponderEvent, StyleSheet, TextProps} from 'react-native'; 3 | import {fontSizeConst, fontWeightConst} from '@/constants/uiConst'; 4 | import openUrl from '@/utils/openUrl'; 5 | import ThemeText from './themeText'; 6 | import Color from 'color'; 7 | 8 | type ILinkTextProps = TextProps & { 9 | fontSize?: keyof typeof fontSizeConst; 10 | fontWeight?: keyof typeof fontWeightConst; 11 | linkTo?: string; 12 | onPress?: (event: GestureResponderEvent) => void; 13 | }; 14 | 15 | export default function LinkText(props: ILinkTextProps) { 16 | const [isPressed, setIsPressed] = useState(false); 17 | 18 | return ( 19 | { 23 | setIsPressed(true); 24 | }} 25 | onPress={evt => { 26 | if (props.onPress) { 27 | props.onPress(evt); 28 | } else { 29 | props?.linkTo && openUrl(props.linkTo); 30 | } 31 | }} 32 | onPressOut={() => { 33 | setIsPressed(false); 34 | }}> 35 | {props.children} 36 | 37 | ); 38 | } 39 | 40 | const style = StyleSheet.create({ 41 | linkText: { 42 | color: '#66ccff', 43 | textDecorationLine: 'underline', 44 | }, 45 | pressed: { 46 | color: Color('#66ccff').alpha(0.4).toString(), 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /src/components/base/listLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ActivityIndicator, StyleSheet, View} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import {fontSizeConst} from '@/constants/uiConst'; 5 | import ThemeText from './themeText'; 6 | import useColors from '@/hooks/useColors'; 7 | 8 | export default function ListLoading() { 9 | const colors = useColors(); 10 | 11 | return ( 12 | 13 | 18 | 加载中... 19 | 20 | ); 21 | } 22 | 23 | const style = StyleSheet.create({ 24 | wrapper: { 25 | width: '100%', 26 | height: rpx(140), 27 | justifyContent: 'center', 28 | alignItems: 'center', 29 | }, 30 | loadingText: { 31 | marginTop: fontSizeConst.content * 1.2, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/base/listReachEnd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import ThemeText from './themeText'; 5 | 6 | export default function ListReachEnd() { 7 | return ( 8 | 9 | 10 | ~~~ 到底啦 ~~~ 11 | 12 | 13 | ); 14 | } 15 | 16 | const style = StyleSheet.create({ 17 | wrapper: { 18 | width: '100%', 19 | flex: 1, 20 | minHeight: rpx(100), 21 | justifyContent: 'center', 22 | alignItems: 'center', 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/base/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ActivityIndicator, StyleSheet, View} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import ThemeText from './themeText'; 5 | import useColors from '@/hooks/useColors'; 6 | 7 | interface ILoadingProps { 8 | text?: string; 9 | showText?: boolean; 10 | height?: number; 11 | color?: string; 12 | } 13 | export default function Loading(props: ILoadingProps) { 14 | const colors = useColors(); 15 | const {showText = true, height, text, color} = props; 16 | 17 | return ( 18 | 19 | 20 | {showText ? ( 21 | 26 | {text ?? '加载中...'} 27 | 28 | ) : null} 29 | 30 | ); 31 | } 32 | 33 | const style = StyleSheet.create({ 34 | wrapper: { 35 | width: '100%', 36 | flex: 1, 37 | justifyContent: 'center', 38 | alignItems: 'center', 39 | }, 40 | text: { 41 | marginTop: rpx(48), 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/components/base/noPlugin.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import ThemeText from '@/components/base/themeText'; 5 | 6 | interface IProps { 7 | notSupportType?: string; 8 | } 9 | 10 | export default function NoPlugin(props: IProps) { 11 | return ( 12 | 13 | 14 | 还没有安装 15 | {props?.notSupportType 16 | ? `支持「${props.notSupportType}」功能的` 17 | : ''} 18 | 插件哦 19 | 20 | 24 | 先去 侧边栏-插件管理 里安装插件吧~ 25 | 26 | 27 | ); 28 | } 29 | 30 | const style = StyleSheet.create({ 31 | wrapper: { 32 | width: rpx(750), 33 | flex: 1, 34 | alignItems: 'center', 35 | justifyContent: 'center', 36 | }, 37 | mt: { 38 | marginTop: rpx(24), 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/base/pageBackground.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react'; 2 | import {StyleSheet, View} from 'react-native'; 3 | import Image from './image'; 4 | import useColors from '@/hooks/useColors'; 5 | import Theme from '@/core/theme'; 6 | 7 | function PageBackground() { 8 | const theme = Theme.useTheme(); 9 | const background = Theme.useBackground(); 10 | const colors = useColors(); 11 | 12 | return ( 13 | <> 14 | 23 | {!theme.id.startsWith('p-') && background?.url ? ( 24 | 34 | ) : null} 35 | 36 | ); 37 | } 38 | export default memo(PageBackground, () => true); 39 | 40 | const style = StyleSheet.create({ 41 | wrapper: { 42 | position: 'absolute', 43 | top: 0, 44 | left: 0, 45 | right: 0, 46 | bottom: 0, 47 | width: '100%', 48 | height: '100%', 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/base/paragraph.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, TextProps} from 'react-native'; 3 | import ThemeText from './themeText'; 4 | import {fontSizeConst} from '@/constants/uiConst'; 5 | 6 | interface IParagraphProps extends TextProps {} 7 | export default function Paragraph(props: IParagraphProps) { 8 | return ; 9 | } 10 | 11 | const styles = StyleSheet.create({ 12 | container: { 13 | fontSize: fontSizeConst.content, 14 | lineHeight: fontSizeConst.content * 1.8, 15 | marginVertical: 2, 16 | letterSpacing: 0.25, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/base/statusBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StatusBar, StatusBarProps, View} from 'react-native'; 3 | import useColors from '@/hooks/useColors'; 4 | 5 | interface IStatusBarProps extends StatusBarProps {} 6 | 7 | export default function (props: IStatusBarProps) { 8 | const colors = useColors(); 9 | const {backgroundColor, barStyle} = props; 10 | 11 | return ( 12 | <> 13 | 17 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/base/tag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import ThemeText from './themeText'; 5 | import useColors from '@/hooks/useColors'; 6 | 7 | interface ITagProps { 8 | tagName: string; 9 | containerStyle?: StyleProp; 10 | style?: StyleProp; 11 | } 12 | export default function Tag(props: ITagProps) { 13 | const colors = useColors(); 14 | return ( 15 | 21 | 22 | {props.tagName} 23 | 24 | 25 | ); 26 | } 27 | 28 | const styles = StyleSheet.create({ 29 | tag: { 30 | height: rpx(32), 31 | marginLeft: rpx(12), 32 | paddingHorizontal: rpx(12), 33 | borderRadius: rpx(24), 34 | justifyContent: 'center', 35 | alignItems: 'center', 36 | flexShrink: 0, 37 | borderWidth: 1, 38 | borderStyle: 'solid', 39 | }, 40 | tagText: { 41 | textAlignVertical: 'center', 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/components/base/textButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Pressable} from 'react-native'; 3 | import ThemeText from './themeText'; 4 | import rpx from '@/utils/rpx'; 5 | import {CustomizedColors} from '@/hooks/useColors'; 6 | 7 | interface IButtonProps { 8 | withHorizontalPadding?: boolean; 9 | style?: any; 10 | hitSlop?: number; 11 | children: string; 12 | fontColor?: keyof CustomizedColors; 13 | onPress?: () => void; 14 | } 15 | export default function (props: IButtonProps) { 16 | const {children, onPress, fontColor, hitSlop, withHorizontalPadding} = 17 | props; 18 | return ( 19 | 33 | {children} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/base/themeText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text, TextProps} from 'react-native'; 3 | import {fontSizeConst, fontWeightConst} from '@/constants/uiConst'; 4 | import useColors, {CustomizedColors} from '@/hooks/useColors'; 5 | 6 | type IThemeTextProps = TextProps & { 7 | color?: string; 8 | fontColor?: keyof CustomizedColors; 9 | fontSize?: keyof typeof fontSizeConst; 10 | fontWeight?: keyof typeof fontWeightConst; 11 | opacity?: number; 12 | }; 13 | 14 | export default function ThemeText(props: IThemeTextProps) { 15 | const colors = useColors(); 16 | const { 17 | style, 18 | color, 19 | children, 20 | fontSize = 'content', 21 | fontColor = 'text', 22 | fontWeight = 'regular', 23 | opacity, 24 | } = props; 25 | 26 | const themeStyle = { 27 | color: color ?? colors[fontColor], 28 | fontSize: fontSizeConst[fontSize], 29 | fontWeight: fontWeightConst[fontWeight], 30 | includeFontPadding: false, 31 | opacity, 32 | }; 33 | 34 | const _style = Array.isArray(style) 35 | ? [themeStyle, ...style] 36 | : [themeStyle, style]; 37 | 38 | return ( 39 | 40 | {children} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/base/verticalSafeAreaView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleProp, ViewStyle} from 'react-native'; 3 | import {SafeAreaView} from 'react-native-safe-area-context'; 4 | 5 | interface IVerticalSafeAreaViewProps { 6 | mode?: 'margin' | 'padding'; 7 | children: JSX.Element | JSX.Element[]; 8 | style?: StyleProp; 9 | } 10 | export default function VerticalSafeAreaView( 11 | props: IVerticalSafeAreaViewProps, 12 | ) { 13 | const {children, style, mode} = props; 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/debug/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyleSheet, View } from "react-native"; 3 | import VDebug from "@/lib/react-native-vdebug"; 4 | import Config from "@/core/config.ts"; 5 | 6 | export default function Debug() { 7 | const showDebug = Config.useConfigValue('debug.devLog'); 8 | return showDebug ? ( 9 | 10 | 11 | 12 | ) : null; 13 | } 14 | 15 | const style = StyleSheet.create({ 16 | wrapper: { 17 | position: 'absolute', 18 | top: 0, 19 | left: 0, 20 | right: 0, 21 | bottom: 0, 22 | width: '100%', 23 | height: '100%', 24 | zIndex: 999, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/dialogs/components/index.ts: -------------------------------------------------------------------------------- 1 | import DownloadDialog from './downloadDialog'; 2 | import EditSheetDetailDialog from './editSheetDetail'; 3 | import LoadingDialog from './loadingDialog'; 4 | import RadioDialog from './radioDialog'; 5 | import SimpleDialog from './simpleDialog'; 6 | import SubscribePluginDialog from './subscribePluginDialog'; 7 | import CheckStorage from '@/components/dialogs/components/checkStorage.tsx'; 8 | 9 | const dialogs = { 10 | SimpleDialog, 11 | RadioDialog, 12 | DownloadDialog, 13 | SubscribePluginDialog, 14 | LoadingDialog, 15 | EditSheetDetailDialog, 16 | CheckStorage, 17 | }; 18 | 19 | export default dialogs; 20 | 21 | export type IDialogType = typeof dialogs; 22 | export type IDialogKey = keyof IDialogType; 23 | -------------------------------------------------------------------------------- /src/components/dialogs/components/simpleDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {hideDialog} from '../useDialog'; 3 | import Dialog from './base'; 4 | 5 | interface ISimpleDialogProps { 6 | title: string; 7 | content: string | JSX.Element; 8 | okText?: string; 9 | cancelText?: string; 10 | onOk?: () => void; 11 | } 12 | export default function SimpleDialog(props: ISimpleDialogProps) { 13 | const {title, content, onOk, okText, cancelText} = props; 14 | 15 | const actions = onOk 16 | ? [ 17 | { 18 | title: cancelText ?? '取消', 19 | type: 'normal', 20 | onPress: hideDialog, 21 | }, 22 | { 23 | title: okText ?? '确认', 24 | type: 'primary', 25 | onPress() { 26 | onOk?.(); 27 | hideDialog(); 28 | }, 29 | }, 30 | ] 31 | : ([ 32 | { 33 | title: okText ?? '我知道了', 34 | type: 'primary', 35 | onPress() { 36 | hideDialog(); 37 | }, 38 | }, 39 | ] as any); 40 | 41 | return ( 42 | 43 | {title} 44 | {content} 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/dialogs/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import components from './components'; 3 | import {dialogInfoStore} from './useDialog'; 4 | import Portal from '../base/portal'; 5 | 6 | export default function () { 7 | const dialogInfoState = dialogInfoStore.useValue(); 8 | 9 | const Component = dialogInfoState.name 10 | ? components[dialogInfoState.name] 11 | : null; 12 | 13 | return ( 14 | 15 | {Component ? ( 16 | 17 | ) : null} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/dialogs/useDialog.ts: -------------------------------------------------------------------------------- 1 | import {GlobalState} from '@/utils/stateMapper'; 2 | import {useCallback} from 'react'; 3 | import {IDialogKey, IDialogType} from './components'; 4 | 5 | interface IDialogInfo { 6 | name: IDialogKey | null; 7 | payload: any; 8 | } 9 | 10 | export const dialogInfoStore = new GlobalState({ 11 | name: null, 12 | payload: null, 13 | }); 14 | 15 | export function showDialog( 16 | name: T, 17 | payload?: Parameters[0], 18 | ) { 19 | dialogInfoStore.setValue({ 20 | name, 21 | payload, 22 | }); 23 | } 24 | 25 | export function hideDialog() { 26 | dialogInfoStore.setValue({ 27 | name: null, 28 | payload: null, 29 | }); 30 | } 31 | 32 | export default function useDialog() { 33 | const showDialog = useCallback( 34 | ( 35 | name: T, 36 | payload?: Parameters[0], 37 | ) => { 38 | dialogInfoStore.setValue({ 39 | name, 40 | payload, 41 | }); 42 | }, 43 | [], 44 | ); 45 | 46 | const hideDialog = useCallback(() => { 47 | dialogInfoStore.setValue({ 48 | name: null, 49 | payload: null, 50 | }); 51 | }, []); 52 | 53 | return {showDialog, hideDialog}; 54 | } 55 | 56 | export function getCurrentDialog() { 57 | return dialogInfoStore.getValue(); 58 | } 59 | -------------------------------------------------------------------------------- /src/components/mediaItem/LyricItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ListItem from '@/components/base/listItem'; 3 | import {ImgAsset} from '@/constants/assetsConst'; 4 | import TitleAndTag from './titleAndTag'; 5 | 6 | interface IAlbumResultsProps { 7 | lyricItem: ILyric.ILyricItem; 8 | onPress?: (musicItem: ILyric.ILyricItem) => void; 9 | } 10 | export default function LyricItem(props: IAlbumResultsProps) { 11 | const {lyricItem, onPress} = props; 12 | 13 | return ( 14 | { 18 | onPress?.(lyricItem); 19 | }}> 20 | 24 | 31 | } 32 | /> 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/mediaItem/sheetItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import {ROUTE_PATH, useNavigate} from '@/core/router'; 5 | import ImageBtn from '../base/imageBtn'; 6 | 7 | interface ISheetItemProps { 8 | pluginHash: string; 9 | sheetInfo: IMusic.IMusicSheetItemBase; 10 | } 11 | 12 | const marginBottom = rpx(16); 13 | 14 | export default function SheetItem(props: ISheetItemProps) { 15 | const {sheetInfo, pluginHash} = props ?? {}; 16 | const navigate = useNavigate(); 17 | return ( 18 | 19 | { 26 | navigate(ROUTE_PATH.PLUGIN_SHEET_DETAIL, { 27 | pluginHash, 28 | sheetInfo, 29 | }); 30 | }} 31 | /> 32 | 33 | ); 34 | } 35 | const style = StyleSheet.create({ 36 | imageWrapper: { 37 | width: '100%', 38 | justifyContent: 'center', 39 | alignItems: 'center', 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /src/components/mediaItem/titleAndTag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View} from 'react-native'; 3 | import ThemeText from '../base/themeText'; 4 | import Tag from '../base/tag'; 5 | 6 | interface ITitleAndTagProps { 7 | title: string; 8 | tag?: string; 9 | } 10 | export default function TitleAndTag(props: ITitleAndTagProps) { 11 | const {title, tag} = props; 12 | return ( 13 | 14 | 15 | {title} 16 | 17 | {tag ? : null} 18 | 19 | ); 20 | } 21 | 22 | const styles = StyleSheet.create({ 23 | container: { 24 | flexDirection: 'row', 25 | alignItems: 'center', 26 | justifyContent: 'space-between', 27 | }, 28 | title: { 29 | flex: 1, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/mediaItem/topListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import {ROUTE_PATH, useNavigate} from '@/entry/router'; 3 | import ListItem from '@/components/base/listItem'; 4 | import {ImgAsset} from '@/constants/assetsConst'; 5 | import {ROUTE_PATH, useNavigate} from '@/core/router'; 6 | 7 | interface ITopListResultsProps { 8 | pluginHash: string; 9 | topListItem: IMusic.IMusicSheetItemBase; 10 | } 11 | 12 | export default function TopListItem(props: ITopListResultsProps) { 13 | const {pluginHash, topListItem} = props; 14 | const navigate = useNavigate(); 15 | 16 | return ( 17 | { 20 | navigate(ROUTE_PATH.TOP_LIST_DETAIL, { 21 | pluginHash: pluginHash, 22 | topList: topListItem, 23 | }); 24 | }}> 25 | 29 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/musicSheetPage/components/navBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {ROUTE_PATH, useNavigate} from '@/core/router'; 4 | import AppBar from '@/components/base/appBar'; 5 | 6 | interface INavBarProps { 7 | navTitle: string; 8 | musicList: IMusic.IMusicItem[] | null; 9 | } 10 | 11 | export default function (props: INavBarProps) { 12 | const navigate = useNavigate(); 13 | const {navTitle, musicList = []} = props; 14 | 15 | return ( 16 | 41 | {navTitle} 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/musicSheetPage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavBar from './components/navBar'; 3 | import MusicBar from '@/components/musicBar'; 4 | import SheetMusicList from './components/sheetMusicList'; 5 | import StatusBar from '@/components/base/statusBar'; 6 | import globalStyle from '@/constants/globalStyle'; 7 | import VerticalSafeAreaView from '../base/verticalSafeAreaView'; 8 | 9 | interface IMusicSheetPageProps { 10 | navTitle: string; 11 | sheetInfo: ICommon.WithMusicList | null; 12 | musicList?: IMusic.IMusicItem[] | null; 13 | onEndReached?: () => void; 14 | loadMore?: 'loading' | 'done' | 'idle'; 15 | // 是否可收藏 16 | canStar?: boolean; 17 | } 18 | 19 | export default function MusicSheetPage(props: IMusicSheetPageProps) { 20 | const {navTitle, sheetInfo, musicList, onEndReached, loadMore, canStar} = 21 | props; 22 | 23 | return ( 24 | 25 | 26 | 30 | { 35 | onEndReached?.(); 36 | }} 37 | loadMore={loadMore} 38 | /> 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/panels/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import panels from './types'; 3 | import {panelInfoStore} from './usePanel'; 4 | 5 | function Panels() { 6 | const panelInfoState = panelInfoStore.useValue(); 7 | 8 | const Component = panelInfoState.name ? panels[panelInfoState.name] : null; 9 | 10 | return Component ? : null; 11 | } 12 | 13 | export default React.memo(Panels, () => true); 14 | -------------------------------------------------------------------------------- /src/components/panels/types/playList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Header from './header'; 4 | import Body from './body'; 5 | import PanelBase from '../../base/panelBase'; 6 | import Divider from '@/components/base/divider'; 7 | import {vh} from '@/utils/rpx'; 8 | 9 | export default function () { 10 | return ( 11 | ( 15 | <> 16 |
17 | 18 | 19 | 20 | )} 21 | /> 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/panels/types/searchLrc/searchResultStore.ts: -------------------------------------------------------------------------------- 1 | import {RequestStateCode} from '@/constants/commonConst'; 2 | import {GlobalState} from '@/utils/stateMapper'; 3 | 4 | export interface ISearchLyricResult { 5 | data: ILyric.ILyricItem[]; 6 | state: RequestStateCode; 7 | page: number; 8 | } 9 | 10 | interface ISearchLyricStoreData { 11 | query?: string; 12 | // plugin - result 13 | data: Record; 14 | } 15 | 16 | export default new GlobalState({data: {}}); 17 | -------------------------------------------------------------------------------- /src/components/panels/usePanel.ts: -------------------------------------------------------------------------------- 1 | import {GlobalState} from '@/utils/stateMapper'; 2 | import {DeviceEventEmitter} from 'react-native'; 3 | import panels from './types'; 4 | 5 | type IPanel = typeof panels; 6 | type IPanelkeys = keyof IPanel; 7 | 8 | interface IPanelInfo { 9 | name: IPanelkeys | null; 10 | payload: any; 11 | } 12 | 13 | /** 浮层信息 */ 14 | export const panelInfoStore = new GlobalState({ 15 | name: null, 16 | payload: null, 17 | }); 18 | 19 | export function showPanel( 20 | name: T, 21 | payload?: Parameters[0], 22 | ) { 23 | if (panelInfoStore.getValue().name) { 24 | DeviceEventEmitter.emit('hidePanel', () => { 25 | panelInfoStore.setValue({ 26 | name, 27 | payload, 28 | }); 29 | }); 30 | } else { 31 | panelInfoStore.setValue({ 32 | name, 33 | payload, 34 | }); 35 | } 36 | } 37 | 38 | export function hidePanel() { 39 | DeviceEventEmitter.emit('hidePanel'); 40 | } 41 | -------------------------------------------------------------------------------- /src/constants/globalStyle.ts: -------------------------------------------------------------------------------- 1 | import {StyleSheet} from 'react-native'; 2 | 3 | const globalStyle = StyleSheet.create({ 4 | /** flex 1 */ 5 | flex1: { 6 | flex: 1, 7 | }, 8 | /** 满宽度 flex1 */ 9 | fwflex1: { 10 | width: '100%', 11 | flex: 1, 12 | }, 13 | /** row 满宽度 flex1 */ 14 | rowfwflex1: { 15 | width: '100%', 16 | flex: 1, 17 | flexDirection: 'row', 18 | }, 19 | /** 居中 */ 20 | fullCenter: { 21 | width: '100%', 22 | flex: 1, 23 | justifyContent: 'center', 24 | alignItems: 'center', 25 | }, 26 | notShrink: { 27 | flexShrink: 0, 28 | flexGrow: 0, 29 | }, 30 | grow: { 31 | flexShrink: 0, 32 | flexGrow: 1, 33 | }, 34 | } as const); 35 | 36 | export default globalStyle; 37 | -------------------------------------------------------------------------------- /src/constants/pathConst.ts: -------------------------------------------------------------------------------- 1 | import {Platform} from 'react-native'; 2 | import RNFS, {CachesDirectoryPath} from 'react-native-fs'; 3 | 4 | export const basePath = 5 | Platform.OS === 'android' 6 | ? RNFS.ExternalDirectoryPath 7 | : RNFS.DocumentDirectoryPath; 8 | 9 | export default { 10 | basePath, 11 | pluginPath: `${basePath}/plugins/`, 12 | logPath: `${basePath}/log/`, 13 | dataPath: `${basePath}/data/`, 14 | cachePath: `${basePath}/cache/`, 15 | musicCachePath: CachesDirectoryPath + '/TrackPlayer', 16 | imageCachePath: CachesDirectoryPath + '/image_manager_disk_cache', 17 | localLrcPath: `${basePath}/local_lrc/`, 18 | lrcCachePath: `${basePath}/cache/lrc/`, 19 | downloadCachePath: `${basePath}/cache/download/`, 20 | downloadPath: `${basePath}/download/`, 21 | downloadMusicPath: `${basePath}/download/music/`, 22 | mmkvPath: `${basePath}/mmkv`, 23 | mmkvCachePath: `${basePath}/cache/mmkv`, 24 | }; 25 | -------------------------------------------------------------------------------- /src/constants/repeatModeConst.ts: -------------------------------------------------------------------------------- 1 | import {MusicRepeatMode} from '@/core/trackPlayer'; 2 | 3 | export default { 4 | [MusicRepeatMode.QUEUE]: { 5 | icon: 'repeat-song-1', 6 | text: '列表循环', 7 | }, 8 | [MusicRepeatMode.SINGLE]: { 9 | icon: 'repeat-song', 10 | text: '单曲循环', 11 | }, 12 | [MusicRepeatMode.SHUFFLE]: { 13 | icon: 'shuffle', 14 | text: '随机播放', 15 | }, 16 | } as const; 17 | -------------------------------------------------------------------------------- /src/constants/strings.ts: -------------------------------------------------------------------------------- 1 | import {ResumeMode} from '@/constants/commonConst.ts'; 2 | 3 | export default { 4 | settings: { 5 | [ResumeMode.Overwrite]: '合并同名歌单', 6 | [ResumeMode.Append]: '恢复为新歌单', 7 | [ResumeMode.OverwriteDefault]: '合并默认歌单,其他歌单恢复为新歌单', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/constants/uiConst.ts: -------------------------------------------------------------------------------- 1 | import {CustomizedColors} from '@/hooks/useColors'; 2 | import rpx from '@/utils/rpx'; 3 | 4 | const fontSizeConst = { 5 | /** 标签 */ 6 | tag: rpx(20), 7 | /** 描述文本等字体 */ 8 | description: rpx(22), 9 | /** 副标题 */ 10 | subTitle: rpx(26), 11 | /** 正文字体 */ 12 | content: rpx(28), 13 | /** 标题字体 */ 14 | title: rpx(32), 15 | /** appbar的字体 */ 16 | appbar: rpx(36), 17 | }; 18 | 19 | const fontWeightConst = { 20 | regular: '400', 21 | medium: '500', 22 | semibold: '600', 23 | bold: '700', 24 | bolder: '800', 25 | } as const; 26 | 27 | const iconSizeConst = { 28 | small: rpx(30), 29 | light: rpx(36), 30 | normal: rpx(42), 31 | big: rpx(60), 32 | large: rpx(72), 33 | }; 34 | 35 | type ColorKey = 'normal' | 'secondary' | 'highlight' | 'primary'; 36 | const colorMap: Record = { 37 | normal: 'text', 38 | secondary: 'textSecondary', 39 | highlight: 'textHighlight', 40 | primary: 'primary', 41 | } as const; 42 | 43 | export {fontSizeConst, fontWeightConst, iconSizeConst, colorMap}; 44 | export type {ColorKey}; 45 | -------------------------------------------------------------------------------- /src/core/appMeta.ts: -------------------------------------------------------------------------------- 1 | import getOrCreateMMKV from '@/utils/getOrCreateMMKV'; 2 | 3 | export function getAppMeta(key: string) { 4 | const metaMMKV = getOrCreateMMKV('App.meta'); 5 | 6 | return metaMMKV.getString(key); 7 | } 8 | 9 | export function setAppMeta(key: string, value: any) { 10 | const metaMMKV = getOrCreateMMKV('App.meta'); 11 | 12 | return metaMMKV.set(key, value); 13 | } 14 | -------------------------------------------------------------------------------- /src/core/musicHistory.ts: -------------------------------------------------------------------------------- 1 | import { isSameMediaItem } from "@/utils/mediaItem"; 2 | import { GlobalState } from "@/utils/stateMapper"; 3 | import { getStorage, setStorage } from "@/utils/storage"; 4 | import Config from "./config.ts"; 5 | import { musicHistorySheetId } from "@/constants/commonConst"; 6 | 7 | const musicHistory = new GlobalState([]); 8 | 9 | async function setupMusicHistory() { 10 | const history = await getStorage(musicHistorySheetId); 11 | musicHistory.setValue(history ?? []); 12 | } 13 | 14 | async function addMusic(musicItem: IMusic.IMusicItem) { 15 | const newMusicHistory = [ 16 | musicItem, 17 | ...musicHistory 18 | .getValue() 19 | .filter(item => !isSameMediaItem(item, musicItem)), 20 | ].slice(0, Config.getConfig('basic.maxHistoryLen') ?? 50); 21 | await setStorage(musicHistorySheetId, newMusicHistory); 22 | musicHistory.setValue(newMusicHistory); 23 | } 24 | 25 | async function removeMusic(musicItem: IMusic.IMusicItem) { 26 | const newMusicHistory = musicHistory 27 | .getValue() 28 | .filter(item => !isSameMediaItem(item, musicItem)); 29 | await setStorage(musicHistorySheetId, newMusicHistory); 30 | musicHistory.setValue(newMusicHistory); 31 | } 32 | 33 | async function clearMusic() { 34 | await setStorage(musicHistorySheetId, []); 35 | musicHistory.setValue([]); 36 | } 37 | 38 | async function setHistory(newHistory: IMusic.IMusicItem[]) { 39 | await setStorage(musicHistorySheetId, newHistory); 40 | musicHistory.setValue(newHistory); 41 | } 42 | 43 | export default { 44 | setupMusicHistory, 45 | addMusic, 46 | removeMusic, 47 | clearMusic, 48 | setHistory, 49 | useMusicHistory: musicHistory.useValue, 50 | }; 51 | -------------------------------------------------------------------------------- /src/core/musicSheet/atoms.ts: -------------------------------------------------------------------------------- 1 | import {atom} from 'jotai'; 2 | import SortedMusicList from '@/core/musicSheet/sortedMusicList.ts'; 3 | 4 | export const musicSheetsBaseAtom = atom([]); 5 | 6 | export const starredMusicSheetsAtom = atom([]); 7 | 8 | // key: sheetId, value: musicList 9 | export const musicListMap = new Map(); 10 | -------------------------------------------------------------------------------- /src/core/musicSheet/ee.ts: -------------------------------------------------------------------------------- 1 | import EventBus from '@/utils/eventBus.ts'; 2 | 3 | interface IMusicSheetEvents { 4 | UpdateMusicList: { 5 | sheetId: string; 6 | updateType: 'length' | 'resort'; // 更新类型 7 | }; 8 | UpdateSheetBasic: { 9 | sheetId: string; 10 | }; 11 | } 12 | 13 | const ee = new EventBus(); 14 | 15 | export default ee; 16 | -------------------------------------------------------------------------------- /src/core/network.ts: -------------------------------------------------------------------------------- 1 | import NetInfo from '@react-native-community/netinfo'; 2 | 3 | let networkState: 'Offline' | 'Wifi' | 'Cellular'; 4 | 5 | function getState() { 6 | return networkState; 7 | } 8 | 9 | const isOffline = () => networkState === 'Offline'; 10 | 11 | const isWifi = () => networkState === 'Wifi'; 12 | 13 | const isCellular = () => networkState === 'Cellular'; 14 | 15 | const mapState = (state: any) => { 16 | if (state.type === 'none') { 17 | networkState = 'Offline'; 18 | } else if (state.type === 'wifi') { 19 | networkState = 'Wifi'; 20 | } else { 21 | networkState = 'Cellular'; 22 | } 23 | }; 24 | 25 | async function setup() { 26 | try { 27 | const state = await NetInfo.fetch(); 28 | mapState(state); 29 | } catch (e) {} 30 | 31 | NetInfo.addEventListener(state => { 32 | mapState(state); 33 | }); 34 | } 35 | 36 | const Network = { 37 | setup, 38 | getState, 39 | isOffline, 40 | isWifi, 41 | isCellular, 42 | }; 43 | 44 | export default Network; 45 | -------------------------------------------------------------------------------- /src/core/trackPlayer/common.ts: -------------------------------------------------------------------------------- 1 | export enum MusicRepeatMode { 2 | /** 随机播放 */ 3 | SHUFFLE = 'SHUFFLE', 4 | /** 列表循环 */ 5 | QUEUE = 'QUEUE', 6 | /** 单曲循环 */ 7 | SINGLE = 'SINGLE', 8 | } 9 | -------------------------------------------------------------------------------- /src/entry/useBootstrap.tsx: -------------------------------------------------------------------------------- 1 | import Config from "@/core/config.ts"; 2 | import Theme from "@/core/theme"; 3 | import useCheckUpdate from "@/hooks/useCheckUpdate.ts"; 4 | import { useListenOrientationChange } from "@/hooks/useOrientation"; 5 | import { useEffect } from "react"; 6 | import { useColorScheme } from "react-native"; 7 | 8 | export function BootstrapComp() { 9 | useListenOrientationChange(); 10 | useCheckUpdate(); 11 | 12 | const followSystem = Config.useConfigValue('theme.followSystem'); 13 | 14 | const colorScheme = useColorScheme(); 15 | 16 | useEffect(() => { 17 | if (followSystem) { 18 | console.log('trg') 19 | if (colorScheme === 'dark') { 20 | Theme.setTheme('p-dark'); 21 | } else if (colorScheme === 'light') { 22 | Theme.setTheme('p-light'); 23 | } 24 | } 25 | }, [colorScheme, followSystem]); 26 | 27 | return null; 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/useCheckUpdate.ts: -------------------------------------------------------------------------------- 1 | import { showDialog } from "@/components/dialogs/useDialog"; 2 | import PersistStatus from "@/core/persistStatus.ts"; 3 | import checkUpdate from "@/utils/checkUpdate"; 4 | import Toast from "@/utils/toast"; 5 | import { compare } from "compare-versions"; 6 | import { useEffect } from "react"; 7 | 8 | export const checkUpdateAndShowResult = ( 9 | showToast = false, 10 | checkSkip = false, 11 | ) => { 12 | checkUpdate().then(updateInfo => { 13 | if (updateInfo?.needUpdate) { 14 | const {data} = updateInfo; 15 | const skipVersion = PersistStatus.get('app.skipVersion'); 16 | console.log(skipVersion, data); 17 | if ( 18 | checkSkip && 19 | skipVersion && 20 | compare(skipVersion, data.version, '>=') 21 | ) { 22 | return; 23 | } 24 | showDialog('DownloadDialog', { 25 | version: data.version, 26 | content: data.changeLog, 27 | fromUrl: data.download[0], 28 | backUrl: data.download[1], 29 | }); 30 | } else { 31 | if (showToast) { 32 | Toast.success('当前是最新版本~'); 33 | } 34 | } 35 | }); 36 | }; 37 | 38 | export default function (callOnMount = true) { 39 | useEffect(() => { 40 | if (callOnMount) { 41 | checkUpdateAndShowResult(false, true); 42 | } 43 | }, []); 44 | 45 | return checkUpdateAndShowResult; 46 | } 47 | -------------------------------------------------------------------------------- /src/hooks/useColors.ts: -------------------------------------------------------------------------------- 1 | import {Theme, useTheme} from '@react-navigation/native'; 2 | import Color from 'color'; 3 | import {useMemo} from 'react'; 4 | 5 | type IColors = Theme['colors']; 6 | 7 | export interface CustomizedColors extends IColors { 8 | /** 普通文字 */ 9 | text: string; 10 | /** 副标题文字颜色 */ 11 | textSecondary?: string; 12 | /** 高亮文本颜色,也就是主色调 */ 13 | textHighlight?: string; 14 | /** 页面背景 */ 15 | pageBackground?: string; 16 | /** 阴影 */ 17 | shadow?: string; 18 | /** 标题栏颜色 */ 19 | appBar?: string; 20 | /** 标题栏字体颜色 */ 21 | appBarText?: string; 22 | /** 音乐栏颜色 */ 23 | musicBar?: string; 24 | /** 音乐栏字体颜色 */ 25 | musicBarText?: string; 26 | /** 分割线 */ 27 | divider?: string; 28 | /** 高亮颜色 */ 29 | listActive?: string; 30 | /** 输入框背景色 */ 31 | placeholder?: string; 32 | /** 弹窗、浮层、菜单背景色 */ 33 | backdrop?: string; 34 | /** 卡片背景色 */ 35 | card: string; 36 | /** paneltabbar 背景色 */ 37 | tabBar?: string; 38 | } 39 | 40 | export default function useColors() { 41 | const {colors} = useTheme(); 42 | 43 | const cColors: CustomizedColors = useMemo(() => { 44 | return { 45 | ...colors, 46 | textSecondary: Color(colors.text).alpha(0.7).toString(), 47 | // @ts-ignore 48 | background: colors.pageBackground ?? colors.background, 49 | }; 50 | }, [colors]); 51 | 52 | return cColors; 53 | } 54 | -------------------------------------------------------------------------------- /src/hooks/useDelayFalsy.ts: -------------------------------------------------------------------------------- 1 | import {useRef, useState} from 'react'; 2 | 3 | export default function useDelayFalsy( 4 | init?: T, 5 | ms: number = 0, 6 | ) { 7 | const [_state, _setState] = useState(init); 8 | const timer = useRef(); 9 | 10 | function setState(st: T) { 11 | if (st === undefined || st === null || st === false) { 12 | timer.current && clearTimeout(timer.current); 13 | timer.current = setTimeout(() => { 14 | _setState(st); 15 | timer.current = undefined; 16 | }, ms); 17 | return; 18 | } 19 | timer.current && clearTimeout(timer.current); 20 | timer.current = undefined; 21 | _setState(st); 22 | } 23 | 24 | return [_state, setState, _setState] as [ 25 | ...ReturnType>, 26 | ReturnType>[1], 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/useHardwareBack.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef} from 'react'; 2 | import {BackHandler, NativeEventSubscription} from 'react-native'; 3 | 4 | export default function ( 5 | onHardwareBackPress: () => boolean | null | undefined, 6 | deps: any[] = [], 7 | ) { 8 | const backHandlerRef = useRef(); 9 | useEffect(() => { 10 | if (backHandlerRef.current) { 11 | backHandlerRef.current.remove(); 12 | backHandlerRef.current = undefined; 13 | } 14 | 15 | backHandlerRef.current = BackHandler.addEventListener( 16 | 'hardwareBackPress', 17 | onHardwareBackPress, 18 | ); 19 | 20 | return () => { 21 | if (backHandlerRef.current) { 22 | backHandlerRef.current.remove(); 23 | backHandlerRef.current = undefined; 24 | } 25 | }; 26 | }, deps); 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/useLogRerender.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef} from 'react'; 2 | 3 | export default function (msg?: string, deps: any[] = []) { 4 | const idRef = useRef(); 5 | useEffect(() => { 6 | idRef.current = Math.random(); 7 | console.log('Mount', msg ?? '', idRef.current); 8 | return () => { 9 | console.log('Unmount', msg ?? '', idRef.current); 10 | }; 11 | }, []); 12 | 13 | useEffect(() => { 14 | if (deps?.length !== 0) { 15 | console.log('State Change', msg ?? '', idRef.current); 16 | } 17 | }, deps); 18 | 19 | useEffect(() => { 20 | idRef.current && console.log('Rerender: ', msg ?? '', idRef.current); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/useMounted.ts: -------------------------------------------------------------------------------- 1 | import {useCallback, useEffect, useRef, useState} from 'react'; 2 | 3 | export function useOnMounted() { 4 | const onMounted = useRef(false); 5 | const [isLoading, setLoading] = useState(true); 6 | 7 | useEffect(() => { 8 | onMounted.current = true; 9 | setTimeout(() => { 10 | setLoading(false); 11 | }); 12 | 13 | return () => { 14 | onMounted.current = false; 15 | }; 16 | }, []); 17 | 18 | return {onMounted: useCallback(() => onMounted.current, []), isLoading}; 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/useOnceEffect.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef} from 'react'; 2 | 3 | export default function useOnceEffect( 4 | cb: () => (() => void) | void, 5 | deps?: any[], 6 | ) { 7 | const flag = useRef(false); 8 | 9 | useEffect(() => { 10 | let result; 11 | if (flag.current) { 12 | return result; 13 | } 14 | if (!deps || deps.every(_ => !!_)) { 15 | flag.current = true; 16 | result = cb(); 17 | } 18 | return result; 19 | }, deps); 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/useOrientation.ts: -------------------------------------------------------------------------------- 1 | import {atom, useAtomValue, useSetAtom} from 'jotai'; 2 | import {useEffect} from 'react'; 3 | import {Dimensions} from 'react-native'; 4 | 5 | const orientationAtom = atom<'vertical' | 'horizontal'>('vertical'); 6 | 7 | export function useListenOrientationChange() { 8 | const setOrientationAtom = useSetAtom(orientationAtom); 9 | useEffect(() => { 10 | const windowSize = Dimensions.get('window'); 11 | const {width, height} = windowSize; 12 | if (width < height) { 13 | setOrientationAtom('vertical'); 14 | } else { 15 | setOrientationAtom('horizontal'); 16 | } 17 | const subscription = Dimensions.addEventListener('change', e => { 18 | if (e.window.width < e.window.height) { 19 | setOrientationAtom('vertical'); 20 | } else { 21 | setOrientationAtom('horizontal'); 22 | } 23 | }); 24 | 25 | return () => { 26 | subscription?.remove(); 27 | }; 28 | }, []); 29 | } 30 | 31 | export default function useOrientation() { 32 | return useAtomValue(orientationAtom); 33 | } 34 | -------------------------------------------------------------------------------- /src/hooks/usePrimaryColor.ts: -------------------------------------------------------------------------------- 1 | import useColors from './useColors'; 2 | 3 | export default function usePrimaryColor() { 4 | const colors = useColors(); 5 | return colors.primary; 6 | } 7 | -------------------------------------------------------------------------------- /src/hooks/useTextColor.ts: -------------------------------------------------------------------------------- 1 | import useColors from './useColors'; 2 | 3 | export default function useTextColor() { 4 | const colors = useColors(); 5 | return colors.text; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/react-native-vdebug/src/event.js: -------------------------------------------------------------------------------- 1 | export default class Event { 2 | constructor() { 3 | this.eventList = {}; 4 | } 5 | 6 | on(eventName, callback) { 7 | if (!this.eventList[eventName]) { 8 | this.eventList[eventName] = []; 9 | } 10 | this.eventList[eventName].push(callback); 11 | return this; 12 | } 13 | 14 | trigger(...args) { 15 | const key = Array.prototype.shift.call(args); 16 | const fns = this.eventList[key]; 17 | if (!fns || fns.length === 0) { 18 | return this; 19 | } 20 | for (let i = 0, fn; (fn = fns[i++]); ) { 21 | fn.apply(this, args); 22 | } 23 | return this; 24 | } 25 | 26 | off(key, fn) { 27 | const fns = this.eventList[key]; 28 | if (!fns) { 29 | return this; 30 | } 31 | if (!fn) { 32 | if (fns) { 33 | fns.length = 0; 34 | } 35 | } else { 36 | for (let i = fns.length - 1; i >= 0; i--) { 37 | const _fn = fns[i]; 38 | if (_fn === fn) { 39 | fns.splice(i, 1); 40 | } 41 | } 42 | } 43 | return this; 44 | } 45 | } 46 | let event; 47 | module.exports = (function () { 48 | if (!event) { 49 | event = new Event(); 50 | } 51 | return event; 52 | })(); 53 | -------------------------------------------------------------------------------- /src/lib/react-native-vdebug/src/hoc.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | 3 | export default (WrappedComponent, getRef = () => {}) => { 4 | return class Hoc extends PureComponent { 5 | constructor(props) { 6 | super(props); 7 | } 8 | render() { 9 | return ( 10 | { 12 | this.comp = comp; 13 | getRef && getRef(comp); 14 | }} 15 | {...this.props} 16 | /> 17 | ); 18 | } 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/react-native-vdebug/src/storage.js: -------------------------------------------------------------------------------- 1 | const storage = { 2 | support: function () { 3 | return false; 4 | }, 5 | }; 6 | 7 | export default storage; 8 | -------------------------------------------------------------------------------- /src/lib/react-native-vdebug/src/tool.js: -------------------------------------------------------------------------------- 1 | function throttle(delay, noTrailing, callback, debounceMode) { 2 | let timeoutID; 3 | let lastExec = 0; 4 | if (typeof noTrailing !== 'boolean') { 5 | debounceMode = callback; 6 | callback = noTrailing; 7 | noTrailing = undefined; 8 | } 9 | 10 | function wrapper(...args) { 11 | const self = this; 12 | const elapsed = Number(new Date()) - lastExec; 13 | 14 | function exec() { 15 | lastExec = Number(new Date()); 16 | callback.apply(self, args); 17 | } 18 | 19 | function clear() { 20 | timeoutID = undefined; 21 | } 22 | 23 | if (debounceMode && !timeoutID) { 24 | exec(); 25 | } 26 | 27 | if (timeoutID) { 28 | clearTimeout(timeoutID); 29 | } 30 | 31 | if (!debounceMode && elapsed > delay) { 32 | exec(); 33 | } else if (noTrailing !== true) { 34 | timeoutID = setTimeout( 35 | debounceMode ? clear : exec, 36 | !debounceMode ? delay - elapsed : delay, 37 | ); 38 | } 39 | } 40 | 41 | return wrapper; 42 | } 43 | 44 | function debounce(delay, atBegin, callback) { 45 | return callback === undefined 46 | ? throttle(delay, atBegin, false) 47 | : throttle(delay, callback, atBegin !== false); 48 | } 49 | 50 | function replaceReg(str) { 51 | const regStr = /\\|\$|\(|\)|\*|\+|\.|\[|\]|\?|\^|\{|\}|\|/gi; 52 | return str.replace(regStr, function (input) { 53 | return `\\${input}`; 54 | }); 55 | } 56 | 57 | module.exports = { 58 | throttle, 59 | debounce, 60 | replaceReg, 61 | }; 62 | -------------------------------------------------------------------------------- /src/native/mp3Util/index.ts: -------------------------------------------------------------------------------- 1 | import {NativeModules} from 'react-native'; 2 | 3 | export interface IBasicMeta { 4 | album?: string; 5 | artist?: string; 6 | author?: string; 7 | duration?: string; 8 | title?: string; 9 | } 10 | 11 | export interface IWritableMeta extends IBasicMeta { 12 | lyric?: string; 13 | comment?: string; 14 | } 15 | 16 | interface IMp3Util { 17 | getBasicMeta: (fileName: string) => Promise; 18 | getMediaMeta: (fileNames: string[]) => Promise; 19 | getMediaCoverImg: (mediaPath: string) => Promise; 20 | /** 读取内嵌歌词 */ 21 | getLyric: (mediaPath: string) => Promise; 22 | /** 写入meta信息 */ 23 | setMediaTag: (filePath: string, meta: IWritableMeta) => Promise; 24 | getMediaTag: (filePath: string) => Promise; 25 | } 26 | 27 | const Mp3Util = NativeModules.Mp3Util; 28 | 29 | export default Mp3Util as IMp3Util; 30 | -------------------------------------------------------------------------------- /src/native/utils/index.ts: -------------------------------------------------------------------------------- 1 | import {NativeModule, NativeModules} from 'react-native'; 2 | 3 | interface INativeUtils extends NativeModule { 4 | exitApp: () => void; 5 | checkStoragePermission: () => Promise; 6 | requestStoragePermission: () => void; 7 | } 8 | 9 | const NativeUtils = NativeModules.NativeUtils; 10 | 11 | export default NativeUtils as INativeUtils; 12 | -------------------------------------------------------------------------------- /src/pages/albumDetail/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useAlbumDetail from './hooks/useAlbumMusicList'; 3 | import {useParams} from '@/core/router'; 4 | import MusicSheetPage from '@/components/musicSheetPage'; 5 | 6 | export default function AlbumDetail() { 7 | const {albumItem: originalAlbumItem} = useParams<'album-detail'>(); 8 | const [loadMore, albumItem, musicList, getAlbumDetail] = 9 | useAlbumDetail(originalAlbumItem); 10 | 11 | return ( 12 | { 18 | getAlbumDetail(); 19 | }} 20 | /> 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/artistDetail/components/content/albumContentItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AlbumItem from '@/components/mediaItem/albumItem'; 3 | 4 | interface IAlbumContentProps { 5 | item: IAlbum.IAlbumItem; 6 | } 7 | export default function AlbumContentItem(props: IAlbumContentProps) { 8 | const {item} = props; 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/artistDetail/components/content/index.ts: -------------------------------------------------------------------------------- 1 | import AlbumContentItem from './albumContentItem'; 2 | import MusicContentItem from './musicContentItem'; 3 | 4 | const content: Record JSX.Element> = 5 | { 6 | music: MusicContentItem, 7 | album: AlbumContentItem, 8 | } as const; 9 | 10 | export default content; 11 | -------------------------------------------------------------------------------- /src/pages/artistDetail/components/content/musicContentItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MusicItem from '@/components/mediaItem/musicItem'; 3 | 4 | interface IMusicContentProps { 5 | item: IMusic.IMusicItem; 6 | } 7 | export default function MusicContentItem(props: IMusicContentProps) { 8 | const {item} = props; 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/artistDetail/store/atoms.ts: -------------------------------------------------------------------------------- 1 | import {RequestStateCode} from '@/constants/commonConst'; 2 | import {atom} from 'jotai'; 3 | 4 | export const scrollToTopAtom = atom(true); 5 | 6 | export interface IQueryResult< 7 | T extends IArtist.ArtistMediaType = IArtist.ArtistMediaType, 8 | > { 9 | state?: RequestStateCode; 10 | page?: number; 11 | data?: ICommon.SupportMediaItemBase[T]; 12 | } 13 | 14 | type IQueryResults< 15 | K extends IArtist.ArtistMediaType = IArtist.ArtistMediaType, 16 | > = { 17 | [T in K]: IQueryResult; 18 | }; 19 | 20 | export const initQueryResult: IQueryResults = { 21 | music: {}, 22 | album: {}, 23 | }; 24 | 25 | export const queryResultAtom = atom(initQueryResult); 26 | -------------------------------------------------------------------------------- /src/pages/downloading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StatusBar from '@/components/base/statusBar'; 3 | import DownloadingList from './downloadingList'; 4 | import MusicBar from '@/components/musicBar'; 5 | import VerticalSafeAreaView from '@/components/base/verticalSafeAreaView'; 6 | import globalStyle from '@/constants/globalStyle'; 7 | import AppBar from '@/components/base/appBar'; 8 | 9 | export default function Downloading() { 10 | return ( 11 | 12 | 13 | 正在下载 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/home/components/homeBody/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import globalStyle from '@/constants/globalStyle'; 3 | import Operations from './operations'; 4 | import Sheets from './sheets'; 5 | import {ScrollView} from 'react-native-gesture-handler'; 6 | 7 | export default function HomeBody() { 8 | return ( 9 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/home/components/homeBodyHorizontal/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import globalStyle from '@/constants/globalStyle'; 3 | import Operations from './operations'; 4 | import {View} from 'react-native'; 5 | import Sheets from '../homeBody/sheets'; 6 | 7 | export default function HomeBodyHorizontal() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/localMusic/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MainPage from './mainPage'; 3 | import VerticalSafeAreaView from '@/components/base/verticalSafeAreaView'; 4 | import globalStyle from '@/constants/globalStyle'; 5 | 6 | export default function LocalMusic() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/localMusic/mainPage/localMusicList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MusicList from '@/components/musicList'; 3 | import LocalMusicSheet from '@/core/localMusicSheet'; 4 | import {localMusicSheetId} from '@/constants/commonConst'; 5 | import HorizontalSafeAreaView from '@/components/base/horizontalSafeAreaView.tsx'; 6 | import globalStyle from '@/constants/globalStyle'; 7 | 8 | export default function LocalMusicList() { 9 | const musicList = LocalMusicSheet.useMusicList(); 10 | 11 | return ( 12 | 13 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/musicDetail/components/background.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Image, StyleSheet, View} from 'react-native'; 3 | import {ImgAsset} from '@/constants/assetsConst'; 4 | import TrackPlayer from '@/core/trackPlayer'; 5 | 6 | export default function Background() { 7 | const musicItem = TrackPlayer.useCurrentMusic(); 8 | const source = musicItem?.artwork 9 | ? { 10 | uri: musicItem.artwork, 11 | } 12 | : ImgAsset.albumDefault; 13 | return ( 14 | <> 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | const style = StyleSheet.create({ 22 | background: { 23 | width: '100%', 24 | height: '100%', 25 | position: 'absolute', 26 | top: 0, 27 | left: 0, 28 | right: 0, 29 | bottom: 0, 30 | backgroundColor: '#000', 31 | }, 32 | blur: { 33 | width: '100%', 34 | height: '100%', 35 | position: 'absolute', 36 | top: 0, 37 | left: 0, 38 | right: 0, 39 | bottom: 0, 40 | opacity: 0.5, 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /src/pages/musicDetail/components/bottom/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import SeekBar from './seekBar'; 5 | import PlayControl from './playControl'; 6 | import useOrientation from '@/hooks/useOrientation'; 7 | 8 | export default function Bottom() { 9 | const orientation = useOrientation(); 10 | return ( 11 | 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | const style = StyleSheet.create({ 27 | wrapper: { 28 | width: '100%', 29 | height: rpx(240), 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /src/pages/musicDetail/components/content/heartIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {iconSizeConst} from '@/constants/uiConst'; 3 | import TrackPlayer from '@/core/trackPlayer'; 4 | import Icon from '@/components/base/icon.tsx'; 5 | import MusicSheet from '@/core/musicSheet'; 6 | 7 | export default function () { 8 | const musicItem = TrackPlayer.useCurrentMusic(); 9 | 10 | const isFavorite = MusicSheet.useFavorite(musicItem); 11 | 12 | return isFavorite ? ( 13 | { 18 | if (!musicItem) { 19 | return; 20 | } 21 | MusicSheet.removeMusic(MusicSheet.defaultSheet.id, musicItem); 22 | }} 23 | /> 24 | ) : ( 25 | { 30 | if (musicItem) { 31 | MusicSheet.addMusic(MusicSheet.defaultSheet.id, musicItem); 32 | } 33 | }} 34 | /> 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/musicDetail/components/content/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { View } from "react-native"; 3 | import AlbumCover from "./albumCover"; 4 | import Lyric from "./lyric"; 5 | import useOrientation from "@/hooks/useOrientation"; 6 | import Config from "@/core/config.ts"; 7 | import globalStyle from "@/constants/globalStyle"; 8 | 9 | export default function Content() { 10 | const [tab, selectTab] = useState<'album' | 'lyric'>( 11 | Config.getConfig('basic.musicDetailDefault') || 'album', 12 | ); 13 | const orientation = useOrientation(); 14 | const showAlbumCover = tab === 'album' || orientation === 'horizontal'; 15 | 16 | const onTurnPageClick = () => { 17 | if (orientation === 'horizontal') { 18 | return; 19 | } 20 | if (tab === 'album') { 21 | selectTab('lyric'); 22 | } else { 23 | selectTab('album'); 24 | } 25 | }; 26 | 27 | return ( 28 | 29 | {showAlbumCover ? ( 30 | 31 | ) : ( 32 | 33 | )} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/musicDetail/components/content/lyric/draggingTime.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, Text} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import timeformat from '@/utils/timeformat'; 5 | import {fontSizeConst} from '@/constants/uiConst'; 6 | import TrackPlayer from '@/core/trackPlayer'; 7 | 8 | export default function DraggingTime(props: {time: number}) { 9 | const progress = TrackPlayer.useProgress(); 10 | 11 | return ( 12 | 13 | {timeformat( 14 | Math.max(Math.min(props.time, progress.duration ?? 0), 0), 15 | )} 16 | 17 | ); 18 | } 19 | 20 | const style = StyleSheet.create({ 21 | draggingTimeText: { 22 | color: '#dddddd', 23 | paddingHorizontal: rpx(8), 24 | paddingVertical: rpx(6), 25 | borderRadius: rpx(12), 26 | backgroundColor: 'rgba(255,255,255,0.1)', 27 | fontSize: fontSizeConst.description, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /src/pages/musicListEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import StatusBar from '@/components/base/statusBar'; 3 | import Bottom from './components/bottom'; 4 | import Body from './components/body'; 5 | import {useSetAtom} from 'jotai'; 6 | import {editingMusicListAtom, musicListChangedAtom} from './store/atom'; 7 | import {useParams} from '@/core/router'; 8 | import globalStyle from '@/constants/globalStyle'; 9 | import VerticalSafeAreaView from '@/components/base/verticalSafeAreaView'; 10 | import AppBar from '@/components/base/appBar'; 11 | 12 | export default function MusicListEditor() { 13 | const {musicSheet, musicList} = useParams<'music-list-editor'>(); 14 | 15 | const setEditingMusicList = useSetAtom(editingMusicListAtom); 16 | const setMusicListChanged = useSetAtom(musicListChangedAtom); 17 | 18 | useEffect(() => { 19 | setEditingMusicList( 20 | (musicList ?? []).map(_ => ({musicItem: _, checked: false})), 21 | ); 22 | return () => { 23 | setEditingMusicList([]); 24 | setMusicListChanged(false); 25 | }; 26 | }, []); 27 | 28 | return ( 29 | 30 | 31 | {musicSheet?.title ?? '歌单'} 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/musicListEditor/store/atom.ts: -------------------------------------------------------------------------------- 1 | import {atom} from 'jotai'; 2 | 3 | export interface IEditorMusicItem { 4 | musicItem: IMusic.IMusicItem; 5 | checked?: boolean; 6 | } 7 | 8 | /** 编辑页中的音乐条目 */ 9 | const editingMusicListAtom = atom([]); 10 | 11 | /** 是否变动过 */ 12 | const musicListChangedAtom = atom(false); 13 | 14 | export {editingMusicListAtom, musicListChangedAtom}; 15 | -------------------------------------------------------------------------------- /src/pages/pluginSheetDetail/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MusicSheetPage from '@/components/musicSheetPage'; 3 | import {useParams} from '@/core/router'; 4 | import usePluginSheetMusicList from './hooks/usePluginSheetMusicList'; 5 | 6 | export default function PluginSheetDetail() { 7 | const {sheetInfo} = useParams<'plugin-sheet-detail'>(); 8 | 9 | const [loadMore, sheetItem, musicList, getSheetDetail] = 10 | usePluginSheetMusicList(sheetInfo as IMusic.IMusicSheetItem); 11 | return ( 12 | { 19 | getSheetDetail(); 20 | }} 21 | /> 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/recommendSheets/hooks/useRecommendListTags.ts: -------------------------------------------------------------------------------- 1 | import PluginManager from '@/core/pluginManager'; 2 | import {useCallback, useEffect, useState} from 'react'; 3 | 4 | export default function (hash: string) { 5 | const [tags, setTags] = 6 | useState(null); 7 | 8 | const query = useCallback(async () => { 9 | const plugin = PluginManager.getByHash(hash); 10 | if (plugin) { 11 | try { 12 | const result = await plugin.methods?.getRecommendSheetTags?.(); 13 | if (!result) { 14 | throw new Error(); 15 | } 16 | setTags(result); 17 | } catch { 18 | setTags(null); 19 | } 20 | } 21 | }, []); 22 | 23 | useEffect(() => { 24 | query(); 25 | }, []); 26 | 27 | return tags; 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/recommendSheets/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import VerticalSafeAreaView from '@/components/base/verticalSafeAreaView'; 3 | import globalStyle from '@/constants/globalStyle'; 4 | import StatusBar from '@/components/base/statusBar'; 5 | import MusicBar from '@/components/musicBar'; 6 | import Body from './components/body'; 7 | import AppBar from '@/components/base/appBar'; 8 | 9 | export default function RecommendSheets() { 10 | return ( 11 | 12 | 13 | 推荐歌单 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/searchMusicList/searchResult.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MusicItem from '@/components/mediaItem/musicItem'; 3 | import Empty from '@/components/base/empty'; 4 | import {FlashList} from '@shopify/flash-list'; 5 | import rpx from '@/utils/rpx.ts'; 6 | 7 | interface ISearchResultProps { 8 | result: IMusic.IMusicItem[]; 9 | musicSheet?: IMusic.IMusicSheetItem; 10 | } 11 | 12 | const ITEM_HEIGHT = rpx(120); 13 | 14 | export default function SearchResult(props: ISearchResultProps) { 15 | const {result, musicSheet} = props; 16 | return ( 17 | } 20 | data={result} 21 | renderItem={({item}) => ( 22 | 23 | )} 24 | /> 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/searchPage/common/historySearch.ts: -------------------------------------------------------------------------------- 1 | import {getStorage, setStorage} from '@/utils/storage'; 2 | 3 | export async function getHistory() { 4 | return (await getStorage('history-search')) ?? []; 5 | } 6 | 7 | export async function addHistory(query: string) { 8 | let searchList = await getHistory(); 9 | searchList = [query].concat(searchList.filter((_: string) => _ !== query)); 10 | await setStorage('history-search', searchList); 11 | } 12 | 13 | export async function removeHistory(query: string) { 14 | let searchList = await getHistory(); 15 | searchList = searchList.filter((_: string) => _ !== query); 16 | await setStorage('history-search', searchList); 17 | } 18 | 19 | export async function removeAllHistory() { 20 | await setStorage('history-search', []); 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/searchPage/components/resultPanel/results/albumResultItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AlbumItem from '@/components/mediaItem/albumItem'; 3 | 4 | interface IAlbumResultsProps { 5 | item: IAlbum.IAlbumItem; 6 | index: number; 7 | } 8 | 9 | export default function AlbumResultItem(props: IAlbumResultsProps) { 10 | const {item: albumItem} = props; 11 | 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/searchPage/components/resultPanel/results/defaultResults.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, Text, View} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | 5 | export default function DefaultResults() { 6 | return ( 7 | 8 | 敬请期待 9 | 10 | ); 11 | } 12 | 13 | const style = StyleSheet.create({ 14 | wrapper: { 15 | width: rpx(750), 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/pages/searchPage/components/resultPanel/results/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AlbumResultItem from './albumResultItem'; 3 | import ArtistResultItem from './artistResultItem'; 4 | import MusicResultItem from './musicResultItem'; 5 | import MusicSheetResultItem from './musicSheetResultItem'; 6 | 7 | const results: Array<{ 8 | key: ICommon.SupportMediaType; 9 | title: string; 10 | component: React.FC; 11 | }> = [ 12 | { 13 | key: 'music', 14 | title: '单曲', 15 | component: MusicResultItem, 16 | }, 17 | { 18 | key: 'album', 19 | title: '专辑', 20 | component: AlbumResultItem, 21 | }, 22 | { 23 | key: 'artist', 24 | title: '作者', 25 | component: ArtistResultItem, 26 | }, 27 | { 28 | key: 'sheet', 29 | title: '歌单', 30 | component: MusicSheetResultItem, 31 | }, 32 | ]; 33 | 34 | const renderMap: Partial>> = {}; 35 | results.forEach(_ => (renderMap[_.key] = _.component)); 36 | 37 | export default results; 38 | export {renderMap}; 39 | -------------------------------------------------------------------------------- /src/pages/searchPage/components/resultPanel/results/musicResultItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MusicItem from "@/components/mediaItem/musicItem"; 3 | import Config from "@/core/config.ts"; 4 | import { ISearchResult } from "@/pages/searchPage/store/atoms"; 5 | import TrackPlayer from "@/core/trackPlayer"; 6 | 7 | interface IMusicResultsProps { 8 | item: IMusic.IMusicItem; 9 | index: number; 10 | pluginSearchResultRef: React.MutableRefObject>; 11 | } 12 | 13 | export default function MusicResultItem(props: IMusicResultsProps) { 14 | const {item: musicItem, pluginSearchResultRef} = props; 15 | 16 | return ( 17 | { 20 | const clickBehavior = Config.getConfig( 21 | 'basic.clickMusicInSearch', 22 | ); 23 | if (clickBehavior === '播放歌曲并替换播放列表') { 24 | TrackPlayer.playWithReplacePlayList( 25 | musicItem, 26 | (pluginSearchResultRef?.current?.data ?? [ 27 | musicItem, 28 | ]) as IMusic.IMusicItem[], 29 | ); 30 | } else { 31 | TrackPlayer.play(musicItem); 32 | } 33 | }} 34 | /> 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/searchPage/components/resultPanel/results/musicSheetResultItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SheetItem from '@/components/mediaItem/sheetItem'; 3 | 4 | interface IMusicSheetResultItemProps { 5 | item: IMusic.IMusicSheetItem; 6 | pluginHash: string; 7 | } 8 | export default function MusicSheetResultItem( 9 | props: IMusicSheetResultItemProps, 10 | ) { 11 | const {item, pluginHash} = props; 12 | 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/searchPage/store/atoms.ts: -------------------------------------------------------------------------------- 1 | import {RequestStateCode} from '@/constants/commonConst'; 2 | import {atom} from 'jotai'; 3 | 4 | /** 搜索状态 */ 5 | 6 | export interface ISearchResult { 7 | /** 当前页码 */ 8 | page?: number; 9 | /** 搜索词 */ 10 | query?: string; 11 | /** 搜索状态 */ 12 | state: RequestStateCode; 13 | /** 数据 */ 14 | data: ICommon.SupportMediaItemBase[T][]; 15 | } 16 | 17 | type ISearchResults< 18 | T extends keyof ICommon.SupportMediaItemBase = ICommon.SupportMediaType, 19 | > = { 20 | [K in T]: Record>; 21 | }; 22 | 23 | /** 初始值 */ 24 | export const initSearchResults: ISearchResults = { 25 | music: {}, 26 | album: {}, 27 | artist: {}, 28 | sheet: {}, 29 | lyric: {}, 30 | }; 31 | 32 | /** key: pluginhash value: searchResult */ 33 | const searchResultsAtom = atom(initSearchResults); 34 | 35 | export enum PageStatus { 36 | /** 编辑中 */ 37 | EDITING = 'EDITING', 38 | /** 搜索中 */ 39 | SEARCHING = 'SEARCHING', 40 | /** 有结果 */ 41 | RESULT = 'RESULT', 42 | /** 没有安装插件 */ 43 | NO_PLUGIN = 'NO_PLUGIN', 44 | } 45 | 46 | /** 当前正在搜索的 */ 47 | const pageStatusAtom = atom(PageStatus.EDITING); 48 | 49 | const queryAtom = atom(''); 50 | 51 | export {pageStatusAtom, searchResultsAtom, queryAtom}; 52 | -------------------------------------------------------------------------------- /src/pages/setCustomTheme/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import AppBar from '@/components/base/appBar'; 5 | import VerticalSafeAreaView from '@/components/base/verticalSafeAreaView'; 6 | import globalStyle from '@/constants/globalStyle'; 7 | import Button from '@/components/base/textButton.tsx'; 8 | import Body from './body'; 9 | import {useNavigation} from '@react-navigation/native'; 10 | 11 | export default function SetCustomTheme() { 12 | const navigation = useNavigation(); 13 | return ( 14 | 15 | { 21 | navigation.goBack(); 22 | }} 23 | fontColor="appBarText"> 24 | 完成 25 | 26 | }> 27 | 自定义背景 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | const styles = StyleSheet.create({ 35 | container: { 36 | width: rpx(750), 37 | }, 38 | submit: { 39 | justifyContent: 'center', 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /src/pages/setting/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet} from 'react-native'; 3 | import settingTypes from './settingTypes'; 4 | import {SafeAreaView} from 'react-native-safe-area-context'; 5 | import StatusBar from '@/components/base/statusBar'; 6 | import {useParams} from '@/core/router'; 7 | import HorizontalSafeAreaView from '@/components/base/horizontalSafeAreaView.tsx'; 8 | import AppBar from '@/components/base/appBar'; 9 | 10 | export default function Setting() { 11 | const {type} = useParams<'setting'>(); 12 | const settingItem = settingTypes[type]; 13 | 14 | return ( 15 | 16 | 17 | {settingItem.showNav === false ? null : ( 18 | {settingItem?.title} 19 | )} 20 | 21 | {type === 'plugin' ? ( 22 | 23 | ) : ( 24 | 25 | 26 | 27 | )} 28 | 29 | ); 30 | } 31 | 32 | const style = StyleSheet.create({ 33 | wrapper: { 34 | width: '100%', 35 | flex: 1, 36 | }, 37 | appbar: { 38 | shadowColor: 'transparent', 39 | backgroundColor: '#2b333eaa', 40 | }, 41 | header: { 42 | backgroundColor: 'transparent', 43 | shadowColor: 'transparent', 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /src/pages/setting/settingTypes/index.ts: -------------------------------------------------------------------------------- 1 | import deviceInfoModule from 'react-native-device-info'; 2 | import AboutSetting from './aboutSetting'; 3 | import BackupSetting from './backupSetting'; 4 | import BasicSetting from './basicSetting'; 5 | import PluginSetting from './pluginSetting'; 6 | import ThemeSetting from './themeSetting'; 7 | 8 | const settingTypes: Record< 9 | string, 10 | { 11 | title: string; 12 | component: (...args: any) => JSX.Element; 13 | showNav?: boolean; 14 | } 15 | > = { 16 | basic: { 17 | title: '基本设置', 18 | component: BasicSetting, 19 | }, 20 | plugin: { 21 | title: '插件管理', 22 | component: PluginSetting, 23 | showNav: false, 24 | }, 25 | theme: { 26 | title: '主题设置', 27 | component: ThemeSetting, 28 | }, 29 | backup: { 30 | title: '备份与恢复', 31 | component: BackupSetting, 32 | }, 33 | about: { 34 | title: `关于${deviceInfoModule.getApplicationName()}`, 35 | component: AboutSetting, 36 | }, 37 | }; 38 | 39 | export default settingTypes; 40 | -------------------------------------------------------------------------------- /src/pages/setting/settingTypes/pluginSetting/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; 4 | import PluginList from "./views/pluginList"; 5 | import PluginSort from "./views/pluginSort"; 6 | import PluginSubscribe from "./views/pluginSubscribe"; 7 | 8 | const Stack = createNativeStackNavigator(); 9 | 10 | const routes = [ 11 | { 12 | path: '/pluginsetting/list', 13 | component: PluginList, 14 | }, 15 | { 16 | path: '/pluginsetting/sort', 17 | component: PluginSort, 18 | }, 19 | { 20 | path: '/pluginsetting/subscribe', 21 | component: PluginSubscribe, 22 | }, 23 | ]; 24 | 25 | export default function PluginSetting() { 26 | return ( 27 | 34 | {routes.map(route => ( 35 | 40 | ))} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/pages/setting/settingTypes/themeSetting/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet} from 'react-native'; 3 | import rpx from '@/utils/rpx'; 4 | import Mode from './mode'; 5 | import Background from './background'; 6 | import {ScrollView} from 'react-native-gesture-handler'; 7 | 8 | export default function ThemeSetting() { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | const style = StyleSheet.create({ 18 | wrapper: { 19 | width: '100%', 20 | marginVertical: rpx(24), 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /src/pages/sheetDetail/components/sheetMusicList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MusicSheet from '@/core/musicSheet'; 3 | import Header from './header'; 4 | import MusicList from '@/components/musicList'; 5 | import {useParams} from '@/core/router'; 6 | import HorizontalSafeAreaView from '@/components/base/horizontalSafeAreaView.tsx'; 7 | import globalStyle from '@/constants/globalStyle'; 8 | 9 | export default function SheetMusicList() { 10 | const {id = 'favorite'} = useParams<'local-sheet-detail'>(); 11 | const musicSheet = MusicSheet.useSheetItem(id); 12 | 13 | return ( 14 | 15 | } 17 | musicList={musicSheet?.musicList} 18 | musicSheet={musicSheet} 19 | showIndex 20 | /> 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/sheetDetail/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavBar from './components/navBar'; 3 | import MusicBar from '@/components/musicBar'; 4 | import SheetMusicList from './components/sheetMusicList'; 5 | import StatusBar from '@/components/base/statusBar'; 6 | import VerticalSafeAreaView from '@/components/base/verticalSafeAreaView'; 7 | import globalStyle from '@/constants/globalStyle'; 8 | 9 | export default function SheetDetail() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/topList/components/boardPanelWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useMemo} from 'react'; 2 | import useGetTopList from '../hooks/useGetTopList'; 3 | import {useAtomValue} from 'jotai'; 4 | import {pluginsTopListAtom} from '../store/atoms'; 5 | import BoardPanel from './boardPanel'; 6 | 7 | interface IBoardPanelProps { 8 | hash: string; 9 | } 10 | export default function BoardPanelWrapper(props: IBoardPanelProps) { 11 | const {hash} = props ?? {}; 12 | const topLists = useAtomValue(pluginsTopListAtom); 13 | const getTopList = useGetTopList(); 14 | const topListData = useMemo(() => topLists[hash], [topLists]); 15 | 16 | useEffect(() => { 17 | getTopList(hash); 18 | }, []); 19 | 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/topList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TopListBody from './components/topListBody'; 3 | import MusicBar from '@/components/musicBar'; 4 | import VerticalSafeAreaView from '@/components/base/verticalSafeAreaView'; 5 | import globalStyle from '@/constants/globalStyle'; 6 | import HorizontalSafeAreaView from '@/components/base/horizontalSafeAreaView.tsx'; 7 | import AppBar from '@/components/base/appBar'; 8 | 9 | export default function TopList() { 10 | return ( 11 | 12 | 榜单 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/topList/store/atoms.ts: -------------------------------------------------------------------------------- 1 | import {RequestStateCode} from '@/constants/commonConst'; 2 | import {atom} from 'jotai'; 3 | 4 | export interface IPluginTopListResult { 5 | state: RequestStateCode; 6 | data: IMusic.IMusicSheetGroupItem[]; 7 | } 8 | 9 | const pluginsTopListAtom = atom>({}); 10 | 11 | export {pluginsTopListAtom}; 12 | -------------------------------------------------------------------------------- /src/pages/topListDetail/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useTopListDetail from './hooks/useTopListDetail'; 3 | import {useParams} from '@/core/router'; 4 | import MusicSheetPage from '@/components/musicSheetPage'; 5 | import {RequestStateCode} from '@/constants/commonConst'; 6 | 7 | export default function TopListDetail() { 8 | const {pluginHash, topList} = useParams<'top-list-detail'>(); 9 | const [topListDetail, state, loadMore] = useTopListDetail( 10 | topList, 11 | pluginHash, 12 | ); 13 | 14 | return ( 15 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /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/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/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/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/media.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace IMedia { 2 | export interface ICommentItem { 3 | id?: string; 4 | // 用户名 5 | nickName: string; 6 | // 头像 7 | avatar?: string; 8 | // 评论内容 9 | comment: string; 10 | // 点赞数 11 | like?: number; 12 | // 评论时间 13 | createAt?: number; 14 | // 地址 15 | location?: string; 16 | } 17 | 18 | export interface IComment extends ICommentItem { 19 | // 回复 20 | replies?: IComment[]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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 | [k: string]: any; 18 | } 19 | /** 歌单项 */ 20 | export interface IMusicSheetItem extends IMusicSheetItemBase { 21 | musicList: Array; 22 | } 23 | 24 | export type IMusicSheet = Array; 25 | } 26 | -------------------------------------------------------------------------------- /src/types/musicSheetGroup.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace IMusic { 2 | /** 歌单项 */ 3 | export interface IMusicSheetGroupItem { 4 | title?: string; 5 | data: Array; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/asyncLock.ts: -------------------------------------------------------------------------------- 1 | import {nanoid} from 'nanoid'; 2 | 3 | const locks = new Map(); 4 | 5 | export interface ILock { 6 | key: string; 7 | lockId: string; 8 | valid: () => boolean; 9 | release: () => void; 10 | } 11 | 12 | function requireLock(key: string): ILock { 13 | const lockId = nanoid(); 14 | locks.set(key, lockId); 15 | 16 | return { 17 | key, 18 | lockId, 19 | /** 锁是否有效 */ 20 | valid() { 21 | const currentLockId = locks.get(key); 22 | return !currentLockId || currentLockId === lockId; 23 | }, 24 | /** 释放后赋空 */ 25 | release() { 26 | const currentLockId = locks.get(key); 27 | if (currentLockId === lockId) { 28 | locks.delete(key); 29 | } 30 | }, 31 | }; 32 | } 33 | 34 | export {requireLock}; 35 | -------------------------------------------------------------------------------- /src/utils/checkUpdate.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {compare} from 'compare-versions'; 3 | import DeviceInfo from 'react-native-device-info'; 4 | 5 | const updateList = [ 6 | 'https://gitee.com/maotoumao/MusicFree/raw/master/release/version.json', 7 | 'https://raw.githubusercontent.com/maotoumao/MusicFree/master/release/version.json', 8 | 'https://cdn.jsdelivr.net/gh/maotoumao/MusicFree@master/release/version.json', 9 | ]; 10 | 11 | interface IUpdateInfo { 12 | needUpdate: boolean; 13 | data: { 14 | version: string; 15 | changeLog: string[]; 16 | download: string[]; 17 | }; 18 | } 19 | 20 | export default async function checkUpdate(): Promise { 21 | const currentVersion = DeviceInfo.getVersion(); 22 | for (let i = 0; i < updateList.length; ++i) { 23 | try { 24 | const rawInfo = (await axios.get(updateList[i])).data; 25 | if (compare(rawInfo.version, currentVersion, '>')) { 26 | return { 27 | needUpdate: true, 28 | data: rawInfo, 29 | }; 30 | } 31 | } catch {} 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/colorUtil.ts: -------------------------------------------------------------------------------- 1 | import Color from 'color'; 2 | 3 | export function grayRate(color: string | Color) { 4 | let _color = typeof color === 'string' ? Color(color) : color; 5 | 6 | return ( 7 | ((0.299 * _color.red() + 8 | 0.587 * _color.green() + 9 | 0.114 * _color.blue()) * 10 | 2 - 11 | 255) / 12 | 255 13 | ); 14 | } 15 | 16 | export function grayLevelCode(color: string | Color) { 17 | const gray = grayRate(color); 18 | console.log(gray); 19 | if (gray < 96) { 20 | return 'dark'; 21 | } else if (gray > 160) { 22 | return 'light'; 23 | } else { 24 | return 'mid'; 25 | } 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/eventBus.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | 3 | class EventBus { 4 | private ee: EventEmitter; 5 | 6 | constructor() { 7 | this.ee = new EventEmitter(); 8 | } 9 | 10 | /** 11 | * 监听 12 | * @param eventName 事件名 13 | * @param callBack 回调 14 | */ 15 | on( 16 | eventName: K, 17 | callBack: (payload: T[K]) => void, 18 | ) { 19 | this.ee.on(eventName, callBack); 20 | } 21 | 22 | once( 23 | eventName: K, 24 | callBack: (payload: T[K]) => void, 25 | ) { 26 | this.ee.once(eventName, callBack); 27 | } 28 | 29 | emit( 30 | eventName: K, 31 | payload?: T[K], 32 | ) { 33 | this.ee.emit(eventName, payload); 34 | } 35 | 36 | off( 37 | eventName: K, 38 | callBack: (payload: T[K]) => void, 39 | ) { 40 | this.ee.off(eventName, callBack); 41 | } 42 | } 43 | 44 | export default EventBus; 45 | -------------------------------------------------------------------------------- /src/utils/getOrCreateMMKV.ts: -------------------------------------------------------------------------------- 1 | import pathConst from '@/constants/pathConst'; 2 | import {MMKV} from 'react-native-mmkv'; 3 | 4 | const _mmkvCache: Record = {}; 5 | 6 | // @ts-ignore; 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/utils/getUrlExt.ts: -------------------------------------------------------------------------------- 1 | import path from 'path-browserify'; 2 | 3 | export default function getUrlExt(url?: string) { 4 | if (!url) { 5 | return; 6 | } 7 | const ext = path.extname(url); 8 | 9 | const extraTag = ext.indexOf('?'); 10 | 11 | if (ext) { 12 | if (extraTag !== -1) { 13 | return ext.slice(0, extraTag); 14 | } else { 15 | return ext; 16 | } 17 | } 18 | return url; 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/mediaIndexMap.ts: -------------------------------------------------------------------------------- 1 | export interface IIndexMap { 2 | getIndexMap: () => Record>; 3 | getIndex: (mediaItem: 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/minDistance.ts: -------------------------------------------------------------------------------- 1 | function makeMatrix(row: number, col: number) { 2 | return Array(row) 3 | .fill(0) 4 | .map(_ => Array(col).fill(Infinity)); 5 | } 6 | 7 | export default function minDistance(word1?: string, word2?: string): number { 8 | if (!word1 || !word2) { 9 | return word1?.length || word2?.length || 0; 10 | } 11 | 12 | const dp = makeMatrix(word1.length + 1, word2.length + 1); 13 | 14 | for (let i = 0; i <= word1.length; ++i) { 15 | for (let j = 0; j <= word2.length; ++j) { 16 | if (i === 0 || j === 0) { 17 | dp[i][j] = i || j; 18 | continue; 19 | } 20 | const currentStr1 = word1[i - 1]; 21 | const currentStr2 = word2[j - 1]; 22 | if (currentStr1 === currentStr2) { 23 | dp[i][j] = Math.min( 24 | dp[i - 1][j - 1], 25 | dp[i - 1][j] + 1, 26 | dp[i][j - 1] + 1, 27 | ); 28 | } else { 29 | dp[i][j] = Math.min( 30 | dp[i - 1][j - 1] + 1, 31 | dp[i - 1][j] + 1, 32 | dp[i][j - 1] + 1, 33 | ); 34 | } 35 | } 36 | } 37 | 38 | return dp[word1.length][word2.length]; 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/musicIsPaused.ts: -------------------------------------------------------------------------------- 1 | import {State} from 'react-native-track-player'; 2 | 3 | export default (state: State | undefined) => state !== State.Playing; 4 | -------------------------------------------------------------------------------- /src/utils/notImplementedFunction.ts: -------------------------------------------------------------------------------- 1 | export default function notImplementedFunction() { 2 | // Not implemented 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/openUrl.ts: -------------------------------------------------------------------------------- 1 | import {Linking} from 'react-native'; 2 | import Toast from './toast'; 3 | 4 | export default async function (url: string) { 5 | try { 6 | await Linking.canOpenURL(url); 7 | return Linking.openURL(url); 8 | } catch { 9 | Toast.warn('无法打开链接'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/perfLogger.ts: -------------------------------------------------------------------------------- 1 | export function perfLogger() { 2 | const s = Date.now(); 3 | 4 | return { 5 | mark(label?: string) { 6 | console.log(`[${label || 'log'}] ${Date.now() - s}ms`); 7 | }, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/qualities.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 音质相关的所有工具代码 3 | */ 4 | 5 | export const qualityKeys: IMusic.IQualityKey[] = [ 6 | 'low', 7 | 'standard', 8 | 'high', 9 | 'super', 10 | ]; 11 | 12 | export const qualityText = { 13 | low: '低音质', 14 | standard: '标准音质', 15 | high: '高音质', 16 | super: '超高音质', 17 | }; 18 | 19 | /** 获取音质顺序 */ 20 | export function getQualityOrder( 21 | qualityKey: IMusic.IQualityKey, 22 | sort: 'asc' | 'desc', 23 | ) { 24 | const idx = qualityKeys.indexOf(qualityKey); 25 | const left = qualityKeys.slice(0, idx); 26 | const right = qualityKeys.slice(idx + 1); 27 | if (sort === 'asc') { 28 | /** 优先高音质 */ 29 | return [qualityKey, ...right, ...left.reverse()]; 30 | } else { 31 | /** 优先低音质 */ 32 | return [qualityKey, ...left.reverse(), ...right]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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/safeStringify.ts: -------------------------------------------------------------------------------- 1 | export default function (raw: any): string { 2 | try { 3 | return JSON.stringify(raw); 4 | } catch { 5 | return ''; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Sleep是用的settimeout,delay用的是backgroundtimer 3 | * @param ms 4 | */ 5 | export default function sleep(ms = 200) { 6 | return new Promise(resolve => { 7 | setTimeout(() => { 8 | resolve(); 9 | }, ms); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /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/storage.ts: -------------------------------------------------------------------------------- 1 | import {errorLog} from '@/utils/log'; 2 | import AsyncStorage from '@react-native-async-storage/async-storage'; 3 | 4 | export async function setStorage(key: string, value: any) { 5 | try { 6 | await AsyncStorage.setItem(key, JSON.stringify(value, null, '')); 7 | } catch (e: any) { 8 | errorLog(`存储失败${key}`, e?.message); 9 | } 10 | } 11 | 12 | export async function getStorage(key: string) { 13 | try { 14 | const result = await AsyncStorage.getItem(key); 15 | if (result) { 16 | return JSON.parse(result); 17 | } 18 | } catch {} 19 | return null; 20 | } 21 | 22 | export async function getMultiStorage(keys: string[]) { 23 | if (keys.length === 0) { 24 | return []; 25 | } 26 | const result = await AsyncStorage.multiGet(keys); 27 | 28 | return result.map(_ => { 29 | try { 30 | if (_[1]) { 31 | return JSON.parse(_[1]); 32 | } 33 | return null; 34 | } catch { 35 | return null; 36 | } 37 | }); 38 | } 39 | 40 | export async function removeStorage(key: string) { 41 | return AsyncStorage.removeItem(key); 42 | } 43 | -------------------------------------------------------------------------------- /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/toast.ts: -------------------------------------------------------------------------------- 1 | import {IToastConfig, showToast} from '@/components/base/toast'; 2 | 3 | function success(message: string, config?: IToastConfig) { 4 | showToast({ 5 | message, 6 | ...config, 7 | type: 'success', 8 | }); 9 | } 10 | 11 | function warn(message: string, config?: IToastConfig) { 12 | showToast({ 13 | message, 14 | ...config, 15 | type: 'warn', 16 | }); 17 | } 18 | 19 | const Toast = { 20 | success, 21 | warn, 22 | }; 23 | 24 | export default Toast; 25 | -------------------------------------------------------------------------------- /src/utils/trackUtils.ts: -------------------------------------------------------------------------------- 1 | import {State} from 'react-native-track-player'; 2 | 3 | /** 4 | * 音乐是否处于停止状态 5 | * @param state 6 | * @returns 7 | */ 8 | export const musicIsPaused = (state: State | undefined) => 9 | state !== State.Playing; 10 | 11 | /** 12 | * 音乐是否处于缓冲中状态 13 | * @param state 14 | * @returns 15 | */ 16 | export const musicIsBuffering = (state: State | undefined) => 17 | state === State.Loading || state === State.Buffering; 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@react-native/typescript-config/tsconfig.json", 3 | "compilerOptions": { 4 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 5 | 6 | /* Completeness */ 7 | "noImplicitAny": false, 8 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | }, 13 | "types": ["node"] 14 | } 15 | } 16 | --------------------------------------------------------------------------------