react-native-audio-browser 0.2.0-next.2 → 0.2.0-next.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -21
- package/android/build.gradle +35 -12
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +389 -164
- package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
- package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
- package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +24 -5
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
- package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
- package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
- package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
- package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
- package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
- package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
- package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
- package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
- package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +56 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
- package/android/src/main/res/values/strings.xml +25 -5
- package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
- package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
- package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
- package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
- package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
- package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
- package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
- package/ios/Browser/BrowserConfig.swift +102 -5
- package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
- package/ios/Browser/BrowserManager.swift +230 -205
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +264 -106
- package/ios/Browser/SearchDrillIn.swift +7 -7
- package/ios/Browser/SectionPresentation.swift +64 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +58 -0
- package/ios/Browser/TabBarEntries.swift +9 -3
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +227 -85
- package/ios/CarPlay/CarPlayImageLoader.swift +42 -20
- package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
- package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
- package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
- package/ios/Extension/Section+Copying.swift +29 -0
- package/ios/Extension/String+IcyCharset.swift +21 -0
- package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
- package/ios/Extension/Track+Copying.swift +8 -12
- package/ios/Extension/Track+Identity.swift +18 -0
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +253 -100
- package/ios/Model/NitroTypeStubs.swift +125 -23
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Model/RemoteCommand.swift +18 -0
- package/ios/Model/TrackPlayerError.swift +2 -2
- package/ios/Option/SessionCategories.swift +3 -2
- package/ios/Player/MediaIntentCriteria.swift +17 -9
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +54 -6
- package/ios/Player/PlaybackCoordinator.swift +13 -6
- package/ios/Player/PlaybackErrorHandler.swift +4 -4
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +46 -13
- package/ios/Player/RetryManager.swift +80 -18
- package/ios/Player/SeekCompletionHandler.swift +3 -1
- package/ios/Player/ShuffleOrder.swift +33 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +62 -24
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
- package/ios/Tests/JsonModelsTests.swift +150 -1
- package/ios/Tests/LRUCacheTests.swift +111 -0
- package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
- package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
- package/ios/Tests/PlaybackCoordinatorTests.swift +1 -1
- package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
- package/ios/Tests/QueueManagerTests.swift +122 -10
- package/ios/Tests/RemoteCommandControllerTests.swift +172 -0
- package/ios/Tests/RemoteCommandTests.swift +67 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
- package/ios/Tests/RetryManagerTests.swift +89 -0
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +90 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +127 -0
- package/ios/Tests/Support/TestTypes.swift +8 -1
- package/ios/Tests/TabBarEntriesTests.swift +19 -19
- package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/TrackPlayer.swift +63 -35
- package/ios/TrackPlayerCallbacks.swift +5 -38
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- package/ios/Util/LRUCache.swift +4 -1
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/SFSymbolRenderer.swift +24 -2
- package/ios/Util/SVGProcessor.swift +9 -2
- package/lib/commonjs/AudioBrowser.js +8 -8
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/CLAUDE.md +52 -0
- package/lib/commonjs/features/browser-config.js +12 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +11 -3
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/carConnection.js +1 -1
- package/lib/commonjs/features/equalizer.js +1 -1
- package/lib/commonjs/features/errors.js +3 -3
- package/lib/commonjs/features/favorites.js +14 -11
- package/lib/commonjs/features/favorites.js.map +1 -1
- package/lib/commonjs/features/gate.js +16 -1
- package/lib/commonjs/features/gate.js.map +1 -1
- package/lib/commonjs/features/metadata.js +20 -3
- package/lib/commonjs/features/metadata.js.map +1 -1
- package/lib/commonjs/features/network.js +1 -1
- package/lib/commonjs/features/nowPlaying.js +1 -1
- package/lib/commonjs/features/output.js +1 -1
- package/lib/commonjs/features/playback/controls.js +4 -2
- package/lib/commonjs/features/playback/controls.js.map +1 -1
- package/lib/commonjs/features/playback/playWhenReady.js +1 -1
- package/lib/commonjs/features/playback/playing.js +1 -1
- package/lib/commonjs/features/playback/progress.js +3 -2
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/rate.js +8 -1
- package/lib/commonjs/features/playback/rate.js.map +1 -1
- package/lib/commonjs/features/playback/state.js +3 -4
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/commonjs/features/playback/volume.js +1 -1
- package/lib/commonjs/features/player/options.js +48 -34
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +18 -9
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +23 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -0
- package/lib/commonjs/features/queue/activeTrack.js +15 -4
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +13 -6
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/features/queue/repeatMode.js +1 -1
- package/lib/commonjs/features/queue/shuffle.js +1 -1
- package/lib/commonjs/features/remoteControls.js +13 -92
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +13 -1
- package/lib/commonjs/features/sleepTimer.js.map +1 -1
- package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
- package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
- package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
- package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/commonjs/utils/getTrackIdentity.js +24 -0
- package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +200 -75
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
- package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
- package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +175 -100
- package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
- package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
- package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
- package/lib/commonjs/web/browser/SearchManager.js +18 -23
- package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
- package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/commonjs/web/browser/sections.js +89 -0
- package/lib/commonjs/web/browser/sections.js.map +1 -0
- package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
- package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
- package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
- package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/AudioBrowser.js +2 -1
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/CLAUDE.md +52 -0
- package/lib/module/features/browser-config.js +12 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +11 -3
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/carConnection.js +1 -1
- package/lib/module/features/equalizer.js +1 -1
- package/lib/module/features/errors.js +3 -3
- package/lib/module/features/favorites.js +14 -11
- package/lib/module/features/favorites.js.map +1 -1
- package/lib/module/features/gate.js +16 -1
- package/lib/module/features/gate.js.map +1 -1
- package/lib/module/features/metadata.js +20 -3
- package/lib/module/features/metadata.js.map +1 -1
- package/lib/module/features/network.js +1 -1
- package/lib/module/features/nowPlaying.js +1 -1
- package/lib/module/features/output.js +1 -1
- package/lib/module/features/playback/controls.js +4 -2
- package/lib/module/features/playback/controls.js.map +1 -1
- package/lib/module/features/playback/playWhenReady.js +1 -1
- package/lib/module/features/playback/playing.js +1 -1
- package/lib/module/features/playback/progress.js +3 -2
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/rate.js +8 -1
- package/lib/module/features/playback/rate.js.map +1 -1
- package/lib/module/features/playback/state.js +3 -4
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +0 -5
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/module/features/playback/volume.js +1 -1
- package/lib/module/features/player/options.js +47 -32
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +17 -8
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +19 -0
- package/lib/module/features/player/validateOptions.js.map +1 -0
- package/lib/module/features/queue/activeTrack.js +15 -4
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +13 -6
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/features/queue/repeatMode.js +1 -1
- package/lib/module/features/queue/shuffle.js +1 -1
- package/lib/module/features/remoteControls.js +13 -92
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +13 -1
- package/lib/module/features/sleepTimer.js.map +1 -1
- package/lib/module/test-utils/reactNativeStub.js +58 -0
- package/lib/module/test-utils/reactNativeStub.js.map +1 -0
- package/lib/module/utils/LazyNativeEmitter.js +28 -14
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/utils/NativeUpdatedValue.js +31 -13
- package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/module/utils/getTrackIdentity.js +20 -0
- package/lib/module/utils/getTrackIdentity.js.map +1 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +200 -75
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
- package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
- package/lib/module/web/TrackPlayer/Player.js +254 -39
- package/lib/module/web/TrackPlayer/Player.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
- package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +175 -100
- package/lib/module/web/browser/BrowserManager.js.map +1 -1
- package/lib/module/web/browser/FavoriteManager.js +31 -21
- package/lib/module/web/browser/FavoriteManager.js.map +1 -1
- package/lib/module/web/browser/SearchManager.js +18 -23
- package/lib/module/web/browser/SearchManager.js.map +1 -1
- package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/module/web/browser/sections.js +83 -0
- package/lib/module/web/browser/sections.js.map +1 -0
- package/lib/module/web/http/RequestConfigBuilder.js +110 -27
- package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/module/web/http/followMediaRedirect.js +215 -0
- package/lib/module/web/http/followMediaRedirect.js.map +1 -0
- package/lib/module/web/player/NowPlayingManager.js +4 -3
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js +56 -11
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AudioBrowser.d.ts +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/browser.d.ts +10 -2
- package/lib/typescript/src/features/browser.d.ts.map +1 -1
- package/lib/typescript/src/features/carConnection.d.ts +1 -1
- package/lib/typescript/src/features/equalizer.d.ts +1 -1
- package/lib/typescript/src/features/errors.d.ts +3 -3
- package/lib/typescript/src/features/favorites.d.ts +14 -11
- package/lib/typescript/src/features/favorites.d.ts.map +1 -1
- package/lib/typescript/src/features/gate.d.ts +18 -2
- package/lib/typescript/src/features/gate.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +20 -3
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/network.d.ts +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
- package/lib/typescript/src/features/output.d.ts +1 -1
- package/lib/typescript/src/features/playback/controls.d.ts +4 -2
- package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
- package/lib/typescript/src/features/playback/playing.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/rate.d.ts +8 -1
- package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +3 -4
- package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/volume.d.ts +1 -1
- package/lib/typescript/src/features/player/options.d.ts +108 -56
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +14 -4
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
- package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +7 -4
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
- package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts +23 -22
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
- package/lib/typescript/src/types/browser-native.d.ts +3 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +380 -126
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +82 -57
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
- package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +16 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +7 -10
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
- package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +45 -25
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +36 -11
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +15 -12
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +7 -10
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +7 -10
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
- package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +57 -49
- package/nitrogen/generated/android/c++/JSection.hpp +116 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +25 -46
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +8 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +15 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -12
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +244 -133
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +31 -16
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +33 -18
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
- package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
- package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
- package/nitrogen/generated/ios/swift/Section.swift +82 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
- package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +28 -63
- package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
- package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
- package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
- package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
- package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
- package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
- package/nitrogen/generated/shared/c++/Section.hpp +102 -0
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
- package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +18 -30
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
- package/package.json +24 -11
- package/src/AudioBrowser.ts +2 -1
- package/src/CLAUDE.md +52 -0
- package/src/features/battery.test.ts +79 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +12 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +11 -3
- package/src/features/carConnection.ts +1 -1
- package/src/features/equalizer.ts +1 -1
- package/src/features/errors.ts +3 -3
- package/src/features/favorites.ts +14 -11
- package/src/features/gate.ts +16 -1
- package/src/features/hook-wiring.test.ts +302 -0
- package/src/features/metadata.ts +20 -3
- package/src/features/network.ts +1 -1
- package/src/features/nowPlaying.ts +1 -1
- package/src/features/output.ts +1 -1
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/playWhenReady.ts +1 -1
- package/src/features/playback/playing.ts +1 -1
- package/src/features/playback/progress.test.ts +169 -0
- package/src/features/playback/progress.ts +3 -2
- package/src/features/playback/rate.ts +8 -1
- package/src/features/playback/state.ts +3 -4
- package/src/features/playback/trackPlaybackTime.test.ts +16 -6
- package/src/features/playback/trackPlaybackTime.ts +1 -6
- package/src/features/playback/volume.ts +1 -1
- package/src/features/player/options.ts +126 -76
- package/src/features/player/setup.test.ts +24 -7
- package/src/features/player/setup.ts +22 -10
- package/src/features/player/validateOptions.ts +21 -0
- package/src/features/queue/activeTrack.ts +15 -4
- package/src/features/queue/queue.ts +13 -6
- package/src/features/queue/repeatMode.ts +1 -1
- package/src/features/queue/shuffle.ts +1 -1
- package/src/features/remoteControls.ts +13 -92
- package/src/features/sleepTimer.test.ts +172 -0
- package/src/features/sleepTimer.ts +9 -1
- package/src/nitro-stub-drift.test.ts +277 -0
- package/src/public-surface.test.ts +147 -0
- package/src/specs/audio-browser.nitro.ts +5 -4
- package/src/style-resolution.test.ts +210 -0
- package/src/test-utils/reactNativeStub.ts +60 -0
- package/src/types/browser-native.ts +7 -3
- package/src/types/browser-nodes.ts +396 -134
- package/src/types/browser.ts +85 -59
- package/src/utils/LazyNativeEmitter.ts +28 -15
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useDebug.test.ts +182 -0
- package/src/utils/useNativeUpdatedValue.test.ts +103 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
- package/src/web/NativeAudioBrowser.ts +239 -82
- package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
- package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
- package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
- package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
- package/src/web/TrackPlayer/Player.load.test.ts +137 -1
- package/src/web/TrackPlayer/Player.ts +284 -37
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
- package/src/web/__tests__/lifecycle.test.ts +345 -0
- package/src/web/__tests__/lifecycleHarness.ts +351 -0
- package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
- package/src/web/__tests__/loadSupersession.test.ts +175 -0
- package/src/web/__tests__/retry.test.ts +169 -0
- package/src/web/browser/BrowserManager.ts +209 -111
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- package/src/web/browser/SearchManager.test.ts +75 -0
- package/src/web/browser/SearchManager.ts +25 -28
- package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
- package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
- package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
- package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
- package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
- package/src/web/browser/assertBrowsePageShape.ts +5 -3
- package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
- package/src/web/browser/classifyTrackNavigation.ts +9 -9
- package/src/web/browser/parseSearchResponse.test.ts +8 -2
- package/src/web/browser/sections.test.ts +91 -0
- package/src/web/browser/sections.ts +72 -0
- package/src/web/http/RequestConfigBuilder.test.ts +241 -1
- package/src/web/http/RequestConfigBuilder.ts +140 -33
- package/src/web/http/followMediaRedirect.test.ts +289 -0
- package/src/web/http/followMediaRedirect.ts +253 -0
- package/src/web/player/NowPlayingManager.ts +4 -3
- package/src/web/util/BrowserPathHelper.test.ts +52 -0
- package/src/web/util/BrowserPathHelper.ts +61 -13
- package/website/guide/android-auto.md +114 -0
- package/website/guide/android-certificates.md +409 -0
- package/website/guide/artwork.md +255 -0
- package/website/guide/audio-output.md +135 -0
- package/website/guide/automotive.md +86 -0
- package/website/guide/basic-usage.md +151 -0
- package/website/guide/battery.md +115 -0
- package/website/guide/browser.md +581 -0
- package/website/guide/carplay.md +620 -0
- package/website/guide/configuration.md +173 -0
- package/website/guide/equalizer.md +231 -0
- package/website/guide/errors.md +310 -0
- package/website/guide/favorites.md +154 -0
- package/website/guide/gate.md +143 -0
- package/website/guide/getting-started.md +46 -0
- package/website/guide/hooks.md +238 -0
- package/website/guide/live-streams.md +125 -0
- package/website/guide/metadata.md +239 -0
- package/website/guide/migrating-from-track-player.md +244 -0
- package/website/guide/native-callback-fetch.md +127 -0
- package/website/guide/network.md +86 -0
- package/website/guide/now-playing.md +175 -0
- package/website/guide/playback.md +328 -0
- package/website/guide/queue.md +300 -0
- package/website/guide/remote-controls.md +256 -0
- package/website/guide/search.md +255 -0
- package/website/guide/sleep-timer.md +268 -0
- package/website/guide/track.md +247 -0
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/ios/Model/MediaURL.swift +0 -67
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
- package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
- package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
- package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
- package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
- package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
- package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
|
@@ -36,35 +36,46 @@ final class CarPlayImageLoader {
|
|
|
36
36
|
return sfSymbolImageForSize(symbolName, canvasSize: canvasSize, backgroundColor: bg, symbolColor: fg)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/// Cached empty
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
/// Cached empty placeholders, keyed by point size — list items and image-row
|
|
40
|
+
/// cells reserve different footprints.
|
|
41
|
+
private var placeholders: [String: UIImage] = [:]
|
|
42
|
+
|
|
43
|
+
/// Empty placeholder image reserving a `size`-point slot while artwork loads.
|
|
44
|
+
func placeholderImage(size: CGSize) -> UIImage? {
|
|
45
|
+
let key = "\(size.width)×\(size.height)"
|
|
46
|
+
if let cached = placeholders[key] { return cached }
|
|
47
|
+
UIGraphicsBeginImageContextWithOptions(size, false, carTraitCollection.displayScale)
|
|
44
48
|
defer { UIGraphicsEndImageContext() }
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
|
|
50
|
+
placeholders[key] = image
|
|
51
|
+
return image
|
|
52
|
+
}
|
|
47
53
|
|
|
48
54
|
// MARK: - Artwork Loading
|
|
49
55
|
|
|
50
56
|
/// Loads artwork for a track with size context, using the artwork transform if configured.
|
|
51
57
|
/// - Parameters:
|
|
52
58
|
/// - track: The track to load artwork for
|
|
59
|
+
/// - style: The track's resolved style block (inherited values folded in).
|
|
60
|
+
/// nil = the track's own declaration — callers without a section context
|
|
61
|
+
/// (tabs, now-playing) have no inheritance to resolve.
|
|
53
62
|
/// - size: The target size in points (will be multiplied by CarPlay display scale)
|
|
54
63
|
/// - completion: Called with the loaded image, or nil on failure
|
|
55
|
-
func loadArtwork(for track: Track, size: CGSize, completion: @escaping @Sendable (UIImage?) -> Void) {
|
|
64
|
+
func loadArtwork(for track: Track, style: TrackStyle? = nil, size: CGSize, completion: @escaping @Sendable (UIImage?) -> Void) {
|
|
56
65
|
// A track that ships one image per appearance is loaded as two ordinary
|
|
57
66
|
// single-URL loads and combined below. Recursing rather than branching
|
|
58
67
|
// inside the loader means each variant still goes through the whole
|
|
59
68
|
// pipeline — route config, headers, SVG rasterising, the resolve transform
|
|
60
69
|
// — instead of a second, thinner fetch path that would drift from it.
|
|
61
70
|
if let variants = track.artwork?.variants {
|
|
62
|
-
loadVariants(for: track, variants: variants, size: size, completion: completion)
|
|
71
|
+
loadVariants(for: track, style: style, variants: variants, size: size, completion: completion)
|
|
63
72
|
return
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
//
|
|
67
|
-
|
|
75
|
+
// The annotation goes on the closure's return type, not the binding:
|
|
76
|
+
// @Sendable isn't inferred through `map`, and omitting it fails with
|
|
77
|
+
// "type of expression is ambiguous" — which doesn't mention @Sendable.
|
|
78
|
+
let urlResolver = browserManager.map { bm -> @Sendable (Double, Double) async -> ArtworkResolvedImage? in
|
|
68
79
|
{ [weak bm] pixelWidth, pixelHeight in
|
|
69
80
|
guard let bm else { return nil }
|
|
70
81
|
let imageContext = ImageContext(width: pixelWidth, height: pixelHeight)
|
|
@@ -77,7 +88,7 @@ final class CarPlayImageLoader {
|
|
|
77
88
|
let action = await CarPlayArtworkResolver.resolve(
|
|
78
89
|
artwork: track.artwork?.url,
|
|
79
90
|
artworkSourceUri: track.artworkSource?.uri,
|
|
80
|
-
|
|
91
|
+
stencil: (style ?? track.style)?.artworkRendering == .stencil,
|
|
81
92
|
targetWidth: Double(size.width),
|
|
82
93
|
targetHeight: Double(size.height),
|
|
83
94
|
displayScale: Double(self.carTraitCollection.displayScale),
|
|
@@ -88,7 +99,7 @@ final class CarPlayImageLoader {
|
|
|
88
99
|
case let .sfSymbol(artwork, width, height):
|
|
89
100
|
completion(self.sfSymbolImage(forArtwork: artwork, canvasSize: CGSize(width: width, height: height)))
|
|
90
101
|
case let .fetch(uri, headers, shouldTint, isSvg):
|
|
91
|
-
let image = await self.fetchImage(uri: uri, headers: headers, isSvg: isSvg)
|
|
102
|
+
let image = await self.fetchImage(uri: uri, headers: headers, isSvg: isSvg, size: size)
|
|
92
103
|
if let image, shouldTint {
|
|
93
104
|
completion(self.createAdaptiveImage(image, carTraitCollection: self.carTraitCollection))
|
|
94
105
|
} else {
|
|
@@ -115,13 +126,18 @@ final class CarPlayImageLoader {
|
|
|
115
126
|
/// beats an empty slot, and this is usually a transient fetch failure.
|
|
116
127
|
private func loadVariants(
|
|
117
128
|
for track: Track,
|
|
129
|
+
style: TrackStyle?,
|
|
118
130
|
variants: ArtworkVariants,
|
|
119
131
|
size: CGSize,
|
|
120
132
|
completion: @escaping @Sendable (UIImage?) -> Void,
|
|
121
133
|
) {
|
|
122
134
|
Task {
|
|
123
|
-
async let light = self.loadArtworkAsync(
|
|
124
|
-
|
|
135
|
+
async let light = self.loadArtworkAsync(
|
|
136
|
+
for: track.copying(artwork: .some(.first(variants.light))), style: style, size: size,
|
|
137
|
+
)
|
|
138
|
+
async let dark = self.loadArtworkAsync(
|
|
139
|
+
for: track.copying(artwork: .some(.first(variants.dark))), style: style, size: size,
|
|
140
|
+
)
|
|
125
141
|
let (lightImage, darkImage) = await (light, dark)
|
|
126
142
|
|
|
127
143
|
guard let lightImage, let darkImage else {
|
|
@@ -137,19 +153,25 @@ final class CarPlayImageLoader {
|
|
|
137
153
|
}
|
|
138
154
|
|
|
139
155
|
/// `loadArtwork` as an awaitable, so the two variants can load concurrently.
|
|
140
|
-
private func loadArtworkAsync(for track: Track, size: CGSize) async -> UIImage? {
|
|
156
|
+
private func loadArtworkAsync(for track: Track, style: TrackStyle?, size: CGSize) async -> UIImage? {
|
|
141
157
|
await withCheckedContinuation { continuation in
|
|
142
|
-
self.loadArtwork(for: track, size: size) { image in
|
|
158
|
+
self.loadArtwork(for: track, style: style, size: size) { image in
|
|
143
159
|
continuation.resume(returning: image)
|
|
144
160
|
}
|
|
145
161
|
}
|
|
146
162
|
}
|
|
147
163
|
|
|
148
164
|
/// Fetches an image from a URL with optional headers and SVG processing.
|
|
149
|
-
|
|
165
|
+
/// Rasters are downsampled to `size` points at the car's display scale —
|
|
166
|
+
/// CPListItem.h: "size them to the display scale of the car screen". SVGs
|
|
167
|
+
/// already rasterize at car scale via SVGProcessor.
|
|
168
|
+
private func fetchImage(uri: String, headers: [String: String]?, isSvg: Bool, size: CGSize) async -> UIImage? {
|
|
150
169
|
let source = ImageSource(uri: uri, method: nil, headers: headers, body: nil)
|
|
151
|
-
let
|
|
152
|
-
|
|
170
|
+
let scale = carTraitCollection.displayScale
|
|
171
|
+
if isSvg {
|
|
172
|
+
return await ArtworkImageFetcher.fetchImage(from: source, svgScale: scale)
|
|
173
|
+
}
|
|
174
|
+
return await ArtworkImageFetcher.fetchImage(from: source, downsampleTo: (size, scale))
|
|
153
175
|
}
|
|
154
176
|
|
|
155
177
|
/// Creates an SF Symbol image rendered at the given canvas size.
|
|
@@ -7,8 +7,9 @@ import os.log
|
|
|
7
7
|
|
|
8
8
|
/// Typed metadata stored on CPListItems, replacing stringly-typed userInfo dictionaries.
|
|
9
9
|
struct CarPlayItemInfo {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/// The track's identity (`id` when non-blank, else `src`) — the comparison
|
|
11
|
+
/// key for the now-playing indicator repaint sweep.
|
|
12
|
+
let identity: String?
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
extension CPListItem {
|
|
@@ -29,11 +30,14 @@ final class CarPlayListItemFactory {
|
|
|
29
30
|
private let logger = Logger(subsystem: "com.audiobrowser", category: "CarPlayListItemFactory")
|
|
30
31
|
|
|
31
32
|
var imageLoader: CarPlayImageLoader?
|
|
32
|
-
|
|
33
|
+
/// Pushed by the controller (at creation and when options change), like
|
|
34
|
+
/// `imageLoader` — the factory never reads options itself.
|
|
35
|
+
var playingIndicatorLocation: CPListItemPlayingIndicatorLocation = .leading
|
|
36
|
+
private let isActiveTrack: (_ identity: String?) -> Bool
|
|
33
37
|
private let onItemSelected: (Track, @escaping () -> Void) -> Void
|
|
34
38
|
|
|
35
39
|
init(
|
|
36
|
-
isActiveTrack: @escaping (_
|
|
40
|
+
isActiveTrack: @escaping (_ identity: String?) -> Bool,
|
|
37
41
|
onItemSelected: @escaping (Track, @escaping () -> Void) -> Void,
|
|
38
42
|
) {
|
|
39
43
|
self.isActiveTrack = isActiveTrack
|
|
@@ -42,55 +46,69 @@ final class CarPlayListItemFactory {
|
|
|
42
46
|
|
|
43
47
|
// MARK: - Sections
|
|
44
48
|
|
|
49
|
+
/// Whether this OS has CarPlay's wrapping tile container (the iOS 26
|
|
50
|
+
/// element API) — the capability `SectionPresentation` degrades against.
|
|
51
|
+
private static var supportsWrappingGrid: Bool {
|
|
52
|
+
if #available(iOS 26.0, *) { return true }
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Maps the page's sections 1:1 to CPListSections (ADR 0010), respecting
|
|
57
|
+
/// CarPlay's section and total-item budgets. Style blocks declare the
|
|
58
|
+
/// requested presentation (resolved `section ?? page` here — ADR 0011);
|
|
59
|
+
/// this renders CarPlay's nearest supported form (`SectionPresentation`):
|
|
60
|
+
/// - list (default): a titled/headerless section of list rows.
|
|
61
|
+
/// - single-line grid (`gridWrap: false`): a headerless section holding
|
|
62
|
+
/// one single-line image-row item whose text is the section title — the
|
|
63
|
+
/// tiles that fit render, the rest truncate (the platform doesn't
|
|
64
|
+
/// report the fit).
|
|
65
|
+
/// - wrapping grid: a wrapping, titled tile grid on iOS 26+; a plain list
|
|
66
|
+
/// before that.
|
|
45
67
|
func createSections(from resolvedTrack: ResolvedTrack) -> [CPListSection] {
|
|
46
|
-
guard let
|
|
68
|
+
guard let sections = resolvedTrack.normalizedSections else {
|
|
47
69
|
return []
|
|
48
70
|
}
|
|
49
71
|
|
|
50
72
|
let maxSections = CPListTemplate.maximumSectionCount
|
|
51
73
|
let maxTotalItems = CPListTemplate.maximumItemCount
|
|
52
74
|
|
|
53
|
-
|
|
54
|
-
// Android Auto's GROUP_TITLE hint has), so sections — titled or not —
|
|
55
|
-
// render in exact source (server) order. Ungrouped runs become headerless
|
|
56
|
-
// sections in place rather than being hoisted to the top.
|
|
57
|
-
var runs: [(title: String?, tracks: [Track])] = []
|
|
58
|
-
for track in children {
|
|
59
|
-
if let last = runs.indices.last, runs[last].title == track.groupTitle {
|
|
60
|
-
runs[last].tracks.append(track)
|
|
61
|
-
} else {
|
|
62
|
-
runs.append((track.groupTitle, [track]))
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Create sections (respecting both section and total item limits)
|
|
67
|
-
var sections: [CPListSection] = []
|
|
75
|
+
var listSections: [CPListSection] = []
|
|
68
76
|
var totalItemCount = 0
|
|
69
77
|
|
|
70
|
-
for
|
|
71
|
-
guard
|
|
78
|
+
for section in sections {
|
|
79
|
+
guard listSections.count < maxSections else { break }
|
|
72
80
|
guard totalItemCount < maxTotalItems else { break }
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
// An empty section is a dead end regardless of style (a "popular"
|
|
82
|
+
// section with no entries yet) — skip it rather than render a header
|
|
83
|
+
// with nothing under it.
|
|
84
|
+
guard !section.children.isEmpty else { continue }
|
|
85
|
+
|
|
86
|
+
let style = StyleResolver.sectionStyle(section: section.style, page: resolvedTrack.style)
|
|
87
|
+
let presentation = SectionPresentation(for: style, supportsWrappingGrid: Self.supportsWrappingGrid)
|
|
88
|
+
switch presentation {
|
|
89
|
+
case .list:
|
|
90
|
+
let availableSlots = maxTotalItems - totalItemCount
|
|
91
|
+
let items: [CPListTemplateItem] = section.children.prefix(availableSlots).map {
|
|
92
|
+
createListItem(for: $0, style: StyleResolver.trackStyle(track: $0.style, section: style))
|
|
81
93
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
if let title = section.title {
|
|
95
|
+
listSections.append(CPListSection(items: items, header: title, sectionIndexTitle: nil))
|
|
96
|
+
} else {
|
|
97
|
+
listSections.append(CPListSection(items: items))
|
|
98
|
+
}
|
|
99
|
+
totalItemCount += items.count
|
|
100
|
+
case .singleLineRow, .wrappingGrid:
|
|
101
|
+
// Tile presentations render as one image-row item inside a
|
|
102
|
+
// headerless CPListSection, with the section title as the item's
|
|
103
|
+
// text — a headed section would render the title twice.
|
|
104
|
+
guard let item = createImageRowItem(for: section, style: style, presentation: presentation)
|
|
105
|
+
else { continue }
|
|
106
|
+
listSections.append(CPListSection(items: [item]))
|
|
107
|
+
totalItemCount += 1
|
|
89
108
|
}
|
|
90
|
-
totalItemCount += items.count
|
|
91
109
|
}
|
|
92
110
|
|
|
93
|
-
return
|
|
111
|
+
return listSections
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
// MARK: - List Item
|
|
@@ -98,9 +116,14 @@ final class CarPlayListItemFactory {
|
|
|
98
116
|
/// Creates a CPListItem for a track with common setup (typed userInfo, artwork, isPlaying).
|
|
99
117
|
/// - Parameters:
|
|
100
118
|
/// - track: The track to create the item for
|
|
119
|
+
/// - style: The track's resolved style block (`track ?? section ?? page`,
|
|
120
|
+
/// via `StyleResolver`). nil = the track's own declaration — the one
|
|
121
|
+
/// genuinely section-less caller (the Up Next queue list) has no
|
|
122
|
+
/// inheritance to resolve.
|
|
101
123
|
/// - handler: Optional custom handler. If nil, uses default browse/play handling.
|
|
102
124
|
func createListItem(
|
|
103
125
|
for track: Track,
|
|
126
|
+
style: TrackStyle? = nil,
|
|
104
127
|
handler: ((CPSelectableListItem, @escaping () -> Void) -> Void)? = nil,
|
|
105
128
|
) -> CPListItem {
|
|
106
129
|
let item = CPListItem(
|
|
@@ -109,46 +132,70 @@ final class CarPlayListItemFactory {
|
|
|
109
132
|
)
|
|
110
133
|
|
|
111
134
|
// Store typed info for updatePlayingIndicators()
|
|
112
|
-
item.setCarPlayItemInfo(CarPlayItemInfo(
|
|
135
|
+
item.setCarPlayItemInfo(CarPlayItemInfo(identity: track.identity))
|
|
113
136
|
|
|
114
137
|
// Set accessory type based on whether track is browsable or playable
|
|
115
138
|
if let src = track.src {
|
|
116
139
|
// Playable track - check if it's currently playing
|
|
117
140
|
item.accessoryType = .none
|
|
118
|
-
//
|
|
119
|
-
//
|
|
141
|
+
// Consumer-configurable via `ios.carPlayPlayingIndicatorLocation`
|
|
142
|
+
// (default leading: the indicator draws in the artwork slot). Note the
|
|
120
143
|
// indicator's rendering is owned by the phone's CarPlay service, and on
|
|
121
144
|
// some iOS/CarPlay-Simulator combinations it isn't drawn at all — for
|
|
122
145
|
// ANY third-party app. Verify on a real head unit before assuming a
|
|
123
146
|
// logic bug here.
|
|
124
|
-
item.playingIndicatorLocation =
|
|
125
|
-
item.isPlaying = isActiveTrack(track.
|
|
147
|
+
item.playingIndicatorLocation = playingIndicatorLocation
|
|
148
|
+
item.isPlaying = isActiveTrack(track.identity)
|
|
126
149
|
if item.isPlaying {
|
|
127
150
|
logger.debug("Setting isPlaying=true for: \(track.title) (src: \(src))")
|
|
128
151
|
}
|
|
129
|
-
} else if track.
|
|
152
|
+
} else if track.path != nil {
|
|
130
153
|
// Browsable only - show disclosure indicator
|
|
131
154
|
item.accessoryType = .disclosureIndicator
|
|
132
155
|
}
|
|
133
156
|
|
|
157
|
+
// A resolved accessorySymbol replaces the derived accessory above
|
|
158
|
+
// (accessoryImage outranks accessoryType in the SDK). 'none' — the
|
|
159
|
+
// inheritance escape — keeps the derived behavior instead.
|
|
160
|
+
if let symbol = SectionPresentation.effectiveAccessorySymbol(style ?? track.style),
|
|
161
|
+
let accessory = UIImage(systemName: symbol)
|
|
162
|
+
{
|
|
163
|
+
// `accessoryImage` is readonly on CPListItem; post-init updates go
|
|
164
|
+
// through the setter (which also clears `accessoryType`, per the SDK).
|
|
165
|
+
item.setAccessoryImage(accessory)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// An unavailable track grays out and goes inert — list rows are the
|
|
169
|
+
// surface that CAN draw the fact, so it shows rather than hides
|
|
170
|
+
// (Track.disabled's rendering ladder).
|
|
171
|
+
if track.disabled == true {
|
|
172
|
+
item.isEnabled = false
|
|
173
|
+
}
|
|
174
|
+
|
|
134
175
|
// Load artwork with size context for proper CDN optimization
|
|
135
176
|
if track.artwork != nil || track.artworkSource != nil {
|
|
136
177
|
// Set empty placeholder to reserve space while loading
|
|
137
|
-
item.setImage(imageLoader?.placeholderImage)
|
|
138
|
-
imageLoader?.loadArtwork(for: track, size: CPListItem.maximumImageSize) { [weak item] image in
|
|
178
|
+
item.setImage(imageLoader?.placeholderImage(size: CPListItem.maximumImageSize))
|
|
179
|
+
imageLoader?.loadArtwork(for: track, style: style, size: CPListItem.maximumImageSize) { [weak item] image in
|
|
139
180
|
Task { @MainActor in
|
|
140
181
|
item?.setImage(image)
|
|
141
182
|
}
|
|
142
183
|
}
|
|
143
184
|
}
|
|
144
185
|
|
|
145
|
-
// Set selection handler
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
186
|
+
// Set selection handler. The disabled guard wraps BOTH handler paths —
|
|
187
|
+
// custom handlers (Up Next rows skipping the queue, say) included: belt
|
|
188
|
+
// over the isEnabled braces, so an unavailable track stays inert even if
|
|
189
|
+
// a surface delivers the tap.
|
|
190
|
+
let selection = handler ?? { [onItemSelected] _, completion in
|
|
191
|
+
onItemSelected(track, completion)
|
|
192
|
+
}
|
|
193
|
+
item.handler = { item, completion in
|
|
194
|
+
guard track.disabled != true else {
|
|
195
|
+
completion()
|
|
196
|
+
return
|
|
151
197
|
}
|
|
198
|
+
selection(item, completion)
|
|
152
199
|
}
|
|
153
200
|
|
|
154
201
|
return item
|
|
@@ -156,58 +203,202 @@ final class CarPlayListItemFactory {
|
|
|
156
203
|
|
|
157
204
|
// MARK: - Image Row Item
|
|
158
205
|
|
|
159
|
-
///
|
|
160
|
-
///
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
206
|
+
/// Target size for images inside an image row. The iOS 26 SDK deprecates
|
|
207
|
+
/// `CPListImageRowItem.maximumImageSize` in favor of the element-based API,
|
|
208
|
+
/// so read whichever the running OS considers canonical.
|
|
209
|
+
private static var rowImageSize: CGSize {
|
|
210
|
+
if #available(iOS 26.0, *) {
|
|
211
|
+
return CPListImageRowItemElement.maximumImageSize
|
|
212
|
+
}
|
|
213
|
+
return CPListImageRowItem.maximumImageSize
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// Target artwork size for one tile. Cards read their own class maximums
|
|
217
|
+
/// ('background' selects the larger full-height target); every other
|
|
218
|
+
/// family uses the shared element size.
|
|
219
|
+
private static func tileImageSize(gridTile: GridTile?, cardImage: CardImage?) -> CGSize {
|
|
220
|
+
if #available(iOS 26.0, *), gridTile == .card {
|
|
221
|
+
return cardImage == .background
|
|
222
|
+
? CPListImageRowItemCardElement.maximumFullHeightImageSize
|
|
223
|
+
: CPListImageRowItemCardElement.maximumImageSize
|
|
164
224
|
}
|
|
225
|
+
return rowImageSize
|
|
226
|
+
}
|
|
165
227
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
228
|
+
/// Creates the image-row item rendering a tile-presented section: a single
|
|
229
|
+
/// truncating line (`.singleLineRow`) or a wrapping multi-line grid
|
|
230
|
+
/// (`.wrappingGrid` — constructed on iOS 26+ only, by `SectionPresentation`).
|
|
231
|
+
///
|
|
232
|
+
/// Returns nil when nothing is drawable — every child hidden by the
|
|
233
|
+
/// disabled ladder (pre-26 only; the element APIs gray instead).
|
|
234
|
+
private func createImageRowItem(
|
|
235
|
+
for section: Section, style: SectionStyle, presentation: SectionPresentation,
|
|
236
|
+
) -> CPListImageRowItem? {
|
|
237
|
+
let singleLine = presentation == .singleLineRow
|
|
238
|
+
// Disabled ladder: iOS 26 elements can draw unavailability (grayed +
|
|
239
|
+
// inert, below); the legacy image row can't, so there a disabled track
|
|
240
|
+
// hides — never a normal-looking dead tile.
|
|
241
|
+
let visibleChildren: [Track] = if #available(iOS 26.0, *) {
|
|
242
|
+
section.children
|
|
243
|
+
} else {
|
|
244
|
+
section.children.filter { $0.disabled != true }
|
|
245
|
+
}
|
|
246
|
+
guard !visibleChildren.isEmpty else { return nil }
|
|
247
|
+
// A single line shows at most CPMaximumNumberOfGridImages; the wrapping
|
|
248
|
+
// grid takes every child (queue scope is declared, never rendered — the
|
|
249
|
+
// cap here only limits what's drawn, and only where the platform does).
|
|
250
|
+
let tracks = singleLine
|
|
251
|
+
? Array(visibleChildren.prefix(CPMaximumNumberOfGridImages))
|
|
252
|
+
: visibleChildren
|
|
253
|
+
let placeholder = { [imageLoader] in
|
|
254
|
+
imageLoader?.placeholderImage(size: Self.rowImageSize) ?? UIImage()
|
|
255
|
+
}
|
|
171
256
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
257
|
+
let item: CPListImageRowItem
|
|
258
|
+
// Applies an async-loaded artwork image to a tile on the element-based
|
|
259
|
+
// API (the elements are captured per availability branch — their type
|
|
260
|
+
// doesn't exist before iOS 26). nil = the legacy gridImages path.
|
|
261
|
+
// @MainActor makes the closure Sendable for the artwork completion hop.
|
|
262
|
+
var applyImage: (@MainActor @Sendable (Int, UIImage) -> Void)?
|
|
263
|
+
// Resolved once per track, shared by element construction and the
|
|
264
|
+
// artwork loads below.
|
|
265
|
+
let resolvedStyles = tracks.map { StyleResolver.trackStyle(track: $0.style, section: style) }
|
|
266
|
+
if #available(iOS 26.0, *) {
|
|
267
|
+
// Builds a family's elements with the shared per-element tail: the
|
|
268
|
+
// disabled fact (isEnabled lives on the element base class) applied
|
|
269
|
+
// once, not per family.
|
|
270
|
+
func makeElements<Element: CPListImageRowItemElement>(
|
|
271
|
+
_ make: (Track, TrackStyle) -> Element,
|
|
272
|
+
) -> [Element] {
|
|
273
|
+
zip(tracks, resolvedStyles).map { track, resolved in
|
|
274
|
+
let element = make(track, resolved)
|
|
275
|
+
if track.disabled == true {
|
|
276
|
+
element.isEnabled = false
|
|
277
|
+
}
|
|
278
|
+
return element
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// The image setter every element family shares (`image` is on the base
|
|
282
|
+
// class); bounds-checked against the elements the row was built with.
|
|
283
|
+
func imageApplier(_ elements: [some CPListImageRowItemElement]) -> @MainActor @Sendable (Int, UIImage) -> Void {
|
|
284
|
+
{ index, image in
|
|
285
|
+
guard index < elements.count else { return }
|
|
286
|
+
elements[index].image = image
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// The family is style-driven (`SectionPresentation.tileFamily`); both
|
|
290
|
+
// non-plain families take either wrap mode. Pre-26 the treatment drops
|
|
291
|
+
// and the layout survives (the legacy branch below).
|
|
292
|
+
switch SectionPresentation.tileFamily(for: style, singleLine: singleLine) {
|
|
293
|
+
case .cardElements:
|
|
294
|
+
// Cards have no shape and no accessory slot; their knobs are the
|
|
295
|
+
// tint and the image mode ('background' fills the card full-height
|
|
296
|
+
// and turns the tint into the color behind the labels).
|
|
297
|
+
let elements = makeElements { track, resolved in
|
|
298
|
+
CPListImageRowItemCardElement(
|
|
299
|
+
image: placeholder(),
|
|
300
|
+
showsImageFullHeight: resolved.cardImage == .background,
|
|
301
|
+
title: track.title,
|
|
302
|
+
subtitle: track.subtitle,
|
|
303
|
+
tintColor: resolved.cardTint.flatMap { UIColor(declaredHex: $0) },
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
item = CPListImageRowItem(
|
|
307
|
+
text: section.title, cardElements: elements, allowsMultipleLines: !singleLine,
|
|
308
|
+
)
|
|
309
|
+
applyImage = imageApplier(elements)
|
|
310
|
+
case .condensedElements:
|
|
311
|
+
let elements = makeElements { track, resolved in
|
|
312
|
+
CPListImageRowItemCondensedElement(
|
|
313
|
+
image: placeholder(),
|
|
314
|
+
imageShape: resolved.imageShape == .circular ? .circular : .roundedRectangle,
|
|
315
|
+
title: track.title,
|
|
316
|
+
subtitle: track.subtitle,
|
|
317
|
+
accessorySymbolName: SectionPresentation.effectiveAccessorySymbol(resolved),
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
item = CPListImageRowItem(
|
|
321
|
+
text: section.title, condensedElements: elements, allowsMultipleLines: !singleLine,
|
|
322
|
+
)
|
|
323
|
+
applyImage = imageApplier(elements)
|
|
324
|
+
case .rowElements:
|
|
325
|
+
// The only plain tile with a subtitle slot — the imageGridElements
|
|
326
|
+
// family has no equivalent, by SDK design.
|
|
327
|
+
let elements = makeElements { track, _ in
|
|
328
|
+
CPListImageRowItemRowElement(
|
|
329
|
+
image: placeholder(), title: track.title, subtitle: track.subtitle,
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
item = CPListImageRowItem(text: section.title, elements: elements, allowsMultipleLines: false)
|
|
333
|
+
applyImage = imageApplier(elements)
|
|
334
|
+
case .imageGridElements:
|
|
335
|
+
// imageGridElements, never the title-less gridElements: an
|
|
336
|
+
// artwork-less track must keep its name on screen (ADR 0010).
|
|
337
|
+
let elements = makeElements { track, resolved in
|
|
338
|
+
CPListImageRowItemImageGridElement(
|
|
339
|
+
image: placeholder(),
|
|
340
|
+
imageShape: resolved.imageShape == .circular ? .circular : .roundedRectangle,
|
|
341
|
+
title: track.title,
|
|
342
|
+
accessorySymbolName: SectionPresentation.effectiveAccessorySymbol(resolved),
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
item = CPListImageRowItem(text: section.title, imageGridElements: elements, allowsMultipleLines: true)
|
|
346
|
+
applyImage = imageApplier(elements)
|
|
347
|
+
}
|
|
175
348
|
} else {
|
|
176
|
-
|
|
349
|
+
let placeholders = tracks.map { _ in placeholder() }
|
|
350
|
+
// Use imageTitles variant on iOS 17.4+ to show titles below each thumbnail
|
|
351
|
+
if #available(iOS 17.4, *) {
|
|
352
|
+
item = CPListImageRowItem(text: section.title ?? "", images: placeholders, imageTitles: tracks.map(\.title))
|
|
353
|
+
} else {
|
|
354
|
+
item = CPListImageRowItem(text: section.title ?? "", images: placeholders)
|
|
355
|
+
}
|
|
177
356
|
}
|
|
178
357
|
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
358
|
+
// Header tap → navigate to section.path ("view all"). A path-less
|
|
359
|
+
// section is a pure preview — its header tap is a no-op rather than a
|
|
360
|
+
// selection that can't resolve.
|
|
182
361
|
item.handler = { [onItemSelected] _, completion in
|
|
183
|
-
guard
|
|
362
|
+
guard let path = section.path else {
|
|
184
363
|
completion()
|
|
185
364
|
return
|
|
186
365
|
}
|
|
187
|
-
onItemSelected(
|
|
366
|
+
onItemSelected(Self.navigationTrack(path: path, title: section.title), completion)
|
|
188
367
|
}
|
|
189
368
|
|
|
190
|
-
//
|
|
369
|
+
// Per-tile taps select the child track directly. A disabled tile is
|
|
370
|
+
// grayed and inert (26+ elements) — the guard keeps it inert even if
|
|
371
|
+
// the surface delivers the tap anyway.
|
|
191
372
|
item.listImageRowHandler = { [onItemSelected] _, index, completion in
|
|
192
|
-
guard index <
|
|
373
|
+
guard index < tracks.count, tracks[index].disabled != true else {
|
|
193
374
|
completion()
|
|
194
375
|
return
|
|
195
376
|
}
|
|
196
|
-
|
|
197
|
-
onItemSelected(tappedItem.toTrack(), completion)
|
|
377
|
+
onItemSelected(tracks[index], completion)
|
|
198
378
|
}
|
|
199
379
|
|
|
200
|
-
// Load artwork for each
|
|
201
|
-
for (index,
|
|
202
|
-
guard
|
|
380
|
+
// Load artwork for each rendered tile asynchronously
|
|
381
|
+
for (index, track) in tracks.enumerated() {
|
|
382
|
+
guard track.artwork != nil || track.artworkSource != nil else { continue }
|
|
203
383
|
|
|
204
|
-
|
|
384
|
+
let resolved = resolvedStyles[index]
|
|
385
|
+
imageLoader?.loadArtwork(
|
|
386
|
+
for: track,
|
|
387
|
+
style: resolved,
|
|
388
|
+
size: Self.tileImageSize(gridTile: style.gridTile, cardImage: resolved.cardImage),
|
|
389
|
+
) { [weak item, applyImage] image in
|
|
205
390
|
Task { @MainActor in
|
|
206
391
|
guard let item, let image else { return }
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
392
|
+
if let applyImage {
|
|
393
|
+
// The element-based API exposes a settable `image`; assigning it
|
|
394
|
+
// updates the element the row observes.
|
|
395
|
+
applyImage(index, image)
|
|
396
|
+
} else {
|
|
397
|
+
var images = item.gridImages
|
|
398
|
+
if index < images.count {
|
|
399
|
+
images[index] = image
|
|
400
|
+
item.update(images)
|
|
401
|
+
}
|
|
211
402
|
}
|
|
212
403
|
}
|
|
213
404
|
}
|
|
@@ -215,4 +406,18 @@ final class CarPlayListItemFactory {
|
|
|
215
406
|
|
|
216
407
|
return item
|
|
217
408
|
}
|
|
409
|
+
|
|
410
|
+
/// A synthetic browsable Track for section-header navigation — the
|
|
411
|
+
/// section's "view all" surface has a path and a title, but no Track.
|
|
412
|
+
/// Style-less by design: CarPlay resolves presentation from the page it
|
|
413
|
+
/// navigates to. (Android's counterpart projects the section's block —
|
|
414
|
+
/// the promise only exists to feed Android Auto's parent-level hint.)
|
|
415
|
+
private static func navigationTrack(path: String, title: String?) -> Track {
|
|
416
|
+
Track(
|
|
417
|
+
id: nil, path: path, src: nil, artwork: nil, artworkSource: nil,
|
|
418
|
+
request: nil, title: title ?? "", subtitle: nil,
|
|
419
|
+
artist: nil, albumPath: nil, album: nil, description: nil, genre: nil,
|
|
420
|
+
duration: nil, style: nil, disabled: nil, favorited: nil, live: nil, icyCharset: nil,
|
|
421
|
+
)
|
|
422
|
+
}
|
|
218
423
|
}
|