Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1402ac06f6 | ||
|
|
f9334c51a3 | ||
|
|
5d26a5395b | ||
|
|
714535feeb | ||
|
|
7ea4857997 | ||
|
|
81ccb6b1f9 | ||
|
|
98ae0216f7 | ||
|
|
55961d5530 | ||
|
|
7c32fb3599 | ||
|
|
56971a9291 | ||
|
|
9bf20b1762 | ||
|
|
76598b38cb | ||
|
|
5d6f65b699 | ||
|
|
f78db52e0a | ||
|
|
611cbc8c69 | ||
|
|
cab3935a92 | ||
|
|
f8e57827f2 |
@@ -136,8 +136,8 @@ android {
|
||||
applicationId "com.jellyfinaudioplayer"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 5
|
||||
versionName "1.2.3"
|
||||
multiDexEnabled true
|
||||
}
|
||||
splits {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
default_platform(:ios)
|
||||
|
||||
package = load_json(json_path: "package.json")
|
||||
|
||||
platform :ios do
|
||||
lane :alpha do
|
||||
get_certificates(
|
||||
development: true,
|
||||
output_path: 'certificates/'
|
||||
)
|
||||
build_app(
|
||||
scheme: "Jellyfin Player",
|
||||
export_method: "development",
|
||||
output_directory: "build",
|
||||
workspace: "ios/JellyfinAudioPlayer.xcworkspace"
|
||||
)
|
||||
end
|
||||
lane :beta do
|
||||
get_certificates(
|
||||
output_path: 'certificates/'
|
||||
@@ -26,8 +16,12 @@ platform :ios do
|
||||
use_automatic_signing: true,
|
||||
path: "ios/JellyfinAudioPlayer.xcodeproj"
|
||||
)
|
||||
increment_version_number(
|
||||
version_number: package["version"],
|
||||
xcodeproj: "ios/JellyfinAudioPlayer.xcodeproj",
|
||||
);
|
||||
increment_build_number(
|
||||
xcodeproj: "ios/JellyfinAudioPlayer.xcodeproj"
|
||||
xcodeproj: "ios/JellyfinAudioPlayer.xcodeproj",
|
||||
)
|
||||
build_app(
|
||||
scheme: "Jellyfin Player",
|
||||
@@ -37,7 +31,7 @@ platform :ios do
|
||||
)
|
||||
upload_to_testflight
|
||||
build_number = get_build_number(
|
||||
xcodeproj: "ios/JellyfinAudioPlayer.xcodeproj"
|
||||
xcodeproj: "ios/JellyfinAudioPlayer.xcodeproj",
|
||||
)
|
||||
Dir.chdir("..") do
|
||||
sh(
|
||||
@@ -78,8 +72,15 @@ end
|
||||
platform :android do
|
||||
desc "Generate beta build"
|
||||
lane :beta do
|
||||
android_set_version_name(
|
||||
version_name: package['version'],
|
||||
gradle_file: "android/app/build.gradle"
|
||||
)
|
||||
android_set_version_code(
|
||||
gradle_file: "android/app/build.gradle"
|
||||
)
|
||||
gradle(
|
||||
task: "clean assembleRelease",
|
||||
task: "assembleRelease",
|
||||
project_dir: "android"
|
||||
)
|
||||
end
|
||||
|
||||
@@ -3,3 +3,5 @@
|
||||
# Ensure this file is checked in to source control!
|
||||
|
||||
gem 'fastlane-plugin-sentry'
|
||||
gem 'fastlane-plugin-load_json'
|
||||
gem 'fastlane-plugin-versioning_android'
|
||||
|
||||
@@ -16,11 +16,6 @@ or alternatively using `brew install fastlane`
|
||||
|
||||
# Available Actions
|
||||
## iOS
|
||||
### ios alpha
|
||||
```
|
||||
fastlane ios alpha
|
||||
```
|
||||
|
||||
### ios beta
|
||||
```
|
||||
fastlane ios beta
|
||||
|
||||
@@ -554,7 +554,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
CURRENT_PROJECT_VERSION = 34;
|
||||
DEVELOPMENT_TEAM = 238P3C58WC;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
@@ -588,8 +588,9 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
CURRENT_PROJECT_VERSION = 34;
|
||||
DEVELOPMENT_TEAM = 238P3C58WC;
|
||||
INFOPLIST_FILE = JellyfinAudioPlayer/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
@@ -704,8 +705,8 @@
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: Bureau Moeilijke Dingen BV (238P3C58WC)";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution: Bureau Moeilijke Dingen BV (238P3C58WC)";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
|
||||
@@ -60,9 +60,4 @@ static void InitializeFlipper(UIApplication *application) {
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
|
||||
{
|
||||
[RNFSManager setCompletionHandlerForIdentifier:identifier completionHandler:completionHandler];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.2.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>19</string>
|
||||
<string>34</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.2.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>19</string>
|
||||
<string>34</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -285,6 +285,8 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-flipper (0.127.0):
|
||||
- React-Core
|
||||
- react-native-netinfo (7.1.7):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (3.3.2):
|
||||
- React-Core
|
||||
- react-native-slider (4.1.12):
|
||||
@@ -375,34 +377,6 @@ PODS:
|
||||
- React-Core
|
||||
- RNLocalize (2.1.7):
|
||||
- React-Core
|
||||
- RNReanimated (2.3.1):
|
||||
- DoubleConversion
|
||||
- FBLazyVector
|
||||
- FBReactNativeSpec
|
||||
- glog
|
||||
- RCT-Folly
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React
|
||||
- React-callinvoker
|
||||
- React-Core
|
||||
- React-Core/DevSupport
|
||||
- React-Core/RCTWebSocket
|
||||
- React-CoreModules
|
||||
- React-cxxreact
|
||||
- React-jsi
|
||||
- React-jsiexecutor
|
||||
- React-jsinspector
|
||||
- React-RCTActionSheet
|
||||
- React-RCTAnimation
|
||||
- React-RCTBlob
|
||||
- React-RCTImage
|
||||
- React-RCTLinking
|
||||
- React-RCTNetwork
|
||||
- React-RCTSettings
|
||||
- React-RCTText
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNScreens (3.10.1):
|
||||
- React-Core
|
||||
- React-RCTImage
|
||||
@@ -468,6 +442,7 @@ DEPENDENCIES:
|
||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||
- react-native-airplay-button (from `../node_modules/react-native-airplay-button`)
|
||||
- react-native-flipper (from `../node_modules/react-native-flipper`)
|
||||
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
|
||||
- react-native-track-player (from `../node_modules/react-native-track-player`)
|
||||
@@ -491,7 +466,6 @@ DEPENDENCIES:
|
||||
- RNFS (from `../node_modules/react-native-fs`)
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
- RNLocalize (from `../node_modules/react-native-localize`)
|
||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||
- RNScreens (from `../node_modules/react-native-screens`)
|
||||
- "RNSentry (from `../node_modules/@sentry/react-native`)"
|
||||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
@@ -558,6 +532,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-airplay-button"
|
||||
react-native-flipper:
|
||||
:path: "../node_modules/react-native-flipper"
|
||||
react-native-netinfo:
|
||||
:path: "../node_modules/@react-native-community/netinfo"
|
||||
react-native-safe-area-context:
|
||||
:path: "../node_modules/react-native-safe-area-context"
|
||||
react-native-slider:
|
||||
@@ -604,8 +580,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-gesture-handler"
|
||||
RNLocalize:
|
||||
:path: "../node_modules/react-native-localize"
|
||||
RNReanimated:
|
||||
:path: "../node_modules/react-native-reanimated"
|
||||
RNScreens:
|
||||
:path: "../node_modules/react-native-screens"
|
||||
RNSentry:
|
||||
@@ -649,6 +623,7 @@ SPEC CHECKSUMS:
|
||||
React-logger: 933f80c97c633ee8965d609876848148e3fef438
|
||||
react-native-airplay-button: 90c7ba52402c8e92342003b8a1ff78dfb4357a9e
|
||||
react-native-flipper: b9e2e817604af8da0d5a9ba20a8516e780e30f3c
|
||||
react-native-netinfo: 27f287f2d191693f3b9d01a4273137fcf91c3b5d
|
||||
react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057
|
||||
react-native-slider: 6e9b86e76cce4b9e35b3403193a6432ed07e0c81
|
||||
react-native-track-player: 23dd515aacf1d36a0e522ef7fdbc55f13f26d4fb
|
||||
@@ -672,7 +647,6 @@ SPEC CHECKSUMS:
|
||||
RNFS: 3ab21fa6c56d65566d1fb26c2228e2b6132e5e32
|
||||
RNGestureHandler: e5c7cab5f214503dcefd6b2b0cefb050e1f51c4a
|
||||
RNLocalize: f567ea0e35116a641cdffe6683b0d212d568f32a
|
||||
RNReanimated: da3860204e5660c0dd66739936732197d359d753
|
||||
RNScreens: 522705f2e5c9d27efb17f24aceb2bf8335bc7b8e
|
||||
RNSentry: 04bb48bfdd435f5b218cf363f89e6419e9a2460c
|
||||
RNSVG: 4ecc2e8f38b6ebe7889909570c26f3abe8059767
|
||||
|
||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "JellyfinAudioPlayer",
|
||||
"version": "0.2.0",
|
||||
"version": "1.2.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "JellyfinAudioPlayer",
|
||||
"version": "0.2.0",
|
||||
"version": "1.2.3",
|
||||
"dependencies": {
|
||||
"@react-native-community/async-storage": "^1.12.1",
|
||||
"@react-native-community/masked-view": "^0.1.11",
|
||||
"@react-native-community/netinfo": "^7.1.7",
|
||||
"@react-native-community/picker": "^1.8.1",
|
||||
"@react-native-community/slider": "^4.1.12",
|
||||
"@react-navigation/bottom-tabs": "^6.0.9",
|
||||
@@ -3229,6 +3230,14 @@
|
||||
"react-native": ">=0.57"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-community/netinfo": {
|
||||
"version": "7.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-7.1.7.tgz",
|
||||
"integrity": "sha512-QCEuvbTAD7vyCsSsgbWedhTfXlClp4TVHVWYYMjnN7nz6xgZbSp+MI3oo7X5C4JlDHpRm/Q+63hsCgAqKt3WVA==",
|
||||
"peerDependencies": {
|
||||
"react-native": ">=0.59"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-community/picker": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/picker/-/picker-1.8.1.tgz",
|
||||
@@ -18920,6 +18929,12 @@
|
||||
"integrity": "sha512-rQfMIGSR/1r/SyN87+VD8xHHzDYeHaJq6elOSCAD+0iLagXkSI2pfA0LmSXP21uw5i3em7GkkRjfJ8wpqWXZNw==",
|
||||
"requires": {}
|
||||
},
|
||||
"@react-native-community/netinfo": {
|
||||
"version": "7.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-7.1.7.tgz",
|
||||
"integrity": "sha512-QCEuvbTAD7vyCsSsgbWedhTfXlClp4TVHVWYYMjnN7nz6xgZbSp+MI3oo7X5C4JlDHpRm/Q+63hsCgAqKt3WVA==",
|
||||
"requires": {}
|
||||
},
|
||||
"@react-native-community/picker": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/picker/-/picker-1.8.1.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "JellyfinAudioPlayer",
|
||||
"version": "0.2.0",
|
||||
"version": "1.2.3",
|
||||
"main": "src/index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -14,6 +14,7 @@
|
||||
"dependencies": {
|
||||
"@react-native-community/async-storage": "^1.12.1",
|
||||
"@react-native-community/masked-view": "^0.1.11",
|
||||
"@react-native-community/netinfo": "^7.1.7",
|
||||
"@react-native-community/picker": "^1.8.1",
|
||||
"@react-native-community/slider": "^4.1.12",
|
||||
"@react-navigation/bottom-tabs": "^6.0.9",
|
||||
|
||||
3
src/assets/cloud-slash.svg
Normal file
3
src/assets/cloud-slash.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.5674 20.4658C23.7207 19.9736 25.2324 18.2334 25.2324 16.1328C25.2324 14.2607 24.1689 12.5996 22.4199 11.8701C22.4287 7.89746 19.5635 5.03223 15.8809 5.03223C13.543 5.03223 11.7852 6.23633 10.6865 7.83594C10.124 7.67773 9.50879 7.67773 8.9375 7.83594L21.5674 20.4658ZM20.3369 22.7773C20.6182 23.0498 21.0576 23.0498 21.3301 22.7773C21.5938 22.5049 21.6025 22.0566 21.3301 21.7842L6.52051 6.9834C6.23926 6.70215 5.78223 6.71973 5.51855 6.9834C5.25488 7.24707 5.26367 7.71289 5.51855 7.96777L20.3369 22.7773ZM8.18164 20.6592H16.6631L6.24805 10.2705C6.11621 10.6045 6.03711 10.9736 6.01953 11.3779C4.00684 11.7383 2.76758 13.54 2.76758 15.7461C2.76758 18.418 5.10547 20.6592 8.18164 20.6592Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 808 B |
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 24 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.3281 15.6592C21.0703 15.6592 23.2324 13.6465 23.2324 11.1328C23.2324 9.26074 22.1689 7.59961 20.4199 6.87012C20.4287 2.89746 17.5635 0.0322266 13.8809 0.0322266C11.543 0.0322266 9.78516 1.23633 8.68652 2.83594C6.4541 2.23828 4.10742 3.89941 4.01953 6.37793C2.00684 6.73828 0.767578 8.54004 0.767578 10.7461C0.767578 13.418 3.10547 15.6504 6.18164 15.6504L18.3281 15.6592ZM18.3281 13.9014H6.19043C4.09863 13.9014 2.54297 12.4424 2.54297 10.7461C2.54297 8.98828 3.62402 7.6875 5.41699 7.6875C5.54883 7.6875 5.60156 7.61719 5.59277 7.49414C5.54004 4.88379 7.41211 3.9873 9.30176 4.58496C9.41602 4.62012 9.48633 4.59375 9.53906 4.49707C10.4092 2.96777 11.6924 1.78125 13.8721 1.78125C16.6318 1.78125 18.6006 3.96973 18.7324 6.52734C18.7588 7.00195 18.7236 7.51172 18.6885 7.93359C18.6709 8.05664 18.7236 8.12695 18.8379 8.14453C20.4287 8.45215 21.457 9.57715 21.457 11.1328C21.457 12.6709 20.0947 13.9014 18.3281 13.9014Z" />
|
||||
<svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.3281 15.6504C21.0703 15.6504 23.2324 13.6377 23.2324 11.124C23.2324 9.26074 22.1689 7.59961 20.4199 6.87012C20.4287 2.88867 17.5635 0.0234375 13.8809 0.0234375C11.543 0.0234375 9.78516 1.23633 8.68652 2.82715C6.4541 2.22949 4.10742 3.89062 4.01953 6.36914C2.00684 6.72949 0.767578 8.54004 0.767578 10.7373C0.767578 13.418 3.10547 15.6504 6.18164 15.6504H18.3281Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1003 B After Width: | Height: | Size: 483 B |
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 22 17" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.400391 12.2139C0.400391 14.5781 2.17578 16.3535 4.71582 16.3535H17.2842C19.8242 16.3535 21.5996 14.5781 21.5996 12.2139C21.5996 11.502 21.3975 10.8604 21.1514 10.2803L18.1279 3.19629C17.5127 1.74609 16.291 0.981445 14.6562 0.981445H7.35254C5.70898 0.981445 4.4873 1.74609 3.88086 3.19629L0.875 10.2451C0.620117 10.834 0.400391 11.4844 0.400391 12.2139ZM3.57324 8.26758L5.48926 3.5918C5.78809 2.83594 6.46484 2.44043 7.37012 2.44043H14.6299C15.5439 2.44043 16.2207 2.83594 16.5195 3.5918L18.4355 8.26758C18.084 8.15332 17.6973 8.08301 17.2842 8.08301H4.71582C4.30273 8.08301 3.9248 8.15332 3.57324 8.26758ZM2.08789 12.2139C2.08789 10.8164 3.13379 9.77051 4.71582 9.77051H17.2842C18.8662 9.77051 19.9121 10.8164 19.9121 12.2139C19.9121 13.7607 18.8662 14.6572 17.2842 14.6572H4.71582C3.13379 14.6572 2.08789 13.6201 2.08789 12.2139ZM8.45117 13.084C8.45117 13.3828 8.68848 13.6113 8.9873 13.6113C9.27734 13.6113 9.50586 13.3828 9.50586 13.084V11.3525C9.50586 11.0625 9.27734 10.8252 8.9873 10.8252C8.68848 10.8252 8.45117 11.0625 8.45117 11.3525V13.084ZM10.4727 13.084C10.4727 13.3828 10.7012 13.6113 11 13.6113C11.29 13.6113 11.5273 13.3828 11.5273 13.084V11.3525C11.5273 11.0625 11.29 10.8252 11 10.8252C10.7012 10.8252 10.4727 11.0625 10.4727 11.3525V13.084ZM12.4854 13.084C12.4854 13.3828 12.7227 13.6113 13.0215 13.6113C13.3115 13.6113 13.5488 13.3828 13.5488 13.084V11.3525C13.5488 11.0625 13.3115 10.8252 13.0215 10.8252C12.7227 10.8252 12.4854 11.0625 12.4854 11.3525V13.084ZM14.5068 13.084C14.5068 13.3828 14.7441 13.6113 15.043 13.6113C15.333 13.6113 15.5703 13.3828 15.5703 13.084V11.3525C15.5703 11.0625 15.333 10.8252 15.043 10.8252C14.7441 10.8252 14.5068 11.0625 14.5068 11.3525V13.084ZM16.5283 13.084C16.5283 13.3828 16.7656 13.6113 17.0645 13.6113C17.3545 13.6113 17.583 13.3828 17.583 13.084V11.3525C17.583 11.0625 17.3545 10.8252 17.0645 10.8252C16.7656 10.8252 16.5283 11.0625 16.5283 11.3525V13.084Z" />
|
||||
<svg width="22" height="16" viewBox="0 0 22 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.33789 6.74023H17.6621C18.5498 6.74023 19.3496 6.94238 20.0439 7.30273L17.873 2.21387C17.293 0.851562 16.1152 0.0957031 14.542 0.0957031H7.45801C5.87598 0.0957031 4.70703 0.851562 4.12695 2.21387L1.94727 7.29395C2.6416 6.94238 3.4502 6.74023 4.33789 6.74023ZM4.33789 15.2305H17.6621C19.8066 15.2305 21.2832 13.7627 21.2832 11.6357C21.2832 9.5 19.8066 8.03223 17.6621 8.03223H4.33789C2.18457 8.03223 0.708008 9.5 0.708008 11.6357C0.708008 13.7627 2.18457 15.2305 4.33789 15.2305ZM16.4316 11.6357C16.4316 11.0029 16.9678 10.4668 17.6094 10.4668C18.2422 10.4668 18.7783 11.0029 18.7783 11.6357C18.7783 12.2773 18.2422 12.7959 17.6094 12.7959C16.9678 12.8047 16.4316 12.2861 16.4316 11.6357Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 806 B |
@@ -11,6 +11,7 @@ import {
|
||||
} from '@react-navigation/native';
|
||||
import { useColorScheme } from 'react-native';
|
||||
import { ColorSchemeContext, themes } from './Colors';
|
||||
import DownloadManager from './DownloadManager';
|
||||
// import ErrorReportingAlert from 'utility/ErrorReportingAlert';
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
@@ -41,6 +42,7 @@ export default function App(): JSX.Element {
|
||||
<ColorSchemeContext.Provider value={theme}>
|
||||
<NavigationContainer theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<Routes />
|
||||
<DownloadManager />
|
||||
</NavigationContainer>
|
||||
</ColorSchemeContext.Provider>
|
||||
</PersistGate>
|
||||
|
||||
@@ -22,7 +22,7 @@ function generateStyles(scheme: ColorSchemeName) {
|
||||
borderColor: scheme === 'dark' ? '#262626' : '#ddd',
|
||||
},
|
||||
activeBackground: {
|
||||
backgroundColor: `${THEME_COLOR}${scheme === 'dark' ? '66' : '16'}`,
|
||||
backgroundColor: `${THEME_COLOR}${scheme === 'dark' ? '26' : '16'}`,
|
||||
},
|
||||
imageBackground: {
|
||||
backgroundColor: scheme === 'dark' ? '#333' : '#ddd',
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { useTypedSelector } from 'store';
|
||||
import CloudIcon from 'assets/cloud.svg';
|
||||
import CloudDownArrow from 'assets/cloud-down-arrow.svg';
|
||||
import CloudExclamationMarkIcon from 'assets/cloud-exclamation-mark.svg';
|
||||
import InternalDriveIcon from 'assets/internal-drive.svg';
|
||||
import useDefaultStyles from './Colors';
|
||||
import { EntityId } from '@reduxjs/toolkit';
|
||||
import Svg, { Circle, CircleProps } from 'react-native-svg';
|
||||
import { Animated, Easing } from 'react-native';
|
||||
import styled from 'styled-components/native';
|
||||
|
||||
interface DownloadIconProps {
|
||||
trackId: EntityId;
|
||||
@@ -14,6 +16,17 @@ interface DownloadIconProps {
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
const DownloadContainer = styled.View`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const IconOverlay = styled.View`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: scale(0.5);
|
||||
`;
|
||||
|
||||
function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
|
||||
// determine styles
|
||||
const defaultStyles = useDefaultStyles();
|
||||
@@ -21,6 +34,7 @@ function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
|
||||
|
||||
// Get download icon from state
|
||||
const entity = useTypedSelector((state) => state.downloads.entities[trackId]);
|
||||
const isQueued = useTypedSelector((state) => state.downloads.queued.includes(trackId));
|
||||
|
||||
// Memoize calculations for radius and circumference of the circle
|
||||
const radius = useMemo(() => size / 2, [size]);
|
||||
@@ -52,43 +66,46 @@ function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
|
||||
return () => offsetAnimation.removeListener(subscription);
|
||||
}, [offsetAnimation]);
|
||||
|
||||
if (!entity) {
|
||||
if (!entity && !isQueued) {
|
||||
return (
|
||||
<CloudIcon width={size} height={size} fill={iconFill} />
|
||||
);
|
||||
}
|
||||
|
||||
const { isComplete, isFailed } = entity;
|
||||
|
||||
if (isComplete) {
|
||||
if (entity?.isComplete) {
|
||||
return (
|
||||
<InternalDriveIcon width={size} height={size} fill={iconFill} />
|
||||
);
|
||||
}
|
||||
|
||||
if (isFailed) {
|
||||
if (entity?.isFailed) {
|
||||
return (
|
||||
<CloudExclamationMarkIcon width={size} height={size} fill={iconFill} />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isComplete && !isFailed) {
|
||||
if (isQueued || (!entity?.isFailed && !entity?.isComplete)) {
|
||||
return (
|
||||
<Svg width={size} height={size} transform={[{ rotate: '-90deg' }]}>
|
||||
<Circle
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={radius - 1}
|
||||
stroke={iconFill}
|
||||
// @ts-expect-error react-native-svg has outdated react-native typings
|
||||
ref={circleRef}
|
||||
strokeWidth={1.5}
|
||||
strokeDasharray={[ circumference, circumference ]}
|
||||
strokeDashoffset={circumference}
|
||||
strokeLinecap='round'
|
||||
fill='transparent'
|
||||
/>
|
||||
</Svg>
|
||||
<DownloadContainer>
|
||||
<Svg width={size} height={size} transform={[{ rotate: '-90deg' }]}>
|
||||
<Circle
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={radius - 1}
|
||||
stroke={iconFill}
|
||||
// @ts-expect-error react-native-svg has outdated react-native typings
|
||||
ref={circleRef}
|
||||
strokeWidth={1.5}
|
||||
strokeDasharray={[ circumference, circumference ]}
|
||||
strokeDashoffset={circumference}
|
||||
strokeLinecap='round'
|
||||
fill='transparent'
|
||||
/>
|
||||
</Svg>
|
||||
<IconOverlay>
|
||||
<CloudDownArrow width={size} height={size} fill={iconFill} />
|
||||
</IconOverlay>
|
||||
</DownloadContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
110
src/components/DownloadManager.ts
Normal file
110
src/components/DownloadManager.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { EntityId } from '@reduxjs/toolkit';
|
||||
import { xor } from 'lodash';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { DocumentDirectoryPath, readDir } from 'react-native-fs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTypedSelector } from 'store';
|
||||
import { completeDownload, downloadTrack } from 'store/downloads/actions';
|
||||
|
||||
/**
|
||||
* The maximum number of concurrent downloads we allow to take place at once.
|
||||
* This is hardcoded at 5 for now, but might be extracted to a setting later.
|
||||
*/
|
||||
const MAX_CONCURRENT_DOWNLOADS = 5;
|
||||
|
||||
/**
|
||||
* This is a component that tracks queued downloads, and starts them one-by-one,
|
||||
* so that we don't overload react-native-fs, as well as the render performance.
|
||||
*/
|
||||
function DownloadManager () {
|
||||
// Retrieve store helpers
|
||||
const { queued, ids } = useTypedSelector((state) => state.downloads);
|
||||
const rehydrated = useTypedSelector((state) => state._persist.rehydrated);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Keep state for the currently active downloads (i.e. the downloads that
|
||||
// have actually been pushed out to react-native-fs).
|
||||
const [hasRehydratedOrphans, setHasRehydratedOrphans] = useState(false);
|
||||
const activeDownloads = useRef(new Set<EntityId>());
|
||||
|
||||
useEffect(() => {
|
||||
// GUARD: Check if the queue is empty
|
||||
if (!queued.length) {
|
||||
// If so, clear any current downloads
|
||||
activeDownloads.current.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Apparently, the queue has changed, and we need to manage
|
||||
// First, we pick the first n downloads
|
||||
const queue = queued.slice(0, MAX_CONCURRENT_DOWNLOADS);
|
||||
|
||||
// We then filter for new downloads
|
||||
queue.filter((id) => !activeDownloads.current.has(id))
|
||||
.forEach((id) => {
|
||||
// We dispatch the actual call to start downloading
|
||||
dispatch(downloadTrack(id));
|
||||
// And add it to the active downloads
|
||||
activeDownloads.current.add(id);
|
||||
});
|
||||
|
||||
// Lastly, if something isn't part of the queue, but is of active
|
||||
// downloads, we can assume the download completed.
|
||||
xor(Array.from(activeDownloads.current), queue)
|
||||
.forEach((id) => activeDownloads.current.delete(id));
|
||||
|
||||
}, [queued, dispatch, activeDownloads]);
|
||||
|
||||
useEffect(() => {
|
||||
// GUARD: We only run this functino once
|
||||
if (hasRehydratedOrphans) {
|
||||
return;
|
||||
}
|
||||
|
||||
// GUARD: If the state has not been rehydrated, we cannot check against
|
||||
// the store ids.
|
||||
if (!rehydrated) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(ids);
|
||||
|
||||
/**
|
||||
* Whenever the store is cleared, existing downloads get "lost" because
|
||||
* the only reference we have is the store. This function checks for
|
||||
* those lost downloads and adds them to the store
|
||||
*/
|
||||
async function hydrateOrphanedDownloads() {
|
||||
// Retrieve all files for this app
|
||||
const files = await readDir(DocumentDirectoryPath);
|
||||
|
||||
// Loop through the mp3 files
|
||||
files.filter((file) => file.isFile() && file.name.endsWith('.mp3'))
|
||||
.forEach((file) => {
|
||||
const id = file.name.replace('.mp3', '');
|
||||
console.log(id, ids.includes(id));
|
||||
|
||||
// GUARD: If the id is already in the store, there's nothing
|
||||
// left for us to do.
|
||||
if (ids.includes(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the download to the store
|
||||
dispatch(completeDownload({
|
||||
id,
|
||||
location: file.path,
|
||||
size: Number.parseInt(file.size),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
hydrateOrphanedDownloads();
|
||||
setHasRehydratedOrphans(true);
|
||||
}, [rehydrated, ids, hasRehydratedOrphans, dispatch]);
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default DownloadManager;
|
||||
@@ -55,5 +55,6 @@
|
||||
"delete-album": "Delete Album",
|
||||
"delete-playlist": "Delete Playlist",
|
||||
"total-download-size": "Total Download Size",
|
||||
"retry-failed-downloads": "Retry Failed Downloads"
|
||||
"retry-failed-downloads": "Retry Failed Downloads",
|
||||
"you-are-offline-message": "You are currently offline. You can only play previously downloaded music."
|
||||
}
|
||||
@@ -44,5 +44,18 @@
|
||||
"playlist": "Playlist",
|
||||
"play-playlist": "Speel Playlist",
|
||||
"shuffle-album": "Shuffle Album",
|
||||
"shuffle-playlist": "Shuffle Playlist"
|
||||
"shuffle-playlist": "Shuffle Playlist",
|
||||
"downloads": "Downloads",
|
||||
"download-track": "Download Track",
|
||||
"download-album": "Download Album",
|
||||
"download-playlist": "Download Playlist",
|
||||
"no-downloads": "Je hebt nog geen nummers gedownload",
|
||||
"delete-track": "Verwijder Track",
|
||||
"delete-all-tracks": "Verwijder alle nummers",
|
||||
"delete-album": "Verwijder Album",
|
||||
"delete-playlist": "Verwijder Playlist",
|
||||
"total-download-size": "Totale grootte downloads",
|
||||
"retry-failed-downloads": "Probeer Mislukte Downloads Opnieuw",
|
||||
"you-are-offline-message": "Je bent op dit moment offline. Je kunt alleen eerder gedownloade nummers afspelen."
|
||||
|
||||
}
|
||||
@@ -53,4 +53,5 @@ export type LocaleKeys = 'play-next'
|
||||
| 'delete-track'
|
||||
| 'total-download-size'
|
||||
| 'no-downloads'
|
||||
| 'retry-failed-downloads'
|
||||
| 'retry-failed-downloads'
|
||||
| 'you-are-offline-message'
|
||||
@@ -10,7 +10,7 @@ import ArrowClockwise from 'assets/arrow-clockwise.svg';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { EntityId } from '@reduxjs/toolkit';
|
||||
import { downloadTrack, removeDownloadedTrack } from 'store/downloads/actions';
|
||||
import { queueTrackForDownload, removeDownloadedTrack } from 'store/downloads/actions';
|
||||
import Button from 'components/Button';
|
||||
import { t } from 'i18n-js';
|
||||
import DownloadIcon from 'components/DownloadIcon';
|
||||
@@ -51,7 +51,7 @@ function Downloads() {
|
||||
|
||||
// Retry a single failed track
|
||||
const retryTrack = useCallback((id: EntityId) => {
|
||||
dispatch(downloadTrack(id));
|
||||
dispatch(queueTrackForDownload(id));
|
||||
}, [dispatch]);
|
||||
|
||||
// Retry all failed tracks
|
||||
@@ -92,7 +92,7 @@ function Downloads() {
|
||||
<DownloadIcon trackId={item} />
|
||||
</View>
|
||||
<View style={{ flexShrink: 1, marginRight: 8 }}>
|
||||
<Text style={{ fontSize: 16, marginBottom: 4 }} numberOfLines={1}>
|
||||
<Text style={[{ fontSize: 16, marginBottom: 4 }, defaultStyles.text]} numberOfLines={1}>
|
||||
{tracks[item]?.Name}
|
||||
</Text>
|
||||
<Text style={[{ flexShrink: 1, fontSize: 11 }, defaultStyles.textHalfOpacity]} numberOfLines={1}>
|
||||
|
||||
@@ -20,7 +20,7 @@ import DownloadIcon from 'components/DownloadIcon';
|
||||
import CloudDownArrow from 'assets/cloud-down-arrow.svg';
|
||||
import Trash from 'assets/trash.svg';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { downloadTrack, removeDownloadedTrack } from 'store/downloads/actions';
|
||||
import { queueTrackForDownload, removeDownloadedTrack } from 'store/downloads/actions';
|
||||
import { selectDownloadedTracks } from 'store/downloads/selectors';
|
||||
|
||||
const Screen = Dimensions.get('screen');
|
||||
@@ -55,7 +55,6 @@ const TrackContainer = styled.View<{isPlaying: boolean}>`
|
||||
flex-direction: row;
|
||||
|
||||
${props => props.isPlaying && css`
|
||||
background-color: ${THEME_COLOR}16;
|
||||
margin: 0 -20px;
|
||||
padding: 15px 24px;
|
||||
`}
|
||||
@@ -112,7 +111,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
||||
navigation.navigate('TrackPopupMenu', { trackId: trackIds[index] });
|
||||
}, [navigation, trackIds]);
|
||||
const downloadAllTracks = useCallback(() => {
|
||||
trackIds.forEach((trackId) => dispatch(downloadTrack(trackId)));
|
||||
trackIds.forEach((trackId) => dispatch(queueTrackForDownload(trackId)));
|
||||
}, [dispatch, trackIds]);
|
||||
const deleteAllTracks = useCallback(() => {
|
||||
downloadedTracks.forEach((trackId) => dispatch(removeDownloadedTrack(trackId)));
|
||||
@@ -140,7 +139,10 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
||||
onPress={selectTrack}
|
||||
onLongPress={longPressTrack}
|
||||
>
|
||||
<TrackContainer isPlaying={currentTrack?.backendId === trackId || false} style={defaultStyles.border}>
|
||||
<TrackContainer
|
||||
isPlaying={currentTrack?.backendId === trackId || false}
|
||||
style={[defaultStyles.border, currentTrack?.backendId === trackId || false ? defaultStyles.activeBackground : null ]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
defaultStyles.text,
|
||||
|
||||
37
src/screens/Player/components/ConnectionNotice.tsx
Normal file
37
src/screens/Player/components/ConnectionNotice.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { useNetInfo } from '@react-native-community/netinfo';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import styled from 'styled-components/native';
|
||||
import CloudSlash from 'assets/cloud-slash.svg';
|
||||
import { Text } from 'react-native';
|
||||
import { t } from '@localisation';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
|
||||
const Well = styled.View`
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
`;
|
||||
|
||||
function ConnectionNotice() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const { isInternetReachable } = useNetInfo();
|
||||
|
||||
if (!isInternetReachable) {
|
||||
return (
|
||||
<Well style={defaultStyles.activeBackground}>
|
||||
<CloudSlash width={24} height={24} fill={THEME_COLOR} />
|
||||
<Text style={{ color: THEME_COLOR, marginLeft: 12 }}>
|
||||
{t('you-are-offline-message')}
|
||||
</Text>
|
||||
</Well>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default ConnectionNotice;
|
||||
@@ -5,6 +5,7 @@ import ProgressBar from './components/ProgressBar';
|
||||
import NowPlaying from './components/NowPlaying';
|
||||
import Queue from './components/Queue';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
import ConnectionNotice from './components/ConnectionNotice';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
inner: {
|
||||
@@ -18,6 +19,7 @@ export default function Player() {
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.inner} style={defaultStyles.view}>
|
||||
<NowPlaying />
|
||||
<ConnectionNotice />
|
||||
<MediaControls />
|
||||
<ProgressBar />
|
||||
<Queue />
|
||||
|
||||
@@ -13,7 +13,7 @@ import TrashIcon from 'assets/trash.svg';
|
||||
import Text from 'components/Text';
|
||||
import { WrappableButton, WrappableButtonRow } from 'components/WrappableButtonRow';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { downloadTrack, removeDownloadedTrack } from 'store/downloads/actions';
|
||||
import { queueTrackForDownload, removeDownloadedTrack } from 'store/downloads/actions';
|
||||
import usePlayTracks from 'utility/usePlayTracks';
|
||||
import { selectIsDownloaded } from 'store/downloads/selectors';
|
||||
|
||||
@@ -57,7 +57,7 @@ function TrackPopupMenu() {
|
||||
|
||||
// Callback for downloading the track
|
||||
const handleDownload = useCallback(() => {
|
||||
dispatch(downloadTrack(trackId));
|
||||
dispatch(queueTrackForDownload(trackId));
|
||||
closeModal();
|
||||
}, [trackId, dispatch, closeModal]);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export const downloadAdapter = createEntityAdapter<DownloadEntity>({
|
||||
selectId: (entity) => entity.id,
|
||||
});
|
||||
|
||||
export const queueTrackForDownload = createAction<EntityId>('download/queue');
|
||||
export const initializeDownload = createAction<{ id: EntityId, size?: number, jobId?: number }>('download/initialize');
|
||||
export const progressDownload = createAction<{ id: EntityId, progress: number, jobId?: number }>('download/progress');
|
||||
export const completeDownload = createAction<{ id: EntityId, location: string, size?: number }>('download/complete');
|
||||
@@ -46,7 +47,7 @@ export const downloadTrack = createAsyncThunk(
|
||||
);
|
||||
|
||||
export const removeDownloadedTrack = createAsyncThunk(
|
||||
'/downloads/track/remove',
|
||||
'/downloads/remove/track',
|
||||
async(id: EntityId) => {
|
||||
return unlink(`${DocumentDirectoryPath}/${id}.mp3`);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { createSlice, Dictionary, EntityId } from '@reduxjs/toolkit';
|
||||
import { completeDownload, downloadAdapter, failDownload, initializeDownload, progressDownload, removeDownloadedTrack } from './actions';
|
||||
import {
|
||||
completeDownload,
|
||||
downloadAdapter,
|
||||
failDownload,
|
||||
initializeDownload,
|
||||
progressDownload,
|
||||
queueTrackForDownload,
|
||||
removeDownloadedTrack
|
||||
} from './actions';
|
||||
import { DownloadEntity } from './types';
|
||||
|
||||
interface State {
|
||||
entities: Dictionary<DownloadEntity>;
|
||||
ids: EntityId[];
|
||||
queued: EntityId[];
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
export const initialState: State = {
|
||||
entities: {},
|
||||
ids: [],
|
||||
queued: [],
|
||||
};
|
||||
|
||||
const downloads = createSlice({
|
||||
@@ -32,6 +42,7 @@ const downloads = createSlice({
|
||||
});
|
||||
});
|
||||
builder.addCase(completeDownload, (state, action) => {
|
||||
// Update the item to be completed
|
||||
downloadAdapter.updateOne(state, {
|
||||
id: action.payload.id,
|
||||
changes: {
|
||||
@@ -40,6 +51,11 @@ const downloads = createSlice({
|
||||
isComplete: true,
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the item from the queue
|
||||
const newSet = new Set(state.queued);
|
||||
newSet.delete(action.payload.id);
|
||||
state.queued = Array.from(newSet);
|
||||
});
|
||||
builder.addCase(failDownload, (state, action) => {
|
||||
downloadAdapter.updateOne(state, {
|
||||
@@ -52,7 +68,17 @@ const downloads = createSlice({
|
||||
});
|
||||
});
|
||||
builder.addCase(removeDownloadedTrack.fulfilled, (state, action) => {
|
||||
// Remove the download if it exists
|
||||
downloadAdapter.removeOne(state, action.meta.arg);
|
||||
|
||||
// Remove the item from the queue if it is in there
|
||||
const newSet = new Set(state.queued);
|
||||
newSet.delete(action.meta.arg);
|
||||
state.queued = Array.from(newSet);
|
||||
});
|
||||
builder.addCase(queueTrackForDownload, (state, action) => {
|
||||
const newSet = new Set(state.queued).add(action.payload);
|
||||
state.queued = Array.from(newSet);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,19 +1,42 @@
|
||||
import { configureStore, getDefaultMiddleware, combineReducers } from '@reduxjs/toolkit';
|
||||
import { useSelector, TypedUseSelectorHook, useDispatch } from 'react-redux';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import { persistStore, persistReducer, PersistConfig } from 'redux-persist';
|
||||
import { persistStore, persistReducer, PersistConfig, createMigrate } from 'redux-persist';
|
||||
import autoMergeLevel2 from 'redux-persist/es/stateReconciler/autoMergeLevel2';
|
||||
|
||||
const persistConfig: PersistConfig<AppState> = {
|
||||
import settings from './settings';
|
||||
import music, { initialState as musicInitialState } from './music';
|
||||
import downloads, { initialState as downloadsInitialState } from './downloads';
|
||||
import { PersistState } from 'redux-persist/es/types';
|
||||
|
||||
const persistConfig: PersistConfig<Omit<AppState, '_persist'>> = {
|
||||
key: 'root',
|
||||
storage: AsyncStorage,
|
||||
stateReconciler: autoMergeLevel2
|
||||
version: 2,
|
||||
stateReconciler: autoMergeLevel2,
|
||||
migrate: createMigrate({
|
||||
// @ts-expect-error migrations are poorly typed
|
||||
1: (state: AppState & PersistState) => {
|
||||
return {
|
||||
...state,
|
||||
settings: state.settings,
|
||||
downloads: downloadsInitialState,
|
||||
music: musicInitialState
|
||||
};
|
||||
},
|
||||
// @ts-expect-error migrations are poorly typed
|
||||
2: (state: AppState) => {
|
||||
return {
|
||||
...state,
|
||||
downloads: {
|
||||
...state.downloads,
|
||||
queued: []
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
import settings from './settings';
|
||||
import music from './music';
|
||||
import downloads from './downloads';
|
||||
|
||||
const reducers = combineReducers({
|
||||
settings,
|
||||
music: music.reducer,
|
||||
@@ -22,15 +45,20 @@ const reducers = combineReducers({
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, reducers);
|
||||
|
||||
const middlewares = [];
|
||||
if (__DEV__) {
|
||||
middlewares.push(require('redux-flipper').default());
|
||||
}
|
||||
|
||||
const store = configureStore({
|
||||
reducer: persistedReducer,
|
||||
middleware: getDefaultMiddleware({ serializableCheck: false, immutableCheck: false }).concat(
|
||||
// logger,
|
||||
__DEV__ ? require('redux-flipper').default() : undefined,
|
||||
...middlewares,
|
||||
),
|
||||
});
|
||||
|
||||
export type AppState = ReturnType<typeof reducers>;
|
||||
export type AppState = ReturnType<typeof reducers> & { _persist: PersistState };
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type AsyncThunkAPI = { state: AppState, dispatch: AppDispatch };
|
||||
export const useTypedSelector: TypedUseSelectorHook<AppState> = useSelector;
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface State {
|
||||
}
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
export const initialState: State = {
|
||||
albums: {
|
||||
...albumAdapter.getInitialState(),
|
||||
isLoading: false,
|
||||
|
||||
Reference in New Issue
Block a user