mirror of
https://github.com/leinelissen/jellyfin-audio-player.git
synced 2026-09-02 21:03:11 +03:00
feat: react native 0.83, liquid glass
This commit is contained in:
+2
-67
@@ -1,69 +1,4 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
// 'plugin:@typescript-eslint/recommended'
|
||||
],
|
||||
globals: {
|
||||
Atomics: 'readonly',
|
||||
SharedArrayBuffer: 'readonly'
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
},
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: [
|
||||
'react',
|
||||
'@typescript-eslint',
|
||||
'react-hooks'
|
||||
],
|
||||
rules: {
|
||||
indent: 'off',
|
||||
'@typescript-eslint/indent': [
|
||||
'error',
|
||||
4,
|
||||
{
|
||||
SwitchCase: 1,
|
||||
}
|
||||
],
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
'unix'
|
||||
],
|
||||
quotes: [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
semi: [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'no-unused-vars': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error'
|
||||
],
|
||||
'react/jsx-no-literals': [
|
||||
'error',
|
||||
{
|
||||
ignoreProps: true
|
||||
}
|
||||
],
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
}
|
||||
}
|
||||
root: true,
|
||||
extends: '@react-native',
|
||||
};
|
||||
@@ -4,41 +4,24 @@ import android.app.Application
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override val reactNativeHost: ReactNativeHost =
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// add(MyReactNativePackage())
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = "index"
|
||||
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
}
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = getDefaultReactHost(applicationContext, reactNativeHost)
|
||||
override val reactHost: ReactHost by lazy {
|
||||
getDefaultReactHost(
|
||||
context = applicationContext,
|
||||
packageList =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// add(MyReactNativePackage())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
}
|
||||
loadReactNative(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="21dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="21">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M5.637,15.2h5.308V9.821c0,-0.571 0.484,-1.037 1.055,-1.037c0.571,0 1.046,0.466 1.046,1.037V15.2h5.818c2.602,0 4.368,-1.6 4.368,-3.797c0,-1.819 -1.063,-3.419 -2.812,-4.131c0.009,-3.973 -2.856,-6.838 -6.539,-6.838c-2.338,0 -4.096,1.204 -5.194,2.795c-2.101,-0.519 -4.579,1.072 -4.667,3.55c-2.013,0.352 -3.252,2.127 -3.252,4.21C0.768,13.319 2.71,15.2 5.637,15.2ZM12,20.579c0.211,0 0.413,-0.07 0.624,-0.281l2.953,-2.848c0.15,-0.149 0.229,-0.308 0.229,-0.519c0,-0.413 -0.334,-0.712 -0.738,-0.712c-0.202,0 -0.404,0.088 -0.545,0.246l-1.222,1.301l-0.571,0.65l0.088,-1.301V15.086c0,-0.439 -0.369,-0.809 -0.817,-0.809c-0.457,0 -0.826,0.37 -0.826,0.809v2.03l0.088,1.301l-0.563,-0.65l-1.23,-1.301c-0.141,-0.158 -0.352,-0.246 -0.554,-0.246c-0.404,0 -0.73,0.299 -0.73,0.712c0,0.211 0.088,0.37 0.238,0.519l2.953,2.848C11.587,20.509 11.78,20.579 12,20.579Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="88.87dp"
|
||||
android:height="76.71dp"
|
||||
android:viewportWidth="88.87"
|
||||
android:viewportHeight="76.71">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M35.06,47c0,6 4.64,10.21 11.13,10.21c5.71,0 14.36,-4.25 14.36,-15.58V2.78A2.86,2.86 0,0 0,57.71 0a2.82,2.82 0,0 0,-2.83 2.78V30.91c0,1.76 -0.68,2.88 -2.19,3.22L46.58,35.4C38.72,37 35.06,41.06 35.06,47ZM0,66.5c0,6 4.64,10.21 11.13,10.21c5.72,0 14.36,-4.25 14.36,-15.58V22.31a2.84,2.84 0,0 0,-5.67 0V50.44c0,1.76 -0.68,2.88 -2.19,3.22l-6.11,1.27C3.66,56.54 0,60.6 0,66.5Zm63.38,0c0,6 4.64,10.21 11.13,10.21c5.71,0 14.36,-4.25 14.36,-15.58V22.31a2.84,2.84 0,0 0,-5.67 0V50.44c0,1.76 -0.68,2.88 -2.19,3.22L74.9,54.93C67,56.54 63.38,60.6 63.38,66.5Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M6.509,13.017c1.315,0 2.547,-0.397 3.573,-1.075l3.622,3.631c0.24,0.231 0.546,0.347 0.877,0.347c0.686,0 1.191,-0.538 1.191,-1.216c0,-0.314 -0.108,-0.62 -0.339,-0.852L11.835,10.238c0.744,-1.058 1.182,-2.34 1.182,-3.73c0,-3.58 -2.928,-6.508 -6.509,-6.508C2.936,0 0,2.928 0,6.509C0,10.09 2.928,13.017 6.509,13.017ZM6.509,11.28c-2.622,0 -4.772,-2.15 -4.772,-4.772c0,-2.622 2.15,-4.772 4.772,-4.772c2.622,0 4.772,2.15 4.772,4.772C11.28,9.13 9.13,11.28 6.509,11.28Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="86.57dp"
|
||||
android:height="85.31dp"
|
||||
android:viewportWidth="86.57"
|
||||
android:viewportHeight="85.31">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M43.31,79.91c0.93,0 1.76,-0.1 2.78,-0.15l2.25,4.3a2.25,2.25 0,0 0,2.44 1.22a2.4,2.4 0,0 0,1.91 -2l0.68,-4.79a46.37,46.37 0,0 0,5.27 -1.9l3.52,3.22a2.34,2.34 0,0 0,2.78 0.29A2.42,2.42 0,0 0,66 77.56l-1,-4.73a32.28,32.28 0,0 0,4.3 -3.52l4.44,1.81a2.23,2.23 0,0 0,2.73 -0.68a2.19,2.19 0,0 0,0.15 -2.69l-2.54,-4.1a33.11,33.11 0,0 0,2.74 -4.88l4.83,0.24a2.33,2.33 0,0 0,2.3 -1.56a2.37,2.37 0,0 0,-0.79 -2.64l-3.8,-3a29.92,29.92 0,0 0,1 -5.52l4.54,-1.46a2.31,2.31 0,0 0,0 -4.44L80.37,39a30.62,30.62 0,0 0,-1 -5.57l3.8,-3a2.28,2.28 0,0 0,-1.51 -4.11l-4.83,0.2a30.73,30.73 0,0 0,-2.74 -4.88l2.54,-4.1a2.19,2.19 0,0 0,-0.15 -2.64a2.3,2.3 0,0 0,-2.73 -0.73l-4.44,1.75A41.16,41.16 0,0 0,65 12.43l1,-4.69A2.39,2.39 0,0 0,64.94 5.2a2.21,2.21 0,0 0,-2.78 0.34L58.64,8.67c-1.76,-0.73 -3.51,-1.37 -5.27,-1.91L52.69,2a2.33,2.33 0,0 0,-4.35 -0.83L46.09,5.54c-1,0 -1.85,-0.09 -2.78,-0.09s-1.85,0 -2.83,0.09L38.18,1.2A2.29,2.29 0,0 0,33.89 2L33.2,6.76a50.08,50.08 0,0 0,-5.27 1.91L24.37,5.54a2.22,2.22 0,0 0,-2.79 -0.34a2.35,2.35 0,0 0,-1 2.54l1,4.69a41,41 0,0 0,-4.29 3.51l-4.4,-1.75a2.29,2.29 0,0 0,-2.73 0.73a2.12,2.12 0,0 0,-0.2 2.59l2.54,4.15a36.25,36.25 0,0 0,-2.73 4.88l-4.84,-0.2a2.28,2.28 0,0 0,-2.29 1.52a2.32,2.32 0,0 0,0.83 2.59l3.76,3A34.46,34.46 0,0 0,6.2 39L1.61,40.41a2.34,2.34 0,0 0,0 4.44L6.2,46.31a33.66,33.66 0,0 0,1 5.52l-3.76,3a2.38,2.38 0,0 0,-0.83 2.64A2.32,2.32 0,0 0,4.88 59l4.84,-0.24a39.6,39.6 0,0 0,2.73 4.88l-2.54,4.1a2.27,2.27 0,0 0,0.2 2.69a2.22,2.22 0,0 0,2.73 0.68l4.4,-1.81a32.2,32.2 0,0 0,4.29 3.52l-1,4.73a2.3,2.3 0,0 0,3.81 2.25l3.56,-3.22a40.86,40.86 0,0 0,5.27 1.9l0.69,4.79a2.34,2.34 0,0 0,1.85 2a2.21,2.21 0,0 0,2.44 -1.22l2.3,-4.3C41.46,79.81 42.29,79.91 43.31,79.91ZM53.17,39.91c-2.05,-5 -5.56,-7.57 -10,-7.57a11.22,11.22 0,0 0,-2.58 0.34L30.22,15a30.32,30.32 0,0 1,43.22 25ZM13,42.7A30.7,30.7 0,0 1,25.29 17.9l10.5,17.82a10.43,10.43 0,0 0,-3 7.08a10.13,10.13 0,0 0,2.93 6.88L25,67.16A30.55,30.55 0,0 1,13 42.7ZM38.88,42.7a4.4,4.4 0,0 1,8.79 0a4.4,4.4 0,0 1,-8.79 0ZM43.27,73.22a31,31 0,0 1,-13.48 -3.08l10.7,-17.38a10.76,10.76 0,0 0,2.63 0.34c4.45,0 8,-2.59 10,-7.67H73.39A30,30 0,0 1,43.31 73.27Z"/>
|
||||
</vector>
|
||||
@@ -1,15 +1,11 @@
|
||||
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 = "35.0.0"
|
||||
buildToolsVersion = "36.0.0"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 35
|
||||
targetSdkVersion = 35
|
||||
compileSdkVersion = 36
|
||||
targetSdkVersion = 36
|
||||
ndkVersion = "27.1.12297006"
|
||||
kotlinVersion = "2.0.21"
|
||||
kotlinVersion = "2.1.20"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
|
||||
@@ -36,4 +36,9 @@ newArchEnabled=true
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
hermesEnabled=true
|
||||
|
||||
# Use this property to enable edge-to-edge display support.
|
||||
# This allows your app to draw behind system bars for an immersive UI.
|
||||
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
||||
edgeToEdgeEnabled=false
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Vendored
+3
-3
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -114,7 +114,7 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
Vendored
+7
-2
@@ -1,3 +1,8 @@
|
||||
@REM Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
@REM
|
||||
@REM This source code is licensed under the MIT license found in the
|
||||
@REM LICENSE file in the root directory of this source tree.
|
||||
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@@ -70,11 +75,11 @@ goto fail
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ module.exports = {
|
||||
[
|
||||
'module:react-native-dotenv'
|
||||
],
|
||||
'react-native-reanimated/plugin',
|
||||
'@babel/plugin-proposal-numeric-separator'
|
||||
'@babel/plugin-proposal-numeric-separator',
|
||||
'react-native-worklets/plugin',
|
||||
]
|
||||
};
|
||||
|
||||
@@ -17,16 +17,6 @@
|
||||
FA01635F2599C28FC19F2EC3 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3896494129CBC30258D9BB1C /* PrivacyInfo.xcprivacy */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
AB4A8E012857C8DA005A1ED0 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 13B07F861A680F5B00A75B9A;
|
||||
remoteInfo = Fintunes;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
13B07F961A680F5B00A75B9A /* Fintunes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Fintunes.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
|
||||
@@ -265,7 +255,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
|
||||
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"\\\"$WITH_ENVIRONMENT\\\" \\\"$REACT_NATIVE_XCODE\\\"\"\n";
|
||||
};
|
||||
062FA28CBD13CA4E60734D70 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@@ -274,11 +264,11 @@
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-frameworks.sh",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermesvm.framework/hermesvm",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
@@ -367,7 +357,6 @@
|
||||
AB4A8E022857C8DA005A1ED0 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 13B07F861A680F5B00A75B9A /* Fintunes */;
|
||||
targetProxy = AB4A8E012857C8DA005A1ED0 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
@@ -509,7 +498,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD = "";
|
||||
LDPLUSPLUS = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
@@ -579,7 +568,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD = "";
|
||||
LDPLUSPLUS = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -33,6 +35,8 @@
|
||||
</dict>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>Inter-VariableFont_slnt,wght.ttf</string>
|
||||
|
||||
@@ -24,7 +24,6 @@ target 'Fintunes' do
|
||||
)
|
||||
|
||||
post_install do |installer|
|
||||
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
|
||||
react_native_post_install(
|
||||
installer,
|
||||
config[:reactNativePath],
|
||||
|
||||
+1913
-988
File diff suppressed because it is too large
Load Diff
+40
-38
@@ -12,41 +12,42 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@d11/react-native-fast-image": "8.13.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-community/blur": "4.4.1",
|
||||
"@react-native-community/datetimepicker": "8.6.0",
|
||||
"@react-native-community/netinfo": "11.4.1",
|
||||
"@react-navigation/bottom-tabs": "6.6.1",
|
||||
"@react-navigation/elements": "1.3.31",
|
||||
"@react-navigation/native": "6.1.18",
|
||||
"@react-navigation/native-stack": "6.11.0",
|
||||
"@react-navigation/stack": "6.4.1",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@sbaiahmed1/react-native-blur": "^4.4.2",
|
||||
"@react-native-community/datetimepicker": "^8.6.0",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-navigation/bottom-tabs": "7.10.1",
|
||||
"@react-navigation/elements": "2.9.5",
|
||||
"@react-navigation/native": "^7.1.28",
|
||||
"@react-navigation/native-stack": "7.10.1",
|
||||
"@react-navigation/stack": "^7.6.16",
|
||||
"@reduxjs/toolkit": "2.11.2",
|
||||
"@shopify/flash-list": "1.8.0",
|
||||
"@shopify/react-native-skia": "2.0.0-next.3",
|
||||
"date-fns": "3.6.0",
|
||||
"@shopify/flash-list": "2.2.0",
|
||||
"@shopify/react-native-skia": "2.4.14",
|
||||
"date-fns": "4.1.0",
|
||||
"events": "3.3.0",
|
||||
"fuse.js": "7.1.0",
|
||||
"i18n-js": "4.5.1",
|
||||
"lodash": "4.17.21",
|
||||
"lodash": "4.17.23",
|
||||
"mime": "4.1.0",
|
||||
"react": "19.1.0",
|
||||
"react": "19.2.3",
|
||||
"react-airplay": "1.2.0",
|
||||
"react-native": "0.79.2",
|
||||
"react-native": "^0.83.1",
|
||||
"react-native-accessibility-settings": "0.1.2",
|
||||
"react-native-collapsible": "1.6.2",
|
||||
"react-native-dotenv": "3.4.11",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-gesture-handler": "2.25.0",
|
||||
"react-native-localize": "3.4.1",
|
||||
"react-native-modal-datetime-picker": "17.1.0",
|
||||
"react-native-reanimated": "3.17.5",
|
||||
"react-native-safe-area-context": "5.6.2",
|
||||
"react-native-screens": "4.10.0",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-gesture-handler": "^2.30.0",
|
||||
"react-native-localize": "3.6.1",
|
||||
"react-native-modal-datetime-picker": "18.0.0",
|
||||
"react-native-reanimated": "^4.2.1",
|
||||
"react-native-safe-area-context": "^5.6.2",
|
||||
"react-native-screens": "^4.20.0",
|
||||
"react-native-shadow-2": "7.1.2",
|
||||
"react-native-svg": "15.15.1",
|
||||
"react-native-track-player": "npm:@weights-ai/react-native-track-player@^4.1.4",
|
||||
"react-native-webview": "13.16.0",
|
||||
"react-native-svg": "^15.15.1",
|
||||
"react-native-track-player": "4.1.2",
|
||||
"react-native-webview": "^13.16.0",
|
||||
"react-native-worklets": "^0.7.2",
|
||||
"react-redux": "9.2.0",
|
||||
"redux": "5.0.1",
|
||||
"redux-persist": "6.0.0",
|
||||
@@ -55,22 +56,23 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.6",
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@react-native-community/cli": "18.0.1",
|
||||
"@react-native/babel-preset": "0.79.2",
|
||||
"@react-native/metro-config": "0.79.2",
|
||||
"@react-native/typescript-config": "0.74.85",
|
||||
"@sentry/cli": "2.44.0",
|
||||
"@sentry/react-native": "6.13.0",
|
||||
"@types/i18n-js": "3.8.9",
|
||||
"@types/lodash": "4.17.16",
|
||||
"@types/node": "20.17.32",
|
||||
"@types/react": "19.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"@react-native-community/cli": "^20.0.0",
|
||||
"@react-native/babel-preset": "^0.83.1",
|
||||
"@react-native/eslint-config": "^0.83.1",
|
||||
"@react-native/metro-config": "^0.83.1",
|
||||
"@react-native/typescript-config": "0.83.1",
|
||||
"@sentry/cli": "3.1.0",
|
||||
"@sentry/react-native": "7.10.0",
|
||||
"@types/events": "^3.0.3",
|
||||
"@types/lodash": "4.17.23",
|
||||
"@types/node": "25.0.10",
|
||||
"@types/react": "19.2.9",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.1",
|
||||
"@typescript-eslint/parser": "8.53.1",
|
||||
"babel-plugin-module-resolver": "5.0.2",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
"metro-react-native-babel-transformer": "0.77.0",
|
||||
"react-native-svg-transformer": "1.5.2",
|
||||
"tslib": "2.8.1",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
diff --git a/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt b/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
|
||||
index baeebce2489e6f9e47ce2ebb9332072e31ba24d5..e52a053d99fcc568d03a0294840b0463f04d01c9 100644
|
||||
--- a/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
|
||||
+++ b/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
|
||||
@@ -756,10 +756,15 @@ class MusicService : HeadlessJsTaskService() {
|
||||
return HeadlessJsTaskConfig(TASK_KEY, Arguments.createMap(), 0, true)
|
||||
}
|
||||
|
||||
- // @MainThread
|
||||
- // fun onBind(intent: Intent?): IBinder {
|
||||
- // return binder
|
||||
- // }
|
||||
+ @MainThread
|
||||
+ override fun onBind(intent: Intent): IBinder {
|
||||
+ return binder
|
||||
+ }
|
||||
+
|
||||
+ @MainThread
|
||||
+ override fun onUnbind(intent: Intent): Boolean {
|
||||
+ return super.onUnbind(intent)
|
||||
+ }
|
||||
|
||||
@MainThread
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
@@ -0,0 +1,690 @@
|
||||
diff --git a/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt b/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt
|
||||
index b2409a09939164c49c0f7a16bb6d3284e8eab8fb..db910e952b7fb6825aac1e8f663163e1c5b1565e 100644
|
||||
--- a/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt
|
||||
+++ b/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt
|
||||
@@ -251,87 +251,97 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun updateOptions(data: ReadableMap?, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun updateOptions(data: ReadableMap?, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- val options = Arguments.toBundle(data)
|
||||
+ val options = Arguments.toBundle(data)
|
||||
|
||||
- options?.let {
|
||||
- musicService.updateOptions(it)
|
||||
- }
|
||||
+ options?.let {
|
||||
+ musicService.updateOptions(it)
|
||||
+ }
|
||||
|
||||
- callback.resolve(null)
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun add(data: ReadableArray?, insertBeforeIndex: Int, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun add(data: ReadableArray?, insertBeforeIndex: Int, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- try {
|
||||
- val tracks = readableArrayToTrackList(data);
|
||||
- if (insertBeforeIndex < -1 || insertBeforeIndex > musicService.tracks.size) {
|
||||
- callback.reject("index_out_of_bounds", "The track index is out of bounds")
|
||||
- return@launch
|
||||
+ try {
|
||||
+ val tracks = readableArrayToTrackList(data);
|
||||
+ if (insertBeforeIndex < -1 || insertBeforeIndex > musicService.tracks.size) {
|
||||
+ callback.reject("index_out_of_bounds", "The track index is out of bounds")
|
||||
+ return@launch
|
||||
+ }
|
||||
+ val index = if (insertBeforeIndex == -1) musicService.tracks.size else insertBeforeIndex
|
||||
+ musicService.add(
|
||||
+ tracks,
|
||||
+ index
|
||||
+ )
|
||||
+ callback.resolve(index)
|
||||
+ } catch (exception: Exception) {
|
||||
+ rejectWithException(callback, exception)
|
||||
}
|
||||
- val index = if (insertBeforeIndex == -1) musicService.tracks.size else insertBeforeIndex
|
||||
- musicService.add(
|
||||
- tracks,
|
||||
- index
|
||||
- )
|
||||
- callback.resolve(index)
|
||||
- } catch (exception: Exception) {
|
||||
- rejectWithException(callback, exception)
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun load(data: ReadableMap?, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
- if (data == null) {
|
||||
- callback.resolve(null)
|
||||
- return@launch
|
||||
- }
|
||||
- val bundle = Arguments.toBundle(data);
|
||||
- if (bundle is Bundle) {
|
||||
- musicService.load(bundleToTrack(bundle))
|
||||
- callback.resolve(null)
|
||||
- } else {
|
||||
- callback.reject("invalid_track_object", "Track was not a dictionary type")
|
||||
+ fun load(data: ReadableMap?, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ if (data == null) {
|
||||
+ callback.resolve(null)
|
||||
+ return@launch
|
||||
+ }
|
||||
+ val bundle = Arguments.toBundle(data);
|
||||
+ if (bundle is Bundle) {
|
||||
+ musicService.load(bundleToTrack(bundle))
|
||||
+ callback.resolve(null)
|
||||
+ } else {
|
||||
+ callback.reject("invalid_track_object", "Track was not a dictionary type")
|
||||
+ }
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun move(fromIndex: Int, toIndex: Int, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
- musicService.move(fromIndex, toIndex)
|
||||
- callback.resolve(null)
|
||||
+ fun move(fromIndex: Int, toIndex: Int, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ musicService.move(fromIndex, toIndex)
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun remove(data: ReadableArray?, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
- val inputIndexes = Arguments.toList(data)
|
||||
- if (inputIndexes != null) {
|
||||
- val size = musicService.tracks.size
|
||||
- val indexes: ArrayList<Int> = ArrayList();
|
||||
- for (inputIndex in inputIndexes) {
|
||||
- val index = if (inputIndex is Int) inputIndex else inputIndex.toString().toInt()
|
||||
- if (index < 0 || index >= size) {
|
||||
- callback.reject(
|
||||
- "index_out_of_bounds",
|
||||
- "One or more indexes was out of bounds"
|
||||
- )
|
||||
- return@launch
|
||||
+ fun remove(data: ReadableArray?, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ val inputIndexes = Arguments.toList(data)
|
||||
+ if (inputIndexes != null) {
|
||||
+ val size = musicService.tracks.size
|
||||
+ val indexes: ArrayList<Int> = ArrayList();
|
||||
+ for (inputIndex in inputIndexes) {
|
||||
+ val index = if (inputIndex is Int) inputIndex else inputIndex.toString().toInt()
|
||||
+ if (index < 0 || index >= size) {
|
||||
+ callback.reject(
|
||||
+ "index_out_of_bounds",
|
||||
+ "One or more indexes was out of bounds"
|
||||
+ )
|
||||
+ return@launch
|
||||
+ }
|
||||
+ indexes.add(index)
|
||||
}
|
||||
- indexes.add(index)
|
||||
+ musicService.remove(indexes)
|
||||
}
|
||||
- musicService.remove(indexes)
|
||||
+ callback.resolve(null)
|
||||
}
|
||||
- callback.resolve(null)
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun updateMetadataForTrack(index: Int, map: ReadableMap?, callback: Promise) =
|
||||
+ fun updateMetadataForTrack(index: Int, map: ReadableMap?, callback: Promise) {
|
||||
scope.launch {
|
||||
if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
@@ -346,284 +356,347 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
|
||||
callback.resolve(null)
|
||||
}
|
||||
}
|
||||
+ }
|
||||
|
||||
@ReactMethod
|
||||
- fun updateNowPlayingMetadata(map: ReadableMap?, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun updateNowPlayingMetadata(map: ReadableMap?, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- if (musicService.tracks.isEmpty())
|
||||
- callback.reject("no_current_item", "There is no current item in the player")
|
||||
+ if (musicService.tracks.isEmpty())
|
||||
+ callback.reject("no_current_item", "There is no current item in the player")
|
||||
|
||||
- val context: ReactContext = context
|
||||
- Arguments.toBundle(map)?.let {
|
||||
- val track = bundleToTrack(it)
|
||||
- musicService.updateNowPlayingMetadata(track)
|
||||
- }
|
||||
+ val context: ReactContext = context
|
||||
+ Arguments.toBundle(map)?.let {
|
||||
+ val track = bundleToTrack(it)
|
||||
+ musicService.updateNowPlayingMetadata(track)
|
||||
+ }
|
||||
|
||||
- callback.resolve(null)
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun clearNowPlayingMetadata(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun clearNowPlayingMetadata(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- if (musicService.tracks.isEmpty())
|
||||
- callback.reject("no_current_item", "There is no current item in the player")
|
||||
+ if (musicService.tracks.isEmpty())
|
||||
+ callback.reject("no_current_item", "There is no current item in the player")
|
||||
|
||||
- musicService.clearNotificationMetadata()
|
||||
- callback.resolve(null)
|
||||
+ musicService.clearNotificationMetadata()
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun removeUpcomingTracks(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun removeUpcomingTracks(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.removeUpcomingTracks()
|
||||
- callback.resolve(null)
|
||||
+ musicService.removeUpcomingTracks()
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun skip(index: Int, initialTime: Float, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun skip(index: Int, initialTime: Float, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.skip(index)
|
||||
+ musicService.skip(index)
|
||||
|
||||
- if (initialTime >= 0) {
|
||||
- musicService.seekTo(initialTime)
|
||||
- }
|
||||
+ if (initialTime >= 0) {
|
||||
+ musicService.seekTo(initialTime)
|
||||
+ }
|
||||
|
||||
- callback.resolve(null)
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun skipToNext(initialTime: Float, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun skipToNext(initialTime: Float, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.skipToNext()
|
||||
+ musicService.skipToNext()
|
||||
|
||||
- if (initialTime >= 0) {
|
||||
- musicService.seekTo(initialTime)
|
||||
- }
|
||||
+ if (initialTime >= 0) {
|
||||
+ musicService.seekTo(initialTime)
|
||||
+ }
|
||||
|
||||
- callback.resolve(null)
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun skipToPrevious(initialTime: Float, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun skipToPrevious(initialTime: Float, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.skipToPrevious()
|
||||
+ musicService.skipToPrevious()
|
||||
|
||||
- if (initialTime >= 0) {
|
||||
- musicService.seekTo(initialTime)
|
||||
- }
|
||||
+ if (initialTime >= 0) {
|
||||
+ musicService.seekTo(initialTime)
|
||||
+ }
|
||||
|
||||
- callback.resolve(null)
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun reset(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun reset(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.stop()
|
||||
- delay(300) // Allow playback to stop
|
||||
- musicService.clear()
|
||||
+ musicService.stop()
|
||||
+ delay(300) // Allow playback to stop
|
||||
+ musicService.clear()
|
||||
|
||||
- callback.resolve(null)
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun play(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun play(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.play()
|
||||
- callback.resolve(null)
|
||||
+ musicService.play()
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun pause(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun pause(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.pause()
|
||||
- callback.resolve(null)
|
||||
+ musicService.pause()
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun stop(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun stop(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.stop()
|
||||
- callback.resolve(null)
|
||||
+ musicService.stop()
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun seekTo(seconds: Float, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun seekTo(seconds: Float, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.seekTo(seconds)
|
||||
- callback.resolve(null)
|
||||
+ musicService.seekTo(seconds)
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun seekBy(offset: Float, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun seekBy(offset: Float, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.seekBy(offset)
|
||||
- callback.resolve(null)
|
||||
+ musicService.seekBy(offset)
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun retry(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun retry(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.retry()
|
||||
- callback.resolve(null)
|
||||
+ musicService.retry()
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun setVolume(volume: Float, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun setVolume(volume: Float, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.setVolume(volume)
|
||||
- callback.resolve(null)
|
||||
+ musicService.setVolume(volume)
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getVolume(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun getVolume(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- callback.resolve(musicService.getVolume())
|
||||
+ callback.resolve(musicService.getVolume())
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun setRate(rate: Float, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun setRate(rate: Float, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.setRate(rate)
|
||||
- callback.resolve(null)
|
||||
+ musicService.setRate(rate)
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getRate(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun getRate(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- callback.resolve(musicService.getRate())
|
||||
+ callback.resolve(musicService.getRate())
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun setRepeatMode(mode: Int, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun setRepeatMode(mode: Int, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.setRepeatMode(RepeatMode.fromOrdinal(mode))
|
||||
- callback.resolve(null)
|
||||
+ musicService.setRepeatMode(RepeatMode.fromOrdinal(mode))
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getRepeatMode(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun getRepeatMode(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- callback.resolve(musicService.getRepeatMode().ordinal)
|
||||
+ callback.resolve(musicService.getRepeatMode().ordinal)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun setPlayWhenReady(playWhenReady: Boolean, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun setPlayWhenReady(playWhenReady: Boolean, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- musicService.playWhenReady = playWhenReady
|
||||
- callback.resolve(null)
|
||||
+ musicService.playWhenReady = playWhenReady
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getPlayWhenReady(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun getPlayWhenReady(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- callback.resolve(musicService.playWhenReady)
|
||||
+ callback.resolve(musicService.playWhenReady)
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getTrack(index: Int, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun getTrack(index: Int, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- if (index >= 0 && index < musicService.tracks.size) {
|
||||
- callback.resolve(Arguments.fromBundle(musicService.tracks[index].originalItem))
|
||||
- } else {
|
||||
- callback.resolve(null)
|
||||
+ if (index >= 0 && index < musicService.tracks.size) {
|
||||
+ callback.resolve(Arguments.fromBundle(musicService.tracks[index].originalItem ?: Bundle()))
|
||||
+ } else {
|
||||
+ callback.resolve(null)
|
||||
+ }
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getQueue(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun getQueue(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- callback.resolve(Arguments.fromList(musicService.tracks.map { it.originalItem }))
|
||||
+ callback.resolve(Arguments.fromList(musicService.tracks.map { it.originalItem }))
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun setQueue(data: ReadableArray?, callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun setQueue(data: ReadableArray?, callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- try {
|
||||
- musicService.clear()
|
||||
- musicService.add(readableArrayToTrackList(data))
|
||||
- callback.resolve(null)
|
||||
- } catch (exception: Exception) {
|
||||
- rejectWithException(callback, exception)
|
||||
+ try {
|
||||
+ musicService.clear()
|
||||
+ musicService.add(readableArrayToTrackList(data))
|
||||
+ callback.resolve(null)
|
||||
+ } catch (exception: Exception) {
|
||||
+ rejectWithException(callback, exception)
|
||||
+ }
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getActiveTrackIndex(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
- callback.resolve(
|
||||
- if (musicService.tracks.isEmpty()) null else musicService.getCurrentTrackIndex()
|
||||
- )
|
||||
+ fun getActiveTrackIndex(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ callback.resolve(
|
||||
+ if (musicService.tracks.isEmpty()) null else musicService.getCurrentTrackIndex()
|
||||
+ )
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getActiveTrack(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
- callback.resolve(
|
||||
- if (musicService.tracks.isEmpty()) null
|
||||
- else Arguments.fromBundle(
|
||||
- musicService.tracks[musicService.getCurrentTrackIndex()].originalItem
|
||||
+ fun getActiveTrack(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ callback.resolve(
|
||||
+ if (musicService.tracks.isEmpty()) null
|
||||
+ else Arguments.fromBundle(
|
||||
+ musicService.tracks[musicService.getCurrentTrackIndex()].originalItem ?: Bundle()
|
||||
+ )
|
||||
)
|
||||
- )
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getDuration(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun getDuration(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- callback.resolve(musicService.getDurationInSeconds())
|
||||
+ callback.resolve(musicService.getDurationInSeconds())
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getBufferedPosition(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun getBufferedPosition(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- callback.resolve(musicService.getBufferedPositionInSeconds())
|
||||
+ callback.resolve(musicService.getBufferedPositionInSeconds())
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getPosition(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ fun getPosition(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
|
||||
- callback.resolve(musicService.getPositionInSeconds())
|
||||
+ callback.resolve(musicService.getPositionInSeconds())
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getProgress(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
- var bundle = Bundle()
|
||||
- bundle.putDouble("duration", musicService.getDurationInSeconds());
|
||||
- bundle.putDouble("position", musicService.getPositionInSeconds());
|
||||
- bundle.putDouble("buffered", musicService.getBufferedPositionInSeconds());
|
||||
- callback.resolve(Arguments.fromBundle(bundle))
|
||||
+ fun getProgress(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ var bundle = Bundle()
|
||||
+ bundle.putDouble("duration", musicService.getDurationInSeconds());
|
||||
+ bundle.putDouble("position", musicService.getPositionInSeconds());
|
||||
+ bundle.putDouble("buffered", musicService.getBufferedPositionInSeconds());
|
||||
+ callback.resolve(Arguments.fromBundle(bundle))
|
||||
+ }
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
- fun getPlaybackState(callback: Promise) = scope.launch {
|
||||
- if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
- callback.resolve(Arguments.fromBundle(musicService.getPlayerStateBundle(musicService.state)))
|
||||
+ fun getPlaybackState(callback: Promise) {
|
||||
+ scope.launch {
|
||||
+ if (verifyServiceBoundOrReject(callback)) return@launch
|
||||
+ callback.resolve(Arguments.fromBundle(musicService.getPlayerStateBundle(musicService.state)))
|
||||
+ }
|
||||
}
|
||||
}
|
||||
diff --git a/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt b/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
|
||||
index afa6b0f335714de0cc8741d5bbdd51b4e04bb3bb..13b2151c24bf892ce218ff0ff65adae6aa07b83b 100644
|
||||
--- a/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
|
||||
+++ b/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
|
||||
@@ -739,9 +739,22 @@ class MusicService : HeadlessJsTaskService() {
|
||||
return bundle
|
||||
}
|
||||
|
||||
+ @MainThread
|
||||
+ private fun getReactContextSafely(): com.facebook.react.bridge.ReactContext? {
|
||||
+ return try {
|
||||
+ val reactHostField = HeadlessJsTaskService::class.java.getDeclaredField("mReactHost")
|
||||
+ reactHostField.isAccessible = true
|
||||
+ val reactHost = reactHostField.get(this) as? com.facebook.react.ReactHost
|
||||
+ reactHost?.currentReactContext
|
||||
+ } catch (e: Exception) {
|
||||
+ // Fallback for older React Native versions or if field is not available
|
||||
+ null
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@MainThread
|
||||
private fun emit(event: String, data: Bundle? = null) {
|
||||
- reactNativeHost.reactInstanceManager.currentReactContext
|
||||
+ getReactContextSafely()
|
||||
?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
||||
?.emit(event, data?.let { Arguments.fromBundle(it) })
|
||||
}
|
||||
@@ -751,7 +764,7 @@ class MusicService : HeadlessJsTaskService() {
|
||||
val payload = Arguments.createArray()
|
||||
data.forEach { payload.pushMap(Arguments.fromBundle(it)) }
|
||||
|
||||
- reactNativeHost.reactInstanceManager.currentReactContext
|
||||
+ getReactContextSafely()
|
||||
?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
||||
?.emit(event, payload)
|
||||
}
|
||||
Generated
+1388
-1125
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -1,5 +1,6 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@sentry/cli'
|
||||
- '@shopify/react-native-skia'
|
||||
patchedDependencies:
|
||||
react-native-webview: patches/react-native-webview.patch
|
||||
'@weights-ai/react-native-track-player': patches/@weights-ai__react-native-track-player.patch
|
||||
react-native-track-player: patches/react-native-track-player.patch
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BlurView, BlurViewProps } from '@react-native-community/blur';
|
||||
import { BlurView, BlurViewProps } from '@sbaiahmed1/react-native-blur';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { ColorSchemeName, Platform, StyleSheet, View, useColorScheme } from 'react-native';
|
||||
@@ -38,7 +38,7 @@ function generateStyles(scheme: ColorSchemeName, highContrast: boolean) {
|
||||
fontSize: 14,
|
||||
},
|
||||
view: {
|
||||
backgroundColor: scheme === 'dark' ? '#111' : '#fff',
|
||||
backgroundColor: scheme === 'dark' ? '#111111' : '#ffffff',
|
||||
},
|
||||
border: {
|
||||
borderColor: scheme === 'dark' ? '#262626' : '#ddd',
|
||||
@@ -132,9 +132,10 @@ export function useUserOrSystemScheme() {
|
||||
export function ColorSchemeProvider({ children }: PropsWithChildren<{}>) {
|
||||
const highContrast = useAccessibilitySetting('darkerSystemColors');
|
||||
const scheme = useUserOrSystemScheme();
|
||||
const normalizedScheme = (scheme === 'dark' ? 'dark' : 'light') as 'light' | 'dark';
|
||||
const theme = highContrast
|
||||
? themes[`${scheme || 'light'}-highcontrast`]
|
||||
: themes[scheme || 'light'];
|
||||
? themes[`${normalizedScheme}-highcontrast` as keyof typeof themes]
|
||||
: themes[normalizedScheme];
|
||||
|
||||
return (
|
||||
<ColorSchemeContext.Provider value={theme}>
|
||||
@@ -174,7 +175,7 @@ export function ColoredBlurView({ children, style, ...props }: PropsWithChildren
|
||||
style={[ StyleSheet.absoluteFill, { overflow: 'hidden'} ]}
|
||||
{...props}
|
||||
blurType={Platform.OS === 'ios' && majorPlatformVersion >= 13
|
||||
? scheme === 'dark' ? 'materialDark' : 'materialLight'
|
||||
? scheme === 'dark' ? 'systemMaterialDark' : 'systemMaterialLight'
|
||||
: scheme === 'dark' ? 'extraDark' : 'xlight'
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import type { TextProps as RNTextProps } from 'react-native';
|
||||
import { StyleSheet, TextInput } from 'react-native';
|
||||
import Animated, { useAnimatedProps } from 'react-native-reanimated';
|
||||
import Animated, { useAnimatedProps, SharedValue, AnimatedProps } from 'react-native-reanimated';
|
||||
import useDefaultStyles from './Colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -12,8 +12,8 @@ const styles = StyleSheet.create({
|
||||
Animated.addWhitelistedNativeProps({ text: true });
|
||||
|
||||
interface TextProps {
|
||||
text: Animated.SharedValue<string>;
|
||||
style?: Animated.AnimateProps<RNTextProps>['style'];
|
||||
text: SharedValue<string>;
|
||||
style?: AnimatedProps<RNTextProps>['style'];
|
||||
}
|
||||
|
||||
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import React, { ForwardedRef, Ref, forwardRef } from 'react';
|
||||
import React, { ForwardedRef, forwardRef } from 'react';
|
||||
import { useHeaderHeight } from '@react-navigation/elements';
|
||||
import { FlatList, FlatListProps, ScrollView, ScrollViewProps, SectionList, SectionListProps } from 'react-native';
|
||||
import useCurrentTrack from '../utility/useCurrentTrack';
|
||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||
import { FlashList, FlashListProps } from '@shopify/flash-list';
|
||||
|
||||
declare module 'react' {
|
||||
function forwardRef<T, P = {}>(
|
||||
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
|
||||
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
|
||||
}
|
||||
import { FlashList, FlashListProps, FlashListRef } from '@shopify/flash-list';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
/**
|
||||
* A wrapper for ScrollView that takes any paddings, margins and insets into
|
||||
@@ -55,7 +49,14 @@ function BareSafeSectionList<I, S>({
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const SafeSectionList = forwardRef(BareSafeSectionList);
|
||||
export const SafeSectionList = forwardRef(BareSafeSectionList) as <I, S>(
|
||||
props: SectionListProps<I, S> & { ref?: ForwardedRef<SectionList<I, S>> }
|
||||
) => React.ReactElement;
|
||||
|
||||
export interface SafeFlatListProps<I> extends FlatListProps<I> {
|
||||
top?: boolean;
|
||||
bottom?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for ScrollView that takes any paddings, margins and insets into
|
||||
@@ -63,24 +64,28 @@ export const SafeSectionList = forwardRef(BareSafeSectionList);
|
||||
*/
|
||||
function BareSafeFlatList<I>({
|
||||
contentContainerStyle,
|
||||
top = true,
|
||||
bottom = true,
|
||||
...props
|
||||
}: FlatListProps<I>, ref: ForwardedRef<FlatList<I>>) {
|
||||
const { top, bottom } = useNavigationOffsets();
|
||||
}: SafeFlatListProps<I>, ref: ForwardedRef<FlatList<I>>) {
|
||||
const offsets = useNavigationOffsets();
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={[
|
||||
{ paddingTop: top, paddingBottom: bottom },
|
||||
{ paddingTop: top ? offsets.top : 0, paddingBottom: bottom ? offsets.bottom : 0 },
|
||||
contentContainerStyle,
|
||||
]}
|
||||
scrollIndicatorInsets={{ top, bottom }}
|
||||
scrollIndicatorInsets={{ top: top ? offsets.top : 0, bottom: bottom ? offsets.bottom : 0 }}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const SafeFlatList = forwardRef(BareSafeFlatList);
|
||||
export const SafeFlatList = forwardRef(BareSafeFlatList) as <I>(
|
||||
props: SafeFlatListProps<I> & { ref?: ForwardedRef<FlatList<I>> }
|
||||
) => React.ReactElement;
|
||||
|
||||
/**
|
||||
* A wrapper for ScrollView that takes any paddings, margins and insets into
|
||||
@@ -89,7 +94,7 @@ export const SafeFlatList = forwardRef(BareSafeFlatList);
|
||||
function BareSafeFlashList<I>({
|
||||
contentContainerStyle,
|
||||
...props
|
||||
}: FlashListProps<I>, ref: ForwardedRef<FlashList<I>>) {
|
||||
}: FlashListProps<I>, ref: ForwardedRef<FlashListRef<I>>) {
|
||||
const { top, bottom } = useNavigationOffsets();
|
||||
|
||||
return (
|
||||
@@ -104,7 +109,9 @@ function BareSafeFlashList<I>({
|
||||
);
|
||||
}
|
||||
|
||||
export const SafeFlashList = forwardRef(BareSafeFlashList);
|
||||
export const SafeFlashList = forwardRef(BareSafeFlashList) as <I>(
|
||||
props: FlashListProps<I> & { ref?: ForwardedRef<FlashListRef<I>> }
|
||||
) => React.ReactElement;
|
||||
|
||||
/**
|
||||
* A hook that returns the correct offset that should be applied to any Views
|
||||
@@ -113,11 +120,12 @@ export const SafeFlashList = forwardRef(BareSafeFlashList);
|
||||
*/
|
||||
export function useNavigationOffsets({ includeOverlay = true } = {} as { includeOverlay?: boolean }) {
|
||||
const headerHeight = useHeaderHeight();
|
||||
const bottomBarHeight = useBottomTabBarHeight();
|
||||
const bottomBarHeight = 60;
|
||||
const { track } = useCurrentTrack();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return {
|
||||
top: headerHeight,
|
||||
top: headerHeight || insets.top,
|
||||
bottom: (track && includeOverlay ? 68 : 0) + bottomBarHeight || 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -187,8 +187,9 @@ function Downloads() {
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
{ListHeaderComponent}
|
||||
<SafeFlatList
|
||||
top={false}
|
||||
data={ids}
|
||||
style={{ flex: 1, paddingTop: 12 }}
|
||||
style={{ flex: 1 }}
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
import React, { useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import { ActivityIndicator, Animated, Dimensions, Platform, Pressable, View } from 'react-native';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { ActivityIndicator, Animated, Dimensions, Platform, Pressable } from 'react-native';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
|
||||
import PlayIcon from '@/assets/icons/play.svg';
|
||||
import PauseIcon from '@/assets/icons/pause.svg';
|
||||
import useCurrentTrack from '@/utility/useCurrentTrack';
|
||||
import TrackPlayer, { State, usePlaybackState, useProgress } from 'react-native-track-player';
|
||||
import { Shadow } from 'react-native-shadow-2';
|
||||
import usePrevious from '@/utility/usePrevious';
|
||||
import { Text } from '@/components/Typography';
|
||||
|
||||
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors';
|
||||
import useDefaultStyles from '@/components/Colors';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { calculateProgressTranslation } from '@/components/Progresstrack';
|
||||
import { NavigationProp } from '@/screens/types';
|
||||
import { ShadowWrapper } from '@/components/Shadow';
|
||||
import { BottomTabBarHeightContext } from '@react-navigation/bottom-tabs';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import AlbumImage from '../../stacks/components/AlbumImage';
|
||||
import { LiquidGlassView } from '@sbaiahmed1/react-native-blur';
|
||||
|
||||
export const NOW_PLAYING_POPOVER_MARGIN = 6;
|
||||
export const NOW_PLAYING_POPOVER_MARGIN = 22;
|
||||
export const NOW_PLAYING_POPOVER_WIDTH = Dimensions.get('screen').width - 2 * NOW_PLAYING_POPOVER_MARGIN;
|
||||
export const NOW_PLAYING_POPOVER_HEIGHT = 58;
|
||||
export const NOW_PLAYING_POPOVER_HEIGHT = 62;
|
||||
|
||||
const PopoverPosition = css`
|
||||
position: absolute;
|
||||
@@ -37,7 +36,7 @@ const Container = styled.ScrollView`
|
||||
|
||||
const InnerContainer = styled.TouchableOpacity`
|
||||
height: ${NOW_PLAYING_POPOVER_HEIGHT}px;
|
||||
padding: 12px;
|
||||
padding: 12px 18px;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
@@ -54,14 +53,6 @@ const ProgressTrack = styled(Animated.View)<{ stroke?: number; opacity?: number}
|
||||
border-radius: 99px;
|
||||
`;
|
||||
|
||||
const ShadowOverlay = styled.View`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
`;
|
||||
|
||||
const Cover = styled(AlbumImage)`
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
@@ -113,12 +104,6 @@ function NowPlaying({ offset = 0, inset }: { offset?: number, inset?: boolean })
|
||||
const { buffered, position } = useProgress();
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
// The regular `useBottomTabBarHeight` hook will throw an error when it
|
||||
// cannot find a height. Since we might use this component in places where
|
||||
// it is unavailable, we'll just use the context directly, which will output
|
||||
// `undefined` when it's not set.
|
||||
const tabBarHeight = useContext(BottomTabBarHeightContext);
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
const previousBuffered = usePrevious(buffered);
|
||||
const previousPosition = usePrevious(position);
|
||||
@@ -172,24 +157,31 @@ function NowPlaying({ offset = 0, inset }: { offset?: number, inset?: boolean })
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(defaultStyles.view.backgroundColor);
|
||||
|
||||
return (
|
||||
<Container
|
||||
style={{
|
||||
bottom: (tabBarHeight || 0)
|
||||
style={[{
|
||||
bottom: (Platform.OS === 'android' ? 104 : 83)
|
||||
+ (inset ? insets.bottom : 0)
|
||||
+ NOW_PLAYING_POPOVER_MARGIN
|
||||
+ 12
|
||||
+ offset
|
||||
}}
|
||||
}]}
|
||||
>
|
||||
{/** TODO: Fix shadow overflow on Android */}
|
||||
{Platform.OS === 'ios' ? (
|
||||
<ShadowOverlay pointerEvents='none'>
|
||||
<Shadow distance={30} style={{ alignSelf: 'stretch', flexBasis: '100%' }} startColor="#00000017">
|
||||
<View style={{ flex: 1, borderRadius: 8 }} />
|
||||
</Shadow>
|
||||
</ShadowOverlay>
|
||||
) : null}
|
||||
<ColoredBlurView style={{ borderRadius: 8 }}>
|
||||
<Animated.View style={ Platform.OS === 'android' && {
|
||||
borderRadius: 100,
|
||||
borderWidth: 1,
|
||||
borderColor: defaultStyles.border.borderColor,
|
||||
}}>
|
||||
<LiquidGlassView
|
||||
glassType="clear"
|
||||
glassOpacity={0.75}
|
||||
glassTintColor={defaultStyles.view.backgroundColor}
|
||||
isInteractive
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
}}
|
||||
>
|
||||
<InnerContainer onPress={openNowPlayingModal} activeOpacity={0.5} testID="open-player-modal">
|
||||
<ShadowWrapper size="small">
|
||||
<Cover source={{ uri: (track.artwork || '') as string }} style={defaultStyles.imageBackground} />
|
||||
@@ -221,7 +213,8 @@ function NowPlaying({ offset = 0, inset }: { offset?: number, inset?: boolean })
|
||||
stroke={4}
|
||||
/>
|
||||
</InnerContainer>
|
||||
</ColoredBlurView>
|
||||
</LiquidGlassView>
|
||||
</Animated.View>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useAppDispatch, useTypedSelector } from '@/store';
|
||||
import { fetchAllAlbums } from '@/store/music/actions';
|
||||
import { ALBUM_CACHE_AMOUNT_OF_DAYS } from '@/CONSTANTS';
|
||||
import TouchableHandler from '@/components/TouchableHandler';
|
||||
import AlbumImage, { AlbumHeight, AlbumItem } from './components/AlbumImage';
|
||||
import AlbumImage, { AlbumItem } from './components/AlbumImage';
|
||||
import { selectAlbumsByAlphabet } from '@/store/music/selectors';
|
||||
import AlphabetScroller from '@/components/AlphabetScroller';
|
||||
import styled from 'styled-components/native';
|
||||
@@ -17,7 +17,7 @@ import { Text } from '@/components/Typography';
|
||||
import { ShadowWrapper } from '@/components/Shadow';
|
||||
import { NavigationProp } from '@/screens/types';
|
||||
import { SafeFlashList, useNavigationOffsets } from '@/components/SafeNavigatorView';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { FlashListRef } from '@shopify/flash-list';
|
||||
|
||||
const SectionContainer = styled.View`
|
||||
justify-content: center;
|
||||
@@ -86,7 +86,7 @@ const Albums: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const getImage = useGetImage();
|
||||
const listRef = useRef<FlashList<any>>(null);
|
||||
const listRef = useRef<FlashListRef<string | string[]>>(null);
|
||||
|
||||
// Convert sections to flat array format for FlashList
|
||||
const flatData = useMemo(() => {
|
||||
@@ -163,7 +163,6 @@ const Albums: React.FC = () => {
|
||||
ref={listRef}
|
||||
renderItem={renderItem}
|
||||
stickyHeaderIndices={stickyHeaderIndices}
|
||||
estimatedItemSize={AlbumHeight}
|
||||
getItemType={(item) => typeof item === 'string' ? 'sectionHeader' : 'row'}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Text } from '@/components/Typography';
|
||||
import { NavigationProp } from '@/screens/types';
|
||||
import { SafeFlashList, useNavigationOffsets } from '@/components/SafeNavigatorView';
|
||||
import { Gap } from '@/components/Utility';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { FlashListRef } from '@shopify/flash-list';
|
||||
|
||||
const SectionContainer = styled.View`
|
||||
justify-content: center;
|
||||
@@ -109,7 +109,7 @@ const Artists: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const getImage = useGetImage();
|
||||
const listRef = useRef<FlashList<any>>(null);
|
||||
const listRef = useRef<FlashListRef<string | SectionArtistItem>>(null);
|
||||
|
||||
// Convert sections to flat array format for FlashList
|
||||
const flatData = useMemo(() => {
|
||||
@@ -180,7 +180,6 @@ const Artists: React.FC = () => {
|
||||
ref={listRef}
|
||||
renderItem={renderItem}
|
||||
stickyHeaderIndices={stickyHeaderIndices}
|
||||
estimatedItemSize={ArtistHeight}
|
||||
getItemType={(item) => typeof item === 'string' ? 'sectionHeader' : 'row'}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import { Text, StyleSheet, View } from 'react-native';
|
||||
import { Text, StyleSheet } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useAppDispatch, useTypedSelector } from '@/store';
|
||||
import { fetchRecentAlbums } from '@/store/music/actions';
|
||||
@@ -18,7 +18,6 @@ import styled from 'styled-components/native';
|
||||
import { ShadowWrapper } from '@/components/Shadow';
|
||||
import { NavigationProp } from '@/screens/types';
|
||||
import { SafeFlatList } from '@/components/SafeNavigatorView';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
columnWrapper: {
|
||||
@@ -79,35 +78,27 @@ const RecentAlbums: React.FC = () => {
|
||||
// Retrieve data on mount
|
||||
useEffect(() => { retrieveData(); }, [retrieveData]);
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingTop: insets.top,
|
||||
paddingBottom: 1 * insets.bottom,
|
||||
}}>
|
||||
<SafeFlatList
|
||||
data={recentAlbums as string[]}
|
||||
refreshing={isLoading}
|
||||
onRefresh={retrieveData}
|
||||
numColumns={2}
|
||||
keyExtractor={d => d}
|
||||
columnWrapperStyle={styles.columnWrapper}
|
||||
ListHeaderComponent={NavigationHeader}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableHandler id={item} onPress={selectAlbum} testID={`select-album-${item}`}>
|
||||
<AlbumItem>
|
||||
<ShadowWrapper size="medium">
|
||||
<AlbumImage source={{ uri: getImage(albums[item]) }} style={defaultStyles.imageBackground} />
|
||||
</ShadowWrapper>
|
||||
<Text style={defaultStyles.text} numberOfLines={1}>{albums[item]?.Name}</Text>
|
||||
<Text style={defaultStyles.textHalfOpacity} numberOfLines={1}>{albums[item]?.AlbumArtist}</Text>
|
||||
</AlbumItem>
|
||||
</TouchableHandler>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
<SafeFlatList
|
||||
data={recentAlbums}
|
||||
refreshing={isLoading}
|
||||
onRefresh={retrieveData}
|
||||
numColumns={2}
|
||||
keyExtractor={d => d}
|
||||
columnWrapperStyle={styles.columnWrapper}
|
||||
ListHeaderComponent={NavigationHeader}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableHandler id={item} onPress={selectAlbum} testID={`select-album-${item}`}>
|
||||
<AlbumItem>
|
||||
<ShadowWrapper size="medium">
|
||||
<AlbumImage source={{ uri: getImage(albums[item]) }} style={defaultStyles.imageBackground} />
|
||||
</ShadowWrapper>
|
||||
<Text style={defaultStyles.text} numberOfLines={1}>{albums[item]?.Name}</Text>
|
||||
<Text style={defaultStyles.textHalfOpacity} numberOfLines={1}>{albums[item]?.AlbumArtist}</Text>
|
||||
</AlbumItem>
|
||||
</TouchableHandler>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ import { t } from '@/localisation';
|
||||
import Button from '@/components/Button';
|
||||
import { Header, Text as BaseText } from '@/components/Typography';
|
||||
import { ShadowWrapper } from '@/components/Shadow';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
const Container = styled.SafeAreaView`
|
||||
const Container = styled(SafeAreaView)`
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import { t } from '@/localisation';
|
||||
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors';
|
||||
@@ -16,7 +16,6 @@ const Stack = createStackNavigator<StackParams>();
|
||||
|
||||
function SearchStack() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const [isInitialRoute, setIsInitialRoute] = useState(true);
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
@@ -29,19 +28,13 @@ function SearchStack() {
|
||||
headerTransparent: true,
|
||||
headerBackground: () => <ColoredBlurView style={StyleSheet.absoluteFill} />,
|
||||
}}
|
||||
screenListeners={{
|
||||
state: (e) => {
|
||||
const { state: { routes } } = e.data as { state: { routes?: { key: string, name: string }[] } };
|
||||
setIsInitialRoute(routes?.length === 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="Search" component={Search} options={{ headerTitle: t('search'), headerShown: false }} />
|
||||
<Stack.Screen name="Album" component={Album} options={{ headerTitle: t('album') }} />
|
||||
<Stack.Screen name="Artist" component={Artist} options={({ route }) => ({ headerTitle: route.params.name })} />
|
||||
<Stack.Screen name="Playlist" component={Playlist} options={{ headerTitle: t('playlist') }} />
|
||||
</Stack.Navigator>
|
||||
<NowPlaying offset={isInitialRoute ? 104 : 0} />
|
||||
<NowPlaying />
|
||||
</GestureHandlerRootView>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
|
||||
@@ -14,9 +14,9 @@ import TouchableHandler from '@/components/TouchableHandler';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import { t } from '@/localisation';
|
||||
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors';
|
||||
import useDefaultStyles from '@/components/Colors';
|
||||
import { searchAndFetch } from '@/store/music/actions';
|
||||
import { Text } from '@/components/Typography';
|
||||
import { SubHeader, Text } from '@/components/Typography';
|
||||
import DownloadIcon from '@/components/DownloadIcon';
|
||||
import ChevronRight from '@/assets/icons/chevron-right.svg';
|
||||
import SearchIcon from '@/assets/icons/magnifying-glass.svg';
|
||||
@@ -44,6 +44,11 @@ const KEYBOARD_OFFSET = Platform.select({
|
||||
});
|
||||
const SEARCH_INPUT_HEIGHT = 104;
|
||||
|
||||
const SEARCH_INPUT_OFFSET = Platform.select({
|
||||
ios: 100,
|
||||
android: 110,
|
||||
});
|
||||
|
||||
const Container = styled(View)`
|
||||
padding: 4px 24px 0 24px;
|
||||
margin-bottom: 0px;
|
||||
@@ -201,7 +206,7 @@ export default function Search() {
|
||||
}));
|
||||
}, 10_000), [dispatch]);
|
||||
|
||||
|
||||
|
||||
const searchItems = useMemo(() => ({
|
||||
...albumEntities,
|
||||
...trackEntities,
|
||||
@@ -242,25 +247,25 @@ export default function Search() {
|
||||
album: (item as AlbumTrack)?.Album,
|
||||
name: item.Name,
|
||||
}));
|
||||
|
||||
|
||||
// Apply active filters (no filters active = all active)
|
||||
if (activeFilters.size > 0) {
|
||||
results = results.filter(result => activeFilters.has(result.type));
|
||||
}
|
||||
|
||||
|
||||
// Apply local playback filter
|
||||
if (localPlaybackOnly) {
|
||||
results = results.filter(result => {
|
||||
const item = searchItems[result.id];
|
||||
if (!item) return false;
|
||||
|
||||
|
||||
switch (result.type) {
|
||||
case 'Audio':
|
||||
// For tracks, check if downloaded
|
||||
return downloadEntities[result.id]?.isComplete === true;
|
||||
case 'MusicAlbum':
|
||||
// For albums, check if any tracks are downloaded
|
||||
return (item as Album).Tracks?.some(trackId =>
|
||||
return (item as Album).Tracks?.some(trackId =>
|
||||
downloadEntities[trackId]?.isComplete === true
|
||||
) ?? false;
|
||||
case 'MusicArtist':
|
||||
@@ -270,7 +275,7 @@ export default function Search() {
|
||||
.some(track => downloadEntities[track.Id]?.isComplete === true);
|
||||
case 'Playlist':
|
||||
// For playlists, check if any tracks are downloaded
|
||||
return (item as Playlist).Tracks?.some(trackId =>
|
||||
return (item as Playlist).Tracks?.some(trackId =>
|
||||
downloadEntities[trackId]?.isComplete === true
|
||||
) ?? false;
|
||||
default:
|
||||
@@ -278,14 +283,14 @@ export default function Search() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Assign the preliminary results
|
||||
setFuseResults(results);
|
||||
setLoading(true);
|
||||
|
||||
|
||||
// Save search query to history after 10 seconds (debounced)
|
||||
saveSearchToHistory(searchTermTrimmed, Array.from(activeFilters), localPlaybackOnly);
|
||||
|
||||
|
||||
try {
|
||||
// Wrap the call in a try/catch block so that we catch any
|
||||
// network issues in search and just use local search if the
|
||||
@@ -362,62 +367,60 @@ export default function Search() {
|
||||
}, [dispatch]);
|
||||
|
||||
const SearchInput = React.useMemo(() => (
|
||||
<Animated.View>
|
||||
<ColoredBlurView>
|
||||
<Container style={[ defaultStyles.border ]}>
|
||||
<View>
|
||||
<Input
|
||||
value={searchTerm}
|
||||
onChangeText={setSearchTerm}
|
||||
style={[defaultStyles.view, { marginBottom: 12 }]}
|
||||
placeholder={t('search') + '...'}
|
||||
icon={<SearchIcon width={14} height={14} fill={defaultStyles.textHalfOpacity.color} />}
|
||||
testID="search-input"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
{searchTerm.length > 0 && !isLoading ? (
|
||||
<ClearButton onPress={handleClearSearch} style={{ marginTop: -4 }}>
|
||||
<XMarkIcon width={16} height={16} fill={defaultStyles.textHalfOpacity.color} />
|
||||
</ClearButton>
|
||||
) : null}
|
||||
{isLoading ? <Loading style={{ marginTop: -4 }}><ActivityIndicator /></Loading> : null}
|
||||
</View>
|
||||
</Container>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<View style={{ paddingHorizontal: 32, paddingBottom: 12, flex: 1, flexDirection: 'row' }}>
|
||||
<SelectableFilter
|
||||
text="Artists"
|
||||
icon={MicrophoneIcon}
|
||||
active={activeFilters.has('MusicArtist')}
|
||||
onPress={() => toggleFilter('MusicArtist')}
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Albums"
|
||||
icon={AlbumIcon}
|
||||
active={activeFilters.has('MusicAlbum')}
|
||||
onPress={() => toggleFilter('MusicAlbum')}
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Tracks"
|
||||
icon={TrackIcon}
|
||||
active={activeFilters.has('Audio')}
|
||||
onPress={() => toggleFilter('Audio')}
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Playlist"
|
||||
icon={PlaylistIcon}
|
||||
active={activeFilters.has('Playlist')}
|
||||
onPress={() => toggleFilter('Playlist')}
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Local Playback"
|
||||
icon={LocalIcon}
|
||||
active={localPlaybackOnly}
|
||||
onPress={() => setLocalPlaybackOnly(prev => !prev)}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</ColoredBlurView>
|
||||
<Animated.View style={{ paddingBottom: SEARCH_INPUT_OFFSET }}>
|
||||
<Container style={[defaultStyles.border]}>
|
||||
<View>
|
||||
<Input
|
||||
value={searchTerm}
|
||||
onChangeText={setSearchTerm}
|
||||
style={[defaultStyles.view, { marginBottom: 12 }]}
|
||||
placeholder={t('search') + '...'}
|
||||
icon={<SearchIcon width={14} height={14} fill={defaultStyles.textHalfOpacity.color} />}
|
||||
testID="search-input"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
{searchTerm.length > 0 && !isLoading ? (
|
||||
<ClearButton onPress={handleClearSearch} style={{ marginTop: -4 }}>
|
||||
<XMarkIcon width={16} height={16} fill={defaultStyles.textHalfOpacity.color} />
|
||||
</ClearButton>
|
||||
) : null}
|
||||
{isLoading ? <Loading style={{ marginTop: -4 }}><ActivityIndicator /></Loading> : null}
|
||||
</View>
|
||||
</Container>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<View style={{ paddingHorizontal: 32, paddingBottom: 12, flex: 1, flexDirection: 'row' }}>
|
||||
<SelectableFilter
|
||||
text="Artists"
|
||||
icon={MicrophoneIcon}
|
||||
active={activeFilters.has('MusicArtist')}
|
||||
onPress={() => toggleFilter('MusicArtist')}
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Albums"
|
||||
icon={AlbumIcon}
|
||||
active={activeFilters.has('MusicAlbum')}
|
||||
onPress={() => toggleFilter('MusicAlbum')}
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Tracks"
|
||||
icon={TrackIcon}
|
||||
active={activeFilters.has('Audio')}
|
||||
onPress={() => toggleFilter('Audio')}
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Playlist"
|
||||
icon={PlaylistIcon}
|
||||
active={activeFilters.has('Playlist')}
|
||||
onPress={() => toggleFilter('Playlist')}
|
||||
/>
|
||||
<SelectableFilter
|
||||
text="Local Playback"
|
||||
icon={LocalIcon}
|
||||
active={localPlaybackOnly}
|
||||
onPress={() => setLocalPlaybackOnly(prev => !prev)}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Animated.View>
|
||||
), [searchTerm, setSearchTerm, defaultStyles, isLoading, activeFilters, toggleFilter, localPlaybackOnly, handleClearSearch]);
|
||||
|
||||
@@ -431,7 +434,7 @@ export default function Search() {
|
||||
keyboardShouldPersistTaps="handled"
|
||||
style={{ flex: 2, }}
|
||||
contentContainerStyle={{ paddingTop: offsets.top, paddingBottom: SEARCH_INPUT_HEIGHT }}
|
||||
scrollIndicatorInsets={{ top: offsets.top / 2, bottom: offsets.bottom / 2 + 10 + SEARCH_INPUT_HEIGHT }}
|
||||
scrollIndicatorInsets={{ top: offsets.top / 2, bottom: offsets.bottom / 2 + 10 + SEARCH_INPUT_HEIGHT }}
|
||||
data={fuseResults}
|
||||
renderItem={({ item: { id, type, name } }: { item: SearchResult }) => {
|
||||
const searchItem = searchItems?.[id];
|
||||
@@ -453,7 +456,7 @@ export default function Search() {
|
||||
{name}
|
||||
</Text>
|
||||
<HalfOpacity style={defaultStyles.text} numberOfLines={1}>
|
||||
{ type === 'MusicAlbum' ?
|
||||
{type === 'MusicAlbum' ?
|
||||
<>
|
||||
<AlbumIcon width={12} height={12} fill={defaultStyles.textHalfOpacity.color} />
|
||||
{' '}
|
||||
@@ -461,9 +464,9 @@ export default function Search() {
|
||||
{' • '}
|
||||
{(searchItem as Album)?.AlbumArtist}
|
||||
</>
|
||||
: null
|
||||
: null
|
||||
}
|
||||
{ type === 'Audio' ?
|
||||
{type === 'Audio' ?
|
||||
<>
|
||||
<TrackIcon width={12} height={12} fill={defaultStyles.textHalfOpacity.color} />
|
||||
{' '}
|
||||
@@ -475,7 +478,7 @@ export default function Search() {
|
||||
</>
|
||||
: null
|
||||
}
|
||||
{ type === 'MusicArtist' ?
|
||||
{type === 'MusicArtist' ?
|
||||
<>
|
||||
<MicrophoneIcon width={12} height={12} fill={defaultStyles.textHalfOpacity.color} />
|
||||
{' '}
|
||||
@@ -483,7 +486,7 @@ export default function Search() {
|
||||
</>
|
||||
: null
|
||||
}
|
||||
{ type === 'Playlist' ?
|
||||
{type === 'Playlist' ?
|
||||
<>
|
||||
<PlaylistIcon width={12} height={12} fill={defaultStyles.textHalfOpacity.color} />
|
||||
{' '}
|
||||
@@ -493,14 +496,14 @@ export default function Search() {
|
||||
}
|
||||
</HalfOpacity>
|
||||
</View>
|
||||
{ type === 'Audio' ?
|
||||
{type === 'Audio' ?
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<DownloadIcon trackId={id} />
|
||||
</View>
|
||||
: null
|
||||
}
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<ChevronRight width={14} height={14} fill={defaultStyles.textQuarterOpacity.color} />
|
||||
<ChevronRight width={14} height={14} fill={defaultStyles.textQuarterOpacity.color} />
|
||||
</View>
|
||||
</SearchResult>
|
||||
</TouchableHandler>
|
||||
@@ -510,14 +513,12 @@ export default function Search() {
|
||||
extraData={[searchTerm, searchItems, activeFilters]}
|
||||
/>
|
||||
) : searchHistory.length > 0 ? (
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
contentContainerStyle={{ paddingTop: offsets.top, paddingBottom: SEARCH_INPUT_HEIGHT }}
|
||||
>
|
||||
<View style={{ paddingTop: 20, paddingBottom: 10, paddingHorizontal: 32 }}>
|
||||
<Text style={{ ...defaultStyles.text, fontSize: 18, letterSpacing: -0.25 }}>
|
||||
{t('recent-searches')}
|
||||
</Text>
|
||||
<SubHeader>{t('recent-searches')}</SubHeader>
|
||||
</View>
|
||||
{searchHistory.map((item, index) => (
|
||||
<TouchableHandler
|
||||
@@ -527,9 +528,9 @@ export default function Search() {
|
||||
>
|
||||
<HistoryItem>
|
||||
<HistoryIconWrapper>
|
||||
<SearchIcon
|
||||
width={18}
|
||||
height={18}
|
||||
<SearchIcon
|
||||
width={18}
|
||||
height={18}
|
||||
fill={defaultStyles.textHalfOpacity.color}
|
||||
/>
|
||||
</HistoryIconWrapper>
|
||||
@@ -559,10 +560,12 @@ export default function Search() {
|
||||
<Button title={t('clear-history')} icon={TrashIcon} onPress={handleClearHistory} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
) : null}
|
||||
) : (
|
||||
<View style={{ flex: 1 }} />
|
||||
)}
|
||||
{(searchTerm.length && !fuseResults.length && !isLoading) ? (
|
||||
<FullSizeContainer>
|
||||
<Text style={{ textAlign: 'center', opacity: 0.5, fontSize: 18 }}>{t('no-results')}</Text>
|
||||
<Text style={{ textAlign: 'center', opacity: 0.5, fontSize: 18 }}>{t('no-results')}</Text>
|
||||
</FullSizeContainer>
|
||||
) : null}
|
||||
{SearchInput}
|
||||
|
||||
+81
-35
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||
import { Platform } from 'react-native';
|
||||
import { createNativeBottomTabNavigator, NativeBottomTabNavigationProp } from '@react-navigation/bottom-tabs/unstable';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import { CompositeNavigationProp } from '@react-navigation/native';
|
||||
@@ -14,20 +14,16 @@ import TrackPopupMenu from './modals/TrackPopupMenu';
|
||||
import SetJellyfinServer from './modals/SetJellyfinServer';
|
||||
import ErrorReportingPopup from './modals/ErrorReportingPopup';
|
||||
|
||||
import SearchIcon from '@/assets/icons/magnifying-glass.svg';
|
||||
import NotesIcon from '@/assets/icons/notes.svg';
|
||||
import GearIcon from '@/assets/icons/gear.svg';
|
||||
import DownloadsIcon from '@/assets/icons/arrow-down-to-line.svg';
|
||||
import { useTypedSelector } from '@/store';
|
||||
import { t } from '@/localisation';
|
||||
import ErrorReportingAlert from '@/utility/ErrorReportingAlert';
|
||||
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors';
|
||||
import useDefaultStyles from '@/components/Colors';
|
||||
import Player from './modals/Player';
|
||||
import { StackParams } from './types';
|
||||
import Lyrics from './modals/Lyrics';
|
||||
|
||||
const Stack = createNativeStackNavigator<StackParams>();
|
||||
const Tab = createBottomTabNavigator();
|
||||
const Tab = createNativeBottomTabNavigator();
|
||||
|
||||
type Screens = {
|
||||
Music: undefined;
|
||||
@@ -48,35 +44,85 @@ function Screens() {
|
||||
<>
|
||||
<ErrorReportingAlert />
|
||||
<Tab.Navigator
|
||||
screenOptions={({ route }) => ({
|
||||
tabBarIcon: function TabBarIcon({ color, size }) {
|
||||
switch (route.name) {
|
||||
case 'SearchTab':
|
||||
return <SearchIcon fill={color} height={size - 4} width={size - 4} />;
|
||||
case 'MusicTab':
|
||||
return <NotesIcon fill={color} height={size} width={size} />;
|
||||
case 'Settings':
|
||||
return <GearIcon fill={color} height={size - 1} width={size - 1} />;
|
||||
case 'Downloads':
|
||||
return <DownloadsIcon fill={color} height={size - 6} width={size - 6} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: styles.themeColor.color,
|
||||
tabBarInactiveTintColor: 'gray',
|
||||
tabBarInactiveTintColor: styles.textHalfOpacity.color,
|
||||
headerShown: false,
|
||||
tabBarShowLabel: false,
|
||||
tabBarStyle: { position: 'absolute' },
|
||||
tabBarBackground: () => (
|
||||
<ColoredBlurView style={StyleSheet.absoluteFill} />
|
||||
)
|
||||
})}
|
||||
tabBarStyle: {
|
||||
backgroundColor: styles.view.backgroundColor,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab.Screen name="MusicTab" component={Music} options={{ tabBarLabel: t('music'), tabBarTestID: 'music-tab' }} />
|
||||
<Tab.Screen name="SearchTab" component={SearchStack} options={{ tabBarLabel: t('search'), tabBarTestID: 'search-tab' }} />
|
||||
<Tab.Screen name="Downloads" component={Downloads} options={{ tabBarLabel: t('downloads'), tabBarTestID: 'downloads-tab'}} />
|
||||
<Tab.Screen name="Settings" component={Settings} options={{ tabBarLabel: t('settings'), tabBarTestID: 'settings-tab' }} />
|
||||
<Tab.Screen
|
||||
name="MusicTab"
|
||||
component={Music}
|
||||
options={{
|
||||
tabBarLabel: t('music'),
|
||||
tabBarIcon: Platform.select({
|
||||
ios: {
|
||||
type: 'sfSymbol',
|
||||
name: 'music.note',
|
||||
},
|
||||
android: {
|
||||
type: 'drawableResource',
|
||||
name: 'ic_tab_music',
|
||||
}
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="SearchTab"
|
||||
component={SearchStack}
|
||||
options={{
|
||||
tabBarLabel: t('search'),
|
||||
...Platform.select({
|
||||
ios: {
|
||||
tabBarSystemItem: 'search',
|
||||
},
|
||||
android: {
|
||||
tabBarIcon: {
|
||||
type: 'drawableResource',
|
||||
name: 'ic_tab_search',
|
||||
}
|
||||
}
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Downloads"
|
||||
component={Downloads}
|
||||
options={{
|
||||
tabBarLabel: t('downloads'),
|
||||
...Platform.select({
|
||||
ios: {
|
||||
tabBarSystemItem: 'downloads',
|
||||
},
|
||||
android: {
|
||||
tabBarIcon: {
|
||||
type: 'drawableResource',
|
||||
name: 'ic_tab_downloads',
|
||||
}
|
||||
}
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Settings"
|
||||
component={Settings}
|
||||
options={{
|
||||
tabBarLabel: t('settings'),
|
||||
tabBarIcon: Platform.select({
|
||||
ios: {
|
||||
type: 'sfSymbol',
|
||||
name: 'gearshape',
|
||||
},
|
||||
android: {
|
||||
type: 'drawableResource',
|
||||
name: 'ic_tab_settings',
|
||||
}
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
</>
|
||||
);
|
||||
@@ -108,5 +154,5 @@ export default function Routes() {
|
||||
|
||||
export type NavigationProp = CompositeNavigationProp<
|
||||
StackNavigationProp<Routes>,
|
||||
BottomTabNavigationProp<Screens>
|
||||
NativeBottomTabNavigationProp<Screens>
|
||||
>;
|
||||
|
||||
+8
-9
@@ -56,15 +56,14 @@ const persistConfig: PersistConfig<Omit<AppState, '_persist'>> = {
|
||||
}
|
||||
};
|
||||
},
|
||||
// @ts-expect-error migrations are poorly typed
|
||||
4: (state: AppState) => {
|
||||
return {
|
||||
...state,
|
||||
sleepTimer: {
|
||||
date: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
// 4: (state: AppState) => {
|
||||
// return {
|
||||
// ...state,
|
||||
// sleepTimer: {
|
||||
// date: null,
|
||||
// }
|
||||
// };
|
||||
// },
|
||||
// @ts-expect-error migrations are poorly typed
|
||||
5: (state: AppState) => {
|
||||
// @ts-expect-error
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import TrackPlayer, { AddTrack } from 'react-native-track-player';
|
||||
import { useEffect } from 'react';
|
||||
import EventEmitter from 'events';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
const eventName = 'track-added';
|
||||
const addedTrackEmitter = new EventEmitter();
|
||||
|
||||
@@ -75,15 +75,20 @@ export default function usePlayTracks() {
|
||||
// Potentially shuffle all tracks
|
||||
const newTracks = shuffle ? shuffleArray(generatedTracks) : generatedTracks;
|
||||
|
||||
console.log('[usePlayTracks] Generated tracks:', newTracks.length, 'tracks');
|
||||
console.log('[usePlayTracks] First track URL:', newTracks[0]?.url);
|
||||
|
||||
// Then, we'll need to check where to add the track
|
||||
switch(method) {
|
||||
case 'add-to-end': {
|
||||
await TrackPlayer.add(newTracks);
|
||||
console.log('[usePlayTracks] Added tracks to end, queue size:', (await TrackPlayer.getQueue()).length);
|
||||
|
||||
// Then we'll skip to it and play it
|
||||
if (play) {
|
||||
await TrackPlayer.skip((await TrackPlayer.getQueue()).length - newTracks.length);
|
||||
await TrackPlayer.play();
|
||||
console.log('[usePlayTracks] Playback started');
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -105,10 +110,12 @@ export default function usePlayTracks() {
|
||||
// Depending on whether this track exists, we either add it there,
|
||||
// or at the end of the queue.
|
||||
await TrackPlayer.add(newTracks, targetTrack);
|
||||
console.log('[usePlayTracks] Added tracks after current');
|
||||
|
||||
if (play) {
|
||||
await TrackPlayer.skip(currentTrackIndex + 1);
|
||||
await TrackPlayer.play();
|
||||
console.log('[usePlayTracks] Playback started');
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -116,6 +123,7 @@ export default function usePlayTracks() {
|
||||
case 'replace': {
|
||||
// Reset the queue first
|
||||
await TrackPlayer.reset();
|
||||
console.log('[usePlayTracks] Queue reset');
|
||||
|
||||
// GUARD: Check if we need to skip to a particular index
|
||||
if (options.playIndex) {
|
||||
@@ -128,7 +136,11 @@ export default function usePlayTracks() {
|
||||
// First, we'll add the current queue and (optionally) force
|
||||
// it to start playing.
|
||||
await TrackPlayer.add(current);
|
||||
if (play) await TrackPlayer.play();
|
||||
console.log('[usePlayTracks] Added current tracks, starting at index:', options.playIndex);
|
||||
if (play) {
|
||||
await TrackPlayer.play();
|
||||
console.log('[usePlayTracks] Playback started');
|
||||
}
|
||||
|
||||
// Then, we'll insert the "previous" tracks after the queue
|
||||
// has started playing. This ensures that these tracks won't
|
||||
@@ -136,7 +148,12 @@ export default function usePlayTracks() {
|
||||
await TrackPlayer.add(before, 0);
|
||||
} else {
|
||||
await TrackPlayer.add(newTracks);
|
||||
if (play) await TrackPlayer.play();
|
||||
console.log('[usePlayTracks] Added all tracks, queue size:', (await TrackPlayer.getQueue()).length);
|
||||
if (play) {
|
||||
await TrackPlayer.play();
|
||||
const state = await TrackPlayer.getPlaybackState();
|
||||
console.log('[usePlayTracks] Playback started, state:', state);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
+4
-4
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"extends": "@react-native/typescript-config/tsconfig.json",
|
||||
"extends": "@react-native/typescript-config",
|
||||
"compilerOptions": {
|
||||
"types": [],
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"*"
|
||||
],
|
||||
},
|
||||
"types": [
|
||||
"@types/node"
|
||||
],
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["**/node_modules", "**/Pods"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user