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
|
@@ -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.
|
|
@@ -7,8 +7,9 @@ import os.log
|
|
|
7
7
|
|
|
8
8
|
/// Typed metadata stored on CPListItems, replacing stringly-typed userInfo dictionaries.
|
|
9
9
|
struct CarPlayItemInfo {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/// The track's identity (`id` when non-blank, else `src`) — the comparison
|
|
11
|
+
/// key for the now-playing indicator repaint sweep.
|
|
12
|
+
let identity: String?
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
extension CPListItem {
|
|
@@ -29,11 +30,14 @@ final class CarPlayListItemFactory {
|
|
|
29
30
|
private let logger = Logger(subsystem: "com.audiobrowser", category: "CarPlayListItemFactory")
|
|
30
31
|
|
|
31
32
|
var imageLoader: CarPlayImageLoader?
|
|
32
|
-
|
|
33
|
+
/// Pushed by the controller (at creation and when options change), like
|
|
34
|
+
/// `imageLoader` — the factory never reads options itself.
|
|
35
|
+
var playingIndicatorLocation: CPListItemPlayingIndicatorLocation = .leading
|
|
36
|
+
private let isActiveTrack: (_ identity: String?) -> Bool
|
|
33
37
|
private let onItemSelected: (Track, @escaping () -> Void) -> Void
|
|
34
38
|
|
|
35
39
|
init(
|
|
36
|
-
isActiveTrack: @escaping (_
|
|
40
|
+
isActiveTrack: @escaping (_ identity: String?) -> Bool,
|
|
37
41
|
onItemSelected: @escaping (Track, @escaping () -> Void) -> Void,
|
|
38
42
|
) {
|
|
39
43
|
self.isActiveTrack = isActiveTrack
|
|
@@ -42,55 +46,69 @@ final class CarPlayListItemFactory {
|
|
|
42
46
|
|
|
43
47
|
// MARK: - Sections
|
|
44
48
|
|
|
49
|
+
/// Whether this OS has CarPlay's wrapping tile container (the iOS 26
|
|
50
|
+
/// element API) — the capability `SectionPresentation` degrades against.
|
|
51
|
+
private static var supportsWrappingGrid: Bool {
|
|
52
|
+
if #available(iOS 26.0, *) { return true }
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Maps the page's sections 1:1 to CPListSections (ADR 0010), respecting
|
|
57
|
+
/// CarPlay's section and total-item budgets. Style blocks declare the
|
|
58
|
+
/// requested presentation (resolved `section ?? page` here — ADR 0011);
|
|
59
|
+
/// this renders CarPlay's nearest supported form (`SectionPresentation`):
|
|
60
|
+
/// - list (default): a titled/headerless section of list rows.
|
|
61
|
+
/// - single-line grid (`gridWrap: false`): a headerless section holding
|
|
62
|
+
/// one single-line image-row item whose text is the section title — the
|
|
63
|
+
/// tiles that fit render, the rest truncate (the platform doesn't
|
|
64
|
+
/// report the fit).
|
|
65
|
+
/// - wrapping grid: a wrapping, titled tile grid on iOS 26+; a plain list
|
|
66
|
+
/// before that.
|
|
45
67
|
func createSections(from resolvedTrack: ResolvedTrack) -> [CPListSection] {
|
|
46
|
-
guard let
|
|
68
|
+
guard let sections = resolvedTrack.normalizedSections else {
|
|
47
69
|
return []
|
|
48
70
|
}
|
|
49
71
|
|
|
50
72
|
let maxSections = CPListTemplate.maximumSectionCount
|
|
51
73
|
let maxTotalItems = CPListTemplate.maximumItemCount
|
|
52
74
|
|
|
53
|
-
|
|
54
|
-
// Android Auto's GROUP_TITLE hint has), so sections — titled or not —
|
|
55
|
-
// render in exact source (server) order. Ungrouped runs become headerless
|
|
56
|
-
// sections in place rather than being hoisted to the top.
|
|
57
|
-
var runs: [(title: String?, tracks: [Track])] = []
|
|
58
|
-
for track in children {
|
|
59
|
-
if let last = runs.indices.last, runs[last].title == track.groupTitle {
|
|
60
|
-
runs[last].tracks.append(track)
|
|
61
|
-
} else {
|
|
62
|
-
runs.append((track.groupTitle, [track]))
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Create sections (respecting both section and total item limits)
|
|
67
|
-
var sections: [CPListSection] = []
|
|
75
|
+
var listSections: [CPListSection] = []
|
|
68
76
|
var totalItemCount = 0
|
|
69
77
|
|
|
70
|
-
for
|
|
71
|
-
guard
|
|
78
|
+
for section in sections {
|
|
79
|
+
guard listSections.count < maxSections else { break }
|
|
72
80
|
guard totalItemCount < maxTotalItems else { break }
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
// An empty section is a dead end regardless of style (a "popular"
|
|
82
|
+
// section with no entries yet) — skip it rather than render a header
|
|
83
|
+
// with nothing under it.
|
|
84
|
+
guard !section.children.isEmpty else { continue }
|
|
85
|
+
|
|
86
|
+
let style = StyleResolver.sectionStyle(section: section.style, page: resolvedTrack.style)
|
|
87
|
+
let presentation = SectionPresentation(for: style, supportsWrappingGrid: Self.supportsWrappingGrid)
|
|
88
|
+
switch presentation {
|
|
89
|
+
case .list:
|
|
90
|
+
let availableSlots = maxTotalItems - totalItemCount
|
|
91
|
+
let items: [CPListTemplateItem] = section.children.prefix(availableSlots).map {
|
|
92
|
+
createListItem(for: $0, style: StyleResolver.trackStyle(track: $0.style, section: style))
|
|
81
93
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
if let title = section.title {
|
|
95
|
+
listSections.append(CPListSection(items: items, header: title, sectionIndexTitle: nil))
|
|
96
|
+
} else {
|
|
97
|
+
listSections.append(CPListSection(items: items))
|
|
98
|
+
}
|
|
99
|
+
totalItemCount += items.count
|
|
100
|
+
case .singleLineRow, .wrappingGrid:
|
|
101
|
+
// Tile presentations render as one image-row item inside a
|
|
102
|
+
// headerless CPListSection, with the section title as the item's
|
|
103
|
+
// text — a headed section would render the title twice.
|
|
104
|
+
guard let item = createImageRowItem(for: section, style: style, presentation: presentation)
|
|
105
|
+
else { continue }
|
|
106
|
+
listSections.append(CPListSection(items: [item]))
|
|
107
|
+
totalItemCount += 1
|
|
89
108
|
}
|
|
90
|
-
totalItemCount += items.count
|
|
91
109
|
}
|
|
92
110
|
|
|
93
|
-
return
|
|
111
|
+
return listSections
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
// MARK: - List Item
|
|
@@ -98,9 +116,14 @@ final class CarPlayListItemFactory {
|
|
|
98
116
|
/// Creates a CPListItem for a track with common setup (typed userInfo, artwork, isPlaying).
|
|
99
117
|
/// - Parameters:
|
|
100
118
|
/// - track: The track to create the item for
|
|
119
|
+
/// - style: The track's resolved style block (`track ?? section ?? page`,
|
|
120
|
+
/// via `StyleResolver`). nil = the track's own declaration — the one
|
|
121
|
+
/// genuinely section-less caller (the Up Next queue list) has no
|
|
122
|
+
/// inheritance to resolve.
|
|
101
123
|
/// - handler: Optional custom handler. If nil, uses default browse/play handling.
|
|
102
124
|
func createListItem(
|
|
103
125
|
for track: Track,
|
|
126
|
+
style: TrackStyle? = nil,
|
|
104
127
|
handler: ((CPSelectableListItem, @escaping () -> Void) -> Void)? = nil,
|
|
105
128
|
) -> CPListItem {
|
|
106
129
|
let item = CPListItem(
|
|
@@ -109,46 +132,59 @@ final class CarPlayListItemFactory {
|
|
|
109
132
|
)
|
|
110
133
|
|
|
111
134
|
// Store typed info for updatePlayingIndicators()
|
|
112
|
-
item.setCarPlayItemInfo(CarPlayItemInfo(
|
|
135
|
+
item.setCarPlayItemInfo(CarPlayItemInfo(identity: track.identity))
|
|
113
136
|
|
|
114
137
|
// Set accessory type based on whether track is browsable or playable
|
|
115
138
|
if let src = track.src {
|
|
116
139
|
// Playable track - check if it's currently playing
|
|
117
140
|
item.accessoryType = .none
|
|
118
|
-
//
|
|
119
|
-
//
|
|
141
|
+
// Consumer-configurable via `ios.carPlayPlayingIndicatorLocation`
|
|
142
|
+
// (default leading: the indicator draws in the artwork slot). Note the
|
|
120
143
|
// indicator's rendering is owned by the phone's CarPlay service, and on
|
|
121
144
|
// some iOS/CarPlay-Simulator combinations it isn't drawn at all — for
|
|
122
145
|
// ANY third-party app. Verify on a real head unit before assuming a
|
|
123
146
|
// logic bug here.
|
|
124
|
-
item.playingIndicatorLocation =
|
|
125
|
-
item.isPlaying = isActiveTrack(track.
|
|
147
|
+
item.playingIndicatorLocation = playingIndicatorLocation
|
|
148
|
+
item.isPlaying = isActiveTrack(track.identity)
|
|
126
149
|
if item.isPlaying {
|
|
127
150
|
logger.debug("Setting isPlaying=true for: \(track.title) (src: \(src))")
|
|
128
151
|
}
|
|
129
|
-
} else if track.
|
|
152
|
+
} else if track.path != nil {
|
|
130
153
|
// Browsable only - show disclosure indicator
|
|
131
154
|
item.accessoryType = .disclosureIndicator
|
|
132
155
|
}
|
|
133
156
|
|
|
157
|
+
// An unavailable track grays out and goes inert — list rows are the
|
|
158
|
+
// surface that CAN draw the fact, so it shows rather than hides
|
|
159
|
+
// (Track.disabled's rendering ladder).
|
|
160
|
+
if track.disabled == true {
|
|
161
|
+
item.isEnabled = false
|
|
162
|
+
}
|
|
163
|
+
|
|
134
164
|
// Load artwork with size context for proper CDN optimization
|
|
135
165
|
if track.artwork != nil || track.artworkSource != nil {
|
|
136
166
|
// Set empty placeholder to reserve space while loading
|
|
137
|
-
item.setImage(imageLoader?.placeholderImage)
|
|
138
|
-
imageLoader?.loadArtwork(for: track, size: CPListItem.maximumImageSize) { [weak item] image in
|
|
167
|
+
item.setImage(imageLoader?.placeholderImage(size: CPListItem.maximumImageSize))
|
|
168
|
+
imageLoader?.loadArtwork(for: track, style: style, size: CPListItem.maximumImageSize) { [weak item] image in
|
|
139
169
|
Task { @MainActor in
|
|
140
170
|
item?.setImage(image)
|
|
141
171
|
}
|
|
142
172
|
}
|
|
143
173
|
}
|
|
144
174
|
|
|
145
|
-
// Set selection handler
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
175
|
+
// Set selection handler. The disabled guard wraps BOTH handler paths —
|
|
176
|
+
// custom handlers (Up Next rows skipping the queue, say) included: belt
|
|
177
|
+
// over the isEnabled braces, so an unavailable track stays inert even if
|
|
178
|
+
// a surface delivers the tap.
|
|
179
|
+
let selection = handler ?? { [onItemSelected] _, completion in
|
|
180
|
+
onItemSelected(track, completion)
|
|
181
|
+
}
|
|
182
|
+
item.handler = { item, completion in
|
|
183
|
+
guard track.disabled != true else {
|
|
184
|
+
completion()
|
|
185
|
+
return
|
|
151
186
|
}
|
|
187
|
+
selection(item, completion)
|
|
152
188
|
}
|
|
153
189
|
|
|
154
190
|
return item
|
|
@@ -156,58 +192,141 @@ final class CarPlayListItemFactory {
|
|
|
156
192
|
|
|
157
193
|
// MARK: - Image Row Item
|
|
158
194
|
|
|
159
|
-
///
|
|
160
|
-
///
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
195
|
+
/// Target size for images inside an image row. The iOS 26 SDK deprecates
|
|
196
|
+
/// `CPListImageRowItem.maximumImageSize` in favor of the element-based API,
|
|
197
|
+
/// so read whichever the running OS considers canonical.
|
|
198
|
+
private static var rowImageSize: CGSize {
|
|
199
|
+
if #available(iOS 26.0, *) {
|
|
200
|
+
return CPListImageRowItemElement.maximumImageSize
|
|
164
201
|
}
|
|
202
|
+
return CPListImageRowItem.maximumImageSize
|
|
203
|
+
}
|
|
165
204
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
205
|
+
/// Creates the image-row item rendering a tile-presented section: a single
|
|
206
|
+
/// truncating line (`.singleLineRow`) or a wrapping multi-line grid
|
|
207
|
+
/// (`.wrappingGrid` — constructed on iOS 26+ only, by `SectionPresentation`).
|
|
208
|
+
///
|
|
209
|
+
/// Returns nil when nothing is drawable — every child hidden by the
|
|
210
|
+
/// disabled ladder (pre-26 only; the element APIs gray instead).
|
|
211
|
+
private func createImageRowItem(
|
|
212
|
+
for section: Section, style: SectionStyle, presentation: SectionPresentation,
|
|
213
|
+
) -> CPListImageRowItem? {
|
|
214
|
+
let singleLine = presentation == .singleLineRow
|
|
215
|
+
// Disabled ladder: iOS 26 elements can draw unavailability (grayed +
|
|
216
|
+
// inert, below); the legacy image row can't, so there a disabled track
|
|
217
|
+
// hides — never a normal-looking dead tile.
|
|
218
|
+
let visibleChildren: [Track] = if #available(iOS 26.0, *) {
|
|
219
|
+
section.children
|
|
220
|
+
} else {
|
|
221
|
+
section.children.filter { $0.disabled != true }
|
|
222
|
+
}
|
|
223
|
+
guard !visibleChildren.isEmpty else { return nil }
|
|
224
|
+
// A single line shows at most CPMaximumNumberOfGridImages; the wrapping
|
|
225
|
+
// grid takes every child (queue scope is declared, never rendered — the
|
|
226
|
+
// cap here only limits what's drawn, and only where the platform does).
|
|
227
|
+
let tracks = singleLine
|
|
228
|
+
? Array(visibleChildren.prefix(CPMaximumNumberOfGridImages))
|
|
229
|
+
: visibleChildren
|
|
230
|
+
let placeholder = { [imageLoader] in
|
|
231
|
+
imageLoader?.placeholderImage(size: Self.rowImageSize) ?? UIImage()
|
|
232
|
+
}
|
|
171
233
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
234
|
+
let item: CPListImageRowItem
|
|
235
|
+
// Applies an async-loaded artwork image to a tile on the element-based
|
|
236
|
+
// API (the elements are captured per availability branch — their type
|
|
237
|
+
// doesn't exist before iOS 26). nil = the legacy gridImages path.
|
|
238
|
+
// @MainActor makes the closure Sendable for the artwork completion hop.
|
|
239
|
+
var applyImage: (@MainActor (Int, UIImage) -> Void)?
|
|
240
|
+
if #available(iOS 26.0, *) {
|
|
241
|
+
if singleLine {
|
|
242
|
+
// Row elements are the only tile element with a subtitle slot — the
|
|
243
|
+
// imageGridElements branch below has no equivalent, by SDK design.
|
|
244
|
+
let rowElements = tracks.map { track in
|
|
245
|
+
let element = CPListImageRowItemRowElement(
|
|
246
|
+
image: placeholder(), title: track.title, subtitle: track.subtitle,
|
|
247
|
+
)
|
|
248
|
+
if track.disabled == true {
|
|
249
|
+
element.isEnabled = false
|
|
250
|
+
}
|
|
251
|
+
return element
|
|
252
|
+
}
|
|
253
|
+
item = CPListImageRowItem(text: section.title, elements: rowElements, allowsMultipleLines: false)
|
|
254
|
+
applyImage = { index, image in
|
|
255
|
+
guard index < rowElements.count else { return }
|
|
256
|
+
rowElements[index].image = image
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
// imageGridElements, never the title-less gridElements: an
|
|
260
|
+
// artwork-less track must keep its name on screen (ADR 0010).
|
|
261
|
+
let gridElements = tracks.map { track in
|
|
262
|
+
let element = CPListImageRowItemImageGridElement(
|
|
263
|
+
image: placeholder(), imageShape: .roundedRectangle, title: track.title,
|
|
264
|
+
accessorySymbolName: nil,
|
|
265
|
+
)
|
|
266
|
+
if track.disabled == true {
|
|
267
|
+
element.isEnabled = false
|
|
268
|
+
}
|
|
269
|
+
return element
|
|
270
|
+
}
|
|
271
|
+
item = CPListImageRowItem(text: section.title, imageGridElements: gridElements, allowsMultipleLines: true)
|
|
272
|
+
applyImage = { index, image in
|
|
273
|
+
guard index < gridElements.count else { return }
|
|
274
|
+
gridElements[index].image = image
|
|
275
|
+
}
|
|
276
|
+
}
|
|
175
277
|
} else {
|
|
176
|
-
|
|
278
|
+
let placeholders = tracks.map { _ in placeholder() }
|
|
279
|
+
// Use imageTitles variant on iOS 17.4+ to show titles below each thumbnail
|
|
280
|
+
if #available(iOS 17.4, *) {
|
|
281
|
+
item = CPListImageRowItem(text: section.title ?? "", images: placeholders, imageTitles: tracks.map(\.title))
|
|
282
|
+
} else {
|
|
283
|
+
item = CPListImageRowItem(text: section.title ?? "", images: placeholders)
|
|
284
|
+
}
|
|
177
285
|
}
|
|
178
286
|
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
287
|
+
// Header tap → navigate to section.path ("view all"). A path-less
|
|
288
|
+
// section is a pure preview — its header tap is a no-op rather than a
|
|
289
|
+
// selection that can't resolve.
|
|
182
290
|
item.handler = { [onItemSelected] _, completion in
|
|
183
|
-
guard
|
|
291
|
+
guard let path = section.path else {
|
|
184
292
|
completion()
|
|
185
293
|
return
|
|
186
294
|
}
|
|
187
|
-
onItemSelected(
|
|
295
|
+
onItemSelected(Self.navigationTrack(path: path, title: section.title), completion)
|
|
188
296
|
}
|
|
189
297
|
|
|
190
|
-
//
|
|
298
|
+
// Per-tile taps select the child track directly. A disabled tile is
|
|
299
|
+
// grayed and inert (26+ elements) — the guard keeps it inert even if
|
|
300
|
+
// the surface delivers the tap anyway.
|
|
191
301
|
item.listImageRowHandler = { [onItemSelected] _, index, completion in
|
|
192
|
-
guard index <
|
|
302
|
+
guard index < tracks.count, tracks[index].disabled != true else {
|
|
193
303
|
completion()
|
|
194
304
|
return
|
|
195
305
|
}
|
|
196
|
-
|
|
197
|
-
onItemSelected(tappedItem.toTrack(), completion)
|
|
306
|
+
onItemSelected(tracks[index], completion)
|
|
198
307
|
}
|
|
199
308
|
|
|
200
|
-
// Load artwork for each
|
|
201
|
-
for (index,
|
|
202
|
-
guard
|
|
309
|
+
// Load artwork for each rendered tile asynchronously
|
|
310
|
+
for (index, track) in tracks.enumerated() {
|
|
311
|
+
guard track.artwork != nil || track.artworkSource != nil else { continue }
|
|
203
312
|
|
|
204
|
-
imageLoader?.loadArtwork(
|
|
313
|
+
imageLoader?.loadArtwork(
|
|
314
|
+
for: track,
|
|
315
|
+
style: StyleResolver.trackStyle(track: track.style, section: style),
|
|
316
|
+
size: Self.rowImageSize,
|
|
317
|
+
) { [weak item] image in
|
|
205
318
|
Task { @MainActor in
|
|
206
319
|
guard let item, let image else { return }
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
320
|
+
if let applyImage {
|
|
321
|
+
// The element-based API exposes a settable `image`; assigning it
|
|
322
|
+
// updates the element the row observes.
|
|
323
|
+
applyImage(index, image)
|
|
324
|
+
} else {
|
|
325
|
+
var images = item.gridImages
|
|
326
|
+
if index < images.count {
|
|
327
|
+
images[index] = image
|
|
328
|
+
item.update(images)
|
|
329
|
+
}
|
|
211
330
|
}
|
|
212
331
|
}
|
|
213
332
|
}
|
|
@@ -215,4 +334,18 @@ final class CarPlayListItemFactory {
|
|
|
215
334
|
|
|
216
335
|
return item
|
|
217
336
|
}
|
|
337
|
+
|
|
338
|
+
/// A synthetic browsable Track for section-header navigation — the
|
|
339
|
+
/// section's "view all" surface has a path and a title, but no Track.
|
|
340
|
+
/// Style-less by design: CarPlay resolves presentation from the page it
|
|
341
|
+
/// navigates to. (Android's counterpart projects the section's block —
|
|
342
|
+
/// the promise only exists to feed Android Auto's parent-level hint.)
|
|
343
|
+
private static func navigationTrack(path: String, title: String?) -> Track {
|
|
344
|
+
Track(
|
|
345
|
+
id: nil, path: path, src: nil, artwork: nil, artworkSource: nil,
|
|
346
|
+
request: nil, title: title ?? "", subtitle: nil,
|
|
347
|
+
artist: nil, albumPath: nil, album: nil, description: nil, genre: nil,
|
|
348
|
+
duration: nil, style: nil, disabled: nil, favorited: nil, live: nil,
|
|
349
|
+
)
|
|
350
|
+
}
|
|
218
351
|
}
|
|
@@ -16,8 +16,8 @@ final class CarPlayNowPlayingManager {
|
|
|
16
16
|
|
|
17
17
|
private let interfaceController: CPInterfaceController
|
|
18
18
|
var listItemFactory: (Track, ((CPSelectableListItem, @escaping () -> Void) -> Void)?) -> CPListItem
|
|
19
|
-
/// Pushes a browse destination (wired to the controller's
|
|
20
|
-
var
|
|
19
|
+
/// Pushes a browse destination (wired to the controller's navigateToPath).
|
|
20
|
+
var navigateToPath: ((_ path: String, _ title: String) -> Void)?
|
|
21
21
|
|
|
22
22
|
private weak var audioBrowser: HybridAudioBrowser?
|
|
23
23
|
private var nowPlayingObserver: NowPlayingObserver?
|
|
@@ -46,11 +46,11 @@ final class CarPlayNowPlayingManager {
|
|
|
46
46
|
private var builtRow: BuiltButtonRow?
|
|
47
47
|
|
|
48
48
|
/// Browse path the album line navigates to for the active track —
|
|
49
|
-
/// `track.
|
|
49
|
+
/// `track.albumPath`, or the pre-resolved `resolveAlbumPath` result. Resolved
|
|
50
50
|
/// at track-change time (not at tap) so the album line only surfaces when a
|
|
51
51
|
/// destination actually exists.
|
|
52
52
|
private var albumArtistDestination: String?
|
|
53
|
-
/// Drops a stale async `
|
|
53
|
+
/// Drops a stale async `resolveAlbumPath` result when the track changed while
|
|
54
54
|
/// the resolver was in flight (latest-update-wins).
|
|
55
55
|
private var albumArtistGeneration: UInt = 0
|
|
56
56
|
/// The just-tapped track whose load is still in flight. CarPlay reads
|
|
@@ -214,7 +214,7 @@ final class CarPlayNowPlayingManager {
|
|
|
214
214
|
|
|
215
215
|
/// Re-resolves the album line's destination for the active track and
|
|
216
216
|
/// enables the (tappable) album/artist button only when one exists:
|
|
217
|
-
/// `track.
|
|
217
|
+
/// `track.albumPath`, or the app's `resolveAlbumPath` result.
|
|
218
218
|
/// Marks `track` as the just-tapped (still loading) track and derives the
|
|
219
219
|
/// album line's destination from it. Must run before the Now Playing
|
|
220
220
|
/// template is pushed — CarPlay reads `isAlbumArtistButtonEnabled` at display
|
|
@@ -242,12 +242,12 @@ final class CarPlayNowPlayingManager {
|
|
|
242
242
|
setAlbumArtistDestination(nil)
|
|
243
243
|
return
|
|
244
244
|
}
|
|
245
|
-
logger.info("AlbumArtist: track=\(track.title)
|
|
246
|
-
if let
|
|
247
|
-
setAlbumArtistDestination(
|
|
245
|
+
logger.info("AlbumArtist: track=\(track.title) albumPath=\(track.albumPath ?? "nil") resolver=\(self.config.resolveAlbumPath != nil)")
|
|
246
|
+
if let albumPath = track.albumPath {
|
|
247
|
+
setAlbumArtistDestination(albumPath)
|
|
248
248
|
return
|
|
249
249
|
}
|
|
250
|
-
guard let resolver = config.
|
|
250
|
+
guard let resolver = config.resolveAlbumPath else {
|
|
251
251
|
setAlbumArtistDestination(nil)
|
|
252
252
|
return
|
|
253
253
|
}
|
|
@@ -256,7 +256,7 @@ final class CarPlayNowPlayingManager {
|
|
|
256
256
|
do {
|
|
257
257
|
path = try await resolver(track).await()
|
|
258
258
|
} catch {
|
|
259
|
-
self?.logger.error("
|
|
259
|
+
self?.logger.error("resolveAlbumPath failed: \(error.localizedDescription)")
|
|
260
260
|
path = nil
|
|
261
261
|
}
|
|
262
262
|
guard let self, self.albumArtistGeneration == generation else { return }
|
|
@@ -295,7 +295,10 @@ final class CarPlayNowPlayingManager {
|
|
|
295
295
|
/// Returns the appropriate image for the favorite button based on state
|
|
296
296
|
private func favoriteButtonImage(isFavorited: Bool) -> UIImage {
|
|
297
297
|
let symbolName = isFavorited ? "heart.fill" : "heart"
|
|
298
|
-
guard let image = UIImage(systemName: symbolName)?.resized(
|
|
298
|
+
guard let image = UIImage(systemName: symbolName)?.resized(
|
|
299
|
+
to: CPNowPlayingButtonMaximumImageSize,
|
|
300
|
+
scale: interfaceController.carTraitCollection.displayScale,
|
|
301
|
+
) else {
|
|
299
302
|
return UIImage()
|
|
300
303
|
}
|
|
301
304
|
return image
|
|
@@ -360,7 +363,7 @@ final class CarPlayNowPlayingManager {
|
|
|
360
363
|
// the pushed destination (artist may be unset, or the live song).
|
|
361
364
|
let title = track?.album ?? track?.artist ?? track?.title ?? ""
|
|
362
365
|
logger.info("Album/Artist button tapped, navigating to \(destination)")
|
|
363
|
-
|
|
366
|
+
navigateToPath?(destination, title)
|
|
364
367
|
}
|
|
365
368
|
|
|
366
369
|
private func updateNowPlayingUpNextButton() {
|
|
@@ -54,7 +54,7 @@ extension PlayerCapabilities {
|
|
|
54
54
|
// Apple silently removed. On the iOS 16+ target it can never be invoked: CarPlay
|
|
55
55
|
// uses its own CPNowPlayingButton heart, and Siri "I like this" routes through
|
|
56
56
|
// INUpdateMediaAffinityIntent. The `favorite` capability still drives the
|
|
57
|
-
//
|
|
57
|
+
// favorite row indicators via setFavoriteEnabled. See issues #67 / #71.
|
|
58
58
|
|
|
59
59
|
// Mode controls
|
|
60
60
|
if shuffleMode != false {
|
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
extension PlayerCapabilities {
|
|
4
|
-
/// Resolved favorite match mode, or `nil` when favoriting is disabled.
|
|
5
|
-
///
|
|
6
|
-
/// `false`/unset → `nil`; `true` → `.exact`; `{ match }` → that match.
|
|
7
|
-
var favoriteMatch: FavoritesMatchMode? {
|
|
8
|
-
switch favorite {
|
|
9
|
-
case .none:
|
|
10
|
-
nil
|
|
11
|
-
case let .some(.first(enabled)):
|
|
12
|
-
enabled ? .exact : nil
|
|
13
|
-
case let .some(.second(config)):
|
|
14
|
-
config.match
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
4
|
/// Whether the favorite/like control is enabled.
|
|
19
|
-
var favoriteEnabled: Bool {
|
|
5
|
+
var favoriteEnabled: Bool { favorite ?? false }
|
|
20
6
|
}
|