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
|
@@ -48,9 +48,9 @@ struct BrowserConfig {
|
|
|
48
48
|
let carPlayLoadingTitle: String?
|
|
49
49
|
|
|
50
50
|
/// Resolves a browse path for the CarPlay Now Playing album line when the
|
|
51
|
-
/// active track has no
|
|
51
|
+
/// active track has no albumPath. Invoked on track changes (not at tap) so
|
|
52
52
|
/// the album line only becomes tappable when a destination exists.
|
|
53
|
-
let
|
|
53
|
+
let resolveAlbumPath: ((_ track: Track) -> Promise<String?>)?
|
|
54
54
|
|
|
55
55
|
/// Custom handler for track load events (overrides default load behavior)
|
|
56
56
|
let handleTrackLoad: ((_ event: TrackLoadEvent) -> Promise<Promise<Void>>)?
|
|
@@ -72,7 +72,7 @@ struct BrowserConfig {
|
|
|
72
72
|
handleTrackLoad: ((_ event: TrackLoadEvent) -> Promise<Promise<Void>>)? = nil,
|
|
73
73
|
androidControllerOfflineError: Bool = true,
|
|
74
74
|
carPlayLoadingTitle: String? = nil,
|
|
75
|
-
|
|
75
|
+
resolveAlbumPath: ((_ track: Track) -> Promise<String?>)? = nil,
|
|
76
76
|
formatNavigationError: ((_ params: FormatNavigationErrorParams) -> Promise<FormattedNavigationError?>)? = nil,
|
|
77
77
|
) {
|
|
78
78
|
self.request = request
|
|
@@ -87,7 +87,7 @@ struct BrowserConfig {
|
|
|
87
87
|
self.handleTrackLoad = handleTrackLoad
|
|
88
88
|
self.androidControllerOfflineError = androidControllerOfflineError
|
|
89
89
|
self.carPlayLoadingTitle = carPlayLoadingTitle
|
|
90
|
-
self.
|
|
90
|
+
self.resolveAlbumPath = resolveAlbumPath
|
|
91
91
|
self.formatNavigationError = formatNavigationError
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -105,10 +105,31 @@ struct BrowserConfig {
|
|
|
105
105
|
handleTrackLoad = config.handleTrackLoad
|
|
106
106
|
androidControllerOfflineError = config.androidControllerOfflineError ?? true
|
|
107
107
|
carPlayLoadingTitle = config.carPlayLoadingTitle
|
|
108
|
-
|
|
108
|
+
resolveAlbumPath = config.resolveAlbumPath
|
|
109
109
|
formatNavigationError = config.formatNavigationError
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/// A copy of this config with every JS-provided function dropped — at every
|
|
113
|
+
/// level, not just the top; each nested type's strip below nils its own —
|
|
114
|
+
/// keeping the static fields (URLs, headers, routes' shape). Makes the config
|
|
115
|
+
/// safe to hold after its JS runtime is gone: callers take their
|
|
116
|
+
/// static/built-in fallback paths (a callback-only route degrades to its
|
|
117
|
+
/// no-content path). See `HybridAudioBrowser.dropStaleJSCallbacks` for why
|
|
118
|
+
/// invoking a stale callback is fatal.
|
|
119
|
+
func strippingJSCallbacks() -> BrowserConfig {
|
|
120
|
+
BrowserConfig(
|
|
121
|
+
request: request?.strippingJSCallbacks(),
|
|
122
|
+
browse: browse?.strippingJSCallbacks(),
|
|
123
|
+
media: media?.strippingJSCallbacks(),
|
|
124
|
+
artwork: artwork?.strippingJSCallbacks(),
|
|
125
|
+
nowPlayingArtwork: nowPlayingArtwork?.strippingJSCallbacks(),
|
|
126
|
+
routes: routes?.map { $0.strippingJSCallbacks() },
|
|
127
|
+
singleTrack: singleTrack,
|
|
128
|
+
androidControllerOfflineError: androidControllerOfflineError,
|
|
129
|
+
carPlayLoadingTitle: carPlayLoadingTitle,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
// MARK: - Track Load Handler
|
|
113
134
|
|
|
114
135
|
private static let logger = Logger(subsystem: "com.audiobrowser", category: "BrowserConfig")
|
|
@@ -139,3 +160,79 @@ struct BrowserConfig {
|
|
|
139
160
|
// MARK: - NativeRouteEntry + RouteEntry conformance
|
|
140
161
|
|
|
141
162
|
extension NativeRouteEntry: RouteEntry {}
|
|
163
|
+
|
|
164
|
+
// MARK: - Deep JS-callback stripping
|
|
165
|
+
|
|
166
|
+
// The generated structs are get-only from Swift, so each strip rebuilds the
|
|
167
|
+
// value through the memberwise initializer with the function fields nil'd.
|
|
168
|
+
|
|
169
|
+
private extension TransformableRequestConfig {
|
|
170
|
+
func strippingJSCallbacks() -> TransformableRequestConfig {
|
|
171
|
+
TransformableRequestConfig(
|
|
172
|
+
transform: nil,
|
|
173
|
+
transformSync: nil,
|
|
174
|
+
method: method,
|
|
175
|
+
path: path,
|
|
176
|
+
baseUrl: baseUrl,
|
|
177
|
+
headers: headers,
|
|
178
|
+
query: query,
|
|
179
|
+
body: body,
|
|
180
|
+
contentType: contentType,
|
|
181
|
+
userAgent: userAgent,
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private extension MediaRequestConfig {
|
|
187
|
+
func strippingJSCallbacks() -> MediaRequestConfig {
|
|
188
|
+
MediaRequestConfig(
|
|
189
|
+
resolve: nil,
|
|
190
|
+
resolveSync: nil,
|
|
191
|
+
transform: nil,
|
|
192
|
+
transformSync: nil,
|
|
193
|
+
method: method,
|
|
194
|
+
path: path,
|
|
195
|
+
baseUrl: baseUrl,
|
|
196
|
+
headers: headers,
|
|
197
|
+
query: query,
|
|
198
|
+
body: body,
|
|
199
|
+
contentType: contentType,
|
|
200
|
+
userAgent: userAgent,
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private extension ArtworkRequestConfig {
|
|
206
|
+
func strippingJSCallbacks() -> ArtworkRequestConfig {
|
|
207
|
+
ArtworkRequestConfig(
|
|
208
|
+
resolve: nil,
|
|
209
|
+
resolveSync: nil,
|
|
210
|
+
transform: nil,
|
|
211
|
+
transformSync: nil,
|
|
212
|
+
imageQueryParams: imageQueryParams,
|
|
213
|
+
method: method,
|
|
214
|
+
path: path,
|
|
215
|
+
baseUrl: baseUrl,
|
|
216
|
+
headers: headers,
|
|
217
|
+
query: query,
|
|
218
|
+
body: body,
|
|
219
|
+
contentType: contentType,
|
|
220
|
+
userAgent: userAgent,
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private extension NativeRouteEntry {
|
|
226
|
+
func strippingJSCallbacks() -> NativeRouteEntry {
|
|
227
|
+
NativeRouteEntry(
|
|
228
|
+
path: path,
|
|
229
|
+
browseCallback: nil,
|
|
230
|
+
browseConfig: browseConfig?.strippingJSCallbacks(),
|
|
231
|
+
browseStatic: browseStatic,
|
|
232
|
+
searchCallback: nil,
|
|
233
|
+
searchConfig: searchConfig?.strippingJSCallbacks(),
|
|
234
|
+
media: media?.strippingJSCallbacks(),
|
|
235
|
+
artwork: artwork?.strippingJSCallbacks(),
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -262,10 +262,14 @@ extension BrowserManager {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
// Substitute the `{id}` template token with the track's id across path/query/header values.
|
|
265
|
-
// (Configs without the token are unaffected — e.g. browse artwork.)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
265
|
+
// (Configs without the token are unaffected — e.g. browse artwork.) A track without a
|
|
266
|
+
// non-blank id leaves the token LITERALLY in place — the request then visibly 404s, which
|
|
267
|
+
// is intended (self-describing error trail) — with a single warning logged below once the
|
|
268
|
+
// URL is built.
|
|
269
|
+
mergedConfig = substituteTrackId(in: mergedConfig, id: track.id)
|
|
270
|
+
let hasUnfilledIdToken = TrackIdTemplate.containsToken(mergedConfig.path)
|
|
271
|
+
|| TrackIdTemplate.containsToken(mergedConfig.query)
|
|
272
|
+
|| TrackIdTemplate.containsToken(mergedConfig.headers)
|
|
269
273
|
|
|
270
274
|
// Build final URL - if no path after merging, no artwork to transform
|
|
271
275
|
guard mergedConfig.path != nil else {
|
|
@@ -280,6 +284,10 @@ extension BrowserManager {
|
|
|
280
284
|
headers["User-Agent"] = userAgent
|
|
281
285
|
}
|
|
282
286
|
|
|
287
|
+
if hasUnfilledIdToken {
|
|
288
|
+
logger.warning("nowPlayingArtwork: track has no id — '{id}' left unfilled in \(uri)")
|
|
289
|
+
}
|
|
290
|
+
|
|
283
291
|
logger.debug("Artwork URL transformed: \(track.artwork?.url ?? "nil") -> \(uri)")
|
|
284
292
|
|
|
285
293
|
return ImageSource(
|
|
@@ -315,18 +323,14 @@ extension BrowserManager {
|
|
|
315
323
|
|
|
316
324
|
/// Replaces the `{id}` token with the track id in a request config's path, query values, and
|
|
317
325
|
/// header values. Used so a `nowPlayingArtwork` like `{ path: "/artwork/{id}" }` resolves.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
guard let d else { return nil }
|
|
322
|
-
return d.mapValues { $0.replacingOccurrences(of: "{id}", with: id) }
|
|
323
|
-
}
|
|
324
|
-
return RequestConfig(
|
|
326
|
+
/// A nil/blank id leaves the values unchanged (token left literal — see TrackIdTemplate).
|
|
327
|
+
private func substituteTrackId(in config: RequestConfig, id: String?) -> RequestConfig {
|
|
328
|
+
RequestConfig(
|
|
325
329
|
method: config.method,
|
|
326
|
-
path:
|
|
330
|
+
path: TrackIdTemplate.substitute(config.path, id: id),
|
|
327
331
|
baseUrl: config.baseUrl,
|
|
328
|
-
headers:
|
|
329
|
-
query:
|
|
332
|
+
headers: TrackIdTemplate.substitute(config.headers, id: id),
|
|
333
|
+
query: TrackIdTemplate.substitute(config.query, id: id),
|
|
330
334
|
body: config.body,
|
|
331
335
|
contentType: config.contentType,
|
|
332
336
|
userAgent: config.userAgent,
|
|
@@ -77,12 +77,12 @@ final class BrowserManager {
|
|
|
77
77
|
var resolvedRequestLayer: TransformableRequestConfig?
|
|
78
78
|
private var resolvedBrowseLayer: TransformableRequestConfig?
|
|
79
79
|
|
|
80
|
-
// Set of favorited track
|
|
80
|
+
// Set of favorited track identities (id when non-blank, else src).
|
|
81
81
|
private var favoriteIds: Set<String> = []
|
|
82
82
|
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
private var
|
|
83
|
+
// Whether favoriting is enabled, propagated from the player's `favorite`
|
|
84
|
+
// capability. false = no row hearts. Set via setFavoriteEnabled.
|
|
85
|
+
private var favoriteEnabled = false
|
|
86
86
|
|
|
87
87
|
// Navigation tracking to prevent race conditions
|
|
88
88
|
private var currentNavigationId: Int = 0
|
|
@@ -162,62 +162,49 @@ final class BrowserManager {
|
|
|
162
162
|
onFavoritesChanged?()
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
///
|
|
166
|
-
///
|
|
167
|
-
func
|
|
168
|
-
|
|
165
|
+
/// Enables/disables favoriting (propagated from the `favorite` capability).
|
|
166
|
+
/// false disables row-heart hydration.
|
|
167
|
+
func setFavoriteEnabled(_ enabled: Bool) {
|
|
168
|
+
favoriteEnabled = enabled
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
/// Optimistically reflects a local favorite toggle
|
|
172
|
-
///
|
|
173
|
-
///
|
|
174
|
-
///
|
|
175
|
-
///
|
|
176
|
-
|
|
177
|
-
/// aren't collaterally lost. The consumer reconciles `favoriteIds` to its
|
|
178
|
-
/// canonical ids on the next `setFavorites`; `isFavorite(src:)` gives the same
|
|
179
|
-
/// answer before and after, so the now-playing heart stays responsive.
|
|
180
|
-
func setFavorited(src: String, favorited: Bool) {
|
|
171
|
+
/// Optimistically reflects a local favorite toggle in the favorite set, keyed
|
|
172
|
+
/// by the track's identity (id when non-blank, else src). The consumer
|
|
173
|
+
/// reconciles `favoriteIds` to its canonical set on the next `setFavorites`;
|
|
174
|
+
/// `isFavorite` gives the same answer before and after, so the now-playing
|
|
175
|
+
/// heart stays responsive.
|
|
176
|
+
func setFavorited(identity: String, favorited: Bool) {
|
|
181
177
|
if favorited {
|
|
182
|
-
favoriteIds.insert(
|
|
183
|
-
} else if favoriteMatch == .partial {
|
|
184
|
-
favoriteIds = favoriteIds.filter { !BrowserPathHelper.containsSegment(src, $0) }
|
|
178
|
+
favoriteIds.insert(identity)
|
|
185
179
|
} else {
|
|
186
|
-
favoriteIds.remove(
|
|
180
|
+
favoriteIds.remove(identity)
|
|
187
181
|
}
|
|
188
182
|
}
|
|
189
183
|
|
|
190
184
|
/// Hydrates the favorited field on a track based on the favoriteIds set.
|
|
191
185
|
/// No-op unless favoriting is enabled (the `favorite` capability). Only
|
|
192
|
-
///
|
|
193
|
-
/// false so non-favorited tracks still show an (empty) heart.
|
|
194
|
-
///
|
|
186
|
+
/// identity-bearing tracks (id or src) are favoritable; the flag is set to
|
|
187
|
+
/// true OR false so non-favorited tracks still show an (empty) heart. A
|
|
188
|
+
/// caller-set `favorited` wins — API/consumer-provided values are never
|
|
189
|
+
/// overwritten (matches Android and web).
|
|
195
190
|
func hydrateFavorite(_ track: Track) -> Track {
|
|
196
|
-
guard
|
|
197
|
-
|
|
198
|
-
let isFavorited = isFavorite(src: src, match: match)
|
|
199
|
-
|
|
200
|
-
// Only create a new track if the favorited state differs
|
|
201
|
-
if track.favorited == isFavorited { return track }
|
|
202
|
-
|
|
203
|
-
return track.copying(favorited: isFavorited)
|
|
191
|
+
guard favoriteEnabled, track.favorited == nil, track.identity != nil else { return track }
|
|
192
|
+
return track.copying(favorited: isFavorite(track))
|
|
204
193
|
}
|
|
205
194
|
|
|
206
|
-
/// Whether
|
|
207
|
-
private func isFavorite(
|
|
208
|
-
|
|
209
|
-
case .exact:
|
|
210
|
-
favoriteIds.contains(src)
|
|
211
|
-
case .partial:
|
|
212
|
-
favoriteIds.contains { BrowserPathHelper.containsSegment(src, $0) }
|
|
213
|
-
}
|
|
195
|
+
/// Whether the track's identity is in the favorite set.
|
|
196
|
+
private func isFavorite(_ track: Track) -> Bool {
|
|
197
|
+
track.identity.map(favoriteIds.contains) ?? false
|
|
214
198
|
}
|
|
215
199
|
|
|
216
|
-
/// Hydrates favorites on all
|
|
200
|
+
/// Hydrates favorites on all tracks of a resolved page. Pages reach
|
|
201
|
+
/// hydration normalized (ADR 0010), so `sections` is the only structure
|
|
202
|
+
/// to walk.
|
|
217
203
|
private func hydrateChildren(_ resolvedTrack: ResolvedTrack) -> ResolvedTrack {
|
|
218
|
-
guard let
|
|
219
|
-
|
|
220
|
-
|
|
204
|
+
guard let sections = resolvedTrack.sections else { return resolvedTrack }
|
|
205
|
+
return resolvedTrack.copying(sections: sections.map { section in
|
|
206
|
+
section.copying(children: section.children.map { hydrateFavorite($0) })
|
|
207
|
+
})
|
|
221
208
|
}
|
|
222
209
|
|
|
223
210
|
// MARK: - Navigation
|
|
@@ -304,12 +291,14 @@ final class BrowserManager {
|
|
|
304
291
|
)
|
|
305
292
|
}
|
|
306
293
|
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
294
|
+
// Normalize to the canonical sectioned shape (children sugar → one
|
|
295
|
+
// untitled section — ADR 0010), then validate and transform. The output
|
|
296
|
+
// never carries `children`.
|
|
297
|
+
if let sections = resolvedTrack.normalizedSections {
|
|
298
|
+
let transformed = try await transformSections(
|
|
299
|
+
sections, parentPath: path, routeEntry: routeEntry?.0,
|
|
311
300
|
)
|
|
312
|
-
return resolvedTrack.copying(children:
|
|
301
|
+
return resolvedTrack.copying(sections: transformed, children: .some(nil))
|
|
313
302
|
}
|
|
314
303
|
|
|
315
304
|
return resolvedTrack
|
|
@@ -426,7 +415,7 @@ final class BrowserManager {
|
|
|
426
415
|
)
|
|
427
416
|
}
|
|
428
417
|
|
|
429
|
-
/// Resolves a browse route: a page object (`{title,
|
|
418
|
+
/// Resolves a browse route: a page object (`{title,path,children:[…]}`).
|
|
430
419
|
/// The browse kind layer is the cached resolved layer (`resolvedBrowseLayer`),
|
|
431
420
|
/// populated by `ensureLayersResolved()` inside `buildApiRequest`.
|
|
432
421
|
private func resolveFromConfig(
|
|
@@ -447,8 +436,8 @@ final class BrowserManager {
|
|
|
447
436
|
let nitroResult = result.toNitro()
|
|
448
437
|
|
|
449
438
|
logger.debug("Resolved: \(nitroResult.title)")
|
|
450
|
-
if let
|
|
451
|
-
logger.debug(" children: \(children.count) tracks")
|
|
439
|
+
if let sections = nitroResult.normalizedSections {
|
|
440
|
+
logger.debug(" children: \(sections.reduce(0) { $0 + $1.children.count }) tracks")
|
|
452
441
|
}
|
|
453
442
|
|
|
454
443
|
return nitroResult
|
|
@@ -525,65 +514,51 @@ final class BrowserManager {
|
|
|
525
514
|
|
|
526
515
|
// MARK: - Child Transformation
|
|
527
516
|
|
|
528
|
-
private func
|
|
529
|
-
_
|
|
517
|
+
private func transformSections(
|
|
518
|
+
_ sections: [Section],
|
|
530
519
|
parentPath: String,
|
|
531
520
|
routeEntry: NativeRouteEntry?,
|
|
532
|
-
) async throws -> [
|
|
533
|
-
var transformed: [
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
521
|
+
) async throws -> [Section] {
|
|
522
|
+
var transformed: [Section] = []
|
|
523
|
+
// The contextual index is the flat page position — children concatenated
|
|
524
|
+
// in section order (ADR 0009/0010).
|
|
525
|
+
var flatIndex = 0
|
|
526
|
+
|
|
527
|
+
for section in sections {
|
|
528
|
+
var transformedChildren: [Track] = []
|
|
529
|
+
|
|
530
|
+
for track in section.children {
|
|
531
|
+
let index = flatIndex
|
|
532
|
+
flatIndex += 1
|
|
533
|
+
|
|
534
|
+
// Validate track has stable identifier.
|
|
535
|
+
if track.path == nil, track.src == nil {
|
|
536
|
+
throw BrowserError.invalidConfiguration(
|
|
537
|
+
"Track must have either 'path' or 'src' for stable identification: \(track.title)",
|
|
538
|
+
)
|
|
539
|
+
}
|
|
546
540
|
|
|
547
|
-
|
|
548
|
-
let contextualUrl = BrowserPathHelper.build(parentPath: parentPath, trackId: track.src!)
|
|
549
|
-
transformedTrack = track.copying(url: contextualUrl)
|
|
550
|
-
}
|
|
541
|
+
var transformedTrack = track
|
|
551
542
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
543
|
+
if track.src != nil, track.path == nil {
|
|
544
|
+
// src != nil guarantees a non-nil identity (id when non-blank, else src).
|
|
545
|
+
// The flat page position rides along as a duplicate-identity tie-breaker.
|
|
546
|
+
let contextualPath = BrowserPathHelper.build(
|
|
547
|
+
parentPath: parentPath, trackId: track.identity!, index: index,
|
|
548
|
+
)
|
|
549
|
+
transformedTrack = track.copying(path: contextualPath)
|
|
550
|
+
}
|
|
557
551
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
let itemImageSource = await resolveArtworkUrl(track: item.toTrack(), perRouteConfig: artworkConfig)
|
|
563
|
-
// Playable items get a contextual URL like any list row, so a
|
|
564
|
-
// thumbnail tap expands into its section's queue instead of a
|
|
565
|
-
// queue of one (ADR 0006).
|
|
566
|
-
let itemUrl = item.url ?? item.src.map {
|
|
567
|
-
BrowserPathHelper.build(parentPath: parentPath, trackId: $0)
|
|
568
|
-
}
|
|
569
|
-
resolvedItems.append(ImageRowItem(
|
|
570
|
-
id: item.id,
|
|
571
|
-
url: itemUrl,
|
|
572
|
-
src: item.src,
|
|
573
|
-
artwork: item.artwork,
|
|
574
|
-
artworkSource: itemImageSource,
|
|
575
|
-
title: item.title,
|
|
576
|
-
artist: item.artist,
|
|
577
|
-
album: item.album,
|
|
578
|
-
albumUrl: item.albumUrl,
|
|
579
|
-
live: item.live,
|
|
580
|
-
request: item.request,
|
|
581
|
-
))
|
|
552
|
+
// Resolve artwork URL at browse-time (no size context)
|
|
553
|
+
let artworkConfig = routeEntry?.artwork ?? config.artwork
|
|
554
|
+
if let imageSource = await resolveArtworkUrl(track: transformedTrack, perRouteConfig: artworkConfig) {
|
|
555
|
+
transformedTrack = transformedTrack.copying(artworkSource: imageSource)
|
|
582
556
|
}
|
|
583
|
-
|
|
557
|
+
|
|
558
|
+
transformedChildren.append(transformedTrack)
|
|
584
559
|
}
|
|
585
560
|
|
|
586
|
-
transformed.append(
|
|
561
|
+
transformed.append(section.copying(children: transformedChildren))
|
|
587
562
|
}
|
|
588
563
|
|
|
589
564
|
return transformed
|
|
@@ -668,37 +643,40 @@ final class BrowserManager {
|
|
|
668
643
|
/// station. Decision in `SearchDrillIn.playable`; mirrors Android's
|
|
669
644
|
/// `searchPlayable`.
|
|
670
645
|
func searchPlayable(_ params: SearchParams) async throws -> [Track]? {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
646
|
+
// Voice search never matches a disabled track (Track.disabled) — filtered
|
|
647
|
+
// before drill-in so an unavailable first result can't capture the play.
|
|
648
|
+
let children = try await ((search(params).flattenedChildren) ?? [])
|
|
649
|
+
.filter { $0.disabled != true }
|
|
650
|
+
return try await SearchDrillIn.playable(from: children) { [self] path in
|
|
651
|
+
logger.debug("First search result is browsable-only, resolving: \(path)")
|
|
652
|
+
return try await ((resolve(path).flattenedChildren) ?? []).filter { $0.disabled != true }
|
|
675
653
|
}
|
|
676
654
|
}
|
|
677
655
|
|
|
678
656
|
private func makeSearchResult(query: String, results: [Track]) -> ResolvedTrack {
|
|
657
|
+
// Search results are a flat list; as a resolved page they are one
|
|
658
|
+
// untitled section (ADR 0010).
|
|
679
659
|
ResolvedTrack(
|
|
680
|
-
|
|
681
|
-
|
|
660
|
+
path: BrowserPathHelper.createSearchPath(query),
|
|
661
|
+
style: nil,
|
|
662
|
+
sections: [.untitled(results)],
|
|
663
|
+
children: nil,
|
|
682
664
|
carPlaySiriListButton: nil,
|
|
683
665
|
id: nil,
|
|
684
666
|
src: nil,
|
|
685
667
|
artwork: nil,
|
|
686
668
|
artworkSource: nil, request: nil,
|
|
687
|
-
artworkCarPlayTinted: nil,
|
|
688
669
|
title: "Search: \(query)",
|
|
689
670
|
subtitle: nil,
|
|
690
671
|
artist: nil,
|
|
691
|
-
|
|
672
|
+
albumPath: nil,
|
|
692
673
|
album: nil,
|
|
693
674
|
description: nil,
|
|
694
675
|
genre: nil,
|
|
695
676
|
duration: nil,
|
|
696
|
-
|
|
697
|
-
childrenStyle: nil,
|
|
677
|
+
disabled: nil,
|
|
698
678
|
favorited: nil,
|
|
699
|
-
groupTitle: nil,
|
|
700
679
|
live: nil,
|
|
701
|
-
imageRow: nil,
|
|
702
680
|
)
|
|
703
681
|
}
|
|
704
682
|
|
|
@@ -715,7 +693,8 @@ final class BrowserManager {
|
|
|
715
693
|
}
|
|
716
694
|
|
|
717
695
|
let resolved = try await resolveRouteEntry(tabsEntry, path: Self.tabsRoutePath, params: [:])
|
|
718
|
-
|
|
696
|
+
// Tabs are a flat list, not a page — a sectioned tabs source flattens.
|
|
697
|
+
let tabTracks = resolved.flattenedChildren ?? []
|
|
719
698
|
|
|
720
699
|
tabs = tabTracks
|
|
721
700
|
return tabTracks
|
|
@@ -723,38 +702,39 @@ final class BrowserManager {
|
|
|
723
702
|
|
|
724
703
|
// MARK: - Queue Expansion (for CarPlay/external controllers)
|
|
725
704
|
|
|
726
|
-
/// Expands a contextual
|
|
727
|
-
func
|
|
728
|
-
guard BrowserPathHelper.isContextual(
|
|
705
|
+
/// Expands a contextual path to a full queue of playable tracks.
|
|
706
|
+
func expandQueueFromContextualPath(_ path: String) async throws -> (tracks: [Track], selectedIndex: Int)? {
|
|
707
|
+
guard BrowserPathHelper.isContextual(path) else { return nil }
|
|
729
708
|
|
|
730
|
-
let parentPath = BrowserPathHelper.stripTrackId(
|
|
731
|
-
guard let trackId = BrowserPathHelper.extractTrackId(
|
|
709
|
+
let parentPath = BrowserPathHelper.stripTrackId(path)
|
|
710
|
+
guard let trackId = BrowserPathHelper.extractTrackId(path) else { return nil }
|
|
732
711
|
|
|
733
|
-
logger.debug("Expanding queue from contextual
|
|
734
|
-
logger.debug("
|
|
712
|
+
logger.debug("Expanding queue from contextual path")
|
|
713
|
+
logger.debug(" path: \(path)")
|
|
735
714
|
logger.debug(" parentPath: \(parentPath)")
|
|
736
715
|
logger.debug(" trackId: \(trackId)")
|
|
737
716
|
|
|
738
717
|
// Resolve the parent container
|
|
739
718
|
let resolved = try await resolve(parentPath, useCache: true)
|
|
740
|
-
guard let
|
|
741
|
-
|
|
742
|
-
// Queue scope is the tapped section, not the whole page (ADR 0006).
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
sectionTracks = tracks
|
|
752
|
-
case nil:
|
|
719
|
+
guard let sections = resolved.normalizedSections else { return nil }
|
|
720
|
+
|
|
721
|
+
// Queue scope is the tapped section, not the whole page (ADR 0006). The
|
|
722
|
+
// stamped flat index pins which section (and which copy) was tapped when
|
|
723
|
+
// the same identity appears more than once; it is only a tie-breaker —
|
|
724
|
+
// a stale index falls back to the first identity match. An id that no
|
|
725
|
+
// longer appears on the page at all aborts the expansion — the caller
|
|
726
|
+
// falls back to the stored single track (matching Android); silently
|
|
727
|
+
// queueing the changed list would resume the wrong station.
|
|
728
|
+
let tappedIndex = BrowserPathHelper.extractIndex(path)
|
|
729
|
+
guard let scoped = SectionScope.scoped(in: sections, containing: trackId, tappedIndex: tappedIndex) else {
|
|
753
730
|
return nil
|
|
754
731
|
}
|
|
732
|
+
let sectionTracks = scoped.section.children
|
|
733
|
+
let tappedOffset = scoped.tappedOffset
|
|
755
734
|
|
|
756
|
-
// Filter to playable tracks (have src)
|
|
757
|
-
|
|
735
|
+
// Filter to playable tracks (have src). A disabled track is unavailable —
|
|
736
|
+
// queue expansion excludes it, so auto-advance never meets one.
|
|
737
|
+
let playableTracks = sectionTracks.filter { $0.src != nil && $0.disabled != true }
|
|
758
738
|
guard !playableTracks.isEmpty else { return nil }
|
|
759
739
|
|
|
760
740
|
logger.debug("Found \(playableTracks.count) playable tracks")
|
|
@@ -762,10 +742,18 @@ final class BrowserManager {
|
|
|
762
742
|
logger.debug(" [\(index)] \(track.title) - src: \(track.src ?? "nil")")
|
|
763
743
|
}
|
|
764
744
|
|
|
765
|
-
// Find selected track index
|
|
766
|
-
//
|
|
767
|
-
//
|
|
768
|
-
|
|
745
|
+
// Find selected track index: the pinned copy when the stamp survived,
|
|
746
|
+
// else the first identity match. A miss must abort rather than silently
|
|
747
|
+
// start the queue at the wrong track (unreachable in practice — the
|
|
748
|
+
// section was found BY this id).
|
|
749
|
+
let selectedIndex: Int
|
|
750
|
+
if let tappedOffset, sectionTracks[tappedOffset].src != nil, sectionTracks[tappedOffset].disabled != true {
|
|
751
|
+
selectedIndex =
|
|
752
|
+
sectionTracks.prefix(through: tappedOffset)
|
|
753
|
+
.count(where: { $0.src != nil && $0.disabled != true }) - 1
|
|
754
|
+
} else if let index = playableTracks.firstIndex(where: { $0.identity == trackId }) {
|
|
755
|
+
selectedIndex = index
|
|
756
|
+
} else {
|
|
769
757
|
return nil
|
|
770
758
|
}
|
|
771
759
|
logger.debug("Selected track index: \(selectedIndex)")
|