react-native-audio-browser 0.2.0-next.3 → 0.2.0-next.31
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 +36 -22
- 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 +541 -278
- 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 +52 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +27 -9
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +288 -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 +62 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +110 -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 +100 -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 +40 -2
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +301 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +168 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +160 -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 +55 -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/AVPlayerEngine.swift +330 -0
- package/ios/AVPlayerEngineCallbacks.swift +15 -0
- package/ios/Browser/BrowseGate.swift +58 -0
- package/ios/Browser/BrowserConfig.swift +117 -78
- package/ios/Browser/BrowserManager.swift +209 -603
- package/ios/Browser/BrowserPathHelper.swift +50 -46
- package/ios/Browser/InertStyleDiagnostic.swift +296 -0
- package/ios/Browser/JsonModels.swift +264 -106
- package/ios/Browser/NavigationErrorState.swift +41 -0
- package/ios/Browser/PageResolver.swift +227 -0
- package/ios/Browser/RequestLayer.swift +90 -0
- package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +193 -121
- package/ios/Browser/SearchDrillIn.swift +7 -7
- 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/CPInterfaceController+Safe.swift +100 -43
- package/ios/CarPlay/CarPlayArtworkResolver.swift +36 -83
- package/ios/CarPlay/CarPlayController.swift +42 -1296
- package/ios/CarPlay/CarPlayImageLoader.swift +78 -97
- package/ios/CarPlay/CarPlayListItemFactory.swift +303 -92
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +129 -136
- package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
- package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
- package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
- package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
- package/ios/CarPlay/CarPlaySession.swift +209 -0
- package/ios/CarPlay/CarPlayTemplates.swift +63 -0
- package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
- package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +17 -9
- package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
- package/ios/CarPlay/RNABMediaAddHandler.swift +8 -24
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +7 -23
- package/ios/CarPlay/RNABMediaIntentHandler.swift +18 -19
- package/ios/CarPlay/SectionPresentation.swift +77 -0
- package/ios/CarPlay/TopTemplateReplacer.swift +96 -0
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/NavigationError+Conversion.swift +14 -4
- 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/Extension/UIImage+Adaptive.swift +12 -0
- package/ios/Extension/URLError+Connectivity.swift +15 -0
- package/ios/Http/HttpClient.swift +66 -142
- package/ios/HybridAudioBrowser+Browser.swift +153 -0
- package/ios/HybridAudioBrowser+Options.swift +144 -0
- package/ios/HybridAudioBrowser+Player.swift +397 -0
- package/ios/HybridAudioBrowser+PlayerCallbacks.swift +189 -0
- package/ios/HybridAudioBrowser+Stubs.swift +46 -0
- package/ios/HybridAudioBrowser.swift +153 -1610
- package/ios/Model/NitroTypeStubs.swift +146 -24
- package/ios/Model/PlayerUpdateOptions.swift +8 -1
- package/ios/Model/RemoteCommand.swift +18 -0
- package/ios/Model/TrackPlayerError.swift +2 -2
- package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
- package/ios/Observer/ItemObserver.swift +90 -0
- package/ios/Observer/PlayerObserver.swift +59 -0
- package/ios/Option/SessionCategories.swift +42 -92
- package/ios/Player/AudioSessionController.swift +189 -0
- package/ios/Player/DeferredSeek.swift +70 -0
- package/ios/Player/ErrorStatus.swift +27 -0
- package/ios/Player/MediaLoader.swift +62 -35
- package/ios/Player/NowPlayingUpdater.swift +197 -50
- package/ios/Player/PlaybackFailure.swift +125 -0
- package/ios/Player/PlaybackPersistence.swift +73 -0
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/Player.swift +1078 -0
- package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
- package/ios/Player/PlayerEngine.swift +87 -0
- package/ios/Player/PlayerStatusTypes.swift +2 -6
- package/ios/Player/QueueManager.swift +93 -67
- package/ios/Player/RetryLoop.swift +261 -0
- package/ios/Player/ShuffleOrder.swift +33 -0
- package/ios/Player/SleepTimerManager.swift +1 -1
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +46 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +87 -331
- package/ios/Tests/BrowserPathHelperTests.swift +56 -15
- package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -373
- package/ios/Tests/DeferredSeekTests.swift +70 -0
- package/ios/Tests/EmitterTests.swift +29 -0
- package/ios/Tests/GenerationMemoTests.swift +121 -0
- package/ios/Tests/InertStyleDiagnosticTests.swift +209 -0
- package/ios/Tests/JsonModelsTests.swift +164 -1
- package/ios/Tests/LRUCacheTests.swift +110 -0
- package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
- package/ios/Tests/MediaLoaderTests.swift +12 -48
- package/ios/Tests/ObserverTests.swift +88 -0
- package/ios/Tests/PlaybackErrorTests.swift +76 -0
- package/ios/Tests/PlaybackFailureTests.swift +189 -0
- package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
- package/ios/Tests/PlayerPolicyTests.swift +420 -0
- package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +300 -248
- package/ios/Tests/PlayingStateTests.swift +42 -0
- package/ios/Tests/QueueManagerTests.swift +196 -115
- package/ios/Tests/ReadyValueTests.swift +73 -0
- package/ios/Tests/RemoteCommandControllerTests.swift +152 -0
- package/ios/Tests/RemoteCommandTests.swift +65 -0
- package/ios/Tests/RequestLayerTests.swift +112 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
- package/ios/Tests/RetryLoopTests.swift +270 -0
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +98 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +132 -0
- package/ios/Tests/Support/TestTypes.swift +108 -104
- package/ios/Tests/TabBarEntriesTests.swift +19 -19
- package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
- package/ios/Tests/TopTemplateReplacerTests.swift +172 -0
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
- package/ios/TrackSelector.swift +46 -31
- package/ios/Util/ArtworkImageFetcher.swift +58 -1
- package/ios/Util/Emitter.swift +42 -0
- package/ios/Util/GenerationMemo.swift +36 -0
- package/ios/Util/LRUCache.swift +23 -159
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/ReadyValue.swift +56 -0
- package/ios/Util/SFSymbolRenderer.swift +59 -31
- package/ios/Util/SVGProcessor.swift +9 -12
- 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 +201 -76
- 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/PlayingStateFactory.js +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/NavigationErrorManager.js +3 -1
- package/lib/commonjs/web/browser/NavigationErrorManager.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 +62 -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 +201 -76
- 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/PlayingStateFactory.js +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/NavigationErrorManager.js +3 -1
- package/lib/module/web/browser/NavigationErrorManager.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 +62 -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 +387 -126
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +93 -61
- 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/resolveAssetSource.d.ts +1 -2
- package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
- 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 +4 -2
- package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +2 -2
- package/nitrogen/generated/android/c++/JAndroidAudioOffloadSettings.hpp +2 -2
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +12 -12
- package/nitrogen/generated/android/c++/JAndroidPlayerWakeMode.hpp +2 -2
- package/nitrogen/generated/android/c++/JAppKilledPlaybackBehavior.hpp +2 -2
- package/nitrogen/generated/android/c++/JArtworkRendering.hpp +58 -0
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +10 -5
- package/nitrogen/generated/android/c++/JArtworkVariants.hpp +2 -2
- package/nitrogen/generated/android/c++/JBatteryOptimizationStatus.hpp +2 -2
- package/nitrogen/generated/android/c++/JBatteryOptimizationStatusChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JBatteryWarningPendingChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JBrowseError.hpp +2 -2
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +19 -7
- package/nitrogen/generated/android/c++/JBrowserSourceCallbackParam.hpp +2 -2
- package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +2 -2
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JCarPlaySiriListButtonPosition.hpp +2 -2
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JChapterMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JEqualizerSettings.hpp +10 -10
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +10 -5
- package/nitrogen/generated/android/c++/JFormatNavigationErrorParams.hpp +11 -5
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +10 -5
- package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +10 -5
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.hpp +4 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +10 -5
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +10 -5
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +10 -5
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +18 -12
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +10 -4
- 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 +18 -6
- package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_BatteryOptimizationStatusChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_BatteryWarningPendingChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_EqualizerSettings.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +10 -5
- package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_NavigationErrorEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +9 -12
- package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +10 -5
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackPlayWhenReadyChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackProgressUpdatedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackQueueEndedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_PlayingState.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpBackwardEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpForwardEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RemotePlayIdEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RemotePlaySearchEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RemoteSeekEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RemoteSkipEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RepeatModeChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_TimedMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_TrackMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_FormattedNavigationError_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +17 -5
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_ChapterMetadata_.hpp +16 -16
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +24 -18
- package/nitrogen/generated/android/c++/JGate.hpp +2 -2
- package/nitrogen/generated/android/c++/JGateDecision.hpp +2 -2
- package/nitrogen/generated/android/c++/JGateEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JGateReason.hpp +2 -2
- package/nitrogen/generated/android/c++/JGridTile.hpp +64 -0
- package/nitrogen/generated/android/c++/JHttpMethod.hpp +2 -2
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +77 -53
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JIOSCategory.hpp +2 -2
- package/nitrogen/generated/android/c++/JIOSCategoryMode.hpp +2 -2
- package/nitrogen/generated/android/c++/JIOSCategoryOptions.hpp +2 -2
- package/nitrogen/generated/android/c++/JIOSCategoryPolicy.hpp +2 -2
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +17 -11
- package/nitrogen/generated/android/c++/JImageContext.hpp +2 -2
- package/nitrogen/generated/android/c++/JImageQueryParams.hpp +2 -2
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JImageSource.hpp +2 -2
- package/nitrogen/generated/android/c++/JMediaReference.hpp +2 -2
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +10 -5
- package/nitrogen/generated/android/c++/JMediaTransformParams.hpp +2 -2
- package/nitrogen/generated/android/c++/JNativeAndroidSetupOptions.hpp +2 -2
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +48 -21
- package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +2 -2
- package/nitrogen/generated/android/c++/JNativeIOSSetupOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +16 -4
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +17 -14
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +9 -12
- package/nitrogen/generated/android/c++/JNavigationError.hpp +2 -2
- package/nitrogen/generated/android/c++/JNavigationErrorEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JNavigationErrorKind.hpp +58 -0
- package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +2 -2
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +12 -12
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +17 -11
- package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +2 -2
- package/nitrogen/generated/android/c++/JOptions.hpp +9 -12
- package/nitrogen/generated/android/c++/JOutput.hpp +2 -2
- package/nitrogen/generated/android/c++/JOutputType.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlayback.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +10 -5
- package/nitrogen/generated/android/c++/JPlaybackError.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackPlayWhenReadyChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackProgressUpdatedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackQueueEndedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackState.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +7 -13
- package/nitrogen/generated/android/c++/JPlayingState.hpp +2 -2
- package/nitrogen/generated/android/c++/JProgress.hpp +2 -2
- 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++/JRemoteJumpBackwardEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRemoteJumpForwardEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRemotePlayIdEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRemotePlaySearchEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRemoteSeekEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRemoteSkipEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRepeatMode.hpp +2 -2
- package/nitrogen/generated/android/c++/JRepeatModeChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRequestConfig.hpp +2 -2
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +67 -59
- package/nitrogen/generated/android/c++/JRetryConfig.hpp +2 -2
- package/nitrogen/generated/android/c++/JSearchMode.hpp +2 -2
- package/nitrogen/generated/android/c++/JSearchParams.hpp +2 -2
- package/nitrogen/generated/android/c++/JSection.hpp +116 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
- package/nitrogen/generated/android/c++/JSleepTimer.hpp +4 -4
- package/nitrogen/generated/android/c++/JSleepTimerEndOfTrack.hpp +2 -2
- package/nitrogen/generated/android/c++/JSleepTimerTime.hpp +2 -2
- package/nitrogen/generated/android/c++/JStallReason.hpp +2 -2
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTimedMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JTrack.hpp +27 -48
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +18 -12
- package/nitrogen/generated/android/c++/JTrackMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JTrackRequest.hpp +2 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +47 -19
- package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +2 -2
- package/nitrogen/generated/android/c++/JVariant_Boolean_AndroidAudioOffloadSettings.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_Boolean_RetryConfig.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +3 -3
- 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 +20 -8
- package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioOffloadSettings.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +20 -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/ArtworkRequestConfig.kt +37 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryOptimizationStatusChangedEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryWarningPendingChangedEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseError.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseResult.kt +16 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowserSourceCallbackParam.kt +15 -0
- 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/ChapterMetadata.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/EqualizerSettings.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteChangedEvent.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNavigationErrorParams.kt +25 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryOptimizationStatusChangedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryWarningPendingChangedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_EqualizerSettings.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_FavoriteChangedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_GateEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NavigationErrorEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NowPlayingMetadata.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Options.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Output.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Playback.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackActiveTrackChangedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackErrorEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackPlayWhenReadyChangedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackProgressUpdatedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackQueueEndedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlayingState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpBackwardEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpForwardEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlayIdEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlaySearchEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSeekEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSkipEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RepeatModeChangedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TimedMetadata.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TrackMetadata.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_FormattedNavigationError_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_ResolvedTrack_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_ChapterMetadata_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_Track_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Gate.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateDecision.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +3 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +25 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageContext.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageQueryParams.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageSource.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaTransformParams.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeAndroidSetupOptions.kt +31 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +51 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeGateRequest.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeIOSSetupOptions.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +29 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationError.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorKind.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +20 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +25 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +29 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Output.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Playback.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackActiveTrackChangedEvent.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackPlayWhenReadyChangedEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackProgressUpdatedEvent.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackQueueEndedEvent.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +37 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayingState.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Progress.kt +17 -0
- 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 +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpBackwardEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpForwardEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlayIdEvent.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlaySearchEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSeekEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSkipEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RepeatModeChangedEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RequestConfig.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +67 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimer.kt +18 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerEndOfTrack.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerTime.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TimedMetadata.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +55 -12
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackLoadEvent.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackMetadata.kt +43 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +58 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +31 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_AndroidAudioOffloadSettings.kt +16 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_RetryConfig.kt +16 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_NullType_Double.kt +16 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +24 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +18 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +16 -7
- package/nitrogen/generated/ios/AudioBrowser+autolinking.rb +2 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +38 -29
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +779 -665
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +34 -16
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +36 -18
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/BrowseResult.swift +12 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/FormatNavigationErrorParams.swift +7 -2
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +48 -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/NavigationErrorKind.swift +40 -0
- 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/SleepTimer.swift +13 -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_Bool_AndroidAudioOffloadSettings.swift +12 -0
- package/nitrogen/generated/ios/swift/Variant_Bool_RetryConfig.swift +12 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +12 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_RemoteButtonLayout.swift +30 -0
- package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +13 -0
- package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +12 -0
- 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++/FormatNavigationErrorParams.hpp +9 -2
- package/nitrogen/generated/shared/c++/GridTile.hpp +84 -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++/NavigationErrorKind.hpp +76 -0
- 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 +403 -134
- package/src/types/browser.ts +97 -63
- 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 +240 -83
- 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/PlayingStateFactory.test.ts +1 -1
- package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
- 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/NavigationErrorManager.ts +3 -1
- 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 +68 -0
- package/src/web/util/BrowserPathHelper.ts +70 -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 +587 -0
- package/website/guide/carplay.md +650 -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/Bridge.h +0 -8
- package/ios/Browser/MediaResolveComposer.swift +0 -114
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/ios/Model/MediaURL.swift +0 -67
- package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
- package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
- package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
- package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
- package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
- package/ios/Observer/PlayerStateObserver.swift +0 -50
- package/ios/Observer/PlayerTimeObserver.swift +0 -55
- package/ios/Option/PitchAlgorithms.swift +0 -17
- package/ios/Option/TimeEventFrequency.swift +0 -18
- package/ios/Player/LoadSeekCoordinator.swift +0 -99
- package/ios/Player/MediaLoaderDelegate.swift +0 -11
- package/ios/Player/PlaybackCoordinator.swift +0 -754
- package/ios/Player/PlaybackEffectHandler.swift +0 -54
- package/ios/Player/PlaybackErrorHandler.swift +0 -175
- package/ios/Player/PlaybackStateStore.swift +0 -27
- package/ios/Player/PlayingStateManager.swift +0 -33
- package/ios/Player/RetryManager.swift +0 -322
- package/ios/Player/SeekCompletionHandler.swift +0 -3
- package/ios/Player/SleepTimerHandling.swift +0 -15
- package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
- package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -325
- package/ios/Tests/MediaResolveTests.swift +0 -126
- package/ios/Tests/OnceValueTests.swift +0 -79
- package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -547
- package/ios/Tests/PlaybackStateStoreTests.swift +0 -48
- package/ios/Tests/PlayingStateManagerTests.swift +0 -57
- package/ios/Tests/RetryManagerTests.swift +0 -89
- package/ios/TrackPlayer.swift +0 -1014
- package/ios/TrackPlayerCallbacks.swift +0 -86
- package/ios/Util/MetadataAdapter.swift +0 -131
- package/ios/Util/OnceValue.swift +0 -104
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JFavoritesMatchMode.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/FavoriteConfig.kt +0 -38
- 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/ios/swift/Variant_NullType_NotificationButtonLayout.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
|
@@ -1,134 +1,36 @@
|
|
|
1
1
|
import CarPlay
|
|
2
2
|
import Foundation
|
|
3
|
-
import NitroModules
|
|
4
3
|
import os.log
|
|
5
4
|
|
|
6
|
-
///
|
|
7
|
-
///
|
|
8
|
-
///
|
|
9
|
-
/// - Creates and manages CPTabBarTemplate from browser tabs
|
|
10
|
-
/// - Converts browser content to CPListTemplate for navigation
|
|
11
|
-
/// - Handles item selection for playback and navigation
|
|
12
|
-
/// - Delegates image loading to CarPlayImageLoader
|
|
13
|
-
/// - Delegates Now Playing management to CarPlayNowPlayingManager
|
|
14
|
-
///
|
|
15
|
-
/// This class is exposed to Objective-C for use by RNABCarPlaySceneDelegate.
|
|
5
|
+
/// The CarPlay scene's controller, exposed to Objective-C for `RNABCarPlaySceneDelegate`. It
|
|
6
|
+
/// shows a loading root until the player and a configured browser exist, then runs a
|
|
7
|
+
/// `CarPlaySession`; `stop()` ends it.
|
|
16
8
|
@MainActor
|
|
17
9
|
@objc(RNABCarPlayController)
|
|
18
10
|
public final class RNABCarPlayController: NSObject {
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
/// The Siri handlers `RNABAudioBrowser.m` looks up by name, referenced so a static link
|
|
12
|
+
/// keeps them.
|
|
13
|
+
@objc public static let siriIntentHandlerClasses: [AnyClass] = [
|
|
14
|
+
RNABMediaIntentHandler.self, RNABMediaAffinityHandler.self, RNABMediaAddHandler.self,
|
|
15
|
+
]
|
|
21
16
|
|
|
22
17
|
private let logger = Logger(subsystem: "com.audiobrowser", category: "CarPlayController")
|
|
23
|
-
|
|
24
18
|
private let interfaceController: CPInterfaceController
|
|
25
|
-
private weak var audioBrowser: HybridAudioBrowser?
|
|
26
|
-
private var trackSelector: TrackSelector?
|
|
27
|
-
|
|
28
|
-
/// Track content subscriptions
|
|
29
|
-
private var isStarted = false
|
|
30
|
-
private var listenerRemovals: [() -> Void] = []
|
|
31
|
-
|
|
32
|
-
/// Paths whose loading template has been pushed but not yet appeared. Guards a
|
|
33
|
-
/// rapid double-tap from pushing the same destination twice (the first push is
|
|
34
|
-
/// async, so `topTemplate` may not reflect it yet). Cleared when it appears.
|
|
35
|
-
private var navigatingPaths: Set<String> = []
|
|
36
|
-
|
|
37
|
-
/// Templates with a content load in flight. Guards a re-entrant load when a
|
|
38
|
-
/// still-loading template re-appears (back-and-forth navigation) from spawning
|
|
39
|
-
/// a second resolve + watchdog.
|
|
40
|
-
private var loadingTemplates: Set<ObjectIdentifier> = []
|
|
41
|
-
|
|
42
|
-
/// Tabs received while the user had templates pushed. Rebuilding the tab bar
|
|
43
|
-
/// replaces the root template, which tears down the pushed navigation stack —
|
|
44
|
-
/// so the rebuild is deferred until the user is back at the tab bar.
|
|
45
|
-
private var pendingTabs: [Track]?
|
|
46
|
-
|
|
47
|
-
/// Whether a gate is active. While set, tabs keep their tab-bar entries but
|
|
48
|
-
/// each path is resolved per-request via `gateDecision(for:)`: a gated path
|
|
49
|
-
/// renders the gate page instead of content; an allowed path shows real
|
|
50
|
-
/// content. Mirrors `audioBrowser.isGateActive`; seeded in start() so a gate
|
|
51
|
-
/// set before the scene connects renders at connect.
|
|
52
|
-
private var isGated = false
|
|
53
|
-
|
|
54
|
-
/// Drops a stale tab-bar build when a newer build supersedes it
|
|
55
|
-
/// (latest-build-wins). Shared across BOTH the gated and non-gated `showTabBar`
|
|
56
|
-
/// paths: each bumps and captures this at entry and bails before any
|
|
57
|
-
/// `setRootTemplate` / `updateTemplates` once it's been bumped past their
|
|
58
|
-
/// captured value. A build suspends the main actor at its `await` points
|
|
59
|
-
/// (per-tab `gateDecision` when gated, first-tab `loadContent` when not), so a
|
|
60
|
-
/// gate change can interleave a build of the opposite kind — most importantly a
|
|
61
|
-
/// `clearGate` kicking the non-gated build while a gated build is mid-await.
|
|
62
|
-
/// Covering both paths with one token means that clear-vs-gated interleave is
|
|
63
|
-
/// caught too (not just gated-vs-gated), so a stale gated build can't paint a
|
|
64
|
-
/// gate page over just-cleared content. Mirrors `albumArtistGeneration` in
|
|
65
|
-
/// `CarPlayNowPlayingManager`.
|
|
66
|
-
private var gateBuildGeneration: UInt = 0
|
|
67
|
-
|
|
68
|
-
/// How long a browse resolve may run before the destination's loading spinner
|
|
69
|
-
/// is replaced with an error state. The selection completion is fired
|
|
70
|
-
/// immediately (so CarPlay never blocks the list — per Apple's async handler
|
|
71
|
-
/// guidance we push the destination and fill it in), so this only bounds how
|
|
72
|
-
/// long the *destination screen* spins. Backing out and re-tapping retries.
|
|
73
|
-
private let resolveTimeout: Duration = .seconds(15)
|
|
74
|
-
|
|
75
|
-
/// Helper object for CPInterfaceControllerDelegate conformance
|
|
76
19
|
private var interfaceDelegate: InterfaceControllerDelegate?
|
|
20
|
+
private var instanceSubscription: Subscription?
|
|
77
21
|
|
|
78
|
-
///
|
|
79
|
-
private var
|
|
80
|
-
|
|
81
|
-
/// List item and section factory
|
|
82
|
-
private var listItemFactory: CarPlayListItemFactory?
|
|
83
|
-
|
|
84
|
-
/// Now Playing template and button management
|
|
85
|
-
private let nowPlayingManager: CarPlayNowPlayingManager
|
|
86
|
-
|
|
87
|
-
/// Convenience accessor for browser config
|
|
88
|
-
private var config: BrowserConfig {
|
|
89
|
-
audioBrowser?.browserManager.config ?? BrowserConfig()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/// Whether a list item identifies the currently active (loaded) track.
|
|
93
|
-
///
|
|
94
|
-
/// Identity is the stable `Track.id` when both sides carry one — the active
|
|
95
|
-
/// track may have been loaded outside the browse tree (the consumer's own
|
|
96
|
-
/// `load`/`setQueue`) with a `src` that differs textually from the browse
|
|
97
|
-
/// row's (absolute vs relative, extra query params) while being the same
|
|
98
|
-
/// item. Exact `src` equality remains the fallback for consumers that don't
|
|
99
|
-
/// assign ids.
|
|
100
|
-
private func isActiveTrack(id: String?, src: String?) -> Bool {
|
|
101
|
-
guard let currentTrack = audioBrowser?.getPlayer()?.currentTrack else { return false }
|
|
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
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// MARK: - Initialization
|
|
22
|
+
/// The pending or running start; nil while stopped.
|
|
23
|
+
private var run: Task<Void, Never>?
|
|
24
|
+
private(set) var session: CarPlaySession?
|
|
110
25
|
|
|
111
26
|
@objc
|
|
112
27
|
public init(interfaceController: CPInterfaceController) {
|
|
113
28
|
self.interfaceController = interfaceController
|
|
114
|
-
audioBrowser = HybridAudioBrowser.shared
|
|
115
|
-
nowPlayingManager = CarPlayNowPlayingManager(interfaceController: interfaceController)
|
|
116
29
|
super.init()
|
|
117
|
-
nowPlayingManager.listItemFactory = { [weak self] track, handler in
|
|
118
|
-
self?.listItemFactory?.createListItem(for: track, handler: handler)
|
|
119
|
-
?? CPListItem(text: track.title, detailText: nil)
|
|
120
|
-
}
|
|
121
|
-
nowPlayingManager.navigateToUrl = { [weak self] url, title in
|
|
122
|
-
Task { @MainActor in await self?.navigateToUrl(url, title: title) }
|
|
123
|
-
}
|
|
124
30
|
}
|
|
125
31
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
/// Reports CarPlay scene connection to the library (drives the JS-side
|
|
129
|
-
/// `isCarPlayConnected` / `onCarPlayConnectedChanged`). Called by
|
|
130
|
-
/// RNABCarPlaySceneDelegate on scene connect/disconnect — deliberately not
|
|
131
|
-
/// from start()/stop(), which also cycle on a JS runtime reload.
|
|
32
|
+
/// Reports the scene's connection (JS `isCarPlayConnected`). Called on scene connect and
|
|
33
|
+
/// disconnect, not from `start()`/`stop()`, which also cycle on a JS reload.
|
|
132
34
|
@objc
|
|
133
35
|
public static func setConnected(_ connected: Bool) {
|
|
134
36
|
HybridAudioBrowser.setCarPlayConnected(connected)
|
|
@@ -136,1147 +38,57 @@ public final class RNABCarPlayController: NSObject {
|
|
|
136
38
|
|
|
137
39
|
@objc
|
|
138
40
|
public func start() {
|
|
139
|
-
guard
|
|
140
|
-
isStarted = true
|
|
141
|
-
|
|
41
|
+
guard run == nil else { return }
|
|
142
42
|
logger.info("Starting CarPlay controller")
|
|
143
43
|
|
|
144
|
-
// Set up interface controller delegate for template lifecycle events
|
|
145
44
|
let delegate = InterfaceControllerDelegate(controller: self)
|
|
146
45
|
interfaceDelegate = delegate
|
|
147
46
|
interfaceController.delegate = delegate
|
|
148
47
|
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
let instanceToken = HybridAudioBrowser.instanceChangedEmitter.addListener { [weak self] _ in
|
|
153
|
-
Task { @MainActor in
|
|
154
|
-
self?.restart()
|
|
155
|
-
}
|
|
48
|
+
// A JS reload replaces the bridge, and with it every emitter the session listens to.
|
|
49
|
+
instanceSubscription = HybridAudioBrowser.instanceChangedEmitter.subscribeOnMain { [weak self] in
|
|
50
|
+
self?.restart()
|
|
156
51
|
}
|
|
157
|
-
listenerRemovals.append {
|
|
158
|
-
HybridAudioBrowser.instanceChangedEmitter.removeListener(instanceToken)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Show loading template while waiting
|
|
162
|
-
showLoadingTemplate()
|
|
163
52
|
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
self.isGated = browser.isGateActive
|
|
171
|
-
self.trackSelector = TrackSelector(browserManager: browser.browserManager)
|
|
172
|
-
|
|
173
|
-
// Create image loader with CarPlay display traits
|
|
174
|
-
self.imageLoader = CarPlayImageLoader(
|
|
175
|
-
carTraitCollection: self.interfaceController.carTraitCollection,
|
|
176
|
-
browserManager: browser.browserManager,
|
|
177
|
-
)
|
|
53
|
+
// Before the config loads, the app's loading title isn't known: the spinner only.
|
|
54
|
+
let browserManager = HybridAudioBrowser.shared?.browserManager
|
|
55
|
+
let loadingTitle = browserManager?.isConfigured == true ? browserManager?.config.carPlayLoadingTitle : nil
|
|
56
|
+
interfaceController.safeSetRoot(
|
|
57
|
+
CPListTemplate.loading(title: nil, path: nil, loadingTitle: loadingTitle), animated: false,
|
|
58
|
+
)
|
|
178
59
|
|
|
179
|
-
|
|
180
|
-
let
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
60
|
+
run = Task { [weak self] in
|
|
61
|
+
guard let (browser, _) = await playerAndConfiguredBrowser.wait(), !Task.isCancelled,
|
|
62
|
+
let self
|
|
63
|
+
else { return }
|
|
64
|
+
let session = CarPlaySession(
|
|
65
|
+
interfaceController: interfaceController, audioBrowser: browser,
|
|
185
66
|
)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
self.setupContentSubscriptions()
|
|
190
|
-
|
|
191
|
-
// Wire up now playing manager
|
|
192
|
-
self.nowPlayingManager.setup(audioBrowser: browser)
|
|
193
|
-
|
|
194
|
-
await self.buildInitialInterface()
|
|
67
|
+
self.session = session
|
|
68
|
+
session.start()
|
|
195
69
|
}
|
|
196
70
|
}
|
|
197
71
|
|
|
198
72
|
@objc
|
|
199
73
|
public func stop() {
|
|
200
|
-
guard
|
|
201
|
-
isStarted = false
|
|
202
|
-
|
|
74
|
+
guard run != nil else { return }
|
|
203
75
|
logger.info("Stopping CarPlay controller")
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
listenerRemovals.removeAll()
|
|
210
|
-
|
|
211
|
-
// Clear config callback
|
|
212
|
-
audioBrowser?.browserManager.onConfigChanged = nil
|
|
213
|
-
audioBrowser?.browserManager.onFavoritesChanged = nil
|
|
214
|
-
|
|
215
|
-
nowPlayingManager.teardown()
|
|
216
|
-
listItemFactory = nil
|
|
217
|
-
|
|
218
|
-
navigatingPaths.removeAll()
|
|
219
|
-
loadingTemplates.removeAll()
|
|
220
|
-
pendingTabs = nil
|
|
76
|
+
run?.cancel()
|
|
77
|
+
run = nil
|
|
78
|
+
session?.end()
|
|
79
|
+
session = nil
|
|
80
|
+
instanceSubscription = nil
|
|
221
81
|
}
|
|
222
82
|
|
|
223
|
-
/// Full stop/start cycle. Used when the JS runtime reloads: the new
|
|
224
|
-
/// HybridAudioBrowser instance replaces the emitters this controller is
|
|
225
|
-
/// subscribed to and resets the readiness gate, so everything must be
|
|
226
|
-
/// re-subscribed against the live instance (start() waits for it).
|
|
227
|
-
@MainActor
|
|
228
83
|
private func restart() {
|
|
229
|
-
guard
|
|
84
|
+
guard run != nil else { return }
|
|
230
85
|
logger.info("AudioBrowser instance changed — restarting CarPlay controller")
|
|
231
86
|
stop()
|
|
232
87
|
start()
|
|
233
88
|
}
|
|
234
|
-
|
|
235
|
-
// MARK: - Content Subscriptions
|
|
236
|
-
|
|
237
|
-
private func setupContentSubscriptions() {
|
|
238
|
-
guard let audioBrowser else {
|
|
239
|
-
logger.warning("AudioBrowser not available for CarPlay")
|
|
240
|
-
return
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Subscribe to tab changes
|
|
244
|
-
let tabsToken = audioBrowser.tabsChangedEmitter.addListener { [weak self] tabs in
|
|
245
|
-
Task { @MainActor in
|
|
246
|
-
self?.handleTabsChanged(tabs)
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
listenerRemovals.append { [weak audioBrowser] in
|
|
250
|
-
audioBrowser?.tabsChangedEmitter.removeListener(tabsToken)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Subscribe to content changes
|
|
254
|
-
let contentToken = audioBrowser.contentChangedEmitter.addListener { [weak self] content in
|
|
255
|
-
Task { @MainActor in
|
|
256
|
-
self?.handleContentChanged(content)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
listenerRemovals.append { [weak audioBrowser] in
|
|
260
|
-
audioBrowser?.contentChangedEmitter.removeListener(contentToken)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Subscribe to config changes (for Now Playing buttons and per-track
|
|
264
|
-
// button state — e.g. resolveAlbumUrl appearing/disappearing)
|
|
265
|
-
audioBrowser.browserManager.onConfigChanged = { [weak self] _ in
|
|
266
|
-
Task { @MainActor in
|
|
267
|
-
self?.nowPlayingManager.setupNowPlayingButtons()
|
|
268
|
-
self?.nowPlayingManager.updateNowPlayingButtonStates()
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Now-playing buttons + Up Next are runtime-updatable via updateOptions();
|
|
273
|
-
// refresh them when player options change (configureBrowser is no longer
|
|
274
|
-
// the only path that can change them).
|
|
275
|
-
let optionsToken = audioBrowser.playerOptionsChangedEmitter.addListener { [weak self] in
|
|
276
|
-
Task { @MainActor in
|
|
277
|
-
self?.nowPlayingManager.setupNowPlayingButtons()
|
|
278
|
-
self?.nowPlayingManager.updateNowPlayingButtonStates()
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
listenerRemovals.append { [weak audioBrowser] in
|
|
282
|
-
audioBrowser?.playerOptionsChangedEmitter.removeListener(optionsToken)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Refresh the Now Playing heart when favorites change externally (app /
|
|
286
|
-
// webview). The favorite/active-track emitters only fire for the player's
|
|
287
|
-
// own toggles, so without this an in-app favorite leaves the CarPlay heart
|
|
288
|
-
// stale.
|
|
289
|
-
audioBrowser.browserManager.onFavoritesChanged = { [weak self] in
|
|
290
|
-
Task { @MainActor in
|
|
291
|
-
self?.nowPlayingManager.updateFavoriteButtonState()
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Subscribe to favorite changes (for Now Playing button)
|
|
296
|
-
let favoriteToken = audioBrowser.favoriteChangedEmitter.addListener { [weak self] _ in
|
|
297
|
-
Task { @MainActor in
|
|
298
|
-
self?.nowPlayingManager.updateFavoriteButtonState()
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
listenerRemovals.append { [weak audioBrowser] in
|
|
302
|
-
audioBrowser?.favoriteChangedEmitter.removeListener(favoriteToken)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Repeat/shuffle mode changes re-stamp the now-playing buttons' serialized
|
|
306
|
-
// `isSelected` (a rebuild, skipped when unchanged) so CarPlay re-renders
|
|
307
|
-
// draw the correct selected background on the first frame — see
|
|
308
|
-
// CarPlayNowPlayingManager.BuiltButtonRow.
|
|
309
|
-
let repeatToken = audioBrowser.repeatModeChangedEmitter.addListener { [weak self] _ in
|
|
310
|
-
Task { @MainActor in
|
|
311
|
-
self?.nowPlayingManager.setupNowPlayingButtons()
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
listenerRemovals.append { [weak audioBrowser] in
|
|
315
|
-
audioBrowser?.repeatModeChangedEmitter.removeListener(repeatToken)
|
|
316
|
-
}
|
|
317
|
-
let shuffleToken = audioBrowser.shuffleChangedEmitter.addListener { [weak self] _ in
|
|
318
|
-
Task { @MainActor in
|
|
319
|
-
self?.nowPlayingManager.setupNowPlayingButtons()
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
listenerRemovals.append { [weak audioBrowser] in
|
|
323
|
-
audioBrowser?.shuffleChangedEmitter.removeListener(shuffleToken)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Subscribe to external content changes (notifyContentChanged /
|
|
327
|
-
// invalidateAllContent).
|
|
328
|
-
let externalContentToken = audioBrowser.externalContentChangedEmitter.addListener { [weak self] path in
|
|
329
|
-
// Hop to the main actor like the sibling listeners — the emitter runs
|
|
330
|
-
// listeners synchronously on the emitting (JS) thread.
|
|
331
|
-
Task { @MainActor in
|
|
332
|
-
if path == HybridAudioBrowser.invalidateAllSentinel {
|
|
333
|
-
self?.invalidateAllContent()
|
|
334
|
-
} else {
|
|
335
|
-
self?.notifyContentChanged(path: path)
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
listenerRemovals.append { [weak audioBrowser] in
|
|
340
|
-
audioBrowser?.externalContentChangedEmitter.removeListener(externalContentToken)
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Subscribe to gate changes (set / in-place update / clear)
|
|
344
|
-
let gateToken = audioBrowser.gateChangedEmitter.addListener { [weak self] active in
|
|
345
|
-
Task { @MainActor in
|
|
346
|
-
self?.handleGateChanged(active)
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
listenerRemovals.append { [weak audioBrowser] in
|
|
350
|
-
audioBrowser?.gateChangedEmitter.removeListener(gateToken)
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Subscribe to active track changes (for playing indicator in lists)
|
|
354
|
-
let activeTrackToken = audioBrowser.activeTrackChangedEmitter.addListener { [weak self] event in
|
|
355
|
-
Task { @MainActor in
|
|
356
|
-
self?.handleActiveTrackChanged(event)
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
listenerRemovals.append { [weak audioBrowser] in
|
|
360
|
-
audioBrowser?.activeTrackChangedEmitter.removeListener(activeTrackToken)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Subscribe to queue changes (for Up Next list updates)
|
|
364
|
-
let queueToken = audioBrowser.queueChangedEmitter.addListener { [weak self] tracks in
|
|
365
|
-
Task { @MainActor in
|
|
366
|
-
self?.nowPlayingManager.handleQueueChanged(tracks)
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
listenerRemovals.append { [weak audioBrowser] in
|
|
370
|
-
audioBrowser?.queueChangedEmitter.removeListener(queueToken)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// A voice media intent started playback — surface Now Playing so the user
|
|
374
|
-
// lands on the playing station (Up Next holds the rest of the results).
|
|
375
|
-
let showNowPlayingToken = audioBrowser.showNowPlayingRequestedEmitter.addListener { [weak self] _ in
|
|
376
|
-
Task { @MainActor in
|
|
377
|
-
self?.nowPlayingManager.showNowPlaying()
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
listenerRemovals.append { [weak audioBrowser] in
|
|
381
|
-
audioBrowser?.showNowPlayingRequestedEmitter.removeListener(showNowPlayingToken)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Subscribe to navigation errors (from browser layer)
|
|
385
|
-
let navErrorToken = audioBrowser.navigationErrorEmitter.addListener { [weak self] event in
|
|
386
|
-
Task { @MainActor in
|
|
387
|
-
self?.handleNavigationError(event)
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
listenerRemovals.append { [weak audioBrowser] in
|
|
391
|
-
audioBrowser?.navigationErrorEmitter.removeListener(navErrorToken)
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/// Handles navigation errors from the browser layer, displaying them in CarPlay
|
|
396
|
-
@MainActor
|
|
397
|
-
private func handleNavigationError(_ event: NavigationErrorEvent) {
|
|
398
|
-
guard let error = event.error else { return }
|
|
399
|
-
logger.warning("Navigation error: \(error.code.stringValue) - \(error.message)")
|
|
400
|
-
// Derive the current path from the live top template (authoritative) rather
|
|
401
|
-
// than a hand-maintained stack that goes stale on back navigation.
|
|
402
|
-
let path = interfaceController.topTemplate.flatMap { getPath(from: $0) } ?? "/"
|
|
403
|
-
showNavigationError(error, path: path)
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// MARK: - Initial Interface
|
|
407
|
-
|
|
408
|
-
@MainActor
|
|
409
|
-
private func buildInitialInterface() async {
|
|
410
|
-
guard let audioBrowser else {
|
|
411
|
-
logger.error("AudioBrowser not available")
|
|
412
|
-
await showRootNavigationError(
|
|
413
|
-
NavigationError(code: .unknownError, message: "", statusCode: nil, statusCodeSuccess: nil),
|
|
414
|
-
)
|
|
415
|
-
return
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Get tabs from browser manager
|
|
419
|
-
let tabs = audioBrowser.browserManager.getTabs()
|
|
420
|
-
|
|
421
|
-
if let tabs, !tabs.isEmpty {
|
|
422
|
-
await showTabBar(tabs: tabs)
|
|
423
|
-
} else {
|
|
424
|
-
// No tabs yet - query them
|
|
425
|
-
logger.info("No tabs available, querying...")
|
|
426
|
-
// Config exists by now (start() waited for it), so re-show the loading
|
|
427
|
-
// root to pick up the app's localized loading title — the first loading
|
|
428
|
-
// template was created before config was available.
|
|
429
|
-
if config.carPlayLoadingTitle != nil {
|
|
430
|
-
showLoadingTemplate()
|
|
431
|
-
}
|
|
432
|
-
do {
|
|
433
|
-
let queriedTabs = try await audioBrowser.browserManager.queryTabs()
|
|
434
|
-
if !queriedTabs.isEmpty {
|
|
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
|
-
}
|
|
443
|
-
} catch {
|
|
444
|
-
logger.error("Failed to query tabs: \(error.localizedDescription)")
|
|
445
|
-
await showRootNavigationError(NavigationError.from(error))
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/// Shows a loading template as root while waiting for initialization. Before
|
|
451
|
-
/// the browser is configured this can only show the spinner (iOS 18.4+) — the
|
|
452
|
-
/// app's `carPlayLoadingTitle` isn't known yet; `buildInitialInterface`
|
|
453
|
-
/// re-shows it once config is available.
|
|
454
|
-
private func showLoadingTemplate() {
|
|
455
|
-
let template = makeLoadingTemplate(title: nil, path: nil)
|
|
456
|
-
interfaceController.safeSetRoot(template, animated: false)
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// MARK: - Tab Bar
|
|
460
|
-
|
|
461
|
-
@MainActor
|
|
462
|
-
private func showTabBar(tabs: [Track]) async {
|
|
463
|
-
// Serialize concurrent tab-bar builds across BOTH the gated and non-gated
|
|
464
|
-
// paths with one shared generation token: bump at entry, capture, and bail
|
|
465
|
-
// before any template mutation once a newer build supersedes us. Both paths
|
|
466
|
-
// suspend the main actor at `await` points (per-tab `gateDecision` when
|
|
467
|
-
// gated, first-tab `loadContent` when not), so a gate change can interleave
|
|
468
|
-
// a build of the opposite kind — a `clearGate` kicking the non-gated build
|
|
469
|
-
// while a gated build is mid-await, or vice versa. A single token covers the
|
|
470
|
-
// clear-vs-gated interleave too, not just gated-vs-gated: the superseding
|
|
471
|
-
// build bumps it, and the in-flight build bails before painting (preventing
|
|
472
|
-
// a stale gate page over just-cleared content, or a clobbered root template).
|
|
473
|
-
gateBuildGeneration &+= 1
|
|
474
|
-
let generation = gateBuildGeneration
|
|
475
|
-
|
|
476
|
-
// While gated, each tab path is resolved per-request: a gated tab renders
|
|
477
|
-
// the gate page with that request's chrome; an allowed tab shows real
|
|
478
|
-
// content. This generalizes the old "all tabs show the gate" — a path the
|
|
479
|
-
// resolver allows now shows real content.
|
|
480
|
-
if isGated {
|
|
481
|
-
await showGatedTabBar(tabs: tabs, generation: generation)
|
|
482
|
-
return
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
logger.info("Building tab bar with \(tabs.count) tabs")
|
|
486
|
-
|
|
487
|
-
let maxTabs = CPTabBarTemplate.maximumTabCount
|
|
488
|
-
|
|
489
|
-
// Create tab templates synchronously (empty shells) - don't block on content loading
|
|
490
|
-
let tabTemplates: [CPListTemplate] = tabs.prefix(maxTabs).map { tab in
|
|
491
|
-
createTabTemplate(for: tab)
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// A newer build (e.g. a gate raised mid-clear) superseded us — don't clobber
|
|
495
|
-
// its root template.
|
|
496
|
-
guard gateBuildGeneration == generation else { return }
|
|
497
|
-
|
|
498
|
-
// Set the tab bar immediately so UI appears fast
|
|
499
|
-
logger.info("Setting tab bar root template with \(tabTemplates.count) templates")
|
|
500
|
-
let tabBar = CPTabBarTemplate(templates: tabTemplates)
|
|
501
|
-
interfaceController.safeSetRoot(tabBar, animated: true)
|
|
502
|
-
|
|
503
|
-
// Load content for the first tab only - others load lazily when selected
|
|
504
|
-
if let firstTemplate = tabTemplates.first, let firstTab = tabs.first, let url = firstTab.url {
|
|
505
|
-
await loadContent(for: url, into: firstTemplate)
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/// Builds the tab bar while a gate is active. Each tab's path is run through
|
|
510
|
-
/// the single gate choke point (`gateDecision(for:)`): a gated tab becomes a
|
|
511
|
-
/// gate page carrying that decision's chrome (and fires `onGate`); an allowed
|
|
512
|
-
/// tab becomes a normal content shell that lazy-loads. With a static gate (no
|
|
513
|
-
/// resolver) every tab is gated with the default chrome — today's behaviour.
|
|
514
|
-
///
|
|
515
|
-
/// `generation` is the shared `gateBuildGeneration` captured by the calling
|
|
516
|
-
/// `showTabBar`; we bail before any template mutation once a newer build (of
|
|
517
|
-
/// either kind) bumps it past `generation`.
|
|
518
|
-
@MainActor
|
|
519
|
-
private func showGatedTabBar(tabs: [Track], generation: UInt) async {
|
|
520
|
-
guard let audioBrowser else { return }
|
|
521
|
-
|
|
522
|
-
guard !tabs.isEmpty else {
|
|
523
|
-
// Tabs unknown (config not loaded yet, or none) — resolve the root path
|
|
524
|
-
// and show a single gate page (or, if allowed, fall back to a normal
|
|
525
|
-
// build once tabs arrive).
|
|
526
|
-
let outcome = await audioBrowser.gateDecision(
|
|
527
|
-
for: NativeGateRequest(reason: .browse, path: nil, search: nil),
|
|
528
|
-
)
|
|
529
|
-
guard gateBuildGeneration == generation else { return } // superseded by a newer build
|
|
530
|
-
if outcome.gated {
|
|
531
|
-
audioBrowser.onGate(GateEvent(reason: .browse))
|
|
532
|
-
interfaceController.safeSetRoot(
|
|
533
|
-
makeGateTemplate(gate: outcome.chrome, tab: nil), animated: true,
|
|
534
|
-
)
|
|
535
|
-
}
|
|
536
|
-
return
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
let limitedTabs = Array(tabs.prefix(CPTabBarTemplate.maximumTabCount))
|
|
540
|
-
var templates: [CPListTemplate] = []
|
|
541
|
-
for tab in limitedTabs {
|
|
542
|
-
let outcome = await audioBrowser.gateDecision(
|
|
543
|
-
for: NativeGateRequest(reason: .browse, path: tab.url, search: nil),
|
|
544
|
-
)
|
|
545
|
-
guard gateBuildGeneration == generation else { return } // superseded by a newer build
|
|
546
|
-
if outcome.gated {
|
|
547
|
-
audioBrowser.onGate(GateEvent(reason: .browse))
|
|
548
|
-
templates.append(makeGateTemplate(gate: outcome.chrome, tab: tab))
|
|
549
|
-
} else {
|
|
550
|
-
// This path is allowed even though a gate is active — show real content.
|
|
551
|
-
templates.append(createTabTemplate(for: tab))
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate,
|
|
556
|
-
tabBar.templates.count == templates.count
|
|
557
|
-
{
|
|
558
|
-
// Equal-count in-place swap keeps the selected tab index.
|
|
559
|
-
tabBar.updateTemplates(templates)
|
|
560
|
-
} else {
|
|
561
|
-
interfaceController.safeSetRoot(
|
|
562
|
-
CPTabBarTemplate(templates: templates), animated: true,
|
|
563
|
-
)
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// Eagerly fill any allowed (content) tabs: templateDidAppear isn't
|
|
567
|
-
// guaranteed to re-fire for templates swapped in via updateTemplates.
|
|
568
|
-
for (tab, template) in zip(limitedTabs, templates) {
|
|
569
|
-
guard getPath(from: template) != nil, let url = tab.url else { continue }
|
|
570
|
-
await loadContent(for: url, into: template)
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/// Creates a tab template shell without loading content (synchronous).
|
|
575
|
-
/// The shell carries the loading state (spinner / `carPlayLoadingTitle`)
|
|
576
|
-
/// until its content lazy-loads on first appearance.
|
|
577
|
-
private func createTabTemplate(for track: Track) -> CPListTemplate {
|
|
578
|
-
// The path stored on the template drives lazy loading and refresh.
|
|
579
|
-
let template = makeLoadingTemplate(title: track.title, path: track.url)
|
|
580
|
-
applyTabBarEntry(to: template, for: track)
|
|
581
|
-
return template
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/// Stamps a template's tab-bar entry: title and image (default, SF Symbol,
|
|
585
|
-
/// or loaded artwork). Shared by content tabs and gate-page tabs so a gate
|
|
586
|
-
/// keeps the familiar tab bar.
|
|
587
|
-
private func applyTabBarEntry(to template: CPTemplate, for track: Track) {
|
|
588
|
-
// Set tab title explicitly (required for tab bar display)
|
|
589
|
-
template.tabTitle = track.title
|
|
590
|
-
|
|
591
|
-
// Set tab image only when the tab supplies one (SF Symbol / artwork /
|
|
592
|
-
// artworkSource). A tab with no icon stays title-only — CPTemplate renders
|
|
593
|
-
// fine with just `tabTitle`. Tab bar icons are 24pt x 24pt per the CarPlay
|
|
594
|
-
// Developer Guide: https://developer.apple.com/download/files/CarPlay-Developer-Guide.pdf
|
|
595
|
-
if let artwork = track.artwork?.url, SFSymbolRenderer.isSFSymbol(artwork) {
|
|
596
|
-
let (symbolName, _, _) = SFSymbolRenderer.parseArtwork(artwork)
|
|
597
|
-
if let image = imageLoader?.sfSymbolImage(symbolName) {
|
|
598
|
-
template.tabImage = image
|
|
599
|
-
}
|
|
600
|
-
} else if track.artwork != nil || track.artworkSource != nil {
|
|
601
|
-
// loadArtwork handles both artwork and artworkSource
|
|
602
|
-
let tabImageSize = CGSize(width: 24, height: 24)
|
|
603
|
-
imageLoader?.loadArtwork(for: track, size: tabImageSize) { [weak template] image in
|
|
604
|
-
Task { @MainActor in
|
|
605
|
-
if let image {
|
|
606
|
-
template?.tabImage = image
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
} else {
|
|
611
|
-
// No icon supplied: clear any prior image so a restamp (handleTabsChanged)
|
|
612
|
-
// of an existing template doesn't leave a stale icon on a now title-only tab.
|
|
613
|
-
template.tabImage = nil
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// MARK: - List Templates
|
|
618
|
-
|
|
619
|
-
/// Finds the path associated with a template, if any
|
|
620
|
-
private func getPath(from template: CPTemplate) -> String? {
|
|
621
|
-
(template.userInfo as? [String: Any])?["path"] as? String
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/// Updates a template's sections and assistant cell from resolved content.
|
|
625
|
-
private func updateTemplate(_ template: CPListTemplate, with resolvedTrack: ResolvedTrack) {
|
|
626
|
-
let sections = listItemFactory?.createSections(from: resolvedTrack) ?? []
|
|
627
|
-
template.updateSections(sections)
|
|
628
|
-
configureAssistantCell(on: template, from: resolvedTrack)
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/// Configures the assistant cell ("Ask Siri to Play Audio") on a template
|
|
632
|
-
/// based on the `carPlaySiriListButton` property of the resolved content.
|
|
633
|
-
private func configureAssistantCell(on template: CPListTemplate, from resolvedTrack: ResolvedTrack) {
|
|
634
|
-
guard let position = resolvedTrack.carPlaySiriListButton else {
|
|
635
|
-
template.assistantCellConfiguration = nil
|
|
636
|
-
return
|
|
637
|
-
}
|
|
638
|
-
let cellPosition: CPListItem.AssistantCellPosition = position == .top ? .top : .bottom
|
|
639
|
-
template.assistantCellConfiguration = CPAssistantCellConfiguration(
|
|
640
|
-
position: cellPosition,
|
|
641
|
-
visibility: .always,
|
|
642
|
-
assistantAction: .playMedia,
|
|
643
|
-
)
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// MARK: - Content Loading
|
|
647
|
-
|
|
648
|
-
/// Resolves `path` and fills an empty (loading) template. Driven by
|
|
649
|
-
/// `templateDidAppear` → `loadContentIfNeeded`, so it runs once the template is
|
|
650
|
-
/// on screen — the timing where CarPlay actually applies updates (updates made
|
|
651
|
-
/// earlier, e.g. straight after push, are dropped). Shows a centered empty
|
|
652
|
-
/// state on empty/error/timeout.
|
|
653
|
-
@MainActor
|
|
654
|
-
private func loadContent(for path: String, into template: CPListTemplate) async {
|
|
655
|
-
guard let audioBrowser else { return }
|
|
656
|
-
|
|
657
|
-
// Single-flight: don't start a second load if one is already in flight for
|
|
658
|
-
// this template (e.g. the user backed out and into a still-loading screen).
|
|
659
|
-
let templateId = ObjectIdentifier(template)
|
|
660
|
-
guard !loadingTemplates.contains(templateId) else { return }
|
|
661
|
-
loadingTemplates.insert(templateId)
|
|
662
|
-
defer { loadingTemplates.remove(templateId) }
|
|
663
|
-
|
|
664
|
-
// Watchdog: if still loading (no rows) after the timeout, show an error.
|
|
665
|
-
let timeout = resolveTimeout
|
|
666
|
-
let watchdog = Task { @MainActor [weak self] in
|
|
667
|
-
do { try await Task.sleep(for: timeout) } catch { return } // cancelled → abort
|
|
668
|
-
guard let self, template.sections.isEmpty else { return }
|
|
669
|
-
self.logger.error("loadContent: resolve timed out for \(path)")
|
|
670
|
-
// A timeout is a navigation error (code .timeout) too, so it formats through
|
|
671
|
-
// the app's formatNavigationError like every other browse failure.
|
|
672
|
-
let timedOut = NavigationError(
|
|
673
|
-
code: .timeout, message: "", statusCode: nil, statusCodeSuccess: nil,
|
|
674
|
-
)
|
|
675
|
-
await self.showNavigationErrorView(timedOut, path: path, on: template)
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
do {
|
|
679
|
-
let resolved = try await audioBrowser.browserManager.resolve(path, useCache: true)
|
|
680
|
-
watchdog.cancel()
|
|
681
|
-
// A page carrying an assistant cell (carPlaySiriListButton) is meaningful
|
|
682
|
-
// even with no rows — the "Ask Siri to Play" cell IS its content — so it
|
|
683
|
-
// renders rather than falling through to the empty-content state.
|
|
684
|
-
let hasAssistantCell = resolved.carPlaySiriListButton != nil
|
|
685
|
-
if resolved.children?.isEmpty ?? true, !hasAssistantCell {
|
|
686
|
-
// Empty is modeled as a navigation error (code .emptyContent) so it goes
|
|
687
|
-
// through the same path-aware formatter as failures — letting an app give
|
|
688
|
-
// an empty Favorites tab different copy than an empty search. ADR 0001.
|
|
689
|
-
let empty = NavigationError(
|
|
690
|
-
code: .emptyContent, message: "", statusCode: nil, statusCodeSuccess: nil,
|
|
691
|
-
)
|
|
692
|
-
await showNavigationErrorView(empty, path: path, on: template)
|
|
693
|
-
} else {
|
|
694
|
-
updateTemplate(template, with: resolved)
|
|
695
|
-
}
|
|
696
|
-
} catch {
|
|
697
|
-
watchdog.cancel()
|
|
698
|
-
logger.error("Failed to load content for \(path): \(error.localizedDescription)")
|
|
699
|
-
await showNavigationErrorView(NavigationError.from(error), path: path, on: template)
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
/// Renders a navigation error as the template's centered empty/error view,
|
|
704
|
-
/// formatted via the app's `formatNavigationError` (or the built-in default):
|
|
705
|
-
/// `title → view title`, `message → subtitle`. Used for both real failures and
|
|
706
|
-
/// the empty-content case (ADR 0001).
|
|
707
|
-
@MainActor
|
|
708
|
-
private func showNavigationErrorView(
|
|
709
|
-
_ navError: NavigationError,
|
|
710
|
-
path: String,
|
|
711
|
-
on template: CPListTemplate,
|
|
712
|
-
) async {
|
|
713
|
-
let formatted = await formattedNavigationError(navError, path: path)
|
|
714
|
-
showMessage(
|
|
715
|
-
on: template,
|
|
716
|
-
title: formatted.title,
|
|
717
|
-
subtitle: formatted.message.flatMap { $0.isEmpty ? nil : $0 },
|
|
718
|
-
)
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
/// Resolves a navigation error to its display form via the app's
|
|
722
|
-
/// `formatNavigationError` callback, falling back to the default if unset/failed.
|
|
723
|
-
@MainActor
|
|
724
|
-
private func formattedNavigationError(
|
|
725
|
-
_ navError: NavigationError,
|
|
726
|
-
path: String,
|
|
727
|
-
) async -> FormattedNavigationError {
|
|
728
|
-
let fallback = navError.defaultFormatted()
|
|
729
|
-
guard let formatter = config.formatNavigationError else { return fallback }
|
|
730
|
-
let params = FormatNavigationErrorParams(
|
|
731
|
-
error: navError, defaultFormatted: fallback, path: path,
|
|
732
|
-
)
|
|
733
|
-
do {
|
|
734
|
-
let custom = try await formatter(params).await()
|
|
735
|
-
return custom ?? fallback
|
|
736
|
-
} catch {
|
|
737
|
-
return fallback
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// MARK: - Selection Handling
|
|
742
|
-
|
|
743
|
-
private func handleItemSelection(track: Track, completion: @escaping () -> Void) {
|
|
744
|
-
logger.info("Selected track: \(track.title)")
|
|
745
|
-
|
|
746
|
-
guard let audioBrowser else {
|
|
747
|
-
completion()
|
|
748
|
-
return
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// If this track is already loaded, resume playback and show Now Playing.
|
|
752
|
-
if track.src != nil, isActiveTrack(id: track.id, src: track.src) {
|
|
753
|
-
try? audioBrowser.play()
|
|
754
|
-
nowPlayingManager.showNowPlaying()
|
|
755
|
-
completion()
|
|
756
|
-
return
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
guard let player = audioBrowser.getPlayer(), let trackSelector else {
|
|
760
|
-
completion()
|
|
761
|
-
return
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// A playable track always lands on the Now Playing surface (selection
|
|
765
|
-
// resolves to .play or .intercepted, never .browse) — push it now and stamp
|
|
766
|
-
// the tapped track's metadata, instead of after selection resolves: queue
|
|
767
|
-
// expansion and the media URL resolve can both hit the network, and waiting
|
|
768
|
-
// animates in a blank Now Playing screen. The load pipeline re-publishes
|
|
769
|
-
// the same fields and dedupes.
|
|
770
|
-
if track.src != nil {
|
|
771
|
-
player.loadNowPlayingMetadata(for: track)
|
|
772
|
-
// Derive the album line's destination from the tapped track before the
|
|
773
|
-
// push — CarPlay reads isAlbumArtistButtonEnabled at display time.
|
|
774
|
-
nowPlayingManager.prepareAlbumArtistButton(for: track)
|
|
775
|
-
nowPlayingManager.showNowPlaying()
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// Release CarPlay immediately so the list never locks up. Apple's handler
|
|
779
|
-
// guidance is to finish processing the tap promptly; for a browse we "finish"
|
|
780
|
-
// by pushing the destination and filling it in (see navigateToUrl), and for
|
|
781
|
-
// playback the Now Playing surface owns its own loading state.
|
|
782
|
-
completion()
|
|
783
|
-
|
|
784
|
-
Task { [weak self] in
|
|
785
|
-
let result = await trackSelector.select(track: track, player: player)
|
|
786
|
-
guard let self else { return }
|
|
787
|
-
switch result {
|
|
788
|
-
case let .play(intent):
|
|
789
|
-
self.executePlayback(intent, player: player)
|
|
790
|
-
self.nowPlayingManager.showNowPlaying(popToFront: false)
|
|
791
|
-
case .intercepted:
|
|
792
|
-
self.nowPlayingManager.showNowPlaying(popToFront: false)
|
|
793
|
-
case let .browse(url):
|
|
794
|
-
await self.navigateToUrl(url, title: track.title)
|
|
795
|
-
case .none:
|
|
796
|
-
break
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
private func executePlayback(_ intent: TrackSelector.PlaybackIntent, player: TrackPlayer) {
|
|
802
|
-
switch intent {
|
|
803
|
-
case let .skipTo(index):
|
|
804
|
-
try? player.skipTo(index, playWhenReady: true)
|
|
805
|
-
case let .setQueue(tracks, startIndex, sourcePath):
|
|
806
|
-
player.setQueue(tracks, initialIndex: startIndex, playWhenReady: true, sourcePath: sourcePath)
|
|
807
|
-
case let .loadTrack(track):
|
|
808
|
-
player.load(track, playWhenReady: true)
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
/// Pushes a browsable URL's destination immediately as an empty, spinning list
|
|
813
|
-
/// template — so the list the user tapped from is never blocked. The content is
|
|
814
|
-
/// filled by `templateDidAppear` → `loadContentIfNeeded` → `loadContent`, which
|
|
815
|
-
/// runs once the template is on screen (the timing CarPlay needs: updates made
|
|
816
|
-
/// right after a push are dropped). Backing out and re-tapping retries.
|
|
817
|
-
@MainActor
|
|
818
|
-
private func navigateToUrl(_ url: String, title: String) async {
|
|
819
|
-
// The gate is resolved per-request: a gated path pushes the gate page
|
|
820
|
-
// (with this request's chrome) instead of browsing into content; this also
|
|
821
|
-
// blocks indirect entries like the Now Playing album line. An allowed path
|
|
822
|
-
// browses normally even while a gate is active.
|
|
823
|
-
if isGated, let audioBrowser {
|
|
824
|
-
let outcome = await audioBrowser.gateDecision(
|
|
825
|
-
for: NativeGateRequest(reason: .browse, path: url, search: nil),
|
|
826
|
-
)
|
|
827
|
-
if outcome.gated {
|
|
828
|
-
audioBrowser.onGate(GateEvent(reason: .browse))
|
|
829
|
-
if let top = interfaceController.topTemplate, getPath(from: top) == url { return }
|
|
830
|
-
interfaceController.safePush(
|
|
831
|
-
makeGateTemplate(gate: outcome.chrome, tab: nil), animated: true,
|
|
832
|
-
)
|
|
833
|
-
return
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
// Avoid pushing a duplicate if the top template already shows this path.
|
|
837
|
-
if let top = interfaceController.topTemplate, getPath(from: top) == url {
|
|
838
|
-
return
|
|
839
|
-
}
|
|
840
|
-
// …and guard a rapid double-tap whose first push hasn't appeared yet (so the
|
|
841
|
-
// check above can't see it). Cleared when the pushed template appears.
|
|
842
|
-
guard !navigatingPaths.contains(url) else { return }
|
|
843
|
-
navigatingPaths.insert(url)
|
|
844
|
-
|
|
845
|
-
let template = makeLoadingTemplate(title: title, path: url)
|
|
846
|
-
interfaceController.safePush(template, animated: true) { [weak self] pushed, error in
|
|
847
|
-
guard !pushed else { return }
|
|
848
|
-
// The push can fail (e.g. CarPlay's template stack depth limit). The
|
|
849
|
-
// appear callback that normally clears the guard will never fire, so
|
|
850
|
-
// clear it here — otherwise the destination stays unreachable until
|
|
851
|
-
// CarPlay reconnects.
|
|
852
|
-
self?.navigatingPaths.remove(url)
|
|
853
|
-
if let error {
|
|
854
|
-
self?.logger.error("pushTemplate failed for \(url): \(error.localizedDescription)")
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
/// Builds an empty list template for content that is still resolving.
|
|
860
|
-
/// On iOS 18.4+ it shows the system spinner (`showsSpinnerWhileEmpty`). On older
|
|
861
|
-
/// iOS it shows the app-localized `carPlayLoadingTitle` as the centered empty
|
|
862
|
-
/// state when configured; otherwise it stays blank rather than ship a
|
|
863
|
-
/// hardcoded, un-localized "Loading…". The empty view is set at creation —
|
|
864
|
-
/// the timing CarPlay renders reliably (see `replaceWithMessage`).
|
|
865
|
-
private func makeLoadingTemplate(title: String?, path: String?) -> CPListTemplate {
|
|
866
|
-
let template = CPListTemplate(title: title, sections: [])
|
|
867
|
-
if let path {
|
|
868
|
-
template.userInfo = ["path": path] as [String: Any]
|
|
869
|
-
}
|
|
870
|
-
if let loadingTitle = config.carPlayLoadingTitle {
|
|
871
|
-
// Subtitle slot on purpose: the title slot renders at CarPlay's large
|
|
872
|
-
// empty-state size, which is too heavy for a transient loading hint.
|
|
873
|
-
template.emptyViewSubtitleVariants = [loadingTitle]
|
|
874
|
-
}
|
|
875
|
-
if #available(iOS 18.4, *) {
|
|
876
|
-
template.showsSpinnerWhileEmpty = true
|
|
877
|
-
}
|
|
878
|
-
return template
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/// Shows a centered empty/error message on a loading template. A *pushed*
|
|
882
|
-
/// template is replaced (see `replaceWithMessage`); a *tab root* can't be
|
|
883
|
-
/// popped, so its empty view is set in place (best-effort — a tab renders its
|
|
884
|
-
/// empty view when selected).
|
|
885
|
-
private func showMessage(on template: CPListTemplate, title: String, subtitle: String?) {
|
|
886
|
-
if isTabRoot(template) {
|
|
887
|
-
if #available(iOS 18.4, *) {
|
|
888
|
-
template.showsSpinnerWhileEmpty = false
|
|
889
|
-
}
|
|
890
|
-
template.emptyViewTitleVariants = [title.isEmpty ? "Couldn't load" : title]
|
|
891
|
-
template.emptyViewSubtitleVariants = subtitle.map { [$0] } ?? []
|
|
892
|
-
template.updateSections([])
|
|
893
|
-
} else {
|
|
894
|
-
replaceWithMessage(template, title: title, subtitle: subtitle)
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
/// Whether `template` is a tab's root template (a child of the tab bar). Such
|
|
899
|
-
/// templates can't be popped, so the replace-by-pop strategy doesn't apply.
|
|
900
|
-
private func isTabRoot(_ template: CPTemplate) -> Bool {
|
|
901
|
-
guard let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate else { return false }
|
|
902
|
-
return tabBar.templates.contains { $0 === template }
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/// Replaces an on-screen loading template with a fresh list template showing a
|
|
906
|
-
/// centered empty-state message (empty result, or an error).
|
|
907
|
-
///
|
|
908
|
-
/// We *replace* rather than mutate because CarPlay reliably renders a list
|
|
909
|
-
/// template's empty view only as its **initial** state at push time — changing
|
|
910
|
-
/// `emptyViewTitleVariants` / `showsSpinnerWhileEmpty` on an already-pushed
|
|
911
|
-
/// template is unreliable (it renders late, or not at all). The replacement
|
|
912
|
-
/// carries no `path`, so `loadContentIfNeeded` won't lazy-reload it; backing
|
|
913
|
-
/// out and re-tapping retries.
|
|
914
|
-
private func replaceWithMessage(_ loadingTemplate: CPListTemplate, title: String, subtitle: String?) {
|
|
915
|
-
// Only replace if the loading template is still the visible top template
|
|
916
|
-
// (the user may have backed out while the resolve was in flight).
|
|
917
|
-
guard interfaceController.topTemplate === loadingTemplate else { return }
|
|
918
|
-
|
|
919
|
-
let message = CPListTemplate(title: loadingTemplate.title, sections: [])
|
|
920
|
-
message.emptyViewTitleVariants = [title.isEmpty ? "Couldn't load" : title]
|
|
921
|
-
message.emptyViewSubtitleVariants = subtitle.map { [$0] } ?? []
|
|
922
|
-
|
|
923
|
-
interfaceController.safePop(animated: false) { [weak self] _, _ in
|
|
924
|
-
self?.interfaceController.safePush(message, animated: false)
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// MARK: - Content Change Handlers
|
|
929
|
-
|
|
930
|
-
@MainActor
|
|
931
|
-
private func handleTabsChanged(_ tabs: [Track]) {
|
|
932
|
-
logger.debug("Tabs changed: \(tabs.count) tabs")
|
|
933
|
-
// Same tabs by URL while ungated → only the tab-bar entries changed
|
|
934
|
-
// (e.g. titles after a locale switch): restamp them in place. No rebuild,
|
|
935
|
-
// so the selected tab and any pushed navigation stack survive. (Gate
|
|
936
|
-
// templates carry no path, so a gated tab bar never matches here.)
|
|
937
|
-
if !isGated,
|
|
938
|
-
let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate,
|
|
939
|
-
tabBar.templates.count == min(tabs.count, CPTabBarTemplate.maximumTabCount),
|
|
940
|
-
zip(tabBar.templates, tabs).allSatisfy({ getPath(from: $0) == $1.url })
|
|
941
|
-
{
|
|
942
|
-
for (template, tab) in zip(tabBar.templates, tabs) {
|
|
943
|
-
applyTabBarEntry(to: template, for: tab)
|
|
944
|
-
}
|
|
945
|
-
pendingTabs = nil
|
|
946
|
-
return
|
|
947
|
-
}
|
|
948
|
-
// Rebuilding replaces the root template, tearing down any pushed
|
|
949
|
-
// navigation stack. If the user is browsing, defer until they're back at
|
|
950
|
-
// the tab bar (applied from templateDidAppear).
|
|
951
|
-
guard interfaceController.templates.count <= 1 else {
|
|
952
|
-
pendingTabs = tabs
|
|
953
|
-
return
|
|
954
|
-
}
|
|
955
|
-
pendingTabs = nil
|
|
956
|
-
Task {
|
|
957
|
-
await showTabBar(tabs: tabs)
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
/// Applies a deferred tab change once the user has popped back to the tab
|
|
962
|
-
/// bar. Returns true if a rebuild was kicked off (the appeared template is
|
|
963
|
-
/// about to be replaced, so callers should skip further work on it).
|
|
964
|
-
@MainActor
|
|
965
|
-
fileprivate func applyPendingTabsIfAtRoot() -> Bool {
|
|
966
|
-
guard let tabs = pendingTabs, interfaceController.templates.count <= 1 else {
|
|
967
|
-
return false
|
|
968
|
-
}
|
|
969
|
-
pendingTabs = nil
|
|
970
|
-
Task {
|
|
971
|
-
await showTabBar(tabs: tabs)
|
|
972
|
-
}
|
|
973
|
-
return true
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
@MainActor
|
|
977
|
-
private func handleContentChanged(_ content: ResolvedTrack?) {
|
|
978
|
-
// This callback fires when the main browser's content changes.
|
|
979
|
-
// For CarPlay-specific refreshes (e.g., favorites), use notifyContentChanged instead.
|
|
980
|
-
guard let content else { return }
|
|
981
|
-
refreshTemplatesForPath(content.url, with: content)
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
@MainActor
|
|
985
|
-
private func handleActiveTrackChanged(_ event: PlaybackActiveTrackChangedEvent) {
|
|
986
|
-
logger.debug("handleActiveTrackChanged: \(event.lastTrack?.src ?? "nil") → \(event.track?.src ?? "nil")")
|
|
987
|
-
updatePlayingIndicators()
|
|
988
|
-
// Refresh per-track button state: favorite heart, Up Next availability,
|
|
989
|
-
// and the album line's pre-resolved destination.
|
|
990
|
-
nowPlayingManager.handleActiveTrackChanged()
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
/// Updates the isPlaying state on all list items based on the current active track.
|
|
994
|
-
@MainActor
|
|
995
|
-
fileprivate func updatePlayingIndicators() {
|
|
996
|
-
var templates: [CPListTemplate] = []
|
|
997
|
-
|
|
998
|
-
if let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate {
|
|
999
|
-
for template in tabBar.templates {
|
|
1000
|
-
if let listTemplate = template as? CPListTemplate {
|
|
1001
|
-
templates.append(listTemplate)
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
if let topTemplate = interfaceController.topTemplate as? CPListTemplate,
|
|
1007
|
-
!templates.contains(where: { $0 === topTemplate })
|
|
1008
|
-
{
|
|
1009
|
-
templates.append(topTemplate)
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
for template in templates {
|
|
1013
|
-
var templateChanged = false
|
|
1014
|
-
for section in template.sections {
|
|
1015
|
-
for item in section.items {
|
|
1016
|
-
guard let listItem = item as? CPListItem,
|
|
1017
|
-
let info = listItem.carPlayItemInfo,
|
|
1018
|
-
info.src != nil
|
|
1019
|
-
else { continue }
|
|
1020
|
-
|
|
1021
|
-
let isPlaying = isActiveTrack(id: info.id, src: info.src)
|
|
1022
|
-
if listItem.isPlaying != isPlaying {
|
|
1023
|
-
logger.debug(
|
|
1024
|
-
"Updating isPlaying for \(info.id ?? info.src ?? ""): \(listItem.isPlaying) → \(isPlaying)")
|
|
1025
|
-
listItem.isPlaying = isPlaying
|
|
1026
|
-
templateChanged = true
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
// Setting isPlaying on an already-displayed item does not reliably reach
|
|
1031
|
-
// the CarPlay screen (rdar-known; the flag only renders when the item is
|
|
1032
|
-
// pushed with it already set). Re-pushing the same sections re-serializes
|
|
1033
|
-
// every item with its current flags — done only when something actually
|
|
1034
|
-
// changed, so routine appear-time passes don't churn the display.
|
|
1035
|
-
if templateChanged {
|
|
1036
|
-
template.updateSections(template.sections)
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// MARK: - Public Content Notification
|
|
1042
|
-
|
|
1043
|
-
/// Notifies CarPlay that content at the given path has changed and should be refreshed.
|
|
1044
|
-
/// Called from HybridAudioBrowser.notifyContentChanged() to update CarPlay lists.
|
|
1045
|
-
///
|
|
1046
|
-
/// - Parameter path: The path where content has changed (e.g., "/favorites")
|
|
1047
|
-
@objc
|
|
1048
|
-
public func notifyContentChanged(path: String) {
|
|
1049
|
-
guard isStarted else { return }
|
|
1050
|
-
|
|
1051
|
-
Task { @MainActor in
|
|
1052
|
-
await refreshContentForPath(path)
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
/// Fetches fresh content for a path and updates any matching CarPlay templates.
|
|
1057
|
-
@MainActor
|
|
1058
|
-
private func refreshContentForPath(_ path: String) async {
|
|
1059
|
-
guard let audioBrowser else { return }
|
|
1060
|
-
|
|
1061
|
-
logger.debug("Refreshing CarPlay content for path: \(path)")
|
|
1062
|
-
|
|
1063
|
-
// Fetch fresh content (bypassing cache since content changed)
|
|
1064
|
-
do {
|
|
1065
|
-
let resolved = try await audioBrowser.browserManager.resolve(path, useCache: false)
|
|
1066
|
-
refreshTemplatesForPath(path, with: resolved)
|
|
1067
|
-
} catch {
|
|
1068
|
-
logger.error("Failed to refresh content for \(path): \(error.localizedDescription)")
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
/// Updates all CarPlay templates that are displaying the given path.
|
|
1073
|
-
@MainActor
|
|
1074
|
-
private func refreshTemplatesForPath(_ path: String, with content: ResolvedTrack) {
|
|
1075
|
-
logger.debug("Content changed for path: \(path)")
|
|
1076
|
-
|
|
1077
|
-
// Check if the root template is a tab bar and refresh matching tabs
|
|
1078
|
-
if let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate {
|
|
1079
|
-
for template in tabBar.templates {
|
|
1080
|
-
guard let listTemplate = template as? CPListTemplate,
|
|
1081
|
-
let templatePath = getPath(from: listTemplate),
|
|
1082
|
-
templatePath == path
|
|
1083
|
-
else { continue }
|
|
1084
|
-
|
|
1085
|
-
logger.info("Refreshing tab template for path: \(path)")
|
|
1086
|
-
updateTemplate(listTemplate, with: content)
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// Check if any template in the navigation stack matches the changed path
|
|
1091
|
-
// The top template is the currently visible one
|
|
1092
|
-
if let topTemplate = interfaceController.topTemplate as? CPListTemplate,
|
|
1093
|
-
let templatePath = getPath(from: topTemplate),
|
|
1094
|
-
templatePath == path
|
|
1095
|
-
{
|
|
1096
|
-
logger.info("Refreshing top template for path: \(path)")
|
|
1097
|
-
updateTemplate(topTemplate, with: content)
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
/// Refreshes every currently-displayed template (tabs + navigation stack) by
|
|
1102
|
-
/// re-fetching each path with the cache bypassed. Called when all content was
|
|
1103
|
-
/// invalidated app-wide (e.g. a locale change).
|
|
1104
|
-
@MainActor
|
|
1105
|
-
public func invalidateAllContent() {
|
|
1106
|
-
guard isStarted else { return }
|
|
1107
|
-
Task { @MainActor in await refreshAllDisplayedTemplates() }
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
@MainActor
|
|
1111
|
-
private func refreshAllDisplayedTemplates() async {
|
|
1112
|
-
guard let audioBrowser else { return }
|
|
1113
|
-
|
|
1114
|
-
var templates: [CPListTemplate] = []
|
|
1115
|
-
if let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate {
|
|
1116
|
-
for template in tabBar.templates {
|
|
1117
|
-
if let listTemplate = template as? CPListTemplate {
|
|
1118
|
-
templates.append(listTemplate)
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
for template in interfaceController.templates {
|
|
1123
|
-
if let listTemplate = template as? CPListTemplate,
|
|
1124
|
-
!templates.contains(where: { $0 === listTemplate })
|
|
1125
|
-
{
|
|
1126
|
-
templates.append(listTemplate)
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
for template in templates {
|
|
1131
|
-
guard let path = getPath(from: template) else { continue }
|
|
1132
|
-
do {
|
|
1133
|
-
let resolved = try await audioBrowser.browserManager.resolve(path, useCache: false)
|
|
1134
|
-
updateTemplate(template, with: resolved)
|
|
1135
|
-
} catch {
|
|
1136
|
-
logger.error("invalidateAllContent: failed to refresh \(path): \(error.localizedDescription)")
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// MARK: - Gate
|
|
1142
|
-
|
|
1143
|
-
/// Applies a gate state change: set or clear. The per-request chrome is
|
|
1144
|
-
/// obtained at render time via `gateDecision(for:)`, so this just tears down
|
|
1145
|
-
/// any pushed navigation and rebuilds the tab bar — `showTabBar` routes to the
|
|
1146
|
-
/// gated build while a gate is active and to the normal build once cleared.
|
|
1147
|
-
/// Custom Now Playing buttons (e.g. favorite) hide while gated and return on
|
|
1148
|
-
/// clear.
|
|
1149
|
-
@MainActor
|
|
1150
|
-
private func handleGateChanged(_ active: Bool) {
|
|
1151
|
-
isGated = active
|
|
1152
|
-
let tabs = audioBrowser?.browserManager.getTabs() ?? []
|
|
1153
|
-
if tabs.isEmpty, !active {
|
|
1154
|
-
// Never had tabs (gate was up since before config) — full initial build.
|
|
1155
|
-
Task { await buildInitialInterface() }
|
|
1156
|
-
} else {
|
|
1157
|
-
// Tear down any pushed navigation — gated content must not stay reachable
|
|
1158
|
-
// behind the gate, and on clear we return to the root. (No-op at root.)
|
|
1159
|
-
interfaceController.safePopToRoot(animated: false)
|
|
1160
|
-
Task { await showTabBar(tabs: tabs) }
|
|
1161
|
-
}
|
|
1162
|
-
nowPlayingManager.setupNowPlayingButtons()
|
|
1163
|
-
nowPlayingManager.updateNowPlayingButtonStates()
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
/// Builds the gate page for one tab-bar slot, carrying the tab's entry so
|
|
1167
|
-
/// the tab bar itself stays familiar.
|
|
1168
|
-
///
|
|
1169
|
-
/// A CPListTemplate, NOT a CPInformationTemplate: CarPlay enforces
|
|
1170
|
-
/// per-entitlement template allowances at runtime, and the audio
|
|
1171
|
-
/// entitlement does not include the information template — handing one to
|
|
1172
|
-
/// CPTabBarTemplate throws an unhandled ObjC exception (crash at init).
|
|
1173
|
-
///
|
|
1174
|
-
/// Within a list, tab children never show navigation-bar buttons (the tab
|
|
1175
|
-
/// bar owns that chrome), and the centered empty view renders only when the
|
|
1176
|
-
/// list has NO rows. The gate is a plain centered message — `title` as the
|
|
1177
|
-
/// empty-view title and `message` as its subtitle, where newlines collapse
|
|
1178
|
-
/// to spaces (the "variants" are width alternatives, not lines). No action
|
|
1179
|
-
/// button: a gate withholds content; the consumer surfaces any "subscribe"
|
|
1180
|
-
/// affordance in its own UI, not on the car surface.
|
|
1181
|
-
private func makeGateTemplate(gate gateChrome: Gate?, tab: Track?) -> CPListTemplate {
|
|
1182
|
-
// A gated decision with neither an override nor a stored default chrome
|
|
1183
|
-
// (resolver-only `true`) falls back to the built-in minimal gate.
|
|
1184
|
-
let gate = gateChrome ?? HybridAudioBrowser.builtInGate
|
|
1185
|
-
let template = CPListTemplate(title: gate.title, sections: [])
|
|
1186
|
-
// Set at creation — the timing CarPlay renders the empty view reliably
|
|
1187
|
-
// (see replaceWithMessage).
|
|
1188
|
-
template.emptyViewTitleVariants = [gate.title]
|
|
1189
|
-
if let message = gate.message, !message.isEmpty {
|
|
1190
|
-
template.emptyViewSubtitleVariants = [
|
|
1191
|
-
message.replacingOccurrences(of: "\n", with: " "),
|
|
1192
|
-
]
|
|
1193
|
-
}
|
|
1194
|
-
// Marks the page as a gate (vs. a content tab, which carries a `path`),
|
|
1195
|
-
// so the lazy-loader and refresh paths never try to fill it.
|
|
1196
|
-
template.userInfo = ["gate": true] as [String: Any]
|
|
1197
|
-
if let tab {
|
|
1198
|
-
applyTabBarEntry(to: template, for: tab)
|
|
1199
|
-
}
|
|
1200
|
-
return template
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// MARK: - Error Handling
|
|
1204
|
-
|
|
1205
|
-
/// Shows a navigation error using CPActionSheetTemplate.
|
|
1206
|
-
/// - Parameters:
|
|
1207
|
-
/// - error: The NavigationError to display
|
|
1208
|
-
/// - path: The path that was being navigated to when the error occurred
|
|
1209
|
-
private func showNavigationError(_ error: NavigationError, path: String) {
|
|
1210
|
-
let defaultFormatted = error.defaultFormatted()
|
|
1211
|
-
|
|
1212
|
-
// Check if custom formatter is configured
|
|
1213
|
-
logger.debug("showNavigationError: formatNavigationError is \(self.config.formatNavigationError != nil ? "set" : "nil")")
|
|
1214
|
-
if let formatter = config.formatNavigationError {
|
|
1215
|
-
logger.debug("Calling formatNavigationError callback...")
|
|
1216
|
-
// Call the JS callback and handle result
|
|
1217
|
-
let params = FormatNavigationErrorParams(error: error, defaultFormatted: defaultFormatted, path: path)
|
|
1218
|
-
formatter(params)
|
|
1219
|
-
.then { [weak self] customDisplay in
|
|
1220
|
-
self?.logger.debug("formatNavigationError returned: \(String(describing: customDisplay))")
|
|
1221
|
-
self?.presentErrorActionSheet(customDisplay: customDisplay ?? defaultFormatted)
|
|
1222
|
-
}
|
|
1223
|
-
.catch { [weak self] callbackError in
|
|
1224
|
-
self?.logger.error("formatNavigationError failed: \(callbackError)")
|
|
1225
|
-
// On error, fall back to defaults
|
|
1226
|
-
self?.presentErrorActionSheet(customDisplay: defaultFormatted)
|
|
1227
|
-
}
|
|
1228
|
-
} else {
|
|
1229
|
-
presentErrorActionSheet(customDisplay: defaultFormatted)
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
/// Presents the error action sheet with the given display info.
|
|
1234
|
-
/// - Parameter customDisplay: The formatted error to display
|
|
1235
|
-
private func presentErrorActionSheet(customDisplay: FormattedNavigationError) {
|
|
1236
|
-
// If another template is already presented, dismiss it first
|
|
1237
|
-
if interfaceController.presentedTemplate != nil {
|
|
1238
|
-
interfaceController.safeDismiss(animated: false) { [weak self] _, _ in
|
|
1239
|
-
self?.showErrorActionSheet(customDisplay: customDisplay)
|
|
1240
|
-
}
|
|
1241
|
-
} else {
|
|
1242
|
-
showErrorActionSheet(customDisplay: customDisplay)
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
/// Actually shows the error action sheet (called after safety checks)
|
|
1247
|
-
private func showErrorActionSheet(customDisplay: FormattedNavigationError) {
|
|
1248
|
-
// OK action - dismiss the action sheet (use system-localized "OK")
|
|
1249
|
-
let okTitle = Bundle(for: UIAlertController.self).localizedString(forKey: "OK", value: "OK", table: nil)
|
|
1250
|
-
let ok = CPAlertAction(title: okTitle, style: .cancel) { [weak self] _ in
|
|
1251
|
-
self?.interfaceController.safeDismiss(animated: true)
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
let actionSheet = CPActionSheetTemplate(
|
|
1255
|
-
title: customDisplay.title,
|
|
1256
|
-
message: customDisplay.message,
|
|
1257
|
-
actions: [ok],
|
|
1258
|
-
)
|
|
1259
|
-
|
|
1260
|
-
interfaceController.safePresent(actionSheet, animated: true)
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
/// Shows an initialization failure as the root template — a centered empty
|
|
1264
|
-
/// view formatted via the app's `formatNavigationError` (path "/"), like every
|
|
1265
|
-
/// other browse failure. The empty view renders reliably here because it's the
|
|
1266
|
-
/// template's initial state at set-root time.
|
|
1267
|
-
@MainActor
|
|
1268
|
-
private func showRootNavigationError(_ navError: NavigationError) async {
|
|
1269
|
-
let formatted = await formattedNavigationError(navError, path: "/")
|
|
1270
|
-
let template = CPListTemplate(title: nil, sections: [])
|
|
1271
|
-
template.emptyViewTitleVariants = [formatted.title]
|
|
1272
|
-
template.emptyViewSubtitleVariants = formatted.message.flatMap { $0.isEmpty ? nil : [$0] } ?? []
|
|
1273
|
-
interfaceController.safeSetRoot(template, animated: true)
|
|
1274
|
-
}
|
|
1275
89
|
}
|
|
1276
90
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
/// Separate delegate class to avoid exposing CPInterfaceControllerDelegate to Obj-C header
|
|
91
|
+
/// Keeps `CPInterfaceControllerDelegate` out of the Objective-C header.
|
|
1280
92
|
private final class InterfaceControllerDelegate: NSObject, CPInterfaceControllerDelegate {
|
|
1281
93
|
private weak var controller: RNABCarPlayController?
|
|
1282
94
|
|
|
@@ -1286,76 +98,10 @@ private final class InterfaceControllerDelegate: NSObject, CPInterfaceController
|
|
|
1286
98
|
}
|
|
1287
99
|
|
|
1288
100
|
func templateDidAppear(_ aTemplate: CPTemplate, animated _: Bool) {
|
|
1289
|
-
|
|
1290
|
-
// guard must let both through — the playing indicator was toggled while
|
|
1291
|
-
// the list was covered, and this appearance is the moment to repaint it.
|
|
1292
|
-
guard aTemplate is CPListTemplate || aTemplate is CPTabBarTemplate else { return }
|
|
1293
|
-
|
|
1294
|
-
// A tab change deferred while the user was browsing replaces the whole
|
|
1295
|
-
// tab bar — no point updating a template that's about to be discarded.
|
|
1296
|
-
if controller?.applyPendingTabsIfAtRoot() == true { return }
|
|
1297
|
-
|
|
1298
|
-
// Update playing indicators when navigating back to a list template
|
|
1299
|
-
controller?.updatePlayingIndicators()
|
|
1300
|
-
|
|
1301
|
-
// Lazy load content for tabs that haven't been loaded yet
|
|
1302
|
-
if let listTemplate = aTemplate as? CPListTemplate {
|
|
1303
|
-
controller?.loadContentIfNeeded(for: listTemplate)
|
|
1304
|
-
}
|
|
101
|
+
controller?.session?.templateDidAppear(aTemplate)
|
|
1305
102
|
}
|
|
1306
103
|
|
|
1307
104
|
func templateDidDisappear(_ aTemplate: CPTemplate, animated _: Bool) {
|
|
1308
|
-
|
|
1309
|
-
// "tab bar appeared" or only "now playing disappeared" is a platform
|
|
1310
|
-
// detail — cover both so the revealed list always repaints its indicator.
|
|
1311
|
-
guard aTemplate is CPNowPlayingTemplate else { return }
|
|
1312
|
-
controller?.updatePlayingIndicators()
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
// MARK: - Lazy Loading
|
|
1317
|
-
|
|
1318
|
-
private extension RNABCarPlayController {
|
|
1319
|
-
/// Loads content for a template if it hasn't been loaded yet (lazy loading for tabs)
|
|
1320
|
-
func loadContentIfNeeded(for template: CPListTemplate) {
|
|
1321
|
-
// Get path from userInfo
|
|
1322
|
-
guard let path = getPath(from: template) else { return }
|
|
1323
|
-
|
|
1324
|
-
// The template is now on screen, so a queued duplicate navigation to it is
|
|
1325
|
-
// no longer a concern (the top-template check will catch any further taps).
|
|
1326
|
-
navigatingPaths.remove(path)
|
|
1327
|
-
|
|
1328
|
-
// Skip if already has content (single-flight in loadContent guards re-entry).
|
|
1329
|
-
guard template.sections.isEmpty else { return }
|
|
1330
|
-
|
|
1331
|
-
logger.debug("Lazy loading content for tab: \(path)")
|
|
1332
|
-
|
|
1333
|
-
Task {
|
|
1334
|
-
await loadContent(for: path, into: template)
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
// MARK: - UIImage Resize
|
|
1340
|
-
|
|
1341
|
-
extension UIImage {
|
|
1342
|
-
/// Draws the image centered within the target size, maintaining aspect ratio
|
|
1343
|
-
func resized(to targetSize: CGSize) -> UIImage? {
|
|
1344
|
-
UIGraphicsBeginImageContextWithOptions(targetSize, false, 0.0)
|
|
1345
|
-
defer { UIGraphicsEndImageContext() }
|
|
1346
|
-
|
|
1347
|
-
// Scale to fit while maintaining aspect ratio
|
|
1348
|
-
let widthRatio = targetSize.width / size.width
|
|
1349
|
-
let heightRatio = targetSize.height / size.height
|
|
1350
|
-
let scale = min(widthRatio, heightRatio)
|
|
1351
|
-
|
|
1352
|
-
let scaledSize = CGSize(width: size.width * scale, height: size.height * scale)
|
|
1353
|
-
let origin = CGPoint(
|
|
1354
|
-
x: (targetSize.width - scaledSize.width) / 2,
|
|
1355
|
-
y: (targetSize.height - scaledSize.height) / 2,
|
|
1356
|
-
)
|
|
1357
|
-
|
|
1358
|
-
draw(in: CGRect(origin: origin, size: scaledSize))
|
|
1359
|
-
return UIGraphicsGetImageFromCurrentImageContext()?.withRenderingMode(.alwaysTemplate)
|
|
105
|
+
controller?.session?.templateDidDisappear(aTemplate)
|
|
1360
106
|
}
|
|
1361
107
|
}
|