This commit is contained in:
lyswhut 2023-02-18 00:07:14 +08:00
parent 3a16de6ea7
commit 4fdf309fe3
736 changed files with 46947 additions and 31633 deletions

View File

@ -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

View File

@ -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',
},
},
],
}

View File

@ -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
View File

@ -1,3 +0,0 @@
# Windows files should use crlf line endings
# https://help.github.com/articles/dealing-with-line-endings/
*.bat text eol=crlf

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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
View File

@ -0,0 +1 @@
16

View File

@ -1 +1 @@
2.7.4
2.7.5

30
.vscode/i18n-ally-custom-framework.yml vendored Normal file
View 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

View File

@ -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')"

View File

@ -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'

View File

@ -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
View File

@ -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.

View File

@ -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"

View File

@ -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.** { *; }

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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) {

View File

@ -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); }

View File

@ -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);
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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);

View File

@ -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;

View File

@ -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);
}
});

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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)

View 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)

View File

@ -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 &params) {
// 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

View File

@ -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 &params);
} // namespace react
} // namespace facebook

View File

@ -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 &params) {
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

View File

@ -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 &params) 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

View File

@ -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

View File

@ -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

View File

@ -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::

View File

@ -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>

View File

@ -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>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary_dark">#000000</color>
</resources>

View File

@ -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>

View File

@ -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

View File

@ -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

Binary file not shown.

View File

@ -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

View File

@ -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')
}

View File

@ -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
View File

@ -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
View 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)

View File

@ -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)",

View File

@ -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

View File

@ -50,6 +50,6 @@
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<true/>
</dict>
</plist>

View File

@ -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

View File

@ -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

View File

@ -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"]
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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
View File

@ -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
View 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,
})
})
})

View File

@ -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

View File

@ -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',
},
})

View 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%',
},
})

View 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>
)
}

View 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,
},
})

View 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>
)
})

View 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,
},
})

View 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
)
})

View File

@ -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',
},
})

View 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>
)
}

View 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,
},
})

View 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>
)
})

View 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,
},
})

View 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
)
})

View File

@ -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',
},
})

View File

@ -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>
)
})

View 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

View File

@ -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,
},
})

View 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',
},
})

View 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
)
})

View 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',
},
})

View File

@ -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,
},
})

View 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,
},
})

View 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)
}

View 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>
)
}

View File

@ -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)

View 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

View 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)',
},
})

View 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',
},
})

View 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,
},
})

View File

@ -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>
})

View 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>
})

View File

@ -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)

View 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>
)
})

View File

@ -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,
},
})

View File

@ -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