react-native-audio-browser 0.2.0-next.11 → 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/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/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 +26 -39
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +228 -50
- 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/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 +46 -21
- 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/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 +1 -1
- 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 +40 -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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#if canImport(NitroModules)
|
|
2
|
+
import NitroModules
|
|
3
|
+
#endif
|
|
4
|
+
|
|
5
|
+
/// The one place style declarations resolve to effective values (ADR 0011).
|
|
6
|
+
///
|
|
7
|
+
/// Two mechanisms, deliberately distinct:
|
|
8
|
+
/// - **Inherited item properties** resolve `track ?? section ?? page`,
|
|
9
|
+
/// per property — a track's own declaration wins over the blocks that
|
|
10
|
+
/// contain it.
|
|
11
|
+
/// - **Container properties and the positional `display`** resolve by scope
|
|
12
|
+
/// override: `section ?? page` — the page declares for its whole scope, a
|
|
13
|
+
/// section overrides for its own children. `display` is never inherited
|
|
14
|
+
/// item-to-container: each holder describes its own children, and a
|
|
15
|
+
/// track's `display` is the promise for the page *it* opens, not its
|
|
16
|
+
/// rendering inside this section.
|
|
17
|
+
///
|
|
18
|
+
/// Every key of `SectionStyle` must be read by `sectionStyle(section:page:)`
|
|
19
|
+
/// and every inherited key of `TrackStyle` by `trackStyle(track:section:)` —
|
|
20
|
+
/// Nitro flattens the spec's `extends`, so `src/style-resolution.test.ts`
|
|
21
|
+
/// enforces this completeness, not the type system.
|
|
22
|
+
enum StyleResolver {
|
|
23
|
+
/// Folds the page block into a section's block: every `SectionStyle` key
|
|
24
|
+
/// resolves `section ?? page`. The result is the section's effective
|
|
25
|
+
/// block; item resolution then only needs `track ?? section`.
|
|
26
|
+
static func sectionStyle(section: SectionStyle?, page: SectionStyle?) -> SectionStyle {
|
|
27
|
+
// Argument order is the generated init's (own properties before inherited
|
|
28
|
+
// ones — Nitro flattens `extends` own-props-first); the stub mirrors it.
|
|
29
|
+
SectionStyle(
|
|
30
|
+
gridWrap: section?.gridWrap ?? page?.gridWrap,
|
|
31
|
+
display: section?.display ?? page?.display,
|
|
32
|
+
artworkRendering: section?.artworkRendering ?? page?.artworkRendering,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Resolves a track's effective item properties against its (page-folded)
|
|
37
|
+
/// section block.
|
|
38
|
+
static func trackStyle(track: TrackStyle?, section: SectionStyle?) -> TrackStyle {
|
|
39
|
+
TrackStyle(
|
|
40
|
+
// Positional deny-list: `display` is never inherited onto an item —
|
|
41
|
+
// resolved nil, so no renderer can mistake the handle's page promise
|
|
42
|
+
// for this item's own layout.
|
|
43
|
+
display: nil,
|
|
44
|
+
artworkRendering: track?.artworkRendering ?? section?.artworkRendering,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// Compares the tab-list fields the CarPlay tab bar actually renders.
|
|
2
2
|
enum TabBarEntries {
|
|
3
|
-
/// Whether two tab lists produce identical tab-bar entries — title,
|
|
3
|
+
/// Whether two tab lists produce identical tab-bar entries — title, path,
|
|
4
4
|
/// and the artwork inputs of `applyTabBarEntry`. Used to suppress
|
|
5
5
|
/// `tabsChanged` when a tabs re-query (e.g. from `invalidateAllContent()`)
|
|
6
6
|
/// resolves to the same entries, so an unchanged result never churns the
|
|
@@ -8,7 +8,7 @@ enum TabBarEntries {
|
|
|
8
8
|
static func same(_ old: [Track]?, _ new: [Track]) -> Bool {
|
|
9
9
|
guard let old, old.count == new.count else { return false }
|
|
10
10
|
return zip(old, new).allSatisfy {
|
|
11
|
-
$0.title == $1.title && $0.
|
|
11
|
+
$0.title == $1.title && $0.path == $1.path && $0.artwork == $1.artwork
|
|
12
12
|
&& $0.artworkSource?.uri == $1.artworkSource?.uri
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// `{id}` template-token substitution for request-config string values (path, query values,
|
|
4
|
+
/// header values), used by artwork URL resolution so a `nowPlayingArtwork` like
|
|
5
|
+
/// `{ path: "/artwork/{id}" }` resolves per track.
|
|
6
|
+
///
|
|
7
|
+
/// A track without a non-blank id leaves the token LITERALLY in place — the request then
|
|
8
|
+
/// visibly 404s, which is intended (garbage in, garbage out with a self-describing error
|
|
9
|
+
/// trail; the caller logs a single warning when `containsToken` still matches after
|
|
10
|
+
/// substitution). Mirrors Android's `substituteTrackId` / `configContainsIdToken` in
|
|
11
|
+
/// `BrowserUrlResolution.kt` — keep the two in sync.
|
|
12
|
+
enum TrackIdTemplate {
|
|
13
|
+
static let token = "{id}"
|
|
14
|
+
|
|
15
|
+
/// Replaces the token when `id` is non-blank; otherwise returns the value unchanged
|
|
16
|
+
/// (token left literal).
|
|
17
|
+
static func substitute(_ value: String?, id: String?) -> String? {
|
|
18
|
+
guard let id, !id.isEmpty else { return value }
|
|
19
|
+
return value?.replacingOccurrences(of: token, with: id)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Dictionary-value variant of `substitute` (query / header values).
|
|
23
|
+
static func substitute(_ values: [String: String]?, id: String?) -> [String: String]? {
|
|
24
|
+
guard let id, !id.isEmpty else { return values }
|
|
25
|
+
return values?.mapValues { $0.replacingOccurrences(of: token, with: id) }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Whether the value still carries the token (an unfilled `{id}`).
|
|
29
|
+
static func containsToken(_ value: String?) -> Bool {
|
|
30
|
+
value?.contains(token) == true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Whether any dictionary value still carries the token.
|
|
34
|
+
static func containsToken(_ values: [String: String]?) -> Bool {
|
|
35
|
+
values?.values.contains { $0.contains(token) } == true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -29,7 +29,8 @@ enum CarPlayArtworkResolver {
|
|
|
29
29
|
/// - Parameters:
|
|
30
30
|
/// - artwork: The track's `artwork` field (may be an SF Symbol string or URL)
|
|
31
31
|
/// - artworkSourceUri: The track's `artworkSource?.uri` field
|
|
32
|
-
/// -
|
|
32
|
+
/// - stencil: Whether the artwork renders as a monochrome glyph tinted to
|
|
33
|
+
/// the surface appearance (`style.artworkRendering: 'stencil'`)
|
|
33
34
|
/// - targetWidth: Target width in points
|
|
34
35
|
/// - targetHeight: Target height in points
|
|
35
36
|
/// - displayScale: The CarPlay display scale factor
|
|
@@ -37,7 +38,7 @@ enum CarPlayArtworkResolver {
|
|
|
37
38
|
static func resolve(
|
|
38
39
|
artwork: String?,
|
|
39
40
|
artworkSourceUri: String?,
|
|
40
|
-
|
|
41
|
+
stencil: Bool,
|
|
41
42
|
targetWidth: Double,
|
|
42
43
|
targetHeight: Double,
|
|
43
44
|
displayScale: Double,
|
|
@@ -57,7 +58,7 @@ enum CarPlayArtworkResolver {
|
|
|
57
58
|
return resolveDirectFallback(
|
|
58
59
|
artwork: artwork,
|
|
59
60
|
artworkSourceUri: artworkSourceUri,
|
|
60
|
-
|
|
61
|
+
stencil: stencil,
|
|
61
62
|
targetWidth: targetWidth,
|
|
62
63
|
targetHeight: targetHeight,
|
|
63
64
|
)
|
|
@@ -66,7 +67,7 @@ enum CarPlayArtworkResolver {
|
|
|
66
67
|
return .fetch(
|
|
67
68
|
uri: resolved.uri,
|
|
68
69
|
headers: resolved.headers,
|
|
69
|
-
shouldTint:
|
|
70
|
+
shouldTint: stencil,
|
|
70
71
|
isSvg: isSvg,
|
|
71
72
|
)
|
|
72
73
|
}
|
|
@@ -76,7 +77,7 @@ enum CarPlayArtworkResolver {
|
|
|
76
77
|
return resolveDirectFallback(
|
|
77
78
|
artwork: artwork,
|
|
78
79
|
artworkSourceUri: artworkSourceUri,
|
|
79
|
-
|
|
80
|
+
stencil: stencil,
|
|
80
81
|
targetWidth: targetWidth,
|
|
81
82
|
targetHeight: targetHeight,
|
|
82
83
|
)
|
|
@@ -85,7 +86,7 @@ enum CarPlayArtworkResolver {
|
|
|
85
86
|
private static func resolveDirectFallback(
|
|
86
87
|
artwork: String?,
|
|
87
88
|
artworkSourceUri: String?,
|
|
88
|
-
|
|
89
|
+
stencil: Bool,
|
|
89
90
|
targetWidth: Double,
|
|
90
91
|
targetHeight: Double,
|
|
91
92
|
) -> ArtworkLoadAction {
|
|
@@ -100,7 +101,7 @@ enum CarPlayArtworkResolver {
|
|
|
100
101
|
return .fetch(
|
|
101
102
|
uri: directUri,
|
|
102
103
|
headers: nil,
|
|
103
|
-
shouldTint:
|
|
104
|
+
shouldTint: stencil,
|
|
104
105
|
isSvg: isSvg,
|
|
105
106
|
)
|
|
106
107
|
}
|
|
@@ -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
|