react-native-audio-browser 0.2.0-next.10 → 0.2.0-next.12
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 +4 -4
- package/android/build.gradle +6 -5
- package/android/gradle.properties +1 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +49 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -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/player/MediaSessionCallback.kt +120 -67
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +93 -73
- package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +46 -5
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +86 -27
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +20 -6
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +249 -86
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +35 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +69 -30
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +345 -8
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -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/player/CapabilityControlsTest.kt +1 -2
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +152 -8
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +218 -10
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -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 +131 -143
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/JsonModels.swift +187 -106
- package/ios/Browser/SearchDrillIn.swift +6 -6
- package/ios/Browser/SectionPresentation.swift +34 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +47 -0
- package/ios/Browser/TabBarEntries.swift +2 -2
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +105 -67
- package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
- package/ios/CarPlay/CarPlayListItemFactory.swift +217 -84
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
- package/ios/Extension/Section+Copying.swift +31 -0
- package/ios/Extension/Track+Copying.swift +6 -12
- package/ios/Extension/Track+Identity.swift +18 -0
- package/ios/HybridAudioBrowser.swift +113 -35
- package/ios/Model/NitroTypeStubs.swift +64 -23
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Player/NowPlayingUpdater.swift +1 -1
- package/ios/Player/PlaybackCoordinator.swift +2 -2
- package/ios/Player/RetryManager.swift +4 -3
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/JsonModelsTests.swift +75 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/QueueManagerTests.swift +4 -4
- package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +61 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +67 -0
- package/ios/Tests/TabBarEntriesTests.swift +18 -18
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- package/lib/commonjs/AudioBrowser.js +11 -0
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/features/browser-config.js +11 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +2 -2
- package/lib/commonjs/features/favorites.js +13 -10
- package/lib/commonjs/features/favorites.js.map +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/state.js +2 -3
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +3 -1
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +2 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -1
- package/lib/commonjs/features/queue/activeTrack.js +14 -3
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +11 -4
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
- 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 +32 -24
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +144 -87
- 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/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/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 +1 -0
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/features/browser-config.js +11 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +2 -2
- package/lib/module/features/favorites.js +13 -10
- package/lib/module/features/favorites.js.map +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/state.js +2 -3
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +3 -1
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +2 -0
- package/lib/module/features/player/validateOptions.js.map +1 -1
- package/lib/module/features/queue/activeTrack.js +14 -3
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +11 -4
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/utils/LazyNativeEmitter.js +21 -10
- 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 +32 -24
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +144 -87
- 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/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/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/src/AudioBrowser.d.ts +1 -0
- 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 +2 -2
- package/lib/typescript/src/features/favorites.d.ts +13 -10
- package/lib/typescript/src/features/favorites.d.ts.map +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/state.d.ts +2 -3
- 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/player/options.d.ts +21 -19
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +5 -2
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-native.d.ts +2 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +260 -127
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +33 -53
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
- 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 +0 -1
- package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +10 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -7
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
- package/nitrogen/generated/android/c++/JSection.hpp +114 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -4
- 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/IOSOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
- 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/ResolvedTrack.kt +12 -18
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
- 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 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +23 -5
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
- package/nitrogen/generated/ios/swift/Section.swift +100 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +64 -0
- package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +20 -73
- package/nitrogen/generated/ios/swift/TrackStyle.swift +28 -22
- 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++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
- package/nitrogen/generated/shared/c++/Section.hpp +106 -0
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +96 -0
- package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +15 -31
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +50 -34
- package/package.json +3 -2
- package/src/AudioBrowser.ts +1 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +11 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +2 -2
- package/src/features/favorites.ts +13 -10
- package/src/features/hook-wiring.test.ts +63 -15
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/state.ts +2 -3
- package/src/features/playback/trackPlaybackTime.ts +1 -1
- package/src/features/player/options.ts +27 -19
- package/src/features/player/setup.test.ts +12 -3
- package/src/features/player/setup.ts +3 -1
- package/src/features/player/validateOptions.ts +2 -0
- package/src/features/queue/activeTrack.ts +14 -3
- package/src/features/queue/queue.ts +11 -4
- package/src/nitro-stub-drift.test.ts +277 -0
- package/src/public-surface.test.ts +40 -0
- package/src/specs/audio-browser.nitro.ts +4 -3
- package/src/style-resolution.test.ts +139 -0
- package/src/types/browser-native.ts +2 -2
- package/src/types/browser-nodes.ts +268 -136
- package/src/types/browser.ts +34 -55
- package/src/utils/LazyNativeEmitter.ts +20 -10
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.ts +37 -22
- package/src/web/browser/BrowserManager.ts +176 -95
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- 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/player/NowPlayingManager.ts +4 -3
- package/src/web/util/BrowserPathHelper.test.ts +52 -0
- package/src/web/util/BrowserPathHelper.ts +61 -13
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- 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/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- 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/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
|
@@ -89,21 +89,24 @@ public final class RNABCarPlayController: NSObject {
|
|
|
89
89
|
audioBrowser?.browserManager.config ?? BrowserConfig()
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/// The configured list playing-indicator location, in CarPlay-framework terms.
|
|
93
|
+
private var playingIndicatorLocation: CPListItemPlayingIndicatorLocation {
|
|
94
|
+
switch audioBrowser?.carPlayPlayingIndicatorLocation {
|
|
95
|
+
case .trailing: .trailing
|
|
96
|
+
case .leading, nil: .leading
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
/// Whether a list item identifies the currently active (loaded) track.
|
|
93
101
|
///
|
|
94
|
-
///
|
|
102
|
+
/// Compares track identities (`id` when non-blank, else `src`): the active
|
|
95
103
|
/// track may have been loaded outside the browse tree (the consumer's own
|
|
96
104
|
/// `load`/`setQueue`) with a `src` that differs textually from the browse
|
|
97
105
|
/// row's (absolute vs relative, extra query params) while being the same
|
|
98
|
-
/// item.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if let id, !id.isEmpty, let currentId = currentTrack.id, !currentId.isEmpty {
|
|
103
|
-
return id == currentId
|
|
104
|
-
}
|
|
105
|
-
guard let src else { return false }
|
|
106
|
-
return currentTrack.src == src
|
|
106
|
+
/// item. A nil identity on either side never matches.
|
|
107
|
+
private func isActiveTrack(identity: String?) -> Bool {
|
|
108
|
+
guard let identity, let currentTrack = audioBrowser?.getPlayer()?.currentTrack else { return false }
|
|
109
|
+
return currentTrack.identity == identity
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
// MARK: - Initialization
|
|
@@ -118,8 +121,8 @@ public final class RNABCarPlayController: NSObject {
|
|
|
118
121
|
self?.listItemFactory?.createListItem(for: track, handler: handler)
|
|
119
122
|
?? CPListItem(text: track.title, detailText: nil)
|
|
120
123
|
}
|
|
121
|
-
nowPlayingManager.
|
|
122
|
-
Task { @MainActor in await self?.
|
|
124
|
+
nowPlayingManager.navigateToPath = { [weak self] path, title in
|
|
125
|
+
Task { @MainActor in await self?.navigateToPath(path, title: title) }
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
|
|
@@ -178,12 +181,13 @@ public final class RNABCarPlayController: NSObject {
|
|
|
178
181
|
|
|
179
182
|
// Create list item factory
|
|
180
183
|
let factory = CarPlayListItemFactory(
|
|
181
|
-
isActiveTrack: { [weak self]
|
|
184
|
+
isActiveTrack: { [weak self] identity in self?.isActiveTrack(identity: identity) ?? false },
|
|
182
185
|
onItemSelected: { [weak self] track, completion in
|
|
183
186
|
self?.handleItemSelection(track: track, completion: completion)
|
|
184
187
|
},
|
|
185
188
|
)
|
|
186
189
|
factory.imageLoader = self.imageLoader
|
|
190
|
+
factory.playingIndicatorLocation = self.playingIndicatorLocation
|
|
187
191
|
self.listItemFactory = factory
|
|
188
192
|
|
|
189
193
|
self.setupContentSubscriptions()
|
|
@@ -261,7 +265,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
261
265
|
}
|
|
262
266
|
|
|
263
267
|
// Subscribe to config changes (for Now Playing buttons and per-track
|
|
264
|
-
// button state — e.g.
|
|
268
|
+
// button state — e.g. resolveAlbumPath appearing/disappearing)
|
|
265
269
|
audioBrowser.browserManager.onConfigChanged = { [weak self] _ in
|
|
266
270
|
Task { @MainActor in
|
|
267
271
|
self?.nowPlayingManager.setupNowPlayingButtons()
|
|
@@ -269,13 +273,22 @@ public final class RNABCarPlayController: NSObject {
|
|
|
269
273
|
}
|
|
270
274
|
}
|
|
271
275
|
|
|
272
|
-
// Now-playing buttons + Up Next
|
|
273
|
-
// refresh them when player options
|
|
274
|
-
// the only path that can change them).
|
|
276
|
+
// Now-playing buttons + Up Next + the list playing-indicator location are
|
|
277
|
+
// runtime-updatable via updateOptions(); refresh them when player options
|
|
278
|
+
// change (configureBrowser is no longer the only path that can change them).
|
|
275
279
|
let optionsToken = audioBrowser.playerOptionsChangedEmitter.addListener { [weak self] in
|
|
276
280
|
Task { @MainActor in
|
|
277
|
-
self
|
|
278
|
-
self
|
|
281
|
+
guard let self else { return }
|
|
282
|
+
self.nowPlayingManager.setupNowPlayingButtons()
|
|
283
|
+
self.nowPlayingManager.updateNowPlayingButtonStates()
|
|
284
|
+
// The factory's copy doubles as the last-applied value: sweep the
|
|
285
|
+
// templates only when the location actually changed, not on every
|
|
286
|
+
// updateOptions() call.
|
|
287
|
+
let location = self.playingIndicatorLocation
|
|
288
|
+
if let factory = self.listItemFactory, factory.playingIndicatorLocation != location {
|
|
289
|
+
factory.playingIndicatorLocation = location
|
|
290
|
+
self.updatePlayingIndicators()
|
|
291
|
+
}
|
|
279
292
|
}
|
|
280
293
|
}
|
|
281
294
|
listenerRemovals.append { [weak audioBrowser] in
|
|
@@ -409,9 +422,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
409
422
|
private func buildInitialInterface() async {
|
|
410
423
|
guard let audioBrowser else {
|
|
411
424
|
logger.error("AudioBrowser not available")
|
|
412
|
-
await showRootNavigationError(
|
|
413
|
-
NavigationError(code: .unknownError, message: "", statusCode: nil, statusCodeSuccess: nil),
|
|
414
|
-
)
|
|
425
|
+
await showRootNavigationError(.code(.unknownError))
|
|
415
426
|
return
|
|
416
427
|
}
|
|
417
428
|
|
|
@@ -430,16 +441,10 @@ public final class RNABCarPlayController: NSObject {
|
|
|
430
441
|
showLoadingTemplate()
|
|
431
442
|
}
|
|
432
443
|
do {
|
|
444
|
+
// An empty query result routes to the .emptyContent error page inside
|
|
445
|
+
// showTabBar, shared with runtime tab changes.
|
|
433
446
|
let queriedTabs = try await audioBrowser.browserManager.queryTabs()
|
|
434
|
-
|
|
435
|
-
await showTabBar(tabs: queriedTabs)
|
|
436
|
-
} else {
|
|
437
|
-
// Empty is a navigation error (.emptyContent) so it formats through
|
|
438
|
-
// the app's formatNavigationError like every other failure (ADR 0001).
|
|
439
|
-
await showRootNavigationError(
|
|
440
|
-
NavigationError(code: .emptyContent, message: "", statusCode: nil, statusCodeSuccess: nil),
|
|
441
|
-
)
|
|
442
|
-
}
|
|
447
|
+
await showTabBar(tabs: queriedTabs)
|
|
443
448
|
} catch {
|
|
444
449
|
logger.error("Failed to query tabs: \(error.localizedDescription)")
|
|
445
450
|
await showRootNavigationError(NavigationError.from(error))
|
|
@@ -459,7 +464,13 @@ public final class RNABCarPlayController: NSObject {
|
|
|
459
464
|
// MARK: - Tab Bar
|
|
460
465
|
|
|
461
466
|
@MainActor
|
|
462
|
-
private func showTabBar(tabs: [Track]) async {
|
|
467
|
+
private func showTabBar(tabs allTabs: [Track]) async {
|
|
468
|
+
// A disabled tab hides — CPTabBarTemplate has no gray affordance, so per
|
|
469
|
+
// Track.disabled's rendering ladder an unavailable tab must not render as
|
|
470
|
+
// a normal-looking control (matches Android Auto, which filters before
|
|
471
|
+
// its 4-tab cap).
|
|
472
|
+
let tabs = allTabs.filter { $0.disabled != true }
|
|
473
|
+
|
|
463
474
|
// Serialize concurrent tab-bar builds across BOTH the gated and non-gated
|
|
464
475
|
// paths with one shared generation token: bump at entry, capture, and bail
|
|
465
476
|
// before any template mutation once a newer build supersedes us. Both paths
|
|
@@ -473,6 +484,17 @@ public final class RNABCarPlayController: NSObject {
|
|
|
473
484
|
gateBuildGeneration &+= 1
|
|
474
485
|
let generation = gateBuildGeneration
|
|
475
486
|
|
|
487
|
+
// A zero-tab CPTabBarTemplate renders blank, with no copy, so empty routes
|
|
488
|
+
// to the .emptyContent error page like every other failure (formatted
|
|
489
|
+
// through the app's formatNavigationError, ADR 0001). The guard lives at
|
|
490
|
+
// this choke point — both the initial build and runtime tab changes pass
|
|
491
|
+
// through here — and sits before the gate check, since a gated build from
|
|
492
|
+
// zero tabs is equally blank.
|
|
493
|
+
guard !tabs.isEmpty else {
|
|
494
|
+
await showRootNavigationError(.code(.emptyContent))
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
|
|
476
498
|
// While gated, each tab path is resolved per-request: a gated tab renders
|
|
477
499
|
// the gate page with that request's chrome; an allowed tab shows real
|
|
478
500
|
// content. This generalizes the old "all tabs show the gate" — a path the
|
|
@@ -501,8 +523,8 @@ public final class RNABCarPlayController: NSObject {
|
|
|
501
523
|
interfaceController.safeSetRoot(tabBar, animated: true)
|
|
502
524
|
|
|
503
525
|
// Load content for the first tab only - others load lazily when selected
|
|
504
|
-
if let firstTemplate = tabTemplates.first, let firstTab = tabs.first, let
|
|
505
|
-
await loadContent(for:
|
|
526
|
+
if let firstTemplate = tabTemplates.first, let firstTab = tabs.first, let path = firstTab.path {
|
|
527
|
+
await loadContent(for: path, into: firstTemplate)
|
|
506
528
|
}
|
|
507
529
|
}
|
|
508
530
|
|
|
@@ -540,7 +562,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
540
562
|
var templates: [CPListTemplate] = []
|
|
541
563
|
for tab in limitedTabs {
|
|
542
564
|
let outcome = await audioBrowser.gateDecision(
|
|
543
|
-
for: NativeGateRequest(reason: .browse, path: tab.
|
|
565
|
+
for: NativeGateRequest(reason: .browse, path: tab.path, search: nil),
|
|
544
566
|
)
|
|
545
567
|
guard gateBuildGeneration == generation else { return } // superseded by a newer build
|
|
546
568
|
if outcome.gated {
|
|
@@ -566,8 +588,8 @@ public final class RNABCarPlayController: NSObject {
|
|
|
566
588
|
// Eagerly fill any allowed (content) tabs: templateDidAppear isn't
|
|
567
589
|
// guaranteed to re-fire for templates swapped in via updateTemplates.
|
|
568
590
|
for (tab, template) in zip(limitedTabs, templates) {
|
|
569
|
-
guard getPath(from: template) != nil, let
|
|
570
|
-
await loadContent(for:
|
|
591
|
+
guard getPath(from: template) != nil, let path = tab.path else { continue }
|
|
592
|
+
await loadContent(for: path, into: template)
|
|
571
593
|
}
|
|
572
594
|
}
|
|
573
595
|
|
|
@@ -576,7 +598,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
576
598
|
/// until its content lazy-loads on first appearance.
|
|
577
599
|
private func createTabTemplate(for track: Track) -> CPListTemplate {
|
|
578
600
|
// The path stored on the template drives lazy loading and refresh.
|
|
579
|
-
let template = makeLoadingTemplate(title: track.title, path: track.
|
|
601
|
+
let template = makeLoadingTemplate(title: track.title, path: track.path)
|
|
580
602
|
applyTabBarEntry(to: template, for: track)
|
|
581
603
|
return template
|
|
582
604
|
}
|
|
@@ -682,14 +704,13 @@ public final class RNABCarPlayController: NSObject {
|
|
|
682
704
|
// even with no rows — the "Ask Siri to Play" cell IS its content — so it
|
|
683
705
|
// renders rather than falling through to the empty-content state.
|
|
684
706
|
let hasAssistantCell = resolved.carPlaySiriListButton != nil
|
|
685
|
-
|
|
707
|
+
// A page of empty sections is still empty.
|
|
708
|
+
let hasChildren = resolved.normalizedSections?.contains { !$0.children.isEmpty } ?? false
|
|
709
|
+
if !hasChildren, !hasAssistantCell {
|
|
686
710
|
// Empty is modeled as a navigation error (code .emptyContent) so it goes
|
|
687
711
|
// through the same path-aware formatter as failures — letting an app give
|
|
688
712
|
// an empty Favorites tab different copy than an empty search. ADR 0001.
|
|
689
|
-
|
|
690
|
-
code: .emptyContent, message: "", statusCode: nil, statusCodeSuccess: nil,
|
|
691
|
-
)
|
|
692
|
-
await showNavigationErrorView(empty, path: path, on: template)
|
|
713
|
+
await showNavigationErrorView(.code(.emptyContent), path: path, on: template)
|
|
693
714
|
} else {
|
|
694
715
|
updateTemplate(template, with: resolved)
|
|
695
716
|
}
|
|
@@ -749,7 +770,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
749
770
|
}
|
|
750
771
|
|
|
751
772
|
// If this track is already loaded, resume playback and show Now Playing.
|
|
752
|
-
if track.src != nil, isActiveTrack(
|
|
773
|
+
if track.src != nil, isActiveTrack(identity: track.identity) {
|
|
753
774
|
try? audioBrowser.play()
|
|
754
775
|
nowPlayingManager.showNowPlaying()
|
|
755
776
|
completion()
|
|
@@ -777,7 +798,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
777
798
|
|
|
778
799
|
// Release CarPlay immediately so the list never locks up. Apple's handler
|
|
779
800
|
// guidance is to finish processing the tap promptly; for a browse we "finish"
|
|
780
|
-
// by pushing the destination and filling it in (see
|
|
801
|
+
// by pushing the destination and filling it in (see navigateToPath), and for
|
|
781
802
|
// playback the Now Playing surface owns its own loading state.
|
|
782
803
|
completion()
|
|
783
804
|
|
|
@@ -790,8 +811,8 @@ public final class RNABCarPlayController: NSObject {
|
|
|
790
811
|
self.nowPlayingManager.showNowPlaying(popToFront: false)
|
|
791
812
|
case .intercepted:
|
|
792
813
|
self.nowPlayingManager.showNowPlaying(popToFront: false)
|
|
793
|
-
case let .browse(
|
|
794
|
-
await self.
|
|
814
|
+
case let .browse(path):
|
|
815
|
+
await self.navigateToPath(path, title: track.title)
|
|
795
816
|
case .none:
|
|
796
817
|
break
|
|
797
818
|
}
|
|
@@ -809,24 +830,24 @@ public final class RNABCarPlayController: NSObject {
|
|
|
809
830
|
}
|
|
810
831
|
}
|
|
811
832
|
|
|
812
|
-
/// Pushes a browsable
|
|
833
|
+
/// Pushes a browsable path's destination immediately as an empty, spinning list
|
|
813
834
|
/// template — so the list the user tapped from is never blocked. The content is
|
|
814
835
|
/// filled by `templateDidAppear` → `loadContentIfNeeded` → `loadContent`, which
|
|
815
836
|
/// runs once the template is on screen (the timing CarPlay needs: updates made
|
|
816
837
|
/// right after a push are dropped). Backing out and re-tapping retries.
|
|
817
838
|
@MainActor
|
|
818
|
-
private func
|
|
839
|
+
private func navigateToPath(_ path: String, title: String) async {
|
|
819
840
|
// The gate is resolved per-request: a gated path pushes the gate page
|
|
820
841
|
// (with this request's chrome) instead of browsing into content; this also
|
|
821
842
|
// blocks indirect entries like the Now Playing album line. An allowed path
|
|
822
843
|
// browses normally even while a gate is active.
|
|
823
844
|
if isGated, let audioBrowser {
|
|
824
845
|
let outcome = await audioBrowser.gateDecision(
|
|
825
|
-
for: NativeGateRequest(reason: .browse, path:
|
|
846
|
+
for: NativeGateRequest(reason: .browse, path: path, search: nil),
|
|
826
847
|
)
|
|
827
848
|
if outcome.gated {
|
|
828
849
|
audioBrowser.onGate(GateEvent(reason: .browse))
|
|
829
|
-
if let top = interfaceController.topTemplate, getPath(from: top) ==
|
|
850
|
+
if let top = interfaceController.topTemplate, getPath(from: top) == path { return }
|
|
830
851
|
interfaceController.safePush(
|
|
831
852
|
makeGateTemplate(gate: outcome.chrome, tab: nil), animated: true,
|
|
832
853
|
)
|
|
@@ -834,24 +855,24 @@ public final class RNABCarPlayController: NSObject {
|
|
|
834
855
|
}
|
|
835
856
|
}
|
|
836
857
|
// Avoid pushing a duplicate if the top template already shows this path.
|
|
837
|
-
if let top = interfaceController.topTemplate, getPath(from: top) ==
|
|
858
|
+
if let top = interfaceController.topTemplate, getPath(from: top) == path {
|
|
838
859
|
return
|
|
839
860
|
}
|
|
840
861
|
// …and guard a rapid double-tap whose first push hasn't appeared yet (so the
|
|
841
862
|
// check above can't see it). Cleared when the pushed template appears.
|
|
842
|
-
guard !navigatingPaths.contains(
|
|
843
|
-
navigatingPaths.insert(
|
|
863
|
+
guard !navigatingPaths.contains(path) else { return }
|
|
864
|
+
navigatingPaths.insert(path)
|
|
844
865
|
|
|
845
|
-
let template = makeLoadingTemplate(title: title, path:
|
|
866
|
+
let template = makeLoadingTemplate(title: title, path: path)
|
|
846
867
|
interfaceController.safePush(template, animated: true) { [weak self] pushed, error in
|
|
847
868
|
guard !pushed else { return }
|
|
848
869
|
// The push can fail (e.g. CarPlay's template stack depth limit). The
|
|
849
870
|
// appear callback that normally clears the guard will never fire, so
|
|
850
871
|
// clear it here — otherwise the destination stays unreachable until
|
|
851
872
|
// CarPlay reconnects.
|
|
852
|
-
self?.navigatingPaths.remove(
|
|
873
|
+
self?.navigatingPaths.remove(path)
|
|
853
874
|
if let error {
|
|
854
|
-
self?.logger.error("pushTemplate failed for \(
|
|
875
|
+
self?.logger.error("pushTemplate failed for \(path): \(error.localizedDescription)")
|
|
855
876
|
}
|
|
856
877
|
}
|
|
857
878
|
}
|
|
@@ -930,14 +951,14 @@ public final class RNABCarPlayController: NSObject {
|
|
|
930
951
|
@MainActor
|
|
931
952
|
private func handleTabsChanged(_ tabs: [Track]) {
|
|
932
953
|
logger.debug("Tabs changed: \(tabs.count) tabs")
|
|
933
|
-
// Same tabs by
|
|
954
|
+
// Same tabs by path while ungated → only the tab-bar entries changed
|
|
934
955
|
// (e.g. titles after a locale switch): restamp them in place. No rebuild,
|
|
935
956
|
// so the selected tab and any pushed navigation stack survive. (Gate
|
|
936
957
|
// templates carry no path, so a gated tab bar never matches here.)
|
|
937
958
|
if !isGated,
|
|
938
959
|
let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate,
|
|
939
960
|
tabBar.templates.count == min(tabs.count, CPTabBarTemplate.maximumTabCount),
|
|
940
|
-
zip(tabBar.templates, tabs).allSatisfy({ getPath(from: $0) == $1.
|
|
961
|
+
zip(tabBar.templates, tabs).allSatisfy({ getPath(from: $0) == $1.path })
|
|
941
962
|
{
|
|
942
963
|
for (template, tab) in zip(tabBar.templates, tabs) {
|
|
943
964
|
applyTabBarEntry(to: template, for: tab)
|
|
@@ -978,7 +999,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
978
999
|
// This callback fires when the main browser's content changes.
|
|
979
1000
|
// For CarPlay-specific refreshes (e.g., favorites), use notifyContentChanged instead.
|
|
980
1001
|
guard let content else { return }
|
|
981
|
-
refreshTemplatesForPath(content.
|
|
1002
|
+
refreshTemplatesForPath(content.path, with: content)
|
|
982
1003
|
}
|
|
983
1004
|
|
|
984
1005
|
@MainActor
|
|
@@ -990,9 +1011,11 @@ public final class RNABCarPlayController: NSObject {
|
|
|
990
1011
|
nowPlayingManager.handleActiveTrackChanged()
|
|
991
1012
|
}
|
|
992
1013
|
|
|
993
|
-
/// Updates the isPlaying state
|
|
1014
|
+
/// Updates the isPlaying state (and indicator location, which options can
|
|
1015
|
+
/// change at runtime) on all list items based on the current active track.
|
|
994
1016
|
@MainActor
|
|
995
1017
|
fileprivate func updatePlayingIndicators() {
|
|
1018
|
+
let location = playingIndicatorLocation
|
|
996
1019
|
var templates: [CPListTemplate] = []
|
|
997
1020
|
|
|
998
1021
|
if let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate {
|
|
@@ -1015,16 +1038,20 @@ public final class RNABCarPlayController: NSObject {
|
|
|
1015
1038
|
for item in section.items {
|
|
1016
1039
|
guard let listItem = item as? CPListItem,
|
|
1017
1040
|
let info = listItem.carPlayItemInfo,
|
|
1018
|
-
info.
|
|
1041
|
+
info.identity != nil
|
|
1019
1042
|
else { continue }
|
|
1020
1043
|
|
|
1021
|
-
let isPlaying = isActiveTrack(
|
|
1044
|
+
let isPlaying = isActiveTrack(identity: info.identity)
|
|
1022
1045
|
if listItem.isPlaying != isPlaying {
|
|
1023
1046
|
logger.debug(
|
|
1024
|
-
"Updating isPlaying for \(info.
|
|
1047
|
+
"Updating isPlaying for \(info.identity ?? ""): \(listItem.isPlaying) → \(isPlaying)")
|
|
1025
1048
|
listItem.isPlaying = isPlaying
|
|
1026
1049
|
templateChanged = true
|
|
1027
1050
|
}
|
|
1051
|
+
if listItem.playingIndicatorLocation != location {
|
|
1052
|
+
listItem.playingIndicatorLocation = location
|
|
1053
|
+
templateChanged = true
|
|
1054
|
+
}
|
|
1028
1055
|
}
|
|
1029
1056
|
}
|
|
1030
1057
|
// Setting isPlaying on an already-displayed item does not reliably reach
|
|
@@ -1336,12 +1363,23 @@ private extension RNABCarPlayController {
|
|
|
1336
1363
|
}
|
|
1337
1364
|
}
|
|
1338
1365
|
|
|
1366
|
+
// MARK: - NavigationError Shorthand
|
|
1367
|
+
|
|
1368
|
+
extension NavigationError {
|
|
1369
|
+
/// A code-only error: the user-facing copy comes from the app's
|
|
1370
|
+
/// formatNavigationError (ADR 0001), so no message travels with it.
|
|
1371
|
+
static func code(_ code: NavigationErrorType) -> NavigationError {
|
|
1372
|
+
NavigationError(code: code, message: "", statusCode: nil, statusCodeSuccess: nil)
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1339
1376
|
// MARK: - UIImage Resize
|
|
1340
1377
|
|
|
1341
1378
|
extension UIImage {
|
|
1342
|
-
/// Draws the image centered within the target size, maintaining aspect
|
|
1343
|
-
|
|
1344
|
-
|
|
1379
|
+
/// Draws the image centered within the target size, maintaining aspect
|
|
1380
|
+
/// ratio, at the given display scale (the car screen's, not the phone's).
|
|
1381
|
+
func resized(to targetSize: CGSize, scale: CGFloat) -> UIImage? {
|
|
1382
|
+
UIGraphicsBeginImageContextWithOptions(targetSize, false, scale)
|
|
1345
1383
|
defer { UIGraphicsEndImageContext() }
|
|
1346
1384
|
|
|
1347
1385
|
// Scale to fit while maintaining aspect ratio
|
|
@@ -36,30 +36,39 @@ 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
|
|
|
@@ -77,7 +86,7 @@ final class CarPlayImageLoader {
|
|
|
77
86
|
let action = await CarPlayArtworkResolver.resolve(
|
|
78
87
|
artwork: track.artwork?.url,
|
|
79
88
|
artworkSourceUri: track.artworkSource?.uri,
|
|
80
|
-
|
|
89
|
+
stencil: (style ?? track.style)?.artworkRendering == .stencil,
|
|
81
90
|
targetWidth: Double(size.width),
|
|
82
91
|
targetHeight: Double(size.height),
|
|
83
92
|
displayScale: Double(self.carTraitCollection.displayScale),
|
|
@@ -88,7 +97,7 @@ final class CarPlayImageLoader {
|
|
|
88
97
|
case let .sfSymbol(artwork, width, height):
|
|
89
98
|
completion(self.sfSymbolImage(forArtwork: artwork, canvasSize: CGSize(width: width, height: height)))
|
|
90
99
|
case let .fetch(uri, headers, shouldTint, isSvg):
|
|
91
|
-
let image = await self.fetchImage(uri: uri, headers: headers, isSvg: isSvg)
|
|
100
|
+
let image = await self.fetchImage(uri: uri, headers: headers, isSvg: isSvg, size: size)
|
|
92
101
|
if let image, shouldTint {
|
|
93
102
|
completion(self.createAdaptiveImage(image, carTraitCollection: self.carTraitCollection))
|
|
94
103
|
} else {
|
|
@@ -115,13 +124,18 @@ final class CarPlayImageLoader {
|
|
|
115
124
|
/// beats an empty slot, and this is usually a transient fetch failure.
|
|
116
125
|
private func loadVariants(
|
|
117
126
|
for track: Track,
|
|
127
|
+
style: TrackStyle?,
|
|
118
128
|
variants: ArtworkVariants,
|
|
119
129
|
size: CGSize,
|
|
120
130
|
completion: @escaping @Sendable (UIImage?) -> Void,
|
|
121
131
|
) {
|
|
122
132
|
Task {
|
|
123
|
-
async let light = self.loadArtworkAsync(
|
|
124
|
-
|
|
133
|
+
async let light = self.loadArtworkAsync(
|
|
134
|
+
for: track.copying(artwork: .some(.first(variants.light))), style: style, size: size,
|
|
135
|
+
)
|
|
136
|
+
async let dark = self.loadArtworkAsync(
|
|
137
|
+
for: track.copying(artwork: .some(.first(variants.dark))), style: style, size: size,
|
|
138
|
+
)
|
|
125
139
|
let (lightImage, darkImage) = await (light, dark)
|
|
126
140
|
|
|
127
141
|
guard let lightImage, let darkImage else {
|
|
@@ -137,19 +151,25 @@ final class CarPlayImageLoader {
|
|
|
137
151
|
}
|
|
138
152
|
|
|
139
153
|
/// `loadArtwork` as an awaitable, so the two variants can load concurrently.
|
|
140
|
-
private func loadArtworkAsync(for track: Track, size: CGSize) async -> UIImage? {
|
|
154
|
+
private func loadArtworkAsync(for track: Track, style: TrackStyle?, size: CGSize) async -> UIImage? {
|
|
141
155
|
await withCheckedContinuation { continuation in
|
|
142
|
-
self.loadArtwork(for: track, size: size) { image in
|
|
156
|
+
self.loadArtwork(for: track, style: style, size: size) { image in
|
|
143
157
|
continuation.resume(returning: image)
|
|
144
158
|
}
|
|
145
159
|
}
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
/// Fetches an image from a URL with optional headers and SVG processing.
|
|
149
|
-
|
|
163
|
+
/// Rasters are downsampled to `size` points at the car's display scale —
|
|
164
|
+
/// CPListItem.h: "size them to the display scale of the car screen". SVGs
|
|
165
|
+
/// already rasterize at car scale via SVGProcessor.
|
|
166
|
+
private func fetchImage(uri: String, headers: [String: String]?, isSvg: Bool, size: CGSize) async -> UIImage? {
|
|
150
167
|
let source = ImageSource(uri: uri, method: nil, headers: headers, body: nil)
|
|
151
|
-
let
|
|
152
|
-
|
|
168
|
+
let scale = carTraitCollection.displayScale
|
|
169
|
+
if isSvg {
|
|
170
|
+
return await ArtworkImageFetcher.fetchImage(from: source, svgScale: scale)
|
|
171
|
+
}
|
|
172
|
+
return await ArtworkImageFetcher.fetchImage(from: source, downsampleTo: (size, scale))
|
|
153
173
|
}
|
|
154
174
|
|
|
155
175
|
/// Creates an SF Symbol image rendered at the given canvas size.
|