mirror of
https://github.com/ikun0014/lx-music-mobile.git
synced 2025-05-23 22:37:41 +08:00
v1.0.0
This commit is contained in:
parent
3a16de6ea7
commit
4fdf309fe3
@ -7,7 +7,3 @@ indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Windows files
|
||||
[*.bat]
|
||||
end_of_line = crlf
|
||||
|
53
.eslintrc.js
53
.eslintrc.js
@ -1,10 +1,10 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'standard',
|
||||
'standard-with-typescript',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
plugins: ['html', 'react'],
|
||||
parser: '@babel/eslint-parser',
|
||||
rules: {
|
||||
'no-new': 'off',
|
||||
camelcase: 'off',
|
||||
@ -16,12 +16,47 @@ module.exports = {
|
||||
eqeqeq: 'off',
|
||||
'no-multiple-empty-lines': [1, { max: 2 }],
|
||||
'comma-dangle': [2, 'always-multiline'],
|
||||
'react/jsx-uses-react': 'error',
|
||||
'react/jsx-uses-vars': 'error',
|
||||
'standard/no-callback-literal': 'off',
|
||||
'prefer-const': 'off',
|
||||
'no-labels': 'off',
|
||||
'node/no-callback-literal': 'off',
|
||||
},
|
||||
settings: {
|
||||
'html/html-extensions': ['.jsx'],
|
||||
},
|
||||
ignorePatterns: ['vendors', '*.min.js'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
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'],
|
||||
'standard/no-callback-literal': 'off',
|
||||
'prefer-const': 'off',
|
||||
'no-labels': 'off',
|
||||
'node/no-callback-literal': 'off',
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/space-before-function-paren': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': [1, {
|
||||
allowBoolean: true,
|
||||
}],
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
'@typescript-eslint/return-await': 'off',
|
||||
'multiline-ternary': 'off',
|
||||
'@typescript-eslint/comma-dangle': 'off',
|
||||
'@typescript-eslint/no-dynamic-delete': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
},
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
67
.flowconfig
67
.flowconfig
@ -1,67 +0,0 @@
|
||||
[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
|
||||
|
||||
.*/node_modules/resolve/test/resolver/malformed_package_json/package\.json$
|
||||
|
||||
[untyped]
|
||||
.*/node_modules/@react-native-community/cli/.*/.*
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
node_modules/react-native/interface.js
|
||||
node_modules/react-native/flow/
|
||||
|
||||
[options]
|
||||
emoji=true
|
||||
|
||||
exact_by_default=true
|
||||
|
||||
format.bracket_spacing=false
|
||||
|
||||
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.170.0
|
3
.gitattributes
vendored
3
.gitattributes
vendored
@ -1,3 +0,0 @@
|
||||
# Windows files should use crlf line endings
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
*.bat text eol=crlf
|
2
.github/workflows/beta-pack.yml
vendored
2
.github/workflows/beta-pack.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '18'
|
||||
|
||||
- name: Cache Gradle Wrapper
|
||||
uses: actions/cache@v3
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '18'
|
||||
|
||||
- name: Cache Gradle Wrapper
|
||||
uses: actions/cache@v3
|
||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -20,6 +20,7 @@ DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
ios/.xcode.env.local
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
@ -30,6 +31,7 @@ local.properties
|
||||
keystore.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
|
||||
# node.js
|
||||
#
|
||||
@ -50,9 +52,10 @@ buck-out/
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
*/fastlane/report.xml
|
||||
*/fastlane/Preview.html
|
||||
*/fastlane/screenshots
|
||||
**/fastlane/report.xml
|
||||
**/fastlane/Preview.html
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/test_output
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
19
.ncurc.js
19
.ncurc.js
@ -1,17 +1,16 @@
|
||||
module.exports = {
|
||||
upgrade: true,
|
||||
// target: 'newest',
|
||||
reject: [
|
||||
'metro-react-native-babel-preset',
|
||||
'readable-stream',
|
||||
'stream-browserify',
|
||||
'url',
|
||||
'util',
|
||||
'babel-jest',
|
||||
'jest',
|
||||
|
||||
// 'metro-react-native-babel-preset',
|
||||
'@types/react-native',
|
||||
'react-native',
|
||||
'react',
|
||||
'react-test-renderer',
|
||||
]
|
||||
|
||||
// target: 'patch',
|
||||
// filter: [
|
||||
// 'react-native',
|
||||
// '@types/react-native',
|
||||
// 'react'
|
||||
// ],
|
||||
}
|
||||
|
1
.node-version
Normal file
1
.node-version
Normal file
@ -0,0 +1 @@
|
||||
16
|
@ -1 +1 @@
|
||||
2.7.4
|
||||
2.7.5
|
||||
|
30
.vscode/i18n-ally-custom-framework.yml
vendored
Normal file
30
.vscode/i18n-ally-custom-framework.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# .vscode/i18n-ally-custom-framework.yml
|
||||
|
||||
# An array of strings which contain Language Ids defined by VS Code
|
||||
# You can check avaliable language ids here: https://code.visualstudio.com/docs/languages/overview#_language-id
|
||||
languageIds:
|
||||
- javascript
|
||||
- javascriptreact
|
||||
- typescript
|
||||
- typescriptreact
|
||||
|
||||
# An array of RegExes to find the key usage. **The key should be captured in the first match group**.
|
||||
# You should unescape RegEx strings in order to fit in the YAML file
|
||||
# To help with this, you can use https://www.freeformatter.com/json-escape.html
|
||||
usageMatchRegex:
|
||||
# The following example shows how to detect `t("your.i18n.keys")`
|
||||
# the `{key}` will be placed by a proper keypath matching regex,
|
||||
# you can ignore it and use your own matching rules as well
|
||||
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
|
||||
|
||||
|
||||
# An array of strings containing refactor templates.
|
||||
# The "$1" will be replaced by the keypath specified.
|
||||
# Optional: uncomment the following two lines to use
|
||||
|
||||
# refactorTemplates:
|
||||
# - i18n.get("$1")
|
||||
|
||||
|
||||
# If set to true, only enables this custom framework (will disable all built-in frameworks)
|
||||
monopoly: true
|
32
.vscode/javascript.code-snippets
vendored
32
.vscode/javascript.code-snippets
vendored
@ -16,7 +16,7 @@
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
"Import translation": {
|
||||
"scope": "javascript,typescript",
|
||||
"scope": "javascript,typescript,typescriptreact",
|
||||
"prefix": "imtl",
|
||||
"body": [
|
||||
"import { useTranslation } from '@/plugins/i18n'",
|
||||
@ -24,16 +24,32 @@
|
||||
],
|
||||
"description": "Translation Language"
|
||||
},
|
||||
"Import store hook": {
|
||||
"scope": "javascript,typescript",
|
||||
"prefix": "imsh",
|
||||
"Import store setting": {
|
||||
"scope": "javascript,typescript,typescriptreact",
|
||||
"prefix": "imss",
|
||||
"body": [
|
||||
"import { useGetter, useDispatch } from '@/store'"
|
||||
"import settingState from '@/store/setting/state'"
|
||||
],
|
||||
"description": "Import store hook"
|
||||
"description": "Import store setting"
|
||||
},
|
||||
"Import store player": {
|
||||
"scope": "javascript,typescript,typescriptreact",
|
||||
"prefix": "imsp",
|
||||
"body": [
|
||||
"import playerState from '@/store/player/state'"
|
||||
],
|
||||
"description": "Import store player"
|
||||
},
|
||||
"Import store list": {
|
||||
"scope": "javascript,typescript,typescriptreact",
|
||||
"prefix": "imsl",
|
||||
"body": [
|
||||
"import listState from '@/store/list/state'"
|
||||
],
|
||||
"description": "Import store list"
|
||||
},
|
||||
"Import toast": {
|
||||
"scope": "javascript,typescript",
|
||||
"scope": "javascript,typescript,typescriptreact",
|
||||
"prefix": "imts",
|
||||
"body": [
|
||||
"import { toast } from '@/utils/tools'",
|
||||
@ -42,7 +58,7 @@
|
||||
"description": "Import toast"
|
||||
},
|
||||
"Use getter theme": {
|
||||
"scope": "javascript,typescript",
|
||||
"scope": "javascript,typescript,typescriptreact",
|
||||
"prefix": "ugt",
|
||||
"body": [
|
||||
"const theme = useGetter('common', 'theme')"
|
||||
|
4
Gemfile
4
Gemfile
@ -1,4 +1,6 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby '2.7.4'
|
||||
ruby '2.7.5'
|
||||
|
||||
gem 'cocoapods', '~> 1.11', '>= 1.11.2'
|
||||
|
100
Gemfile.lock
100
Gemfile.lock
@ -1,100 +0,0 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
rexml
|
||||
activesupport (6.1.5)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
algoliasearch (1.27.5)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
atomos (0.1.3)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.11.3)
|
||||
addressable (~> 2.8)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.11.3)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 1.4.0, < 2.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.4.0, < 2.0)
|
||||
cocoapods-try (>= 1.1.0, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
fourflusher (>= 2.3.0, < 3.0)
|
||||
gh_inspector (~> 1.0)
|
||||
molinillo (~> 0.8.0)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (>= 1.0, < 3.0)
|
||||
xcodeproj (>= 1.21.0, < 2.0)
|
||||
cocoapods-core (1.11.3)
|
||||
activesupport (>= 5.0, < 7)
|
||||
addressable (~> 2.8)
|
||||
algoliasearch (~> 1.0)
|
||||
concurrent-ruby (~> 1.1)
|
||||
fuzzy_match (~> 2.0.4)
|
||||
nap (~> 1.0)
|
||||
netrc (~> 0.11)
|
||||
public_suffix (~> 4.0)
|
||||
typhoeus (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.5)
|
||||
cocoapods-downloader (1.6.3)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.1)
|
||||
cocoapods-trunk (1.6.0)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.2.0)
|
||||
colored2 (3.1.2)
|
||||
concurrent-ruby (1.1.10)
|
||||
escape (0.0.4)
|
||||
ethon (0.15.0)
|
||||
ffi (>= 1.15.0)
|
||||
ffi (1.15.5)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.10.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.6.1)
|
||||
minitest (5.15.0)
|
||||
molinillo (0.8.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
netrc (0.11.0)
|
||||
public_suffix (4.0.7)
|
||||
rexml (3.2.5)
|
||||
ruby-macho (2.5.1)
|
||||
typhoeus (1.4.0)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
xcodeproj (1.21.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
zeitwerk (2.5.4)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
cocoapods (~> 1.11, >= 1.11.2)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.7.4p191
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.27
|
201
LICENSE
201
LICENSE
@ -1,201 +0,0 @@
|
||||
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.
|
@ -152,30 +152,17 @@ android {
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode verCode
|
||||
versionName verName
|
||||
multiDexEnabled true
|
||||
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
// We configure the NDK build only if you decide to opt-in for the New Architecture.
|
||||
// We configure the CMake build only if you decide to opt-in for the New Architecture.
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
arguments "APP_PLATFORM=android-21",
|
||||
"APP_STL=c++_shared",
|
||||
"NDK_TOOLCHAIN_VERSION=clang",
|
||||
"GENERATED_SRC_DIR=$buildDir/generated/source",
|
||||
"PROJECT_BUILD_DIR=$buildDir",
|
||||
"REACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
|
||||
"REACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build"
|
||||
cFlags "-Wall", "-Werror", "-fexceptions", "-frtti", "-DWITH_INSPECTOR=1"
|
||||
cppFlags "-std=c++17"
|
||||
// Make sure this target name is the same you specify inside the
|
||||
// src/main/jni/Android.mk file for the `LOCAL_MODULE` variable.
|
||||
targets "lxmusicmobile_appmodules"
|
||||
|
||||
|
||||
// Fix for windows limit on number of character in file paths and in command lines
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
arguments "NDK_APP_SHORT_COMMANDS=true"
|
||||
}
|
||||
cmake {
|
||||
arguments "-DPROJECT_BUILD_DIR=$buildDir",
|
||||
"-DREACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
|
||||
"-DREACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build",
|
||||
"-DNODE_MODULES_DIR=$rootDir/../node_modules",
|
||||
"-DANDROID_STL=c++_shared"
|
||||
}
|
||||
}
|
||||
if (!enableSeparateBuildPerCPUArchitecture) {
|
||||
@ -185,11 +172,12 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
// We configure the NDK build only if you decide to opt-in for the New Architecture.
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path "$projectDir/src/main/jni/Android.mk"
|
||||
cmake {
|
||||
path "$projectDir/src/main/jni/CMakeLists.txt"
|
||||
}
|
||||
}
|
||||
def reactAndroidProjectDir = project(':ReactAndroid').projectDir
|
||||
@ -209,16 +197,17 @@ android {
|
||||
// preBuild.dependsOn("generateCodegenArtifactsFromSchema")
|
||||
preDebugBuild.dependsOn(packageReactNdkDebugLibs)
|
||||
preReleaseBuild.dependsOn(packageReactNdkReleaseLibs)
|
||||
|
||||
// Due to a bug inside AGP, we have to explicitly set a dependency
|
||||
// between configureNdkBuild* tasks and the preBuild tasks.
|
||||
// between configureCMakeDebug* tasks and the preBuild tasks.
|
||||
// This can be removed once this is solved: https://issuetracker.google.com/issues/207403732
|
||||
configureNdkBuildRelease.dependsOn(preReleaseBuild)
|
||||
configureNdkBuildDebug.dependsOn(preDebugBuild)
|
||||
configureCMakeRelWithDebInfo.dependsOn(preReleaseBuild)
|
||||
configureCMakeDebug.dependsOn(preDebugBuild)
|
||||
reactNativeArchitectures().each { architecture ->
|
||||
tasks.findByName("configureNdkBuildDebug[${architecture}]")?.configure {
|
||||
tasks.findByName("configureCMakeDebug[${architecture}]")?.configure {
|
||||
dependsOn("preDebugBuild")
|
||||
}
|
||||
tasks.findByName("configureNdkBuildRelease[${architecture}]")?.configure {
|
||||
tasks.findByName("configureCMakeRelWithDebInfo[${architecture}]")?.configure {
|
||||
dependsOn("preReleaseBuild")
|
||||
}
|
||||
}
|
||||
@ -303,11 +292,8 @@ dependencies {
|
||||
|
||||
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'
|
||||
implementation 'commons-io:commons-io:2.11.0'
|
||||
implementation 'org.apache.commons:commons-compress:1.22'
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
@ -323,9 +309,10 @@ dependencies {
|
||||
}
|
||||
|
||||
if (enableHermes) {
|
||||
def hermesPath = "../../node_modules/hermes-engine/android/";
|
||||
debugImplementation files(hermesPath + "hermes-debug.aar")
|
||||
releaseImplementation files(hermesPath + "hermes-release.aar")
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation("com.facebook.react:hermes-engine:+") { // From node_modules
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
@ -338,7 +325,11 @@ if (isNewArchitectureEnabled()) {
|
||||
configurations.all {
|
||||
resolutionStrategy.dependencySubstitution {
|
||||
substitute(module("com.facebook.react:react-native"))
|
||||
.using(project(":ReactAndroid")).because("On New Architecture we're building React Native from source")
|
||||
.using(project(":ReactAndroid"))
|
||||
.because("On New Architecture we're building React Native from source")
|
||||
substitute(module("com.facebook.react:hermes-engine"))
|
||||
.using(project(":ReactAndroid:hermes-engine"))
|
||||
.because("On New Architecture we're building Hermes from source")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -361,4 +352,3 @@ def isNewArchitectureEnabled() {
|
||||
}
|
||||
|
||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
|
||||
|
4
android/app/proguard-rules.pro
vendored
4
android/app/proguard-rules.pro
vendored
@ -9,5 +9,5 @@
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
-keep class com.facebook.hermes.unicode.** { *; }
|
||||
-keep class com.facebook.jni.** { *; }
|
||||
# -keep class com.facebook.hermes.unicode.** { *; }
|
||||
# -keep class com.facebook.jni.** { *; }
|
||||
|
@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cn.toside.music.mobile">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cn.toside.music.mobile">
|
||||
|
||||
<!-- 获取读写外置存储权限 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
@ -11,42 +10,40 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<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"
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<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>
|
||||
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|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<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>
|
||||
|
Binary file not shown.
@ -1,15 +1,33 @@
|
||||
package cn.toside.music.mobile;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.reactnativenavigation.NavigationActivity;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreen;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.ReactRootView;
|
||||
|
||||
public class MainActivity extends NavigationActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
SplashScreen.show(this, R.style.SplashScreenTheme, true); // here
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static class MainActivityDelegate extends ReactActivityDelegate {
|
||||
public MainActivityDelegate(NavigationActivity activity, String mainComponentName) {
|
||||
super(activity, mainComponentName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactRootView createRootView() {
|
||||
ReactRootView reactRootView = new ReactRootView(getContext());
|
||||
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED);
|
||||
return reactRootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isConcurrentRootEnabled() {
|
||||
// If you opted-in for the New Architecture, we enable Concurrent Root (i.e. React 18).
|
||||
// More on this on https://reactjs.org/blog/2022/03/29/react-v18.html
|
||||
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,22 @@
|
||||
package cn.toside.music.mobile;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import com.facebook.react.PackageList;
|
||||
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 com.facebook.react.config.ReactFeatureFlags;
|
||||
import com.reactnativenavigation.NavigationApplication;
|
||||
import com.reactnativenavigation.react.NavigationReactNativeHost;
|
||||
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import cn.toside.music.mobile.newarchitecture.MainApplicationReactNativeHost;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
import cn.toside.music.mobile.cache.CachePackage;
|
||||
import cn.toside.music.mobile.gzip.GzipPackage;
|
||||
import cn.toside.music.mobile.lyric.LyricPackage;
|
||||
import cn.toside.music.mobile.newarchitecture.MainApplicationReactNativeHost;
|
||||
import cn.toside.music.mobile.utils.UtilsPackage;
|
||||
|
||||
public class MainApplication extends NavigationApplication {
|
||||
@ -34,10 +34,10 @@ public class MainApplication extends NavigationApplication {
|
||||
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());
|
||||
packages.add(new GzipPackage());
|
||||
packages.add(new LyricPackage());
|
||||
packages.add(new UtilsPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@ -48,7 +48,8 @@ public class MainApplication extends NavigationApplication {
|
||||
};
|
||||
|
||||
private final ReactNativeHost mNewArchitectureNativeHost =
|
||||
new MainApplicationReactNativeHost(this);
|
||||
new MainApplicationReactNativeHost(this);
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
|
@ -196,8 +196,8 @@ public class Lyric extends LyricPlayer {
|
||||
refreshLyric();
|
||||
}
|
||||
|
||||
public void setColor(String color) {
|
||||
lyricView.setColor(color);
|
||||
public void setPlayedColor(String unplayColor, String playedColor, String shadowColor) {
|
||||
lyricView.setColor(unplayColor, playedColor, shadowColor);
|
||||
}
|
||||
|
||||
public void setAlpha(float alpha) { lyricView.setAlpha(alpha); }
|
||||
|
@ -107,8 +107,8 @@ public class LyricModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void setColor(String themeColor, Promise promise) {
|
||||
if (lyric != null) lyric.setColor(themeColor);
|
||||
public void setColor(String unplayColor, String playedColor, String shadowColor, Promise promise) {
|
||||
if (lyric != null) lyric.setPlayedColor(unplayColor, playedColor, shadowColor);
|
||||
promise.resolve(null);
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,8 @@ import java.util.regex.Pattern;
|
||||
public class LyricPlayer {
|
||||
final String timeFieldExp = "^(?:\\[[\\d:.]+])+";
|
||||
final String timeExp = "[\\d:.]+";
|
||||
final String timeLabelRxp = "^(\\[[\\d:]+\\.)0+(\\d+])";
|
||||
final String timeLabelFixRxp = "(?:\\.0+|0+)$";
|
||||
// HashMap tagRegMap;
|
||||
Pattern timeFieldPattern;
|
||||
Pattern timePattern;
|
||||
@ -124,8 +126,9 @@ public class LyricPlayer {
|
||||
Matcher timeMatchResult = timePattern.matcher(timeField);
|
||||
while (timeMatchResult.find()) {
|
||||
String timeStr = timeMatchResult.group();
|
||||
if (!timeStr.contains(".")) timeStr += ".0";
|
||||
timeStr = timeStr.replaceAll("(?:\\.0+|0+)$", "");
|
||||
if (timeStr.contains(".")) timeStr = timeStr.replaceAll(timeLabelRxp, "$1$2");
|
||||
else timeStr += ".0";
|
||||
timeStr = timeStr.replaceAll(timeLabelFixRxp, "");
|
||||
HashMap targetLine = (HashMap) linesMap.get(timeStr);
|
||||
if (targetLine != null) ((ArrayList<String>) targetLine.get("extendedLyrics")).add(text);
|
||||
}
|
||||
@ -151,8 +154,9 @@ public class LyricPlayer {
|
||||
Matcher timeMatchResult = timePattern.matcher(timeField);
|
||||
while (timeMatchResult.find()) {
|
||||
String timeStr = timeMatchResult.group();
|
||||
if (!timeStr.contains(".")) timeStr += ".0";
|
||||
timeStr = timeStr.replaceAll("(?:\\.0+|0+)$", "");
|
||||
if (timeStr.contains(".")) timeStr = timeStr.replaceAll(timeLabelRxp, "$1$2");
|
||||
else timeStr += ".0";
|
||||
timeStr = timeStr.replaceAll(timeLabelFixRxp, "");
|
||||
if (linesMap.containsKey(timeStr)) {
|
||||
((ArrayList<String>) ((HashMap) linesMap.get(timeStr)).get("extendedLyrics")).add(text);
|
||||
continue;
|
||||
|
@ -27,10 +27,13 @@ public final class LyricSwitchView extends TextSwitcher {
|
||||
// private final boolean isSingleLine;
|
||||
private boolean isShowAnima;
|
||||
|
||||
private boolean isSingleLine;
|
||||
|
||||
public LyricSwitchView(Context context, boolean isSingleLine, boolean isShowAnima) {
|
||||
super(context);
|
||||
// this.isSingleLine = isSingleLine;
|
||||
this.isShowAnima = isShowAnima;
|
||||
this.isSingleLine = isSingleLine;
|
||||
|
||||
if (isSingleLine) {
|
||||
viewArray = new ArrayList<>(2);
|
||||
@ -38,9 +41,9 @@ public final class LyricSwitchView extends TextSwitcher {
|
||||
textView2 = new LyricTextView(context);
|
||||
viewArray.add(textView);
|
||||
viewArray.add(textView2);
|
||||
for (TextView v : viewArray) {
|
||||
v.setShadowLayer(0.1f, 0, 0, Color.BLACK);
|
||||
}
|
||||
// for (TextView v : viewArray) {
|
||||
// v.setShadowLayer(0.1f, 0, 0, Color.BLACK);
|
||||
// }
|
||||
} else {
|
||||
viewArray = new ArrayList<>(2);
|
||||
textView = new TextView(context);
|
||||
@ -48,7 +51,7 @@ public final class LyricSwitchView extends TextSwitcher {
|
||||
viewArray.add(textView);
|
||||
viewArray.add(textView2);
|
||||
for (TextView v : viewArray) {
|
||||
v.setShadowLayer(0.2f, 0, 0, Color.BLACK);
|
||||
// v.setShadowLayer(0.2f, 0, 0, Color.BLACK);
|
||||
v.setEllipsize(TextUtils.TruncateAt.END);
|
||||
}
|
||||
}
|
||||
@ -149,6 +152,16 @@ public final class LyricSwitchView extends TextSwitcher {
|
||||
for (TextView v : viewArray) v.setTextColor(i);
|
||||
}
|
||||
|
||||
public void setShadowColor(int i) {
|
||||
float radius;
|
||||
if (isSingleLine) {
|
||||
radius = 0.1f;
|
||||
} else {
|
||||
radius = 0.2f;
|
||||
}
|
||||
for (TextView v : viewArray) v.setShadowLayer(radius, 0, 0, i);
|
||||
}
|
||||
|
||||
public void setSourceText(CharSequence str) {
|
||||
for (TextView v : viewArray) v.setText(str);
|
||||
}
|
||||
|
@ -64,6 +64,12 @@ public class LyricTextView extends TextView {
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShadowLayer(float radius, float dx, float dy, int shadowColor) {
|
||||
if (mPaint != null) mPaint.setShadowLayer(radius, dx, dy, shadowColor);
|
||||
post(mStartScrollRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTextSize(float size) {
|
||||
super.setTextSize(size);
|
||||
|
@ -23,6 +23,8 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import cn.toside.music.mobile.R;
|
||||
|
||||
@ -51,7 +53,9 @@ public class LyricView extends Activity implements View.OnTouchListener {
|
||||
private boolean isLock = false;
|
||||
private boolean isSingleLine = false;
|
||||
private boolean isShowToggleAnima = false;
|
||||
private String themeColor = "#07c556";
|
||||
private String unplayColor = "rgba(255, 255, 255, 1)";
|
||||
private String playedColor = "rgba(7, 197, 86, 1)";
|
||||
private String shadowColor = "rgba(0, 0, 0, 0.15)";
|
||||
// private String lastText = "LX Music ^-^";
|
||||
private String textX = "LEFT";
|
||||
private String textY = "TOP";
|
||||
@ -204,7 +208,9 @@ public class LyricView extends Activity implements View.OnTouchListener {
|
||||
isLock = options.getBoolean("isLock", isLock);
|
||||
isSingleLine = options.getBoolean("isSingleLine", isSingleLine);
|
||||
isShowToggleAnima = options.getBoolean("isShowToggleAnima", isShowToggleAnima);
|
||||
themeColor = options.getString("themeColor", themeColor);
|
||||
unplayColor = options.getString("unplayColor", unplayColor);
|
||||
playedColor = options.getString("playedColor", playedColor);
|
||||
shadowColor = options.getString("shadowColor", shadowColor);
|
||||
prevViewPercentageX = (float) options.getDouble("lyricViewX", 0f) / 100f;
|
||||
prevViewPercentageY = (float) options.getDouble("lyricViewY", 0f) / 100f;
|
||||
textX = options.getString("textX", textX);
|
||||
@ -225,13 +231,28 @@ public class LyricView extends Activity implements View.OnTouchListener {
|
||||
}
|
||||
listenOrientationEvent();
|
||||
}
|
||||
public static int parseColor(String input) {
|
||||
if (input.startsWith("#")) return Color.parseColor(input);
|
||||
Pattern c = Pattern.compile("rgba? *\\( *(\\d+), *(\\d+), *(\\d+)(?:, *([\\d.]+))? *\\)");
|
||||
Matcher m = c.matcher(input);
|
||||
if (m.matches()) {
|
||||
int red = Integer.parseInt(m.group(1));
|
||||
int green = Integer.parseInt(m.group(2));
|
||||
int blue = Integer.parseInt(m.group(3));
|
||||
float a = 1;
|
||||
if (m.group(4) != null) a = Float.parseFloat(m.group(4));
|
||||
return Color.argb((int) (a * 255), red, green, blue);
|
||||
}
|
||||
return Color.parseColor("#000000");
|
||||
}
|
||||
|
||||
private void createTextView() {
|
||||
textView = new LyricSwitchView(reactContext, isSingleLine, isShowToggleAnima);
|
||||
textView.setText("");
|
||||
textView.setText(currentLyric);
|
||||
|
||||
textView.setTextColor(Color.parseColor(themeColor));
|
||||
textView.setTextColor(parseColor(playedColor));
|
||||
textView.setShadowColor(parseColor(shadowColor));
|
||||
textView.setAlpha(alpha);
|
||||
textView.setTextSize(textSize);
|
||||
// Log.d("Lyric", "alpha: " + alpha + " text size: " + textSize);
|
||||
@ -248,7 +269,7 @@ public class LyricView extends Activity implements View.OnTouchListener {
|
||||
case "RIGHT":
|
||||
textPositionX = Gravity.END;
|
||||
break;
|
||||
case "left":
|
||||
case "Left":
|
||||
default:
|
||||
textPositionX = Gravity.START;
|
||||
break;
|
||||
@ -489,10 +510,13 @@ public class LyricView extends Activity implements View.OnTouchListener {
|
||||
windowManager.updateViewLayout(textView, layoutParams);
|
||||
}
|
||||
|
||||
public void setColor(String color) {
|
||||
themeColor = color;
|
||||
public void setColor(String unplayColor, String playedColor, String shadowColor) {
|
||||
this.unplayColor = unplayColor;
|
||||
this.playedColor = playedColor;
|
||||
this.shadowColor = shadowColor;
|
||||
if (textView == null) return;
|
||||
textView.setTextColor(Color.parseColor(color));
|
||||
textView.setTextColor(parseColor(playedColor));
|
||||
textView.setShadowColor(parseColor(shadowColor));
|
||||
// windowManager.updateViewLayout(textView, layoutParams);
|
||||
}
|
||||
|
||||
@ -510,7 +534,7 @@ public class LyricView extends Activity implements View.OnTouchListener {
|
||||
case "RIGHT":
|
||||
textPositionX = Gravity.END;
|
||||
break;
|
||||
case "left":
|
||||
case "LEFT":
|
||||
default:
|
||||
textPositionX = Gravity.START;
|
||||
break;
|
||||
|
@ -16,8 +16,8 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.UIManager;
|
||||
import com.facebook.react.fabric.ComponentFactory;
|
||||
import com.facebook.react.fabric.CoreComponentsRegistry;
|
||||
import com.facebook.react.fabric.EmptyReactNativeConfig;
|
||||
import com.facebook.react.fabric.FabricJSIModuleProvider;
|
||||
import com.facebook.react.fabric.ReactNativeConfig;
|
||||
import com.facebook.react.uimanager.ViewManagerRegistry;
|
||||
import cn.toside.music.mobile.BuildConfig;
|
||||
import cn.toside.music.mobile.newarchitecture.components.MainComponentsRegistry;
|
||||
@ -105,7 +105,7 @@ public class MainApplicationReactNativeHost extends ReactNativeHost {
|
||||
return new FabricJSIModuleProvider(
|
||||
reactApplicationContext,
|
||||
componentFactory,
|
||||
new EmptyReactNativeConfig(),
|
||||
ReactNativeConfig.DEFAULT_CONFIG,
|
||||
viewManagerRegistry);
|
||||
}
|
||||
});
|
||||
|
@ -1,8 +1,10 @@
|
||||
package cn.toside.music.mobile.newarchitecture.components;
|
||||
|
||||
import com.facebook.jni.HybridData;
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.react.fabric.ComponentFactory;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
/**
|
||||
* Class responsible to load the custom Fabric Components. This class has native methods and needs a
|
||||
* corresponding C++ implementation/header file to work correctly (already placed inside the jni/
|
||||
@ -16,13 +18,17 @@ public class MainComponentsRegistry {
|
||||
static {
|
||||
SoLoader.loadLibrary("fabricjni");
|
||||
}
|
||||
|
||||
@DoNotStrip private final HybridData mHybridData;
|
||||
|
||||
@DoNotStrip
|
||||
private native HybridData initHybrid(ComponentFactory componentFactory);
|
||||
|
||||
@DoNotStrip
|
||||
private MainComponentsRegistry(ComponentFactory componentFactory) {
|
||||
mHybridData = initHybrid(componentFactory);
|
||||
}
|
||||
|
||||
@DoNotStrip
|
||||
public static MainComponentsRegistry register(ComponentFactory componentFactory) {
|
||||
return new MainComponentsRegistry(componentFactory);
|
||||
|
@ -1,10 +1,12 @@
|
||||
package cn.toside.music.mobile.newarchitecture.modules;
|
||||
|
||||
import com.facebook.jni.HybridData;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.ReactPackageTurboModuleManagerDelegate;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class responsible to load the TurboModules. This class has native methods and needs a
|
||||
* corresponding C++ implementation/header file to work correctly (already placed inside the jni/
|
||||
@ -15,25 +17,31 @@ import java.util.List;
|
||||
*/
|
||||
public class MainApplicationTurboModuleManagerDelegate
|
||||
extends ReactPackageTurboModuleManagerDelegate {
|
||||
|
||||
private static volatile boolean sIsSoLibraryLoaded;
|
||||
|
||||
protected MainApplicationTurboModuleManagerDelegate(
|
||||
ReactApplicationContext reactApplicationContext, List<ReactPackage> packages) {
|
||||
super(reactApplicationContext, packages);
|
||||
}
|
||||
|
||||
protected native HybridData initHybrid();
|
||||
|
||||
native boolean canCreateTurboModule(String moduleName);
|
||||
|
||||
public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder {
|
||||
protected MainApplicationTurboModuleManagerDelegate build(
|
||||
ReactApplicationContext context, List<ReactPackage> packages) {
|
||||
return new MainApplicationTurboModuleManagerDelegate(context, packages);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void maybeLoadOtherSoLibraries() {
|
||||
if (!sIsSoLibraryLoaded) {
|
||||
// If you change the name of your application .so file in the Android.mk file,
|
||||
// make sure you update the name here as well.
|
||||
SoLoader.loadLibrary("lxmusic_appmodules");
|
||||
SoLoader.loadLibrary("cn_toside_music_mobile_appmodules");
|
||||
sIsSoLibraryLoaded = true;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import cn.toside.music.mobile.gzip.GzipModule;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
@ -1,40 +0,0 @@
|
||||
THIS_DIR := $(call my-dir)
|
||||
include $(REACT_ANDROID_DIR)/Android-prebuilt.mk
|
||||
# If you wish to add a custom TurboModule or Fabric component in your app you
|
||||
# will have to include the following autogenerated makefile.
|
||||
# include $(GENERATED_SRC_DIR)/codegen/jni/Android.mk
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_PATH := $(THIS_DIR)
|
||||
# You can customize the name of your application .so file here.
|
||||
LOCAL_MODULE := lxmusic_appmodules
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)
|
||||
LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp)
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
|
||||
# If you wish to add a custom TurboModule or Fabric component in your app you
|
||||
# will have to uncomment those lines to include the generated source
|
||||
# files from the codegen (placed in $(GENERATED_SRC_DIR)/codegen/jni)
|
||||
#
|
||||
# LOCAL_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni
|
||||
# LOCAL_SRC_FILES += $(wildcard $(GENERATED_SRC_DIR)/codegen/jni/*.cpp)
|
||||
# LOCAL_EXPORT_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni
|
||||
# Here you should add any native library you wish to depend on.
|
||||
LOCAL_SHARED_LIBRARIES := \
|
||||
libfabricjni \
|
||||
libfbjni \
|
||||
libfolly_futures \
|
||||
libfolly_json \
|
||||
libglog \
|
||||
libjsi \
|
||||
libreact_codegen_rncore \
|
||||
libreact_debug \
|
||||
libreact_nativemodule_core \
|
||||
libreact_render_componentregistry \
|
||||
libreact_render_core \
|
||||
libreact_render_debug \
|
||||
libreact_render_graphics \
|
||||
librrc_view \
|
||||
libruntimeexecutor \
|
||||
libturbomodulejsijni \
|
||||
libyoga
|
||||
LOCAL_CFLAGS := -DLOG_TAG=\"ReactNative\" -fexceptions -frtti -std=c++17 -Wall
|
||||
include $(BUILD_SHARED_LIBRARY)
|
7
android/app/src/main/jni/CMakeLists.txt
Normal file
7
android/app/src/main/jni/CMakeLists.txt
Normal file
@ -0,0 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
# Define the library name here.
|
||||
project(cn_toside_music_mobile_appmodules)
|
||||
|
||||
# This file includes all the necessary to let you build your application with the New Architecture.
|
||||
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)
|
@ -1,9 +1,13 @@
|
||||
#include "MainApplicationModuleProvider.h"
|
||||
|
||||
#include <rncli.h>
|
||||
#include <rncore.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
const std::string moduleName,
|
||||
const std::string &moduleName,
|
||||
const JavaTurboModule::InitParams ¶ms) {
|
||||
// Here you can provide your own module provider for TurboModules coming from
|
||||
// either your application or from external libraries. The approach to follow
|
||||
@ -14,7 +18,15 @@ std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
// return module;
|
||||
// }
|
||||
// return rncore_ModuleProvider(moduleName, params);
|
||||
|
||||
// Module providers autolinked by RN CLI
|
||||
auto rncli_module = rncli_ModuleProvider(moduleName, params);
|
||||
if (rncli_module != nullptr) {
|
||||
return rncli_module;
|
||||
}
|
||||
|
||||
return rncore_ModuleProvider(moduleName, params);
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
|
@ -1,11 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <ReactCommon/JavaTurboModule.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
const std::string moduleName,
|
||||
const std::string &moduleName,
|
||||
const JavaTurboModule::InitParams ¶ms);
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
|
@ -1,12 +1,15 @@
|
||||
#include "MainApplicationTurboModuleManagerDelegate.h"
|
||||
#include "MainApplicationModuleProvider.h"
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
jni::local_ref<MainApplicationTurboModuleManagerDelegate::jhybriddata>
|
||||
MainApplicationTurboModuleManagerDelegate::initHybrid(
|
||||
jni::alias_ref<jhybridobject>) {
|
||||
return makeCxxInstance();
|
||||
}
|
||||
|
||||
void MainApplicationTurboModuleManagerDelegate::registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod(
|
||||
@ -16,23 +19,27 @@ void MainApplicationTurboModuleManagerDelegate::registerNatives() {
|
||||
MainApplicationTurboModuleManagerDelegate::canCreateTurboModule),
|
||||
});
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule>
|
||||
MainApplicationTurboModuleManagerDelegate::getTurboModule(
|
||||
const std::string name,
|
||||
const std::shared_ptr<CallInvoker> jsInvoker) {
|
||||
const std::string &name,
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker) {
|
||||
// Not implemented yet: provide pure-C++ NativeModules here.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule>
|
||||
MainApplicationTurboModuleManagerDelegate::getTurboModule(
|
||||
const std::string name,
|
||||
const std::string &name,
|
||||
const JavaTurboModule::InitParams ¶ms) {
|
||||
return MainApplicationModuleProvider(name, params);
|
||||
}
|
||||
|
||||
bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule(
|
||||
std::string name) {
|
||||
const std::string &name) {
|
||||
return getTurboModule(name, nullptr) != nullptr ||
|
||||
getTurboModule(name, {.moduleName = name}) != nullptr;
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
|
@ -1,9 +1,12 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <ReactCommon/TurboModuleManagerDelegate.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
class MainApplicationTurboModuleManagerDelegate
|
||||
: public jni::HybridClass<
|
||||
MainApplicationTurboModuleManagerDelegate,
|
||||
@ -11,20 +14,25 @@ class MainApplicationTurboModuleManagerDelegate
|
||||
public:
|
||||
// Adapt it to the package you used for your Java class.
|
||||
static constexpr auto kJavaDescriptor =
|
||||
"Lcom/cn/toside/music/mobile/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;";
|
||||
"Lcn/toside/music/mobile/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;";
|
||||
|
||||
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject>);
|
||||
|
||||
static void registerNatives();
|
||||
|
||||
std::shared_ptr<TurboModule> getTurboModule(
|
||||
const std::string name,
|
||||
const std::shared_ptr<CallInvoker> jsInvoker) override;
|
||||
const std::string &name,
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker) override;
|
||||
std::shared_ptr<TurboModule> getTurboModule(
|
||||
const std::string name,
|
||||
const std::string &name,
|
||||
const JavaTurboModule::InitParams ¶ms) override;
|
||||
|
||||
/**
|
||||
* Test-only method. Allows user to verify whether a TurboModule can be
|
||||
* created by instances of this class.
|
||||
*/
|
||||
bool canCreateTurboModule(std::string name);
|
||||
bool canCreateTurboModule(const std::string &name);
|
||||
};
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
|
@ -1,14 +1,23 @@
|
||||
#include "MainComponentsRegistry.h"
|
||||
|
||||
#include <CoreComponentsRegistry.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
||||
#include <react/renderer/components/rncore/ComponentDescriptors.h>
|
||||
#include <rncli.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {}
|
||||
|
||||
std::shared_ptr<ComponentDescriptorProviderRegistry const>
|
||||
MainComponentsRegistry::sharedProviderRegistry() {
|
||||
auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry();
|
||||
|
||||
// Autolinked providers registered by RN CLI
|
||||
rncli_registerProviders(providerRegistry);
|
||||
|
||||
// Custom Fabric Components go here. You can register custom
|
||||
// components coming from your App or from 3rd party libraries here.
|
||||
//
|
||||
@ -16,11 +25,13 @@ MainComponentsRegistry::sharedProviderRegistry() {
|
||||
// AocViewerComponentDescriptor>());
|
||||
return providerRegistry;
|
||||
}
|
||||
|
||||
jni::local_ref<MainComponentsRegistry::jhybriddata>
|
||||
MainComponentsRegistry::initHybrid(
|
||||
jni::alias_ref<jclass>,
|
||||
ComponentFactory *delegate) {
|
||||
auto instance = makeCxxInstance(delegate);
|
||||
|
||||
auto buildRegistryFunction =
|
||||
[](EventDispatcher::Weak const &eventDispatcher,
|
||||
ContextContainer::Shared const &contextContainer)
|
||||
@ -28,21 +39,27 @@ MainComponentsRegistry::initHybrid(
|
||||
auto registry = MainComponentsRegistry::sharedProviderRegistry()
|
||||
->createComponentDescriptorRegistry(
|
||||
{eventDispatcher, contextContainer});
|
||||
|
||||
auto mutableRegistry =
|
||||
std::const_pointer_cast<ComponentDescriptorRegistry>(registry);
|
||||
|
||||
mutableRegistry->setFallbackComponentDescriptor(
|
||||
std::make_shared<UnimplementedNativeViewComponentDescriptor>(
|
||||
ComponentDescriptorParameters{
|
||||
eventDispatcher, contextContainer, nullptr}));
|
||||
|
||||
return registry;
|
||||
};
|
||||
|
||||
delegate->buildRegistryFunction = buildRegistryFunction;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void MainComponentsRegistry::registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
|
@ -1,24 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <ComponentFactory.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorRegistry.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
class MainComponentsRegistry
|
||||
: public facebook::jni::HybridClass<MainComponentsRegistry> {
|
||||
public:
|
||||
// Adapt it to the package you used for your Java class.
|
||||
constexpr static auto kJavaDescriptor =
|
||||
"Lcom/lxmusicmobile/newarchitecture/components/MainComponentsRegistry;";
|
||||
"Lcn/toside/music/mobile/newarchitecture/components/MainComponentsRegistry;";
|
||||
|
||||
static void registerNatives();
|
||||
|
||||
MainComponentsRegistry(ComponentFactory *delegate);
|
||||
|
||||
private:
|
||||
static std::shared_ptr<ComponentDescriptorProviderRegistry const>
|
||||
sharedProviderRegistry();
|
||||
|
||||
static jni::local_ref<jhybriddata> initHybrid(
|
||||
jni::alias_ref<jclass>,
|
||||
ComponentFactory *delegate);
|
||||
};
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
|
@ -1,6 +1,7 @@
|
||||
#include <fbjni/fbjni.h>
|
||||
#include "MainApplicationTurboModuleManagerDelegate.h"
|
||||
#include "MainComponentsRegistry.h"
|
||||
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
|
||||
return facebook::jni::initialize(vm, [] {
|
||||
facebook::react::MainApplicationTurboModuleManagerDelegate::
|
||||
|
@ -1,11 +0,0 @@
|
||||
<?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>
|
@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
<item name="android:forceDarkAllowed">false</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>
|
||||
<item name="android:forceDarkAllowed">true</item>
|
||||
</style>
|
||||
</resources>
|
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary_dark">#000000</color>
|
||||
</resources>
|
@ -6,10 +6,4 @@
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</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>
|
||||
|
@ -1,15 +1,13 @@
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "31.0.0"
|
||||
buildToolsVersion = "33.0.0"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 30
|
||||
// https://github.com/itinance/react-native-fs/issues/998#issuecomment-831337442
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 29
|
||||
kotlinVersion = "1.5.31" // Or any version above 1.3.x
|
||||
|
||||
kotlinVersion = "1.6.10" // Or any version above 1.3.x
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
|
||||
if (System.properties['os.arch'] == "aarch64") {
|
||||
@ -19,16 +17,15 @@ buildscript {
|
||||
// Otherwise we default to the side-by-side NDK version from AGP.
|
||||
ndkVersion = "21.4.7075529"
|
||||
}
|
||||
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.0.4")
|
||||
classpath("com.android.tools.build:gradle:7.2.1")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("de.undercouch:gradle-download-task:4.1.2")
|
||||
classpath("de.undercouch:gradle-download-task:5.0.1")
|
||||
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
|
||||
|
@ -31,6 +31,7 @@ FLIPPER_VERSION=0.125.0
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
# your application. You should enable this flag either if you want
|
||||
@ -44,10 +45,11 @@ newArchEnabled=false
|
||||
|
||||
|
||||
# AsyncStorage_dedicatedExecutor = true
|
||||
# https://react-native-async-storage.github.io/async-storage/docs/advanced/next
|
||||
AsyncStorage_useNextStorage=true
|
||||
AsyncStorage_kotlinVersion=1.4.32
|
||||
AsyncStorage_kotlinVersion=1.6.10
|
||||
# https://developer.android.com/jetpack/androidx/releases/room
|
||||
AsyncStorage_next_roomVersion=2.4.2
|
||||
|
||||
# https://github.com/wix/react-native-navigation/issues/7403
|
||||
# android.jetifier.blacklist = bcprov-jdk15on
|
||||
|
||||
AsyncStorage_next_roomVersion=2.3.0
|
||||
|
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -1,12 +1,11 @@
|
||||
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')
|
||||
rootProject.name = 'cn.toside.music.mobile'
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
include ':app'
|
||||
includeBuild('../node_modules/react-native-gradle-plugin')
|
||||
|
||||
if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") {
|
||||
include(":ReactAndroid")
|
||||
project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid')
|
||||
include(":ReactAndroid:hermes-engine")
|
||||
project(":ReactAndroid:hermes-engine").projectDir = file('../node_modules/react-native/ReactAndroid/hermes-engine')
|
||||
}
|
||||
|
@ -7,6 +7,12 @@ module.exports = {
|
||||
{
|
||||
root: ['.'],
|
||||
extensions: [
|
||||
'.android.ts',
|
||||
'.ios.ts',
|
||||
'.android.tsx',
|
||||
'.ios.tsx',
|
||||
'.tsx',
|
||||
'.ts',
|
||||
'.android.js',
|
||||
'.ios.js',
|
||||
'.android.jsx',
|
||||
|
174
index.js
174
index.js
@ -1,158 +1,26 @@
|
||||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
|
||||
// import '@/utils/log'
|
||||
import './shim'
|
||||
import '@/utils/errorHandle'
|
||||
import { init as initLog, log } from '@/utils/log'
|
||||
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, toggleTranslation, toggleRoma } from '@/utils/lyric'
|
||||
import { showLyric, onPositionChange } from '@/utils/lyricDesktop'
|
||||
import { init as initI18n, supportedLngs } from '@/plugins/i18n'
|
||||
import { deviceLanguage, getPlayInfo, toast, onAppearanceChange, getIsSupportedAutoTheme, getAppearance } from '@/utils/tools'
|
||||
import { LIST_ID_PLAY_TEMP } from '@/config/constant'
|
||||
import { connect, SYNC_CODE } from '@/plugins/sync'
|
||||
|
||||
console.log('starting app...')
|
||||
|
||||
let store
|
||||
let isInited = false
|
||||
let isFirstRun = true
|
||||
initLog()
|
||||
|
||||
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 setting = store.getState().common.setting
|
||||
|
||||
if (getIsSupportedAutoTheme()) {
|
||||
onAppearanceChange(color => {
|
||||
store.dispatch(commonAction.setSystemColor(color))
|
||||
})
|
||||
}
|
||||
|
||||
toggleTranslation(setting.player.isShowLyricTranslation)
|
||||
toggleRoma(setting.player.isShowLyricRoma)
|
||||
if (setting.sync.enable) {
|
||||
connect().catch(err => {
|
||||
if (err.message == SYNC_CODE.unknownServiceAddress) {
|
||||
store.dispatch(commonAction.setIsEnableSync(false))
|
||||
}
|
||||
})
|
||||
}
|
||||
if (setting.desktopLyric.enable) {
|
||||
showLyric({
|
||||
isShowToggleAnima: setting.desktopLyric.showToggleAnima,
|
||||
isSingleLine: setting.desktopLyric.isSingleLine,
|
||||
isLock: setting.desktopLyric.isLock,
|
||||
themeId: setting.desktopLyric.theme,
|
||||
opacity: setting.desktopLyric.style.opacity,
|
||||
textSize: setting.desktopLyric.style.fontSize,
|
||||
width: setting.desktopLyric.width,
|
||||
maxLineNum: setting.desktopLyric.maxLineNum,
|
||||
positionX: setting.desktopLyric.position.x,
|
||||
positionY: setting.desktopLyric.position.y,
|
||||
textPositionX: setting.desktopLyric.textPosition.x,
|
||||
textPositionY: setting.desktopLyric.textPosition.y,
|
||||
}).catch(() => {
|
||||
store.dispatch(commonAction.setIsShowDesktopLyric(false))
|
||||
})
|
||||
}
|
||||
onPositionChange(position => {
|
||||
store.dispatch(commonAction.setDesktopLyricPosition(position))
|
||||
})
|
||||
|
||||
let lang = 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
|
||||
if (info.listId != LIST_ID_PLAY_TEMP) {
|
||||
info.list = global.allList[info.listId]
|
||||
if (info.list) info.list = info.list.list
|
||||
}
|
||||
|
||||
if (!info.list || !info.list[info.index]) {
|
||||
const info2 = { ...info }
|
||||
if (info2.list) {
|
||||
info2.music = info2.list[info2.index]?.name
|
||||
info2.list = info2.list.length
|
||||
}
|
||||
toast('恢复播放数据失败,请去错误日志查看', 'long')
|
||||
log.warn('Restore Play Info failed: ', JSON.stringify(info2, null, 2))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let setting = store.getState().common.setting
|
||||
global.restorePlayInfo = {
|
||||
info,
|
||||
startupAutoPlay: setting.startupAutoPlay,
|
||||
}
|
||||
|
||||
store.dispatch(playerAction.setList({
|
||||
list: {
|
||||
list: info.list,
|
||||
id: info.listId,
|
||||
},
|
||||
index: info.index,
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
initNavigation(() => {
|
||||
init().then(() => {
|
||||
if (getIsSupportedAutoTheme()) store.dispatch(commonAction.setSystemColor(getAppearance()))
|
||||
|
||||
return navigations.pushHomeScreen().then(() => {
|
||||
SplashScreen.hide()
|
||||
if (store.getState().common.setting.isAgreePact) {
|
||||
if (isFirstRun) {
|
||||
isFirstRun = false
|
||||
store.dispatch(commonAction.checkVersion())
|
||||
}
|
||||
} else {
|
||||
if (isFirstRun) isFirstRun = false
|
||||
showPactModal()
|
||||
}
|
||||
})
|
||||
}).catch(err => {
|
||||
toast(err.stack, 'long')
|
||||
})
|
||||
})
|
||||
import './src/app'
|
||||
// import './test'
|
||||
// import '@/utils/errorHandle'
|
||||
// import { Navigation } from 'react-native-navigation'
|
||||
// import App from './App'
|
||||
|
||||
// Navigation.registerComponent('com.myApp.WelcomeScreen', () => App)
|
||||
// Navigation.events().registerAppLaunchedListener(() => {
|
||||
// Navigation.setRoot({
|
||||
// root: {
|
||||
// stack: {
|
||||
// children: [
|
||||
// {
|
||||
// component: {
|
||||
// name: 'com.myApp.WelcomeScreen',
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
11
ios/.xcode.env
Normal file
11
ios/.xcode.env
Normal file
@ -0,0 +1,11 @@
|
||||
# This `.xcode.env` file is versioned and is used to source the environment
|
||||
# used when running script phases inside Xcode.
|
||||
# To customize your local environment, you can create an `.xcode.env.local`
|
||||
# file that is not versioned.
|
||||
|
||||
# NODE_BINARY variable contains the PATH to the node executable.
|
||||
#
|
||||
# Customize the NODE_BINARY variable here.
|
||||
# For example, to use nvm with brew, add the following line
|
||||
# . "$(brew --prefix nvm)/nvm.sh" --no-use
|
||||
export NODE_BINARY=$(command -v node)
|
@ -256,13 +256,15 @@
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"$(SRCROOT)/.xcode.env.local",
|
||||
"$(SRCROOT)/.xcode.env",
|
||||
);
|
||||
name = "Bundle React Native code and images";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\n\nexport NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n";
|
||||
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
|
||||
};
|
||||
00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@ -436,7 +438,7 @@
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = LxMusicMobileTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -460,7 +462,7 @@
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
INFOPLIST_FILE = LxMusicMobileTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -576,7 +578,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
@ -640,7 +642,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
|
@ -1,8 +1,11 @@
|
||||
#import "AppDelegate.h"
|
||||
#import <ReactNativeNavigation/ReactNativeNavigation.h>
|
||||
|
||||
#import <React/RCTBridge.h>
|
||||
#import <React/RCTBundleURLProvider.h>
|
||||
#import <React/RCTRootView.h>
|
||||
|
||||
#import <React/RCTAppSetupUtils.h>
|
||||
|
||||
#if RCT_NEW_ARCH_ENABLED
|
||||
#import <React/CoreModulesPlugins.h>
|
||||
#import <React/RCTCxxBridgeDelegate.h>
|
||||
@ -10,7 +13,11 @@
|
||||
#import <React/RCTSurfacePresenter.h>
|
||||
#import <React/RCTSurfacePresenterBridgeAdapter.h>
|
||||
#import <ReactCommon/RCTTurboModuleManager.h>
|
||||
|
||||
#import <react/config/ReactNativeConfig.h>
|
||||
|
||||
static NSString *const kRNConcurrentRoot = @"concurrentRoot";
|
||||
|
||||
@interface AppDelegate () <RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate> {
|
||||
RCTTurboModuleManager *_turboModuleManager;
|
||||
RCTSurfacePresenterBridgeAdapter *_bridgeAdapter;
|
||||
@ -19,11 +26,16 @@
|
||||
}
|
||||
@end
|
||||
#endif
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
RCTAppSetupPrepareApp(application);
|
||||
|
||||
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
|
||||
[ReactNativeNavigation bootstrapWithBridge:bridge];
|
||||
|
||||
#if RCT_NEW_ARCH_ENABLED
|
||||
_contextContainer = std::make_shared<facebook::react::ContextContainer const>();
|
||||
_reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>();
|
||||
@ -31,19 +43,40 @@
|
||||
_bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer];
|
||||
bridge.surfacePresenter = _bridgeAdapter.surfacePresenter;
|
||||
#endif
|
||||
UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"LxMusicMobile", nil);
|
||||
if (@available(iOS 13.0, *)) {
|
||||
rootView.backgroundColor = [UIColor systemBackgroundColor];
|
||||
} else {
|
||||
rootView.backgroundColor = [UIColor whiteColor];
|
||||
}
|
||||
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||
UIViewController *rootViewController = [UIViewController new];
|
||||
rootViewController.view = rootView;
|
||||
self.window.rootViewController = rootViewController;
|
||||
[self.window makeKeyAndVisible];
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off.
|
||||
///
|
||||
/// @see: https://reactjs.org/blog/2022/03/29/react-v18.html
|
||||
/// @note: This requires to be rendering on Fabric (i.e. on the New Architecture).
|
||||
/// @return: `true` if the `concurrentRoot` feture is enabled. Otherwise, it returns `false`.
|
||||
- (BOOL)concurrentRootEnabled
|
||||
{
|
||||
// Switch this bool to turn on and off the concurrent root
|
||||
return true;
|
||||
}
|
||||
|
||||
- (NSDictionary *)prepareInitialProps
|
||||
{
|
||||
NSMutableDictionary *initProps = [NSMutableDictionary new];
|
||||
|
||||
#ifdef RCT_NEW_ARCH_ENABLED
|
||||
initProps[kRNConcurrentRoot] = @([self concurrentRootEnabled]);
|
||||
#endif
|
||||
|
||||
return initProps;
|
||||
}
|
||||
|
||||
- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge {
|
||||
return [ReactNativeNavigation extraModulesForBridge:bridge];
|
||||
}
|
||||
|
||||
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
|
||||
{
|
||||
#if DEBUG
|
||||
@ -52,8 +85,11 @@
|
||||
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
|
||||
#endif
|
||||
}
|
||||
|
||||
#if RCT_NEW_ARCH_ENABLED
|
||||
|
||||
#pragma mark - RCTCxxBridgeDelegate
|
||||
|
||||
- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
|
||||
{
|
||||
_turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
|
||||
@ -61,25 +97,32 @@
|
||||
jsInvoker:bridge.jsCallInvoker];
|
||||
return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager);
|
||||
}
|
||||
|
||||
#pragma mark RCTTurboModuleManagerDelegate
|
||||
|
||||
- (Class)getModuleClassFromName:(const char *)name
|
||||
{
|
||||
return RCTCoreModulesClassProvider(name);
|
||||
}
|
||||
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
|
||||
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
|
||||
initParams:
|
||||
(const facebook::react::ObjCTurboModule::InitParams &)params
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
|
||||
{
|
||||
return RCTAppSetupDefaultModuleFromClass(moduleClass);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
@ -50,6 +50,6 @@
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -47,12 +47,12 @@
|
||||
[[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
||||
|
||||
foundElement = [self findSubviewInView:vc.view
|
||||
matching:^BOOL(UIView *view) {
|
||||
if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}];
|
||||
matching:^BOOL(UIView *view) {
|
||||
if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}];
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
|
27
ios/Podfile
27
ios/Podfile
@ -1,7 +1,7 @@
|
||||
require_relative '../node_modules/react-native/scripts/react_native_pods'
|
||||
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
|
||||
|
||||
platform :ios, '11.0'
|
||||
platform :ios, '12.4'
|
||||
install! 'cocoapods', :deterministic_uuids => false
|
||||
|
||||
target 'LxMusicMobile' do
|
||||
@ -10,12 +10,18 @@ target 'LxMusicMobile' do
|
||||
# Flags change depending on the env values.
|
||||
flags = get_default_flags()
|
||||
|
||||
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
# to enable hermes on iOS, change `false` to `true` and then install pods
|
||||
:hermes_enabled => flags[:hermes_enabled],
|
||||
# Hermes is now enabled by default. Disable by setting this flag to false.
|
||||
# Upcoming versions of React Native may rely on get_default_flags(), but
|
||||
# we make it explicit here to aid in the React Native upgrade process.
|
||||
:hermes_enabled => true,
|
||||
:fabric_enabled => flags[:fabric_enabled],
|
||||
# Enables Flipper.
|
||||
#
|
||||
# Note that if you have use_frameworks! enabled, Flipper will not work and
|
||||
# you should disable the next line.
|
||||
:flipper_configuration => FlipperConfiguration.enabled,
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
)
|
||||
@ -25,14 +31,13 @@ target 'LxMusicMobile' do
|
||||
# Pods for testing
|
||||
end
|
||||
|
||||
# Enables Flipper.
|
||||
#
|
||||
# Note that if you have use_frameworks! enabled, Flipper will not work and
|
||||
# you should disable the next line.
|
||||
use_flipper!()
|
||||
|
||||
post_install do |installer|
|
||||
react_native_post_install(installer)
|
||||
react_native_post_install(
|
||||
installer,
|
||||
# Set `mac_catalyst_enabled` to `true` in order to apply patches
|
||||
# necessary for Mac Catalyst builds
|
||||
:mac_catalyst_enabled => false
|
||||
)
|
||||
__apply_Xcode_12_5_M1_post_install_workaround(installer)
|
||||
end
|
||||
end
|
||||
|
@ -1,16 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
@ -16,9 +16,9 @@ module.exports = {
|
||||
},
|
||||
resolver: {
|
||||
extraNodeModules: {
|
||||
console: require.resolve('console-browserify'),
|
||||
crypto: require.resolve('react-native-crypto'),
|
||||
crypto: require.resolve('react-native-quick-crypto'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
buffer: require.resolve('@craftzdog/react-native-buffer'),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
18354
package-lock.json
generated
18354
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
109
package.json
109
package.json
@ -1,24 +1,24 @@
|
||||
{
|
||||
"name": "lx-music-mobile",
|
||||
"version": "0.15.5",
|
||||
"versionCode": 52,
|
||||
"version": "1.0.0-beta.1",
|
||||
"versionCode": 53,
|
||||
"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 .",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"rd": "react-devtools",
|
||||
"menu": "adb shell input keyevent 82",
|
||||
"bundle-android": "react-native bundle --platform android --dev true --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res",
|
||||
"pack:android:debug": "./gradlew assembleDebug",
|
||||
"pack": "npm run pack:android",
|
||||
"pack:android": "./gradlew assembleRelease",
|
||||
"pack:android": "cd android && gradlew.bat assembleRelease",
|
||||
"clear": "react-native clean-project",
|
||||
"publish": "node publish",
|
||||
"nodeify": "rn-nodeify --hack --install process,crypto,events,constant,console,stream,url,util",
|
||||
"postinstall": "npm run nodeify"
|
||||
"build:theme": "node src/theme/themes/createThemes.js",
|
||||
"publish": "node publish"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16",
|
||||
@ -42,88 +42,41 @@
|
||||
},
|
||||
"homepage": "https://github.com/lyswhut/lx-music-mobile#readme",
|
||||
"dependencies": {
|
||||
"@craftzdog/react-native-buffer": "^6.0.5",
|
||||
"@react-native-async-storage/async-storage": "^1.17.11",
|
||||
"@react-native-clipboard/clipboard": "^1.11.1",
|
||||
"@react-native-community/checkbox": "^0.5.14",
|
||||
"@react-native-community/slider": "^4.3.3",
|
||||
"buffer": "^6.0.3",
|
||||
"console-browserify": "^1.2.0",
|
||||
"events": "^3.3.0",
|
||||
"i18next": "^22.1.5",
|
||||
"js-htmlencode": "^0.3.0",
|
||||
"lrc-file-parser": "^2.2.8",
|
||||
"@react-native-community/slider": "^4.4.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"lrc-file-parser": "^2.3.0",
|
||||
"pako": "^2.1.0",
|
||||
"process": "^0.11.10",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "17.0.2",
|
||||
"react-i18next": "^12.1.1",
|
||||
"react-native": "0.68.5",
|
||||
"react": "18.1.0",
|
||||
"react-native": "0.70.7",
|
||||
"react-native-background-timer": "^2.4.1",
|
||||
"react-native-crypto": "^2.2.0",
|
||||
"react-native-exception-handler": "^2.10.10",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-navigation": "^7.30.3",
|
||||
"react-native-pager-view": "^6.1.2",
|
||||
"react-native-randombytes": "^3.6.1",
|
||||
"react-native-splash-screen": "^3.3.0",
|
||||
"react-native-track-player": "git+https://github.com/lyswhut/react-native-track-player.git#5fb0bec8694d3783f32a1e4ed1251c163e9842f7",
|
||||
"react-native-navigation": "^7.32.1",
|
||||
"react-native-pager-view": "^6.1.4",
|
||||
"react-native-quick-base64": "^2.0.5",
|
||||
"react-native-quick-crypto": "^0.5.0",
|
||||
"react-native-track-player": "github:lyswhut/react-native-track-player#38027954a5ac6e3d92961745e0a9633fc647f47a",
|
||||
"react-native-vector-icons": "^9.2.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"readable-stream": "1.0.33",
|
||||
"redux": "^4.2.0",
|
||||
"redux-subscriber": "^1.1.0",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"reselect": "^4.1.7",
|
||||
"socket.io": "^4.5.4",
|
||||
"stream-browserify": "^1.0.0",
|
||||
"url": "~0.10.1",
|
||||
"util": "~0.10.3"
|
||||
"socket.io-client": "^4.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.5",
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.18.9",
|
||||
"@babel/runtime": "^7.20.6",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"changelog-parser": "^2.8.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.29.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-plugin-html": "^7.1.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@tsconfig/react-native": "^2.0.3",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-native": "^0.70.11",
|
||||
"@types/react-native-background-timer": "^2.0.0",
|
||||
"@types/react-native-vector-icons": "^6.4.13",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"eslint-config-standard-with-typescript": "^34.0.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"jest": "^26.6.3",
|
||||
"metro-react-native-babel-preset": "^0.67.0",
|
||||
"react-native-clean-project": "^4.0.1",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"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"
|
||||
"metro-react-native-babel-preset": "0.72.3",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,29 @@
|
||||
### 修复
|
||||
### 不兼容性变更说明
|
||||
|
||||
- 修复导入PC端v2列表文件歌曲信息转换丢失的问题
|
||||
- 修复上面问题导致的tx源评论加载失败的问题
|
||||
- 同步功能,该功能不支持与移动端v2.0.0之前版本的使用
|
||||
|
||||
### 新增
|
||||
|
||||
- 新增聚合搜索,注:由于这个方式需要对各个源的结果进行排序,所以需要以“歌曲名 歌手”的顺序输入(例如:突然的自我 伍佰),否则排序后的结果可能不是你想要的
|
||||
- 新增歌单搜索功能
|
||||
- 新增热门搜索显示,默认关闭,需要到设置-搜索设置开启
|
||||
- 新增搜索历史记录,默认关闭,需要到设置-搜索设置开启
|
||||
- 启动软件时自动回到上次的界面,例如上次退出软件时在我的收藏,下次启动软件时会自动进入我的收藏
|
||||
- 新增PC端所拥有的内置皮肤
|
||||
- 新增界面字体大小设置
|
||||
- 添加kg源评论图片展示(感谢@helloplhm-qwq)
|
||||
|
||||
### 优化(界面/交互/功能)
|
||||
|
||||
- 调整了首页的界面布局
|
||||
- 优化大屏幕下的字体大小及界面布局显示
|
||||
|
||||
### 优化(程序)
|
||||
|
||||
- 优化程序启动性能,优化与程序交互的流畅度
|
||||
- 重构整个程序,重新梳理了程序逻辑,使其更容易扩展及维护,将大部分代码从JavaScript迁移到TypeScript
|
||||
- 重写配置管理、列表管理功能,使其与PC端同步,更容易复用PC端的代码
|
||||
|
||||
### 其他
|
||||
|
||||
- 升级React Native到v0.70.7
|
||||
|
27
shim.js
27
shim.js
@ -1,26 +1 @@
|
||||
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')
|
||||
global.Buffer = require('buffer').Buffer
|
||||
|
45
src/app.ts
Normal file
45
src/app.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import '@/utils/errorHandle'
|
||||
// import { init as initLog, log } from '@/utils/log'
|
||||
import '@/config/globalData'
|
||||
import { init as initNavigation, navigations } from '@/navigation'
|
||||
import { getFontSize } from '@/utils/data'
|
||||
import { Alert } from 'react-native'
|
||||
import { exitApp } from './utils/nativeModules/utils'
|
||||
|
||||
console.log('starting app...')
|
||||
|
||||
initNavigation(async() => {
|
||||
global.lx.fontSize = await getFontSize()
|
||||
const { default: init } = await import('@/core/init')
|
||||
let handlePushedHomeScreen: () => void
|
||||
try {
|
||||
handlePushedHomeScreen = await init()
|
||||
} catch (err: any) {
|
||||
Alert.alert('初始化失败 (Init Failed)', err.stack ?? err.message, [
|
||||
{
|
||||
text: 'Exit',
|
||||
onPress() {
|
||||
exitApp()
|
||||
},
|
||||
},
|
||||
], {
|
||||
cancelable: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
navigations.pushHomeScreen().then(() => {
|
||||
handlePushedHomeScreen()
|
||||
}).catch((err: any) => {
|
||||
Alert.alert('Error', err.message, [
|
||||
{
|
||||
text: 'Exit',
|
||||
onPress() {
|
||||
exitApp()
|
||||
},
|
||||
},
|
||||
], {
|
||||
cancelable: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,71 +0,0 @@
|
||||
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
|
||||
|
@ -1,201 +0,0 @@
|
||||
import React, { useCallback, useMemo, memo, useState, useRef, useEffect } from 'react'
|
||||
import { View, StyleSheet, Text, ScrollView, TouchableOpacity } 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'
|
||||
import { BorderWidths } from '@/theme'
|
||||
import Input from '@/components/common/Input'
|
||||
import { toast } from '@/utils/tools'
|
||||
|
||||
const ListItem = ({ list, onPress, musicInfo, width }) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
const isExists = useMemo(() => {
|
||||
return list.list.some(s => s.songmid == musicInfo.songmid)
|
||||
}, [list, musicInfo])
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.listItem, width: width }}>
|
||||
<Button
|
||||
style={{ ...styles.button, backgroundColor: theme.secondary45, borderColor: theme.secondary45, opacity: isExists ? 0.6 : 1 }}
|
||||
onPress={() => { onPress(list, isExists) }}
|
||||
>
|
||||
<Text numberOfLines={1} style={{ fontSize: 12, color: isExists ? 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, color: theme.normal }}>{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
|
||||
)
|
||||
}
|
||||
|
||||
const CreateUserList = ({ isEdit, hideEdit }) => {
|
||||
const [text, setText] = useState('')
|
||||
const inputRef = useRef()
|
||||
const { t } = useTranslation()
|
||||
const theme = useGetter('common', 'theme')
|
||||
const createUserList = useDispatch('list', 'createUserList')
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit) {
|
||||
setText('')
|
||||
global.requestAnimationFrame(() => {
|
||||
inputRef.current.focus()
|
||||
})
|
||||
}
|
||||
}, [isEdit])
|
||||
|
||||
const handleSubmitEditing = () => {
|
||||
hideEdit()
|
||||
const name = text.trim()
|
||||
if (!name.length) return
|
||||
createUserList({ name })
|
||||
}
|
||||
|
||||
return isEdit
|
||||
? (
|
||||
<View style={styles.imputContainer}>
|
||||
<Input
|
||||
placeholder={t('list_create_input_placeholder')}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
ref={inputRef}
|
||||
onBlur={handleSubmitEditing}
|
||||
onSubmitEditing={handleSubmitEditing}
|
||||
style={{ ...styles.input, backgroundColor: theme.secondary45 }}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
: 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 theme = useGetter('common', 'theme')
|
||||
const [isEdit, setIsEdit] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
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, isExists) => {
|
||||
if (isMove) {
|
||||
moveMusicToList({
|
||||
fromId: listId,
|
||||
toId: list.id,
|
||||
musicInfo,
|
||||
})
|
||||
toast(t('list_edit_action_tip_move_success'))
|
||||
hideModal()
|
||||
} else {
|
||||
if (isExists) {
|
||||
toast(t('list_edit_action_tip_exist'))
|
||||
} else {
|
||||
addMusicToList({
|
||||
musicInfo,
|
||||
id: list.id,
|
||||
})
|
||||
toast(t('list_edit_action_tip_add_success'))
|
||||
hideModal()
|
||||
}
|
||||
}
|
||||
}, [addMusicToList, hideModal, isMove, listId, moveMusicToList, musicInfo, t])
|
||||
|
||||
const hideEdit = useCallback(() => {
|
||||
setIsEdit(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
hideEdit()
|
||||
}
|
||||
}, [hideEdit, visible])
|
||||
|
||||
return (
|
||||
<Dialog visible={visible} hideDialog={hideModal}>
|
||||
<Title musicInfo={musicInfo} isMove={isMove} />
|
||||
<View style={{ flexShrink: 1 }} onStartShouldSetResponder={() => true}>
|
||||
<ScrollView style={{ flexGrow: 0 }}>
|
||||
<View style={{ ...styles.list }}>
|
||||
{ allList.map(list => <ListItem key={list.id} list={list} musicInfo={musicInfo} onPress={handleSelect} width={itemWidth} />) }
|
||||
<View style={{ ...styles.listItem, width: itemWidth }}>
|
||||
<TouchableOpacity
|
||||
style={{ ...styles.button, borderColor: theme.secondary20, borderStyle: 'dashed' }}
|
||||
onPress={() => setIsEdit(true)}>
|
||||
<Text numberOfLines={1} style={{ fontSize: 12, color: theme.secondary }}>{t('list_create')}</Text>
|
||||
</TouchableOpacity>
|
||||
{
|
||||
isEdit
|
||||
? <CreateUserList isEdit={isEdit} hideEdit={hideEdit} />
|
||||
: null
|
||||
}
|
||||
</View>
|
||||
</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: 8,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
marginRight: 10,
|
||||
marginBottom: 10,
|
||||
borderRadius: 4,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
borderWidth: BorderWidths.normal2,
|
||||
},
|
||||
imputContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
paddingBottom: 10,
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 12,
|
||||
borderRadius: 4,
|
||||
textAlign: 'center',
|
||||
},
|
||||
})
|
67
src/components/MusicAddModal/CreateUserList.tsx
Normal file
67
src/components/MusicAddModal/CreateUserList.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import Input, { InputType } from '@/components/common/Input'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
import { useI18n } from '@/lang'
|
||||
import { createUserList } from '@/core/list'
|
||||
import listState from '@/store/list/state'
|
||||
|
||||
export default ({ isEdit, onHide }: {
|
||||
isEdit: boolean
|
||||
onHide: () => void
|
||||
}) => {
|
||||
const [text, setText] = useState('')
|
||||
const inputRef = useRef<InputType>(null)
|
||||
const t = useI18n()
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit) {
|
||||
setText('')
|
||||
requestAnimationFrame(() => {
|
||||
inputRef.current?.focus()
|
||||
})
|
||||
}
|
||||
}, [isEdit])
|
||||
|
||||
const handleSubmitEditing = () => {
|
||||
onHide()
|
||||
const name = text.trim()
|
||||
if (!name.length) return
|
||||
void createUserList(listState.userList.length, [{ id: `userlist_${Date.now()}`, name, locationUpdateTime: null }])
|
||||
}
|
||||
|
||||
return isEdit
|
||||
? (
|
||||
<View style={styles.imputContainer}>
|
||||
<Input
|
||||
placeholder={t('list_create_input_placeholder')}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
ref={inputRef}
|
||||
onBlur={handleSubmitEditing}
|
||||
onSubmitEditing={handleSubmitEditing}
|
||||
style={styles.input}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
||||
const styles = createStyle({
|
||||
imputContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
paddingBottom: 10,
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
borderRadius: 4,
|
||||
textAlign: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
})
|
74
src/components/MusicAddModal/List.tsx
Normal file
74
src/components/MusicAddModal/List.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { ScrollView, TouchableOpacity, View } from 'react-native'
|
||||
|
||||
import Text from '@/components/common/Text'
|
||||
import { useMyList } from '@/store/list/hook'
|
||||
import ListItem, { styles as listStyles } from './ListItem'
|
||||
import CreateUserList from './CreateUserList'
|
||||
import { useDimensions } from '@/utils/hooks'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import { useI18n } from '@/lang'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
import { scaleSizeW } from '@/utils/pixelRatio'
|
||||
|
||||
const styles = createStyle({
|
||||
list: {
|
||||
paddingLeft: 15,
|
||||
paddingRight: 2,
|
||||
paddingBottom: 5,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)'
|
||||
// justifyContent: 'center',
|
||||
},
|
||||
})
|
||||
const MIN_WIDTH = scaleSizeW(150)
|
||||
const PADDING = styles.list.paddingLeft + styles.list.paddingRight
|
||||
|
||||
|
||||
const EditListItem = ({ itemWidth }: {
|
||||
itemWidth: number
|
||||
}) => {
|
||||
const [isEdit, setEdit] = useState(false)
|
||||
const theme = useTheme()
|
||||
const t = useI18n()
|
||||
|
||||
return (
|
||||
<View style={{ ...listStyles.listItem, width: itemWidth }}>
|
||||
<TouchableOpacity
|
||||
style={{ ...listStyles.button, borderColor: theme['c-primary-light-200-alpha-700'], borderStyle: 'dashed' }}
|
||||
onPress={() => { setEdit(true) }}
|
||||
>
|
||||
<Text style={{ opacity: isEdit ? 0 : 1 }} numberOfLines={1} size={14} color={theme['c-button-font']}>{t('list_create')}</Text>
|
||||
</TouchableOpacity>
|
||||
{
|
||||
isEdit
|
||||
? <CreateUserList isEdit={isEdit} onHide={() => { setEdit(false) }} />
|
||||
: null
|
||||
}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ({ musicInfo, onPress }: {
|
||||
musicInfo: LX.Music.MusicInfo
|
||||
onPress: (listInfo: LX.List.MyListInfo) => void
|
||||
}) => {
|
||||
const { window } = useDimensions()
|
||||
const allList = useMyList()
|
||||
const itemWidth = useMemo(() => {
|
||||
let w = Math.floor(window.width * 0.9 - PADDING)
|
||||
let n = Math.floor(w / MIN_WIDTH)
|
||||
if (n > 10) n = 10
|
||||
return Math.floor((w - 1) / n)
|
||||
}, [window])
|
||||
|
||||
return (
|
||||
<ScrollView style={{ flexGrow: 0 }}>
|
||||
<View style={styles.list}>
|
||||
{ allList.map(info => <ListItem key={info.id} listInfo={info} musicInfo={musicInfo} onPress={onPress} width={itemWidth} />) }
|
||||
<EditListItem itemWidth={itemWidth} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
57
src/components/MusicAddModal/ListItem.tsx
Normal file
57
src/components/MusicAddModal/ListItem.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React from 'react'
|
||||
import { View } from 'react-native'
|
||||
import Button from '@/components/common/Button'
|
||||
import Text from '@/components/common/Text'
|
||||
import { BorderWidths } from '@/theme'
|
||||
import { createStyle, toast } from '@/utils/tools'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import { useMusicExistsList } from '@/store/list/hook'
|
||||
|
||||
export default ({ listInfo, onPress, musicInfo, width }: {
|
||||
listInfo: LX.List.MyListInfo
|
||||
onPress: (listInfo: LX.List.MyListInfo) => void
|
||||
musicInfo: LX.Music.MusicInfo
|
||||
width: number
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const isExists = useMusicExistsList(listInfo, musicInfo)
|
||||
|
||||
const handlePress = () => {
|
||||
if (isExists) {
|
||||
toast(global.i18n.t('list_add_tip_exists'))
|
||||
return
|
||||
}
|
||||
onPress(listInfo)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.listItem, width }}>
|
||||
<Button
|
||||
style={{ ...styles.button, backgroundColor: theme['c-button-background'], borderColor: theme['c-primary-light-400-alpha-300'], opacity: isExists ? 0.4 : 1 }}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<Text numberOfLines={1} size={14} color={theme['c-button-font']}>{listInfo.name}</Text>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export const styles = createStyle({
|
||||
listItem: {
|
||||
// width: '50%',
|
||||
paddingRight: 13,
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
},
|
||||
button: {
|
||||
height: 34,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
marginRight: 10,
|
||||
marginBottom: 10,
|
||||
borderRadius: 4,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: BorderWidths.normal2,
|
||||
},
|
||||
})
|
86
src/components/MusicAddModal/MusicAddModal.tsx
Normal file
86
src/components/MusicAddModal/MusicAddModal.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'
|
||||
import Dialog, { type DialogType } from '@/components/common/Dialog'
|
||||
import { toast } from '@/utils/tools'
|
||||
import Title from './Title'
|
||||
import List from './List'
|
||||
import { useI18n } from '@/lang'
|
||||
import { addListMusics, moveListMusics } from '@/core/list'
|
||||
import settingState from '@/store/setting/state'
|
||||
|
||||
export interface SelectInfo {
|
||||
musicInfo: LX.Music.MusicInfo | null
|
||||
listId: string
|
||||
isMove: boolean
|
||||
// single: boolean
|
||||
}
|
||||
const initSelectInfo = {}
|
||||
|
||||
// export interface MusicAddModalProps {
|
||||
// onRename: (listInfo: LX.List.UserListInfo) => void
|
||||
// onImport: (listInfo: LX.List.MyListInfo, index: number) => void
|
||||
// onExport: (listInfo: LX.List.MyListInfo, index: number) => void
|
||||
// onSync: (listInfo: LX.List.UserListInfo) => void
|
||||
// onRemove: (listInfo: LX.List.UserListInfo) => void
|
||||
// }
|
||||
export interface MusicAddModalType {
|
||||
show: (info: SelectInfo) => void
|
||||
}
|
||||
|
||||
export default forwardRef<MusicAddModalType, {}>((props, ref) => {
|
||||
const t = useI18n()
|
||||
const dialogRef = useRef<DialogType>(null)
|
||||
const [selectInfo, setSelectInfo] = useState<SelectInfo>(initSelectInfo as SelectInfo)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show(selectInfo) {
|
||||
setSelectInfo(selectInfo)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
dialogRef.current?.setVisible(true)
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
const handleHide = () => {
|
||||
requestAnimationFrame(() => {
|
||||
setSelectInfo({ ...selectInfo, musicInfo: null })
|
||||
})
|
||||
}
|
||||
|
||||
const handleSelect = (listInfo: LX.List.MyListInfo) => {
|
||||
dialogRef.current?.setVisible(false)
|
||||
if (selectInfo.isMove) {
|
||||
void moveListMusics(selectInfo.listId, listInfo.id,
|
||||
[selectInfo.musicInfo as LX.Music.MusicInfo],
|
||||
settingState.setting['list.addMusicLocationType'],
|
||||
).then(() => {
|
||||
toast(t('list_edit_action_tip_move_success'))
|
||||
}).catch(() => {
|
||||
toast(t('list_edit_action_tip_move_failed'))
|
||||
})
|
||||
} else {
|
||||
void addListMusics(listInfo.id,
|
||||
[selectInfo.musicInfo as LX.Music.MusicInfo],
|
||||
settingState.setting['list.addMusicLocationType'],
|
||||
).then(() => {
|
||||
toast(t('list_edit_action_tip_add_success'))
|
||||
}).catch(() => {
|
||||
toast(t('list_edit_action_tip_add_failed'))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog ref={dialogRef} onHide={handleHide}>
|
||||
{
|
||||
selectInfo.musicInfo
|
||||
? (<>
|
||||
<Title musicInfo={selectInfo.musicInfo} isMove={selectInfo.isMove} />
|
||||
<List musicInfo={selectInfo.musicInfo} onPress={handleSelect} />
|
||||
</>)
|
||||
: null
|
||||
}
|
||||
</Dialog>
|
||||
)
|
||||
})
|
||||
|
26
src/components/MusicAddModal/Title.tsx
Normal file
26
src/components/MusicAddModal/Title.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import Text from '@/components/common/Text'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import { useI18n } from '@/lang'
|
||||
|
||||
export default ({ musicInfo, isMove }: {
|
||||
musicInfo: LX.Music.MusicInfo
|
||||
isMove: boolean
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const t = useI18n()
|
||||
return (
|
||||
<Text style={styles.title}>
|
||||
{t(isMove ? 'list_add_title_first_move' : 'list_add_title_first_add')} <Text color={theme['c-primary-font']}>{musicInfo.name}</Text> {t('list_add_title_last')}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = createStyle({
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
paddingTop: 15,
|
||||
paddingBottom: 15,
|
||||
},
|
||||
})
|
29
src/components/MusicAddModal/index.tsx
Normal file
29
src/components/MusicAddModal/index.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React, { useRef, useImperativeHandle, forwardRef, useState } from 'react'
|
||||
import Modal, { type MusicAddModalType as ModalType, type SelectInfo } from './MusicAddModal'
|
||||
|
||||
export interface MusicAddModalType {
|
||||
show: (info: SelectInfo) => void
|
||||
}
|
||||
|
||||
export default forwardRef<MusicAddModalType, {}>((props, ref) => {
|
||||
const musicAddModalRef = useRef<ModalType>(null)
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show(listInfo) {
|
||||
if (visible) musicAddModalRef.current?.show(listInfo)
|
||||
else {
|
||||
setVisible(true)
|
||||
requestAnimationFrame(() => {
|
||||
musicAddModalRef.current?.show(listInfo)
|
||||
})
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
return (
|
||||
visible
|
||||
? <Modal ref={musicAddModalRef} />
|
||||
: null
|
||||
)
|
||||
})
|
@ -1,199 +0,0 @@
|
||||
import React, { useCallback, useMemo, memo, useState, useRef, useEffect } from 'react'
|
||||
import { View, StyleSheet, Text, ScrollView, TouchableOpacity } 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'
|
||||
import { BorderWidths } from '@/theme'
|
||||
import Input from '@/components/common/Input'
|
||||
import { toast } from '@/utils/tools'
|
||||
|
||||
|
||||
const ListItem = ({ list, onPress, width }) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.listItem, width: width }}>
|
||||
<Button
|
||||
style={{ ...styles.button, backgroundColor: theme.secondary45, borderColor: 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, color: theme.normal }}>{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
|
||||
)
|
||||
}
|
||||
|
||||
const CreateUserList = ({ isEdit, hideEdit }) => {
|
||||
const [text, setText] = useState('')
|
||||
const inputRef = useRef()
|
||||
const { t } = useTranslation()
|
||||
const theme = useGetter('common', 'theme')
|
||||
const createUserList = useDispatch('list', 'createUserList')
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit) {
|
||||
setText('')
|
||||
global.requestAnimationFrame(() => {
|
||||
inputRef.current.focus()
|
||||
})
|
||||
}
|
||||
}, [isEdit])
|
||||
|
||||
const handleSubmitEditing = () => {
|
||||
hideEdit()
|
||||
const name = text.trim()
|
||||
if (!name.length) return
|
||||
createUserList({ name })
|
||||
}
|
||||
|
||||
return isEdit
|
||||
? (
|
||||
<View style={styles.imputContainer}>
|
||||
<Input
|
||||
placeholder={t('list_create_input_placeholder')}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
ref={inputRef}
|
||||
onBlur={handleSubmitEditing}
|
||||
onSubmitEditing={handleSubmitEditing}
|
||||
style={{ ...styles.input, backgroundColor: theme.secondary45 }}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
: 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 theme = useGetter('common', 'theme')
|
||||
const [isEdit, setIsEdit] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
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,
|
||||
})
|
||||
toast(t('list_edit_action_tip_move_success'))
|
||||
} else {
|
||||
addMultiMusicToList({
|
||||
list,
|
||||
id,
|
||||
})
|
||||
toast(t('list_edit_action_tip_add_success'))
|
||||
}
|
||||
hideModal()
|
||||
onAdd()
|
||||
}, [isMove, hideModal, onAdd, moveMultiMusicToList, listId, list, t, addMultiMusicToList])
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
return allList.filter(({ id }) => !excludeList.includes(id))
|
||||
}, [allList, excludeList])
|
||||
|
||||
const hideEdit = useCallback(() => {
|
||||
setIsEdit(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
hideEdit()
|
||||
}
|
||||
}, [hideEdit, visible])
|
||||
|
||||
return (
|
||||
<Dialog visible={visible} hideDialog={hideModal}>
|
||||
<Title list={list} isMove={isMove} />
|
||||
<View style={{ flexShrink: 1 }} onStartShouldSetResponder={() => true}>
|
||||
<ScrollView style={{ flexGrow: 0 }}>
|
||||
<View style={{ ...styles.list }}>
|
||||
{ filteredList.map(list => <ListItem key={list.id} list={list} onPress={handleSelect} width={itemWidth} />) }
|
||||
<View style={{ ...styles.listItem, width: itemWidth }}>
|
||||
<TouchableOpacity
|
||||
style={{ ...styles.button, borderColor: theme.secondary20, borderStyle: 'dashed' }}
|
||||
onPress={() => setIsEdit(true)}>
|
||||
<Text numberOfLines={1} style={{ fontSize: 12, color: theme.secondary }}>{t('list_create')}</Text>
|
||||
</TouchableOpacity>
|
||||
{
|
||||
isEdit
|
||||
? <CreateUserList isEdit={isEdit} hideEdit={hideEdit} />
|
||||
: null
|
||||
}
|
||||
</View>
|
||||
</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: 8,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
marginRight: 10,
|
||||
marginBottom: 10,
|
||||
borderRadius: 4,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
borderWidth: BorderWidths.normal2,
|
||||
},
|
||||
imputContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
paddingBottom: 10,
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 12,
|
||||
borderRadius: 4,
|
||||
textAlign: 'center',
|
||||
},
|
||||
})
|
72
src/components/MusicMultiAddModal/List.tsx
Normal file
72
src/components/MusicMultiAddModal/List.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { ScrollView, TouchableOpacity, View } from 'react-native'
|
||||
|
||||
import Text from '@/components/common/Text'
|
||||
import { useMyList } from '@/store/list/hook'
|
||||
import ListItem, { styles as listStyles } from './ListItem'
|
||||
import CreateUserList from '../MusicAddModal/CreateUserList'
|
||||
import { useDimensions } from '@/utils/hooks'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import { useI18n } from '@/lang'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
import { scaleSizeW } from '@/utils/pixelRatio'
|
||||
|
||||
const styles = createStyle({
|
||||
list: {
|
||||
paddingLeft: 15,
|
||||
paddingRight: 2,
|
||||
paddingBottom: 5,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)'
|
||||
},
|
||||
})
|
||||
const MIN_WIDTH = scaleSizeW(140)
|
||||
const PADDING = styles.list.paddingLeft + styles.list.paddingRight
|
||||
|
||||
const EditListItem = ({ itemWidth }: {
|
||||
itemWidth: number
|
||||
}) => {
|
||||
const [isEdit, setEdit] = useState(false)
|
||||
const theme = useTheme()
|
||||
const t = useI18n()
|
||||
|
||||
return (
|
||||
<View style={{ ...listStyles.listItem, width: itemWidth }}>
|
||||
<TouchableOpacity
|
||||
style={{ ...listStyles.button, borderColor: theme['c-primary-light-200-alpha-700'], borderStyle: 'dashed' }}
|
||||
onPress={() => { setEdit(true) }}
|
||||
>
|
||||
<Text style={{ opacity: isEdit ? 0 : 1 }} numberOfLines={1} size={14} color={theme['c-button-font']}>{t('list_create')}</Text>
|
||||
</TouchableOpacity>
|
||||
{
|
||||
isEdit
|
||||
? <CreateUserList isEdit={isEdit} onHide={() => { setEdit(false) }} />
|
||||
: null
|
||||
}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ({ listId, onPress }: {
|
||||
listId: string
|
||||
onPress: (listInfo: LX.List.MyListInfo) => void
|
||||
}) => {
|
||||
const { window } = useDimensions()
|
||||
const allList = useMyList().filter(l => l.id != listId)
|
||||
const itemWidth = useMemo(() => {
|
||||
let w = Math.floor(window.width * 0.9 - PADDING)
|
||||
let n = Math.floor(w / MIN_WIDTH)
|
||||
if (n > 10) n = 10
|
||||
return Math.floor((w - 1) / n)
|
||||
}, [window])
|
||||
|
||||
return (
|
||||
<ScrollView style={{ flexGrow: 0 }}>
|
||||
<View style={{ ...styles.list }}>
|
||||
{ allList.map(info => <ListItem key={info.id} listInfo={info} onPress={onPress} width={itemWidth} />) }
|
||||
<EditListItem itemWidth={itemWidth} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
49
src/components/MusicMultiAddModal/ListItem.tsx
Normal file
49
src/components/MusicMultiAddModal/ListItem.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import { View } from 'react-native'
|
||||
import Button from '@/components/common/Button'
|
||||
import Text from '@/components/common/Text'
|
||||
import { BorderWidths } from '@/theme'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
|
||||
export default ({ listInfo, onPress, width }: {
|
||||
listInfo: LX.List.MyListInfo
|
||||
onPress: (listInfo: LX.List.MyListInfo) => void
|
||||
width: number
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
|
||||
const handlePress = () => {
|
||||
onPress(listInfo)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.listItem, width }}>
|
||||
<Button
|
||||
style={{ ...styles.button, backgroundColor: theme['c-button-background'], borderColor: theme['c-primary-light-200-alpha-700'] }}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<Text numberOfLines={1} size={14} color={theme['c-button-font']}>{listInfo.name}</Text>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export const styles = createStyle({
|
||||
listItem: {
|
||||
// width: '50%',
|
||||
paddingRight: 13,
|
||||
},
|
||||
button: {
|
||||
height: 34,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
marginRight: 10,
|
||||
marginBottom: 10,
|
||||
borderRadius: 4,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: BorderWidths.normal2,
|
||||
},
|
||||
})
|
86
src/components/MusicMultiAddModal/MusicMultiAddModal.tsx
Normal file
86
src/components/MusicMultiAddModal/MusicMultiAddModal.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'
|
||||
import Dialog, { DialogType } from '@/components/common/Dialog'
|
||||
import { toast } from '@/utils/tools'
|
||||
import Title from './Title'
|
||||
import List from './List'
|
||||
import { useI18n } from '@/lang'
|
||||
import { addListMusics, moveListMusics } from '@/core/list'
|
||||
import settingState from '@/store/setting/state'
|
||||
|
||||
export interface SelectInfo {
|
||||
selectedList: LX.Music.MusicInfo[]
|
||||
listId: string
|
||||
isMove: boolean
|
||||
// single: boolean
|
||||
}
|
||||
const initSelectInfo = { selectedList: [], listId: '', isMove: false }
|
||||
|
||||
// export interface MusicMultiAddModalProps {
|
||||
// onRename: (listInfo: LX.List.UserListInfo) => void
|
||||
// onImport: (listInfo: LX.List.MyListInfo, index: number) => void
|
||||
// onExport: (listInfo: LX.List.MyListInfo, index: number) => void
|
||||
// onSync: (listInfo: LX.List.UserListInfo) => void
|
||||
// onRemove: (listInfo: LX.List.UserListInfo) => void
|
||||
// }
|
||||
export interface MusicMultiAddModalType {
|
||||
show: (info: SelectInfo) => void
|
||||
}
|
||||
|
||||
export default forwardRef<MusicMultiAddModalType, {}>((props, ref) => {
|
||||
const t = useI18n()
|
||||
const dialogRef = useRef<DialogType>(null)
|
||||
const [selectInfo, setSelectInfo] = useState<SelectInfo>(initSelectInfo)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show(selectInfo) {
|
||||
setSelectInfo(selectInfo)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
dialogRef.current?.setVisible(true)
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
const handleHide = () => {
|
||||
requestAnimationFrame(() => {
|
||||
setSelectInfo({ ...selectInfo, selectedList: [] })
|
||||
})
|
||||
}
|
||||
|
||||
const handleSelect = (listInfo: LX.List.MyListInfo) => {
|
||||
dialogRef.current?.setVisible(false)
|
||||
if (selectInfo.isMove) {
|
||||
void moveListMusics(selectInfo.listId, listInfo.id,
|
||||
[...selectInfo.selectedList],
|
||||
settingState.setting['list.addMusicLocationType'],
|
||||
).then(() => {
|
||||
toast(t('list_edit_action_tip_move_success'))
|
||||
}).catch(() => {
|
||||
toast(t('list_edit_action_tip_move_failed'))
|
||||
})
|
||||
} else {
|
||||
void addListMusics(listInfo.id,
|
||||
[...selectInfo.selectedList],
|
||||
settingState.setting['list.addMusicLocationType'],
|
||||
).then(() => {
|
||||
toast(t('list_edit_action_tip_add_success'))
|
||||
}).catch(() => {
|
||||
toast(t('list_edit_action_tip_add_failed'))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog ref={dialogRef} onHide={handleHide}>
|
||||
{
|
||||
selectInfo.selectedList.length
|
||||
? (<>
|
||||
<Title selectedList={selectInfo.selectedList} isMove={selectInfo.isMove} />
|
||||
<List listId={selectInfo.listId} onPress={handleSelect} />
|
||||
</>)
|
||||
: null
|
||||
}
|
||||
</Dialog>
|
||||
)
|
||||
})
|
||||
|
26
src/components/MusicMultiAddModal/Title.tsx
Normal file
26
src/components/MusicMultiAddModal/Title.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import Text from '@/components/common/Text'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import { useI18n } from '@/lang'
|
||||
|
||||
export default ({ selectedList, isMove }: {
|
||||
selectedList: LX.Music.MusicInfo[]
|
||||
isMove: boolean
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const t = useI18n()
|
||||
return (
|
||||
<Text style={styles.title} size={16}>
|
||||
{t(isMove ? 'list_multi_add_title_first_move' : 'list_multi_add_title_first_add')} <Text color={theme['c-primary-font']} size={16}>{selectedList.length}</Text> {t('list_multi_add_title_last')}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = createStyle({
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
paddingTop: 15,
|
||||
paddingBottom: 15,
|
||||
},
|
||||
})
|
29
src/components/MusicMultiAddModal/index.tsx
Normal file
29
src/components/MusicMultiAddModal/index.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React, { useRef, useImperativeHandle, forwardRef, useState } from 'react'
|
||||
import Modal, { MusicMultiAddModalType as ModalType, SelectInfo } from './MusicMultiAddModal'
|
||||
|
||||
export interface MusicMultiAddModalType {
|
||||
show: (info: SelectInfo) => void
|
||||
}
|
||||
|
||||
export default forwardRef<MusicMultiAddModalType, {}>((props, ref) => {
|
||||
const musicMultiAddModalRef = useRef<ModalType>(null)
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show(listInfo) {
|
||||
if (visible) musicMultiAddModalRef.current?.show(listInfo)
|
||||
else {
|
||||
setVisible(true)
|
||||
requestAnimationFrame(() => {
|
||||
musicMultiAddModalRef.current?.show(listInfo)
|
||||
})
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
return (
|
||||
visible
|
||||
? <Modal ref={musicMultiAddModalRef} />
|
||||
: null
|
||||
)
|
||||
})
|
@ -1,142 +0,0 @@
|
||||
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',
|
||||
},
|
||||
})
|
@ -1,26 +0,0 @@
|
||||
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>
|
||||
)
|
||||
})
|
||||
|
278
src/components/OnlineList/List.tsx
Normal file
278
src/components/OnlineList/List.tsx
Normal file
@ -0,0 +1,278 @@
|
||||
import React, { useMemo, useRef, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
import { FlatList, type FlatListProps, RefreshControl, View } from 'react-native'
|
||||
|
||||
// import { useMusicList } from '@/store/list/hook'
|
||||
import ListItem, { ITEM_HEIGHT } from './ListItem'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
import type { Position } from './ListMenu'
|
||||
import type { SelectMode } from './MultipleModeBar'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import settingState from '@/store/setting/state'
|
||||
import { MULTI_SELECT_BAR_HEIGHT } from './MultipleModeBar'
|
||||
import { useI18n } from '@/lang'
|
||||
import Text from '@/components/common/Text'
|
||||
import { handlePlay } from './listAction'
|
||||
|
||||
type FlatListType = FlatListProps<LX.Music.MusicInfoOnline>
|
||||
|
||||
export interface ListProps {
|
||||
onShowMenu: (musicInfo: LX.Music.MusicInfoOnline, index: number, position: Position) => void
|
||||
onMuiltSelectMode: () => void
|
||||
onSelectAll: (isAll: boolean) => void
|
||||
onRefresh: () => void
|
||||
onLoadMore: () => void
|
||||
onPlayList?: (index: number) => void
|
||||
progressViewOffset?: number
|
||||
ListHeaderComponent?: FlatListType['ListEmptyComponent']
|
||||
}
|
||||
export interface ListType {
|
||||
setList: (list: LX.Music.MusicInfoOnline[], showSource?: boolean) => void
|
||||
setIsMultiSelectMode: (isMultiSelectMode: boolean) => void
|
||||
setSelectMode: (mode: SelectMode) => void
|
||||
selectAll: (isAll: boolean) => void
|
||||
getSelectedList: () => LX.Music.MusicInfoOnline[]
|
||||
getList: () => LX.Music.MusicInfoOnline[]
|
||||
setStatus: (val: Status) => void
|
||||
}
|
||||
export type Status = 'loading' | 'refreshing' | 'end' | 'error' | 'idle'
|
||||
|
||||
|
||||
const List = forwardRef<ListType, ListProps>(({
|
||||
onShowMenu,
|
||||
onMuiltSelectMode,
|
||||
onSelectAll,
|
||||
onRefresh,
|
||||
onLoadMore,
|
||||
onPlayList,
|
||||
progressViewOffset,
|
||||
ListHeaderComponent,
|
||||
}, ref) => {
|
||||
// const t = useI18n()
|
||||
const theme = useTheme()
|
||||
const flatListRef = useRef<FlatList>(null)
|
||||
const [currentList, setList] = useState<LX.Music.MusicInfoOnline[]>([])
|
||||
const [showSource, setShowSource] = useState(false)
|
||||
const isMultiSelectModeRef = useRef(false)
|
||||
const selectModeRef = useRef<SelectMode>('single')
|
||||
const prevSelectIndexRef = useRef(-1)
|
||||
const [selectedList, setSelectedList] = useState<LX.Music.MusicInfoOnline[]>([])
|
||||
const selectedListRef = useRef<LX.Music.MusicInfoOnline[]>([])
|
||||
const [visibleMultiSelect, setVisibleMultiSelect] = useState(false)
|
||||
const [status, setStatus] = useState<Status>('idle')
|
||||
// const currentListIdRef = useRef('')
|
||||
// console.log('render music list')
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
setList(list, showSource = false) {
|
||||
setList(list)
|
||||
setShowSource(showSource)
|
||||
},
|
||||
setIsMultiSelectMode(isMultiSelectMode) {
|
||||
isMultiSelectModeRef.current = isMultiSelectMode
|
||||
if (!isMultiSelectMode) {
|
||||
prevSelectIndexRef.current = -1
|
||||
handleUpdateSelectedList([])
|
||||
}
|
||||
setVisibleMultiSelect(isMultiSelectMode)
|
||||
},
|
||||
setSelectMode(mode) {
|
||||
selectModeRef.current = mode
|
||||
},
|
||||
selectAll(isAll) {
|
||||
let list: LX.Music.MusicInfoOnline[]
|
||||
if (isAll) {
|
||||
list = [...currentList]
|
||||
} else {
|
||||
list = []
|
||||
}
|
||||
selectedListRef.current = list
|
||||
setSelectedList(list)
|
||||
},
|
||||
getSelectedList() {
|
||||
return selectedListRef.current
|
||||
},
|
||||
getList() {
|
||||
return currentList
|
||||
},
|
||||
setStatus(val) {
|
||||
setStatus(val)
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
const handleUpdateSelectedList = (newList: LX.Music.MusicInfoOnline[]) => {
|
||||
if (selectedListRef.current.length && newList.length == currentList.length) onSelectAll(true)
|
||||
else if (selectedListRef.current.length == currentList.length) onSelectAll(false)
|
||||
selectedListRef.current = newList
|
||||
setSelectedList(newList)
|
||||
}
|
||||
const handleSelect = (item: LX.Music.MusicInfoOnline, pressIndex: number) => {
|
||||
let newList: LX.Music.MusicInfoOnline[]
|
||||
if (selectModeRef.current == 'single') {
|
||||
prevSelectIndexRef.current = pressIndex
|
||||
const index = selectedListRef.current.indexOf(item)
|
||||
if (index < 0) {
|
||||
newList = [...selectedListRef.current, item]
|
||||
} else {
|
||||
newList = [...selectedListRef.current]
|
||||
newList.splice(index, 1)
|
||||
}
|
||||
} else {
|
||||
if (selectedListRef.current.length) {
|
||||
const prevIndex = prevSelectIndexRef.current
|
||||
const currentIndex = pressIndex
|
||||
if (prevIndex == currentIndex) {
|
||||
newList = []
|
||||
} else if (currentIndex > prevIndex) {
|
||||
newList = currentList.slice(prevIndex, currentIndex + 1)
|
||||
} else {
|
||||
newList = currentList.slice(currentIndex, prevIndex + 1)
|
||||
newList.reverse()
|
||||
}
|
||||
} else {
|
||||
newList = [item]
|
||||
prevSelectIndexRef.current = pressIndex
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdateSelectedList(newList)
|
||||
}
|
||||
|
||||
const handlePress = (item: LX.Music.MusicInfoOnline, index: number) => {
|
||||
if (isMultiSelectModeRef.current) {
|
||||
handleSelect(item, index)
|
||||
} else {
|
||||
if (settingState.setting['list.isClickPlayList'] && onPlayList != null) {
|
||||
onPlayList(index)
|
||||
} else {
|
||||
// console.log(currentList[index])
|
||||
handlePlay(currentList[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleLongPress = (item: LX.Music.MusicInfoOnline, index: number) => {
|
||||
if (isMultiSelectModeRef.current) return
|
||||
prevSelectIndexRef.current = index
|
||||
handleUpdateSelectedList([item])
|
||||
onMuiltSelectMode()
|
||||
}
|
||||
|
||||
const handleLoadMore = () => {
|
||||
switch (status) {
|
||||
case 'end':
|
||||
case 'loading':
|
||||
case 'refreshing': return
|
||||
}
|
||||
onLoadMore()
|
||||
}
|
||||
|
||||
|
||||
const renderItem: FlatListType['renderItem'] = ({ item, index }) => (
|
||||
<ListItem
|
||||
item={item}
|
||||
index={index}
|
||||
showSource={showSource}
|
||||
onPress={handlePress}
|
||||
onLongPress={handleLongPress}
|
||||
onShowMenu={onShowMenu}
|
||||
selectedList={selectedList}
|
||||
/>
|
||||
)
|
||||
const getkey: FlatListType['keyExtractor'] = item => item.id
|
||||
const getItemLayout: FlatListType['getItemLayout'] = (data, index) => {
|
||||
return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }
|
||||
}
|
||||
const refreshControl = useMemo(() => (
|
||||
<RefreshControl
|
||||
colors={[theme['c-primary']]}
|
||||
// progressBackgroundColor={theme.primary}
|
||||
refreshing={status == 'refreshing'}
|
||||
onRefresh={onRefresh} />
|
||||
), [status, onRefresh, theme])
|
||||
const footerComponent = useMemo(() => {
|
||||
let label: FooterLabel
|
||||
switch (status) {
|
||||
case 'refreshing': return null
|
||||
case 'loading':
|
||||
label = 'list_loading'
|
||||
break
|
||||
case 'end':
|
||||
label = 'list_end'
|
||||
break
|
||||
case 'error':
|
||||
label = 'list_error'
|
||||
break
|
||||
case 'idle':
|
||||
label = null
|
||||
break
|
||||
}
|
||||
return (
|
||||
<View style={{ width: '100%', paddingBottom: visibleMultiSelect ? MULTI_SELECT_BAR_HEIGHT : 0 }} >
|
||||
<Footer label={label} onLoadMore={onLoadMore} />
|
||||
</View>
|
||||
)
|
||||
}, [onLoadMore, status, visibleMultiSelect])
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
style={styles.list}
|
||||
data={currentList}
|
||||
maxToRenderPerBatch={4}
|
||||
// updateCellsBatchingPeriod={80}
|
||||
windowSize={8}
|
||||
removeClippedSubviews={true}
|
||||
initialNumToRender={12}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={getkey}
|
||||
getItemLayout={getItemLayout}
|
||||
// onRefresh={onRefresh}
|
||||
// refreshing={refreshing}
|
||||
onEndReachedThreshold={0.5}
|
||||
onEndReached={handleLoadMore}
|
||||
progressViewOffset={progressViewOffset}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
refreshControl={refreshControl}
|
||||
ListFooterComponent={footerComponent}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
type FooterLabel = 'list_loading' | 'list_end' | 'list_error' | null
|
||||
const Footer = ({ label, onLoadMore }: {
|
||||
label: FooterLabel
|
||||
onLoadMore: () => void
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const t = useI18n()
|
||||
const handlePress = () => {
|
||||
if (label != 'list_error') return
|
||||
onLoadMore()
|
||||
}
|
||||
return (
|
||||
label
|
||||
? (
|
||||
<View>
|
||||
<Text onPress={handlePress} style={styles.footer} color={theme['c-font-label']}>{t(label)}</Text>
|
||||
</View>
|
||||
)
|
||||
: null
|
||||
)
|
||||
}
|
||||
|
||||
const styles = createStyle({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
list: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
},
|
||||
footer: {
|
||||
textAlign: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
})
|
||||
|
||||
export default List
|
@ -1,133 +0,0 @@
|
||||
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.flac32bit) {
|
||||
info.type = 'secondary'
|
||||
info.text = t('quality_lossless_24bit')
|
||||
} else 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,
|
||||
},
|
||||
})
|
||||
|
151
src/components/OnlineList/ListItem.tsx
Normal file
151
src/components/OnlineList/ListItem.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { memo, useRef } from 'react'
|
||||
import { StyleSheet, View, TouchableOpacity } from 'react-native'
|
||||
// import Button from '@/components/common/Button'
|
||||
import Text from '@/components/common/Text'
|
||||
import Badge, { type BadgeType } from '@/components/common/Badge'
|
||||
import { Icon } from '@/components/common/Icon'
|
||||
import { useI18n } from '@/lang'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import { scaleSizeH } from '@/utils/pixelRatio'
|
||||
import { LIST_ITEM_HEIGHT } from '@/config/constant'
|
||||
|
||||
export const ITEM_HEIGHT = scaleSizeH(LIST_ITEM_HEIGHT)
|
||||
|
||||
const useQualityTag = (musicInfo: LX.Music.MusicInfoOnline) => {
|
||||
const t = useI18n()
|
||||
let info: { type: BadgeType | null, text: string } = { type: null, text: '' }
|
||||
if (musicInfo.meta._qualitys.flac24bit) {
|
||||
info.type = 'secondary'
|
||||
info.text = t('quality_lossless_24bit')
|
||||
} else if (musicInfo.meta._qualitys.flac ?? musicInfo.meta._qualitys.ape) {
|
||||
info.type = 'secondary'
|
||||
info.text = t('quality_lossless')
|
||||
} else if (musicInfo.meta._qualitys['320k']) {
|
||||
info.type = 'tertiary'
|
||||
info.text = t('quality_high_quality')
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
export default memo(({ item, index, showSource, onPress, onLongPress, onShowMenu, selectedList }: {
|
||||
item: LX.Music.MusicInfoOnline
|
||||
index: number
|
||||
showSource?: boolean
|
||||
onPress: (item: LX.Music.MusicInfoOnline, index: number) => void
|
||||
onLongPress: (item: LX.Music.MusicInfoOnline, index: number) => void
|
||||
onShowMenu: (item: LX.Music.MusicInfoOnline, index: number, position: { x: number, y: number, w: number, h: number }) => void
|
||||
selectedList: LX.Music.MusicInfoOnline[]
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
|
||||
const isSelected = selectedList.includes(item)
|
||||
|
||||
const moreButtonRef = useRef<TouchableOpacity>(null)
|
||||
const handleShowMenu = () => {
|
||||
if (moreButtonRef.current?.measure) {
|
||||
moreButtonRef.current.measure((fx, fy, width, height, px, py) => {
|
||||
// console.log(fx, fy, width, height, px, py)
|
||||
onShowMenu(item, index, { x: Math.ceil(px), y: Math.ceil(py), w: Math.ceil(width), h: Math.ceil(height) })
|
||||
})
|
||||
}
|
||||
}
|
||||
const tagInfo = useQualityTag(item)
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.listItem, height: ITEM_HEIGHT, backgroundColor: isSelected ? theme['c-primary-background-hover'] : 'rgba(0,0,0,0)' }}>
|
||||
<TouchableOpacity style={styles.listItemLeft} onPress={() => { onPress(item, index) }} onLongPress={() => { onLongPress(item, index) }}>
|
||||
<Text style={styles.sn} size={14} color={theme['c-300']}>{index + 1}</Text>
|
||||
<View style={styles.itemInfo}>
|
||||
<Text numberOfLines={1}>{item.name}</Text>
|
||||
<View style={styles.listItemSingle}>
|
||||
<Text style={styles.listItemSingleText} size={13} color={theme['c-500']} numberOfLines={1}>{item.singer}</Text>
|
||||
{ tagInfo.type ? <Badge type={tagInfo.type}>{tagInfo.text}</Badge> : null }
|
||||
{ showSource ? <Badge type="tertiary">{item.source}</Badge> : null }
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={handleShowMenu} ref={moreButtonRef} style={styles.moreButton}>
|
||||
<Icon name="dots-vertical" style={{ color: theme['c-350'] }} size={12} />
|
||||
</TouchableOpacity>
|
||||
</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',
|
||||
// paddingLeft: 10,
|
||||
paddingRight: 2,
|
||||
alignItems: 'center',
|
||||
// borderBottomWidth: BorderWidths.normal,
|
||||
},
|
||||
listItemLeft: {
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
sn: {
|
||||
width: 38,
|
||||
// fontSize: 12,
|
||||
textAlign: 'center',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
paddingLeft: 3,
|
||||
paddingRight: 3,
|
||||
},
|
||||
itemInfo: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
// paddingTop: 10,
|
||||
// paddingBottom: 10,
|
||||
},
|
||||
// listItemTitle: {
|
||||
// // backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
// flexGrow: 0,
|
||||
// flexShrink: 1,
|
||||
// // fontSize: 15,
|
||||
// },
|
||||
listItemSingle: {
|
||||
paddingTop: 2,
|
||||
flexDirection: 'row',
|
||||
// alignItems: 'flex-end',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
},
|
||||
listItemSingleText: {
|
||||
// fontSize: 13,
|
||||
// paddingTop: 2,
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
},
|
||||
listItemBadge: {
|
||||
// fontSize: 10,
|
||||
paddingLeft: 5,
|
||||
paddingTop: 2,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
listItemRight: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexBasis: 'auto',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
moreButton: {
|
||||
height: '80%',
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
// paddingTop: 10,
|
||||
// paddingBottom: 10,
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
||||
|
81
src/components/OnlineList/ListMenu.tsx
Normal file
81
src/components/OnlineList/ListMenu.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import React, { useMemo, useRef, useImperativeHandle, forwardRef, useState } from 'react'
|
||||
import { useI18n } from '@/lang'
|
||||
import Menu, { type MenuType, type Position } from '@/components/common/Menu'
|
||||
|
||||
export interface SelectInfo {
|
||||
musicInfo: LX.Music.MusicInfoOnline
|
||||
selectedList: LX.Music.MusicInfoOnline[]
|
||||
index: number
|
||||
single: boolean
|
||||
}
|
||||
const initSelectInfo = {}
|
||||
|
||||
export interface ListMenuProps {
|
||||
onPlay: (selectInfo: SelectInfo) => void
|
||||
onPlayLater: (selectInfo: SelectInfo) => void
|
||||
onAdd: (selectInfo: SelectInfo) => void
|
||||
onCopyName: (selectInfo: SelectInfo) => void
|
||||
}
|
||||
export interface ListMenuType {
|
||||
show: (selectInfo: SelectInfo, position: Position) => void
|
||||
}
|
||||
|
||||
export type {
|
||||
Position,
|
||||
}
|
||||
|
||||
export default forwardRef<ListMenuType, ListMenuProps>((props: ListMenuProps, ref) => {
|
||||
const t = useI18n()
|
||||
const [visible, setVisible] = useState(false)
|
||||
const menuRef = useRef<MenuType>(null)
|
||||
const selectInfoRef = useRef<SelectInfo>(initSelectInfo as SelectInfo)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show(selectInfo, position) {
|
||||
selectInfoRef.current = selectInfo
|
||||
if (visible) menuRef.current?.show(position)
|
||||
else {
|
||||
setVisible(true)
|
||||
requestAnimationFrame(() => {
|
||||
menuRef.current?.show(position)
|
||||
})
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
const menus = useMemo(() => {
|
||||
return [
|
||||
{ action: 'play', label: t('play') },
|
||||
{ action: 'playLater', label: t('play_later') },
|
||||
// { action: 'download', label: '下载' },
|
||||
{ action: 'add', label: t('add_to') },
|
||||
{ action: 'copyName', label: t('copy_name') },
|
||||
] as const
|
||||
}, [t])
|
||||
|
||||
const handleMenuPress = ({ action }: typeof menus[number]) => {
|
||||
const selectInfo = selectInfoRef.current
|
||||
switch (action) {
|
||||
case 'play':
|
||||
props.onPlay(selectInfo)
|
||||
break
|
||||
case 'playLater':
|
||||
props.onPlayLater(selectInfo)
|
||||
break
|
||||
case 'add':
|
||||
props.onAdd(selectInfo)
|
||||
break
|
||||
case 'copyName':
|
||||
props.onCopyName(selectInfo)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
visible
|
||||
? <Menu ref={menuRef} menus={menus} onPress={handleMenuPress} />
|
||||
: null
|
||||
)
|
||||
})
|
158
src/components/OnlineList/MultipleModeBar.tsx
Normal file
158
src/components/OnlineList/MultipleModeBar.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
import React, { useState, useRef, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'
|
||||
import { Animated, View, TouchableOpacity } from 'react-native'
|
||||
|
||||
import Text from '@/components/common/Text'
|
||||
import Button from '@/components/common/Button'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
import { BorderWidths } from '@/theme'
|
||||
import { scaleSizeH } from '@/utils/pixelRatio'
|
||||
|
||||
export type SelectMode = 'single' | 'range'
|
||||
|
||||
export const MULTI_SELECT_BAR_HEIGHT = scaleSizeH(40)
|
||||
|
||||
export interface MultipleModeBarProps {
|
||||
onSwitchMode: (mode: SelectMode) => void
|
||||
onSelectAll: (isAll: boolean) => void
|
||||
onExitSelectMode: () => void
|
||||
}
|
||||
export interface MultipleModeBarType {
|
||||
show: () => void
|
||||
setIsSelectAll: (isAll: boolean) => void
|
||||
setSwitchMode: (mode: SelectMode) => void
|
||||
exitSelectMode: () => void
|
||||
}
|
||||
|
||||
export default forwardRef<MultipleModeBarType, MultipleModeBarProps>(({ onSelectAll, onSwitchMode, onExitSelectMode }, ref) => {
|
||||
// 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 [selectMode, setSelectMode] = useState<SelectMode>('single')
|
||||
const [isSelectAll, setIsSelectAll] = useState(false)
|
||||
const theme = useTheme()
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show() {
|
||||
handleShow()
|
||||
},
|
||||
setIsSelectAll(isAll) {
|
||||
setIsSelectAll(isAll)
|
||||
},
|
||||
setSwitchMode(mode: SelectMode) {
|
||||
setSelectMode(mode)
|
||||
},
|
||||
exitSelectMode() {
|
||||
handleHide()
|
||||
},
|
||||
}))
|
||||
|
||||
const handleShow = 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 handleHide = 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])
|
||||
|
||||
|
||||
const animaStyle = useMemo(() => ({
|
||||
...styles.container,
|
||||
height: MULTI_SELECT_BAR_HEIGHT,
|
||||
// backgroundColor: theme['c-content-background'],
|
||||
borderBottomColor: theme['c-border-background'],
|
||||
opacity: animFade, // Bind opacity to animated value
|
||||
transform: [
|
||||
{ translateY: animTranslateY },
|
||||
],
|
||||
}), [animFade, animTranslateY, theme])
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
const selectAll = !isSelectAll
|
||||
setIsSelectAll(selectAll)
|
||||
onSelectAll(selectAll)
|
||||
}, [isSelectAll, onSelectAll])
|
||||
|
||||
const component = useMemo(() => {
|
||||
return (
|
||||
<Animated.View style={animaStyle}>
|
||||
<View style={styles.switchBtn}>
|
||||
<Button onPress={() => onSwitchMode('single')} style={{ ...styles.btn, backgroundColor: selectMode == 'single' ? theme['c-button-background'] : 'rgba(0,0,0,0)' }}>
|
||||
<Text color={theme['c-button-font']}>{global.i18n.t('list_select_single')}</Text>
|
||||
</Button>
|
||||
<Button onPress={() => onSwitchMode('range')} style={{ ...styles.btn, backgroundColor: selectMode == 'range' ? theme['c-button-background'] : 'rgba(0,0,0,0)' }}>
|
||||
<Text color={theme['c-button-font']}>{global.i18n.t('list_select_range')}</Text>
|
||||
</Button>
|
||||
</View>
|
||||
<TouchableOpacity onPress={handleSelectAll} style={styles.btn}>
|
||||
<Text color={theme['c-button-font']}>{global.i18n.t(isSelectAll ? 'list_select_unall' : 'list_select_all')}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={onExitSelectMode} style={styles.btn}>
|
||||
<Text color={theme['c-button-font']}>{global.i18n.t('list_select_cancel')}</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
)
|
||||
}, [animaStyle, selectMode, theme, handleSelectAll, isSelectAll, onExitSelectMode, onSwitchMode])
|
||||
|
||||
return !visible && animatePlayed ? null : component
|
||||
})
|
||||
|
||||
const styles = createStyle({
|
||||
container: {
|
||||
flex: 1,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
// height: 40,
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: BorderWidths.normal,
|
||||
},
|
||||
switchBtn: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
},
|
||||
btn: {
|
||||
// flex: 1,
|
||||
paddingLeft: 18,
|
||||
paddingRight: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
@ -1,308 +0,0 @@
|
||||
import React, { useState, useCallback, memo, useMemo, useRef, useEffect } from 'react'
|
||||
import { StyleSheet, FlatList, View, RefreshControl } 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'
|
||||
import { shareMusic } from '@/utils/tools'
|
||||
|
||||
export default memo(({
|
||||
list,
|
||||
isEnd,
|
||||
page,
|
||||
isListRefreshing,
|
||||
// visibleLoadingMask,
|
||||
onRefresh,
|
||||
onLoadMore,
|
||||
onPlayList,
|
||||
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 isClickPlayList = useGetter('common', 'isClickPlayList')
|
||||
const downloadFileName = useGetter('common', 'downloadFileName')
|
||||
const shareType = useGetter('common', 'shareType')
|
||||
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)
|
||||
const addMultiMusicToList = useDispatch('list', 'listAddMultiple')
|
||||
const theme = useGetter('common', 'theme')
|
||||
|
||||
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 {
|
||||
if (isClickPlayList && typeof onPlayList == 'function') {
|
||||
onPlayList(index, item)
|
||||
} else {
|
||||
handlePlay(item, index)
|
||||
}
|
||||
}
|
||||
}, [handlePlay, handleSelect, isClickPlayList, onPlayList])
|
||||
|
||||
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':
|
||||
if (selectedListRef.current.length) {
|
||||
addMultiMusicToList({ id: 'default', list: [...selectedListRef.current] })
|
||||
handleCancelMultiSelect()
|
||||
}
|
||||
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':
|
||||
shareMusic(shareType, downloadFileName, selectedData.current.data)
|
||||
break
|
||||
case 'add':
|
||||
// console.log(selectedListRef.current.length)
|
||||
selectedListRef.current.length
|
||||
? setVisibleMusicMultiAddModal(true)
|
||||
: setVisibleMusicAddModal(true)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, [addMultiMusicToList, downloadFileName, handleCancelMultiSelect, handlePlay, setTempPlayList, shareType])
|
||||
|
||||
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])
|
||||
|
||||
const refreshControl = useMemo(() => (
|
||||
<RefreshControl
|
||||
colors={[theme.secondary]}
|
||||
progressBackgroundColor={theme.primary}
|
||||
refreshing={isListRefreshing}
|
||||
onRefresh={onRefresh} />
|
||||
), [isListRefreshing, onRefresh, theme])
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<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}
|
||||
refreshControl={refreshControl}
|
||||
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({
|
||||
container: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
list: {
|
||||
flex: 1,
|
||||
},
|
||||
exitMultipleModeBtn: {
|
||||
height: 40,
|
||||
},
|
||||
})
|
||||
|
124
src/components/OnlineList/index.tsx
Normal file
124
src/components/OnlineList/index.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react'
|
||||
import { View } from 'react-native'
|
||||
// import LoadingMask, { LoadingMaskType } from '@/components/common/LoadingMask'
|
||||
import List, { type ListProps, type ListType, type Status } from './List'
|
||||
import ListMenu, { type ListMenuType, type Position, type SelectInfo } from './ListMenu'
|
||||
import ListMusicMultiAdd, { type MusicMultiAddModalType as ListAddMultiType } from '@/components/MusicMultiAddModal'
|
||||
import ListMusicAdd, { type MusicAddModalType as ListMusicAddType } from '@/components/MusicAddModal'
|
||||
import MultipleModeBar, { type MultipleModeBarType, type SelectMode } from './MultipleModeBar'
|
||||
import { handlePlay, handlePlayLater, handleShare } from './listAction'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
|
||||
export interface OnlineListProps {
|
||||
onRefresh: ListProps['onRefresh']
|
||||
onLoadMore: ListProps['onLoadMore']
|
||||
onPlayList?: ListProps['onPlayList']
|
||||
progressViewOffset?: ListProps['progressViewOffset']
|
||||
ListHeaderComponent?: ListProps['ListHeaderComponent']
|
||||
}
|
||||
export interface OnlineListType {
|
||||
setList: (list: LX.Music.MusicInfoOnline[], showSource?: boolean) => void
|
||||
setStatus: (val: Status) => void
|
||||
}
|
||||
|
||||
export default forwardRef<OnlineListType, OnlineListProps>(({
|
||||
onRefresh,
|
||||
onLoadMore,
|
||||
onPlayList,
|
||||
progressViewOffset,
|
||||
ListHeaderComponent,
|
||||
}, ref) => {
|
||||
const listRef = useRef<ListType>(null)
|
||||
const multipleModeBarRef = useRef<MultipleModeBarType>(null)
|
||||
const listMusicAddRef = useRef<ListMusicAddType>(null)
|
||||
const listMusicMultiAddRef = useRef<ListAddMultiType>(null)
|
||||
const listMenuRef = useRef<ListMenuType>(null)
|
||||
// const loadingMaskRef = useRef<LoadingMaskType>(null)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
setList(list, showSource) {
|
||||
listRef.current?.setList(list, showSource)
|
||||
},
|
||||
setStatus(val) {
|
||||
listRef.current?.setStatus(val)
|
||||
},
|
||||
}))
|
||||
|
||||
const hancelMultiSelect = () => {
|
||||
multipleModeBarRef.current?.show()
|
||||
listRef.current?.setIsMultiSelectMode(true)
|
||||
}
|
||||
const hancelSwitchSelectMode = (mode: SelectMode) => {
|
||||
multipleModeBarRef.current?.setSwitchMode(mode)
|
||||
listRef.current?.setSelectMode(mode)
|
||||
}
|
||||
const hancelExitSelect = () => {
|
||||
multipleModeBarRef.current?.exitSelectMode()
|
||||
listRef.current?.setIsMultiSelectMode(false)
|
||||
}
|
||||
|
||||
const showMenu = (musicInfo: LX.Music.MusicInfoOnline, index: number, position: Position) => {
|
||||
listMenuRef.current?.show({
|
||||
musicInfo,
|
||||
index,
|
||||
single: false,
|
||||
selectedList: listRef.current!.getSelectedList(),
|
||||
}, position)
|
||||
}
|
||||
const handleAddMusic = (info: SelectInfo) => {
|
||||
if (info.selectedList.length) {
|
||||
listMusicMultiAddRef.current?.show({ selectedList: info.selectedList, listId: '', isMove: false })
|
||||
} else {
|
||||
listMusicAddRef.current?.show({ musicInfo: info.musicInfo, listId: '', isMove: false })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<List
|
||||
ref={listRef}
|
||||
onShowMenu={showMenu}
|
||||
onMuiltSelectMode={hancelMultiSelect}
|
||||
onSelectAll={isAll => multipleModeBarRef.current?.setIsSelectAll(isAll)}
|
||||
onRefresh={onRefresh}
|
||||
onLoadMore={onLoadMore}
|
||||
onPlayList={onPlayList}
|
||||
progressViewOffset={progressViewOffset}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
/>
|
||||
<MultipleModeBar
|
||||
ref={multipleModeBarRef}
|
||||
onSwitchMode={hancelSwitchSelectMode}
|
||||
onSelectAll={isAll => listRef.current?.selectAll(isAll)}
|
||||
onExitSelectMode={hancelExitSelect}
|
||||
/>
|
||||
</View>
|
||||
<ListMusicAdd ref={listMusicAddRef} />
|
||||
<ListMusicMultiAdd ref={listMusicMultiAddRef} />
|
||||
<ListMenu
|
||||
ref={listMenuRef}
|
||||
onPlay={info => { handlePlay(info.musicInfo) }}
|
||||
onPlayLater={info => { handlePlayLater(info.musicInfo, info.selectedList, hancelExitSelect) }}
|
||||
onCopyName={info => { handleShare(info.musicInfo) }}
|
||||
onAdd={handleAddMusic}
|
||||
/>
|
||||
{/* <LoadingMask ref={loadingMaskRef} /> */}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
const styles = createStyle({
|
||||
container: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
list: {
|
||||
flex: 1,
|
||||
},
|
||||
exitMultipleModeBtn: {
|
||||
height: 40,
|
||||
},
|
||||
})
|
||||
|
29
src/components/OnlineList/listAction.ts
Normal file
29
src/components/OnlineList/listAction.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { LIST_IDS } from '@/config/constant'
|
||||
import { addListMusics } from '@/core/list'
|
||||
import { playList } from '@/core/player/player'
|
||||
import { addTempPlayList } from '@/core/player/tempPlayList'
|
||||
import settingState from '@/store/setting/state'
|
||||
import { getListMusicSync } from '@/utils/listManage'
|
||||
import { shareMusic } from '@/utils/tools'
|
||||
|
||||
export const handlePlay = (musicInfo: LX.Music.MusicInfoOnline) => {
|
||||
void addListMusics(LIST_IDS.DEFAULT, [musicInfo], settingState.setting['list.addMusicLocationType']).then(() => {
|
||||
const index = getListMusicSync(LIST_IDS.DEFAULT).findIndex(m => m.id == musicInfo.id)
|
||||
if (index < 0) return
|
||||
void playList(LIST_IDS.DEFAULT, index)
|
||||
})
|
||||
}
|
||||
export const handlePlayLater = (musicInfo: LX.Music.MusicInfoOnline, selectedList: LX.Music.MusicInfoOnline[], onCancelSelect: () => void) => {
|
||||
if (selectedList.length) {
|
||||
addTempPlayList(selectedList.map(s => ({ listId: '', musicInfo: s })))
|
||||
onCancelSelect()
|
||||
} else {
|
||||
addTempPlayList([{ listId: '', musicInfo }])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const handleShare = (musicInfo: LX.Music.MusicInfoOnline) => {
|
||||
shareMusic(settingState.setting['common.shareType'], settingState.setting['download.fileName'], musicInfo)
|
||||
}
|
||||
|
41
src/components/PageContent.tsx
Normal file
41
src/components/PageContent.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Dimensions, ImageBackground, type LayoutChangeEvent, View } from 'react-native'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
// import { useDimensions } from '@/utils/hooks'
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
|
||||
export default ({ children }: Props) => {
|
||||
const theme = useTheme()
|
||||
// const { window } = useDimensions()
|
||||
const [wh, setWH] = useState<{ width: number | string, height: number | string }>({ width: '100%', height: '100%' })
|
||||
|
||||
// 固定宽高度 防止弹窗键盘时大小改变导致背景被缩放
|
||||
useEffect(() => {
|
||||
const onChange = () => {
|
||||
setWH({ width: '100%', height: '100%' })
|
||||
}
|
||||
|
||||
const changeEvent = Dimensions.addEventListener('change', onChange)
|
||||
return () => { changeEvent.remove() }
|
||||
}, [])
|
||||
const handleLayout = (e: LayoutChangeEvent) => {
|
||||
setWH({ width: e.nativeEvent.layout.width, height: e.nativeEvent.layout.height })
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
onLayout={handleLayout}
|
||||
style={{ height: wh.height, width: wh.width, backgroundColor: theme['c-content-background'] }}
|
||||
source={theme['bg-image']}
|
||||
resizeMode="cover"
|
||||
>
|
||||
<View style={{ flex: 1, flexDirection: 'column', backgroundColor: theme['c-main-background'] }}>
|
||||
{children}
|
||||
</View>
|
||||
</ImageBackground>
|
||||
)
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
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)
|
36
src/components/SearchTipList/List.tsx
Normal file
36
src/components/SearchTipList/List.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { useState, forwardRef, useImperativeHandle, Ref } from 'react'
|
||||
import { FlatList, FlatListProps } from 'react-native'
|
||||
|
||||
// import InsetShadow from 'react-native-inset-shadow'
|
||||
|
||||
export type ItemT<T> = FlatListProps<T>['data']
|
||||
|
||||
export type ListProps<T> = Pick<FlatListProps<T>,
|
||||
| 'renderItem'
|
||||
| 'maxToRenderPerBatch'
|
||||
| 'windowSize'
|
||||
| 'initialNumToRender'
|
||||
| 'keyExtractor'
|
||||
| 'getItemLayout'
|
||||
| 'keyboardShouldPersistTaps'
|
||||
>
|
||||
|
||||
export interface ListType<T> {
|
||||
setList: (list: T[]) => void
|
||||
}
|
||||
|
||||
const List = <T extends ItemT<T>>(props: ListProps<T>, ref: Ref<ListType<T>>) => {
|
||||
const [list, setList] = useState<T[]>([])
|
||||
useImperativeHandle(ref, () => ({
|
||||
setList(list) {
|
||||
setList(list)
|
||||
},
|
||||
}))
|
||||
|
||||
return <FlatList removeClippedSubviews={true} keyboardShouldPersistTaps={'always'} {...props} data={list} />
|
||||
}
|
||||
|
||||
export default forwardRef(List) as
|
||||
<M,>(p: ListProps<M> & { ref?: Ref<ListType<M>> }) => JSX.Element | null
|
||||
|
||||
|
146
src/components/SearchTipList/index.tsx
Normal file
146
src/components/SearchTipList/index.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import React, { useRef, useState, useCallback, useMemo, forwardRef, useImperativeHandle, type Ref } from 'react'
|
||||
import { StyleSheet, View, Animated } from 'react-native'
|
||||
// import PropTypes from 'prop-types'
|
||||
// import { AppColors } from '@/theme'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import List, { type ItemT, type ListProps, type ListType } from './List'
|
||||
// import InsetShadow from 'react-native-inset-shadow'
|
||||
|
||||
export interface SearchTipListProps<T> extends ListProps<T> {
|
||||
onPressBg?: () => void
|
||||
}
|
||||
export interface SearchTipListType<T> {
|
||||
setList: (list: T[]) => void
|
||||
setHeight: (height: number) => void
|
||||
}
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
const Component = <T extends ItemT<T>>({ onPressBg = noop, ...props }: SearchTipListProps<T>, ref: Ref<SearchTipListType<T>>) => {
|
||||
const theme = useTheme()
|
||||
const translateY = useRef(new Animated.Value(0)).current
|
||||
const scaleY = useRef(new Animated.Value(0)).current
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [animatePlayed, setAnimatPlayed] = useState(true)
|
||||
const listRef = useRef<ListType<T>>(null)
|
||||
const prevListRef = useRef<T[]>([])
|
||||
const heightRef = useRef(0)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
setList(list) {
|
||||
if (prevListRef.current.length) {
|
||||
if (!list.length) handleHide()
|
||||
} else if (list.length) handleShow()
|
||||
prevListRef.current = list
|
||||
requestAnimationFrame(() => {
|
||||
listRef.current?.setList(list)
|
||||
})
|
||||
},
|
||||
setHeight(height) {
|
||||
heightRef.current = height
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
const handleShow = useCallback(() => {
|
||||
// console.log('handleShow', height, visible)
|
||||
if (!heightRef.current) return
|
||||
setVisible(true)
|
||||
setAnimatPlayed(false)
|
||||
|
||||
translateY.setValue(-heightRef.current / 2)
|
||||
scaleY.setValue(0)
|
||||
|
||||
Animated.parallel([
|
||||
// Animated.timing(fade, {
|
||||
// toValue: 1,
|
||||
// duration: 300,
|
||||
// useNativeDriver: true,
|
||||
// }),
|
||||
Animated.timing(translateY, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scaleY, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => {
|
||||
setAnimatPlayed(true)
|
||||
})
|
||||
}, [translateY, scaleY])
|
||||
|
||||
const handleHide = useCallback(() => {
|
||||
setAnimatPlayed(false)
|
||||
Animated.parallel([
|
||||
// Animated.timing(fade, {
|
||||
// toValue: 0,
|
||||
// duration: 200,
|
||||
// useNativeDriver: true,
|
||||
// }),
|
||||
Animated.timing(translateY, {
|
||||
toValue: -heightRef.current / 2,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scaleY, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start((finished) => {
|
||||
// console.log(finished)
|
||||
if (!finished) return
|
||||
setVisible(false)
|
||||
setAnimatPlayed(true)
|
||||
})
|
||||
}, [translateY, scaleY])
|
||||
|
||||
|
||||
const component = useMemo(() => (
|
||||
<Animated.View
|
||||
style={{
|
||||
...styles.anima,
|
||||
transform: [
|
||||
{ translateY },
|
||||
{ scaleY },
|
||||
],
|
||||
}}>
|
||||
<View style={{ ...styles.container, backgroundColor: theme['c-content-background'] }}>
|
||||
<List ref={listRef} {...props} />
|
||||
</View>
|
||||
<View style={styles.blank} onTouchStart={onPressBg}></View>
|
||||
</Animated.View>
|
||||
), [onPressBg, props, scaleY, theme, translateY])
|
||||
|
||||
return !visible && animatePlayed ? null : component
|
||||
}
|
||||
|
||||
export default forwardRef(Component) as
|
||||
<T,>(p: SearchTipListProps<T> & { ref?: Ref<SearchTipListType<T>> }) => JSX.Element | null
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
anima: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
zIndex: 10,
|
||||
},
|
||||
container: {
|
||||
flex: 0,
|
||||
// flexGrow: 0,
|
||||
// borderBottomWidth: BorderWidths.normal,
|
||||
elevation: 2,
|
||||
maxHeight: '80%',
|
||||
},
|
||||
blank: {
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
// backgroundColor: 'transparent',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
},
|
||||
})
|
77
src/components/SourceSelector.tsx
Normal file
77
src/components/SourceSelector.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import React, { forwardRef, type Ref, useImperativeHandle, useMemo, useState } from 'react'
|
||||
import { View } from 'react-native'
|
||||
|
||||
import DorpDownMenu, { type DorpDownMenuProps as _DorpDownMenuProps } from '@/components/common/DorpDownMenu'
|
||||
import Text from '@/components/common/Text'
|
||||
import { useI18n } from '@/lang'
|
||||
|
||||
import { useSettingValue } from '@/store/setting/hook'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
|
||||
type Sources = Readonly<Array<LX.OnlineSource | 'all'>>
|
||||
|
||||
export interface SourceSelectorProps<S extends Sources> {
|
||||
fontSize?: number
|
||||
center?: _DorpDownMenuProps<any>['center']
|
||||
onSourceChange: (source: S[number]) => void
|
||||
}
|
||||
|
||||
export interface SourceSelectorType<S extends Sources> {
|
||||
setSourceList: (list: S, activeSource: S[number]) => void
|
||||
}
|
||||
|
||||
const Component = <S extends Sources>({ fontSize = 15, center, onSourceChange }: SourceSelectorProps<S>, ref: Ref<SourceSelectorType<S>>) => {
|
||||
const sourceNameType = useSettingValue('common.sourceNameType')
|
||||
const [list, setList] = useState([] as unknown as S)
|
||||
const [source, setSource] = useState<S[number]>('kw')
|
||||
const t = useI18n()
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
setSourceList(list, activeSource) {
|
||||
setList(list)
|
||||
setSource(activeSource)
|
||||
},
|
||||
}), [])
|
||||
|
||||
const sourceList_t = useMemo(() => {
|
||||
return list.map(s => ({ label: t(`source_${sourceNameType}_${s}`), action: s }))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [list, sourceNameType, t])
|
||||
|
||||
type DorpDownMenuProps = _DorpDownMenuProps<typeof sourceList_t>
|
||||
|
||||
const handleChangeSource: DorpDownMenuProps['onPress'] = ({ action }) => {
|
||||
onSourceChange(action)
|
||||
setSource(action)
|
||||
}
|
||||
|
||||
return (
|
||||
<DorpDownMenu
|
||||
menus={sourceList_t}
|
||||
center={center}
|
||||
onPress={handleChangeSource}
|
||||
fontSize={fontSize}
|
||||
activeId={source}
|
||||
>
|
||||
<View style={styles.sourceMenu}>
|
||||
<Text style={{ textAlign: center ? 'center' : 'left' }} numberOfLines={1} size={fontSize}>{t(`source_${sourceNameType}_${source}`)}</Text>
|
||||
</View>
|
||||
</DorpDownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(Component) as <S extends Sources>(p: SourceSelectorProps<S> & { ref?: Ref<SourceSelectorType<S>> }) => JSX.Element | null
|
||||
|
||||
|
||||
const styles = createStyle({
|
||||
sourceMenu: {
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
// paddingTop: 12,
|
||||
// paddingBottom: 12,
|
||||
paddingLeft: 15,
|
||||
paddingRight: 15,
|
||||
// backgroundColor: '#ccc',
|
||||
|
||||
},
|
||||
})
|
258
src/components/TimeoutExitEditModal.tsx
Normal file
258
src/components/TimeoutExitEditModal.tsx
Normal file
@ -0,0 +1,258 @@
|
||||
import React, { useRef, useImperativeHandle, forwardRef, useState, useEffect } from 'react'
|
||||
import ConfirmAlert, { ConfirmAlertType } from '@/components/common/ConfirmAlert'
|
||||
import Text from '@/components/common/Text'
|
||||
import { View } from 'react-native'
|
||||
import Input, { InputType } from '@/components/common/Input'
|
||||
import { createStyle, toast } from '@/utils/tools'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import { cancelTimeoutExit, getTimeoutExitTime, onTimeUpdate, startTimeoutExit, stopTimeoutExit, useTimeoutExitTimeInfo } from '@/core/player/timeoutExit'
|
||||
import { useI18n } from '@/lang'
|
||||
import CheckBox from './common/CheckBox'
|
||||
import { useSettingValue } from '@/store/setting/hook'
|
||||
import { updateSetting } from '@/core/common'
|
||||
import settingState from '@/store/setting/state'
|
||||
|
||||
const MAX_MIN = 1440
|
||||
const rxp = /([1-9]\d*)/
|
||||
const formatTime = (time: number) => {
|
||||
// let d = parseInt(time / 86400)
|
||||
// d = d ? d.toString() + ':' : ''
|
||||
// time = time % 86400
|
||||
let h = Math.trunc(time / 3600)
|
||||
let hStr = h ? h.toString() + ':' : ''
|
||||
time = time % 3600
|
||||
const m = Math.trunc(time / 60).toString().padStart(2, '0')
|
||||
const s = Math.trunc(time % 60).toString().padStart(2, '0')
|
||||
return `${hStr}${m}:${s}`
|
||||
}
|
||||
const Status = () => {
|
||||
const theme = useTheme()
|
||||
const t = useI18n()
|
||||
const exitTimeInfo = useTimeoutExitTimeInfo()
|
||||
return (
|
||||
<View style={styles.tip}>
|
||||
{
|
||||
exitTimeInfo.time < 0
|
||||
? (
|
||||
<Text>{t('timeout_exit_tip_off')}</Text>
|
||||
)
|
||||
: (
|
||||
<Text>{t('timeout_exit_tip_on', { time: formatTime(exitTimeInfo.time) })}</Text>
|
||||
)
|
||||
}
|
||||
{exitTimeInfo.isPlayedStop ? <Text color={theme['c-font-label']} size={13}>{t('timeout_exit_btn_wait_tip')}</Text> : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
interface TimeInputType {
|
||||
setText: (text: string) => void
|
||||
getText: () => string
|
||||
focus: () => void
|
||||
}
|
||||
const TimeInput = forwardRef<TimeInputType, {}>((props, ref) => {
|
||||
const theme = useTheme()
|
||||
const [text, setText] = useState('')
|
||||
const inputRef = useRef<InputType>(null)
|
||||
const t = useI18n()
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getText() {
|
||||
return text.trim()
|
||||
},
|
||||
setText(text) {
|
||||
setText(text)
|
||||
},
|
||||
focus() {
|
||||
inputRef.current?.focus()
|
||||
},
|
||||
}))
|
||||
|
||||
return (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder={t('timeout_exit_input_tip')}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
const Setting = () => {
|
||||
const t = useI18n()
|
||||
const timeoutExitPlayed = useSettingValue('player.timeoutExitPlayed')
|
||||
const onCheckChange = (check: boolean) => {
|
||||
updateSetting({ 'player.timeoutExitPlayed': check })
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.checkbox}>
|
||||
<CheckBox check={timeoutExitPlayed} label={t('timeout_exit_label_isPlayed')} onChange={onCheckChange} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTimeInfo = () => {
|
||||
const [exitTimeInfo, setExitTimeInfo] = useState({
|
||||
cancelText: '',
|
||||
confirmText: '',
|
||||
isPlayedStop: false,
|
||||
active: false,
|
||||
})
|
||||
const t = useI18n()
|
||||
|
||||
useEffect(() => {
|
||||
let active: boolean | null = null
|
||||
const remove = onTimeUpdate((time, isPlayedStop) => {
|
||||
if (time < 0) {
|
||||
if (active) {
|
||||
setExitTimeInfo({
|
||||
cancelText: isPlayedStop ? t('timeout_exit_btn_wait_cancel') : '',
|
||||
confirmText: '',
|
||||
isPlayedStop,
|
||||
active: false,
|
||||
})
|
||||
active = false
|
||||
}
|
||||
} else {
|
||||
if (active !== true) {
|
||||
setExitTimeInfo({
|
||||
cancelText: t('timeout_exit_btn_cancel'),
|
||||
confirmText: t('timeout_exit_btn_update'),
|
||||
isPlayedStop,
|
||||
active: true,
|
||||
})
|
||||
active = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
remove()
|
||||
}
|
||||
}, [t])
|
||||
|
||||
return exitTimeInfo
|
||||
}
|
||||
|
||||
export interface TimeoutExitEditModalType {
|
||||
show: () => void
|
||||
}
|
||||
interface TimeoutExitEditModalProps {
|
||||
timeInfo: ReturnType<typeof useTimeInfo>
|
||||
}
|
||||
|
||||
export default forwardRef<TimeoutExitEditModalType, TimeoutExitEditModalProps>(({ timeInfo }, ref) => {
|
||||
const alertRef = useRef<ConfirmAlertType>(null)
|
||||
const timeInputRef = useRef<TimeInputType>(null)
|
||||
const [visible, setVisible] = useState(false)
|
||||
const t = useI18n()
|
||||
|
||||
const handleShow = () => {
|
||||
alertRef.current?.setVisible(true)
|
||||
requestAnimationFrame(() => {
|
||||
if (settingState.setting['player.timeoutExit']) timeInputRef.current?.setText(settingState.setting['player.timeoutExit'])
|
||||
// setTimeout(() => {
|
||||
// timeInputRef.current?.focus()
|
||||
// }, 300)
|
||||
})
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
show() {
|
||||
if (visible) handleShow()
|
||||
else {
|
||||
setVisible(true)
|
||||
requestAnimationFrame(() => {
|
||||
handleShow()
|
||||
})
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
const handleCancel = () => {
|
||||
if (timeInfo.isPlayedStop) {
|
||||
cancelTimeoutExit()
|
||||
return
|
||||
}
|
||||
if (!timeInfo.active) return
|
||||
stopTimeoutExit()
|
||||
toast(t('timeout_exit_tip_cancel'))
|
||||
}
|
||||
const handleConfirm = () => {
|
||||
let timeStr = timeInputRef.current?.getText() ?? ''
|
||||
if (rxp.test(timeStr)) {
|
||||
// if (timeStr != RegExp.$1) toast(t('input_error'))
|
||||
timeStr = RegExp.$1
|
||||
if (parseInt(timeStr) > MAX_MIN) {
|
||||
toast(t('timeout_exit_tip_max', { num: MAX_MIN }))
|
||||
// timeStr = timeStr.substring(0, timeStr.length - 1)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (timeStr.length) toast(t('input_error'))
|
||||
timeStr = ''
|
||||
}
|
||||
if (!timeStr) return
|
||||
const time = parseInt(timeStr)
|
||||
cancelTimeoutExit()
|
||||
startTimeoutExit(time * 60)
|
||||
toast(t('timeout_exit_tip_on', { time: formatTime(getTimeoutExitTime()) }))
|
||||
updateSetting({ 'player.timeoutExit': String(time) })
|
||||
alertRef.current?.setVisible(false)
|
||||
}
|
||||
|
||||
return (
|
||||
visible
|
||||
? <ConfirmAlert
|
||||
ref={alertRef}
|
||||
cancelText={timeInfo.cancelText}
|
||||
confirmText={timeInfo.confirmText}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
>
|
||||
<View style={styles.alertContent}>
|
||||
<Status />
|
||||
<View style={styles.inputContent}>
|
||||
<TimeInput ref={timeInputRef} />
|
||||
<Text style={styles.inputLabel}>{t('timeout_exit_min')}</Text>
|
||||
</View>
|
||||
<Setting />
|
||||
</View>
|
||||
</ConfirmAlert>
|
||||
: null
|
||||
)
|
||||
})
|
||||
|
||||
const styles = createStyle({
|
||||
alertContent: {
|
||||
flexShrink: 1,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
tip: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
checkbox: {
|
||||
marginTop: 5,
|
||||
},
|
||||
inputContent: {
|
||||
marginTop: 8,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
input: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
// borderRadius: 4,
|
||||
// paddingTop: 2,
|
||||
// paddingBottom: 2,
|
||||
},
|
||||
inputLabel: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -1,46 +0,0 @@
|
||||
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>
|
||||
})
|
||||
|
52
src/components/common/Badge.tsx
Normal file
52
src/components/common/Badge.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React, { memo, useMemo } from 'react'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import Text from './Text'
|
||||
// const menuItemHeight = 42
|
||||
// const menuItemWidth = 100
|
||||
|
||||
const styles = createStyle({
|
||||
text: {
|
||||
// paddingLeft: 4,
|
||||
// paddingRight: 4,
|
||||
// borderRadius: 2,
|
||||
// lineHeight: 12,
|
||||
// marginTop: 2,
|
||||
marginLeft: 5,
|
||||
// marginRight: 5,
|
||||
// marginBottom: 2,
|
||||
// alignSelf: 'flex-start',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
export type BadgeType = 'normal' | 'secondary' | 'tertiary'
|
||||
|
||||
export default memo(({ type = 'normal', children }: {
|
||||
type?: BadgeType
|
||||
children: string
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
// console.log(visible)
|
||||
const colors = useMemo(() => {
|
||||
const colors = { textColor: '' }
|
||||
switch (type) {
|
||||
case 'normal':
|
||||
// colors.bgColor = theme.primary
|
||||
colors.textColor = theme['c-badge-primary']
|
||||
break
|
||||
case 'secondary':
|
||||
// colors.bgColor = theme.primary
|
||||
colors.textColor = theme['c-badge-secondary']
|
||||
break
|
||||
case 'tertiary':
|
||||
// colors.bgColor = theme.primary
|
||||
colors.textColor = theme['c-badge-tertiary']
|
||||
break
|
||||
}
|
||||
return colors
|
||||
}, [type, theme])
|
||||
|
||||
return <Text style={styles.text} size={9} color={colors.textColor}>{children}</Text>
|
||||
})
|
||||
|
@ -1,30 +0,0 @@
|
||||
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.3 : 1, ...style }} {...props} ref={btnRef}>
|
||||
{children}
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default forwardRef(Btn)
|
||||
|
46
src/components/common/Button.tsx
Normal file
46
src/components/common/Button.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import React, { useMemo, useRef, useImperativeHandle, forwardRef } from 'react'
|
||||
import { Pressable, PressableProps, StyleSheet, View, ViewProps } from 'react-native'
|
||||
// import { AppColors } from '@/theme'
|
||||
|
||||
|
||||
export interface BtnProps extends PressableProps {
|
||||
ripple?: PressableProps['android_ripple']
|
||||
style?: ViewProps['style']
|
||||
onChangeText?: (value: string) => void
|
||||
onClearText?: () => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
|
||||
export interface BtnType {
|
||||
measure: (callback: (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void) => void
|
||||
}
|
||||
|
||||
export default forwardRef<BtnType, BtnProps>(({ ripple: propsRipple = {}, disabled, children, style, ...props }, ref) => {
|
||||
const theme = useTheme()
|
||||
const btnRef = useRef<View>(null)
|
||||
const ripple = useMemo(() => ({
|
||||
color: theme['c-primary-light-200-alpha-700'],
|
||||
...propsRipple,
|
||||
}), [theme, propsRipple])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
measure(callback) {
|
||||
btnRef.current?.measure(callback)
|
||||
},
|
||||
}))
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
android_ripple={ripple}
|
||||
disabled={disabled}
|
||||
style={StyleSheet.compose({ opacity: disabled ? 0.3 : 1 }, style)}
|
||||
{...props}
|
||||
ref={btnRef}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
)
|
||||
})
|
||||
|
@ -1,24 +1,34 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { StyleSheet, View, TouchableOpacity, Text } from 'react-native'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { View, TouchableOpacity } from 'react-native'
|
||||
import CheckBox from '@react-native-community/checkbox'
|
||||
|
||||
import { useGetter } from '@/store'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
import { scaleSizeH, scaleSizeW } from '@/utils/pixelRatio'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import Text from './Text'
|
||||
|
||||
export default ({ check, label, children, onChange, disabled = false, need = false, marginRight = 0, marginBottom = 0 }) => {
|
||||
const theme = useGetter('common', 'theme')
|
||||
export interface CheckBoxProps {
|
||||
check: boolean
|
||||
label: string
|
||||
children?: React.ReactNode
|
||||
onChange: (check: boolean) => void
|
||||
disabled?: boolean
|
||||
need?: boolean
|
||||
marginRight?: number
|
||||
marginBottom?: number
|
||||
}
|
||||
|
||||
export default ({ check, label, children, onChange, disabled = false, need = false, marginRight = 0, marginBottom = 0 }: CheckBoxProps) => {
|
||||
const theme = useTheme()
|
||||
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])
|
||||
const tintColors = {
|
||||
true: theme['c-primary'],
|
||||
false: theme['c-600'],
|
||||
}
|
||||
const disabledTintColors = {
|
||||
true: theme['c-primary-alpha-600'],
|
||||
false: theme['c-400'],
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (need) {
|
||||
@ -34,33 +44,33 @@ export default ({ check, label, children, onChange, disabled = false, need = fal
|
||||
|
||||
const handleLabelPress = useCallback(() => {
|
||||
if (isDisabled) return
|
||||
onChange && onChange(!check)
|
||||
onChange?.(!check)
|
||||
}, [isDisabled, onChange, check])
|
||||
|
||||
|
||||
const contentStyle = StyleSheet.compose(styles.content, { marginBottom })
|
||||
const labelStyle = StyleSheet.compose(styles.label, { marginRight })
|
||||
const contentStyle = { ...styles.content, marginBottom: scaleSizeH(marginBottom) }
|
||||
const labelStyle = { ...styles.label, marginRight: scaleSizeW(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 style={labelStyle}>{label ? <Text style={styles.name} color={theme['c-500']}>{label}</Text> : children}</View>
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<View style={contentStyle}>
|
||||
<CheckBox value={check} disabled={isDisabled} onValueChange={onChange} tintColors={tintColors} />
|
||||
<CheckBox value={check} disabled={isDisabled} onValueChange={onChange} tintColors={tintColors} scale={1} />
|
||||
<TouchableOpacity style={labelStyle} activeOpacity={0.3} onPress={handleLabelPress}>
|
||||
{label ? <Text style={{ ...styles.name, color: theme.normal }}>{label}</Text> : children}
|
||||
{label ? <Text style={styles.name}>{label}</Text> : children}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const styles = createStyle({
|
||||
content: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
@ -79,11 +89,10 @@ const styles = StyleSheet.create({
|
||||
// marginRight: 15,
|
||||
// alignItems: 'center',
|
||||
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
// paddingRight: 8,
|
||||
paddingRight: 3,
|
||||
},
|
||||
name: {
|
||||
marginTop: 2,
|
||||
fontSize: 13,
|
||||
},
|
||||
})
|
||||
|
@ -1,128 +0,0 @@
|
||||
import React, { useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { View, StyleSheet } 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,
|
||||
},
|
||||
})
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user