mirror of
https://github.com/leinelissen/jellyfin-audio-player.git
synced 2026-09-02 21:03:11 +03:00
feat: CarPlay and Android Auto (#334)
* feat: base carplay * fix: android auto * fix: directly reference redux store * feat: icons instead of buttons * fix: implement viewfactory so carplay app can start without phone app * fix: mvoe react-native-auto-play to stable version
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<automotiveApp>
|
||||
<uses name="template"/>
|
||||
</automotiveApp>
|
||||
@@ -7,9 +7,11 @@ import PlaybackService from './src/utility/PlaybackService';
|
||||
import { setupSentry } from '@/utility/Sentry';
|
||||
import { enableScreens } from 'react-native-screens';
|
||||
import { patchTrackPlayer } from '@/utility/AddedTrackEvents';
|
||||
import { registerAutoPlay } from './src/screens/carplay/register';
|
||||
|
||||
setupSentry();
|
||||
enableScreens();
|
||||
patchTrackPlayer();
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
TrackPlayer.registerPlaybackService(() => PlaybackService);
|
||||
TrackPlayer.registerPlaybackService(() => PlaybackService);
|
||||
registerAutoPlay();
|
||||
@@ -1,48 +0,0 @@
|
||||
import UIKit
|
||||
import React
|
||||
import React_RCTAppDelegate
|
||||
import ReactAppDependencyProvider
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
var reactNativeDelegate: ReactNativeDelegate?
|
||||
var reactNativeFactory: RCTReactNativeFactory?
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
let delegate = ReactNativeDelegate()
|
||||
let factory = RCTReactNativeFactory(delegate: delegate)
|
||||
delegate.dependencyProvider = RCTAppDependencyProvider()
|
||||
|
||||
reactNativeDelegate = delegate
|
||||
reactNativeFactory = factory
|
||||
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
|
||||
factory.startReactNative(
|
||||
withModuleName: "Fintunes",
|
||||
in: window,
|
||||
launchOptions: launchOptions
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
|
||||
override func sourceURL(for bridge: RCTBridge) -> URL? {
|
||||
self.bundleURL()
|
||||
}
|
||||
|
||||
override func bundleURL() -> URL? {
|
||||
#if DEBUG
|
||||
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
|
||||
#else
|
||||
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
@@ -13,10 +13,21 @@
|
||||
4C04FC6E055249ABB204D3BC /* Inter-VariableFont_slnt,wght.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4B4A0465FF364579B28CF5D7 /* Inter-VariableFont_slnt,wght.ttf */; };
|
||||
AB393FCA2857CC8400773469 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB393FC92857CC8400773469 /* SnapshotHelper.swift */; };
|
||||
AB4A8DFE2857C8DA005A1ED0 /* FintunesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4A8DFD2857C8DA005A1ED0 /* FintunesUITests.swift */; };
|
||||
AB7AA5F92DC8E5D600578CAC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7AA5F82DC8E5D600578CAC /* AppDelegate.swift */; };
|
||||
ABD245FC2F3605DE00135530 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD245FB2F3605DE00135530 /* AppDelegate.swift */; };
|
||||
ABD245FE2F3605F500135530 /* PhoneSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD245FD2F3605F500135530 /* PhoneSceneDelegate.swift */; };
|
||||
FA01635F2599C28FC19F2EC3 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3896494129CBC30258D9BB1C /* PrivacyInfo.xcprivacy */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
AB87931D2F2CE1530093DF3D /* 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>"; };
|
||||
@@ -30,7 +41,10 @@
|
||||
AB393FC92857CC8400773469 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = "<group>"; };
|
||||
AB4A8DFB2857C8DA005A1ED0 /* FintunesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FintunesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AB4A8DFD2857C8DA005A1ED0 /* FintunesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FintunesUITests.swift; sourceTree = "<group>"; };
|
||||
AB7AA5F82DC8E5D600578CAC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
AB78871B2F35FDA400C6CF3D /* Fintunes-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Fintunes-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
AB8793222F2D399F0093DF3D /* Fintunes.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Fintunes.entitlements; path = Fintunes/Fintunes.entitlements; sourceTree = "<group>"; };
|
||||
ABD245FB2F3605DE00135530 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Fintunes/AppDelegate.swift; sourceTree = "<group>"; };
|
||||
ABD245FD2F3605F500135530 /* PhoneSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneSceneDelegate.swift; sourceTree = "<group>"; };
|
||||
E22EC545298DA9F9017776C0 /* libPods-Fintunes.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Fintunes.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; };
|
||||
@@ -58,11 +72,14 @@
|
||||
13B07FAE1A68108700A75B9A /* Fintunes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AB7AA5F82DC8E5D600578CAC /* AppDelegate.swift */,
|
||||
ABD245FD2F3605F500135530 /* PhoneSceneDelegate.swift */,
|
||||
ABD245FB2F3605DE00135530 /* AppDelegate.swift */,
|
||||
AB8793222F2D399F0093DF3D /* Fintunes.entitlements */,
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
|
||||
3896494129CBC30258D9BB1C /* PrivacyInfo.xcprivacy */,
|
||||
AB78871B2F35FDA400C6CF3D /* Fintunes-Bridging-Header.h */,
|
||||
);
|
||||
name = Fintunes;
|
||||
sourceTree = "<group>";
|
||||
@@ -189,7 +206,7 @@
|
||||
TargetAttributes = {
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
DevelopmentTeam = HD2D35G9Y4;
|
||||
LastSwiftMigration = 1210;
|
||||
LastSwiftMigration = 2620;
|
||||
ProvisioningStyle = Manual;
|
||||
};
|
||||
AB4A8DFA2857C8DA005A1ED0 = {
|
||||
@@ -309,6 +326,7 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactNativeAutoPlay/ReactNativeAutoPlay.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
||||
@@ -321,6 +339,7 @@
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReactNativeAutoPlay.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Sentry.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
||||
@@ -338,7 +357,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
AB7AA5F92DC8E5D600578CAC /* AppDelegate.swift in Sources */,
|
||||
ABD245FE2F3605F500135530 /* PhoneSceneDelegate.swift in Sources */,
|
||||
ABD245FC2F3605DE00135530 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -357,6 +377,7 @@
|
||||
AB4A8E022857C8DA005A1ED0 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 13B07F861A680F5B00A75B9A /* Fintunes */;
|
||||
targetProxy = AB87931D2F2CE1530093DF3D /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
@@ -379,6 +400,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Fintunes/Fintunes.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
@@ -405,7 +427,8 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.jellyfinaudioplayer;
|
||||
PRODUCT_NAME = Fintunes;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore 1754345870";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore 17";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Fintunes-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -418,6 +441,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Fintunes/Fintunes.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
@@ -440,7 +464,8 @@
|
||||
PRODUCT_NAME = Fintunes;
|
||||
PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore 1754345870";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore 17";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Fintunes-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
|
||||
@@ -31,6 +31,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@objc func getRootViewForAutoplay(
|
||||
moduleName: String,
|
||||
initialProperties: [String: Any]?
|
||||
) -> UIView? {
|
||||
if RCTIsNewArchEnabled() {
|
||||
if let factory = reactNativeFactory?.rootViewFactory as? RCTRootViewFactory {
|
||||
return factory.view(
|
||||
withModuleName: moduleName,
|
||||
initialProperties: initialProperties,
|
||||
launchOptions: nil
|
||||
)
|
||||
}
|
||||
|
||||
return reactNativeFactory?.rootViewFactory.view(
|
||||
withModuleName: moduleName,
|
||||
initialProperties: initialProperties
|
||||
)
|
||||
}
|
||||
|
||||
if let rootView = window?.rootViewController?.view as? RCTRootView {
|
||||
return RCTRootView(
|
||||
bridge: rootView.bridge,
|
||||
moduleName: moduleName,
|
||||
initialProperties: initialProperties
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-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>com.apple.developer.carplay-audio</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -64,5 +64,35 @@
|
||||
<true/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>CPTemplateApplicationSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>CPTemplateApplicationScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>CarPlayHeadUnit</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>HeadUnitSceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Phone</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>Fintunes.PhoneSceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import UIKit
|
||||
import React_RCTAppDelegate
|
||||
|
||||
class PhoneSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func scene(
|
||||
_ scene: UIScene,
|
||||
willConnectTo session: UISceneSession,
|
||||
options connectionOptions: UIScene.ConnectionOptions
|
||||
) {
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
|
||||
// Get the AppDelegate to access the React Native setup
|
||||
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
|
||||
|
||||
// Create the window for this scene
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
|
||||
// Start React Native if not already started
|
||||
if let factory = appDelegate.reactNativeFactory {
|
||||
factory.startReactNative(
|
||||
withModuleName: "Fintunes",
|
||||
in: window,
|
||||
launchOptions: nil
|
||||
)
|
||||
}
|
||||
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import UIKit
|
||||
import React_RCTAppDelegate
|
||||
|
||||
class PhoneSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func scene(
|
||||
_ scene: UIScene,
|
||||
willConnectTo session: UISceneSession,
|
||||
options connectionOptions: UIScene.ConnectionOptions
|
||||
) {
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
|
||||
// Get the AppDelegate to access the React Native setup
|
||||
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
|
||||
|
||||
// Create the window for this scene
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
|
||||
// Start React Native if not already started
|
||||
if let factory = appDelegate.reactNativeFactory {
|
||||
factory.startReactNative(
|
||||
withModuleName: "Fintunes",
|
||||
in: window,
|
||||
launchOptions: nil
|
||||
)
|
||||
}
|
||||
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
+103
-10
@@ -25,6 +25,35 @@ PODS:
|
||||
- libwebp/sharpyuv (1.3.2)
|
||||
- libwebp/webp (1.3.2):
|
||||
- libwebp/sharpyuv
|
||||
- NitroModules (0.33.7):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
- fmt
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly
|
||||
- RCT-Folly/Fabric
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-callinvoker
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-renderercss
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RCT-Folly (2024.11.18.00):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
@@ -1939,8 +1968,34 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- react-native-netinfo (11.4.1):
|
||||
- react-native-netinfo (11.5.2):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
- fmt
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly
|
||||
- RCT-Folly/Fabric
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-renderercss
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- react-native-safe-area-context (5.6.2):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
@@ -2646,6 +2701,36 @@ PODS:
|
||||
- React-perflogger (= 0.83.1)
|
||||
- React-utils (= 0.83.1)
|
||||
- SocketRocket
|
||||
- ReactNativeAutoPlay (0.2.2):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
- fmt
|
||||
- glog
|
||||
- hermes-engine
|
||||
- NitroModules
|
||||
- RCT-Folly
|
||||
- RCT-Folly/Fabric
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-callinvoker
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-renderercss
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- ReactNativeBlur (4.5.3):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
@@ -2914,7 +2999,7 @@ PODS:
|
||||
- RNWorklets
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RNScreens (4.20.0):
|
||||
- RNScreens (4.22.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -2941,10 +3026,10 @@ PODS:
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNScreens/common (= 4.20.0)
|
||||
- RNScreens/common (= 4.22.0)
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RNScreens/common (4.20.0):
|
||||
- RNScreens/common (4.22.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -3003,7 +3088,7 @@ PODS:
|
||||
- Sentry/HybridSDK (= 8.58.0)
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RNSVG (15.15.1):
|
||||
- RNSVG (15.15.2):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -3029,10 +3114,10 @@ PODS:
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNSVG/common (= 15.15.1)
|
||||
- RNSVG/common (= 15.15.2)
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RNSVG/common (15.15.1):
|
||||
- RNSVG/common (15.15.2):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -3174,6 +3259,7 @@ DEPENDENCIES:
|
||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
||||
- libwebp (= 1.3.2)
|
||||
- NitroModules (from `../node_modules/react-native-nitro-modules`)
|
||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
|
||||
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
|
||||
@@ -3251,6 +3337,7 @@ DEPENDENCIES:
|
||||
- ReactAppDependencyProvider (from `build/generated/ios/ReactAppDependencyProvider`)
|
||||
- ReactCodegen (from `build/generated/ios/ReactCodegen`)
|
||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
||||
- "ReactNativeAutoPlay (from `../node_modules/@iternio/react-native-auto-play`)"
|
||||
- "ReactNativeBlur (from `../node_modules/@sbaiahmed1/react-native-blur`)"
|
||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
||||
@@ -3295,6 +3382,8 @@ EXTERNAL SOURCES:
|
||||
hermes-engine:
|
||||
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
||||
:tag: hermes-v0.14.0
|
||||
NitroModules:
|
||||
:path: "../node_modules/react-native-nitro-modules"
|
||||
RCT-Folly:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
||||
RCTDeprecation:
|
||||
@@ -3447,6 +3536,8 @@ EXTERNAL SOURCES:
|
||||
:path: build/generated/ios/ReactCodegen
|
||||
ReactCommon:
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
ReactNativeAutoPlay:
|
||||
:path: "../node_modules/@iternio/react-native-auto-play"
|
||||
ReactNativeBlur:
|
||||
:path: "../node_modules/@sbaiahmed1/react-native-blur"
|
||||
RNCAsyncStorage:
|
||||
@@ -3485,6 +3576,7 @@ SPEC CHECKSUMS:
|
||||
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
|
||||
NitroModules: e8ec707a245a85cf1eb4289f91a9372f9590a897
|
||||
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
|
||||
RCTDeprecation: a41bbdd9af30bf2e5715796b313e44ec43eefff1
|
||||
RCTRequired: 7be34aabb0b77c3cefe644528df0fa0afad4e4d0
|
||||
@@ -3523,7 +3615,7 @@ SPEC CHECKSUMS:
|
||||
React-Mapbuffer: 7b72a669e94662359dad4f42b5af005eb24b4e83
|
||||
React-microtasksnativemodule: cdc02da075f2857803ed63f24f5f72fc40e094c0
|
||||
react-native-accessibility-settings: 8803c0bd7e1724269bf0ac5064be51773c05df9f
|
||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||
react-native-netinfo: 57447b5a45c98808f8eae292cf641f3d91d13830
|
||||
react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460
|
||||
react-native-skia: b60d0b9d9aea330baef956b7f015d0af2b42076d
|
||||
react-native-track-player: dd56f9948e03756a217c75e0a61aa04aab65b5a1
|
||||
@@ -3561,6 +3653,7 @@ SPEC CHECKSUMS:
|
||||
ReactAppDependencyProvider: 0eb286cc274abb059ee601b862ebddac2e681d01
|
||||
ReactCodegen: 3d48510bcef445f6403c0004047d4d9cbb915435
|
||||
ReactCommon: ac934cb340aee91282ecd6f273a26d24d4c55cae
|
||||
ReactNativeAutoPlay: c7130c09e5c07962d26fcee8dfa53ebc78a40eb4
|
||||
ReactNativeBlur: 500e90ffcc66e89a3a27a6f3a50a7b765ac1f145
|
||||
RNCAsyncStorage: 29f0230e1a25f36c20b05f65e2eb8958d6526e82
|
||||
RNDateTimePicker: 97a86d7c8b51f2d51f694a9dddfe5996dfd9a407
|
||||
@@ -3569,9 +3662,9 @@ SPEC CHECKSUMS:
|
||||
RNGestureHandler: cd4be101cfa17ea6bbd438710caa02e286a84381
|
||||
RNLocalize: 23e6689ec46c6cba49eb8cde959e30e49160a35b
|
||||
RNReanimated: 292cd58688552a22b3fc1cefcfbc49b336dfed68
|
||||
RNScreens: 714e10b6b554f7dc7ad9f78dcf36dc8e3fc73415
|
||||
RNScreens: c77cc7a5d1fbc23f0725b54e96eec81118453382
|
||||
RNSentry: a4c81a5c5bcd58d765126726432348e60cd7183d
|
||||
RNSVG: ba3c3bc3cf413f32d1a7012274fe503c6fc1eadd
|
||||
RNSVG: a4ea97e14b7a2221d85091a6b3128f807445a7ec
|
||||
RNWorklets: 01efdd402d236a13651ea5ea5437ca85a44e7afa
|
||||
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
|
||||
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
|
||||
|
||||
+3
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "fintunes",
|
||||
"version": "2.5.0",
|
||||
"main": "src/index.js",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@d11/react-native-fast-image": "8.13.0",
|
||||
"@iternio/react-native-auto-play": "0.2.2",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@react-native-community/datetimepicker": "^8.6.0",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
@@ -41,6 +42,7 @@
|
||||
"react-native-gesture-handler": "^2.30.0",
|
||||
"react-native-localize": "3.6.1",
|
||||
"react-native-modal-datetime-picker": "18.0.0",
|
||||
"react-native-nitro-modules": "^0.33.2",
|
||||
"react-native-reanimated": "^4.2.1",
|
||||
"react-native-safe-area-context": "^5.6.2",
|
||||
"react-native-screens": "^4.20.0",
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
diff --git a/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoScreen.kt b/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoScreen.kt
|
||||
index 19eb2de81576c4c9df9f0353f9dc7aef3f5fd1b5..8b9db9044e40c405fb8d166829eebe8391357150 100644
|
||||
--- a/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoScreen.kt
|
||||
+++ b/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoScreen.kt
|
||||
@@ -124,7 +124,14 @@ class AndroidAutoScreen(
|
||||
}
|
||||
|
||||
fun getScreenManager(): ScreenManager? {
|
||||
- return screens[AndroidAutoSession.ROOT_SESSION]?.screenManager
|
||||
+ // First try to get the ROOT_SESSION screen
|
||||
+ val rootScreen = screens[AndroidAutoSession.ROOT_SESSION]
|
||||
+ if (rootScreen != null) {
|
||||
+ return rootScreen.screenManager
|
||||
+ }
|
||||
+ // If ROOT_SESSION doesn't exist, get any screen's screenManager
|
||||
+ // (after setRootTemplate, the root might be under a different key)
|
||||
+ return screens.values.firstOrNull()?.screenManager
|
||||
}
|
||||
|
||||
fun invalidateScreens() {
|
||||
diff --git a/ios/extensions/CarPlayTemplateExtensions.swift b/ios/extensions/CarPlayTemplateExtensions.swift
|
||||
index e15d37c34acc692ead9a39e962242e46e3aea525..73ac36aeee867dca7e8c8a6bbe9f79cc254a5e30 100644
|
||||
--- a/ios/extensions/CarPlayTemplateExtensions.swift
|
||||
+++ b/ios/extensions/CarPlayTemplateExtensions.swift
|
||||
@@ -18,8 +18,8 @@ extension CPTemplate {
|
||||
self.init()
|
||||
initTemplate(template: self, id: id)
|
||||
}
|
||||
- @objc var id: String {
|
||||
- return (self.userInfo as? [String: Any])?["id"] as! String
|
||||
+ @objc var id: String? {
|
||||
+ return (self.userInfo as? [String: Any])?["id"] as? String
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/ios/scenes/AutoPlayInterfaceController.swift b/ios/scenes/AutoPlayInterfaceController.swift
|
||||
index 5008b1ed3790e2d2798317aaeb4df3e65378dbce..927288c8a13bcdbc370cb48c3868c7aa57d89e33 100644
|
||||
--- a/ios/scenes/AutoPlayInterfaceController.swift
|
||||
+++ b/ios/scenes/AutoPlayInterfaceController.swift
|
||||
@@ -90,7 +90,9 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
||||
var templateIds: [String] = []
|
||||
|
||||
templates.forEach { template in
|
||||
- let templateId = template.id
|
||||
+ guard let templateId = template.id else {
|
||||
+ return
|
||||
+ }
|
||||
|
||||
if templateId == rootTemplateId {
|
||||
return
|
||||
@@ -117,12 +119,12 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
||||
{
|
||||
guard
|
||||
let template = interfaceController.templates.first(
|
||||
- where: {
|
||||
- templateId == $0.id
|
||||
+ where: { template in
|
||||
+ template.id == templateId
|
||||
})
|
||||
else { return [] }
|
||||
|
||||
- var templateIds: [String] = interfaceController.templates.map {
|
||||
+ var templateIds: [String] = interfaceController.templates.compactMap {
|
||||
template in template.id
|
||||
}
|
||||
|
||||
@@ -173,7 +175,10 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
||||
_ aTemplate: CPTemplate,
|
||||
animated: Bool
|
||||
) {
|
||||
- let templateId = aTemplate.id
|
||||
+ guard let templateId = aTemplate.id else {
|
||||
+ // System template without ID (e.g., CPNowPlayingTemplate)
|
||||
+ return
|
||||
+ }
|
||||
|
||||
try? RootModule.withAutoPlayTemplate(templateId: templateId) {
|
||||
(template: AutoPlayTemplate) in
|
||||
@@ -187,7 +192,10 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
||||
_ aTemplate: CPTemplate,
|
||||
animated: Bool
|
||||
) {
|
||||
- let templateId = aTemplate.id
|
||||
+ guard let templateId = aTemplate.id else {
|
||||
+ // System template without ID (e.g., CPNowPlayingTemplate)
|
||||
+ return
|
||||
+ }
|
||||
|
||||
if rootTemplateId == templateId {
|
||||
// this makes sure we purge outdated CPSearchTemplate since that one can be popped on with a CarPlay native button we can not intercept
|
||||
@@ -210,7 +218,10 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
||||
_ aTemplate: CPTemplate,
|
||||
animated: Bool
|
||||
) {
|
||||
- let templateId = aTemplate.id
|
||||
+ guard let templateId = aTemplate.id else {
|
||||
+ // System template without ID (e.g., CPNowPlayingTemplate)
|
||||
+ return
|
||||
+ }
|
||||
|
||||
try? RootModule.withAutoPlayTemplate(
|
||||
templateId: templateId,
|
||||
@@ -228,7 +239,10 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
||||
_ aTemplate: CPTemplate,
|
||||
animated: Bool
|
||||
) {
|
||||
- let templateId = aTemplate.id
|
||||
+ guard let templateId = aTemplate.id else {
|
||||
+ // System template without ID (e.g., CPNowPlayingTemplate)
|
||||
+ return
|
||||
+ }
|
||||
|
||||
try? RootModule.withAutoPlayTemplate(
|
||||
templateId: templateId,
|
||||
Generated
+307
-123
File diff suppressed because it is too large
Load Diff
@@ -4,3 +4,4 @@ onlyBuiltDependencies:
|
||||
patchedDependencies:
|
||||
react-native-webview: patches/react-native-webview.patch
|
||||
react-native-track-player: patches/react-native-track-player.patch
|
||||
'@iternio/react-native-auto-play': patches/@iternio__react-native-auto-play.patch
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"kbps": "kbps",
|
||||
"transcoded": "Транскодиран",
|
||||
"khz": "kHz",
|
||||
"direct-play": "Direct play"
|
||||
"direct-play": "Direct play",
|
||||
"browse": "Преглед",
|
||||
"play": "Възпроизвеждане",
|
||||
"shuffle": "Разбъркай",
|
||||
"tracks": "песни",
|
||||
"unknown-artist": "Неизвестен изпълнител",
|
||||
"unknown-track": "Неизвестна песен"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"khz": "kHz",
|
||||
"direct-play": "Reproducció directa",
|
||||
"transcoded": "Transcodificació",
|
||||
"kbps": "kbps"
|
||||
"kbps": "kbps",
|
||||
"browse": "Explorar",
|
||||
"play": "Reproduir",
|
||||
"shuffle": "Aleatori",
|
||||
"tracks": "pistes",
|
||||
"unknown-artist": "Artista desconegut",
|
||||
"unknown-track": "Pista desconeguda"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Přímé přehrávání",
|
||||
"transcoded": "Překódované",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps"
|
||||
"kbps": "kbps",
|
||||
"browse": "Procházet",
|
||||
"play": "Přehrát",
|
||||
"shuffle": "Zamíchat",
|
||||
"tracks": "skladeb",
|
||||
"unknown-artist": "Neznámý interpret",
|
||||
"unknown-track": "Neznámá skladba"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Direkte spil",
|
||||
"transcoded": "Transcode",
|
||||
"kbps": "kbps",
|
||||
"khz": "kHz"
|
||||
"khz": "kHz",
|
||||
"browse": "Gennemse",
|
||||
"play": "Afspil",
|
||||
"shuffle": "Bland",
|
||||
"tracks": "numre",
|
||||
"unknown-artist": "Ukendt kunstner",
|
||||
"unknown-track": "Ukendt nummer"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Direkte Wiedergabe",
|
||||
"transcoded": "Transkodiert",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps"
|
||||
"kbps": "kbps",
|
||||
"browse": "Durchsuchen",
|
||||
"play": "Abspielen",
|
||||
"shuffle": "Zufällig",
|
||||
"tracks": "Titel",
|
||||
"unknown-artist": "Unbekannter Künstler",
|
||||
"unknown-track": "Unbekannter Titel"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Direct play",
|
||||
"transcoded": "Transcoded",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps"
|
||||
"kbps": "kbps",
|
||||
"browse": "Browse",
|
||||
"play": "Play",
|
||||
"shuffle": "Shuffle",
|
||||
"tracks": "tracks",
|
||||
"unknown-artist": "Unknown Artist",
|
||||
"unknown-track": "Unknown Track"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Reproducción directa",
|
||||
"transcoded": "Transcodificar",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps"
|
||||
"kbps": "kbps",
|
||||
"browse": "Explorar",
|
||||
"play": "Reproducir",
|
||||
"shuffle": "Aleatorio",
|
||||
"tracks": "pistas",
|
||||
"unknown-artist": "Artista desconocido",
|
||||
"unknown-track": "Pista desconocida"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"similar-albums": "Samanlaisia albumeja",
|
||||
"playback-reporting": "Toiston raportointi",
|
||||
"playback-reporting-description": "Toiston raportoinnin avulla kaikki toistotapahtumat palautetaan palvelimellesi. Näin voit seurata eniten kuunneltuja kappaleita, esimerkiksi pluginien, kuten ListenBrainz tai LastFM, avulla.",
|
||||
"color-scheme-description": "Fintunes noudattaa oletuksena käyttöjärjestelmän ulkoasua. Voit kuitenkin ohittaa tämän varmistaaksesi, että Fintunes on aina tummassa tai vaaleassa tilassa."
|
||||
"color-scheme-description": "Fintunes noudattaa oletuksena käyttöjärjestelmän ulkoasua. Voit kuitenkin ohittaa tämän varmistaaksesi, että Fintunes on aina tummassa tai vaaleassa tilassa.",
|
||||
"browse": "Selaa",
|
||||
"play": "Toista",
|
||||
"shuffle": "Sekoita",
|
||||
"tracks": "kappaletta",
|
||||
"unknown-artist": "Tuntematon artisti",
|
||||
"unknown-track": "Tuntematon kappale"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Lecture directe",
|
||||
"transcoded": "Converti",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps"
|
||||
"kbps": "kbps",
|
||||
"browse": "Parcourir",
|
||||
"play": "Lire",
|
||||
"shuffle": "Aléatoire",
|
||||
"tracks": "pistes",
|
||||
"unknown-artist": "Artiste inconnu",
|
||||
"unknown-track": "Piste inconnue"
|
||||
}
|
||||
|
||||
@@ -80,5 +80,11 @@
|
||||
"where-is-data-stored": "Hol van tárolva a gyűjtött adat?",
|
||||
"where-is-data-stored-description": "A Sentry rendszert saját szervereken futtatjuk, a mi saját infrastruktúránkon. A szerverekhez, adatbázisokhoz és naplófájlokhoz csak mi férünk hozzá, legkevésbé a Sentry munkatársai. Az infrastruktúra az Európai Unióban van futtatva.",
|
||||
"enable-error-reporting": "Engedélyezni szeretnéd a hibajelentést?",
|
||||
"enable-error-reporting-description": "Segít nekünk fejleszteni az alkalmazásélményt a hibák és összeomlások jelentésével."
|
||||
"enable-error-reporting-description": "Segít nekünk fejleszteni az alkalmazásélményt a hibák és összeomlások jelentésével.",
|
||||
"browse": "Böngészés",
|
||||
"play": "Lejátszás",
|
||||
"shuffle": "Keverés",
|
||||
"tracks": "számok",
|
||||
"unknown-artist": "Ismeretlen előadó",
|
||||
"unknown-track": "Ismeretlen szám"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"privacy-policy": "Kebijakan Privasi",
|
||||
"sleep-timer": "Pengatur waktu tidur",
|
||||
"delete": "Menghapus",
|
||||
"cancel": "Membatalkan"
|
||||
"cancel": "Membatalkan",
|
||||
"browse": "Jelajahi",
|
||||
"play": "Mainkan",
|
||||
"shuffle": "Acak",
|
||||
"tracks": "lagu",
|
||||
"unknown-artist": "Artis Tidak Dikenal",
|
||||
"unknown-track": "Lagu Tidak Dikenal"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Gioco diretto",
|
||||
"transcoded": "Transcodifica",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps"
|
||||
"kbps": "kbps",
|
||||
"browse": "Sfoglia",
|
||||
"play": "Riproduci",
|
||||
"shuffle": "Casuale",
|
||||
"tracks": "tracce",
|
||||
"unknown-artist": "Artista sconosciuto",
|
||||
"unknown-track": "Traccia sconosciuta"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "ダイレクトプレイ",
|
||||
"transcoded": "トランスコード",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps"
|
||||
"kbps": "kbps",
|
||||
"browse": "ブラウズ",
|
||||
"play": "再生",
|
||||
"shuffle": "シャッフル",
|
||||
"tracks": "トラック",
|
||||
"unknown-artist": "不明なアーティスト",
|
||||
"unknown-track": "不明なトラック"
|
||||
}
|
||||
|
||||
@@ -82,5 +82,11 @@
|
||||
"lyrics": "Sangtekster",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps",
|
||||
"transcoded": "Transkodet"
|
||||
"transcoded": "Transkodet",
|
||||
"browse": "Bla gjennom",
|
||||
"play": "Spill",
|
||||
"shuffle": "Stokk",
|
||||
"tracks": "spor",
|
||||
"unknown-artist": "Ukjent artist",
|
||||
"unknown-track": "Ukjent spor"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"transcoded": "Getranscodeerd",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps",
|
||||
"lyrics": "Songtekst"
|
||||
"lyrics": "Songtekst",
|
||||
"browse": "Bladeren",
|
||||
"play": "Afspelen",
|
||||
"shuffle": "Willekeurig",
|
||||
"tracks": "nummers",
|
||||
"unknown-artist": "Onbekende artiest",
|
||||
"unknown-track": "Onbekend nummer"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Dyrektywa",
|
||||
"transcoded": "Transcode",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps"
|
||||
"kbps": "kbps",
|
||||
"browse": "Przeglądaj",
|
||||
"play": "Odtwórz",
|
||||
"shuffle": "Losowo",
|
||||
"tracks": "utworów",
|
||||
"unknown-artist": "Nieznany artysta",
|
||||
"unknown-track": "Nieznany utwór"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Reprodução Direta",
|
||||
"transcoded": "Transcodificado",
|
||||
"khz": "kHz",
|
||||
"retry-failed-downloads": "Tentar Transferir Novamente"
|
||||
"retry-failed-downloads": "Tentar Transferir Novamente",
|
||||
"browse": "Explorar",
|
||||
"play": "Reproduzir",
|
||||
"shuffle": "Aleatório",
|
||||
"tracks": "faixas",
|
||||
"unknown-artist": "Artista Desconhecido",
|
||||
"unknown-track": "Faixa Desconhecida"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Reprodução direta",
|
||||
"transcoded": "Transcodificado",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps"
|
||||
"kbps": "kbps",
|
||||
"browse": "Navegar",
|
||||
"play": "Reproduzir",
|
||||
"shuffle": "Aleatório",
|
||||
"tracks": "faixas",
|
||||
"unknown-artist": "Artista Desconhecido",
|
||||
"unknown-track": "Faixa Desconhecida"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Прямая игра",
|
||||
"transcoded": "Транскод",
|
||||
"khz": "кГц",
|
||||
"kbps": "кбит/с"
|
||||
"kbps": "кбит/с",
|
||||
"browse": "Обзор",
|
||||
"play": "Воспроизвести",
|
||||
"shuffle": "Перемешать",
|
||||
"tracks": "треков",
|
||||
"unknown-artist": "Неизвестный исполнитель",
|
||||
"unknown-track": "Неизвестный трек"
|
||||
}
|
||||
|
||||
@@ -81,5 +81,11 @@
|
||||
"disc": "Disk",
|
||||
"lyrics": "Besedilo",
|
||||
"khz": "kHz",
|
||||
"transcoded": "Transcode"
|
||||
"transcoded": "Transcode",
|
||||
"browse": "Prebrskaj",
|
||||
"play": "Predvajaj",
|
||||
"shuffle": "Naključno",
|
||||
"tracks": "skladbe",
|
||||
"unknown-artist": "Neznan izvajalec",
|
||||
"unknown-track": "Neznana skladba"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Direkt spel",
|
||||
"transcoded": "Transkodning",
|
||||
"khz": "kHz",
|
||||
"kbps": "kb/s"
|
||||
"kbps": "kb/s",
|
||||
"browse": "Bläddra",
|
||||
"play": "Spela",
|
||||
"shuffle": "Blanda",
|
||||
"tracks": "spår",
|
||||
"unknown-artist": "Okänd artist",
|
||||
"unknown-track": "Okänt spår"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"color-scheme-description": "இயல்பாக, உங்கள் இயக்க முறைமையின் வண்ணத் திட்டத்தை FINTUNES பின்பற்றும். இருப்பினும் நீங்கள் இதை மேலெழுத தேர்வு செய்யலாம் fintunes எப்போதும் இருண்ட பயன்முறையில் அல்லது ஒளி பயன்முறையில் இருப்பதை உறுதிப்படுத்தலாம்.",
|
||||
"direct-play": "நேரடி நாடகம்",
|
||||
"transcoded": "டிரான்ச்கோடட்",
|
||||
"kbps": "கே.பி.பி.எச்"
|
||||
"kbps": "கே.பி.பி.எச்",
|
||||
"browse": "உலாவு",
|
||||
"play": "இயக்கு",
|
||||
"shuffle": "சீரற்றது",
|
||||
"tracks": "பாடல்கள்",
|
||||
"unknown-artist": "தெரியாத கலைஞர்",
|
||||
"unknown-track": "தெரியாத பாடல்"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "Пряме відтворення",
|
||||
"transcoded": "Перекодований",
|
||||
"khz": "кГц",
|
||||
"kbps": "кбіт/с"
|
||||
"kbps": "кбіт/с",
|
||||
"browse": "Огляд",
|
||||
"play": "Відтворити",
|
||||
"shuffle": "Перемішати",
|
||||
"tracks": "треків",
|
||||
"unknown-artist": "Невідомий виконавець",
|
||||
"unknown-track": "Невідомий трек"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"direct-play": "直接播放",
|
||||
"transcoded": "转码",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps"
|
||||
"kbps": "kbps",
|
||||
"browse": "浏览",
|
||||
"play": "播放",
|
||||
"shuffle": "随机播放",
|
||||
"tracks": "曲目",
|
||||
"unknown-artist": "未知艺术家",
|
||||
"unknown-track": "未知曲目"
|
||||
}
|
||||
|
||||
@@ -83,5 +83,11 @@
|
||||
"why-use-tracking": "為什麼要使用跟蹤?",
|
||||
"why-use-tracking-description": "跟蹤透過報告奇怪的邊緣案例和疏忽來幫助加快此應用程式的開發。 這有助於使應用程式更加穩定和強大,從而改善每個人的應用程式體驗。",
|
||||
"what-data-is-gathered": "收集了哪些資料?",
|
||||
"enable-error-reporting-description": "這透過向我們傳送崩潰和錯誤報告來幫助改善應用程式體驗。"
|
||||
"enable-error-reporting-description": "這透過向我們傳送崩潰和錯誤報告來幫助改善應用程式體驗。",
|
||||
"browse": "瀏覽",
|
||||
"play": "播放",
|
||||
"shuffle": "隨機播放",
|
||||
"tracks": "曲目",
|
||||
"unknown-artist": "未知藝術家",
|
||||
"unknown-track": "未知曲目"
|
||||
}
|
||||
|
||||
@@ -83,3 +83,9 @@ export type LocaleKeys = 'play-next'
|
||||
| 'transcoded'
|
||||
| 'khz'
|
||||
| 'kbps'
|
||||
| 'browse'
|
||||
| 'play'
|
||||
| 'shuffle'
|
||||
| 'tracks'
|
||||
| 'unknown-artist'
|
||||
| 'unknown-track'
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { HybridAutoPlay } from '@iternio/react-native-auto-play';
|
||||
import { createBrowseMenu } from './templates/BrowseMenu';
|
||||
|
||||
/**
|
||||
* Registers CarPlay AutoPlay listeners and sets up the root browse menu
|
||||
* template when the device is connected to a CarPlay-enabled vehicle.
|
||||
*/
|
||||
export function registerAutoPlay(): void {
|
||||
// Listen for CarPlay connection events
|
||||
HybridAutoPlay.addListener('didConnect', async () => {
|
||||
console.log('[AutoPlay] Connected');
|
||||
|
||||
try {
|
||||
// Set up and display the browse menu as the root template
|
||||
const browseTemplate = createBrowseMenu();
|
||||
await browseTemplate.setRootTemplate();
|
||||
|
||||
console.log('[AutoPlay] Root template set successfully');
|
||||
} catch (error) {
|
||||
console.error('[AutoPlay] Error setting up templates:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for CarPlay disconnection events
|
||||
HybridAutoPlay.addListener('didDisconnect', () => {
|
||||
console.log('[AutoPlay] Disconnected');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import { ListTemplate, HybridAutoPlay, type ImageButton } from '@iternio/react-native-auto-play';
|
||||
import type { Album } from '@/store/music/types';
|
||||
import { t } from '@/localisation';
|
||||
import { fetchTracksByAlbum } from '@/store/music/actions';
|
||||
import { ALPHABET_LETTERS } from '@/CONSTANTS';
|
||||
import { playTracks } from '@/utility/usePlayTracks';
|
||||
import store from '@/store';
|
||||
|
||||
/**
|
||||
* Creates a list template showing the 24 most recently added albums,
|
||||
* sorted by creation date in descending order.
|
||||
*/
|
||||
export function createRecentAlbumsTemplate(): ListTemplate {
|
||||
console.log('[AlbumsList] Creating Recent Albums template...');
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
const allAlbums = state.music.albums.ids
|
||||
.map(id => state.music.albums.entities[id])
|
||||
.filter((album): album is Album => album !== undefined);
|
||||
|
||||
const albumsWithDates = allAlbums.filter(album => album.DateCreated !== undefined);
|
||||
const albums = albumsWithDates
|
||||
.sort((a, b) => {
|
||||
const dateA = new Date(a.DateCreated!).getTime();
|
||||
const dateB = new Date(b.DateCreated!).getTime();
|
||||
return dateB - dateA;
|
||||
})
|
||||
.slice(0, 24);
|
||||
|
||||
console.log('[AlbumsList] Recent albums count:', albums.length);
|
||||
|
||||
const items = albums.map(album => ({
|
||||
type: 'default' as const,
|
||||
title: { text: album.Name },
|
||||
detailedText: { text: album.AlbumArtist || t('unknown-artist') },
|
||||
onPress: async () => {
|
||||
console.log('[AlbumsList] Album selected:', album.Name);
|
||||
try {
|
||||
const detailTemplate = await createAlbumDetailTemplate(album);
|
||||
await detailTemplate.push();
|
||||
console.log('[AlbumsList] Album detail pushed');
|
||||
} catch (error) {
|
||||
console.error('[AlbumsList] Error pushing album detail:', error);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
return new ListTemplate({
|
||||
title: { text: t('recent-albums') },
|
||||
sections: items.length > 0 ? { type: 'default', items } : undefined,
|
||||
headerActions: {
|
||||
android: {
|
||||
startHeaderAction: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
ios: {
|
||||
backButton: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list template showing all albums grouped by alphabetical
|
||||
* sections, sorted by artist name then album name.
|
||||
*/
|
||||
export function createAllAlbumsTemplate(): ListTemplate {
|
||||
console.log('[AlbumsList] Creating All Albums template...');
|
||||
|
||||
const state = store.getState();
|
||||
const albums = state.music.albums.ids
|
||||
.map(id => state.music.albums.entities[id])
|
||||
.filter((album): album is Album => album !== undefined)
|
||||
.sort((a, b) => {
|
||||
const artistA = (a.AlbumArtist || a.Artists?.[0] || t('unknown-artist')).toUpperCase();
|
||||
const artistB = (b.AlbumArtist || b.Artists?.[0] || t('unknown-artist')).toUpperCase();
|
||||
if (artistA < artistB) return -1;
|
||||
if (artistA > artistB) return 1;
|
||||
const nameA = a.Name.toUpperCase();
|
||||
const nameB = b.Name.toUpperCase();
|
||||
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0;
|
||||
});
|
||||
|
||||
console.log('[AlbumsList] Total albums:', albums.length);
|
||||
|
||||
// Group into alphabetical sections
|
||||
const sectionMap = new Map<string, Album[]>();
|
||||
|
||||
albums.forEach(album => {
|
||||
const firstLetter = (album.AlbumArtist || album.Artists?.[0] || '#')[0].toUpperCase();
|
||||
const section = ALPHABET_LETTERS.includes(firstLetter) ? firstLetter : '#';
|
||||
|
||||
if (!sectionMap.has(section)) {
|
||||
sectionMap.set(section, []);
|
||||
}
|
||||
sectionMap.get(section)!.push(album);
|
||||
});
|
||||
|
||||
const sections = Array.from(sectionMap.entries())
|
||||
.filter(([_, sectionAlbums]) => sectionAlbums.length > 0)
|
||||
.sort(([a], [b]) => {
|
||||
if (a === '#') return 1;
|
||||
if (b === '#') return -1;
|
||||
return a.localeCompare(b);
|
||||
})
|
||||
.map(([letter, sectionAlbums]) => ({
|
||||
type: 'default' as const,
|
||||
title: letter,
|
||||
items: sectionAlbums.map(album => ({
|
||||
type: 'default' as const,
|
||||
title: { text: album.Name },
|
||||
detailedText: { text: album.AlbumArtist || t('unknown-artist') },
|
||||
onPress: async () => {
|
||||
console.log('[AlbumsList] Album selected:', album.Name);
|
||||
try {
|
||||
const detailTemplate = await createAlbumDetailTemplate(album);
|
||||
await detailTemplate.push();
|
||||
console.log('[AlbumsList] Album detail pushed');
|
||||
} catch (error) {
|
||||
console.error('[AlbumsList] Error pushing album detail:', error);
|
||||
}
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
return new ListTemplate({
|
||||
title: { text: t('all-albums') },
|
||||
sections: sections.length > 0 ? sections : undefined,
|
||||
headerActions: {
|
||||
android: {
|
||||
startHeaderAction: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
ios: {
|
||||
backButton: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a detail template for a specific album showing its tracks
|
||||
* with play and shuffle actions. Fetches tracks from the API if they're
|
||||
* not already in the store.
|
||||
*/
|
||||
export async function createAlbumDetailTemplate(album: Album): Promise<ListTemplate> {
|
||||
let state = store.getState();
|
||||
let trackIds = state.music.tracks.byAlbum[album.Id] || [];
|
||||
|
||||
// Only fetch if tracks are missing
|
||||
if (trackIds.length === 0) {
|
||||
console.log('[AlbumsList] Tracks missing, fetching for album:', album.Name);
|
||||
await store.dispatch(fetchTracksByAlbum(album.Id));
|
||||
state = store.getState();
|
||||
trackIds = state.music.tracks.byAlbum[album.Id] || [];
|
||||
}
|
||||
|
||||
const tracks = trackIds
|
||||
.map(id => state.music.tracks.entities[id])
|
||||
.filter((track): track is NonNullable<typeof track> => track !== undefined);
|
||||
|
||||
console.log('[AlbumsList] Album tracks:', tracks.length);
|
||||
|
||||
const items = tracks.map((track, index) => ({
|
||||
type: 'default' as const,
|
||||
title: { text: track.Name },
|
||||
detailedText: { text: track.Artists?.join(', ') || t('unknown-artist') },
|
||||
onPress: async () => {
|
||||
try {
|
||||
await playTracks(
|
||||
trackIds,
|
||||
state.music.tracks.entities,
|
||||
state.downloads.entities,
|
||||
{ playIndex: index }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[AlbumsList] Error playing track:', error);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const playAction: ImageButton<ListTemplate> = {
|
||||
type: 'image',
|
||||
image: { type: 'glyph', name: 'play_arrow', fontScale: 0.8 },
|
||||
onPress: async () => {
|
||||
console.log('[AlbumsList] Play album:', album.Name);
|
||||
try {
|
||||
await playTracks(
|
||||
trackIds,
|
||||
state.music.tracks.entities,
|
||||
state.downloads.entities
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[AlbumsList] Error playing album:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const shuffleAction: ImageButton<ListTemplate> = {
|
||||
type: 'image',
|
||||
image: { type: 'glyph', name: 'shuffle', fontScale: 0.8 },
|
||||
onPress: async () => {
|
||||
console.log('[AlbumsList] Shuffle album:', album.Name);
|
||||
try {
|
||||
await playTracks(
|
||||
trackIds,
|
||||
state.music.tracks.entities,
|
||||
state.downloads.entities,
|
||||
{ shuffle: true }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[AlbumsList] Error shuffling album:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (items.length === 0) {
|
||||
return new ListTemplate({
|
||||
title: { text: album.Name },
|
||||
headerActions: {
|
||||
android: {
|
||||
startHeaderAction: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
ios: {
|
||||
backButton: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new ListTemplate({
|
||||
title: { text: album.Name },
|
||||
sections: {
|
||||
type: 'default',
|
||||
items,
|
||||
},
|
||||
headerActions: {
|
||||
android: {
|
||||
startHeaderAction: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
endHeaderActions: [playAction, shuffleAction],
|
||||
},
|
||||
ios: {
|
||||
backButton: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
trailingNavigationBarButtons: [playAction, shuffleAction],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { ListTemplate, HybridAutoPlay } from '@iternio/react-native-auto-play';
|
||||
import type { MusicArtist } from '@/store/music/types';
|
||||
import { t } from '@/localisation';
|
||||
import { createAlbumDetailTemplate } from './AlbumsList';
|
||||
import { ALPHABET_LETTERS } from '@/CONSTANTS';
|
||||
import store from '@/store';
|
||||
|
||||
/**
|
||||
* Creates a list template showing all artists grouped by alphabetical
|
||||
* sections, sorted alphabetically by name.
|
||||
*/
|
||||
export function createArtistsTemplate(): ListTemplate {
|
||||
console.log('[ArtistsList] Creating Artists template...');
|
||||
|
||||
const state = store.getState();
|
||||
const artists = state.music.artists.ids
|
||||
.map(id => state.music.artists.entities[id])
|
||||
.filter((artist): artist is MusicArtist => artist !== undefined)
|
||||
.sort((a, b) => a.Name.toUpperCase().localeCompare(b.Name.toUpperCase()));
|
||||
|
||||
console.log('[ArtistsList] Total artists:', artists.length);
|
||||
|
||||
// Group into alphabetical sections
|
||||
const sectionMap = new Map<string, MusicArtist[]>();
|
||||
|
||||
artists.forEach(artist => {
|
||||
const firstLetter = artist.Name[0].toUpperCase();
|
||||
const section = ALPHABET_LETTERS.includes(firstLetter) ? firstLetter : '#';
|
||||
|
||||
if (!sectionMap.has(section)) {
|
||||
sectionMap.set(section, []);
|
||||
}
|
||||
sectionMap.get(section)!.push(artist);
|
||||
});
|
||||
|
||||
const sections = Array.from(sectionMap.entries())
|
||||
.filter(([_, sectionArtists]) => sectionArtists.length > 0)
|
||||
.sort(([a], [b]) => {
|
||||
if (a === '#') return 1;
|
||||
if (b === '#') return -1;
|
||||
return a.localeCompare(b);
|
||||
})
|
||||
.map(([letter, sectionArtists]) => ({
|
||||
type: 'default' as const,
|
||||
title: letter,
|
||||
items: sectionArtists.map(artist => ({
|
||||
type: 'default' as const,
|
||||
title: { text: artist.Name },
|
||||
onPress: async () => {
|
||||
console.log('[ArtistsList] Artist selected:', artist.Name);
|
||||
try {
|
||||
const detailTemplate = createArtistDetailTemplate(artist);
|
||||
await detailTemplate.push();
|
||||
console.log('[ArtistsList] Artist detail pushed');
|
||||
} catch (error) {
|
||||
console.error('[ArtistsList] Error pushing artist detail:', error);
|
||||
}
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
return new ListTemplate({
|
||||
title: { text: t('artists') },
|
||||
sections: sections.length > 0 ? sections : undefined,
|
||||
headerActions: {
|
||||
android: {
|
||||
startHeaderAction: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
ios: {
|
||||
backButton: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a detail template for a specific artist showing all their
|
||||
* albums.
|
||||
*/
|
||||
function createArtistDetailTemplate(artist: MusicArtist): ListTemplate {
|
||||
const state = store.getState();
|
||||
|
||||
// Find albums by this artist
|
||||
const albums = state.music.albums.ids
|
||||
.map(id => state.music.albums.entities[id])
|
||||
.filter(album => album?.ArtistItems?.find(item => item.Id === artist.Id))
|
||||
.filter((album): album is NonNullable<typeof album> => album !== undefined);
|
||||
|
||||
console.log('[ArtistsList] Artist albums:', albums.length);
|
||||
|
||||
const items = albums.map(album => ({
|
||||
type: 'default' as const,
|
||||
title: { text: album.Name },
|
||||
detailedText: { text: album.ProductionYear?.toString() || t('album') },
|
||||
onPress: async () => {
|
||||
console.log('[ArtistsList] Album selected:', album.Name);
|
||||
try {
|
||||
const detailTemplate = await createAlbumDetailTemplate(album);
|
||||
await detailTemplate.push();
|
||||
console.log('[ArtistsList] Album detail pushed');
|
||||
} catch (error) {
|
||||
console.error('[ArtistsList] Error opening album:', error);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
// Play/shuffle actions removed - artist view shows albums, not tracks
|
||||
|
||||
if (items.length === 0) {
|
||||
return new ListTemplate({
|
||||
title: { text: artist.Name },
|
||||
headerActions: {
|
||||
android: {
|
||||
startHeaderAction: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
ios: {
|
||||
backButton: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new ListTemplate({
|
||||
title: { text: artist.Name },
|
||||
sections: { type: 'default', items },
|
||||
headerActions: {
|
||||
android: {
|
||||
startHeaderAction: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
ios: {
|
||||
backButton: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { ListTemplate } from '@iternio/react-native-auto-play';
|
||||
import { t } from '@/localisation';
|
||||
import {
|
||||
createRecentAlbumsTemplate,
|
||||
createAllAlbumsTemplate,
|
||||
} from './AlbumsList';
|
||||
import { createPlaylistsTemplate } from './PlaylistsList';
|
||||
import { createArtistsTemplate } from './ArtistsList';
|
||||
|
||||
/**
|
||||
* Creates the main CarPlay browse menu with navigation to recent albums,
|
||||
* all albums, playlists, and artists.
|
||||
*/
|
||||
export function createBrowseMenu(): ListTemplate {
|
||||
return new ListTemplate({
|
||||
title: { text: t('browse') },
|
||||
sections: {
|
||||
type: 'default',
|
||||
items: [
|
||||
{
|
||||
type: 'default',
|
||||
title: { text: t('recent-albums') },
|
||||
image: { type: 'glyph', name: 'album', fontScale: 0.6 },
|
||||
onPress: async () => {
|
||||
console.log('[BrowseMenu] Recent Albums selected');
|
||||
try {
|
||||
const template = createRecentAlbumsTemplate();
|
||||
await template.push();
|
||||
console.log('[BrowseMenu] Recent Albums template pushed');
|
||||
} catch (error) {
|
||||
console.error('[BrowseMenu] Error pushing Recent Albums:', error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'default',
|
||||
title: { text: t('all-albums') },
|
||||
image: { type: 'glyph', name: 'album', fontScale: 0.6 },
|
||||
onPress: async () => {
|
||||
console.log('[BrowseMenu] All Albums selected');
|
||||
try {
|
||||
const template = createAllAlbumsTemplate();
|
||||
await template.push();
|
||||
console.log('[BrowseMenu] All Albums template pushed');
|
||||
} catch (error) {
|
||||
console.error('[BrowseMenu] Error pushing All Albums:', error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'default',
|
||||
title: { text: t('playlists') },
|
||||
image: { type: 'glyph', name: 'queue', fontScale: 0.6 },
|
||||
onPress: async () => {
|
||||
console.log('[BrowseMenu] Playlists selected');
|
||||
try {
|
||||
const template = createPlaylistsTemplate();
|
||||
await template.push();
|
||||
console.log('[BrowseMenu] Playlists template pushed');
|
||||
} catch (error) {
|
||||
console.error('[BrowseMenu] Error pushing Playlists:', error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'default',
|
||||
title: { text: t('artists') },
|
||||
image: { type: 'glyph', name: 'artist', fontScale: 0.6 },
|
||||
onPress: async () => {
|
||||
console.log('[BrowseMenu] Artists selected');
|
||||
try {
|
||||
const template = createArtistsTemplate();
|
||||
await template.push();
|
||||
console.log('[BrowseMenu] Artists template pushed');
|
||||
} catch (error) {
|
||||
console.error('[BrowseMenu] Error pushing Artists:', error);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { ListTemplate, HybridAutoPlay, type ImageButton } from '@iternio/react-native-auto-play';
|
||||
import type { Playlist } from '@/store/music/types';
|
||||
import { t } from '@/localisation';
|
||||
import { fetchTracksByPlaylist } from '@/store/music/actions';
|
||||
import { playTracks } from '@/utility/usePlayTracks';
|
||||
import store from '@/store';
|
||||
|
||||
/**
|
||||
* Creates a list template showing all user playlists with track counts.
|
||||
*/
|
||||
export function createPlaylistsTemplate(): ListTemplate {
|
||||
console.log('[PlaylistsList] Creating Playlists template...');
|
||||
|
||||
const state = store.getState();
|
||||
const playlists = state.music.playlists.ids
|
||||
.map(id => state.music.playlists.entities[id])
|
||||
.filter((playlist): playlist is Playlist => playlist !== undefined);
|
||||
|
||||
console.log('[PlaylistsList] Total playlists:', playlists.length);
|
||||
|
||||
const items = playlists.map(playlist => {
|
||||
const trackCount = playlist.ChildCount ||
|
||||
state.music.tracks.byPlaylist[playlist.Id]?.length ||
|
||||
playlist.Tracks?.length ||
|
||||
0;
|
||||
|
||||
return {
|
||||
type: 'default' as const,
|
||||
title: { text: playlist.Name },
|
||||
detailedText: { text: `${trackCount} ${trackCount !== 1 ? t('tracks') : t('track')}` },
|
||||
onPress: async () => {
|
||||
console.log('[PlaylistsList] Playlist selected:', playlist.Name);
|
||||
try {
|
||||
const detailTemplate = await createPlaylistDetailTemplate(playlist);
|
||||
await detailTemplate.push();
|
||||
console.log('[PlaylistsList] Playlist detail pushed');
|
||||
} catch (error) {
|
||||
console.error('[PlaylistsList] Error pushing playlist detail:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return new ListTemplate({
|
||||
title: { text: t('playlists') },
|
||||
sections: items.length > 0 ? { type: 'default', items } : undefined,
|
||||
headerActions: {
|
||||
android: {
|
||||
startHeaderAction: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
ios: {
|
||||
backButton: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a detail template for a specific playlist showing its tracks
|
||||
* with play and shuffle actions. Fetches tracks from the API if they're
|
||||
* not already in the store.
|
||||
*/
|
||||
async function createPlaylistDetailTemplate(playlist: Playlist): Promise<ListTemplate> {
|
||||
let state = store.getState();
|
||||
let trackIds = state.music.tracks.byPlaylist[playlist.Id] || [];
|
||||
|
||||
// Only fetch if tracks are missing
|
||||
if (trackIds.length === 0) {
|
||||
console.log('[PlaylistsList] Tracks missing, fetching for playlist:', playlist.Name);
|
||||
await store.dispatch(fetchTracksByPlaylist(playlist.Id));
|
||||
state = store.getState();
|
||||
trackIds = state.music.tracks.byPlaylist[playlist.Id] || [];
|
||||
}
|
||||
|
||||
const tracks = trackIds
|
||||
.map(id => state.music.tracks.entities[id])
|
||||
.filter((track): track is NonNullable<typeof track> => track !== undefined);
|
||||
|
||||
console.log('[PlaylistsList] Playlist tracks:', tracks.length);
|
||||
|
||||
const items = tracks.map((track, index) => ({
|
||||
type: 'default' as const,
|
||||
title: { text: track.Name },
|
||||
detailedText: { text: track.Artists?.join(', ') || t('unknown-artist') },
|
||||
onPress: async () => {
|
||||
try {
|
||||
await playTracks(
|
||||
trackIds,
|
||||
state.music.tracks.entities,
|
||||
state.downloads.entities,
|
||||
{ playIndex: index }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[PlaylistsList] Error playing track:', error);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const playAction: ImageButton<ListTemplate> = {
|
||||
type: 'image',
|
||||
image: { type: 'glyph', name: 'play_arrow', fontScale: 0.8 },
|
||||
onPress: async () => {
|
||||
console.log('[PlaylistsList] Play playlist:', playlist.Name);
|
||||
try {
|
||||
await playTracks(
|
||||
trackIds,
|
||||
state.music.tracks.entities,
|
||||
state.downloads.entities
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[PlaylistsList] Error playing playlist:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const shuffleAction: ImageButton<ListTemplate> = {
|
||||
type: 'image',
|
||||
image: { type: 'glyph', name: 'shuffle', fontScale: 0.8 },
|
||||
onPress: async () => {
|
||||
console.log('[PlaylistsList] Shuffle playlist:', playlist.Name);
|
||||
try {
|
||||
await playTracks(
|
||||
trackIds,
|
||||
state.music.tracks.entities,
|
||||
state.downloads.entities,
|
||||
{ shuffle: true }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[PlaylistsList] Error shuffling playlist:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (items.length === 0) {
|
||||
return new ListTemplate({
|
||||
title: { text: playlist.Name },
|
||||
headerActions: {
|
||||
android: {
|
||||
startHeaderAction: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
ios: {
|
||||
backButton: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new ListTemplate({
|
||||
title: { text: playlist.Name },
|
||||
sections: { type: 'default', items },
|
||||
headerActions: {
|
||||
android: {
|
||||
startHeaderAction: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
endHeaderActions: [playAction, shuffleAction],
|
||||
},
|
||||
ios: {
|
||||
backButton: {
|
||||
type: 'back',
|
||||
onPress: () => HybridAutoPlay.popTemplate(),
|
||||
},
|
||||
trailingNavigationBarButtons: [playAction, shuffleAction],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -166,6 +166,7 @@ export interface Playlist {
|
||||
BackdropImageTags: any[];
|
||||
LocationType: string;
|
||||
MediaType: string;
|
||||
ChildCount?: number;
|
||||
Tracks?: string[];
|
||||
lastRefreshed?: number;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const deviceMap: Record<typeof Platform['OS'], string> = {
|
||||
* from the Redux store to a HTTP Header that authenticates the user against the
|
||||
* Jellyfin server.
|
||||
*/
|
||||
function generateConfig(credentials: Credentials): RequestInit {
|
||||
export function generateConfig(credentials: Credentials): RequestInit {
|
||||
switch (credentials?.type) {
|
||||
case 'jellyfin':
|
||||
return {
|
||||
|
||||
@@ -6,7 +6,7 @@ const playlistOptions = {
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Playlist',
|
||||
Recursive: 'true',
|
||||
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
|
||||
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated,ChildCount',
|
||||
ImageTypeLimit: '1',
|
||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb'
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AlbumTrack, CodecMetadata } from '@/store/music/types';
|
||||
import { Platform } from 'react-native';
|
||||
import { Track } from 'react-native-track-player';
|
||||
import { asyncFetchStore, fetchApi, getImage } from './lib';
|
||||
import { asyncFetchStore, fetchApi, getImage, generateConfig } from './lib';
|
||||
|
||||
const trackOptionsOsOverrides: Record<typeof Platform.OS, Record<string, string>> = {
|
||||
ios: {
|
||||
@@ -49,6 +49,11 @@ export function generateTrackUrl(trackId: string) {
|
||||
export async function generateTrack(track: AlbumTrack): Promise<Track> {
|
||||
// Also construct the URL for the stream
|
||||
const url = generateTrackUrl(track.Id);
|
||||
|
||||
// Get credentials and generate authentication headers
|
||||
const credentials = asyncFetchStore().getState().settings.credentials;
|
||||
const config = generateConfig(credentials);
|
||||
const headers = config.headers;
|
||||
|
||||
return {
|
||||
url,
|
||||
@@ -58,6 +63,7 @@ export async function generateTrack(track: AlbumTrack): Promise<Track> {
|
||||
album: track.Album,
|
||||
duration: track.RunTimeTicks,
|
||||
artwork: getImage(track),
|
||||
headers,
|
||||
bitRate: baseTrackOptions.audioBitRate,
|
||||
};
|
||||
}
|
||||
|
||||
+143
-127
@@ -3,6 +3,8 @@ import { useCallback } from 'react';
|
||||
import TrackPlayer, { Track } from 'react-native-track-player';
|
||||
import { shuffle as shuffleArray } from 'lodash';
|
||||
import { generateTrack } from './JellyfinApi/track';
|
||||
import type { AlbumTrack } from '@/store/music/types';
|
||||
import type { DownloadEntity } from '@/store/downloads/types';
|
||||
|
||||
interface PlayOptions {
|
||||
play: boolean;
|
||||
@@ -24,137 +26,151 @@ const defaults: PlayOptions = {
|
||||
method: 'replace',
|
||||
};
|
||||
|
||||
/**
|
||||
* Core playback logic that can be used outside of React components
|
||||
* Used by both usePlayTracks hook and CarPlay templates
|
||||
*/
|
||||
export async function playTracks(
|
||||
trackIds: string[] | undefined,
|
||||
tracks: Record<string, AlbumTrack>,
|
||||
downloads: Record<string, DownloadEntity>,
|
||||
options: Partial<PlayOptions> = {},
|
||||
): Promise<Track[] | undefined> {
|
||||
if (!trackIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
// GUARD: Check options and queue
|
||||
const {
|
||||
play,
|
||||
shuffle,
|
||||
method,
|
||||
} = Object.assign({}, defaults, options);
|
||||
|
||||
// Convert all trackIds to the relevant format for react-native-track-player
|
||||
const generatedTracks = (await Promise.all(trackIds.map(async (trackId) => {
|
||||
const track = tracks[trackId];
|
||||
|
||||
// GUARD: Check that the track actually exists in Redux
|
||||
if (!trackId || !track) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the generated track from Jellyfin
|
||||
const generatedTrack = await generateTrack(track);
|
||||
|
||||
// Check if a downloaded version exists, and if so rewrite the URL
|
||||
const download = downloads[trackId];
|
||||
if (download?.location) {
|
||||
generatedTrack.url = 'file://' + download.location;
|
||||
}
|
||||
if (download?.image) {
|
||||
generatedTrack.artwork = 'file://' + download.image;
|
||||
}
|
||||
|
||||
return generatedTrack;
|
||||
}))).filter((t): t is Track => typeof t !== 'undefined');
|
||||
|
||||
// Potentially shuffle all tracks
|
||||
const newTracks = shuffle ? shuffleArray(generatedTracks) : generatedTracks;
|
||||
|
||||
console.log('[playTracks] Generated tracks:', newTracks.length, 'tracks');
|
||||
console.log('[playTracks] 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('[playTracks] 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('[playTracks] Playback started');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'add-after-currently-playing': {
|
||||
// Try and locate the current track
|
||||
const currentTrackIndex = await TrackPlayer.getActiveTrackIndex();
|
||||
|
||||
if (currentTrackIndex === undefined) {
|
||||
break;
|
||||
}
|
||||
|
||||
// TrackPlayer.add wants the index of the track to insert in front of,
|
||||
// or just one more
|
||||
const targetTrack = currentTrackIndex + 1;
|
||||
|
||||
// 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('[playTracks] Added tracks after current');
|
||||
|
||||
if (play) {
|
||||
await TrackPlayer.skip(currentTrackIndex + 1);
|
||||
await TrackPlayer.play();
|
||||
console.log('[playTracks] Playback started');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'replace': {
|
||||
// Reset the queue first
|
||||
await TrackPlayer.reset();
|
||||
console.log('[playTracks] Queue reset');
|
||||
|
||||
// GUARD: Check if we need to skip to a particular index
|
||||
if (options.playIndex !== undefined) {
|
||||
// If so, we'll split the tracks into tracks before the
|
||||
// index that should be played, and the queue of tracks that
|
||||
// will start playing
|
||||
const before = newTracks.slice(0, options.playIndex);
|
||||
const current = newTracks.slice(options.playIndex);
|
||||
|
||||
// First, we'll add the current queue and (optionally) force
|
||||
// it to start playing.
|
||||
await TrackPlayer.add(current);
|
||||
console.log('[playTracks] Added current tracks, starting at index:', options.playIndex);
|
||||
if (play) {
|
||||
await TrackPlayer.play();
|
||||
console.log('[playTracks] Playback started');
|
||||
}
|
||||
|
||||
// Then, we'll insert the "previous" tracks after the queue
|
||||
// has started playing. This ensures that these tracks won't
|
||||
// trigger any events on the track player.
|
||||
await TrackPlayer.add(before, 0);
|
||||
} else {
|
||||
await TrackPlayer.add(newTracks);
|
||||
console.log('[playTracks] Added all tracks, queue size:', (await TrackPlayer.getQueue()).length);
|
||||
if (play) {
|
||||
await TrackPlayer.play();
|
||||
const state = await TrackPlayer.getPlaybackState();
|
||||
console.log('[playTracks] Playback started, state:', state);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return newTracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a callback function that starts playing a full album given its
|
||||
* supplied id.
|
||||
*/
|
||||
export default function usePlayTracks() {
|
||||
const tracks = useTypedSelector(state => state.music.tracks.entities);
|
||||
const downloads = useTypedSelector(state => state.downloads.entities);
|
||||
const tracksEntities = useTypedSelector(state => state.music.tracks.entities);
|
||||
const downloadsEntities = useTypedSelector(state => state.downloads.entities);
|
||||
|
||||
return useCallback(async function playTracks(
|
||||
trackIds: string[] | undefined,
|
||||
options: Partial<PlayOptions> = {},
|
||||
): Promise<Track[] | undefined> {
|
||||
if (!trackIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve options and queue
|
||||
const {
|
||||
play,
|
||||
shuffle,
|
||||
method,
|
||||
} = Object.assign({}, defaults, options);
|
||||
|
||||
// Convert all trackIds to the relevant format for react-native-track-player
|
||||
const generatedTracks = (await Promise.all(trackIds.map(async (trackId) => {
|
||||
const track = tracks[trackId];
|
||||
|
||||
// GUARD: Check that the track actually exists in Redux
|
||||
if (!trackId || !track) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the generated track from Jellyfin
|
||||
const generatedTrack = await generateTrack(track);
|
||||
|
||||
// Check if a downloaded version exists, and if so rewrite the URL
|
||||
const download = downloads[trackId];
|
||||
if (download?.location) {
|
||||
generatedTrack.url = 'file://' + download.location;
|
||||
}
|
||||
if (download?.image) {
|
||||
generatedTrack.artwork = 'file://' + download.image;
|
||||
}
|
||||
|
||||
return generatedTrack;
|
||||
}))).filter((t): t is Track => typeof t !== 'undefined');
|
||||
|
||||
// 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;
|
||||
}
|
||||
case 'add-after-currently-playing': {
|
||||
// Try and locate the current track
|
||||
const currentTrackIndex = await TrackPlayer.getActiveTrackIndex();
|
||||
|
||||
if (currentTrackIndex === undefined) {
|
||||
break;
|
||||
}
|
||||
|
||||
// TrackPlayer.add wants the index of the track to insert in front of,
|
||||
// or just one more
|
||||
const targetTrack = currentTrackIndex + 1;
|
||||
|
||||
// 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;
|
||||
}
|
||||
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) {
|
||||
// If so, we'll split the tracks into tracks before the
|
||||
// index that should be played, and the queue of tracks that
|
||||
// will start playing
|
||||
const before = newTracks.slice(0, options.playIndex);
|
||||
const current = newTracks.slice(options.playIndex);
|
||||
|
||||
// First, we'll add the current queue and (optionally) force
|
||||
// it to start playing.
|
||||
await TrackPlayer.add(current);
|
||||
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
|
||||
// trigger any events on the track player.
|
||||
await TrackPlayer.add(before, 0);
|
||||
} else {
|
||||
await TrackPlayer.add(newTracks);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}, [downloads, tracks]);
|
||||
return useCallback(
|
||||
(trackIds: string[] | undefined, options: Partial<PlayOptions> = {}) =>
|
||||
playTracks(trackIds, tracksEntities, downloadsEntities, options),
|
||||
[downloadsEntities, tracksEntities]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user