first commit
6
.buckconfig
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
[android]
|
||||
target = Google Inc.:Google APIs:23
|
||||
|
||||
[maven_repositories]
|
||||
central = https://repo1.maven.org/maven2
|
13
.editorconfig
Normal file
@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Windows files
|
||||
[*.bat]
|
||||
end_of_line = crlf
|
26
.eslintrc.js
Normal file
@ -0,0 +1,26 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'standard',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
plugins: ['html', 'react'],
|
||||
parser: 'babel-eslint',
|
||||
rules: {
|
||||
'no-new': 'off',
|
||||
camelcase: 'off',
|
||||
'no-return-assign': 'off',
|
||||
'space-before-function-paren': ['error', 'never'],
|
||||
'no-var': 'error',
|
||||
'no-fallthrough': 'off',
|
||||
'prefer-promise-reject-errors': 'off',
|
||||
eqeqeq: 'off',
|
||||
'no-multiple-empty-lines': [1, { max: 2 }],
|
||||
'comma-dangle': [2, 'always-multiline'],
|
||||
'react/jsx-uses-react': 'error',
|
||||
'react/jsx-uses-vars': 'error',
|
||||
'prefer-const': 'off',
|
||||
},
|
||||
settings: {
|
||||
'html/html-extensions': ['.jsx'],
|
||||
},
|
||||
}
|
66
.flowconfig
Normal file
@ -0,0 +1,66 @@
|
||||
[ignore]
|
||||
; We fork some components by platform
|
||||
.*/*[.]android.js
|
||||
|
||||
; Ignore "BUCK" generated dirs
|
||||
<PROJECT_ROOT>/\.buckd/
|
||||
|
||||
; Ignore polyfills
|
||||
node_modules/react-native/Libraries/polyfills/.*
|
||||
|
||||
; Flow doesn't support platforms
|
||||
.*/Libraries/Utilities/LoadingView.js
|
||||
|
||||
[untyped]
|
||||
.*/node_modules/@react-native-community/cli/.*/.*
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
node_modules/react-native/interface.js
|
||||
node_modules/react-native/flow/
|
||||
|
||||
[options]
|
||||
emoji=true
|
||||
|
||||
esproposal.optional_chaining=enable
|
||||
esproposal.nullish_coalescing=enable
|
||||
|
||||
exact_by_default=true
|
||||
|
||||
module.file_ext=.js
|
||||
module.file_ext=.json
|
||||
module.file_ext=.ios.js
|
||||
|
||||
munge_underscores=true
|
||||
|
||||
module.name_mapper='^react-native/\(.*\)$' -> '<PROJECT_ROOT>/node_modules/react-native/\1'
|
||||
module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '<PROJECT_ROOT>/node_modules/react-native/Libraries/Image/RelativeImageStub'
|
||||
|
||||
suppress_type=$FlowIssue
|
||||
suppress_type=$FlowFixMe
|
||||
suppress_type=$FlowFixMeProps
|
||||
suppress_type=$FlowFixMeState
|
||||
|
||||
[lints]
|
||||
sketchy-null-number=warn
|
||||
sketchy-null-mixed=warn
|
||||
sketchy-number=warn
|
||||
untyped-type-import=warn
|
||||
nonstrict-import=warn
|
||||
deprecated-type=warn
|
||||
unsafe-getters-setters=warn
|
||||
unnecessary-invariant=warn
|
||||
signature-verification-failure=warn
|
||||
|
||||
[strict]
|
||||
deprecated-type
|
||||
nonstrict-import
|
||||
sketchy-null
|
||||
unclear-type
|
||||
unsafe-getters-setters
|
||||
untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.137.0
|
3
.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Windows files should use crlf line endings
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
*.bat text eol=crlf
|
25
.github/ISSUE_TEMPLATE/----.md
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
name: 功能请求(请先查看常见问题及搜索issue列表中有无你要提的问题)
|
||||
about: 为这个项目提出一个想法
|
||||
title: 例如:添加xxx功能、优化xxx功能
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**解决方案检查**
|
||||
<!-- 请确保你已从以下渠道寻找过解决方案,然后将 [ ] 替换成 [x] -->
|
||||
- [ ] 我已阅读常见问题(<https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md>)
|
||||
- [ ] 我已搜索issue列表(<https://github.com/lyswhut/lx-music-desktop/issues?utf8=✓&q=>)
|
||||
|
||||
**描述您想要的解决方案**
|
||||
<!-- 简洁明了地描述您要发生的事情。 -->
|
||||
|
||||
|
||||
**描述您考虑过的替代方案**
|
||||
<!-- 对您考虑过的所有替代解决方案或功能的简洁明了的描述。 -->
|
||||
|
||||
|
||||
**其他内容**
|
||||
<!-- 在此处添加有关功能请求的任何其他上下文或屏幕截图(直接把图片拖到编辑框即可添加图片)。 -->
|
||||
|
42
.github/ISSUE_TEMPLATE/--bug.md
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
name: 报告Bug(请先查看常见问题及搜索issue列表中有无你要提的问题)
|
||||
about: 创建报告以帮助我们改进
|
||||
title: 例如:音乐无法播放
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**解决方案检查**
|
||||
<!-- 请确保你已从以下渠道寻找过解决方案,然后将 [ ] 替换成 [x] -->
|
||||
- [ ] 我已阅读常见问题(<https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md>)
|
||||
- [ ] 我已搜索issue列表(<https://github.com/lyswhut/lx-music-desktop/issues?utf8=✓&q=>)
|
||||
|
||||
**描述错误**
|
||||
<!-- 清楚简洁地说明错误是什么。 -->
|
||||
|
||||
|
||||
**重现**
|
||||
重现行为的步骤:
|
||||
1.转到“ ...”
|
||||
2.点击“ ....”
|
||||
3.向下滚动到“ ....”
|
||||
4.看到错误
|
||||
|
||||
|
||||
**预期行为**
|
||||
<!-- 对您期望发生的事情的简洁明了的描述。 -->
|
||||
|
||||
|
||||
**截图**
|
||||
<!-- 如果适用,请添加屏幕截图以帮助解释您的问题(直接把图片拖到编辑框即可添加图片)。 -->
|
||||
|
||||
|
||||
**环境:**
|
||||
-操作系统及版本:[例如:Windows 10 64位 18362.156]
|
||||
-软件安装包及版本:[例如:1.0.0 安装版]
|
||||
|
||||
|
||||
**其他内容**
|
||||
<!-- 在此处添加有关该问题的任何其他上下文。 -->
|
||||
|
80
.github/workflows/beta-pack.yml
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
name: Build Beta
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- beta
|
||||
|
||||
jobs:
|
||||
Android:
|
||||
name: Android
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: Cache Gradle Wrapper
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Cache Gradle Dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-caches-
|
||||
|
||||
- name: Cache Node Dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-caches-${{ hashFiles('**/yarn-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-caches-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Build Packages
|
||||
run: |
|
||||
cd android
|
||||
echo ${{ secrets.KEYSTORE_STORE_FILE_BASE64 }} | base64 --decode > app/${{ secrets.KEYSTORE_STORE_FILE }}
|
||||
./gradlew assembleRelease -PMYAPP_UPLOAD_STORE_FILE='${{ secrets.KEYSTORE_STORE_FILE }}' -PMYAPP_UPLOAD_KEY_ALIAS='${{ secrets.KEYSTORE_KEY_ALIAS }}' -PMYAPP_UPLOAD_STORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}' -PMYAPP_UPLOAD_KEY_PASSWORD='${{ secrets.KEYSTORE_KEY_PASSWORD }}'
|
||||
|
||||
- name: Upload Artifact arm64-v8a
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-arm64-v8a-release
|
||||
path: android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload Artifact armeabi-v7a
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-armeabi-v7a-release
|
||||
path: android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload Artifact universal
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-universal-release
|
||||
path: android/app/build/outputs/apk/release/app-universal-release.apk
|
||||
|
||||
- name: Upload Artifact x86_64
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-x86_64-release
|
||||
path: android/app/build/outputs/apk/release/app-x86_64-release.apk
|
||||
|
||||
- name: Upload Artifact x86
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-x86-release
|
||||
path: android/app/build/outputs/apk/release/app-x86-release.apk
|
109
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,109 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
Android:
|
||||
name: Android
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: Cache Gradle Wrapper
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Cache Gradle Dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-caches-
|
||||
|
||||
- name: Cache Node Dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-caches-${{ hashFiles('**/yarn-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-caches-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Build Packages
|
||||
run: |
|
||||
cd android
|
||||
echo ${{ secrets.KEYSTORE_STORE_FILE_BASE64 }} | base64 --decode > app/${{ secrets.KEYSTORE_STORE_FILE }}
|
||||
./gradlew assembleRelease -PMYAPP_UPLOAD_STORE_FILE='${{ secrets.KEYSTORE_STORE_FILE }}' -PMYAPP_UPLOAD_KEY_ALIAS='${{ secrets.KEYSTORE_KEY_ALIAS }}' -PMYAPP_UPLOAD_STORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}' -PMYAPP_UPLOAD_KEY_PASSWORD='${{ secrets.KEYSTORE_KEY_PASSWORD }}'
|
||||
|
||||
# Push tag to GitHub if package.json version's tag is not tagged
|
||||
- name: Get package version
|
||||
run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV
|
||||
|
||||
- name: Create git tag
|
||||
uses: pkgdeps/git-tag-action@v2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github_repo: ${{ github.repository }}
|
||||
version: ${{ env.PACKAGE_VERSION }}
|
||||
git_commit_sha: ${{ github.sha }}
|
||||
git_tag_prefix: "v"
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: ./publish/changeLog.md
|
||||
prerelease: false
|
||||
draft: false
|
||||
tag_name: v${{ env.PACKAGE_VERSION }}
|
||||
files: |
|
||||
android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk
|
||||
android/app/build/outputs/apk/release/app-x86_64-release.apk
|
||||
android/app/build/outputs/apk/release/app-x86-release.apk
|
||||
android/app/build/outputs/apk/release/app-universal-release.apk
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Artifact arm64-v8a
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-arm64-v8a-release
|
||||
path: android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload Artifact armeabi-v7a
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-armeabi-v7a-release
|
||||
path: android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload Artifact universal
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-universal-release
|
||||
path: android/app/build/outputs/apk/release/app-universal-release.apk
|
||||
|
||||
- name: Upload Artifact x86_64
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-x86_64-release
|
||||
path: android/app/build/outputs/apk/release/app-x86_64-release.apk
|
||||
|
||||
- name: Upload Artifact x86
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-x86-release
|
||||
path: android/app/build/outputs/apk/release/app-x86-release.apk
|
60
.gitignore
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
keystore.properties
|
||||
*.iml
|
||||
|
||||
# node.js
|
||||
#
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# BUCK
|
||||
buck-out/
|
||||
\.buckd/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
*/fastlane/report.xml
|
||||
*/fastlane/Preview.html
|
||||
*/fastlane/screenshots
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
# CocoaPods
|
||||
/ios/Pods/
|
7
.prettierrc.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
bracketSpacing: false,
|
||||
jsxBracketSameLine: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
};
|
52
.vscode/javascript.code-snippets
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
// Place your LxMusicMobile 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
"Import translation": {
|
||||
"scope": "javascript,typescript",
|
||||
"prefix": "imtl",
|
||||
"body": [
|
||||
"import { useTranslation } from '@/plugins/i18n'",
|
||||
"$1const { t } = useTranslation()"
|
||||
],
|
||||
"description": "Translation Language"
|
||||
},
|
||||
"Import store hook": {
|
||||
"scope": "javascript,typescript",
|
||||
"prefix": "imsh",
|
||||
"body": [
|
||||
"import { useGetter, useDispatch } from '@/store'"
|
||||
],
|
||||
"description": "Import store hook"
|
||||
},
|
||||
"Import toast": {
|
||||
"scope": "javascript,typescript",
|
||||
"prefix": "imts",
|
||||
"body": [
|
||||
"import { toast } from '@/utils/tools'",
|
||||
"$1toast(t(''), 'long')"
|
||||
],
|
||||
"description": "Import toast"
|
||||
},
|
||||
"Use getter theme": {
|
||||
"scope": "javascript,typescript",
|
||||
"prefix": "ugt",
|
||||
"body": [
|
||||
"const theme = useGetter('common', 'theme')"
|
||||
],
|
||||
"description": "Use getter theme"
|
||||
},
|
||||
}
|
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"path-intellisense.mappings": {
|
||||
// "@config": "${workspaceFolder}/src/config",
|
||||
// "@store": "${workspaceFolder}/src/store",
|
||||
// "@components": "${workspaceFolder}/src/components",
|
||||
// "@navigation": "${workspaceFolder}/src/navigation",
|
||||
// "@screens": "${workspaceFolder}/src/screens",
|
||||
// "@theme": "${workspaceFolder}/src/theme",
|
||||
"@/*": "${workspaceFolder}/src/*"
|
||||
}
|
||||
}
|
1
.watchmanconfig
Normal file
@ -0,0 +1 @@
|
||||
{}
|
11
CHANGELOG.md
Normal file
@ -0,0 +1,11 @@
|
||||
# lx-music-desktop change log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
Project versioning adheres to [Semantic Versioning](http://semver.org/).
|
||||
Commit convention is based on [Conventional Commits](http://conventionalcommits.org).
|
||||
Change log format is based on [Keep a Changelog](http://keepachangelog.com/).
|
||||
|
||||
## [0.1.1] - 2020-05-15
|
||||
|
||||
- v0.1.1版本发布 🎊 🎉
|
66
FAQ.md
Normal file
@ -0,0 +1,66 @@
|
||||
# lx-music-mobile 常见问题
|
||||
|
||||
在阅读本常见问题后,仍然无法解决你的问题,请提交issue或者加企鹅群`830125506`反馈(无事勿加,入群先看群公告),反馈时请**注明**已阅读常见问题!
|
||||
|
||||
## 歌曲无法试听与下载
|
||||
|
||||
### 所有歌曲都提示 `请求异常😮,可以多试几次,若还是不行就换一首吧。。。`
|
||||
|
||||
尝试更换网络,如切换到移动网络,若移动网络还是不行则尝试开关下手机的飞行模式后再试,<br>
|
||||
若使用家庭网络的话,可尝试将光猫断电5分钟左右再通电联网后播放。
|
||||
|
||||
### 其他情况
|
||||
|
||||
尝试在在浏览器打开这个地址`http://ts.tempmusic.tk`,浏览器显示404是正常的,如果不是404那就证明所在网络无法访问接口服务器,对于此类情况请尝试切换其他网络。
|
||||
|
||||
### 通用解决方法
|
||||
|
||||
尝试按以下顺序解决:
|
||||
|
||||
1. 尝试更新到最新版本
|
||||
2. 尝试切换其他歌曲(或直接搜索该歌曲),若全部歌曲都无法试听与下载则进行下一步
|
||||
3. 尝试到 设置-音乐来源 切换到其他接口
|
||||
4. 尝试切换网络,比如用手机开热点(所有歌曲都提示请求异常时可通过此方法解决,或等一两天后再试)
|
||||
5. 若还不行请到这个链接查看详情:<https://github.com/lyswhut/lx-music-desktop/issues/5>
|
||||
6. 若没有在第5条链接中的第一条评论中看到接口无法使用的说明,则应该是你网络无法访问接口服务器的问题,如果接口有问题我会在那里说明。
|
||||
|
||||
想要知道是不是自己网络的问题可以看看`http://ts.tempmusic.tk`能不能在浏览器打开,浏览器显示404是正常的,如果不是404那就证明所在网络无法访问接口服务器。
|
||||
若网页无法打开或打来不是404,则应该是DNS的问题,可以尝试以下办法:
|
||||
|
||||
1. 将DNS改成自动获取试试
|
||||
2. 手动把DNS改一下,不要用360的DNS,可以把DNS改成`114.114.114.114`、`8.8.8.8`
|
||||
|
||||
## 列表多选
|
||||
|
||||
长按列表将会进入多选模式。
|
||||
|
||||
- 例子一:想要选中1-5项,进入多选模式后,取消所有选中的内容,切换到区间,点击第一项,再点击第五项即可完成选择;
|
||||
- 例子二:想要选中1项与第3项,进入多选模式后,点击第一项,再点击第三项即可完成选择;
|
||||
- 例子三:想要选中当前列表的全部内容,进入多选模式后,点击全选即可完成选择(注:由于**在线列表**使用分页加载,全选只会选择目前已加载的内容,若要完整选择整个在线列表的内容则需要往下滑动将列表加载完毕再进行全选)。
|
||||
|
||||
注:选完后可用鼠标右击弹出右键菜单操作已选的内容
|
||||
|
||||
## 播放整个歌单或排行榜
|
||||
|
||||
播放在线列表内的歌曲需要将它们都添加到我的列表才能播放,你可以全选列表内的歌曲然后添加到现有列表或者新创建的列表,然后去播放该列表内的歌曲。
|
||||
|
||||
<!-- ## 无法打开外部歌单
|
||||
|
||||
不支持垮源打开歌单,请**确认**你需要打开的歌单平台是否与软件标签所写的**歌单源**对应(不一样的话请通过右上角切换歌单源);<br>
|
||||
对于分享出来的歌单,若打开失败,可尝试先在浏览器中打开后,再从浏览器地址栏复制URL地址到软件打开;<br>
|
||||
或者如果你知道歌单 id 也可以直接输入歌单 id 打开。<br> -->
|
||||
|
||||
<!--
|
||||
## 更新已收藏的在线歌单
|
||||
|
||||
该功能仅对直接从歌单详情页点“收藏”按钮收藏的歌单有效,可右击已收藏的列表名从弹出的菜单中选择“同步”使用该功能,
|
||||
|
||||
需要注意的是:这将会覆盖本地的目标列表,歌曲将被替换成最新的在线列表。 -->
|
||||
|
||||
## 杀毒软件提示有病毒或恶意行为
|
||||
|
||||
本人只能保证我写的代码不包含任何**恶意代码**、**收集用户信息**的行为,并且软件代码已开源,请自行查阅,软件安装包也是由CI拉取源代码构建,构建日志:[GitHub Actions](https://github.com/lyswhut/lx-music-mobile/actions)<br>
|
||||
尽管如此,但这不意味着软件是100%安全的,由于软件使用了第三方依赖,当这些依赖存在恶意行为时([供应链攻击](https://docs.microsoft.com/zh-cn/windows/security/threat-protection/intelligence/supply-chain-malware)),软件也将会受到牵连,所以我只能尽量选择使用较多人用、信任度较高的依赖。<br>
|
||||
当然,以上说明建立的前提是在你所用的安装包是从**本项目主页上写的链接**下载的,或者有相关能力者还可以下载源代码自己构建安装包。
|
||||
|
||||
最后,若出现杀毒软件报毒、存在恶意行为,请自行判断选择是否继续使用本软件!
|
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
57
README.md
Normal file
@ -0,0 +1,57 @@
|
||||
<p align="center"><a href="https://github.com/lyswhut/lx-music-mobile"><img width="200" src="https://github.com/lyswhut/lx-music-mobile/blob/master/doc/images/icon.png" alt="lx-music logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/lyswhut/lx-music-mobile/releases"><img src="https://img.shields.io/github/release/lyswhut/lx-music-mobile" alt="Release version"></a>
|
||||
<a href="https://github.com/lyswhut/lx-music-mobile/actions/workflows/release.yml"><img src="https://github.com/lyswhut/lx-music-mobile/workflows/Build/badge.svg" alt="Build status"></a>
|
||||
<a href="https://github.com/lyswhut/lx-music-mobile/actions/workflows/beta-pack.yml"><img src="https://github.com/lyswhut/lx-music-mobile/workflows/Build%20Beta/badge.svg" alt="Build status"></a>
|
||||
<a href="https://electronjs.org/releases/stable"><img src="https://img.shields.io/github/package-json/dependency-version/lyswhut/lx-music-mobile/react-native/master" alt="React native version"></a>
|
||||
<!-- <a href="https://github.com/lyswhut/lx-music-mobile/releases"><img src="https://img.shields.io/github/downloads/lyswhut/lx-music-mobile/latest/total" alt="Downloads"></a> -->
|
||||
<a href="https://github.com/lyswhut/lx-music-mobile/tree/dev"><img src="https://img.shields.io/github/package-json/v/lyswhut/lx-music-mobile/dev" alt="Dev branch version"></a>
|
||||
<!-- <a href="https://github.com/lyswhut/lx-music-mobile/blob/master/LICENSE"><img src="https://img.shields.io/github/license/lyswhut/lx-music-mobile" alt="License"></a> -->
|
||||
</p>
|
||||
|
||||
|
||||
<h2 align="center">洛雪音乐助手移动版</h2>
|
||||
|
||||
### 说明
|
||||
|
||||
一个基于 React native 开发的音乐软件。
|
||||
|
||||
所用技术栈:
|
||||
|
||||
- React native
|
||||
|
||||
支持的平台:
|
||||
|
||||
- Android
|
||||
|
||||
注:不计划支持IOS
|
||||
|
||||
软件变化请查看:[更新日志](https://github.com/lyswhut/lx-music-mobile/blob/master/CHANGELOG.md)<br>
|
||||
软件下载请转到:[发布页面](https://github.com/lyswhut/lx-music-mobile/releases)<br>
|
||||
或者到网盘下载(网盘内有MAC、windows版):`https://www.lanzous.com/b0bf2cfa/` 密码:`glqw`(若链接无法打开请百度:蓝奏云链接打不开)<br>
|
||||
使用常见问题请转至:[常见问题](https://github.com/lyswhut/lx-music-mobile/blob/master/FAQ.md)
|
||||
|
||||
|
||||
<!--
|
||||
### UI界面
|
||||
|
||||
<p><a href="https://github.com/lyswhut/lx-music-mobile"><img width="100%" src="https://github.com/lyswhut/lx-music-mobile/blob/master/doc/images/app.png" alt="lx-music UI"></a></p> -->
|
||||
|
||||
### 项目协议
|
||||
|
||||
本项目基于 [Apache License 2.0](https://github.com/lyswhut/lx-music-mobile/blob/master/LICENSE) 许可证发行,以下协议是对于 Apache License 2.0 的补充,如有冲突,以以下协议为准。
|
||||
|
||||
词语约定:本协议中的“本项目”指洛雪音乐桌面版项目;“使用者”指签署本协议的使用者;“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。
|
||||
|
||||
1. 本项目的数据来源原理是从各官方音乐平台的公开服务器中拉取数据,经过对数据简单地筛选与合并后进行展示,因此本项目不对数据的准确性负责。
|
||||
2. 使用本项目的过程中可能会产生版权数据,对于这些版权数据,本项目不拥有它们的所有权,为了避免造成侵权,使用者务必在**24小时**内清除使用本项目的过程中所产生的版权数据。
|
||||
3. 本项目内的官方音乐平台别名为本项目内对官方音乐平台的一个称呼,不包含恶意,如果官方音乐平台觉得不妥,可联系本项目更改或移除。
|
||||
4. 本项目内使用的部分包括但不限于字体、图片等资源来源于互联网,如果出现侵权可联系本项目移除。
|
||||
5. 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。
|
||||
6. 本项目完全免费,且开源发布于 GitHub 面向全世界人用作对技术的学习交流,本项目不对项目内的技术可能存在违反当地法律法规的行为作保证,**禁止在违反当地法律法规的情况下使用本项目**,对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。
|
||||
|
||||
若你使用了本项目,将代表你接受以上协议。
|
||||
|
||||
音乐平台不易,请尊重版权,支持正版。<br>
|
||||
若对此有疑问请 mail to: lyswhut+qq.com (请将`+`替换成`@`)
|
55
android/app/BUCK
Normal file
@ -0,0 +1,55 @@
|
||||
# To learn about Buck see [Docs](https://buckbuild.com/).
|
||||
# To run your application with Buck:
|
||||
# - install Buck
|
||||
# - `npm start` - to start the packager
|
||||
# - `cd android`
|
||||
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
|
||||
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
|
||||
# - `buck install -r android/app` - compile, install and run application
|
||||
#
|
||||
|
||||
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
|
||||
|
||||
lib_deps = []
|
||||
|
||||
create_aar_targets(glob(["libs/*.aar"]))
|
||||
|
||||
create_jar_targets(glob(["libs/*.jar"]))
|
||||
|
||||
android_library(
|
||||
name = "all-libs",
|
||||
exported_deps = lib_deps,
|
||||
)
|
||||
|
||||
android_library(
|
||||
name = "app-code",
|
||||
srcs = glob([
|
||||
"src/main/java/**/*.java",
|
||||
]),
|
||||
deps = [
|
||||
":all-libs",
|
||||
":build_config",
|
||||
":res",
|
||||
],
|
||||
)
|
||||
|
||||
android_build_config(
|
||||
name = "build_config",
|
||||
package = "cn.toside.music.mobile",
|
||||
)
|
||||
|
||||
android_resource(
|
||||
name = "res",
|
||||
package = "cn.toside.music.mobile",
|
||||
res = "src/main/res",
|
||||
)
|
||||
|
||||
android_binary(
|
||||
name = "app",
|
||||
keystore = "//android/keystores:debug",
|
||||
manifest = "src/main/AndroidManifest.xml",
|
||||
package_type = "debug",
|
||||
deps = [
|
||||
":app-code",
|
||||
],
|
||||
)
|
268
android/app/build.gradle
Normal file
@ -0,0 +1,268 @@
|
||||
apply plugin: "com.android.application"
|
||||
|
||||
import com.android.build.OutputFile
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
/**
|
||||
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
|
||||
* and bundleReleaseJsAndAssets).
|
||||
* These basically call `react-native bundle` with the correct arguments during the Android build
|
||||
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
|
||||
* bundle directly from the development server. Below you can see all the possible configurations
|
||||
* and their defaults. If you decide to add a configuration block, make sure to add it before the
|
||||
* `apply from: "../../node_modules/react-native/react.gradle"` line.
|
||||
*
|
||||
* project.ext.react = [
|
||||
* // the name of the generated asset file containing your JS bundle
|
||||
* bundleAssetName: "index.android.bundle",
|
||||
*
|
||||
* // the entry file for bundle generation. If none specified and
|
||||
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
|
||||
* // default. Can be overridden with ENTRY_FILE environment variable.
|
||||
* entryFile: "index.android.js",
|
||||
*
|
||||
* // https://reactnative.dev/docs/performance#enable-the-ram-format
|
||||
* bundleCommand: "ram-bundle",
|
||||
*
|
||||
* // whether to bundle JS and assets in debug mode
|
||||
* bundleInDebug: false,
|
||||
*
|
||||
* // whether to bundle JS and assets in release mode
|
||||
* bundleInRelease: true,
|
||||
*
|
||||
* // whether to bundle JS and assets in another build variant (if configured).
|
||||
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
|
||||
* // The configuration property can be in the following formats
|
||||
* // 'bundleIn${productFlavor}${buildType}'
|
||||
* // 'bundleIn${buildType}'
|
||||
* // bundleInFreeDebug: true,
|
||||
* // bundleInPaidRelease: true,
|
||||
* // bundleInBeta: true,
|
||||
*
|
||||
* // whether to disable dev mode in custom build variants (by default only disabled in release)
|
||||
* // for example: to disable dev mode in the staging build type (if configured)
|
||||
* devDisabledInStaging: true,
|
||||
* // The configuration property can be in the following formats
|
||||
* // 'devDisabledIn${productFlavor}${buildType}'
|
||||
* // 'devDisabledIn${buildType}'
|
||||
*
|
||||
* // the root of your project, i.e. where "package.json" lives
|
||||
* root: "../../",
|
||||
*
|
||||
* // where to put the JS bundle asset in debug mode
|
||||
* jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
|
||||
*
|
||||
* // where to put the JS bundle asset in release mode
|
||||
* jsBundleDirRelease: "$buildDir/intermediates/assets/release",
|
||||
*
|
||||
* // where to put drawable resources / React Native assets, e.g. the ones you use via
|
||||
* // require('./image.png')), in debug mode
|
||||
* resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
|
||||
*
|
||||
* // where to put drawable resources / React Native assets, e.g. the ones you use via
|
||||
* // require('./image.png')), in release mode
|
||||
* resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
|
||||
*
|
||||
* // by default the gradle tasks are skipped if none of the JS files or assets change; this means
|
||||
* // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
|
||||
* // date; if you have any other folders that you want to ignore for performance reasons (gradle
|
||||
* // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
|
||||
* // for example, you might want to remove it from here.
|
||||
* inputExcludes: ["android/**", "ios/**"],
|
||||
*
|
||||
* // override which node gets called and with what additional arguments
|
||||
* nodeExecutableAndArgs: ["node"],
|
||||
*
|
||||
* // supply additional arguments to the packager
|
||||
* extraPackagerArgs: []
|
||||
* ]
|
||||
*/
|
||||
|
||||
project.ext.react = [
|
||||
enableHermes: true, // clean and rebuild if changing
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
|
||||
/**
|
||||
* Set this to true to create two separate APKs instead of one:
|
||||
* - An APK that only works on ARM devices
|
||||
* - An APK that only works on x86 devices
|
||||
* The advantage is the size of the APK is reduced by about 4MB.
|
||||
* Upload all the APKs to the Play Store and people will download
|
||||
* the correct one based on the CPU architecture of their device.
|
||||
*/
|
||||
def enableSeparateBuildPerCPUArchitecture = true
|
||||
|
||||
/**
|
||||
* Run Proguard to shrink the Java bytecode in release builds.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = true
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore.
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
|
||||
/**
|
||||
* Whether to enable the Hermes VM.
|
||||
*
|
||||
* This should be set on project.ext.react and mirrored here. If it is not set
|
||||
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
|
||||
* and the benefits of using Hermes will therefore be sharply reduced.
|
||||
*/
|
||||
def enableHermes = project.ext.react.get("enableHermes", false);
|
||||
|
||||
|
||||
// Get version number
|
||||
def getNpmPackageJson() {
|
||||
def inputFile = new File("../package.json")
|
||||
def packageJson = new JsonSlurper().parseText(inputFile.text)
|
||||
return packageJson
|
||||
}
|
||||
def npmPackageJson = getNpmPackageJson()
|
||||
def verCode = npmPackageJson["versionCode"]
|
||||
def verName = npmPackageJson["version"]
|
||||
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
dexOptions {
|
||||
javaMaxHeapSize "3g"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "cn.toside.music.mobile"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode verCode
|
||||
versionName verName
|
||||
multiDexEnabled true
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk true // If true, also generate a universal APK
|
||||
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
|
||||
storeFile file(MYAPP_UPLOAD_STORE_FILE)
|
||||
storePassword MYAPP_UPLOAD_STORE_PASSWORD
|
||||
keyAlias MYAPP_UPLOAD_KEY_ALIAS
|
||||
keyPassword MYAPP_UPLOAD_KEY_PASSWORD
|
||||
} else {
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
}
|
||||
}
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
// For each separate APK per architecture, set a unique version code as described here:
|
||||
// https://developer.android.com/studio/build/configure-apk-splits.html
|
||||
// Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.
|
||||
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
defaultConfig.versionCode * 1000 + versionCodes.get(abi)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
|
||||
implementation 'com.android.support:multidex:1.0.3'
|
||||
|
||||
implementation 'commons-io:commons-io:2.8.0'
|
||||
|
||||
implementation 'org.apache.commons:commons-compress:1.20'
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
}
|
||||
|
||||
if (enableHermes) {
|
||||
def hermesPath = "../../node_modules/hermes-engine/android/";
|
||||
debugImplementation files(hermesPath + "hermes-debug.aar")
|
||||
releaseImplementation files(hermesPath + "hermes-release.aar")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
|
||||
// Run this once to be able to run the application with BUCK
|
||||
// puts all compile dependencies into folder libs for BUCK to use
|
||||
task copyDownloadableDepsToLibs(type: Copy) {
|
||||
from configurations.compile
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
|
||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
|
19
android/app/build_defs.bzl
Normal file
@ -0,0 +1,19 @@
|
||||
"""Helper definitions to glob .aar and .jar targets"""
|
||||
|
||||
def create_aar_targets(aarfiles):
|
||||
for aarfile in aarfiles:
|
||||
name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
|
||||
lib_deps.append(":" + name)
|
||||
android_prebuilt_aar(
|
||||
name = name,
|
||||
aar = aarfile,
|
||||
)
|
||||
|
||||
def create_jar_targets(jarfiles):
|
||||
for jarfile in jarfiles:
|
||||
name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
|
||||
lib_deps.append(":" + name)
|
||||
prebuilt_jar(
|
||||
name = name,
|
||||
binary_jar = jarfile,
|
||||
)
|
BIN
android/app/debug.keystore
Normal file
10
android/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
13
android/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
</application>
|
||||
</manifest>
|
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package cn.toside.music.mobile;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
import com.facebook.flipper.android.utils.FlipperUtils;
|
||||
import com.facebook.flipper.core.FlipperClient;
|
||||
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.network.NetworkingModule;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (FlipperUtils.shouldEnableFlipper(context)) {
|
||||
final FlipperClient client = AndroidFlipperClient.getInstance(context);
|
||||
|
||||
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
|
||||
client.addPlugin(new ReactFlipperPlugin());
|
||||
client.addPlugin(new DatabasesFlipperPlugin(context));
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
|
||||
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
|
||||
NetworkingModule.setCustomClientBuilder(
|
||||
new NetworkingModule.CustomClientBuilder() {
|
||||
@Override
|
||||
public void apply(OkHttpClient.Builder builder) {
|
||||
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
|
||||
}
|
||||
});
|
||||
client.addPlugin(networkFlipperPlugin);
|
||||
client.start();
|
||||
|
||||
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
|
||||
// Hence we run if after all native modules have been initialized
|
||||
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext == null) {
|
||||
reactInstanceManager.addReactInstanceEventListener(
|
||||
new ReactInstanceManager.ReactInstanceEventListener() {
|
||||
@Override
|
||||
public void onReactContextInitialized(ReactContext reactContext) {
|
||||
reactInstanceManager.removeReactInstanceEventListener(this);
|
||||
reactContext.runOnNativeModulesQueueThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
49
android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,49 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cn.toside.music.mobile">
|
||||
|
||||
<!--获取读写外置存储权限-->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Define a FileProvider for API24+ -->
|
||||
<!-- note this is the authority name used by other modules like rn-fetch-blob, easy to have conflicts -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<!-- you might need the tools:replace thing to workaround rn-fetch-blob or other definitions of provider -->
|
||||
<!-- just make sure if you "replace" here that you include all the paths you are replacing *plus* the cache path we use -->
|
||||
<meta-data tools:replace="android:resource"
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
BIN
android/app/src/main/assets/fonts/AntDesign.ttf
Normal file
BIN
android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf
Normal file
BIN
android/app/src/main/assets/fonts/icomoon.ttf
Normal file
@ -0,0 +1,14 @@
|
||||
package cn.toside.music.mobile;
|
||||
|
||||
import android.os.Bundle; // here
|
||||
import com.reactnativenavigation.NavigationActivity;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreen; // here
|
||||
|
||||
public class MainActivity extends NavigationActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
SplashScreen.show(this, R.style.SplashScreenTheme); // here
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package cn.toside.music.mobile;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.react.PackageList;
|
||||
import com.lxmusicmobile.cache.CachePackage;
|
||||
import com.lxmusicmobile.gzip.GzipPackage;
|
||||
import com.lxmusicmobile.utils.UtilsPackage;
|
||||
import com.reactnativenavigation.NavigationApplication;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.reactnativenavigation.react.NavigationReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
public class MainApplication extends NavigationApplication {
|
||||
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
new NavigationReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
packages.add(new GzipPackage());
|
||||
packages.add(new CachePackage());
|
||||
packages.add(new UtilsPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
return mReactNativeHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
|
||||
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
*
|
||||
* @param context
|
||||
* @param reactInstanceManager
|
||||
*/
|
||||
private static void initializeFlipper(
|
||||
Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
try {
|
||||
/*
|
||||
We use reflection here to pick up the class that initializes Flipper,
|
||||
since Flipper library is not available in release mode
|
||||
*/
|
||||
Class<?> aClass = Class.forName("cn.toside.music.mobile.ReactNativeFlipper");
|
||||
aClass
|
||||
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
|
||||
.invoke(null, context, reactInstanceManager);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (NoSuchMethodException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
android/app/src/main/java/com/lxmusicmobile/cache/CacheClearAsyncTask.java
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
package com.lxmusicmobile.cache;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
|
||||
// https://github.com/midas-gufei/react-native-clear-app-cache/tree/master/android/src/main/java/com/learnta/clear
|
||||
public class CacheClearAsyncTask extends AsyncTask<Integer,Integer,String> {
|
||||
public CacheModule cacheModule = null;
|
||||
public Promise promise;
|
||||
public CacheClearAsyncTask(CacheModule clearCacheModule, Promise promise) {
|
||||
super();
|
||||
this.cacheModule = clearCacheModule;
|
||||
this.promise = promise;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String s) {
|
||||
super.onPostExecute(s);
|
||||
promise.resolve(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(Integer... params) {
|
||||
cacheModule.clearCache();
|
||||
return null;
|
||||
}
|
||||
}
|
74
android/app/src/main/java/com/lxmusicmobile/cache/CacheModule.java
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
package com.lxmusicmobile.cache;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static com.lxmusicmobile.cache.Utils.clearCacheFolder;
|
||||
import static com.lxmusicmobile.cache.Utils.getDirSize;
|
||||
import static com.lxmusicmobile.cache.Utils.isMethodsCompat;
|
||||
|
||||
// https://github.com/midas-gufei/react-native-clear-app-cache/tree/master/android/src/main/java/com/learnta/clear
|
||||
public class CacheModule extends ReactContextBaseJavaModule {
|
||||
private final CacheModule cacheModule;
|
||||
|
||||
CacheModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.cacheModule = this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CacheModule";
|
||||
}
|
||||
|
||||
|
||||
@ReactMethod
|
||||
public void getAppCacheSize(Promise promise) {
|
||||
// 计算缓存大小
|
||||
long fileSize = 0;
|
||||
// File filesDir = getReactApplicationContext().getFilesDir();// /data/data/package_name/files
|
||||
File cacheDir = getReactApplicationContext().getCacheDir();// /data/data/package_name/cache
|
||||
// fileSize += getDirSize(filesDir);
|
||||
fileSize += getDirSize(cacheDir);
|
||||
// 2.2版本才有将应用缓存转移到sd卡的功能
|
||||
if (isMethodsCompat(android.os.Build.VERSION_CODES.FROYO)) {
|
||||
File externalCacheDir = Utils.getExternalCacheDir(getReactApplicationContext());//"<sdcard>/Android/data/<package_name>/cache/"
|
||||
fileSize += getDirSize(externalCacheDir);
|
||||
}
|
||||
|
||||
promise.resolve(String.valueOf(fileSize));
|
||||
}
|
||||
|
||||
//清除缓存
|
||||
@ReactMethod
|
||||
public void clearAppCache(Promise promise) {
|
||||
CacheClearAsyncTask asyncTask = new CacheClearAsyncTask(cacheModule, promise);
|
||||
asyncTask.execute(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除app缓存
|
||||
*/
|
||||
public void clearCache() {
|
||||
|
||||
getReactApplicationContext().deleteDatabase("webview.db");
|
||||
getReactApplicationContext().deleteDatabase("webview.db-shm");
|
||||
getReactApplicationContext().deleteDatabase("webview.db-wal");
|
||||
getReactApplicationContext().deleteDatabase("webviewCache.db");
|
||||
getReactApplicationContext().deleteDatabase("webviewCache.db-shm");
|
||||
getReactApplicationContext().deleteDatabase("webviewCache.db-wal");
|
||||
//清除数据缓存
|
||||
// clearCacheFolder(getReactApplicationContext().getFilesDir(), System.currentTimeMillis());
|
||||
clearCacheFolder(getReactApplicationContext().getCacheDir(), System.currentTimeMillis());
|
||||
//2.2版本才有将应用缓存转移到sd卡的功能
|
||||
if (isMethodsCompat(android.os.Build.VERSION_CODES.FROYO)) {
|
||||
clearCacheFolder(Utils.getExternalCacheDir(getReactApplicationContext()), System.currentTimeMillis());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
24
android/app/src/main/java/com/lxmusicmobile/cache/CachePackage.java
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
package com.lxmusicmobile.cache;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class CachePackage implements ReactPackage {
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(new CacheModule(reactContext));
|
||||
}
|
||||
|
||||
}
|
80
android/app/src/main/java/com/lxmusicmobile/cache/Utils.java
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
package com.lxmusicmobile.cache;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
// https://github.com/midas-gufei/react-native-clear-app-cache/tree/master/android/src/main/java/com/learnta/clear
|
||||
public class Utils {
|
||||
/**
|
||||
* 获取目录文件大小
|
||||
*
|
||||
* @param dir
|
||||
* @return
|
||||
*/
|
||||
static public long getDirSize(File dir) {
|
||||
if (dir == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!dir.isDirectory()) {
|
||||
return 0;
|
||||
}
|
||||
long dirSize = 0;
|
||||
File[] files = dir.listFiles();
|
||||
for (File file : files) {
|
||||
if (file.isFile()) {
|
||||
dirSize += file.length();
|
||||
} else if (file.isDirectory()) {
|
||||
dirSize += file.length();
|
||||
dirSize += getDirSize(file); // 递归调用继续统计
|
||||
}
|
||||
}
|
||||
return dirSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前版本是否兼容目标版本的方法
|
||||
*
|
||||
* @param VersionCode
|
||||
* @return
|
||||
*/
|
||||
static public boolean isMethodsCompat(int VersionCode) {
|
||||
int currentVersion = android.os.Build.VERSION.SDK_INT;
|
||||
return currentVersion >= VersionCode;
|
||||
}
|
||||
|
||||
static public File getExternalCacheDir(Context context) {
|
||||
|
||||
// return context.getExternalCacheDir(); API level 8
|
||||
|
||||
// e.g. "<sdcard>/Android/data/<package_name>/cache/"
|
||||
|
||||
return context.getExternalCacheDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存目录
|
||||
* 目录
|
||||
* 当前系统时间
|
||||
*/
|
||||
static public int clearCacheFolder(File dir, long curTime) {
|
||||
int deletedFiles = 0;
|
||||
if (dir != null && dir.isDirectory()) {
|
||||
try {
|
||||
for (File child : dir.listFiles()) {
|
||||
if (child.isDirectory()) {
|
||||
deletedFiles += clearCacheFolder(child, curTime);
|
||||
}
|
||||
if (child.lastModified() < curTime) {
|
||||
if (child.delete()) {
|
||||
deletedFiles++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return deletedFiles;
|
||||
}
|
||||
}
|
103
android/app/src/main/java/com/lxmusicmobile/gzip/GzipModule.java
Normal file
@ -0,0 +1,103 @@
|
||||
package com.lxmusicmobile.gzip;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
import org.apache.commons.compress.compressors.CompressorException;
|
||||
import org.apache.commons.compress.compressors.CompressorInputStream;
|
||||
import org.apache.commons.compress.compressors.CompressorStreamFactory;
|
||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
|
||||
import org.apache.commons.compress.utils.IOUtils;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.apache.commons.compress.compressors.CompressorStreamFactory.GZIP;
|
||||
|
||||
// https://github.com/FWC1994/react-native-gzip/blob/main/android/src/main/java/com/reactlibrary/GzipModule.java
|
||||
public class GzipModule extends ReactContextBaseJavaModule {
|
||||
private final ReactApplicationContext reactContext;
|
||||
|
||||
GzipModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "GzipModule";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void unGzip(String source, String target, Boolean force, Promise promise) {
|
||||
File sourceFile = new File(source);
|
||||
File targetFile = new File(target);
|
||||
if(!Utils.checkDir(sourceFile, targetFile, force)){
|
||||
promise.reject("-2", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
FileInputStream fileInputStream;
|
||||
|
||||
try{
|
||||
fileInputStream = FileUtils.openInputStream(sourceFile);
|
||||
final CompressorInputStream compressorInputStream = new CompressorStreamFactory()
|
||||
.createCompressorInputStream(GZIP, fileInputStream);
|
||||
|
||||
final FileOutputStream outputStream = FileUtils.openOutputStream(targetFile);
|
||||
IOUtils.copy(compressorInputStream, outputStream);
|
||||
outputStream.close();
|
||||
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putString("path", targetFile.getAbsolutePath());
|
||||
promise.resolve(map);
|
||||
} catch (IOException | CompressorException e) {
|
||||
e.printStackTrace();
|
||||
promise.reject("-2", "unGzip error");
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void gzip(String source, String target, Boolean force, Promise promise) {
|
||||
File sourceFile = new File(source);
|
||||
File targetFile = new File(target);
|
||||
if(!Utils.checkFile(sourceFile, targetFile, force)){
|
||||
promise.reject("-2", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
FileInputStream fileInputStream;
|
||||
FileOutputStream fileOutputStream;
|
||||
|
||||
try{
|
||||
fileInputStream = FileUtils.openInputStream(sourceFile);
|
||||
fileOutputStream = FileUtils.openOutputStream(targetFile);
|
||||
|
||||
BufferedOutputStream out = new BufferedOutputStream(fileOutputStream);
|
||||
GzipCompressorOutputStream gzOut = new GzipCompressorOutputStream(out);
|
||||
final byte[] buffer = new byte[2048];
|
||||
int n = 0;
|
||||
while (-1 != (n = fileInputStream.read(buffer))) {
|
||||
gzOut.write(buffer, 0, n);
|
||||
}
|
||||
gzOut.close();
|
||||
fileInputStream.close();
|
||||
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putString("path", targetFile.getAbsolutePath());
|
||||
promise.resolve(map);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
promise.reject("-2", "gzip error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
package com.lxmusicmobile.gzip;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class GzipPackage implements ReactPackage {
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(new GzipModule(reactContext));
|
||||
}
|
||||
|
||||
}
|
50
android/app/src/main/java/com/lxmusicmobile/gzip/Utils.java
Normal file
@ -0,0 +1,50 @@
|
||||
package com.lxmusicmobile.gzip;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
// https://github.com/FWC1994/react-native-gzip/blob/main/android/src/main/java/com/reactlibrary/GzipModule.java
|
||||
public class Utils {
|
||||
static public Boolean checkDir(File sourceFile, File targetFile, Boolean force) {
|
||||
if (!sourceFile.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetFile.exists()) {
|
||||
if (!force) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (targetFile.isDirectory()) {
|
||||
FileUtils.deleteDirectory(targetFile);
|
||||
} else {
|
||||
targetFile.delete();
|
||||
}
|
||||
targetFile.mkdirs();
|
||||
} catch (IOException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static public Boolean checkFile(File sourceFile, File targetFile, Boolean force) {
|
||||
if (!sourceFile.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetFile.exists()) {
|
||||
if (!force) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetFile.isFile()) {
|
||||
targetFile.delete();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package com.lxmusicmobile.utils;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableNativeArray;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class UtilsModule extends ReactContextBaseJavaModule {
|
||||
private final ReactApplicationContext reactContext;
|
||||
|
||||
UtilsModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "UtilsModule";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void exitApp() {
|
||||
// https://github.com/wumke/react-native-exit-app/blob/master/android/src/main/java/com/github/wumke/RNExitApp/RNExitAppModule.java
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getSupportedAbis(Promise promise) {
|
||||
// https://github.com/react-native-device-info/react-native-device-info/blob/ff8f672cb08fa39a887567d6e23e2f08778e8340/android/src/main/java/com/learnium/RNDeviceInfo/RNDeviceModule.java#L877
|
||||
WritableArray array = new WritableNativeArray();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
for (String abi : Build.SUPPORTED_ABIS) {
|
||||
array.pushString(abi);
|
||||
}
|
||||
} else {
|
||||
array.pushString(Build.CPU_ABI);
|
||||
}
|
||||
promise.resolve(array);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void installApk(String filePath, String fileProviderAuthority, Promise promise) {
|
||||
// https://github.com/mikehardy/react-native-update-apk/blob/master/android/src/main/java/net/mikehardy/rnupdateapk/RNUpdateAPK.java
|
||||
File file = new File(filePath);
|
||||
if (!file.exists()) {
|
||||
Log.e("Utils", "installApk: file doe snot exist '" + filePath + "'");
|
||||
// FIXME this should take a promise and fail it
|
||||
promise.reject("Utils", "installApk: file doe snot exist '" + filePath + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
// API24 and up has a package installer that can handle FileProvider content:// URIs
|
||||
Uri contentUri;
|
||||
try {
|
||||
contentUri = FileProvider.getUriForFile(getReactApplicationContext(), fileProviderAuthority, file);
|
||||
} catch (Exception e) {
|
||||
// FIXME should be a Promise.reject really
|
||||
Log.e("Utils", "installApk exception with authority name '" + fileProviderAuthority + "'", e);
|
||||
promise.reject("Utils", "installApk exception with authority name '" + fileProviderAuthority + "'");
|
||||
return;
|
||||
// throw e;
|
||||
}
|
||||
Intent installApp = new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
||||
installApp.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
installApp.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
installApp.setData(contentUri);
|
||||
installApp.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, reactContext.getApplicationInfo().packageName);
|
||||
reactContext.startActivity(installApp);
|
||||
promise.resolve(null);
|
||||
} else {
|
||||
// Old APIs do not handle content:// URIs, so use an old file:// style
|
||||
String cmd = "chmod 777 " + file;
|
||||
try {
|
||||
Runtime.getRuntime().exec(cmd);
|
||||
} catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
Log.e("Utils", "installApk exception : " + e.getMessage(), e);
|
||||
promise.reject("Utils", e.getMessage());
|
||||
}
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setDataAndType(Uri.parse("file://" + file), "application/vnd.android.package-archive");
|
||||
reactContext.startActivity(intent);
|
||||
promise.resolve(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
package com.lxmusicmobile.utils;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.lxmusicmobile.gzip.GzipModule;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class UtilsPackage implements ReactPackage {
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(new UtilsModule(reactContext));
|
||||
}
|
||||
}
|
BIN
android/app/src/main/res/drawable-hdpi/launch_screen.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
android/app/src/main/res/drawable-land-hdpi/launch_screen.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
android/app/src/main/res/drawable-land-mdpi/launch_screen.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/drawable-land-xhdpi/launch_screen.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
android/app/src/main/res/drawable-land-xxhdpi/launch_screen.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
android/app/src/main/res/drawable-land-xxxhdpi/launch_screen.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
android/app/src/main/res/drawable-mdpi/launch_screen.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/launch_screen.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/launch_screen.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/launch_screen.png
Normal file
After Width: | Height: | Size: 51 KiB |
11
android/app/src/main/res/layout/launch_screen.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
>
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:src="@drawable/launch_screen"
|
||||
android:scaleType="centerCrop" />
|
||||
</LinearLayout>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 15 KiB |
4
android/app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary_dark">#000000</color>
|
||||
</resources>
|
3
android/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">LX Music</string>
|
||||
</resources>
|
17
android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:textColor">#000000</item>
|
||||
|
||||
<!-- <item name="android:windowBackground">@drawable/launch_screen</item> -->
|
||||
</style>
|
||||
|
||||
<style name="SplashScreenTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<!-- <item name="android:windowBackground">@drawable/launch_screen</item> -->
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
22
android/app/src/main/res/xml/file_paths.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
|
||||
<!-- Select one of the following based on your apk location -->
|
||||
|
||||
<!-- cache dir is always available and requires no permissions, but space may be limited -->
|
||||
<cache-path name="cache" path="/" />
|
||||
<!-- <files-path name="name" path="/" /> -->
|
||||
|
||||
<!-- External cache dir is maybe user-friendly for downloaded APKs, but you must be careful. -->
|
||||
<!-- 1) in API <19 (KitKat) this requires WRITE_EXTERNAL_STORAGE permission. >=19, no permission -->
|
||||
<!-- 2) this directory may not be available, check Environment.isExternalStorageEmulated(file) to see -->
|
||||
<!-- 3) there may be no beneifit versus cache-path if external storage is emulated. Check Environment.isExternalStorageEmulated(File) to verify -->
|
||||
<!-- 4) the path will change for each app 'com.example' must be replaced by your application package -->
|
||||
<!-- <external-cache-path name="external-cache" path="/data/user/0/com.example/cache" /> -->
|
||||
|
||||
<!-- Note that these external paths require WRITE_EXTERNAL_STORAGE permission -->
|
||||
<!-- <external-path name="some_external_path" path="put-your-specific-external-path-here" /> -->
|
||||
<!-- <external-files-path name="external-files" path="/data/user/0/com.example/cache" /> -->
|
||||
<!-- <external-media-path name="external-media" path="put-your-path-to-media-here" /> -->
|
||||
</paths>
|
4
android/app/src/main/res/xml/network_security_config.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true" />
|
||||
</network-security-config>
|
43
android/build.gradle
Normal file
@ -0,0 +1,43 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "29.0.3"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
ndkVersion = "20.1.5948944"
|
||||
kotlinVersion = "1.4.21" // Or any version above 1.3.x
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:4.1.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url("$rootDir/../node_modules/react-native/android")
|
||||
}
|
||||
maven {
|
||||
// Android JSC is installed from npm
|
||||
url("$rootDir/../node_modules/jsc-android/dist")
|
||||
}
|
||||
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
37
android/gradle.properties
Normal file
@ -0,0 +1,37 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.75.1
|
||||
|
||||
|
||||
# org.gradle.daemon=true
|
||||
# org.gradle.configureondemand=true
|
||||
org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
|
||||
AsyncStorage_dedicatedExecutor = true
|
||||
|
||||
AsyncStorage_useNextStorage = true
|
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
185
android/gradlew
vendored
Executable file
@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
89
android/gradlew.bat
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
7
android/settings.gradle
Normal file
@ -0,0 +1,7 @@
|
||||
rootProject.name = 'LxMusicMobile'
|
||||
include ':react-native-splash-screen'
|
||||
project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android')
|
||||
include ':react-native-vector-icons'
|
||||
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
include ':app'
|
29
babel.config.js
Normal file
@ -0,0 +1,29 @@
|
||||
module.exports = {
|
||||
presets: ['module:metro-react-native-babel-preset'],
|
||||
plugins: [
|
||||
[
|
||||
'module-resolver',
|
||||
{
|
||||
root: ['.'],
|
||||
extensions: [
|
||||
'.android.js',
|
||||
'.ios.js',
|
||||
'.android.jsx',
|
||||
'.ios.jsx',
|
||||
'.jsx',
|
||||
'.js',
|
||||
'.json',
|
||||
],
|
||||
alias: {
|
||||
'@': './src',
|
||||
// '@config': './src/config',
|
||||
// '@store': './src/store',
|
||||
// '@components': './src/components',
|
||||
// '@navigation': './src/navigation',
|
||||
// '@screens': './src/screens',
|
||||
// '@theme': './src/theme',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
BIN
doc/images/icon.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
86
index.js
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
|
||||
// import '@/utils/log'
|
||||
import './shim'
|
||||
import '@/config/globalData'
|
||||
import SplashScreen from 'react-native-splash-screen'
|
||||
import { init as initNavigation, navigations, showPactModal } from '@/navigation'
|
||||
import { registerPlaybackService } from '@/plugins/player'
|
||||
import { getStore } from '@/store'
|
||||
import { action as commonAction } from '@/store/modules/common'
|
||||
import { action as playerAction } from '@/store/modules/player'
|
||||
import { action as listAction } from '@/store/modules/list'
|
||||
import { init as initMusicTools } from '@/utils/music'
|
||||
import { init as initLyric } from '@/plugins/lyric'
|
||||
import { init as initI18n, supportedLngs } from '@/plugins/i18n'
|
||||
import { deviceLanguage, getPlayInfo } from '@/utils/tools'
|
||||
import { LIST_ID_PLAY_TEMP, LIST_ID_PLAY_LATER } from '@/config/constant'
|
||||
|
||||
console.log('starting app...')
|
||||
|
||||
let store
|
||||
let isInited = false
|
||||
|
||||
const init = () => {
|
||||
if (isInited) return Promise.resolve()
|
||||
isInited = true
|
||||
store = getStore()
|
||||
// console.log('deviceLanguage', deviceLanguage)
|
||||
return Promise.all([
|
||||
store.dispatch(commonAction.initSetting()),
|
||||
store.dispatch(listAction.initList()),
|
||||
initLyric(),
|
||||
registerPlaybackService(),
|
||||
]).then(() => {
|
||||
let lang = store.getState().common.setting.langId
|
||||
let needSetLang = false
|
||||
if (!supportedLngs.includes(lang)) {
|
||||
if (typeof deviceLanguage == 'string' && supportedLngs.includes(deviceLanguage)) {
|
||||
lang = deviceLanguage
|
||||
} else {
|
||||
lang = 'en_us'
|
||||
}
|
||||
needSetLang = true
|
||||
}
|
||||
console.log(lang)
|
||||
return initI18n(lang).then(() => {
|
||||
if (needSetLang) return store.dispatch(commonAction.setLang(lang))
|
||||
})
|
||||
// .catch(_ => _)
|
||||
// StatusBar.setHidden(false)
|
||||
// console.log('init')
|
||||
}).then(() => {
|
||||
initMusicTools()
|
||||
getPlayInfo().then(info => {
|
||||
if (!info) return
|
||||
global.restorePlayInfo = info
|
||||
if (info.listId != LIST_ID_PLAY_TEMP && info.listId != LIST_ID_PLAY_LATER) {
|
||||
info.list = global.allList[info.listId]
|
||||
if (info.list) info.list = info.list.list
|
||||
}
|
||||
store.dispatch(playerAction.setList({
|
||||
list: {
|
||||
list: info.list,
|
||||
id: info.listId,
|
||||
},
|
||||
index: info.index,
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
initNavigation(async() => {
|
||||
init().then(() => {
|
||||
navigations.pushHomeScreen()
|
||||
SplashScreen.hide()
|
||||
if (!store.getState().common.setting.isAgreePact) {
|
||||
showPactModal()
|
||||
} else {
|
||||
store.dispatch(commonAction.checkVersion())
|
||||
}
|
||||
})
|
||||
})
|
||||
|
16
jsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
// "@config": ["src/config"],
|
||||
// "@store": ["src/store"],
|
||||
// "@components": ["src/components"],
|
||||
// "@navigation": ["src/navigation"],
|
||||
// "@screens": ["src/screens"],
|
||||
// "@theme": ["src/theme"],
|
||||
// "@utils": ["src/utils"],
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
17
metro.config.js
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Metro configuration for React Native
|
||||
* https://github.com/facebook/react-native
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
transformer: {
|
||||
getTransformOptions: async() => ({
|
||||
transform: {
|
||||
experimentalImportSupport: false,
|
||||
inlineRequires: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
123
package.json
Normal file
@ -0,0 +1,123 @@
|
||||
{
|
||||
"name": "lx-music-mobile",
|
||||
"version": "0.1.1",
|
||||
"versionCode": 2,
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"ar": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"start": "react-native start",
|
||||
"sc": "react-native start --reset-cache",
|
||||
"test": "jest",
|
||||
"lint": "eslint .",
|
||||
"rd": "react-devtools",
|
||||
"pack": "npm run pack:android",
|
||||
"pack:android": "./gradlew assembleRelease",
|
||||
"clear": "react-native clean-project",
|
||||
"publish": "node publish",
|
||||
"nodeify": "rn-nodeify --hack --yarn --install process,crypto,events,constant,console,stream,url,util",
|
||||
"postinstall": "yarn nodeify"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lyswhut/lx-music-mobile.git"
|
||||
},
|
||||
"keywords": [
|
||||
"music-player",
|
||||
"react-native-app"
|
||||
],
|
||||
"author": {
|
||||
"name": "lyswhut",
|
||||
"email": "lyswhut@qq.com"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/lyswhut/lx-music-mobile/issues"
|
||||
},
|
||||
"homepage": "https://github.com/lyswhut/lx-music-mobile#readme",
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.15.4",
|
||||
"@react-native-community/checkbox": "^0.5.7",
|
||||
"buffer": "^6.0.3",
|
||||
"console-browserify": "^1.2.0",
|
||||
"events": "^3.3.0",
|
||||
"i18next": "^20.2.2",
|
||||
"js-htmlencode": "^0.3.0",
|
||||
"lrc-file-parser": "^1.0.7",
|
||||
"pako": "^2.0.3",
|
||||
"process": "^0.11.10",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "17.0.1",
|
||||
"react-i18next": "^11.8.15",
|
||||
"react-native": "0.64.0",
|
||||
"react-native-background-timer": "^2.4.1",
|
||||
"react-native-crypto": "^2.2.0",
|
||||
"react-native-fs": "^2.18.0",
|
||||
"react-native-navigation": "^7.14.0",
|
||||
"react-native-pager-view": "^5.1.8",
|
||||
"react-native-randombytes": "^3.6.1",
|
||||
"react-native-splash-screen": "^3.2.0",
|
||||
"react-native-track-player": "lyswhut/react-native-track-player#63d6e5147bba3ce476ee751756d0a7de5fe88757",
|
||||
"react-native-vector-icons": "^8.1.0",
|
||||
"react-redux": "^7.2.4",
|
||||
"readable-stream": "1.0.33",
|
||||
"redux": "^4.1.0",
|
||||
"redux-subscriber": "^1.1.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"stream-browserify": "^1.0.0",
|
||||
"url": "~0.10.1",
|
||||
"util": "~0.10.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.2",
|
||||
"@babel/runtime": "^7.14.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"changelog-parser": "^2.8.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^7.26.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-html": "^6.1.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"metro-react-native-babel-preset": "^0.64.0",
|
||||
"react-devtools": "^4.13.2",
|
||||
"react-native-clean-project": "^3.6.3",
|
||||
"react-test-renderer": "17.0.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"rn-nodeify": "^10.3.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "react-native"
|
||||
},
|
||||
"react-native": {
|
||||
"console": "console-browserify",
|
||||
"crypto": "react-native-crypto",
|
||||
"_stream_transform": "readable-stream/transform",
|
||||
"_stream_readable": "readable-stream/readable",
|
||||
"_stream_writable": "readable-stream/writable",
|
||||
"_stream_duplex": "readable-stream/duplex",
|
||||
"_stream_passthrough": "readable-stream/passthrough",
|
||||
"stream": "stream-browserify"
|
||||
},
|
||||
"browser": {
|
||||
"console": "console-browserify",
|
||||
"crypto": "react-native-crypto",
|
||||
"_stream_transform": "readable-stream/transform",
|
||||
"_stream_readable": "readable-stream/readable",
|
||||
"_stream_writable": "readable-stream/writable",
|
||||
"_stream_duplex": "readable-stream/duplex",
|
||||
"_stream_passthrough": "readable-stream/passthrough",
|
||||
"stream": "stream-browserify"
|
||||
}
|
||||
}
|
3
publish/changeLog.md
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
lx-music移动端v0.1.1版本发布 🎊 🎉
|
||||
|
50
publish/index.js
Normal file
@ -0,0 +1,50 @@
|
||||
// const fs = require('fs')
|
||||
// const path = require('path')
|
||||
const chalk = require('chalk')
|
||||
// const clearAssets = require('./utils/clearAssets')
|
||||
// const packAssets = require('./utils/packAssets')
|
||||
// const compileAssets = require('./utils/compileAssets')
|
||||
const updateVersionFile = require('./utils/updateChangeLog')
|
||||
// const copyFile = require('./utils/copyFile')
|
||||
// const githubRelease = require('./utils/githubRelease')
|
||||
// const { parseArgv } = require('./utils')
|
||||
|
||||
const run = async() => {
|
||||
// const params = parseArgv(process.argv.slice(2))
|
||||
// const bak = await updateVersionFile(params.ver)
|
||||
await updateVersionFile(process.argv.slice(2)[0])
|
||||
console.log(chalk.green('日志更新完成~'))
|
||||
|
||||
// try {
|
||||
// console.log(chalk.blue('Clearing assets...'))
|
||||
// await clearAssets()
|
||||
// console.log(chalk.green('Assets clear completed...'))
|
||||
|
||||
// // console.log(chalk.blue('Compileing assets...'))
|
||||
// // await compileAssets()
|
||||
// // console.log(chalk.green('Asset compiled successfully.'))
|
||||
|
||||
// // console.log(chalk.blue('Building assets...'))
|
||||
// // await packAssets()
|
||||
// // console.log(chalk.green('Asset build successfully.'))
|
||||
|
||||
// // console.log(chalk.blue('Copy files...'))
|
||||
// // await copyFile()
|
||||
// // console.log(chalk.green('Complete copy of all files.'))
|
||||
|
||||
// // console.log(chalk.blue('Create release...'))
|
||||
// // await githubRelease(params)
|
||||
// // console.log(chalk.green('Release created.'))
|
||||
|
||||
// } catch (error) {
|
||||
// console.log(error)
|
||||
// console.log(chalk.red('程序发布失败'))
|
||||
// console.log(chalk.blue('正在还原版本信息'))
|
||||
// fs.writeFileSync(path.join(__dirname, './version.json'), bak.version_bak + '\n', 'utf-8')
|
||||
// fs.writeFileSync(path.join(__dirname, '../package.json'), bak.pkg_bak + '\n', 'utf-8')
|
||||
// console.log(chalk.blue('版本信息还原完成'))
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
run()
|
62
publish/utils/index.js
Normal file
@ -0,0 +1,62 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
exports.jp = (...p) => p.length ? path.join(__dirname, ...p) : __dirname
|
||||
|
||||
exports.copyFile = (source, target) => new Promise((resolve, reject) => {
|
||||
const rd = fs.createReadStream(source)
|
||||
rd.on('error', err => reject(err))
|
||||
const wr = fs.createWriteStream(target)
|
||||
wr.on('error', err => reject(err))
|
||||
wr.on('close', () => resolve())
|
||||
rd.pipe(wr)
|
||||
})
|
||||
|
||||
/**
|
||||
* 时间格式化
|
||||
* @param {Date} d 格式化的时间
|
||||
* @param {boolean} b 是否精确到秒
|
||||
*/
|
||||
exports.formatTime = (d, b) => {
|
||||
const _date = d == null ? new Date() : typeof d == 'string' ? new Date(d) : d
|
||||
const year = _date.getFullYear()
|
||||
const month = fm(_date.getMonth() + 1)
|
||||
const day = fm(_date.getDate())
|
||||
if (!b) return year + '-' + month + '-' + day
|
||||
return year + '-' + month + '-' + day + ' ' + fm(_date.getHours()) + ':' + fm(_date.getMinutes()) + ':' + fm(_date.getSeconds())
|
||||
}
|
||||
|
||||
function fm(value) {
|
||||
if (value < 10) return '0' + value
|
||||
return value
|
||||
}
|
||||
|
||||
exports.sizeFormate = size => {
|
||||
// https://gist.github.com/thomseddon/3511330
|
||||
if (!size) return '0 b'
|
||||
let units = ['b', 'kB', 'MB', 'GB', 'TB']
|
||||
let number = Math.floor(Math.log(size) / Math.log(1024))
|
||||
return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}`
|
||||
}
|
||||
|
||||
exports.parseArgv = argv => {
|
||||
const params = {}
|
||||
argv.forEach(item => {
|
||||
const argv = item.split('=')
|
||||
switch (argv[0]) {
|
||||
case 'ver':
|
||||
params.ver = argv[1]
|
||||
break
|
||||
case 'draft':
|
||||
params.isDraft = argv[1] === 'true' || argv[1] === undefined
|
||||
break
|
||||
case 'prerelease':
|
||||
params.isPrerelease = argv[1] === 'true' || argv[1] === undefined
|
||||
break
|
||||
case 'target_commitish':
|
||||
params.target_commitish = argv[1]
|
||||
break
|
||||
}
|
||||
})
|
||||
return params
|
||||
}
|
54
publish/utils/updateChangeLog.js
Normal file
@ -0,0 +1,54 @@
|
||||
const fs = require('fs')
|
||||
const { jp, formatTime } = require('./index')
|
||||
const pkgDir = '../../package.json'
|
||||
const pkg = require(pkgDir)
|
||||
const version = require('../version.json')
|
||||
const chalk = require('chalk')
|
||||
const pkg_bak = JSON.stringify(pkg, null, 2)
|
||||
const version_bak = JSON.stringify(version, null, 2)
|
||||
const parseChangelog = require('changelog-parser')
|
||||
const changelogPath = jp('../../CHANGELOG.md')
|
||||
|
||||
const getPrevVer = () => parseChangelog(changelogPath).then(res => {
|
||||
if (!res.versions.length) throw new Error('CHANGELOG 无法解析到版本号')
|
||||
return res.versions[0].version
|
||||
})
|
||||
|
||||
const updateChangeLog = async(newVerNum, newChangeLog) => {
|
||||
let changeLog = fs.readFileSync(changelogPath, 'utf-8')
|
||||
const prevVer = await getPrevVer()
|
||||
const log = `## [${newVerNum}](${pkg.repository.url.replace(/^git\+(http.+)\.git$/, '$1')}/compare/v${prevVer}...v${newVerNum}) - ${formatTime()}\n\n${newChangeLog}`
|
||||
fs.writeFileSync(changelogPath, changeLog.replace(/(## [?0.1.1]?)/, log + '\n$1'), 'utf-8')
|
||||
}
|
||||
|
||||
|
||||
module.exports = async newVerNum => {
|
||||
if (!newVerNum) {
|
||||
let verArr = pkg.version.split('.')
|
||||
verArr[verArr.length - 1] = parseInt(verArr[verArr.length - 1]) + 1
|
||||
newVerNum = verArr.join('.')
|
||||
}
|
||||
const newMDChangeLog = fs.readFileSync(jp('../changeLog.md'), 'utf-8')
|
||||
version.history.unshift({
|
||||
version: version.version,
|
||||
desc: version.desc,
|
||||
})
|
||||
version.version = newVerNum
|
||||
version.desc = newMDChangeLog.replace(/(?:^|(\n))#{1,6} (.+)\n/g, '$1$2').trim()
|
||||
pkg.version = newVerNum
|
||||
pkg.versionCode = pkg.versionCode + 1
|
||||
|
||||
console.log(chalk.blue('new version: ') + chalk.green(newVerNum))
|
||||
|
||||
fs.writeFileSync(jp('../version.json'), JSON.stringify(version) + '\n', 'utf-8')
|
||||
|
||||
fs.writeFileSync(jp(pkgDir), JSON.stringify(pkg, null, 2) + '\n', 'utf-8')
|
||||
|
||||
await updateChangeLog(newVerNum, newMDChangeLog)
|
||||
|
||||
return {
|
||||
pkg_bak,
|
||||
version_bak,
|
||||
}
|
||||
}
|
||||
|
5
publish/version.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"version":"0.1.1",
|
||||
"desc":"lx-music移动端v0.1.1版本发布 🎊 🎉",
|
||||
"history":[]
|
||||
}
|
26
shim.js
Normal file
@ -0,0 +1,26 @@
|
||||
if (typeof __dirname === 'undefined') global.__dirname = '/'
|
||||
if (typeof __filename === 'undefined') global.__filename = ''
|
||||
if (typeof process === 'undefined') {
|
||||
global.process = require('process')
|
||||
} else {
|
||||
const bProcess = require('process')
|
||||
for (let p in bProcess) {
|
||||
if (!(p in process)) {
|
||||
process[p] = bProcess[p]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.browser = false
|
||||
if (typeof Buffer === 'undefined') global.Buffer = require('buffer').Buffer
|
||||
|
||||
// global.location = global.location || { port: 80 }
|
||||
const isDev = typeof __DEV__ === 'boolean' && __DEV__
|
||||
process.env.NODE_ENV = isDev ? 'development' : 'production'
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.debug = isDev ? '*' : ''
|
||||
}
|
||||
|
||||
// If using the crypto shim, uncomment the following line to ensure
|
||||
// crypto is loaded first, so it can populate global.crypto
|
||||
require('crypto')
|
71
src/components/ListItem/index.js
Normal file
@ -0,0 +1,71 @@
|
||||
import React from 'react'
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'
|
||||
import PropTypes from 'prop-types'
|
||||
import { BorderWidths } from '@/theme'
|
||||
import { useGetter } from '@/store'
|
||||
|
||||
const ListItem = ({ data, onPress, badge }) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
return (
|
||||
<View style={{ ...styles.container, borderBottomColor: theme.borderColor2, borderBottomWidth: BorderWidths.normal }}>
|
||||
<TouchableOpacity style={styles.left} onPress={onPress}>
|
||||
<View style={styles.row1}>
|
||||
<Text style={styles.title}>{data.title}</Text>
|
||||
{!!data.badge && <Text style={[styles.badge, styles[`badge_${badge}`]]}>{data.badge}</Text>}
|
||||
</View>
|
||||
{!!data.desc && <View style={styles.row2}><Text style={styles.desc}>{data.desc}</Text></View>}
|
||||
</TouchableOpacity>
|
||||
{!!data.right && <View style={styles.right}>{data.right}</View>}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// class ListItem extends Component {
|
||||
// state = {
|
||||
|
||||
// }
|
||||
|
||||
// static propTypes = {
|
||||
// data: PropTypes.object.isRequired,
|
||||
// onPress: PropTypes.func,
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// const { data, onPress, badge } = this.props
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'nowrap',
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
left: {
|
||||
flex: 1,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
},
|
||||
desc: {
|
||||
color: '#888',
|
||||
},
|
||||
badge: {
|
||||
|
||||
},
|
||||
right: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexBasis: 'auto',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
export default ListItem
|
||||
|
112
src/components/MusicAddModal.js
Normal file
@ -0,0 +1,112 @@
|
||||
import React, { useCallback, useMemo, memo } from 'react'
|
||||
import { View, StyleSheet, Text, ScrollView } from 'react-native'
|
||||
import Dialog from '@/components/common/Dialog'
|
||||
import Button from '@/components/common/Button'
|
||||
import { useGetter, useDispatch } from '@/store'
|
||||
import { useTranslation } from '@/plugins/i18n'
|
||||
import { useDimensions } from '@/utils/hooks'
|
||||
|
||||
|
||||
const ListItem = ({ list, onPress, musicInfo, width }) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
const isDisabled = useMemo(() => {
|
||||
return list.list.some(s => s.songmid == musicInfo.songmid)
|
||||
}, [list, musicInfo])
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.listItem, width: width }}>
|
||||
<Button
|
||||
disabled={isDisabled}
|
||||
style={{ ...styles.button, backgroundColor: theme.secondary45, opacity: isDisabled ? 0.6 : 1 }}
|
||||
onPress={() => { onPress(list) }}
|
||||
>
|
||||
<Text numberOfLines={1} style={{ fontSize: 12, color: isDisabled ? theme.secondary10 : theme.secondary }}>{list.name}</Text>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const Title = ({ musicInfo, isMove }) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
musicInfo
|
||||
? <Text style={styles.title}>{t(isMove ? 'list_add_title_first_move' : 'list_add_title_first_add')} <Text style={{ color: theme.secondary }} >{musicInfo.name}</Text> {t('list_add_title_last')}</Text>
|
||||
: null
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(({ visible, hideModal, musicInfo, listId, isMove = false }) => {
|
||||
const allList = useGetter('list', 'allList')
|
||||
const addMusicToList = useDispatch('list', 'listAdd')
|
||||
const moveMusicToList = useDispatch('list', 'listMove')
|
||||
const { window } = useDimensions()
|
||||
|
||||
const itemWidth = useMemo(() => {
|
||||
let w = window.width * 0.9 - 20
|
||||
let n = 1
|
||||
while (true) {
|
||||
if (w / n < 100 + n * 30 || n > 9) return parseInt(w / n)
|
||||
n++
|
||||
}
|
||||
}, [window])
|
||||
|
||||
const handleSelect = useCallback(list => {
|
||||
if (isMove) {
|
||||
moveMusicToList({
|
||||
fromId: listId,
|
||||
toId: list.id,
|
||||
musicInfo,
|
||||
})
|
||||
} else {
|
||||
addMusicToList({
|
||||
musicInfo,
|
||||
id: list.id,
|
||||
})
|
||||
}
|
||||
hideModal()
|
||||
}, [addMusicToList, hideModal, isMove, listId, moveMusicToList, musicInfo])
|
||||
|
||||
return (
|
||||
<Dialog visible={visible} hideDialog={hideModal}>
|
||||
<Title musicInfo={musicInfo} isMove={isMove} />
|
||||
<View style={{ flexShrink: 1 }} onStartShouldSetResponder={() => true}>
|
||||
<ScrollView style={{ flexGrow: 0 }} keyboardShouldPersistTaps={'always'}>
|
||||
<View style={{ ...styles.list }}>
|
||||
{ allList.map(list => <ListItem key={list.id} list={list} musicInfo={musicInfo} onPress={handleSelect} width={itemWidth} />) }
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Dialog>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
padding: 15,
|
||||
},
|
||||
list: {
|
||||
paddingLeft: 15,
|
||||
paddingRight: 5,
|
||||
paddingBottom: 5,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)'
|
||||
},
|
||||
listItem: {
|
||||
// width: '50%',
|
||||
paddingRight: 10,
|
||||
},
|
||||
button: {
|
||||
paddingTop: 9,
|
||||
paddingBottom: 9,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
marginRight: 10,
|
||||
marginBottom: 10,
|
||||
borderRadius: 4,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
})
|
113
src/components/MusicMultiAddModal.js
Normal file
@ -0,0 +1,113 @@
|
||||
import React, { useCallback, useMemo, memo } from 'react'
|
||||
import { View, StyleSheet, Text, ScrollView } from 'react-native'
|
||||
import Dialog from '@/components/common/Dialog'
|
||||
import Button from '@/components/common/Button'
|
||||
import { useGetter, useDispatch } from '@/store'
|
||||
import { useTranslation } from '@/plugins/i18n'
|
||||
import { useDimensions } from '@/utils/hooks'
|
||||
|
||||
|
||||
const ListItem = ({ list, onPress, width }) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.listItem, width: width }}>
|
||||
<Button
|
||||
style={{ ...styles.button, backgroundColor: theme.secondary45 }}
|
||||
onPress={() => { onPress(list) }}
|
||||
>
|
||||
<Text numberOfLines={1} style={{ fontSize: 12, color: theme.secondary }}>{list.name}</Text>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const Title = ({ list, isMove }) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
list.length
|
||||
? <Text style={styles.title}>{t(isMove ? 'list_multi_add_title_first_move' : 'list_multi_add_title_first_add')} <Text style={{ color: theme.secondary }} >{list.length}</Text> {t('list_multi_add_title_last')}</Text>
|
||||
: null
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(({ visible, hideModal, list, onAdd, excludeList = [], listId, isMove = false }) => {
|
||||
const allList = useGetter('list', 'allList')
|
||||
const addMultiMusicToList = useDispatch('list', 'listAddMultiple')
|
||||
const moveMultiMusicToList = useDispatch('list', 'listMoveMultiple')
|
||||
const { window } = useDimensions()
|
||||
|
||||
const itemWidth = useMemo(() => {
|
||||
let w = window.width * 0.9 - 20
|
||||
let n = 1
|
||||
while (true) {
|
||||
if (w / n < 100 + n * 30 || n > 9) return parseInt(w / n)
|
||||
n++
|
||||
}
|
||||
}, [window])
|
||||
|
||||
const handleSelect = useCallback(({ id }) => {
|
||||
if (isMove) {
|
||||
moveMultiMusicToList({
|
||||
fromId: listId,
|
||||
toId: id,
|
||||
list,
|
||||
})
|
||||
} else {
|
||||
addMultiMusicToList({
|
||||
list,
|
||||
id,
|
||||
})
|
||||
}
|
||||
hideModal()
|
||||
onAdd()
|
||||
}, [isMove, hideModal, onAdd, addMultiMusicToList, list, moveMultiMusicToList, listId])
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
return allList.filter(({ id }) => !excludeList.includes(id))
|
||||
}, [allList, excludeList])
|
||||
|
||||
return (
|
||||
<Dialog visible={visible} hideDialog={hideModal}>
|
||||
<Title list={list} isMove={isMove} />
|
||||
<View style={{ flexShrink: 1 }} onStartShouldSetResponder={() => true}>
|
||||
<ScrollView style={{ flexGrow: 0 }} keyboardShouldPersistTaps={'always'}>
|
||||
<View style={{ ...styles.list }}>
|
||||
{ filteredList.map(list => <ListItem key={list.id} list={list} onPress={handleSelect} width={itemWidth} />) }
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Dialog>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
padding: 15,
|
||||
},
|
||||
list: {
|
||||
paddingLeft: 15,
|
||||
paddingRight: 5,
|
||||
paddingBottom: 5,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)'
|
||||
},
|
||||
listItem: {
|
||||
// width: '50%',
|
||||
paddingRight: 10,
|
||||
},
|
||||
button: {
|
||||
paddingTop: 9,
|
||||
paddingBottom: 9,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
marginRight: 10,
|
||||
marginBottom: 10,
|
||||
borderRadius: 4,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
})
|
142
src/components/OnlineList/ExitMultipleModeBar.js
Normal file
@ -0,0 +1,142 @@
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo, memo } from 'react'
|
||||
import { Text, StyleSheet, Animated, View, TouchableOpacity } from 'react-native'
|
||||
import { useTranslation } from '@/plugins/i18n'
|
||||
|
||||
import Button from '@/components/common/Button'
|
||||
import { useGetter } from '@/store'
|
||||
|
||||
|
||||
export default memo(({ multipleMode, onCancel, onSelectAll, selectMode, onSwitchMode, isSelectAll }) => {
|
||||
const { t } = useTranslation()
|
||||
// const isGetDetailFailedRef = useRef(false)
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [animatePlayed, setAnimatPlayed] = useState(true)
|
||||
const animFade = useRef(new Animated.Value(0)).current
|
||||
const animTranslateY = useRef(new Animated.Value(0)).current
|
||||
|
||||
const theme = useGetter('common', 'theme')
|
||||
|
||||
useEffect(() => {
|
||||
setAnimatPlayed(true)
|
||||
if (multipleMode) {
|
||||
animFade.setValue(0.92)
|
||||
animTranslateY.setValue(0)
|
||||
setVisible(true)
|
||||
} else {
|
||||
animFade.setValue(0)
|
||||
animTranslateY.setValue(20)
|
||||
setVisible(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const showList = useCallback(() => {
|
||||
// console.log('show List')
|
||||
setVisible(true)
|
||||
setAnimatPlayed(false)
|
||||
animTranslateY.setValue(20)
|
||||
|
||||
Animated.parallel([
|
||||
Animated.timing(animFade, {
|
||||
toValue: 0.92,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(animTranslateY, {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => {
|
||||
setAnimatPlayed(true)
|
||||
})
|
||||
}, [animFade, animTranslateY])
|
||||
|
||||
const hideList = useCallback(() => {
|
||||
setAnimatPlayed(false)
|
||||
Animated.parallel([
|
||||
Animated.timing(animFade, {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(animTranslateY, {
|
||||
toValue: 20,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(finished => {
|
||||
if (!finished) return
|
||||
setVisible(false)
|
||||
setAnimatPlayed(true)
|
||||
})
|
||||
}, [animFade, animTranslateY])
|
||||
|
||||
useEffect(() => {
|
||||
if (multipleMode) {
|
||||
showList()
|
||||
} else {
|
||||
hideList()
|
||||
}
|
||||
}, [hideList, multipleMode, showList])
|
||||
|
||||
|
||||
const animaStyle = useMemo(() => StyleSheet.compose(styles.container, {
|
||||
backgroundColor: theme.secondary45,
|
||||
opacity: animFade, // Bind opacity to animated value
|
||||
transform: [
|
||||
{ translateY: animTranslateY },
|
||||
],
|
||||
}), [animFade, animTranslateY, theme])
|
||||
|
||||
const switchModeSingle = useCallback(() => {
|
||||
onSwitchMode('single')
|
||||
}, [onSwitchMode])
|
||||
const switchModeRange = useCallback(() => {
|
||||
onSwitchMode('range')
|
||||
}, [onSwitchMode])
|
||||
|
||||
const component = useMemo(() => (
|
||||
<Animated.View style={animaStyle}>
|
||||
<View style={styles.switchBtn}>
|
||||
<Button onPress={switchModeSingle} style={{ ...styles.btn, backgroundColor: selectMode == 'single' ? theme.secondary40 : 'rgba(0,0,0,0)' }}>
|
||||
<Text style={{ color: theme.secondary }}>{t('list_select_single')}</Text>
|
||||
</Button>
|
||||
<Button onPress={switchModeRange} style={{ ...styles.btn, backgroundColor: selectMode == 'range' ? theme.secondary40 : 'rgba(0,0,0,0)' }}>
|
||||
<Text style={{ color: theme.secondary }}>{t('list_select_range')}</Text>
|
||||
</Button>
|
||||
</View>
|
||||
<TouchableOpacity onPress={onSelectAll} style={styles.btn}>
|
||||
<Text style={{ color: theme.secondary }}>{t(isSelectAll ? 'list_select_unall' : 'list_select_all')}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={onCancel} style={styles.btn}>
|
||||
<Text style={{ color: theme.secondary }}>{t('list_select_cancel')}</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
), [animaStyle, isSelectAll, selectMode, onCancel, onSelectAll, switchModeRange, switchModeSingle, t, theme])
|
||||
|
||||
return !visible && animatePlayed ? null : component
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
height: 40,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
switchBtn: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
},
|
||||
btn: {
|
||||
// flex: 1,
|
||||
paddingLeft: 15,
|
||||
paddingRight: 15,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
26
src/components/OnlineList/Footer.js
Normal file
@ -0,0 +1,26 @@
|
||||
import React, { memo } from 'react'
|
||||
import { View, Text } from 'react-native'
|
||||
import { useGetter } from '@/store'
|
||||
import { useTranslation } from '@/plugins/i18n'
|
||||
|
||||
export const Loading = memo(() => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<View style={{ alignItems: 'center', padding: 10 }}>
|
||||
<Text style={{ color: theme.normal30 }}>{t('list_loading')}</Text>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
export const End = memo(() => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<View style={{ alignItems: 'center', padding: 10 }}>
|
||||
<Text style={{ color: theme.normal30 }}>{t('list_end')}</Text>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
130
src/components/OnlineList/ListItem.js
Normal file
@ -0,0 +1,130 @@
|
||||
import React, { useCallback, memo, useRef, useMemo } from 'react'
|
||||
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native'
|
||||
import { useGetter } from '@/store'
|
||||
import Button from '@/components/common/Button'
|
||||
import Badge from '@/components/common/Badge'
|
||||
import { BorderWidths } from '@/theme'
|
||||
import { useTranslation } from '@/plugins/i18n'
|
||||
import Icon from '@/components/common/Icon'
|
||||
|
||||
const useQualityTag = musicInfo => {
|
||||
const { t } = useTranslation()
|
||||
let info = {}
|
||||
if (musicInfo._types.ape || musicInfo._types.flac) {
|
||||
info.type = 'secondary'
|
||||
info.text = t('quality_lossless')
|
||||
} else if (musicInfo._types['320k']) {
|
||||
info.type = 'tertiary'
|
||||
info.text = t('quality_high_quality')
|
||||
} else info = null
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
export default memo(({ item, index, onPress, showMenu, handleLongPress, selectedList }) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
|
||||
const isSelected = selectedList.indexOf(item) != -1
|
||||
|
||||
const moreButtonRef = useRef()
|
||||
const handleShowMenu = useCallback(() => {
|
||||
if (moreButtonRef.current && moreButtonRef.current.measure) {
|
||||
moreButtonRef.current.measure((fx, fy, width, height, px, py) => {
|
||||
// console.log(fx, fy, width, height, px, py)
|
||||
showMenu(item, index, { x: Math.ceil(px), y: Math.ceil(py), w: Math.ceil(width), h: Math.ceil(height) })
|
||||
})
|
||||
}
|
||||
}, [item, index, showMenu])
|
||||
const tagInfo = useQualityTag(item)
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.listItem, backgroundColor: isSelected ? theme.secondary45 : theme.primary, borderBottomColor: theme.secondary45 }}>
|
||||
<TouchableOpacity style={styles.listItemLeft} onPress={ () => { onPress(item, index) }} onLongPress={() => { handleLongPress(item, index) }}>
|
||||
<Text style={{ ...styles.sn, color: theme.normal50 }}>{index + 1}</Text>
|
||||
<View style={styles.itemInfo}>
|
||||
<View style={styles.listItemTitle}>
|
||||
<Text style={{ ...styles.listItemTitleText, color: theme.normal }}>{item.name}</Text>
|
||||
{ tagInfo ? <Badge type={tagInfo.type}>{tagInfo.text}</Badge> : null }
|
||||
</View>
|
||||
<View style={styles.row2}><Text style={{ ...styles.listItemSingle, color: theme.normal40 }}>{item.singer}</Text></View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.listItemRight}>
|
||||
<TouchableOpacity onPress={handleShowMenu} ref={moreButtonRef} style={styles.moreButton}>
|
||||
<Icon name="dots-vertical" style={{ color: theme.normal35 }} size={16} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}, (prevProps, nextProps) => {
|
||||
return !!(prevProps.item === nextProps.item &&
|
||||
prevProps.index === nextProps.index &&
|
||||
nextProps.selectedList.includes(nextProps.item) == prevProps.selectedList.includes(nextProps.item)
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listItem: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'nowrap',
|
||||
borderBottomWidth: BorderWidths.normal,
|
||||
// paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
listItemLeft: {
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
// backgroundColor: 'rgba(0,0,0,0.1)',
|
||||
},
|
||||
sn: {
|
||||
width: 32,
|
||||
fontSize: 11,
|
||||
textAlign: 'center',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
paddingLeft: 3,
|
||||
paddingRight: 3,
|
||||
},
|
||||
itemInfo: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
listItemTitle: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
listItemTitleText: {
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
fontSize: 14,
|
||||
},
|
||||
listItemSingle: {
|
||||
fontSize: 12,
|
||||
paddingTop: 2,
|
||||
},
|
||||
listItemBadge: {
|
||||
fontSize: 10,
|
||||
paddingLeft: 5,
|
||||
paddingBottom: 2,
|
||||
},
|
||||
listItemRight: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexBasis: 'auto',
|
||||
justifyContent: 'center',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
},
|
||||
moreButton: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
})
|
||||
|
279
src/components/OnlineList/index.js
Normal file
@ -0,0 +1,279 @@
|
||||
import React, { useState, useCallback, memo, useMemo, useRef, useEffect } from 'react'
|
||||
import { StyleSheet, FlatList, View } from 'react-native'
|
||||
import { useGetter, useDispatch } from '@/store'
|
||||
import Menu from '@/components/common/Menu'
|
||||
import MusicAddModal from '@/components/MusicAddModal'
|
||||
import MusicMultiAddModal from '@/components/MusicMultiAddModal'
|
||||
import ListItem from './ListItem'
|
||||
import ExitMultipleModeBar from './ExitMultipleModeBar'
|
||||
import LoadingMask from '@/components/common/LoadingMask'
|
||||
import { useTranslation } from '@/plugins/i18n'
|
||||
import { Loading as FooterLoading, End as FooterEnd } from './Footer'
|
||||
import { LIST_ID_PLAY_LATER } from '@/config/constant'
|
||||
|
||||
export default memo(({
|
||||
list,
|
||||
isEnd,
|
||||
page,
|
||||
isListRefreshing,
|
||||
// visibleLoadingMask,
|
||||
onRefresh,
|
||||
onLoadMore,
|
||||
isLoading,
|
||||
progressViewOffset,
|
||||
ListHeaderComponent,
|
||||
}) => {
|
||||
const defaultList = useGetter('list', 'defaultList')
|
||||
const defaultListRef = useRef(defaultList)
|
||||
const addMusicToList = useDispatch('list', 'listAdd')
|
||||
const setPlayList = useDispatch('player', 'setList')
|
||||
const setTempPlayList = useDispatch('player', 'setTempPlayList')
|
||||
const [buttonPosition, setButtonPosition] = useState({ w: 0, h: 0, x: 0, y: 0 })
|
||||
const selectedData = useRef({ data: null, index: -1 })
|
||||
const [visibleMenu, setVisibleMenu] = useState(false)
|
||||
const [visibleLoadingMask, setVisibleLoadingMask] = useState(false)
|
||||
const flatListRef = useRef()
|
||||
const { t } = useTranslation()
|
||||
const [visibleMusicAddModal, setVisibleMusicAddModal] = useState(false)
|
||||
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
|
||||
const isMultiSelectModeRef = useRef(isMultiSelectMode)
|
||||
const [selectedList, setSelectedList] = useState([])
|
||||
const selectedListRef = useRef([])
|
||||
const [visibleMusicMultiAddModal, setVisibleMusicMultiAddModal] = useState(false)
|
||||
const listRef = useRef([])
|
||||
const [selectMode, setSelectMode] = useState('single')
|
||||
const selectModeRef = useRef('single')
|
||||
const prevSelectIndexRef = useRef(-1)
|
||||
|
||||
useEffect(() => {
|
||||
defaultListRef.current = defaultList
|
||||
}, [defaultList])
|
||||
useEffect(() => {
|
||||
listRef.current = list
|
||||
}, [list])
|
||||
|
||||
const handlePlay = useCallback((targetSong, index) => {
|
||||
addMusicToList({
|
||||
musicInfo: targetSong,
|
||||
id: defaultListRef.current.id,
|
||||
})
|
||||
|
||||
const targetIndex = defaultListRef.current.list.findIndex(s => s.songmid === targetSong.songmid)
|
||||
if (targetIndex > -1) {
|
||||
setPlayList({
|
||||
list: defaultListRef.current,
|
||||
index: targetIndex,
|
||||
})
|
||||
}
|
||||
}, [addMusicToList, setPlayList])
|
||||
|
||||
const handleSelect = useCallback((item, index) => {
|
||||
if (selectModeRef.current == 'single') {
|
||||
const index = selectedListRef.current.indexOf(item)
|
||||
if (index < 0) {
|
||||
selectedListRef.current.push(item)
|
||||
// setSelectedItem({ item, isChecked: true })
|
||||
} else {
|
||||
selectedListRef.current.splice(index, 1)
|
||||
// setSelectedItem({ item, isChecked: false })
|
||||
}
|
||||
} else {
|
||||
if (selectedListRef.current.length) {
|
||||
const prevIndex = prevSelectIndexRef.current
|
||||
const currentIndex = index
|
||||
if (prevIndex == currentIndex) {
|
||||
selectedListRef.current = []
|
||||
} else if (currentIndex > prevIndex) {
|
||||
selectedListRef.current = listRef.current.slice(prevIndex, currentIndex + 1)
|
||||
} else {
|
||||
selectedListRef.current = listRef.current.slice(currentIndex, prevIndex + 1)
|
||||
selectedListRef.current.reverse()
|
||||
}
|
||||
} else {
|
||||
selectedListRef.current.push(item)
|
||||
prevSelectIndexRef.current = index
|
||||
}
|
||||
}
|
||||
setSelectedList([...selectedListRef.current])
|
||||
}, [])
|
||||
const handleSelectAll = useCallback(() => {
|
||||
if (!listRef.current.length) return
|
||||
if (selectedListRef.current.length == listRef.current.length) {
|
||||
selectedListRef.current = []
|
||||
} else {
|
||||
selectedListRef.current = [...listRef.current]
|
||||
}
|
||||
setSelectedList([...selectedListRef.current])
|
||||
}, [])
|
||||
|
||||
const handleSetSelectMode = useCallback(mode => {
|
||||
setSelectMode(mode)
|
||||
selectModeRef.current = mode
|
||||
if (mode == 'range' && selectedListRef.current.length) {
|
||||
prevSelectIndexRef.current = listRef.current.indexOf(selectedListRef.current[selectedListRef.current.length - 1])
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleCancelMultiSelect = useCallback(() => {
|
||||
setIsMultiSelectMode(false)
|
||||
isMultiSelectModeRef.current = false
|
||||
selectedListRef.current = []
|
||||
setSelectedList([])
|
||||
}, [])
|
||||
const handlePress = useCallback((item, index) => {
|
||||
if (isMultiSelectModeRef.current) {
|
||||
handleSelect(item, index)
|
||||
} else {
|
||||
handlePlay(item, index)
|
||||
}
|
||||
}, [handlePlay, handleSelect])
|
||||
|
||||
const handleLongPress = useCallback((item, index) => {
|
||||
setIsMultiSelectMode(true)
|
||||
isMultiSelectModeRef.current = true
|
||||
handleSelect(item, index)
|
||||
}, [handleSelect])
|
||||
|
||||
const menus = useMemo(() => {
|
||||
return [
|
||||
{ action: 'play', label: t('play') },
|
||||
{ action: 'playLater', label: t('play_later') },
|
||||
// { action: 'copyName', label: t('copy_name') },
|
||||
// { action: 'download', label: '下载' },
|
||||
// { action: 'add', label: '添加到...' },
|
||||
// { action: 'move', label: '移动到...' },
|
||||
{ action: 'add', label: t('add_to') },
|
||||
]
|
||||
}, [t])
|
||||
const showMenu = useCallback((item, index, position) => {
|
||||
setButtonPosition({ ...position })
|
||||
selectedData.current.data = item
|
||||
selectedData.current.index = index
|
||||
setVisibleMenu(true)
|
||||
}, [setButtonPosition])
|
||||
const hideMenu = useCallback(() => {
|
||||
setVisibleMenu(false)
|
||||
}, [setVisibleMenu])
|
||||
const handleMenuPress = useCallback(({ action }) => {
|
||||
switch (action) {
|
||||
case 'play':
|
||||
handlePlay(selectedData.current.data, selectedData.current.index)
|
||||
break
|
||||
case 'playLater':
|
||||
if (selectedListRef.current.length) {
|
||||
setTempPlayList(selectedListRef.current.map(s => ({ listId: LIST_ID_PLAY_LATER, musicInfo: s })))
|
||||
handleCancelMultiSelect()
|
||||
} else {
|
||||
setTempPlayList([{ listId: LIST_ID_PLAY_LATER, musicInfo: selectedData.current.data }])
|
||||
}
|
||||
break
|
||||
// case 'copyName':
|
||||
// break
|
||||
case 'add':
|
||||
console.log(selectedListRef.current.length)
|
||||
selectedListRef.current.length
|
||||
? setVisibleMusicMultiAddModal(true)
|
||||
: setVisibleMusicAddModal(true)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, [handleCancelMultiSelect, handlePlay, setTempPlayList])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading && page == 1) {
|
||||
setVisibleLoadingMask(true)
|
||||
} else {
|
||||
setVisibleLoadingMask(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading])
|
||||
|
||||
useEffect(() => {
|
||||
if (!flatListRef.current) return
|
||||
if (page == 1) flatListRef.current.scrollToOffset({ offset: 0, animated: true })
|
||||
}, [list, page])
|
||||
|
||||
const hideMusicAddModal = useCallback(() => {
|
||||
setVisibleMusicAddModal(false)
|
||||
}, [])
|
||||
|
||||
const hideMusicMultiAddModal = useCallback(() => {
|
||||
setVisibleMusicMultiAddModal(false)
|
||||
}, [])
|
||||
|
||||
const loadingMaskmomponent = useMemo(() => (
|
||||
<LoadingMask visible={visibleLoadingMask} />
|
||||
), [visibleLoadingMask])
|
||||
const exitMultipleModeBtn = useMemo(() => (
|
||||
<ExitMultipleModeBar
|
||||
multipleMode={isMultiSelectMode}
|
||||
onCancel={handleCancelMultiSelect}
|
||||
onSwitchMode={handleSetSelectMode}
|
||||
onSelectAll={handleSelectAll}
|
||||
selectMode={selectMode}
|
||||
isSelectAll={selectedList.length && list.length == selectedList.length} />
|
||||
), [handleCancelMultiSelect, handleSelectAll, handleSetSelectMode, isMultiSelectMode, list, selectMode, selectedList])
|
||||
|
||||
const renderItem = useCallback(({ item, index }) => (
|
||||
<ListItem
|
||||
item={item}
|
||||
index={index}
|
||||
onPress={handlePress}
|
||||
showMenu={showMenu}
|
||||
selectedList={selectedList}
|
||||
handleLongPress={handleLongPress} />
|
||||
), [handleLongPress, handlePress, selectedList, showMenu])
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
style={styles.list}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
data={list}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.songmid.toString()}
|
||||
onRefresh={onRefresh}
|
||||
refreshing={isListRefreshing}
|
||||
maxToRenderPerBatch={8}
|
||||
updateCellsBatchingPeriod={80}
|
||||
windowSize={18}
|
||||
removeClippedSubviews={true}
|
||||
initialNumToRender={15}
|
||||
onEndReached={onLoadMore}
|
||||
progressViewOffset={progressViewOffset}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
ListFooterComponent={<View style={{ paddingBottom: isMultiSelectMode ? 40 : 0 }}>{isLoading ? <FooterLoading /> : isEnd ? <FooterEnd /> : null}</View>}
|
||||
/>
|
||||
{ exitMultipleModeBtn }
|
||||
<Menu
|
||||
menus={menus}
|
||||
buttonPosition={buttonPosition}
|
||||
onPress={handleMenuPress}
|
||||
visible={visibleMenu}
|
||||
hideMenu={hideMenu} />
|
||||
<MusicAddModal
|
||||
visible={visibleMusicAddModal}
|
||||
hideModal={hideMusicAddModal}
|
||||
musicInfo={selectedData.current.data} />
|
||||
<MusicMultiAddModal
|
||||
visible={visibleMusicMultiAddModal}
|
||||
hideModal={hideMusicMultiAddModal}
|
||||
list={selectedListRef.current}
|
||||
onAdd={handleCancelMultiSelect} />
|
||||
{ loadingMaskmomponent }
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
list: {
|
||||
flex: 1,
|
||||
},
|
||||
exitMultipleModeBtn: {
|
||||
height: 40,
|
||||
},
|
||||
})
|
||||
|
35
src/components/SearchInput.js
Normal file
@ -0,0 +1,35 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react'
|
||||
import { View, StyleSheet } from 'react-native'
|
||||
import Input from './common/Input'
|
||||
|
||||
const SearchInput = (props, ref) => {
|
||||
const textInputRef = useRef()
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
blur() {
|
||||
if (!textInputRef.current) return
|
||||
textInputRef.current.blur()
|
||||
},
|
||||
}))
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.container, ...props.styles }}>
|
||||
<Input {...props} ref={textInputRef} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
// backgroundColor: AppColors.secondary40,
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
width: '100%',
|
||||
// borderBottomLeftRadius: 10,
|
||||
// borderBottomRightRadius: 10,
|
||||
},
|
||||
})
|
||||
|
||||
export default forwardRef(SearchInput)
|
46
src/components/common/Badge.js
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { memo, useMemo } from 'react'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
import { useGetter } from '@/store'
|
||||
// const menuItemHeight = 42
|
||||
// const menuItemWidth = 100
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
text: {
|
||||
// paddingLeft: 4,
|
||||
// paddingRight: 4,
|
||||
fontSize: 9,
|
||||
// borderRadius: 2,
|
||||
// lineHeight: 12,
|
||||
marginTop: 2,
|
||||
marginLeft: 5,
|
||||
marginBottom: 2,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
export default memo(({ type, children }) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
// console.log(visible)
|
||||
const colors = useMemo(() => {
|
||||
const colors = {}
|
||||
switch (type) {
|
||||
case 'normal':
|
||||
// colors.bgColor = theme.primary
|
||||
colors.textColor = theme.normal10
|
||||
break
|
||||
case 'secondary':
|
||||
// colors.bgColor = theme.primary
|
||||
colors.textColor = theme.secondary10
|
||||
break
|
||||
case 'tertiary':
|
||||
// colors.bgColor = theme.primary
|
||||
colors.textColor = theme.tertiary10
|
||||
break
|
||||
}
|
||||
return colors
|
||||
}, [type, theme])
|
||||
|
||||
return <Text style={{ ...styles.text, color: colors.textColor }}>{children}</Text>
|
||||
})
|
||||
|
30
src/components/common/Button.js
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { useMemo, useRef, useImperativeHandle, forwardRef } from 'react'
|
||||
import { Pressable } from 'react-native'
|
||||
import { useGetter } from '@/store'
|
||||
// import { AppColors } from '@/theme'
|
||||
|
||||
const Btn = ({ ripple: propsRipple, children, disabled, style, ...props }, ref) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
const btnRef = useRef()
|
||||
const ripple = useMemo(() => ({
|
||||
color: theme.secondary30,
|
||||
...(propsRipple || {}),
|
||||
}), [theme, propsRipple])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
measure(callback) {
|
||||
if (!btnRef.current) return
|
||||
btnRef.current.measure(callback)
|
||||
},
|
||||
}))
|
||||
|
||||
return (
|
||||
<Pressable android_ripple={ripple} disabled={disabled} style={{ opacity: disabled ? 0.5 : 1, ...style }} {...props} ref={btnRef}>
|
||||
{children}
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default forwardRef(Btn)
|
||||
|
89
src/components/common/CheckBox.js
Normal file
@ -0,0 +1,89 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { StyleSheet, View, TouchableOpacity, Text } from 'react-native'
|
||||
import CheckBox from '@react-native-community/checkbox'
|
||||
|
||||
import { useGetter } from '@/store'
|
||||
|
||||
export default ({ check, label, children, onChange, disabled = false, need = false, marginRight = 0, marginBottom = 0 }) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
const [isDisabled, setDisabled] = useState(false)
|
||||
const tintColors = useMemo(() => {
|
||||
return {
|
||||
true: theme.secondary,
|
||||
false: theme.normal35,
|
||||
}
|
||||
}, [theme])
|
||||
const disabledTintColors = useMemo(() => {
|
||||
return {
|
||||
true: theme.secondary30,
|
||||
false: theme.normal60,
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
if (need) {
|
||||
if (check) {
|
||||
if (!isDisabled) setDisabled(true)
|
||||
} else {
|
||||
if (isDisabled) setDisabled(false)
|
||||
}
|
||||
} else {
|
||||
isDisabled && setDisabled(false)
|
||||
}
|
||||
}, [check, need, isDisabled])
|
||||
|
||||
const handleLabelPress = useCallback(() => {
|
||||
if (isDisabled) return
|
||||
onChange && onChange(!check)
|
||||
}, [isDisabled, onChange, check])
|
||||
|
||||
|
||||
const contentStyle = StyleSheet.compose(styles.content, { marginBottom })
|
||||
const labelStyle = StyleSheet.compose(styles.label, { marginRight })
|
||||
|
||||
return (
|
||||
disabled
|
||||
? (
|
||||
<View style={contentStyle}>
|
||||
<CheckBox style={styles.checkbox} value={check} disabled={true} tintColors={disabledTintColors} />
|
||||
<View style={labelStyle}>{label ? <Text style={{ ...styles.name, color: theme.normal40 }}>{label}</Text> : children}</View>
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<View style={contentStyle}>
|
||||
<CheckBox value={check} disabled={isDisabled} onValueChange={onChange} tintColors={tintColors} />
|
||||
<TouchableOpacity style={labelStyle} activeOpacity={0.5} onPress={handleLabelPress}>
|
||||
{label ? <Text style={{ ...styles.name, color: theme.normal }}>{label}</Text> : children}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
content: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
marginRight: 15,
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
},
|
||||
checkbox: {
|
||||
flex: 0,
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
},
|
||||
label: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
// marginRight: 15,
|
||||
// alignItems: 'center',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
// paddingRight: 8,
|
||||
},
|
||||
name: {
|
||||
marginTop: 2,
|
||||
fontSize: 13,
|
||||
},
|
||||
})
|
||||
|
128
src/components/common/ChoosePath/List.js
Normal file
@ -0,0 +1,128 @@
|
||||
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'
|
||||
import { View, StyleSheet, FlatList, Text } from 'react-native'
|
||||
import { readDir, externalStorageDirectoryPath } from '@/utils/fs'
|
||||
import { toast } from '@/utils/tools'
|
||||
import { useTranslation } from '@/plugins/i18n'
|
||||
import { useGetter, useDispatch } from '@/store'
|
||||
import Modal from '@/components/common/Modal'
|
||||
|
||||
import Header from './components/Header'
|
||||
import Main from './components/Main'
|
||||
import Footer from './components/Footer'
|
||||
import { sizeFormate } from '@/utils'
|
||||
// let prevPath = externalStorageDirectoryPath
|
||||
|
||||
const caches = {}
|
||||
|
||||
const handleReadDir = (path, dirOnly, filter, isRefresh = false) => {
|
||||
const cacheKey = `${path}_${dirOnly ? 'true' : 'false'}_${filter ? filter.toString() : 'null'}`
|
||||
if (!isRefresh && caches[cacheKey]) return Promise.resolve(caches[cacheKey])
|
||||
return readDir(path).then(paths => {
|
||||
// console.log('read')
|
||||
// prevPath = path
|
||||
const list = []
|
||||
// console.log(paths)
|
||||
for (const path of paths) {
|
||||
// console.log(path)
|
||||
const isDirectory = path.isDirectory()
|
||||
if (dirOnly) {
|
||||
if (!isDirectory) continue
|
||||
list.push({
|
||||
name: path.name,
|
||||
path: path.path,
|
||||
mtime: path.mtime,
|
||||
size: path.size,
|
||||
isDir: true,
|
||||
})
|
||||
} else {
|
||||
if (filter != null && path.isFile() && !filter.test(path.name)) continue
|
||||
|
||||
list.push({
|
||||
name: path.name,
|
||||
path: path.path,
|
||||
mtime: path.mtime,
|
||||
size: path.size,
|
||||
isDir: isDirectory,
|
||||
sizeText: isDirectory ? '' : sizeFormate(path.size),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
list.sort((a, b) => a.name.charCodeAt(0) - b.name.charCodeAt(0))
|
||||
caches[cacheKey] = list
|
||||
return list
|
||||
})
|
||||
}
|
||||
|
||||
export default ({ dirOnly = false, filter, onConfirm, title, granted, visible, hide }) => {
|
||||
const [path, setPath] = useState(externalStorageDirectoryPath)
|
||||
const [list, setList] = useState([])
|
||||
const isUnmountedRef = useRef(true)
|
||||
const isReadingDir = useRef(false)
|
||||
const theme = useGetter('common', 'theme')
|
||||
|
||||
useEffect(() => {
|
||||
isUnmountedRef.current = false
|
||||
return () => isUnmountedRef.current = true
|
||||
}, [])
|
||||
|
||||
const readDir = useCallback((path, dirOnly, filter, isRefresh) => {
|
||||
if (isReadingDir.current) return
|
||||
isReadingDir.current = true
|
||||
return handleReadDir(path, dirOnly, filter, isRefresh).then(list => {
|
||||
if (isUnmountedRef.current) return
|
||||
setList(list)
|
||||
setPath(path)
|
||||
}).catch(err => {
|
||||
toast(`Read dir error: ${err.message}`, 'long')
|
||||
// console.log('prevPath', prevPath)
|
||||
// if (isReadingDir.current) return
|
||||
// setPath(prevPath)
|
||||
}).finally(() => {
|
||||
isReadingDir.current = false
|
||||
})
|
||||
}, [setPath])
|
||||
|
||||
useEffect(() => {
|
||||
// console.log(granted)
|
||||
if (!granted) return
|
||||
readDir(path, dirOnly, filter)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [granted, filter, dirOnly])
|
||||
|
||||
const onSetPath = useCallback(pathInfo => {
|
||||
// console.log('onSetPath')
|
||||
if (pathInfo.isDir) {
|
||||
readDir(pathInfo.path, dirOnly, filter)
|
||||
} else {
|
||||
onConfirm(pathInfo.path)
|
||||
// setPath(pathInfo.path)
|
||||
}
|
||||
}, [dirOnly, filter, onConfirm, readDir])
|
||||
|
||||
|
||||
const toParentDir = useCallback(() => {
|
||||
const parentPath = path.substring(0, path.lastIndexOf('/'))
|
||||
readDir(parentPath.length ? parentPath : externalStorageDirectoryPath, dirOnly, filter)
|
||||
}, [dirOnly, filter, path, readDir])
|
||||
|
||||
// const dirList = useMemo(() => [parentDir, ...list], [list, parentDir])
|
||||
|
||||
return (
|
||||
<Modal visible={visible} hideModal={hide} bgHide={false}>
|
||||
<View style={{ ...styles.container, backgroundColor: theme.primary }}>
|
||||
<Header refreshDir={path => readDir(path, dirOnly, filter, true)} title={title} path={path} />
|
||||
<Main list={list} granted={granted} toParentDir={toParentDir} onSetPath={onSetPath} />
|
||||
<Footer onConfirm={() => onConfirm(path)} hide={hide} dirOnly={dirOnly} />
|
||||
</View>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
})
|
||||
|
40
src/components/common/ChoosePath/components/Footer.js
Normal file
@ -0,0 +1,40 @@
|
||||
import React, { memo } from 'react'
|
||||
import { View, StyleSheet, Text } from 'react-native'
|
||||
import { useTranslation } from '@/plugins/i18n'
|
||||
import { useGetter } from '@/store'
|
||||
import Button from '@/components/common/Button'
|
||||
import { BorderWidths } from '@/theme'
|
||||
|
||||
export default memo(({ onConfirm, hide, dirOnly }) => {
|
||||
const { t } = useTranslation()
|
||||
const theme = useGetter('common', 'theme')
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.footer, borderTopColor: theme.secondary30 }} >
|
||||
<Button style={{ ...styles.footerBtn, backgroundColor: theme.secondary45, width: dirOnly ? '50%' : '100%' }} onPress={hide}>
|
||||
<Text style={{ color: theme.secondary_5 }}>{t('cancel')}</Text>
|
||||
</Button>
|
||||
{dirOnly
|
||||
? <Button style={{ ...styles.footerBtn, backgroundColor: theme.secondary45 }} onPress={onConfirm}>
|
||||
<Text style={{ fontSize: 14, color: theme.secondary_5 }}>{t('confirm')}</Text>
|
||||
</Button>
|
||||
: null
|
||||
}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
footer: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: BorderWidths.normal2,
|
||||
},
|
||||
footerBtn: {
|
||||
width: '50%',
|
||||
paddingTop: 15,
|
||||
paddingBottom: 15,
|
||||
alignItems: 'center',
|
||||
},
|
||||
})
|
127
src/components/common/ChoosePath/components/Header.js
Normal file
@ -0,0 +1,127 @@
|
||||
import React, { useCallback, memo, useRef, useState } from 'react'
|
||||
import { StyleSheet, View, Text, TouchableOpacity, StatusBar, InteractionManager } from 'react-native'
|
||||
import { useGetter } from '@/store'
|
||||
import Icon from '@/components/common/Icon'
|
||||
import Input from '@/components/common/Input'
|
||||
import ConfirmAlert from '@/components/common/ConfirmAlert'
|
||||
import { useTranslation } from '@/plugins/i18n'
|
||||
import { toast } from '@/utils/tools'
|
||||
import { mkdir } from '@/utils/fs'
|
||||
const filterFileName = /[\\/:*?#"<>|]/
|
||||
|
||||
export default memo(({ title, path, refreshDir }) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
const [visibleNewFolder, setVisibleNewFolder] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const [text, setText] = useState('')
|
||||
// const moreButtonRef = useRef()
|
||||
// const handleShowMenu = useCallback(() => {
|
||||
// if (moreButtonRef.current && moreButtonRef.current.measure) {
|
||||
// moreButtonRef.current.measure((fx, fy, width, height, px, py) => {
|
||||
// // console.log(fx, fy, width, height, px, py)
|
||||
// showMenu(item, index, { x: Math.ceil(px), y: Math.ceil(py), w: Math.ceil(width), h: Math.ceil(height) })
|
||||
// })
|
||||
// }
|
||||
// }, [item, index, showMenu])
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
refreshDir(path)
|
||||
})
|
||||
}, [refreshDir, path])
|
||||
|
||||
const handleCancelNewFolderAlert = useCallback(() => {
|
||||
setVisibleNewFolder(false)
|
||||
setText('')
|
||||
}, [])
|
||||
const handleConfirmNewFolderAlert = useCallback(() => {
|
||||
if (filterFileName.test(text)) {
|
||||
toast(t('create_new_folder_error_tip'), 'long')
|
||||
return
|
||||
}
|
||||
const newPath = `${path}/${text}`
|
||||
mkdir(newPath).then(() => {
|
||||
refreshDir(path).then(() => {
|
||||
refreshDir(newPath)
|
||||
})
|
||||
setText('')
|
||||
}).catch(err => {
|
||||
toast('Create failed: ' + err.message)
|
||||
})
|
||||
setVisibleNewFolder(false)
|
||||
}, [path, refreshDir, t, text])
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={{ ...styles.header, backgroundColor: theme.secondary }} onStartShouldSetResponder={() => true}>
|
||||
<View style={styles.titleContent}>
|
||||
<Text style={{ ...styles.title, color: theme.primary }} numberOfLines={1}>{title}</Text>
|
||||
<Text style={{ color: theme.primary, fontSize: 12 }} numberOfLines={1}>{path}</Text>
|
||||
</View>
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={() => setVisibleNewFolder(true)}><Icon name="folder-plus" style={{ color: theme.primary, fontSize: 20 }} /></TouchableOpacity>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={refresh}><Icon name="autorenew" style={{ color: theme.primary, fontSize: 20 }} /></TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<ConfirmAlert
|
||||
visible={visibleNewFolder}
|
||||
onCancel={handleCancelNewFolderAlert}
|
||||
onConfirm={handleConfirmNewFolderAlert}
|
||||
>
|
||||
<View style={styles.newFolderContent}>
|
||||
<Text style={{ color: theme.normal, marginBottom: 5 }}>{t('create_new_folder')}</Text>
|
||||
<Input
|
||||
placeholder={t('create_new_folder_tip')}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
style={{ ...styles.input, backgroundColor: theme.secondary40 }}
|
||||
/>
|
||||
</View>
|
||||
</ConfirmAlert>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexDirection: 'row',
|
||||
paddingLeft: 15,
|
||||
paddingRight: 15,
|
||||
paddingTop: StatusBar.currentHeight,
|
||||
alignItems: 'center',
|
||||
},
|
||||
titleContent: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
height: 57,
|
||||
paddingRight: 5,
|
||||
// paddingBottom: 10,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
paddingTop: 10,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
},
|
||||
actionBtn: {
|
||||
padding: 8,
|
||||
},
|
||||
newFolderContent: {
|
||||
flexShrink: 1,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
input: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
minWidth: 240,
|
||||
borderRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
fontSize: 12,
|
||||
},
|
||||
})
|
||||
|