react-native-audio-browser 0.1.1 → 0.2.0-next.1
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/AudioBrowser.podspec +1 -1
- package/README.md +180 -2
- package/android/build.gradle +10 -0
- package/android/gradle.properties +1 -1
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +401 -141
- package/android/src/main/java/com/audiobrowser/Callbacks.kt +4 -6
- package/android/src/main/java/com/audiobrowser/Service.kt +67 -18
- package/android/src/main/java/com/audiobrowser/browser/ArtworkResolutionRegistry.kt +37 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowseArtworkRegistry.kt +34 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +443 -178
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +300 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonArtwork.kt +79 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +41 -4
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +43 -0
- package/android/src/main/java/com/audiobrowser/browser/TrackLoadHandler.kt +4 -4
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +35 -0
- package/android/src/main/java/com/audiobrowser/http/RequestConfigBuilder.kt +109 -97
- package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +6 -10
- package/android/src/main/java/com/audiobrowser/model/PlayerSetupOptions.kt +40 -8
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +31 -37
- package/android/src/main/java/com/audiobrowser/player/AutomaticBufferManager.kt +4 -1
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +119 -0
- package/android/src/main/java/com/audiobrowser/player/DynamicLoadControl.kt +18 -1
- package/android/src/main/java/com/audiobrowser/player/EqualizerController.kt +153 -0
- package/android/src/main/java/com/audiobrowser/player/InterceptingPlayer.kt +141 -0
- package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +15 -3
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +415 -131
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +154 -276
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +348 -0
- package/android/src/main/java/com/audiobrowser/player/PlaybackErrorClassifier.kt +129 -0
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateMachine.kt +85 -0
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +27 -5
- package/android/src/main/java/com/audiobrowser/player/PlaybackTimer.kt +56 -0
- package/android/src/main/java/com/audiobrowser/player/Player.kt +452 -576
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +20 -0
- package/android/src/main/java/com/audiobrowser/player/PlayerEngineFactory.kt +130 -0
- package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +190 -67
- package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +117 -53
- package/android/src/main/java/com/audiobrowser/player/SleepTimer.kt +37 -3
- package/android/src/main/java/com/audiobrowser/player/SleepTimerManager.kt +80 -0
- package/android/src/main/java/com/audiobrowser/player/StuckRecoveryPolicy.kt +50 -0
- package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +84 -40
- package/android/src/main/java/com/audiobrowser/player/VolumeFader.kt +81 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +141 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +37 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +41 -0
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +209 -0
- package/android/src/main/java/com/audiobrowser/util/ArtworkContentProvider.kt +161 -0
- package/android/src/main/java/com/audiobrowser/util/ArtworkUris.kt +28 -0
- package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +26 -0
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +24 -0
- package/android/src/main/java/com/audiobrowser/util/CoilArtworkLoader.kt +49 -0
- package/android/src/main/java/com/audiobrowser/util/CoilArtworkLoaderHolder.kt +36 -0
- package/android/src/main/java/com/audiobrowser/util/CoilBitmapLoader.kt +23 -300
- package/android/src/main/java/com/audiobrowser/util/EqualizerManager.kt +13 -23
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +2 -2
- package/android/src/main/java/com/audiobrowser/util/MetadataAdapter.kt +4 -3
- package/android/src/main/java/com/audiobrowser/util/OutputMonitor.kt +211 -0
- package/android/src/main/java/com/audiobrowser/util/OutputSwitcher.kt +58 -0
- package/android/src/main/java/com/audiobrowser/util/PlayingStateFactory.kt +2 -1
- package/android/src/main/java/com/audiobrowser/util/RatingFavorites.kt +14 -0
- package/android/src/main/java/com/audiobrowser/util/SvgArtworkRenderer.kt +6 -111
- package/android/src/main/java/com/audiobrowser/util/SystemVolumeMonitor.kt +22 -8
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +111 -80
- package/android/src/main/res/values/strings.xml +4 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +166 -0
- package/android/src/test/java/com/audiobrowser/browser/ArtworkResolutionRegistryTest.kt +46 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowseArtworkRegistryTest.kt +39 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerBuildApiRequestTest.kt +114 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +141 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +4 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +177 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +54 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +96 -0
- package/android/src/test/java/com/audiobrowser/browser/SimpleRouterTest.kt +1 -2
- package/android/src/test/java/com/audiobrowser/http/RequestConfigBuilderTest.kt +72 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +159 -0
- package/android/src/test/java/com/audiobrowser/player/DynamicLoadControlTest.kt +56 -0
- package/android/src/test/java/com/audiobrowser/player/EqualizerControllerTest.kt +216 -0
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +442 -0
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +191 -0
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateMachineTest.kt +198 -0
- package/android/src/test/java/com/audiobrowser/player/PlaybackTimerTest.kt +137 -0
- package/android/src/test/java/com/audiobrowser/player/PlayerEngineFactoryTest.kt +36 -0
- package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +268 -0
- package/android/src/test/java/com/audiobrowser/player/SessionErrorMaskTest.kt +68 -0
- package/android/src/test/java/com/audiobrowser/player/StuckRecoveryPolicyTest.kt +40 -0
- package/android/src/test/java/com/audiobrowser/player/TransformingDataSourceTest.kt +193 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +107 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +73 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +75 -0
- package/android/src/test/java/com/audiobrowser/tls/CertFixtures.kt +18 -0
- package/android/src/test/java/com/audiobrowser/util/ArtworkContentProviderTest.kt +163 -0
- package/android/src/test/java/com/audiobrowser/util/ArtworkUrisTest.kt +34 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +28 -0
- package/android/src/test/java/com/audiobrowser/util/CoilArtworkLoaderHolderTest.kt +53 -0
- package/android/src/test/java/com/audiobrowser/util/CoilArtworkLoaderTest.kt +58 -0
- package/android/src/test/java/com/audiobrowser/util/FakeImageLoader.kt +57 -0
- package/android/src/test/java/com/audiobrowser/util/PlayingStateFactoryTest.kt +40 -0
- package/android/src/test/java/com/audiobrowser/util/RatingFavoritesTest.kt +34 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryBrowseTest.kt +50 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +63 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryRatingTest.kt +35 -0
- package/android/src/test/resources/certs/isrg-root-x1.pem +31 -0
- package/android/src/test/resources/certs/letsencrypt-r13.der +0 -0
- package/android/src/test/resources/certs/letsencrypt-r13.pem +29 -0
- package/android/src/test/resources/certs/stationplaylist-leaf.pem +34 -0
- package/ios/Browser/BrowserConfig.swift +41 -11
- package/ios/Browser/BrowserManager+URLResolution.swift +213 -93
- package/ios/Browser/BrowserManager.swift +303 -110
- package/ios/Browser/BrowserPathHelper.swift +32 -3
- package/ios/Browser/JsonModels.swift +287 -66
- package/ios/Browser/MediaResolveComposer.swift +114 -0
- package/ios/Browser/SearchDrillIn.swift +44 -0
- package/ios/Browser/SectionScope.swift +35 -0
- package/ios/Browser/TabBarEntries.swift +15 -0
- package/ios/CarPlay/CPInterfaceController+Safe.swift +126 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +6 -6
- package/ios/CarPlay/CarPlayController.swift +739 -149
- package/ios/CarPlay/CarPlayImageLoader.swift +57 -8
- package/ios/CarPlay/CarPlayListItemFactory.swift +44 -35
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +180 -36
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +19 -0
- package/ios/CarPlay/RNABAudioBrowser.h +15 -10
- package/ios/CarPlay/RNABAudioBrowser.m +15 -14
- package/ios/CarPlay/RNABCarPlaySceneDelegate.m +2 -0
- package/ios/CarPlay/RNABMediaAddHandler.swift +75 -0
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +69 -0
- package/ios/CarPlay/RNABMediaIntentHandler.swift +53 -43
- package/ios/Extension/Capability+RemoteCommand.swift +7 -9
- package/ios/Extension/ChapterMetadata+AVFoundation.swift +1 -1
- package/ios/Extension/NavigationError+Conversion.swift +8 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +20 -0
- package/ios/Extension/RequestConfig+Copying.swift +31 -0
- package/ios/Extension/ResolvedTrack+Copying.swift +10 -4
- package/ios/Extension/TimedMetadata+AVFoundation.swift +1 -1
- package/ios/Extension/Track+AVPlayer.swift +4 -4
- package/ios/Extension/Track+Copying.swift +9 -3
- package/ios/Extension/TrackMetadata+AVFoundation.swift +1 -1
- package/ios/Extension/Variant+Artwork.swift +49 -0
- package/ios/HybridAudioBrowser.swift +750 -173
- package/ios/Model/NitroTypeStubs.swift +299 -224
- package/ios/Model/PlayerUpdateOptions.swift +23 -23
- package/ios/Model/RemoteCommand.swift +6 -6
- package/ios/Model/TrackPlayerError.swift +82 -18
- package/ios/NowPlayingInfo/NowPlayingInfoController.swift +99 -121
- package/ios/NowPlayingInfo/SilentAudioPrimer.swift +73 -0
- package/ios/Observer/PlayerItemNotificationObserver.swift +45 -5
- package/ios/Observer/PlayerItemPropertyObserver.swift +27 -4
- package/ios/Observer/PlayerTimeObserver.swift +3 -53
- package/ios/Option/SessionCategories.swift +33 -11
- package/ios/Player/AirPlayStallJudgement.swift +25 -0
- package/ios/Player/EndOfTrackJudgement.swift +32 -0
- package/ios/Player/LoadSeekCoordinator.swift +16 -1
- package/ios/Player/MediaIntentCriteria.swift +139 -0
- package/ios/Player/MediaLoader.swift +28 -10
- package/ios/Player/NowPlayingUpdater.swift +119 -26
- package/ios/Player/PersistedPlaybackState.swift +11 -0
- package/ios/Player/PlaybackCoordinator.swift +282 -51
- package/ios/Player/PlaybackCoordinatorCallbacks.swift +6 -1
- package/ios/Player/PlaybackEffectHandler.swift +19 -9
- package/ios/Player/PlaybackErrorHandler.swift +97 -13
- package/ios/Player/PlaybackStateMachine.swift +18 -13
- package/ios/Player/PlaybackStateStore.swift +27 -0
- package/ios/Player/PlaybackTimer.swift +61 -0
- package/ios/Player/PlayingStateManager.swift +3 -2
- package/ios/Player/QueueManager.swift +67 -61
- package/ios/Player/RetryManager.swift +97 -21
- package/ios/Player/SleepTimerHandling.swift +8 -0
- package/ios/Player/SleepTimerManager.swift +38 -5
- package/ios/Player/VolumeFader.swift +77 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +31 -15
- package/ios/Tests/AirPlayStallJudgementTests.swift +52 -0
- package/ios/Tests/BrowserPathHelperTests.swift +28 -0
- package/ios/Tests/CarPlayArtworkResolverTests.swift +39 -39
- package/ios/Tests/ChapterMetadataAVFoundationTests.swift +2 -2
- package/ios/Tests/EndOfTrackJudgementTests.swift +35 -0
- package/ios/Tests/JsonModelsTests.swift +58 -0
- package/ios/Tests/LoadSeekCoordinatorTests.swift +38 -7
- package/ios/Tests/MediaIntentCriteriaTests.swift +170 -0
- package/ios/Tests/MediaLoaderAssetOptionsTests.swift +46 -0
- package/ios/Tests/MediaLoaderTests.swift +23 -8
- package/ios/Tests/MediaResolveTests.swift +126 -0
- package/ios/Tests/OnceValueTests.swift +79 -0
- package/ios/Tests/PlaybackCoordinatorTests.swift +784 -13
- package/ios/Tests/PlaybackErrorHandlerTests.swift +380 -46
- package/ios/Tests/PlaybackStateMachineTests.swift +33 -8
- package/ios/Tests/PlaybackStateStoreTests.swift +48 -0
- package/ios/Tests/PlaybackTimerTests.swift +55 -0
- package/ios/Tests/PlayingStateManagerTests.swift +57 -0
- package/ios/Tests/QueueManagerTests.swift +149 -18
- package/ios/Tests/ResolvedTrackCopyingTests.swift +15 -8
- package/ios/Tests/RetryManagerTests.swift +89 -0
- package/ios/Tests/SearchDrillInTests.swift +63 -0
- package/ios/Tests/SectionScopeTests.swift +82 -0
- package/ios/Tests/SleepTimerManagerTests.swift +104 -0
- package/ios/Tests/Support/TestTypes.swift +61 -20
- package/ios/Tests/TabBarEntriesTests.swift +71 -0
- package/ios/Tests/TrackSelectorTests.swift +15 -15
- package/ios/TrackPlayer.swift +380 -58
- package/ios/TrackPlayerCallbacks.swift +0 -6
- package/ios/TrackSelector.swift +5 -5
- package/ios/Util/ArtworkImageFetcher.swift +4 -2
- package/ios/Util/NetworkMonitor.swift +7 -3
- package/ios/Util/OnceValue.swift +34 -0
- package/lib/commonjs/features/browser-config.js +227 -0
- package/lib/commonjs/features/browser-config.js.map +1 -0
- package/lib/commonjs/features/browser.js +59 -158
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/carConnection.js +42 -0
- package/lib/commonjs/features/carConnection.js.map +1 -0
- package/lib/commonjs/features/errors.js +34 -2
- package/lib/commonjs/features/errors.js.map +1 -1
- package/lib/commonjs/features/gate.js +149 -0
- package/lib/commonjs/features/gate.js.map +1 -0
- package/lib/commonjs/features/index.js +22 -0
- package/lib/commonjs/features/index.js.map +1 -1
- package/lib/commonjs/features/metadata.js.map +1 -1
- package/lib/commonjs/features/nowPlaying.js +35 -0
- package/lib/commonjs/features/nowPlaying.js.map +1 -1
- package/lib/commonjs/features/output.js +37 -23
- package/lib/commonjs/features/output.js.map +1 -1
- package/lib/commonjs/features/playback/controls.js +9 -0
- package/lib/commonjs/features/playback/controls.js.map +1 -1
- package/lib/commonjs/features/playback/index.js +11 -0
- package/lib/commonjs/features/playback/index.js.map +1 -1
- package/lib/commonjs/features/playback/progress.js +28 -12
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +92 -0
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -0
- package/lib/commonjs/features/playback/volume.js +1 -1
- package/lib/commonjs/features/player/options.js +49 -13
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +249 -3
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +10 -3
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/features/remoteControls.js +1 -12
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +9 -4
- package/lib/commonjs/features/sleepTimer.js.map +1 -1
- package/lib/commonjs/native.js +5 -0
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.web.js +4 -1
- package/lib/commonjs/native.web.js.map +1 -1
- package/lib/commonjs/utils/LazyNativeEmitter.js +0 -3
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/utils/NativeUpdatedValue.js +0 -3
- package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/commonjs/utils/artwork.js +31 -0
- package/lib/commonjs/utils/artwork.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +301 -124
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +71 -0
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/PlaybackTimer.js +34 -0
- package/lib/commonjs/web/TrackPlayer/PlaybackTimer.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/Player.js +90 -26
- package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +21 -0
- package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +273 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +158 -0
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/RepeatMode.js +1 -1
- package/lib/commonjs/web/TrackPlayer/RepeatMode.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/SleepTimer.js +34 -3
- package/lib/commonjs/web/TrackPlayer/SleepTimer.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/VolumeFader.js +75 -0
- package/lib/commonjs/web/TrackPlayer/VolumeFader.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/index.js +13 -5
- package/lib/commonjs/web/TrackPlayer/index.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +176 -50
- package/lib/commonjs/web/browser/BrowserManager.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 +11 -9
- package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
- package/lib/commonjs/web/browser/assertBrowsePageShape.js +22 -0
- package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -0
- package/lib/commonjs/web/browser/classifyTrackNavigation.js +41 -0
- package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -0
- package/lib/commonjs/web/browser/parseSearchResponse.js +20 -0
- package/lib/commonjs/web/browser/parseSearchResponse.js.map +1 -0
- package/lib/commonjs/web/http/HttpClient.js +4 -23
- package/lib/commonjs/web/http/HttpClient.js.map +1 -1
- package/lib/commonjs/web/http/RequestConfigBuilder.js +107 -58
- package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/commonjs/web/playbackErrorKind.js +57 -0
- package/lib/commonjs/web/playbackErrorKind.js.map +1 -0
- package/lib/commonjs/web/player/MediaSessionManager.js +121 -0
- package/lib/commonjs/web/player/MediaSessionManager.js.map +1 -0
- package/lib/commonjs/web/player/NowPlayingManager.js +6 -5
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/player/OptionsManager.js +9 -17
- package/lib/commonjs/web/player/OptionsManager.js.map +1 -1
- package/lib/commonjs/web/player/RemoteCommandController.js +96 -0
- package/lib/commonjs/web/player/RemoteCommandController.js.map +1 -0
- package/lib/module/features/browser-config.js +221 -0
- package/lib/module/features/browser-config.js.map +1 -0
- package/lib/module/features/browser.js +53 -156
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/carConnection.js +37 -0
- package/lib/module/features/carConnection.js.map +1 -0
- package/lib/module/features/errors.js +34 -2
- package/lib/module/features/errors.js.map +1 -1
- package/lib/module/features/gate.js +144 -0
- package/lib/module/features/gate.js.map +1 -0
- package/lib/module/features/index.js +2 -0
- package/lib/module/features/index.js.map +1 -1
- package/lib/module/features/metadata.js.map +1 -1
- package/lib/module/features/nowPlaying.js +33 -0
- package/lib/module/features/nowPlaying.js.map +1 -1
- package/lib/module/features/output.js +32 -19
- package/lib/module/features/output.js.map +1 -1
- package/lib/module/features/playback/controls.js +8 -0
- package/lib/module/features/playback/controls.js.map +1 -1
- package/lib/module/features/playback/index.js +1 -0
- package/lib/module/features/playback/index.js.map +1 -1
- package/lib/module/features/playback/progress.js +28 -12
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +88 -0
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -0
- package/lib/module/features/playback/volume.js +1 -1
- package/lib/module/features/player/options.js +48 -13
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +249 -3
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/queue/queue.js +10 -3
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/features/remoteControls.js +0 -11
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +9 -4
- package/lib/module/features/sleepTimer.js.map +1 -1
- package/lib/module/native.js +5 -0
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.web.js +4 -1
- package/lib/module/native.web.js.map +1 -1
- package/lib/module/utils/LazyNativeEmitter.js +0 -3
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/utils/NativeUpdatedValue.js +0 -3
- package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/module/utils/artwork.js +26 -0
- package/lib/module/utils/artwork.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +302 -125
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +67 -0
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -0
- package/lib/module/web/TrackPlayer/PlaybackTimer.js +29 -0
- package/lib/module/web/TrackPlayer/PlaybackTimer.js.map +1 -0
- package/lib/module/web/TrackPlayer/Player.js +90 -26
- package/lib/module/web/TrackPlayer/Player.js.map +1 -1
- package/lib/module/web/TrackPlayer/PlayingStateFactory.js +17 -0
- package/lib/module/web/TrackPlayer/PlayingStateFactory.js.map +1 -0
- package/lib/module/web/TrackPlayer/QueueManager.js +269 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -0
- package/lib/module/web/TrackPlayer/QueuePlayer.js +154 -0
- package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -0
- package/lib/module/web/TrackPlayer/RepeatMode.js +1 -1
- package/lib/module/web/TrackPlayer/RepeatMode.js.map +1 -1
- package/lib/module/web/TrackPlayer/SleepTimer.js +34 -3
- package/lib/module/web/TrackPlayer/SleepTimer.js.map +1 -1
- package/lib/module/web/TrackPlayer/VolumeFader.js +70 -0
- package/lib/module/web/TrackPlayer/VolumeFader.js.map +1 -0
- package/lib/module/web/TrackPlayer/index.js +2 -1
- package/lib/module/web/TrackPlayer/index.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +176 -50
- package/lib/module/web/browser/BrowserManager.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 +11 -9
- package/lib/module/web/browser/SearchManager.js.map +1 -1
- package/lib/module/web/browser/assertBrowsePageShape.js +18 -0
- package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -0
- package/lib/module/web/browser/classifyTrackNavigation.js +38 -0
- package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -0
- package/lib/module/web/browser/parseSearchResponse.js +16 -0
- package/lib/module/web/browser/parseSearchResponse.js.map +1 -0
- package/lib/module/web/http/HttpClient.js +4 -23
- package/lib/module/web/http/HttpClient.js.map +1 -1
- package/lib/module/web/http/RequestConfigBuilder.js +107 -58
- package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/module/web/playbackErrorKind.js +52 -0
- package/lib/module/web/playbackErrorKind.js.map +1 -0
- package/lib/module/web/player/MediaSessionManager.js +116 -0
- package/lib/module/web/player/MediaSessionManager.js.map +1 -0
- package/lib/module/web/player/NowPlayingManager.js +7 -5
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/player/OptionsManager.js +9 -17
- package/lib/module/web/player/OptionsManager.js.map +1 -1
- package/lib/module/web/player/RemoteCommandController.js +92 -0
- package/lib/module/web/player/RemoteCommandController.js.map +1 -0
- package/lib/typescript/src/features/browser-config.d.ts +12 -0
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -0
- package/lib/typescript/src/features/browser.d.ts +40 -1
- package/lib/typescript/src/features/browser.d.ts.map +1 -1
- package/lib/typescript/src/features/carConnection.d.ts +20 -0
- package/lib/typescript/src/features/carConnection.d.ts.map +1 -0
- package/lib/typescript/src/features/errors.d.ts +58 -3
- package/lib/typescript/src/features/errors.d.ts.map +1 -1
- package/lib/typescript/src/features/gate.d.ts +77 -0
- package/lib/typescript/src/features/gate.d.ts.map +1 -0
- package/lib/typescript/src/features/index.d.ts +2 -0
- package/lib/typescript/src/features/index.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +6 -4
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +27 -0
- package/lib/typescript/src/features/nowPlaying.d.ts.map +1 -1
- package/lib/typescript/src/features/output.d.ts +28 -18
- package/lib/typescript/src/features/output.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/controls.d.ts +5 -0
- package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/index.d.ts +1 -0
- package/lib/typescript/src/features/playback/index.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts +3 -0
- package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +34 -0
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -0
- package/lib/typescript/src/features/playback/volume.d.ts +1 -1
- package/lib/typescript/src/features/player/options.d.ts +92 -93
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +206 -167
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +9 -2
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts +0 -13
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +18 -3
- package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native.d.ts +1 -2
- package/lib/typescript/src/native.d.ts.map +1 -1
- package/lib/typescript/src/specs/audio-browser.nitro.d.ts +17 -225
- package/lib/typescript/src/specs/audio-browser.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-native.d.ts +9 -3
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +145 -13
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +349 -85
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +0 -2
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +0 -2
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/artwork.d.ts +19 -0
- package/lib/typescript/src/utils/artwork.d.ts.map +1 -0
- package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +3 -1
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +24 -10
- package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +6 -6
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +1 -11
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +33 -1
- package/nitrogen/generated/android/c++/JArtworkVariants.hpp +61 -0
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +5 -0
- package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +3 -3
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +6 -0
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoritesMatchMode.hpp +58 -0
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +100 -0
- package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +3 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +112 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +121 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +123 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +133 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +121 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +139 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +134 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +6 -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 +131 -0
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +79 -0
- package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +0 -10
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +9 -4
- package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +10 -10
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +6 -0
- package/nitrogen/generated/android/c++/JGate.hpp +62 -0
- package/nitrogen/generated/android/c++/JGateDecision.hpp +64 -0
- package/nitrogen/generated/android/c++/{JThumbsRating.hpp → JGateEvent.hpp} +16 -15
- package/nitrogen/generated/android/c++/JGateReason.hpp +58 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +236 -212
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +25 -22
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +95 -0
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +33 -3
- package/nitrogen/generated/android/c++/JMediaReference.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +33 -1
- package/nitrogen/generated/android/c++/{JPartialAndroidSetupPlayerOptions.hpp → JNativeAndroidSetupOptions.hpp} +10 -10
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +61 -28
- package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +74 -0
- package/nitrogen/generated/android/c++/{JPartialIOSSetupPlayerOptions.hpp → JNativeIOSSetupOptions.hpp} +10 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +20 -9
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +182 -0
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +16 -19
- package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +6 -0
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +1 -11
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +96 -0
- package/nitrogen/generated/android/c++/JNotificationButton.hpp +3 -3
- package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +3 -17
- package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +7 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +14 -9
- package/nitrogen/generated/android/c++/{JIosOutput.hpp → JOutput.hpp} +15 -15
- package/nitrogen/generated/android/c++/JOutputType.hpp +82 -0
- package/nitrogen/generated/android/c++/JPlayback.hpp +2 -0
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +6 -0
- package/nitrogen/generated/android/c++/JPlaybackError.hpp +18 -3
- package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -0
- package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +76 -0
- package/nitrogen/generated/android/c++/JPlaybackState.hpp +3 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +11 -5
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +23 -5
- package/nitrogen/generated/android/c++/JRetryConfig.hpp +11 -7
- package/nitrogen/generated/android/c++/JSearchMode.hpp +27 -9
- package/nitrogen/generated/android/c++/JSearchParams.hpp +9 -3
- package/nitrogen/generated/android/c++/JStallReason.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +23 -5
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +6 -0
- package/nitrogen/generated/android/c++/JTrackRequest.hpp +93 -0
- package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +14 -1
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +71 -0
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.cpp +41 -0
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +121 -0
- package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioContentType.kt +4 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +2 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkRequestConfig.kt +10 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayNowPlayingButton.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{ThumbsRating.kt → FavoriteConfig.kt} +6 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{RatingType.kt → FavoritesMatchMode.kt} +5 -10
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +80 -0
- 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 +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.kt → Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.kt} +12 -12
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_void_RemoteSetRatingEvent.kt → Func_void_GateEvent.kt} +9 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_void_IosOutput.kt → Func_void_Output.kt} +9 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +9 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{HeartRating.kt → Gate.kt} +9 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PercentageRating.kt → GateDecision.kt} +9 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{StarRating.kt → GateEvent.kt} +6 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateReason.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +103 -109
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +24 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaReference.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +10 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PartialAndroidSetupPlayerOptions.kt → NativeAndroidSetupOptions.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +18 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{RemoteSetRatingEvent.kt → NativeGateRequest.kt} +12 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PartialIOSSetupPlayerOptions.kt → NativeIOSSetupOptions.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +4 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +6 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorType.kt +3 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +2 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButton.kt +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +3 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{IosOutput.kt → Output.kt} +6 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/OutputType.kt +31 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +12 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorKind.kt +29 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackState.kt +6 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +7 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchMode.kt +12 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StallReason.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +12 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +7 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +53 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +139 -28
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +1098 -447
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +60 -39
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +170 -112
- package/nitrogen/generated/ios/swift/AndroidAudioContentType.swift +8 -8
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +2 -12
- package/nitrogen/generated/ios/swift/ArtworkRequestConfig.swift +95 -1
- package/nitrogen/generated/ios/swift/ArtworkVariants.swift +34 -0
- package/nitrogen/generated/ios/swift/CarPlayNowPlayingButton.swift +4 -4
- package/nitrogen/generated/ios/swift/FavoriteConfig.swift +29 -0
- package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +40 -0
- package/nitrogen/generated/ios/swift/FormatNowPlayingParams.swift +67 -0
- package/nitrogen/generated/ios/swift/FormattedNavigationError.swift +17 -4
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.swift +54 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.swift +69 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___Track.swift +54 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.swift +60 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__optional_std__string____Track.swift +60 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.swift +61 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.swift +61 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.swift +77 -0
- package/nitrogen/generated/ios/swift/Func_void_GateDecision.swift +46 -0
- package/nitrogen/generated/ios/swift/{Func_void_RemoteSetRatingEvent.swift → Func_void_GateEvent.swift} +10 -10
- package/nitrogen/generated/ios/swift/{Func_void_IosOutput.swift → Func_void_Output.swift} +10 -10
- package/nitrogen/generated/ios/swift/Func_void_TransformableRequestConfig.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_std__optional_NowPlayingUpdate_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__optional_std__string_.swift +53 -0
- package/nitrogen/generated/ios/swift/{Func_void_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError___.swift → Func_void_std__shared_ptr_Promise_GateDecision__.swift} +17 -17
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_Promise_TransformableRequestConfig__.swift +66 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError____.swift +81 -0
- package/nitrogen/generated/ios/swift/Gate.swift +47 -0
- package/nitrogen/generated/ios/swift/GateDecision.swift +40 -0
- package/nitrogen/generated/ios/swift/GateEvent.swift +29 -0
- package/nitrogen/generated/ios/swift/GateReason.swift +40 -0
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +20 -14
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +230 -197
- package/nitrogen/generated/ios/swift/IOSOptions.swift +51 -0
- package/nitrogen/generated/ios/swift/ImageRowItem.swift +121 -2
- package/nitrogen/generated/ios/swift/MediaReference.swift +40 -0
- package/nitrogen/generated/ios/swift/MediaRequestConfig.swift +107 -1
- package/nitrogen/generated/ios/swift/{PartialAndroidSetupPlayerOptions.swift → NativeAndroidSetupOptions.swift} +5 -5
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +161 -22
- package/nitrogen/generated/ios/swift/NativeGateRequest.swift +58 -0
- package/nitrogen/generated/ios/swift/{PartialIOSSetupPlayerOptions.swift → NativeIOSSetupOptions.swift} +5 -5
- package/nitrogen/generated/ios/swift/NativeRouteEntry.swift +19 -19
- package/nitrogen/generated/ios/swift/NativeSetupPlayerOptions.swift +206 -0
- package/nitrogen/generated/ios/swift/NativeUpdateOptions.swift +12 -25
- package/nitrogen/generated/ios/swift/NavigationErrorType.swift +8 -0
- package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +1 -30
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +90 -0
- package/nitrogen/generated/ios/swift/NotificationButton.swift +4 -4
- package/nitrogen/generated/ios/swift/NowPlayingMetadata.swift +1 -48
- package/nitrogen/generated/ios/swift/NowPlayingUpdate.swift +19 -1
- package/nitrogen/generated/ios/swift/Options.swift +10 -4
- package/nitrogen/generated/ios/swift/{IosOutput.swift → Output.swift} +7 -7
- package/nitrogen/generated/ios/swift/OutputType.swift +72 -0
- package/nitrogen/generated/ios/swift/PlaybackError.swift +43 -2
- package/nitrogen/generated/ios/swift/PlaybackErrorKind.swift +64 -0
- package/nitrogen/generated/ios/swift/PlaybackState.swift +4 -4
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +27 -8
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +74 -8
- package/nitrogen/generated/ios/swift/RetryConfig.swift +35 -4
- package/nitrogen/generated/ios/swift/SearchMode.swift +36 -12
- package/nitrogen/generated/ios/swift/SearchParams.swift +7 -2
- package/nitrogen/generated/ios/swift/StallReason.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +74 -8
- package/nitrogen/generated/ios/swift/TrackRequest.swift +106 -0
- package/nitrogen/generated/ios/swift/TransformableRequestConfig.swift +60 -1
- package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +19 -0
- package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +18 -0
- package/nitrogen/generated/shared/c++/AndroidAudioContentType.hpp +10 -10
- package/nitrogen/generated/shared/c++/AndroidOptions.hpp +1 -12
- package/nitrogen/generated/shared/c++/ArtworkRequestConfig.hpp +10 -2
- package/nitrogen/generated/shared/c++/ArtworkVariants.hpp +87 -0
- package/nitrogen/generated/shared/c++/CarPlayNowPlayingButton.hpp +6 -6
- package/nitrogen/generated/shared/c++/EqualizerSettings.hpp +0 -2
- package/nitrogen/generated/shared/c++/{StarRating.hpp → FavoriteConfig.hpp} +18 -17
- package/nitrogen/generated/shared/c++/FavoritesMatchMode.hpp +76 -0
- package/nitrogen/generated/shared/c++/FormatNowPlayingParams.hpp +110 -0
- package/nitrogen/generated/shared/c++/FormattedNavigationError.hpp +6 -5
- package/nitrogen/generated/shared/c++/{HeartRating.hpp → Gate.hpp} +21 -16
- package/nitrogen/generated/shared/c++/{ThumbsRating.hpp → GateDecision.hpp} +23 -17
- package/nitrogen/generated/shared/c++/{PercentageRating.hpp → GateEvent.hpp} +18 -17
- package/nitrogen/generated/shared/c++/GateReason.hpp +76 -0
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +21 -18
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +44 -35
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +91 -0
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +33 -2
- package/nitrogen/generated/shared/c++/MediaReference.hpp +76 -0
- package/nitrogen/generated/shared/c++/MediaRequestConfig.hpp +10 -2
- package/nitrogen/generated/shared/c++/{PartialAndroidSetupPlayerOptions.hpp → NativeAndroidSetupOptions.hpp} +11 -11
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +31 -15
- package/nitrogen/generated/shared/c++/NativeGateRequest.hpp +97 -0
- package/nitrogen/generated/shared/c++/{PartialIOSSetupPlayerOptions.hpp → NativeIOSSetupOptions.hpp} +11 -11
- package/nitrogen/generated/shared/c++/NativeRouteEntry.hpp +5 -5
- package/nitrogen/generated/shared/c++/NativeSetupPlayerOptions.hpp +138 -0
- package/nitrogen/generated/shared/c++/NativeUpdateOptions.hpp +9 -7
- package/nitrogen/generated/shared/c++/NavigationErrorType.hpp +8 -0
- package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +1 -12
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +92 -0
- package/nitrogen/generated/shared/c++/NotificationButton.hpp +8 -8
- package/nitrogen/generated/shared/c++/NowPlayingMetadata.hpp +3 -19
- package/nitrogen/generated/shared/c++/NowPlayingUpdate.hpp +6 -2
- package/nitrogen/generated/shared/c++/Options.hpp +8 -8
- package/nitrogen/generated/shared/c++/{IosOutput.hpp → Output.hpp} +18 -18
- package/nitrogen/generated/shared/c++/OutputType.hpp +108 -0
- package/nitrogen/generated/shared/c++/PlaybackError.hpp +18 -3
- package/nitrogen/generated/shared/c++/PlaybackErrorKind.hpp +100 -0
- package/nitrogen/generated/shared/c++/PlaybackState.hpp +9 -9
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +9 -6
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +24 -5
- package/nitrogen/generated/shared/c++/RetryConfig.hpp +10 -6
- package/nitrogen/generated/shared/c++/SearchMode.hpp +39 -15
- package/nitrogen/generated/shared/c++/SearchParams.hpp +9 -2
- package/nitrogen/generated/shared/c++/StallReason.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +24 -5
- package/nitrogen/generated/shared/c++/TrackRequest.hpp +93 -0
- package/nitrogen/generated/shared/c++/TransformableRequestConfig.hpp +6 -2
- package/package.json +11 -6
- package/src/codegen-no-config-variant.test.ts +70 -0
- package/src/features/browser-config.test.ts +110 -0
- package/src/features/browser-config.ts +293 -0
- package/src/features/browser.test.ts +19 -0
- package/src/features/browser.ts +58 -201
- package/src/features/carConnection.ts +36 -0
- package/src/features/errors.ts +68 -2
- package/src/features/gate.test.ts +72 -0
- package/src/features/gate.ts +155 -0
- package/src/features/index.ts +2 -0
- package/src/features/metadata.ts +6 -12
- package/src/features/nowPlaying.ts +36 -0
- package/src/features/output.ts +35 -22
- package/src/features/playback/controls.ts +8 -0
- package/src/features/playback/index.ts +1 -0
- package/src/features/playback/progress.ts +31 -12
- package/src/features/playback/trackPlaybackTime.test.ts +66 -0
- package/src/features/playback/trackPlaybackTime.ts +106 -0
- package/src/features/playback/volume.ts +1 -1
- package/src/features/player/options.test.ts +38 -0
- package/src/features/player/options.ts +140 -75
- package/src/features/player/setup.test.ts +117 -0
- package/src/features/player/setup.ts +390 -175
- package/src/features/queue/queue.test.ts +36 -0
- package/src/features/queue/queue.ts +15 -4
- package/src/features/remoteControls.ts +0 -23
- package/src/features/sleepTimer.ts +23 -4
- package/src/native.ts +5 -0
- package/src/native.web.ts +4 -1
- package/src/specs/audio-browser.nitro.ts +131 -41
- package/src/types/browser-native.ts +19 -6
- package/src/types/browser-nodes.ts +160 -13
- package/src/types/browser.ts +377 -89
- package/src/utils/LazyNativeEmitter.ts +0 -2
- package/src/utils/NativeUpdatedValue.ts +0 -2
- package/src/utils/artwork.ts +30 -0
- package/src/web/NativeAudioBrowser.playWhenReady.test.ts +301 -0
- package/src/web/NativeAudioBrowser.ts +365 -158
- package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +139 -0
- package/src/web/TrackPlayer/PlaybackStateMachine.ts +82 -0
- package/src/web/TrackPlayer/PlaybackTimer.test.ts +67 -0
- package/src/web/TrackPlayer/PlaybackTimer.ts +33 -0
- package/src/web/TrackPlayer/Player.dispatch.test.ts +229 -0
- package/src/web/TrackPlayer/Player.load.test.ts +86 -0
- package/src/web/TrackPlayer/Player.ts +113 -39
- package/src/web/TrackPlayer/PlayingStateFactory.test.ts +51 -0
- package/src/web/TrackPlayer/PlayingStateFactory.ts +24 -0
- package/src/web/TrackPlayer/QueueManager.test.ts +289 -0
- package/src/web/TrackPlayer/QueueManager.ts +302 -0
- package/src/web/TrackPlayer/QueuePlayer.ts +188 -0
- package/src/web/TrackPlayer/RepeatMode.ts +1 -1
- package/src/web/TrackPlayer/SleepTimer.ts +38 -3
- package/src/web/TrackPlayer/VolumeFader.ts +74 -0
- package/src/web/TrackPlayer/index.ts +2 -1
- package/src/web/browser/BrowserManager.ts +231 -62
- package/src/web/browser/NavigationErrorManager.ts +3 -1
- package/src/web/browser/SearchManager.ts +11 -15
- package/src/web/browser/__tests__/layer-resolver.test.ts +69 -0
- package/src/web/browser/assertBrowsePageShape.test.ts +27 -0
- package/src/web/browser/assertBrowsePageShape.ts +27 -0
- package/src/web/browser/classifyTrackNavigation.test.ts +41 -0
- package/src/web/browser/classifyTrackNavigation.ts +35 -0
- package/src/web/browser/parseSearchResponse.test.ts +25 -0
- package/src/web/browser/parseSearchResponse.ts +15 -0
- package/src/web/http/HttpClient.ts +5 -35
- package/src/web/http/RequestConfigBuilder.test.ts +59 -0
- package/src/web/http/RequestConfigBuilder.ts +120 -63
- package/src/web/playbackErrorKind.test.ts +55 -0
- package/src/web/playbackErrorKind.ts +56 -0
- package/src/web/player/MediaSessionManager.test.ts +220 -0
- package/src/web/player/MediaSessionManager.ts +151 -0
- package/src/web/player/NowPlayingManager.test.ts +46 -0
- package/src/web/player/NowPlayingManager.ts +6 -5
- package/src/web/player/OptionsManager.ts +12 -18
- package/src/web/player/RemoteCommandController.test.ts +171 -0
- package/src/web/player/RemoteCommandController.ts +166 -0
- package/android/src/main/java/com/audiobrowser/player/PlaybackProgressUpdateManager.kt +0 -59
- package/android/src/main/java/com/audiobrowser/util/RatingFactory.kt +0 -72
- package/android/src/main/java/com/audiobrowser/util/ResolvedTrackFactory.kt +0 -36
- package/android/src/test/java/com/audiobrowser/player/PlaybackProgressUpdateManagerTest.kt +0 -146
- package/ios/CarPlay/Track+CarPlay.swift +0 -63
- package/ios/NowPlayingInfo/NowPlayingInfoCenter.swift +0 -9
- package/ios/Player/PlaybackProgressUpdateManager.swift +0 -68
- package/ios/Tests/PlaybackProgressUpdateManagerTests.swift +0 -166
- package/lib/commonjs/features/rating.js +0 -2
- package/lib/commonjs/features/rating.js.map +0 -1
- package/lib/commonjs/types/player.js +0 -2
- package/lib/commonjs/types/player.js.map +0 -1
- package/lib/commonjs/web/SimpleRouter.test.js +0 -174
- package/lib/commonjs/web/SimpleRouter.test.js.map +0 -1
- package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js +0 -293
- package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js.map +0 -1
- package/lib/module/features/rating.js +0 -2
- package/lib/module/features/rating.js.map +0 -1
- package/lib/module/types/player.js +0 -2
- package/lib/module/types/player.js.map +0 -1
- package/lib/module/web/SimpleRouter.test.js +0 -174
- package/lib/module/web/SimpleRouter.test.js.map +0 -1
- package/lib/module/web/TrackPlayer/PlaylistPlayer.js +0 -288
- package/lib/module/web/TrackPlayer/PlaylistPlayer.js.map +0 -1
- package/lib/typescript/src/features/rating.d.ts +0 -14
- package/lib/typescript/src/features/rating.d.ts.map +0 -1
- package/lib/typescript/src/native.web.d.ts +0 -3
- package/lib/typescript/src/native.web.d.ts.map +0 -1
- package/lib/typescript/src/types/player.d.ts +0 -64
- package/lib/typescript/src/types/player.d.ts.map +0 -1
- package/lib/typescript/src/web/NativeAudioBrowser.d.ts +0 -188
- package/lib/typescript/src/web/NativeAudioBrowser.d.ts.map +0 -1
- package/lib/typescript/src/web/SimpleRouter.d.ts +0 -40
- package/lib/typescript/src/web/SimpleRouter.d.ts.map +0 -1
- package/lib/typescript/src/web/SimpleRouter.test.d.ts +0 -2
- package/lib/typescript/src/web/SimpleRouter.test.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/Event.d.ts +0 -11
- package/lib/typescript/src/web/TrackPlayer/Event.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/Player.d.ts +0 -59
- package/lib/typescript/src/web/TrackPlayer/Player.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts +0 -39
- package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/RepeatMode.d.ts +0 -6
- package/lib/typescript/src/web/TrackPlayer/RepeatMode.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/SetupNotCalledError.d.ts +0 -4
- package/lib/typescript/src/web/TrackPlayer/SetupNotCalledError.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/SleepTimer.d.ts +0 -34
- package/lib/typescript/src/web/TrackPlayer/SleepTimer.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/State.d.ts +0 -13
- package/lib/typescript/src/web/TrackPlayer/State.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/index.d.ts +0 -5
- package/lib/typescript/src/web/TrackPlayer/index.d.ts.map +0 -1
- package/lib/typescript/src/web/browser/BrowserManager.d.ts +0 -154
- package/lib/typescript/src/web/browser/BrowserManager.d.ts.map +0 -1
- package/lib/typescript/src/web/browser/FavoriteManager.d.ts +0 -42
- package/lib/typescript/src/web/browser/FavoriteManager.d.ts.map +0 -1
- package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts +0 -40
- package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts.map +0 -1
- package/lib/typescript/src/web/browser/SearchManager.d.ts +0 -22
- package/lib/typescript/src/web/browser/SearchManager.d.ts.map +0 -1
- package/lib/typescript/src/web/http/HttpClient.d.ts +0 -25
- package/lib/typescript/src/web/http/HttpClient.d.ts.map +0 -1
- package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts +0 -98
- package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts.map +0 -1
- package/lib/typescript/src/web/player/NowPlayingManager.d.ts +0 -34
- package/lib/typescript/src/web/player/NowPlayingManager.d.ts.map +0 -1
- package/lib/typescript/src/web/player/OptionsManager.d.ts +0 -32
- package/lib/typescript/src/web/player/OptionsManager.d.ts.map +0 -1
- package/lib/typescript/src/web/util/BrowserPathHelper.d.ts +0 -69
- package/lib/typescript/src/web/util/BrowserPathHelper.d.ts.map +0 -1
- package/lib/typescript/src/web/util/shuffle.d.ts +0 -8
- package/lib/typescript/src/web/util/shuffle.d.ts.map +0 -1
- package/nitrogen/generated/android/c++/JAndroidUpdateOptions.hpp +0 -86
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.hpp +0 -147
- package/nitrogen/generated/android/c++/JFunc_void_IosOutput.hpp +0 -80
- package/nitrogen/generated/android/c++/JFunc_void_RemoteSetRatingEvent.hpp +0 -87
- package/nitrogen/generated/android/c++/JHeartRating.hpp +0 -57
- package/nitrogen/generated/android/c++/JIosOutputType.hpp +0 -85
- package/nitrogen/generated/android/c++/JPartialSetupPlayerOptions.hpp +0 -96
- package/nitrogen/generated/android/c++/JPercentageRating.hpp +0 -57
- package/nitrogen/generated/android/c++/JRatingType.hpp +0 -73
- package/nitrogen/generated/android/c++/JRemoteSetRatingEvent.hpp +0 -66
- package/nitrogen/generated/android/c++/JStarRating.hpp +0 -57
- package/nitrogen/generated/android/c++/JUpdateOptions.hpp +0 -105
- package/nitrogen/generated/android/c++/JVariant_HeartRating_ThumbsRating_StarRating_PercentageRating.cpp +0 -34
- package/nitrogen/generated/android/c++/JVariant_HeartRating_ThumbsRating_StarRating_PercentageRating.hpp +0 -105
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidUpdateOptions.kt +0 -50
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IosOutputType.kt +0 -32
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PartialSetupPlayerOptions.kt +0 -47
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/UpdateOptions.kt +0 -53
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_HeartRating_ThumbsRating_StarRating_PercentageRating.kt +0 -77
- package/nitrogen/generated/ios/swift/AndroidUpdateOptions.swift +0 -119
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.swift +0 -68
- package/nitrogen/generated/ios/swift/HeartRating.swift +0 -29
- package/nitrogen/generated/ios/swift/IosOutputType.swift +0 -76
- package/nitrogen/generated/ios/swift/PartialSetupPlayerOptions.swift +0 -101
- package/nitrogen/generated/ios/swift/PercentageRating.swift +0 -29
- package/nitrogen/generated/ios/swift/RatingType.swift +0 -60
- package/nitrogen/generated/ios/swift/RemoteSetRatingEvent.swift +0 -58
- package/nitrogen/generated/ios/swift/StarRating.swift +0 -29
- package/nitrogen/generated/ios/swift/ThumbsRating.swift +0 -29
- package/nitrogen/generated/ios/swift/UpdateOptions.swift +0 -143
- package/nitrogen/generated/ios/swift/Variant_HeartRating_ThumbsRating_StarRating_PercentageRating.swift +0 -20
- package/nitrogen/generated/shared/c++/AndroidUpdateOptions.hpp +0 -109
- package/nitrogen/generated/shared/c++/IosOutputType.hpp +0 -112
- package/nitrogen/generated/shared/c++/PartialSetupPlayerOptions.hpp +0 -104
- package/nitrogen/generated/shared/c++/RatingType.hpp +0 -96
- package/nitrogen/generated/shared/c++/RemoteSetRatingEvent.hpp +0 -94
- package/nitrogen/generated/shared/c++/UpdateOptions.hpp +0 -111
- package/src/features/rating.ts +0 -17
- package/src/types/player.ts +0 -69
- package/src/web/TrackPlayer/PlaylistPlayer.ts +0 -346
|
@@ -29,16 +29,48 @@ public final class RNABCarPlayController: NSObject {
|
|
|
29
29
|
private var isStarted = false
|
|
30
30
|
private var listenerRemovals: [() -> Void] = []
|
|
31
31
|
|
|
32
|
-
///
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
///
|
|
40
|
-
|
|
41
|
-
|
|
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)
|
|
42
74
|
|
|
43
75
|
/// Helper object for CPInterfaceControllerDelegate conformance
|
|
44
76
|
private var interfaceDelegate: InterfaceControllerDelegate?
|
|
@@ -57,9 +89,21 @@ public final class RNABCarPlayController: NSObject {
|
|
|
57
89
|
audioBrowser?.browserManager.config ?? BrowserConfig()
|
|
58
90
|
}
|
|
59
91
|
|
|
60
|
-
///
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
63
107
|
}
|
|
64
108
|
|
|
65
109
|
// MARK: - Initialization
|
|
@@ -74,10 +118,22 @@ public final class RNABCarPlayController: NSObject {
|
|
|
74
118
|
self?.listItemFactory?.createListItem(for: track, handler: handler)
|
|
75
119
|
?? CPListItem(text: track.title, detailText: nil)
|
|
76
120
|
}
|
|
121
|
+
nowPlayingManager.navigateToUrl = { [weak self] url, title in
|
|
122
|
+
Task { @MainActor in await self?.navigateToUrl(url, title: title) }
|
|
123
|
+
}
|
|
77
124
|
}
|
|
78
125
|
|
|
79
126
|
// MARK: - Lifecycle
|
|
80
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.
|
|
132
|
+
@objc
|
|
133
|
+
public static func setConnected(_ connected: Bool) {
|
|
134
|
+
HybridAudioBrowser.setCarPlayConnected(connected)
|
|
135
|
+
}
|
|
136
|
+
|
|
81
137
|
@objc
|
|
82
138
|
public func start() {
|
|
83
139
|
guard !isStarted else { return }
|
|
@@ -90,6 +146,18 @@ public final class RNABCarPlayController: NSObject {
|
|
|
90
146
|
interfaceDelegate = delegate
|
|
91
147
|
interfaceController.delegate = delegate
|
|
92
148
|
|
|
149
|
+
// Restart when a new HybridAudioBrowser replaces the shared instance
|
|
150
|
+
// (JS runtime reload) — our subscriptions point at the old instance's
|
|
151
|
+
// emitters and would otherwise go silent.
|
|
152
|
+
let instanceToken = HybridAudioBrowser.instanceChangedEmitter.addListener { [weak self] _ in
|
|
153
|
+
Task { @MainActor in
|
|
154
|
+
self?.restart()
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
listenerRemovals.append {
|
|
158
|
+
HybridAudioBrowser.instanceChangedEmitter.removeListener(instanceToken)
|
|
159
|
+
}
|
|
160
|
+
|
|
93
161
|
// Show loading template while waiting
|
|
94
162
|
showLoadingTemplate()
|
|
95
163
|
|
|
@@ -99,20 +167,21 @@ public final class RNABCarPlayController: NSObject {
|
|
|
99
167
|
guard self.isStarted else { return }
|
|
100
168
|
self.logger.debug("AudioBrowser and player ready, setting up CarPlay")
|
|
101
169
|
self.audioBrowser = browser
|
|
170
|
+
self.isGated = browser.isGateActive
|
|
102
171
|
self.trackSelector = TrackSelector(browserManager: browser.browserManager)
|
|
103
172
|
|
|
104
173
|
// Create image loader with CarPlay display traits
|
|
105
174
|
self.imageLoader = CarPlayImageLoader(
|
|
106
175
|
carTraitCollection: self.interfaceController.carTraitCollection,
|
|
107
|
-
browserManager: browser.browserManager
|
|
176
|
+
browserManager: browser.browserManager,
|
|
108
177
|
)
|
|
109
178
|
|
|
110
179
|
// Create list item factory
|
|
111
180
|
let factory = CarPlayListItemFactory(
|
|
112
|
-
isActiveTrack: { [weak self] src in self?.isActiveTrack(src: src) ?? false },
|
|
181
|
+
isActiveTrack: { [weak self] id, src in self?.isActiveTrack(id: id, src: src) ?? false },
|
|
113
182
|
onItemSelected: { [weak self] track, completion in
|
|
114
183
|
self?.handleItemSelection(track: track, completion: completion)
|
|
115
|
-
}
|
|
184
|
+
},
|
|
116
185
|
)
|
|
117
186
|
factory.imageLoader = self.imageLoader
|
|
118
187
|
self.listItemFactory = factory
|
|
@@ -134,18 +203,33 @@ public final class RNABCarPlayController: NSObject {
|
|
|
134
203
|
logger.info("Stopping CarPlay controller")
|
|
135
204
|
|
|
136
205
|
// Remove all emitter listeners
|
|
137
|
-
for removal in listenerRemovals {
|
|
206
|
+
for removal in listenerRemovals {
|
|
207
|
+
removal()
|
|
208
|
+
}
|
|
138
209
|
listenerRemovals.removeAll()
|
|
139
210
|
|
|
140
211
|
// Clear config callback
|
|
141
212
|
audioBrowser?.browserManager.onConfigChanged = nil
|
|
213
|
+
audioBrowser?.browserManager.onFavoritesChanged = nil
|
|
142
214
|
|
|
143
215
|
nowPlayingManager.teardown()
|
|
144
216
|
listItemFactory = nil
|
|
145
217
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
218
|
+
navigatingPaths.removeAll()
|
|
219
|
+
loadingTemplates.removeAll()
|
|
220
|
+
pendingTabs = nil
|
|
221
|
+
}
|
|
222
|
+
|
|
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
|
+
private func restart() {
|
|
229
|
+
guard isStarted else { return }
|
|
230
|
+
logger.info("AudioBrowser instance changed — restarting CarPlay controller")
|
|
231
|
+
stop()
|
|
232
|
+
start()
|
|
149
233
|
}
|
|
150
234
|
|
|
151
235
|
// MARK: - Content Subscriptions
|
|
@@ -176,10 +260,35 @@ public final class RNABCarPlayController: NSObject {
|
|
|
176
260
|
audioBrowser?.contentChangedEmitter.removeListener(contentToken)
|
|
177
261
|
}
|
|
178
262
|
|
|
179
|
-
// Subscribe to config changes (for Now Playing buttons
|
|
263
|
+
// Subscribe to config changes (for Now Playing buttons and per-track
|
|
264
|
+
// button state — e.g. resolveAlbumUrl appearing/disappearing)
|
|
180
265
|
audioBrowser.browserManager.onConfigChanged = { [weak self] _ in
|
|
181
266
|
Task { @MainActor in
|
|
182
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()
|
|
183
292
|
}
|
|
184
293
|
}
|
|
185
294
|
|
|
@@ -193,14 +302,54 @@ public final class RNABCarPlayController: NSObject {
|
|
|
193
302
|
audioBrowser?.favoriteChangedEmitter.removeListener(favoriteToken)
|
|
194
303
|
}
|
|
195
304
|
|
|
196
|
-
//
|
|
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).
|
|
197
328
|
let externalContentToken = audioBrowser.externalContentChangedEmitter.addListener { [weak self] path in
|
|
198
|
-
|
|
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
|
+
}
|
|
199
338
|
}
|
|
200
339
|
listenerRemovals.append { [weak audioBrowser] in
|
|
201
340
|
audioBrowser?.externalContentChangedEmitter.removeListener(externalContentToken)
|
|
202
341
|
}
|
|
203
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
|
+
|
|
204
353
|
// Subscribe to active track changes (for playing indicator in lists)
|
|
205
354
|
let activeTrackToken = audioBrowser.activeTrackChangedEmitter.addListener { [weak self] event in
|
|
206
355
|
Task { @MainActor in
|
|
@@ -221,6 +370,17 @@ public final class RNABCarPlayController: NSObject {
|
|
|
221
370
|
audioBrowser?.queueChangedEmitter.removeListener(queueToken)
|
|
222
371
|
}
|
|
223
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
|
+
|
|
224
384
|
// Subscribe to navigation errors (from browser layer)
|
|
225
385
|
let navErrorToken = audioBrowser.navigationErrorEmitter.addListener { [weak self] event in
|
|
226
386
|
Task { @MainActor in
|
|
@@ -237,8 +397,9 @@ public final class RNABCarPlayController: NSObject {
|
|
|
237
397
|
private func handleNavigationError(_ event: NavigationErrorEvent) {
|
|
238
398
|
guard let error = event.error else { return }
|
|
239
399
|
logger.warning("Navigation error: \(error.code.stringValue) - \(error.message)")
|
|
240
|
-
//
|
|
241
|
-
|
|
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) } ?? "/"
|
|
242
403
|
showNavigationError(error, path: path)
|
|
243
404
|
}
|
|
244
405
|
|
|
@@ -248,7 +409,9 @@ public final class RNABCarPlayController: NSObject {
|
|
|
248
409
|
private func buildInitialInterface() async {
|
|
249
410
|
guard let audioBrowser else {
|
|
250
411
|
logger.error("AudioBrowser not available")
|
|
251
|
-
|
|
412
|
+
await showRootNavigationError(
|
|
413
|
+
NavigationError(code: .unknownError, message: "", statusCode: nil, statusCodeSuccess: nil),
|
|
414
|
+
)
|
|
252
415
|
return
|
|
253
416
|
}
|
|
254
417
|
|
|
@@ -260,33 +423,65 @@ public final class RNABCarPlayController: NSObject {
|
|
|
260
423
|
} else {
|
|
261
424
|
// No tabs yet - query them
|
|
262
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
|
+
}
|
|
263
432
|
do {
|
|
264
433
|
let queriedTabs = try await audioBrowser.browserManager.queryTabs()
|
|
265
434
|
if !queriedTabs.isEmpty {
|
|
266
435
|
await showTabBar(tabs: queriedTabs)
|
|
267
436
|
} else {
|
|
268
|
-
|
|
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
|
+
)
|
|
269
442
|
}
|
|
270
443
|
} catch {
|
|
271
444
|
logger.error("Failed to query tabs: \(error.localizedDescription)")
|
|
272
|
-
|
|
445
|
+
await showRootNavigationError(NavigationError.from(error))
|
|
273
446
|
}
|
|
274
447
|
}
|
|
275
448
|
}
|
|
276
449
|
|
|
277
|
-
/// Shows a loading template while waiting for initialization
|
|
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.
|
|
278
454
|
private func showLoadingTemplate() {
|
|
279
|
-
let template =
|
|
280
|
-
|
|
281
|
-
sections: [],
|
|
282
|
-
)
|
|
283
|
-
interfaceController.setRootTemplate(template, animated: false, completion: nil)
|
|
455
|
+
let template = makeLoadingTemplate(title: nil, path: nil)
|
|
456
|
+
interfaceController.safeSetRoot(template, animated: false)
|
|
284
457
|
}
|
|
285
458
|
|
|
286
459
|
// MARK: - Tab Bar
|
|
287
460
|
|
|
288
461
|
@MainActor
|
|
289
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
|
+
|
|
290
485
|
logger.info("Building tab bar with \(tabs.count) tabs")
|
|
291
486
|
|
|
292
487
|
let maxTabs = CPTabBarTemplate.maximumTabCount
|
|
@@ -296,10 +491,14 @@ public final class RNABCarPlayController: NSObject {
|
|
|
296
491
|
createTabTemplate(for: tab)
|
|
297
492
|
}
|
|
298
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
|
+
|
|
299
498
|
// Set the tab bar immediately so UI appears fast
|
|
300
499
|
logger.info("Setting tab bar root template with \(tabTemplates.count) templates")
|
|
301
500
|
let tabBar = CPTabBarTemplate(templates: tabTemplates)
|
|
302
|
-
interfaceController.
|
|
501
|
+
interfaceController.safeSetRoot(tabBar, animated: true)
|
|
303
502
|
|
|
304
503
|
// Load content for the first tab only - others load lazily when selected
|
|
305
504
|
if let firstTemplate = tabTemplates.first, let firstTab = tabs.first, let url = firstTab.url {
|
|
@@ -307,27 +506,93 @@ public final class RNABCarPlayController: NSObject {
|
|
|
307
506
|
}
|
|
308
507
|
}
|
|
309
508
|
|
|
310
|
-
///
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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 }
|
|
316
521
|
|
|
317
|
-
|
|
318
|
-
|
|
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
|
+
}
|
|
319
565
|
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
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)
|
|
323
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
|
+
}
|
|
324
583
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
329
590
|
|
|
330
|
-
|
|
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) {
|
|
331
596
|
let (symbolName, _, _) = SFSymbolRenderer.parseArtwork(artwork)
|
|
332
597
|
if let image = imageLoader?.sfSymbolImage(symbolName) {
|
|
333
598
|
template.tabImage = image
|
|
@@ -342,28 +607,15 @@ public final class RNABCarPlayController: NSObject {
|
|
|
342
607
|
}
|
|
343
608
|
}
|
|
344
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
|
|
345
614
|
}
|
|
346
|
-
|
|
347
|
-
return template
|
|
348
615
|
}
|
|
349
616
|
|
|
350
617
|
// MARK: - List Templates
|
|
351
618
|
|
|
352
|
-
@MainActor
|
|
353
|
-
private func createListTemplate(
|
|
354
|
-
for resolvedTrack: ResolvedTrack,
|
|
355
|
-
path: String,
|
|
356
|
-
) -> CPListTemplate {
|
|
357
|
-
let template = CPListTemplate(
|
|
358
|
-
title: resolvedTrack.title,
|
|
359
|
-
sections: [],
|
|
360
|
-
)
|
|
361
|
-
updateTemplate(template, with: resolvedTrack)
|
|
362
|
-
template.userInfo = ["path": path] as [String: Any]
|
|
363
|
-
|
|
364
|
-
return template
|
|
365
|
-
}
|
|
366
|
-
|
|
367
619
|
/// Finds the path associated with a template, if any
|
|
368
620
|
private func getPath(from template: CPTemplate) -> String? {
|
|
369
621
|
(template.userInfo as? [String: Any])?["path"] as? String
|
|
@@ -379,7 +631,6 @@ public final class RNABCarPlayController: NSObject {
|
|
|
379
631
|
/// Configures the assistant cell ("Ask Siri to Play Audio") on a template
|
|
380
632
|
/// based on the `carPlaySiriListButton` property of the resolved content.
|
|
381
633
|
private func configureAssistantCell(on template: CPListTemplate, from resolvedTrack: ResolvedTrack) {
|
|
382
|
-
guard #available(iOS 15.4, *) else { return }
|
|
383
634
|
guard let position = resolvedTrack.carPlaySiriListButton else {
|
|
384
635
|
template.assistantCellConfiguration = nil
|
|
385
636
|
return
|
|
@@ -394,15 +645,96 @@ public final class RNABCarPlayController: NSObject {
|
|
|
394
645
|
|
|
395
646
|
// MARK: - Content Loading
|
|
396
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.
|
|
397
653
|
@MainActor
|
|
398
654
|
private func loadContent(for path: String, into template: CPListTemplate) async {
|
|
399
655
|
guard let audioBrowser else { return }
|
|
400
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
|
+
|
|
401
678
|
do {
|
|
402
679
|
let resolved = try await audioBrowser.browserManager.resolve(path, useCache: true)
|
|
403
|
-
|
|
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
|
+
}
|
|
404
696
|
} catch {
|
|
697
|
+
watchdog.cancel()
|
|
405
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
|
|
406
738
|
}
|
|
407
739
|
}
|
|
408
740
|
|
|
@@ -416,17 +748,8 @@ public final class RNABCarPlayController: NSObject {
|
|
|
416
748
|
return
|
|
417
749
|
}
|
|
418
750
|
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
if let key, key == pendingSelectionKey {
|
|
422
|
-
completion()
|
|
423
|
-
return
|
|
424
|
-
}
|
|
425
|
-
pendingSelectionKey = key
|
|
426
|
-
|
|
427
|
-
// If this track is already loaded, resume playback and show Now Playing
|
|
428
|
-
if let src = track.src, isActiveTrack(src: src) {
|
|
429
|
-
pendingSelectionKey = nil
|
|
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) {
|
|
430
753
|
try? audioBrowser.play()
|
|
431
754
|
nowPlayingManager.showNowPlaying()
|
|
432
755
|
completion()
|
|
@@ -434,78 +757,171 @@ public final class RNABCarPlayController: NSObject {
|
|
|
434
757
|
}
|
|
435
758
|
|
|
436
759
|
guard let player = audioBrowser.getPlayer(), let trackSelector else {
|
|
437
|
-
pendingSelectionKey = nil
|
|
438
760
|
completion()
|
|
439
761
|
return
|
|
440
762
|
}
|
|
441
763
|
|
|
442
|
-
|
|
443
|
-
|
|
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
|
|
444
785
|
let result = await trackSelector.select(track: track, player: player)
|
|
786
|
+
guard let self else { return }
|
|
445
787
|
switch result {
|
|
446
|
-
case .play(
|
|
788
|
+
case let .play(intent):
|
|
447
789
|
self.executePlayback(intent, player: player)
|
|
448
|
-
self.nowPlayingManager.showNowPlaying()
|
|
790
|
+
self.nowPlayingManager.showNowPlaying(popToFront: false)
|
|
449
791
|
case .intercepted:
|
|
450
|
-
self.nowPlayingManager.showNowPlaying()
|
|
451
|
-
case .browse(
|
|
452
|
-
self.navigateToUrl(url,
|
|
453
|
-
return // navigateToUrl handles its own completion
|
|
792
|
+
self.nowPlayingManager.showNowPlaying(popToFront: false)
|
|
793
|
+
case let .browse(url):
|
|
794
|
+
await self.navigateToUrl(url, title: track.title)
|
|
454
795
|
case .none:
|
|
455
796
|
break
|
|
456
797
|
}
|
|
457
|
-
completion()
|
|
458
798
|
}
|
|
459
799
|
}
|
|
460
800
|
|
|
461
801
|
private func executePlayback(_ intent: TrackSelector.PlaybackIntent, player: TrackPlayer) {
|
|
462
802
|
switch intent {
|
|
463
|
-
case .skipTo(
|
|
803
|
+
case let .skipTo(index):
|
|
464
804
|
try? player.skipTo(index, playWhenReady: true)
|
|
465
|
-
case .setQueue(
|
|
805
|
+
case let .setQueue(tracks, startIndex, sourcePath):
|
|
466
806
|
player.setQueue(tracks, initialIndex: startIndex, playWhenReady: true, sourcePath: sourcePath)
|
|
467
|
-
case .loadTrack(
|
|
807
|
+
case let .loadTrack(track):
|
|
468
808
|
player.load(track, playWhenReady: true)
|
|
469
809
|
}
|
|
470
810
|
}
|
|
471
811
|
|
|
472
|
-
///
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
+
}
|
|
477
835
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
guard !pendingNavigationUrls.contains(url) else {
|
|
481
|
-
completion()
|
|
836
|
+
// Avoid pushing a duplicate if the top template already shows this path.
|
|
837
|
+
if let top = interfaceController.topTemplate, getPath(from: top) == url {
|
|
482
838
|
return
|
|
483
839
|
}
|
|
484
|
-
|
|
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
|
+
}
|
|
485
858
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
}
|
|
498
880
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
completion()
|
|
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
|
|
508
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)
|
|
509
925
|
}
|
|
510
926
|
}
|
|
511
927
|
|
|
@@ -514,12 +930,49 @@ public final class RNABCarPlayController: NSObject {
|
|
|
514
930
|
@MainActor
|
|
515
931
|
private func handleTabsChanged(_ tabs: [Track]) {
|
|
516
932
|
logger.debug("Tabs changed: \(tabs.count) tabs")
|
|
517
|
-
//
|
|
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
|
|
518
956
|
Task {
|
|
519
957
|
await showTabBar(tabs: tabs)
|
|
520
958
|
}
|
|
521
959
|
}
|
|
522
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
|
+
|
|
523
976
|
@MainActor
|
|
524
977
|
private func handleContentChanged(_ content: ResolvedTrack?) {
|
|
525
978
|
// This callback fires when the main browser's content changes.
|
|
@@ -532,8 +985,9 @@ public final class RNABCarPlayController: NSObject {
|
|
|
532
985
|
private func handleActiveTrackChanged(_ event: PlaybackActiveTrackChangedEvent) {
|
|
533
986
|
logger.debug("handleActiveTrackChanged: \(event.lastTrack?.src ?? "nil") → \(event.track?.src ?? "nil")")
|
|
534
987
|
updatePlayingIndicators()
|
|
535
|
-
//
|
|
536
|
-
|
|
988
|
+
// Refresh per-track button state: favorite heart, Up Next availability,
|
|
989
|
+
// and the album line's pre-resolved destination.
|
|
990
|
+
nowPlayingManager.handleActiveTrackChanged()
|
|
537
991
|
}
|
|
538
992
|
|
|
539
993
|
/// Updates the isPlaying state on all list items based on the current active track.
|
|
@@ -556,19 +1010,31 @@ public final class RNABCarPlayController: NSObject {
|
|
|
556
1010
|
}
|
|
557
1011
|
|
|
558
1012
|
for template in templates {
|
|
1013
|
+
var templateChanged = false
|
|
559
1014
|
for section in template.sections {
|
|
560
1015
|
for item in section.items {
|
|
561
1016
|
guard let listItem = item as? CPListItem,
|
|
562
|
-
let
|
|
1017
|
+
let info = listItem.carPlayItemInfo,
|
|
1018
|
+
info.src != nil
|
|
563
1019
|
else { continue }
|
|
564
1020
|
|
|
565
|
-
let isPlaying = isActiveTrack(src:
|
|
1021
|
+
let isPlaying = isActiveTrack(id: info.id, src: info.src)
|
|
566
1022
|
if listItem.isPlaying != isPlaying {
|
|
567
|
-
logger.debug(
|
|
1023
|
+
logger.debug(
|
|
1024
|
+
"Updating isPlaying for \(info.id ?? info.src ?? ""): \(listItem.isPlaying) → \(isPlaying)")
|
|
568
1025
|
listItem.isPlaying = isPlaying
|
|
1026
|
+
templateChanged = true
|
|
569
1027
|
}
|
|
570
1028
|
}
|
|
571
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
|
+
}
|
|
572
1038
|
}
|
|
573
1039
|
}
|
|
574
1040
|
|
|
@@ -632,6 +1098,108 @@ public final class RNABCarPlayController: NSObject {
|
|
|
632
1098
|
}
|
|
633
1099
|
}
|
|
634
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
|
+
|
|
635
1203
|
// MARK: - Error Handling
|
|
636
1204
|
|
|
637
1205
|
/// Shows a navigation error using CPActionSheetTemplate.
|
|
@@ -667,7 +1235,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
667
1235
|
private func presentErrorActionSheet(customDisplay: FormattedNavigationError) {
|
|
668
1236
|
// If another template is already presented, dismiss it first
|
|
669
1237
|
if interfaceController.presentedTemplate != nil {
|
|
670
|
-
interfaceController.
|
|
1238
|
+
interfaceController.safeDismiss(animated: false) { [weak self] _, _ in
|
|
671
1239
|
self?.showErrorActionSheet(customDisplay: customDisplay)
|
|
672
1240
|
}
|
|
673
1241
|
} else {
|
|
@@ -680,7 +1248,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
680
1248
|
// OK action - dismiss the action sheet (use system-localized "OK")
|
|
681
1249
|
let okTitle = Bundle(for: UIAlertController.self).localizedString(forKey: "OK", value: "OK", table: nil)
|
|
682
1250
|
let ok = CPAlertAction(title: okTitle, style: .cancel) { [weak self] _ in
|
|
683
|
-
self?.interfaceController.
|
|
1251
|
+
self?.interfaceController.safeDismiss(animated: true)
|
|
684
1252
|
}
|
|
685
1253
|
|
|
686
1254
|
let actionSheet = CPActionSheetTemplate(
|
|
@@ -689,19 +1257,20 @@ public final class RNABCarPlayController: NSObject {
|
|
|
689
1257
|
actions: [ok],
|
|
690
1258
|
)
|
|
691
1259
|
|
|
692
|
-
interfaceController.
|
|
1260
|
+
interfaceController.safePresent(actionSheet, animated: true)
|
|
693
1261
|
}
|
|
694
1262
|
|
|
695
|
-
/// Shows
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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)
|
|
705
1274
|
}
|
|
706
1275
|
}
|
|
707
1276
|
|
|
@@ -717,13 +1286,30 @@ private final class InterfaceControllerDelegate: NSObject, CPInterfaceController
|
|
|
717
1286
|
}
|
|
718
1287
|
|
|
719
1288
|
func templateDidAppear(_ aTemplate: CPTemplate, animated _: Bool) {
|
|
720
|
-
|
|
1289
|
+
// Popping back from Now Playing surfaces the tab bar (not a list), so the
|
|
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 }
|
|
721
1297
|
|
|
722
1298
|
// Update playing indicators when navigating back to a list template
|
|
723
1299
|
controller?.updatePlayingIndicators()
|
|
724
1300
|
|
|
725
1301
|
// Lazy load content for tabs that haven't been loaded yet
|
|
726
|
-
|
|
1302
|
+
if let listTemplate = aTemplate as? CPListTemplate {
|
|
1303
|
+
controller?.loadContentIfNeeded(for: listTemplate)
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
func templateDidDisappear(_ aTemplate: CPTemplate, animated _: Bool) {
|
|
1308
|
+
// The complement of the appear hook: whether popping Now Playing reports
|
|
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()
|
|
727
1313
|
}
|
|
728
1314
|
}
|
|
729
1315
|
|
|
@@ -732,12 +1318,16 @@ private final class InterfaceControllerDelegate: NSObject, CPInterfaceController
|
|
|
732
1318
|
private extension RNABCarPlayController {
|
|
733
1319
|
/// Loads content for a template if it hasn't been loaded yet (lazy loading for tabs)
|
|
734
1320
|
func loadContentIfNeeded(for template: CPListTemplate) {
|
|
735
|
-
// Skip if already has content
|
|
736
|
-
guard template.sections.isEmpty else { return }
|
|
737
|
-
|
|
738
1321
|
// Get path from userInfo
|
|
739
1322
|
guard let path = getPath(from: template) else { return }
|
|
740
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
|
+
|
|
741
1331
|
logger.debug("Lazy loading content for tab: \(path)")
|
|
742
1332
|
|
|
743
1333
|
Task {
|