react-native-audio-browser 0.2.0-next.2 → 0.2.0-next.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -21
- package/android/build.gradle +35 -12
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +389 -164
- package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
- package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
- package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +24 -5
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
- package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
- package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
- package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
- package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
- package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
- package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
- package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
- package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
- package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +56 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
- package/android/src/main/res/values/strings.xml +25 -5
- package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
- package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
- package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
- package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
- package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
- package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
- package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
- package/ios/Browser/BrowserConfig.swift +102 -5
- package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
- package/ios/Browser/BrowserManager.swift +230 -205
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +264 -106
- package/ios/Browser/SearchDrillIn.swift +7 -7
- package/ios/Browser/SectionPresentation.swift +64 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +58 -0
- package/ios/Browser/TabBarEntries.swift +9 -3
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +227 -85
- package/ios/CarPlay/CarPlayImageLoader.swift +42 -20
- package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
- package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
- package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
- package/ios/Extension/Section+Copying.swift +29 -0
- package/ios/Extension/String+IcyCharset.swift +21 -0
- package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
- package/ios/Extension/Track+Copying.swift +8 -12
- package/ios/Extension/Track+Identity.swift +18 -0
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +253 -100
- package/ios/Model/NitroTypeStubs.swift +125 -23
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Model/RemoteCommand.swift +18 -0
- package/ios/Model/TrackPlayerError.swift +2 -2
- package/ios/Option/SessionCategories.swift +3 -2
- package/ios/Player/MediaIntentCriteria.swift +17 -9
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +54 -6
- package/ios/Player/PlaybackCoordinator.swift +13 -6
- package/ios/Player/PlaybackErrorHandler.swift +4 -4
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +46 -13
- package/ios/Player/RetryManager.swift +80 -18
- package/ios/Player/SeekCompletionHandler.swift +3 -1
- package/ios/Player/ShuffleOrder.swift +33 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +62 -24
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
- package/ios/Tests/JsonModelsTests.swift +150 -1
- package/ios/Tests/LRUCacheTests.swift +111 -0
- package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
- package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
- package/ios/Tests/PlaybackCoordinatorTests.swift +1 -1
- package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
- package/ios/Tests/QueueManagerTests.swift +122 -10
- package/ios/Tests/RemoteCommandControllerTests.swift +172 -0
- package/ios/Tests/RemoteCommandTests.swift +67 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
- package/ios/Tests/RetryManagerTests.swift +89 -0
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +90 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +127 -0
- package/ios/Tests/Support/TestTypes.swift +8 -1
- package/ios/Tests/TabBarEntriesTests.swift +19 -19
- package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/TrackPlayer.swift +63 -35
- package/ios/TrackPlayerCallbacks.swift +5 -38
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- package/ios/Util/LRUCache.swift +4 -1
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/SFSymbolRenderer.swift +24 -2
- package/ios/Util/SVGProcessor.swift +9 -2
- package/lib/commonjs/AudioBrowser.js +8 -8
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/CLAUDE.md +52 -0
- package/lib/commonjs/features/browser-config.js +12 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +11 -3
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/carConnection.js +1 -1
- package/lib/commonjs/features/equalizer.js +1 -1
- package/lib/commonjs/features/errors.js +3 -3
- package/lib/commonjs/features/favorites.js +14 -11
- package/lib/commonjs/features/favorites.js.map +1 -1
- package/lib/commonjs/features/gate.js +16 -1
- package/lib/commonjs/features/gate.js.map +1 -1
- package/lib/commonjs/features/metadata.js +20 -3
- package/lib/commonjs/features/metadata.js.map +1 -1
- package/lib/commonjs/features/network.js +1 -1
- package/lib/commonjs/features/nowPlaying.js +1 -1
- package/lib/commonjs/features/output.js +1 -1
- package/lib/commonjs/features/playback/controls.js +4 -2
- package/lib/commonjs/features/playback/controls.js.map +1 -1
- package/lib/commonjs/features/playback/playWhenReady.js +1 -1
- package/lib/commonjs/features/playback/playing.js +1 -1
- package/lib/commonjs/features/playback/progress.js +3 -2
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/rate.js +8 -1
- package/lib/commonjs/features/playback/rate.js.map +1 -1
- package/lib/commonjs/features/playback/state.js +3 -4
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/commonjs/features/playback/volume.js +1 -1
- package/lib/commonjs/features/player/options.js +48 -34
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +18 -9
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +23 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -0
- package/lib/commonjs/features/queue/activeTrack.js +15 -4
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +13 -6
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/features/queue/repeatMode.js +1 -1
- package/lib/commonjs/features/queue/shuffle.js +1 -1
- package/lib/commonjs/features/remoteControls.js +13 -92
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +13 -1
- package/lib/commonjs/features/sleepTimer.js.map +1 -1
- package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
- package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
- package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
- package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/commonjs/utils/getTrackIdentity.js +24 -0
- package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +200 -75
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
- package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
- package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +175 -100
- package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
- package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
- package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
- package/lib/commonjs/web/browser/SearchManager.js +18 -23
- package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
- package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/commonjs/web/browser/sections.js +89 -0
- package/lib/commonjs/web/browser/sections.js.map +1 -0
- package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
- package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
- package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
- package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/AudioBrowser.js +2 -1
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/CLAUDE.md +52 -0
- package/lib/module/features/browser-config.js +12 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +11 -3
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/carConnection.js +1 -1
- package/lib/module/features/equalizer.js +1 -1
- package/lib/module/features/errors.js +3 -3
- package/lib/module/features/favorites.js +14 -11
- package/lib/module/features/favorites.js.map +1 -1
- package/lib/module/features/gate.js +16 -1
- package/lib/module/features/gate.js.map +1 -1
- package/lib/module/features/metadata.js +20 -3
- package/lib/module/features/metadata.js.map +1 -1
- package/lib/module/features/network.js +1 -1
- package/lib/module/features/nowPlaying.js +1 -1
- package/lib/module/features/output.js +1 -1
- package/lib/module/features/playback/controls.js +4 -2
- package/lib/module/features/playback/controls.js.map +1 -1
- package/lib/module/features/playback/playWhenReady.js +1 -1
- package/lib/module/features/playback/playing.js +1 -1
- package/lib/module/features/playback/progress.js +3 -2
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/rate.js +8 -1
- package/lib/module/features/playback/rate.js.map +1 -1
- package/lib/module/features/playback/state.js +3 -4
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +0 -5
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/module/features/playback/volume.js +1 -1
- package/lib/module/features/player/options.js +47 -32
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +17 -8
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +19 -0
- package/lib/module/features/player/validateOptions.js.map +1 -0
- package/lib/module/features/queue/activeTrack.js +15 -4
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +13 -6
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/features/queue/repeatMode.js +1 -1
- package/lib/module/features/queue/shuffle.js +1 -1
- package/lib/module/features/remoteControls.js +13 -92
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +13 -1
- package/lib/module/features/sleepTimer.js.map +1 -1
- package/lib/module/test-utils/reactNativeStub.js +58 -0
- package/lib/module/test-utils/reactNativeStub.js.map +1 -0
- package/lib/module/utils/LazyNativeEmitter.js +28 -14
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/utils/NativeUpdatedValue.js +31 -13
- package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/module/utils/getTrackIdentity.js +20 -0
- package/lib/module/utils/getTrackIdentity.js.map +1 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +200 -75
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
- package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
- package/lib/module/web/TrackPlayer/Player.js +254 -39
- package/lib/module/web/TrackPlayer/Player.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
- package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +175 -100
- package/lib/module/web/browser/BrowserManager.js.map +1 -1
- package/lib/module/web/browser/FavoriteManager.js +31 -21
- package/lib/module/web/browser/FavoriteManager.js.map +1 -1
- package/lib/module/web/browser/SearchManager.js +18 -23
- package/lib/module/web/browser/SearchManager.js.map +1 -1
- package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/module/web/browser/sections.js +83 -0
- package/lib/module/web/browser/sections.js.map +1 -0
- package/lib/module/web/http/RequestConfigBuilder.js +110 -27
- package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/module/web/http/followMediaRedirect.js +215 -0
- package/lib/module/web/http/followMediaRedirect.js.map +1 -0
- package/lib/module/web/player/NowPlayingManager.js +4 -3
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js +56 -11
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AudioBrowser.d.ts +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/browser.d.ts +10 -2
- package/lib/typescript/src/features/browser.d.ts.map +1 -1
- package/lib/typescript/src/features/carConnection.d.ts +1 -1
- package/lib/typescript/src/features/equalizer.d.ts +1 -1
- package/lib/typescript/src/features/errors.d.ts +3 -3
- package/lib/typescript/src/features/favorites.d.ts +14 -11
- package/lib/typescript/src/features/favorites.d.ts.map +1 -1
- package/lib/typescript/src/features/gate.d.ts +18 -2
- package/lib/typescript/src/features/gate.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +20 -3
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/network.d.ts +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
- package/lib/typescript/src/features/output.d.ts +1 -1
- package/lib/typescript/src/features/playback/controls.d.ts +4 -2
- package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
- package/lib/typescript/src/features/playback/playing.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/rate.d.ts +8 -1
- package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +3 -4
- package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/volume.d.ts +1 -1
- package/lib/typescript/src/features/player/options.d.ts +108 -56
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +14 -4
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
- package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +7 -4
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
- package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts +23 -22
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
- package/lib/typescript/src/types/browser-native.d.ts +3 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +380 -126
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +82 -57
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
- package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +16 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +7 -10
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
- package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +45 -25
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +36 -11
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +15 -12
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +7 -10
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +7 -10
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
- package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +57 -49
- package/nitrogen/generated/android/c++/JSection.hpp +116 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +25 -46
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +8 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +15 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -12
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +244 -133
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +31 -16
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +33 -18
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
- package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
- package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
- package/nitrogen/generated/ios/swift/Section.swift +82 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
- package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +28 -63
- package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
- package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
- package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
- package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
- package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
- package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
- package/nitrogen/generated/shared/c++/Section.hpp +102 -0
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
- package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +18 -30
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
- package/package.json +24 -11
- package/src/AudioBrowser.ts +2 -1
- package/src/CLAUDE.md +52 -0
- package/src/features/battery.test.ts +79 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +12 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +11 -3
- package/src/features/carConnection.ts +1 -1
- package/src/features/equalizer.ts +1 -1
- package/src/features/errors.ts +3 -3
- package/src/features/favorites.ts +14 -11
- package/src/features/gate.ts +16 -1
- package/src/features/hook-wiring.test.ts +302 -0
- package/src/features/metadata.ts +20 -3
- package/src/features/network.ts +1 -1
- package/src/features/nowPlaying.ts +1 -1
- package/src/features/output.ts +1 -1
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/playWhenReady.ts +1 -1
- package/src/features/playback/playing.ts +1 -1
- package/src/features/playback/progress.test.ts +169 -0
- package/src/features/playback/progress.ts +3 -2
- package/src/features/playback/rate.ts +8 -1
- package/src/features/playback/state.ts +3 -4
- package/src/features/playback/trackPlaybackTime.test.ts +16 -6
- package/src/features/playback/trackPlaybackTime.ts +1 -6
- package/src/features/playback/volume.ts +1 -1
- package/src/features/player/options.ts +126 -76
- package/src/features/player/setup.test.ts +24 -7
- package/src/features/player/setup.ts +22 -10
- package/src/features/player/validateOptions.ts +21 -0
- package/src/features/queue/activeTrack.ts +15 -4
- package/src/features/queue/queue.ts +13 -6
- package/src/features/queue/repeatMode.ts +1 -1
- package/src/features/queue/shuffle.ts +1 -1
- package/src/features/remoteControls.ts +13 -92
- package/src/features/sleepTimer.test.ts +172 -0
- package/src/features/sleepTimer.ts +9 -1
- package/src/nitro-stub-drift.test.ts +277 -0
- package/src/public-surface.test.ts +147 -0
- package/src/specs/audio-browser.nitro.ts +5 -4
- package/src/style-resolution.test.ts +210 -0
- package/src/test-utils/reactNativeStub.ts +60 -0
- package/src/types/browser-native.ts +7 -3
- package/src/types/browser-nodes.ts +396 -134
- package/src/types/browser.ts +85 -59
- package/src/utils/LazyNativeEmitter.ts +28 -15
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useDebug.test.ts +182 -0
- package/src/utils/useNativeUpdatedValue.test.ts +103 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
- package/src/web/NativeAudioBrowser.ts +239 -82
- package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
- package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
- package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
- package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
- package/src/web/TrackPlayer/Player.load.test.ts +137 -1
- package/src/web/TrackPlayer/Player.ts +284 -37
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
- package/src/web/__tests__/lifecycle.test.ts +345 -0
- package/src/web/__tests__/lifecycleHarness.ts +351 -0
- package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
- package/src/web/__tests__/loadSupersession.test.ts +175 -0
- package/src/web/__tests__/retry.test.ts +169 -0
- package/src/web/browser/BrowserManager.ts +209 -111
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- package/src/web/browser/SearchManager.test.ts +75 -0
- package/src/web/browser/SearchManager.ts +25 -28
- package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
- package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
- package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
- package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
- package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
- package/src/web/browser/assertBrowsePageShape.ts +5 -3
- package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
- package/src/web/browser/classifyTrackNavigation.ts +9 -9
- package/src/web/browser/parseSearchResponse.test.ts +8 -2
- package/src/web/browser/sections.test.ts +91 -0
- package/src/web/browser/sections.ts +72 -0
- package/src/web/http/RequestConfigBuilder.test.ts +241 -1
- package/src/web/http/RequestConfigBuilder.ts +140 -33
- package/src/web/http/followMediaRedirect.test.ts +289 -0
- package/src/web/http/followMediaRedirect.ts +253 -0
- package/src/web/player/NowPlayingManager.ts +4 -3
- package/src/web/util/BrowserPathHelper.test.ts +52 -0
- package/src/web/util/BrowserPathHelper.ts +61 -13
- package/website/guide/android-auto.md +114 -0
- package/website/guide/android-certificates.md +409 -0
- package/website/guide/artwork.md +255 -0
- package/website/guide/audio-output.md +135 -0
- package/website/guide/automotive.md +86 -0
- package/website/guide/basic-usage.md +151 -0
- package/website/guide/battery.md +115 -0
- package/website/guide/browser.md +581 -0
- package/website/guide/carplay.md +620 -0
- package/website/guide/configuration.md +173 -0
- package/website/guide/equalizer.md +231 -0
- package/website/guide/errors.md +310 -0
- package/website/guide/favorites.md +154 -0
- package/website/guide/gate.md +143 -0
- package/website/guide/getting-started.md +46 -0
- package/website/guide/hooks.md +238 -0
- package/website/guide/live-streams.md +125 -0
- package/website/guide/metadata.md +239 -0
- package/website/guide/migrating-from-track-player.md +244 -0
- package/website/guide/native-callback-fetch.md +127 -0
- package/website/guide/network.md +86 -0
- package/website/guide/now-playing.md +175 -0
- package/website/guide/playback.md +328 -0
- package/website/guide/queue.md +300 -0
- package/website/guide/remote-controls.md +256 -0
- package/website/guide/search.md +255 -0
- package/website/guide/sleep-timer.md +268 -0
- package/website/guide/track.md +247 -0
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/ios/Model/MediaURL.swift +0 -67
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
- package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
- package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
- package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
- package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
- package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
- package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
|
@@ -26,9 +26,22 @@ public final class RNABCarPlayController: NSObject {
|
|
|
26
26
|
private var trackSelector: TrackSelector?
|
|
27
27
|
|
|
28
28
|
/// Track content subscriptions
|
|
29
|
-
private var isStarted = false
|
|
30
29
|
private var listenerRemovals: [() -> Void] = []
|
|
31
30
|
|
|
31
|
+
/// Identifies the current `start()` run; nil while stopped.
|
|
32
|
+
///
|
|
33
|
+
/// A run needs an identity, not just a flag, because `start()` parks an
|
|
34
|
+
/// untracked Task on the uncancellable `playerAndConfiguredBrowser.wait()`,
|
|
35
|
+
/// which stays queued across a `reset()` and resumes every waiter. `restart()`
|
|
36
|
+
/// is `stop(); start()`, so a boolean is `true` again by the time the old
|
|
37
|
+
/// waiter resumes and both runs proceed — duplicate subscriptions and a leaked
|
|
38
|
+
/// `NowPlayingObserver` for the session. Comparing the captured run against
|
|
39
|
+
/// this answers "still running?" and "still me?" in one condition.
|
|
40
|
+
private var currentRun: UInt?
|
|
41
|
+
private var runCounter: UInt = 0
|
|
42
|
+
|
|
43
|
+
private var isStarted: Bool { currentRun != nil }
|
|
44
|
+
|
|
32
45
|
/// Paths whose loading template has been pushed but not yet appeared. Guards a
|
|
33
46
|
/// rapid double-tap from pushing the same destination twice (the first push is
|
|
34
47
|
/// async, so `topTemplate` may not reflect it yet). Cleared when it appears.
|
|
@@ -89,21 +102,37 @@ public final class RNABCarPlayController: NSObject {
|
|
|
89
102
|
audioBrowser?.browserManager.config ?? BrowserConfig()
|
|
90
103
|
}
|
|
91
104
|
|
|
105
|
+
/// The configured list playing-indicator location, in CarPlay-framework terms.
|
|
106
|
+
private var playingIndicatorLocation: CPListItemPlayingIndicatorLocation {
|
|
107
|
+
switch audioBrowser?.carPlayPlayingIndicatorLocation {
|
|
108
|
+
case .trailing: .trailing
|
|
109
|
+
case .leading, nil: .leading
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// The tabs the tab bar actually shows. A disabled tab hides rather than
|
|
114
|
+
/// greys: `CPTabBarTemplate` has no disabled affordance, so per `Track`'s
|
|
115
|
+
/// rendering ladder an unavailable tab must not render as a normal-looking
|
|
116
|
+
/// control (matches Android Auto, which filters before its 4-tab cap).
|
|
117
|
+
///
|
|
118
|
+
/// Named once because every site that reasons about the tab bar must agree on
|
|
119
|
+
/// it — `showTabBar` builds from this list, and `handleTabsChanged` compares
|
|
120
|
+
/// against it. Those two disagreeing is exactly what made the in-place
|
|
121
|
+
/// restamp dead code whenever any tab was disabled.
|
|
122
|
+
private static func visibleTabs(_ tabs: [Track]) -> [Track] {
|
|
123
|
+
tabs.filter { $0.disabled != true }
|
|
124
|
+
}
|
|
125
|
+
|
|
92
126
|
/// Whether a list item identifies the currently active (loaded) track.
|
|
93
127
|
///
|
|
94
|
-
///
|
|
128
|
+
/// Compares track identities (`id` when non-blank, else `src`): the active
|
|
95
129
|
/// track may have been loaded outside the browse tree (the consumer's own
|
|
96
130
|
/// `load`/`setQueue`) with a `src` that differs textually from the browse
|
|
97
131
|
/// row's (absolute vs relative, extra query params) while being the same
|
|
98
|
-
/// item.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if let id, !id.isEmpty, let currentId = currentTrack.id, !currentId.isEmpty {
|
|
103
|
-
return id == currentId
|
|
104
|
-
}
|
|
105
|
-
guard let src else { return false }
|
|
106
|
-
return currentTrack.src == src
|
|
132
|
+
/// item. A nil identity on either side never matches.
|
|
133
|
+
private func isActiveTrack(identity: String?) -> Bool {
|
|
134
|
+
guard let identity, let currentTrack = audioBrowser?.getPlayer()?.currentTrack else { return false }
|
|
135
|
+
return currentTrack.identity == identity
|
|
107
136
|
}
|
|
108
137
|
|
|
109
138
|
// MARK: - Initialization
|
|
@@ -118,8 +147,8 @@ public final class RNABCarPlayController: NSObject {
|
|
|
118
147
|
self?.listItemFactory?.createListItem(for: track, handler: handler)
|
|
119
148
|
?? CPListItem(text: track.title, detailText: nil)
|
|
120
149
|
}
|
|
121
|
-
nowPlayingManager.
|
|
122
|
-
Task { @MainActor in await self?.
|
|
150
|
+
nowPlayingManager.navigateToPath = { [weak self] path, title in
|
|
151
|
+
Task { @MainActor in await self?.navigateToPath(path, title: title) }
|
|
123
152
|
}
|
|
124
153
|
}
|
|
125
154
|
|
|
@@ -137,7 +166,9 @@ public final class RNABCarPlayController: NSObject {
|
|
|
137
166
|
@objc
|
|
138
167
|
public func start() {
|
|
139
168
|
guard !isStarted else { return }
|
|
140
|
-
|
|
169
|
+
runCounter &+= 1
|
|
170
|
+
currentRun = runCounter
|
|
171
|
+
let run = runCounter
|
|
141
172
|
|
|
142
173
|
logger.info("Starting CarPlay controller")
|
|
143
174
|
|
|
@@ -164,7 +195,8 @@ public final class RNABCarPlayController: NSObject {
|
|
|
164
195
|
// Wait for both browser and player to be ready
|
|
165
196
|
Task { @MainActor in
|
|
166
197
|
let (browser, _) = await playerAndConfiguredBrowser.wait()
|
|
167
|
-
|
|
198
|
+
// Answers "still running?" and "still this run?" at once.
|
|
199
|
+
guard self.currentRun == run else { return }
|
|
168
200
|
self.logger.debug("AudioBrowser and player ready, setting up CarPlay")
|
|
169
201
|
self.audioBrowser = browser
|
|
170
202
|
self.isGated = browser.isGateActive
|
|
@@ -178,12 +210,13 @@ public final class RNABCarPlayController: NSObject {
|
|
|
178
210
|
|
|
179
211
|
// Create list item factory
|
|
180
212
|
let factory = CarPlayListItemFactory(
|
|
181
|
-
isActiveTrack: { [weak self]
|
|
213
|
+
isActiveTrack: { [weak self] identity in self?.isActiveTrack(identity: identity) ?? false },
|
|
182
214
|
onItemSelected: { [weak self] track, completion in
|
|
183
215
|
self?.handleItemSelection(track: track, completion: completion)
|
|
184
216
|
},
|
|
185
217
|
)
|
|
186
218
|
factory.imageLoader = self.imageLoader
|
|
219
|
+
factory.playingIndicatorLocation = self.playingIndicatorLocation
|
|
187
220
|
self.listItemFactory = factory
|
|
188
221
|
|
|
189
222
|
self.setupContentSubscriptions()
|
|
@@ -198,7 +231,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
198
231
|
@objc
|
|
199
232
|
public func stop() {
|
|
200
233
|
guard isStarted else { return }
|
|
201
|
-
|
|
234
|
+
currentRun = nil
|
|
202
235
|
|
|
203
236
|
logger.info("Stopping CarPlay controller")
|
|
204
237
|
|
|
@@ -261,7 +294,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
261
294
|
}
|
|
262
295
|
|
|
263
296
|
// Subscribe to config changes (for Now Playing buttons and per-track
|
|
264
|
-
// button state — e.g.
|
|
297
|
+
// button state — e.g. resolveAlbumPath appearing/disappearing)
|
|
265
298
|
audioBrowser.browserManager.onConfigChanged = { [weak self] _ in
|
|
266
299
|
Task { @MainActor in
|
|
267
300
|
self?.nowPlayingManager.setupNowPlayingButtons()
|
|
@@ -269,13 +302,22 @@ public final class RNABCarPlayController: NSObject {
|
|
|
269
302
|
}
|
|
270
303
|
}
|
|
271
304
|
|
|
272
|
-
// Now-playing buttons + Up Next
|
|
273
|
-
// refresh them when player options
|
|
274
|
-
// the only path that can change them).
|
|
305
|
+
// Now-playing buttons + Up Next + the list playing-indicator location are
|
|
306
|
+
// runtime-updatable via updateOptions(); refresh them when player options
|
|
307
|
+
// change (configureBrowser is no longer the only path that can change them).
|
|
275
308
|
let optionsToken = audioBrowser.playerOptionsChangedEmitter.addListener { [weak self] in
|
|
276
309
|
Task { @MainActor in
|
|
277
|
-
self
|
|
278
|
-
self
|
|
310
|
+
guard let self else { return }
|
|
311
|
+
self.nowPlayingManager.setupNowPlayingButtons()
|
|
312
|
+
self.nowPlayingManager.updateNowPlayingButtonStates()
|
|
313
|
+
// The factory's copy doubles as the last-applied value: sweep the
|
|
314
|
+
// templates only when the location actually changed, not on every
|
|
315
|
+
// updateOptions() call.
|
|
316
|
+
let location = self.playingIndicatorLocation
|
|
317
|
+
if let factory = self.listItemFactory, factory.playingIndicatorLocation != location {
|
|
318
|
+
factory.playingIndicatorLocation = location
|
|
319
|
+
self.updatePlayingIndicators()
|
|
320
|
+
}
|
|
279
321
|
}
|
|
280
322
|
}
|
|
281
323
|
listenerRemovals.append { [weak audioBrowser] in
|
|
@@ -409,9 +451,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
409
451
|
private func buildInitialInterface() async {
|
|
410
452
|
guard let audioBrowser else {
|
|
411
453
|
logger.error("AudioBrowser not available")
|
|
412
|
-
await showRootNavigationError(
|
|
413
|
-
NavigationError(code: .unknownError, message: "", statusCode: nil, statusCodeSuccess: nil),
|
|
414
|
-
)
|
|
454
|
+
await showRootNavigationError(.code(.unknownError))
|
|
415
455
|
return
|
|
416
456
|
}
|
|
417
457
|
|
|
@@ -430,16 +470,10 @@ public final class RNABCarPlayController: NSObject {
|
|
|
430
470
|
showLoadingTemplate()
|
|
431
471
|
}
|
|
432
472
|
do {
|
|
473
|
+
// An empty query result routes to the .emptyContent error page inside
|
|
474
|
+
// showTabBar, shared with runtime tab changes.
|
|
433
475
|
let queriedTabs = try await audioBrowser.browserManager.queryTabs()
|
|
434
|
-
|
|
435
|
-
await showTabBar(tabs: queriedTabs)
|
|
436
|
-
} else {
|
|
437
|
-
// Empty is a navigation error (.emptyContent) so it formats through
|
|
438
|
-
// the app's formatNavigationError like every other failure (ADR 0001).
|
|
439
|
-
await showRootNavigationError(
|
|
440
|
-
NavigationError(code: .emptyContent, message: "", statusCode: nil, statusCodeSuccess: nil),
|
|
441
|
-
)
|
|
442
|
-
}
|
|
476
|
+
await showTabBar(tabs: queriedTabs)
|
|
443
477
|
} catch {
|
|
444
478
|
logger.error("Failed to query tabs: \(error.localizedDescription)")
|
|
445
479
|
await showRootNavigationError(NavigationError.from(error))
|
|
@@ -459,7 +493,9 @@ public final class RNABCarPlayController: NSObject {
|
|
|
459
493
|
// MARK: - Tab Bar
|
|
460
494
|
|
|
461
495
|
@MainActor
|
|
462
|
-
private func showTabBar(tabs: [Track]) async {
|
|
496
|
+
private func showTabBar(tabs allTabs: [Track]) async {
|
|
497
|
+
let tabs = Self.visibleTabs(allTabs)
|
|
498
|
+
|
|
463
499
|
// Serialize concurrent tab-bar builds across BOTH the gated and non-gated
|
|
464
500
|
// paths with one shared generation token: bump at entry, capture, and bail
|
|
465
501
|
// before any template mutation once a newer build supersedes us. Both paths
|
|
@@ -477,11 +513,27 @@ public final class RNABCarPlayController: NSObject {
|
|
|
477
513
|
// the gate page with that request's chrome; an allowed tab shows real
|
|
478
514
|
// content. This generalizes the old "all tabs show the gate" — a path the
|
|
479
515
|
// resolver allows now shows real content.
|
|
516
|
+
//
|
|
517
|
+
// Ahead of the empty guard below, because a gated build from zero tabs is
|
|
518
|
+
// NOT blank: it paints a full-screen gate page as root (a gate raised
|
|
519
|
+
// before config loads is exactly that case). `showGatedTabBar` owns the
|
|
520
|
+
// empty-tabs decision and falls back to the same .emptyContent page when
|
|
521
|
+
// the resolver allows the root.
|
|
480
522
|
if isGated {
|
|
481
523
|
await showGatedTabBar(tabs: tabs, generation: generation)
|
|
482
524
|
return
|
|
483
525
|
}
|
|
484
526
|
|
|
527
|
+
// A zero-tab CPTabBarTemplate renders blank, with no copy, so empty routes
|
|
528
|
+
// to the .emptyContent error page like every other failure (formatted
|
|
529
|
+
// through the app's formatNavigationError, ADR 0001). The guard lives at
|
|
530
|
+
// this choke point — both the initial build and runtime tab changes pass
|
|
531
|
+
// through here.
|
|
532
|
+
guard !tabs.isEmpty else {
|
|
533
|
+
await showRootNavigationError(.code(.emptyContent))
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
|
|
485
537
|
logger.info("Building tab bar with \(tabs.count) tabs")
|
|
486
538
|
|
|
487
539
|
let maxTabs = CPTabBarTemplate.maximumTabCount
|
|
@@ -501,8 +553,8 @@ public final class RNABCarPlayController: NSObject {
|
|
|
501
553
|
interfaceController.safeSetRoot(tabBar, animated: true)
|
|
502
554
|
|
|
503
555
|
// Load content for the first tab only - others load lazily when selected
|
|
504
|
-
if let firstTemplate = tabTemplates.first, let firstTab = tabs.first, let
|
|
505
|
-
await loadContent(for:
|
|
556
|
+
if let firstTemplate = tabTemplates.first, let firstTab = tabs.first, let path = firstTab.path {
|
|
557
|
+
await loadContent(for: path, into: firstTemplate)
|
|
506
558
|
}
|
|
507
559
|
}
|
|
508
560
|
|
|
@@ -521,8 +573,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
521
573
|
|
|
522
574
|
guard !tabs.isEmpty else {
|
|
523
575
|
// Tabs unknown (config not loaded yet, or none) — resolve the root path
|
|
524
|
-
// and show a single gate page
|
|
525
|
-
// build once tabs arrive).
|
|
576
|
+
// and show a single gate page.
|
|
526
577
|
let outcome = await audioBrowser.gateDecision(
|
|
527
578
|
for: NativeGateRequest(reason: .browse, path: nil, search: nil),
|
|
528
579
|
)
|
|
@@ -532,6 +583,11 @@ public final class RNABCarPlayController: NSObject {
|
|
|
532
583
|
interfaceController.safeSetRoot(
|
|
533
584
|
makeGateTemplate(gate: outcome.chrome, tab: nil), animated: true,
|
|
534
585
|
)
|
|
586
|
+
} else {
|
|
587
|
+
// The resolver allows the root, and there are no tabs to show — the
|
|
588
|
+
// same nothing-to-render case the ungated build routes to .emptyContent.
|
|
589
|
+
// Without this the loading spinner would stay as root forever.
|
|
590
|
+
await showRootNavigationError(.code(.emptyContent))
|
|
535
591
|
}
|
|
536
592
|
return
|
|
537
593
|
}
|
|
@@ -540,7 +596,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
540
596
|
var templates: [CPListTemplate] = []
|
|
541
597
|
for tab in limitedTabs {
|
|
542
598
|
let outcome = await audioBrowser.gateDecision(
|
|
543
|
-
for: NativeGateRequest(reason: .browse, path: tab.
|
|
599
|
+
for: NativeGateRequest(reason: .browse, path: tab.path, search: nil),
|
|
544
600
|
)
|
|
545
601
|
guard gateBuildGeneration == generation else { return } // superseded by a newer build
|
|
546
602
|
if outcome.gated {
|
|
@@ -566,8 +622,8 @@ public final class RNABCarPlayController: NSObject {
|
|
|
566
622
|
// Eagerly fill any allowed (content) tabs: templateDidAppear isn't
|
|
567
623
|
// guaranteed to re-fire for templates swapped in via updateTemplates.
|
|
568
624
|
for (tab, template) in zip(limitedTabs, templates) {
|
|
569
|
-
guard getPath(from: template) != nil, let
|
|
570
|
-
await loadContent(for:
|
|
625
|
+
guard getPath(from: template) != nil, let path = tab.path else { continue }
|
|
626
|
+
await loadContent(for: path, into: template)
|
|
571
627
|
}
|
|
572
628
|
}
|
|
573
629
|
|
|
@@ -576,7 +632,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
576
632
|
/// until its content lazy-loads on first appearance.
|
|
577
633
|
private func createTabTemplate(for track: Track) -> CPListTemplate {
|
|
578
634
|
// The path stored on the template drives lazy loading and refresh.
|
|
579
|
-
let template = makeLoadingTemplate(title: track.title, path: track.
|
|
635
|
+
let template = makeLoadingTemplate(title: track.title, path: track.path)
|
|
580
636
|
applyTabBarEntry(to: template, for: track)
|
|
581
637
|
return template
|
|
582
638
|
}
|
|
@@ -682,14 +738,13 @@ public final class RNABCarPlayController: NSObject {
|
|
|
682
738
|
// even with no rows — the "Ask Siri to Play" cell IS its content — so it
|
|
683
739
|
// renders rather than falling through to the empty-content state.
|
|
684
740
|
let hasAssistantCell = resolved.carPlaySiriListButton != nil
|
|
685
|
-
|
|
741
|
+
// A page of empty sections is still empty.
|
|
742
|
+
let hasChildren = resolved.normalizedSections?.contains { !$0.children.isEmpty } ?? false
|
|
743
|
+
if !hasChildren, !hasAssistantCell {
|
|
686
744
|
// Empty is modeled as a navigation error (code .emptyContent) so it goes
|
|
687
745
|
// through the same path-aware formatter as failures — letting an app give
|
|
688
746
|
// an empty Favorites tab different copy than an empty search. ADR 0001.
|
|
689
|
-
|
|
690
|
-
code: .emptyContent, message: "", statusCode: nil, statusCodeSuccess: nil,
|
|
691
|
-
)
|
|
692
|
-
await showNavigationErrorView(empty, path: path, on: template)
|
|
747
|
+
await showNavigationErrorView(.code(.emptyContent), path: path, on: template)
|
|
693
748
|
} else {
|
|
694
749
|
updateTemplate(template, with: resolved)
|
|
695
750
|
}
|
|
@@ -749,7 +804,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
749
804
|
}
|
|
750
805
|
|
|
751
806
|
// If this track is already loaded, resume playback and show Now Playing.
|
|
752
|
-
if track.src != nil, isActiveTrack(
|
|
807
|
+
if track.src != nil, isActiveTrack(identity: track.identity) {
|
|
753
808
|
try? audioBrowser.play()
|
|
754
809
|
nowPlayingManager.showNowPlaying()
|
|
755
810
|
completion()
|
|
@@ -777,7 +832,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
777
832
|
|
|
778
833
|
// Release CarPlay immediately so the list never locks up. Apple's handler
|
|
779
834
|
// guidance is to finish processing the tap promptly; for a browse we "finish"
|
|
780
|
-
// by pushing the destination and filling it in (see
|
|
835
|
+
// by pushing the destination and filling it in (see navigateToPath), and for
|
|
781
836
|
// playback the Now Playing surface owns its own loading state.
|
|
782
837
|
completion()
|
|
783
838
|
|
|
@@ -790,8 +845,8 @@ public final class RNABCarPlayController: NSObject {
|
|
|
790
845
|
self.nowPlayingManager.showNowPlaying(popToFront: false)
|
|
791
846
|
case .intercepted:
|
|
792
847
|
self.nowPlayingManager.showNowPlaying(popToFront: false)
|
|
793
|
-
case let .browse(
|
|
794
|
-
await self.
|
|
848
|
+
case let .browse(path):
|
|
849
|
+
await self.navigateToPath(path, title: track.title)
|
|
795
850
|
case .none:
|
|
796
851
|
break
|
|
797
852
|
}
|
|
@@ -809,53 +864,93 @@ public final class RNABCarPlayController: NSObject {
|
|
|
809
864
|
}
|
|
810
865
|
}
|
|
811
866
|
|
|
812
|
-
/// Pushes a browsable
|
|
867
|
+
/// Pushes a browsable path's destination immediately as an empty, spinning list
|
|
813
868
|
/// template — so the list the user tapped from is never blocked. The content is
|
|
814
869
|
/// filled by `templateDidAppear` → `loadContentIfNeeded` → `loadContent`, which
|
|
815
870
|
/// runs once the template is on screen (the timing CarPlay needs: updates made
|
|
816
871
|
/// right after a push are dropped). Backing out and re-tapping retries.
|
|
872
|
+
///
|
|
873
|
+
/// One exception to pushing: a path that IS a tab selects that tab (see
|
|
874
|
+
/// `selectTab`).
|
|
817
875
|
@MainActor
|
|
818
|
-
private func
|
|
876
|
+
private func navigateToPath(_ path: String, title: String) async {
|
|
819
877
|
// The gate is resolved per-request: a gated path pushes the gate page
|
|
820
878
|
// (with this request's chrome) instead of browsing into content; this also
|
|
821
879
|
// blocks indirect entries like the Now Playing album line. An allowed path
|
|
822
880
|
// browses normally even while a gate is active.
|
|
823
881
|
if isGated, let audioBrowser {
|
|
824
882
|
let outcome = await audioBrowser.gateDecision(
|
|
825
|
-
for: NativeGateRequest(reason: .browse, path:
|
|
883
|
+
for: NativeGateRequest(reason: .browse, path: path, search: nil),
|
|
826
884
|
)
|
|
827
885
|
if outcome.gated {
|
|
828
886
|
audioBrowser.onGate(GateEvent(reason: .browse))
|
|
829
|
-
if let top = interfaceController.topTemplate, getPath(from: top) ==
|
|
887
|
+
if let top = interfaceController.topTemplate, getPath(from: top) == path { return }
|
|
830
888
|
interfaceController.safePush(
|
|
831
889
|
makeGateTemplate(gate: outcome.chrome, tab: nil), animated: true,
|
|
832
890
|
)
|
|
833
891
|
return
|
|
834
892
|
}
|
|
835
893
|
}
|
|
894
|
+
// A link to a tab's own path switches to that tab instead of pushing a
|
|
895
|
+
// second copy of it into the current tab's stack.
|
|
896
|
+
if selectTab(owning: path) { return }
|
|
836
897
|
// Avoid pushing a duplicate if the top template already shows this path.
|
|
837
|
-
if let top = interfaceController.topTemplate, getPath(from: top) ==
|
|
898
|
+
if let top = interfaceController.topTemplate, getPath(from: top) == path {
|
|
838
899
|
return
|
|
839
900
|
}
|
|
840
901
|
// …and guard a rapid double-tap whose first push hasn't appeared yet (so the
|
|
841
902
|
// check above can't see it). Cleared when the pushed template appears.
|
|
842
|
-
guard !navigatingPaths.contains(
|
|
843
|
-
navigatingPaths.insert(
|
|
903
|
+
guard !navigatingPaths.contains(path) else { return }
|
|
904
|
+
navigatingPaths.insert(path)
|
|
844
905
|
|
|
845
|
-
let template = makeLoadingTemplate(title: title, path:
|
|
906
|
+
let template = makeLoadingTemplate(title: title, path: path)
|
|
846
907
|
interfaceController.safePush(template, animated: true) { [weak self] pushed, error in
|
|
847
908
|
guard !pushed else { return }
|
|
848
909
|
// The push can fail (e.g. CarPlay's template stack depth limit). The
|
|
849
910
|
// appear callback that normally clears the guard will never fire, so
|
|
850
911
|
// clear it here — otherwise the destination stays unreachable until
|
|
851
912
|
// CarPlay reconnects.
|
|
852
|
-
self?.navigatingPaths.remove(
|
|
913
|
+
self?.navigatingPaths.remove(path)
|
|
853
914
|
if let error {
|
|
854
|
-
self?.logger.error("pushTemplate failed for \(
|
|
915
|
+
self?.logger.error("pushTemplate failed for \(path): \(error.localizedDescription)")
|
|
855
916
|
}
|
|
856
917
|
}
|
|
857
918
|
}
|
|
858
919
|
|
|
920
|
+
/// Switches to the tab that owns `path`, if any — a "view all" header or row
|
|
921
|
+
/// pointing at a tab's own path belongs in the tab bar, not pushed on top of
|
|
922
|
+
/// whichever tab the user tapped from (which would strand them one level deep
|
|
923
|
+
/// in a list the bar already offers, with a Back button to a page they never
|
|
924
|
+
/// came from). Any pushed stack is popped first, so the tab opens at its root.
|
|
925
|
+
///
|
|
926
|
+
/// Returns false — and the caller pushes as usual — when `path` is no tab,
|
|
927
|
+
/// when the root isn't the tab bar (the gate replaces it), or before iOS 17,
|
|
928
|
+
/// which has no tab-selection API.
|
|
929
|
+
@MainActor
|
|
930
|
+
private func selectTab(owning path: String) -> Bool {
|
|
931
|
+
guard #available(iOS 17.0, *),
|
|
932
|
+
let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate,
|
|
933
|
+
let tab = tabBar.templates.first(where: { getPath(from: $0) == path })
|
|
934
|
+
else { return false }
|
|
935
|
+
let isPushed = interfaceController.templates.count > 1
|
|
936
|
+
// Already showing: the destination is where the user asked to be, so this
|
|
937
|
+
// is handled — just don't animate a pop-and-select over it.
|
|
938
|
+
if tabBar.selectedTemplate === tab, !isPushed { return true }
|
|
939
|
+
logger.debug("Navigating to tab \(path) instead of pushing it")
|
|
940
|
+
guard isPushed else {
|
|
941
|
+
tabBar.select(tab)
|
|
942
|
+
return true
|
|
943
|
+
}
|
|
944
|
+
interfaceController.safePopToRoot(animated: true) { popped, _ in
|
|
945
|
+
// Selecting after a failed pop would leave the pushed stack over the
|
|
946
|
+
// newly selected tab; the caller has already returned, so a failure just
|
|
947
|
+
// leaves the user where they were.
|
|
948
|
+
guard popped else { return }
|
|
949
|
+
tabBar.select(tab)
|
|
950
|
+
}
|
|
951
|
+
return true
|
|
952
|
+
}
|
|
953
|
+
|
|
859
954
|
/// Builds an empty list template for content that is still resolving.
|
|
860
955
|
/// On iOS 18.4+ it shows the system spinner (`showsSpinnerWhileEmpty`). On older
|
|
861
956
|
/// iOS it shows the app-localized `carPlayLoadingTitle` as the centered empty
|
|
@@ -930,16 +1025,18 @@ public final class RNABCarPlayController: NSObject {
|
|
|
930
1025
|
@MainActor
|
|
931
1026
|
private func handleTabsChanged(_ tabs: [Track]) {
|
|
932
1027
|
logger.debug("Tabs changed: \(tabs.count) tabs")
|
|
933
|
-
//
|
|
1028
|
+
// Compare against the list showTabBar builds templates from, not the raw one.
|
|
1029
|
+
let visibleTabs = Self.visibleTabs(tabs)
|
|
1030
|
+
// Same tabs by path while ungated → only the tab-bar entries changed
|
|
934
1031
|
// (e.g. titles after a locale switch): restamp them in place. No rebuild,
|
|
935
1032
|
// so the selected tab and any pushed navigation stack survive. (Gate
|
|
936
1033
|
// templates carry no path, so a gated tab bar never matches here.)
|
|
937
1034
|
if !isGated,
|
|
938
1035
|
let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate,
|
|
939
|
-
tabBar.templates.count == min(
|
|
940
|
-
zip(tabBar.templates,
|
|
1036
|
+
tabBar.templates.count == min(visibleTabs.count, CPTabBarTemplate.maximumTabCount),
|
|
1037
|
+
zip(tabBar.templates, visibleTabs).allSatisfy({ getPath(from: $0) == $1.path })
|
|
941
1038
|
{
|
|
942
|
-
for (template, tab) in zip(tabBar.templates,
|
|
1039
|
+
for (template, tab) in zip(tabBar.templates, visibleTabs) {
|
|
943
1040
|
applyTabBarEntry(to: template, for: tab)
|
|
944
1041
|
}
|
|
945
1042
|
pendingTabs = nil
|
|
@@ -978,7 +1075,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
978
1075
|
// This callback fires when the main browser's content changes.
|
|
979
1076
|
// For CarPlay-specific refreshes (e.g., favorites), use notifyContentChanged instead.
|
|
980
1077
|
guard let content else { return }
|
|
981
|
-
refreshTemplatesForPath(content.
|
|
1078
|
+
refreshTemplatesForPath(content.path, with: content)
|
|
982
1079
|
}
|
|
983
1080
|
|
|
984
1081
|
@MainActor
|
|
@@ -990,9 +1087,11 @@ public final class RNABCarPlayController: NSObject {
|
|
|
990
1087
|
nowPlayingManager.handleActiveTrackChanged()
|
|
991
1088
|
}
|
|
992
1089
|
|
|
993
|
-
/// Updates the isPlaying state
|
|
1090
|
+
/// Updates the isPlaying state (and indicator location, which options can
|
|
1091
|
+
/// change at runtime) on all list items based on the current active track.
|
|
994
1092
|
@MainActor
|
|
995
1093
|
fileprivate func updatePlayingIndicators() {
|
|
1094
|
+
let location = playingIndicatorLocation
|
|
996
1095
|
var templates: [CPListTemplate] = []
|
|
997
1096
|
|
|
998
1097
|
if let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate {
|
|
@@ -1015,16 +1114,20 @@ public final class RNABCarPlayController: NSObject {
|
|
|
1015
1114
|
for item in section.items {
|
|
1016
1115
|
guard let listItem = item as? CPListItem,
|
|
1017
1116
|
let info = listItem.carPlayItemInfo,
|
|
1018
|
-
info.
|
|
1117
|
+
info.identity != nil
|
|
1019
1118
|
else { continue }
|
|
1020
1119
|
|
|
1021
|
-
let isPlaying = isActiveTrack(
|
|
1120
|
+
let isPlaying = isActiveTrack(identity: info.identity)
|
|
1022
1121
|
if listItem.isPlaying != isPlaying {
|
|
1023
1122
|
logger.debug(
|
|
1024
|
-
"Updating isPlaying for \(info.
|
|
1123
|
+
"Updating isPlaying for \(info.identity ?? ""): \(listItem.isPlaying) → \(isPlaying)")
|
|
1025
1124
|
listItem.isPlaying = isPlaying
|
|
1026
1125
|
templateChanged = true
|
|
1027
1126
|
}
|
|
1127
|
+
if listItem.playingIndicatorLocation != location {
|
|
1128
|
+
listItem.playingIndicatorLocation = location
|
|
1129
|
+
templateChanged = true
|
|
1130
|
+
}
|
|
1028
1131
|
}
|
|
1029
1132
|
}
|
|
1030
1133
|
// Setting isPlaying on an already-displayed item does not reliably reach
|
|
@@ -1215,15 +1318,29 @@ public final class RNABCarPlayController: NSObject {
|
|
|
1215
1318
|
logger.debug("Calling formatNavigationError callback...")
|
|
1216
1319
|
// Call the JS callback and handle result
|
|
1217
1320
|
let params = FormatNavigationErrorParams(error: error, defaultFormatted: defaultFormatted, path: path)
|
|
1321
|
+
// The result callbacks are @Sendable and hop back explicitly: Nitro resolves
|
|
1322
|
+
// its promises synchronously on the JS thread, so a plain closure would
|
|
1323
|
+
// inherit this type's MainActor isolation and trap on entry under Swift 6's
|
|
1324
|
+
// dynamic isolation check — and would otherwise drive UIKit off the main
|
|
1325
|
+
// thread. Same pattern as HybridAudioBrowser.handleNavigationError and
|
|
1326
|
+
// NowPlayingUpdater.render.
|
|
1218
1327
|
formatter(params)
|
|
1219
|
-
.then { [weak self] customDisplay in
|
|
1220
|
-
|
|
1221
|
-
|
|
1328
|
+
.then { @Sendable [weak self] customDisplay in
|
|
1329
|
+
let description = String(describing: customDisplay)
|
|
1330
|
+
Task { @MainActor in
|
|
1331
|
+
guard let self else { return }
|
|
1332
|
+
self.logger.debug("formatNavigationError returned: \(description)")
|
|
1333
|
+
self.presentErrorActionSheet(customDisplay: customDisplay ?? defaultFormatted)
|
|
1334
|
+
}
|
|
1222
1335
|
}
|
|
1223
|
-
.catch { [weak self] callbackError in
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1336
|
+
.catch { @Sendable [weak self] callbackError in
|
|
1337
|
+
let message = callbackError.localizedDescription
|
|
1338
|
+
Task { @MainActor in
|
|
1339
|
+
guard let self else { return }
|
|
1340
|
+
self.logger.error("formatNavigationError failed: \(message)")
|
|
1341
|
+
// On error, fall back to defaults
|
|
1342
|
+
self.presentErrorActionSheet(customDisplay: defaultFormatted)
|
|
1343
|
+
}
|
|
1227
1344
|
}
|
|
1228
1345
|
} else {
|
|
1229
1346
|
presentErrorActionSheet(customDisplay: defaultFormatted)
|
|
@@ -1264,9 +1381,22 @@ public final class RNABCarPlayController: NSObject {
|
|
|
1264
1381
|
/// view formatted via the app's `formatNavigationError` (path "/"), like every
|
|
1265
1382
|
/// other browse failure. The empty view renders reliably here because it's the
|
|
1266
1383
|
/// template's initial state at set-root time.
|
|
1384
|
+
///
|
|
1385
|
+
/// Formatting awaits the app's `formatNavigationError` callback, so a tab-bar
|
|
1386
|
+
/// build can start and paint during the hop — this error root must not
|
|
1387
|
+
/// clobber it. Captured here rather than passed in: the `showTabBar` /
|
|
1388
|
+
/// `showGatedTabBar` callers hold the newest generation when they call, so
|
|
1389
|
+
/// capturing is equivalent for them and covers the others for free.
|
|
1390
|
+
///
|
|
1391
|
+
/// Not airtight for `buildInitialInterface`'s two callers: that function
|
|
1392
|
+
/// doesn't participate in `gateBuildGeneration` and suspends at `queryTabs()`,
|
|
1393
|
+
/// so a build that superseded it *before* this was entered isn't caught.
|
|
1394
|
+
/// Closing that needs a generation captured at `buildInitialInterface` entry.
|
|
1267
1395
|
@MainActor
|
|
1268
1396
|
private func showRootNavigationError(_ navError: NavigationError) async {
|
|
1397
|
+
let generation = gateBuildGeneration
|
|
1269
1398
|
let formatted = await formattedNavigationError(navError, path: "/")
|
|
1399
|
+
guard gateBuildGeneration == generation else { return }
|
|
1270
1400
|
let template = CPListTemplate(title: nil, sections: [])
|
|
1271
1401
|
template.emptyViewTitleVariants = [formatted.title]
|
|
1272
1402
|
template.emptyViewSubtitleVariants = formatted.message.flatMap { $0.isEmpty ? nil : [$0] } ?? []
|
|
@@ -1336,20 +1466,32 @@ private extension RNABCarPlayController {
|
|
|
1336
1466
|
}
|
|
1337
1467
|
}
|
|
1338
1468
|
|
|
1469
|
+
// MARK: - NavigationError Shorthand
|
|
1470
|
+
|
|
1471
|
+
extension NavigationError {
|
|
1472
|
+
/// A code-only error: the user-facing copy comes from the app's
|
|
1473
|
+
/// formatNavigationError (ADR 0001), so no message travels with it.
|
|
1474
|
+
static func code(_ code: NavigationErrorType) -> NavigationError {
|
|
1475
|
+
NavigationError(code: code, message: "", statusCode: nil, statusCodeSuccess: nil)
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1339
1479
|
// MARK: - UIImage Resize
|
|
1340
1480
|
|
|
1341
1481
|
extension UIImage {
|
|
1342
|
-
/// Draws the image centered within the target size, maintaining aspect
|
|
1343
|
-
|
|
1344
|
-
|
|
1482
|
+
/// Draws the image centered within the target size, maintaining aspect
|
|
1483
|
+
/// ratio, at the given display scale (the car screen's, not the phone's).
|
|
1484
|
+
func resized(to targetSize: CGSize, scale: CGFloat) -> UIImage? {
|
|
1485
|
+
UIGraphicsBeginImageContextWithOptions(targetSize, false, scale)
|
|
1345
1486
|
defer { UIGraphicsEndImageContext() }
|
|
1346
1487
|
|
|
1347
|
-
// Scale to fit while maintaining aspect ratio
|
|
1488
|
+
// Scale to fit while maintaining aspect ratio. Named `fitScale` so it does
|
|
1489
|
+
// not shadow the `scale` parameter (the display scale) it sits next to.
|
|
1348
1490
|
let widthRatio = targetSize.width / size.width
|
|
1349
1491
|
let heightRatio = targetSize.height / size.height
|
|
1350
|
-
let
|
|
1492
|
+
let fitScale = min(widthRatio, heightRatio)
|
|
1351
1493
|
|
|
1352
|
-
let scaledSize = CGSize(width: size.width *
|
|
1494
|
+
let scaledSize = CGSize(width: size.width * fitScale, height: size.height * fitScale)
|
|
1353
1495
|
let origin = CGPoint(
|
|
1354
1496
|
x: (targetSize.width - scaledSize.width) / 2,
|
|
1355
1497
|
y: (targetSize.height - scaledSize.height) / 2,
|