feat: react native 0.83, liquid glass

This commit is contained in:
Lei Nelissen
2026-01-25 19:04:55 +01:00
parent 91a58109d9
commit b75fb680a6
38 changed files with 4413 additions and 2553 deletions
+2 -67
View File
@@ -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>
+4 -8
View File
@@ -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()
+6 -1
View File
@@ -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
Binary file not shown.
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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.
+7 -2
View File
@@ -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
View File
@@ -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',
]
};
+5 -16
View File
@@ -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)";
+4
View File
@@ -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>
-1
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+40 -38
View File
@@ -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?) {
+690
View File
@@ -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)
}
+1388 -1125
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -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
+6 -5
View File
@@ -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'
}
/>
+3 -3
View File
@@ -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);
+27 -19
View File
@@ -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,
};
}
+2 -1
View File
@@ -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}
/>
+29 -36
View File
@@ -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>
);
}
+3 -4
View File
@@ -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'}
/>
</>
+2 -3
View File
@@ -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'}
/>
</>
+21 -30
View File
@@ -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>
)}
/>
);
};
+2 -1
View File
@@ -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;
`;
+2 -9
View File
@@ -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>
);
+88 -85
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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();
+19 -2
View File
@@ -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
View File
@@ -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"]
}