├── .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 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-down-tray.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-long-left.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-path.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-right-end-on-rectangle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-up-tray.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-uturn-left.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/arrows-left-right.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/bars-3.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/bookmark-square.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/chat-bubble-oval-left-ellipsis.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/check-circle-outline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/check-circle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/check.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/circle-stack.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/clock-outline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/code-bracket-square.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/cog-8-tooth.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/document-outline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/ellipsis-vertical.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/exclamation-circle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/fire-outline.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/fire.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/folder-music-outline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/folder-outline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/folder-plus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/font-size.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/hand-thumb-up.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/heart-outline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/heart.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/home-outline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/identification.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/inbox-arrow-down.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/information-circle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/javascript.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/link-slash.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/link.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/lyric.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/magnifying-glass.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/minus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/motion-play.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/icons/musical-note.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/pause-circle-outline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/pause.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/pencil-outline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/pencil-square.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/play-circle-outline.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/play-circle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/play.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/playlist.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/plus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/power-outline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/repeat-song-1.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/repeat-song.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/assets/icons/share.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/shield-keyhole-outline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/shuffle.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/skip-left.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/skip-right.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/sort-outline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/t-shirt-outline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/translation.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/trash-outline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/trophy.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/user.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/x-mark.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
--------------------------------------------------------------------------------