react-native-audio-browser 0.2.0-next.10 → 0.2.0-next.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/android/build.gradle +6 -5
- package/android/gradle.properties +1 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +49 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -7
- package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
- package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +93 -73
- package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +46 -5
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +86 -27
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +20 -6
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +249 -86
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +35 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +69 -30
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +345 -8
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
- package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +152 -8
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +218 -10
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
- package/ios/Browser/BrowserConfig.swift +102 -5
- package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
- package/ios/Browser/BrowserManager.swift +131 -143
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/JsonModels.swift +187 -106
- package/ios/Browser/SearchDrillIn.swift +6 -6
- package/ios/Browser/SectionPresentation.swift +34 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +47 -0
- package/ios/Browser/TabBarEntries.swift +2 -2
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +105 -67
- package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
- package/ios/CarPlay/CarPlayListItemFactory.swift +217 -84
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
- package/ios/Extension/Section+Copying.swift +31 -0
- package/ios/Extension/Track+Copying.swift +6 -12
- package/ios/Extension/Track+Identity.swift +18 -0
- package/ios/HybridAudioBrowser.swift +113 -35
- package/ios/Model/NitroTypeStubs.swift +64 -23
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Player/NowPlayingUpdater.swift +1 -1
- package/ios/Player/PlaybackCoordinator.swift +2 -2
- package/ios/Player/RetryManager.swift +4 -3
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/JsonModelsTests.swift +75 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/QueueManagerTests.swift +4 -4
- package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +61 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +67 -0
- package/ios/Tests/TabBarEntriesTests.swift +18 -18
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- package/lib/commonjs/AudioBrowser.js +11 -0
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/features/browser-config.js +11 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +2 -2
- package/lib/commonjs/features/favorites.js +13 -10
- package/lib/commonjs/features/favorites.js.map +1 -1
- package/lib/commonjs/features/playback/controls.js +4 -2
- package/lib/commonjs/features/playback/controls.js.map +1 -1
- package/lib/commonjs/features/playback/state.js +2 -3
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +3 -1
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +2 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -1
- package/lib/commonjs/features/queue/activeTrack.js +14 -3
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +11 -4
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
- package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/commonjs/utils/getTrackIdentity.js +24 -0
- package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +144 -87
- package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
- package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
- package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
- package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/commonjs/web/browser/sections.js +89 -0
- package/lib/commonjs/web/browser/sections.js.map +1 -0
- package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/AudioBrowser.js +1 -0
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/features/browser-config.js +11 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +2 -2
- package/lib/module/features/favorites.js +13 -10
- package/lib/module/features/favorites.js.map +1 -1
- package/lib/module/features/playback/controls.js +4 -2
- package/lib/module/features/playback/controls.js.map +1 -1
- package/lib/module/features/playback/state.js +2 -3
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +3 -1
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +2 -0
- package/lib/module/features/player/validateOptions.js.map +1 -1
- package/lib/module/features/queue/activeTrack.js +14 -3
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +11 -4
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/utils/LazyNativeEmitter.js +21 -10
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/utils/NativeUpdatedValue.js +31 -13
- package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/module/utils/getTrackIdentity.js +20 -0
- package/lib/module/utils/getTrackIdentity.js.map +1 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +32 -24
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +144 -87
- package/lib/module/web/browser/BrowserManager.js.map +1 -1
- package/lib/module/web/browser/FavoriteManager.js +31 -21
- package/lib/module/web/browser/FavoriteManager.js.map +1 -1
- package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/module/web/browser/sections.js +83 -0
- package/lib/module/web/browser/sections.js.map +1 -0
- package/lib/module/web/player/NowPlayingManager.js +4 -3
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js +56 -11
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts +1 -0
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/browser.d.ts +2 -2
- package/lib/typescript/src/features/favorites.d.ts +13 -10
- package/lib/typescript/src/features/favorites.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/controls.d.ts +4 -2
- package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +2 -3
- package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
- package/lib/typescript/src/features/player/options.d.ts +21 -19
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +5 -2
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-native.d.ts +2 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +260 -127
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +33 -53
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
- package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
- package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +10 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -7
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
- package/nitrogen/generated/android/c++/JSection.hpp +114 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +23 -5
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
- package/nitrogen/generated/ios/swift/Section.swift +100 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +64 -0
- package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +20 -73
- package/nitrogen/generated/ios/swift/TrackStyle.swift +28 -22
- package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
- package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
- package/nitrogen/generated/shared/c++/Section.hpp +106 -0
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +96 -0
- package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +15 -31
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +50 -34
- package/package.json +3 -2
- package/src/AudioBrowser.ts +1 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +11 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +2 -2
- package/src/features/favorites.ts +13 -10
- package/src/features/hook-wiring.test.ts +63 -15
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/state.ts +2 -3
- package/src/features/playback/trackPlaybackTime.ts +1 -1
- package/src/features/player/options.ts +27 -19
- package/src/features/player/setup.test.ts +12 -3
- package/src/features/player/setup.ts +3 -1
- package/src/features/player/validateOptions.ts +2 -0
- package/src/features/queue/activeTrack.ts +14 -3
- package/src/features/queue/queue.ts +11 -4
- package/src/nitro-stub-drift.test.ts +277 -0
- package/src/public-surface.test.ts +40 -0
- package/src/specs/audio-browser.nitro.ts +4 -3
- package/src/style-resolution.test.ts +139 -0
- package/src/types/browser-native.ts +2 -2
- package/src/types/browser-nodes.ts +268 -136
- package/src/types/browser.ts +34 -55
- package/src/utils/LazyNativeEmitter.ts +20 -10
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.ts +37 -22
- package/src/web/browser/BrowserManager.ts +176 -95
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
- package/src/web/browser/assertBrowsePageShape.ts +5 -3
- package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
- package/src/web/browser/classifyTrackNavigation.ts +9 -9
- package/src/web/browser/parseSearchResponse.test.ts +8 -2
- package/src/web/browser/sections.test.ts +91 -0
- package/src/web/browser/sections.ts +72 -0
- package/src/web/player/NowPlayingManager.ts +4 -3
- package/src/web/util/BrowserPathHelper.test.ts +52 -0
- package/src/web/util/BrowserPathHelper.ts +61 -13
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
- package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
- package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
- package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
- package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
|
@@ -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
|
}
|
|
@@ -10,9 +10,11 @@ extension ResolvedTrack {
|
|
|
10
10
|
/// - `.some(nil)` → set field to nil
|
|
11
11
|
/// - `.some(value)` → set field to value
|
|
12
12
|
///
|
|
13
|
-
/// `
|
|
13
|
+
/// `path` and `title` use single-optional since they are non-optional on ResolvedTrack.
|
|
14
14
|
func copying(
|
|
15
|
-
|
|
15
|
+
path: String? = nil,
|
|
16
|
+
style: SectionStyle?? = nil,
|
|
17
|
+
sections: [Section]?? = nil,
|
|
16
18
|
children: [Track]?? = nil,
|
|
17
19
|
carPlaySiriListButton: CarPlaySiriListButtonPosition?? = nil,
|
|
18
20
|
id: String?? = nil,
|
|
@@ -20,24 +22,22 @@ extension ResolvedTrack {
|
|
|
20
22
|
artwork: Variant_String_ArtworkVariants?? = nil,
|
|
21
23
|
artworkSource: ImageSource?? = nil,
|
|
22
24
|
request: TrackRequest?? = nil,
|
|
23
|
-
artworkCarPlayTinted: Bool?? = nil,
|
|
24
25
|
title: String? = nil,
|
|
25
26
|
subtitle: String?? = nil,
|
|
26
27
|
artist: String?? = nil,
|
|
27
|
-
|
|
28
|
+
albumPath: String?? = nil,
|
|
28
29
|
album: String?? = nil,
|
|
29
30
|
description: String?? = nil,
|
|
30
31
|
genre: String?? = nil,
|
|
31
32
|
duration: Double?? = nil,
|
|
32
|
-
|
|
33
|
-
childrenStyle: TrackStyle?? = nil,
|
|
33
|
+
disabled: Bool?? = nil,
|
|
34
34
|
favorited: Bool?? = nil,
|
|
35
|
-
groupTitle: String?? = nil,
|
|
36
35
|
live: Bool?? = nil,
|
|
37
|
-
imageRow: [ImageRowItem]?? = nil,
|
|
38
36
|
) -> ResolvedTrack {
|
|
39
37
|
ResolvedTrack(
|
|
40
|
-
|
|
38
|
+
path: path ?? self.path,
|
|
39
|
+
style: style ?? self.style,
|
|
40
|
+
sections: sections ?? self.sections,
|
|
41
41
|
children: children ?? self.children,
|
|
42
42
|
carPlaySiriListButton: carPlaySiriListButton ?? self.carPlaySiriListButton,
|
|
43
43
|
id: id ?? self.id,
|
|
@@ -45,21 +45,17 @@ extension ResolvedTrack {
|
|
|
45
45
|
artwork: artwork ?? self.artwork,
|
|
46
46
|
artworkSource: artworkSource ?? self.artworkSource,
|
|
47
47
|
request: request ?? self.request,
|
|
48
|
-
artworkCarPlayTinted: artworkCarPlayTinted ?? self.artworkCarPlayTinted,
|
|
49
48
|
title: title ?? self.title,
|
|
50
49
|
subtitle: subtitle ?? self.subtitle,
|
|
51
50
|
artist: artist ?? self.artist,
|
|
52
|
-
|
|
51
|
+
albumPath: albumPath ?? self.albumPath,
|
|
53
52
|
album: album ?? self.album,
|
|
54
53
|
description: description ?? self.description,
|
|
55
54
|
genre: genre ?? self.genre,
|
|
56
55
|
duration: duration ?? self.duration,
|
|
57
|
-
|
|
58
|
-
childrenStyle: childrenStyle ?? self.childrenStyle,
|
|
56
|
+
disabled: disabled ?? self.disabled,
|
|
59
57
|
favorited: favorited ?? self.favorited,
|
|
60
|
-
groupTitle: groupTitle ?? self.groupTitle,
|
|
61
58
|
live: live ?? self.live,
|
|
62
|
-
imageRow: imageRow ?? self.imageRow,
|
|
63
59
|
)
|
|
64
60
|
}
|
|
65
61
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#if canImport(NitroModules)
|
|
2
|
+
import NitroModules
|
|
3
|
+
#endif
|
|
4
|
+
|
|
5
|
+
extension Section {
|
|
6
|
+
/// Returns a copy of this Section with only the specified fields changed.
|
|
7
|
+
///
|
|
8
|
+
/// Same double-optional (`T??`) convention as the other copying helpers;
|
|
9
|
+
/// `children` is single-optional since it is non-optional on Section.
|
|
10
|
+
func copying(
|
|
11
|
+
title: String?? = nil,
|
|
12
|
+
subtitle: String?? = nil,
|
|
13
|
+
style: SectionStyle?? = nil,
|
|
14
|
+
path: String?? = nil,
|
|
15
|
+
children: [Track]? = nil,
|
|
16
|
+
) -> Section {
|
|
17
|
+
Section(
|
|
18
|
+
title: title ?? self.title,
|
|
19
|
+
subtitle: subtitle ?? self.subtitle,
|
|
20
|
+
style: style ?? self.style,
|
|
21
|
+
path: path ?? self.path,
|
|
22
|
+
children: children ?? self.children,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// One untitled section — the canonical wrap of a flat track list
|
|
27
|
+
/// (`children` authoring sugar, search results — ADR 0010).
|
|
28
|
+
static func untitled(_ children: [Track]) -> Section {
|
|
29
|
+
Section(title: nil, subtitle: nil, style: nil, path: nil, children: children)
|
|
30
|
+
}
|
|
31
|
+
}
|