react-native-audio-browser 0.2.0-next.11 → 0.2.0-next.13
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 +14 -4
- package/android/build.gradle +20 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +115 -26
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +262 -188
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +95 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +19 -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/model/PlayerUpdateOptions.kt +1 -1
- 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 +109 -74
- package/android/src/main/java/com/audiobrowser/player/Player.kt +51 -30
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- 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 +108 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +52 -53
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +74 -41
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +229 -50
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -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/player/ShuffleOrderLedByTest.kt +79 -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 +229 -180
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +258 -106
- package/ios/Browser/SearchDrillIn.swift +6 -6
- package/ios/Browser/SectionPresentation.swift +64 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +58 -0
- package/ios/Browser/TabBarEntries.swift +9 -3
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +227 -85
- package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
- package/ios/CarPlay/CarPlayListItemFactory.swift +289 -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/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +166 -45
- package/ios/Model/NitroTypeStubs.swift +106 -21
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Player/MediaIntentCriteria.swift +13 -5
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +54 -6
- package/ios/Player/PlaybackCoordinator.swift +13 -6
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +36 -10
- package/ios/Player/RetryManager.swift +4 -3
- package/ios/Player/ShuffleOrder.swift +23 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
- package/ios/Tests/JsonModelsTests.swift +150 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/QueueManagerTests.swift +72 -6
- package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +90 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +127 -0
- package/ios/Tests/Support/TestTypes.swift +7 -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/TrackPlayer.swift +57 -34
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/SFSymbolRenderer.swift +24 -2
- package/ios/Util/SVGProcessor.swift +9 -2
- 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/progress.js +2 -1
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/rate.js +8 -1
- package/lib/commonjs/features/playback/rate.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/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.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/progress.js +2 -1
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/rate.js +8 -1
- package/lib/module/features/playback/rate.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/TrackPlayer/QueueManager.js +14 -0
- package/lib/module/web/TrackPlayer/QueueManager.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/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/rate.d.ts +8 -1
- package/lib/typescript/src/features/playback/rate.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 +361 -126
- 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 +8 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -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 +16 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
- package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +33 -14
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +22 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +10 -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 +8 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +55 -51
- package/nitrogen/generated/android/c++/JSection.hpp +120 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +23 -48
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -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/CardImage.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- 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/{FavoriteConfig.kt → Section.kt} +18 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
- 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 +35 -5
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +158 -82
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +25 -10
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +25 -10
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- 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 +133 -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 +86 -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++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- 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 +126 -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 +73 -34
- package/package.json +7 -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/progress.ts +2 -1
- package/src/features/playback/rate.ts +8 -1
- package/src/features/playback/state.ts +2 -3
- package/src/features/playback/trackPlaybackTime.ts +1 -1
- package/src/features/player/options.ts +27 -19
- package/src/features/player/setup.test.ts +12 -3
- package/src/features/player/setup.ts +3 -1
- package/src/features/player/validateOptions.ts +2 -0
- package/src/features/queue/activeTrack.ts +14 -3
- package/src/features/queue/queue.ts +11 -4
- package/src/nitro-stub-drift.test.ts +40 -0
- package/src/public-surface.test.ts +40 -0
- package/src/specs/audio-browser.nitro.ts +4 -3
- package/src/style-resolution.test.ts +210 -0
- package/src/types/browser-native.ts +2 -2
- package/src/types/browser-nodes.ts +377 -135
- 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/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- 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/website/guide/android-auto.md +114 -0
- package/website/guide/android-certificates.md +409 -0
- package/website/guide/artwork.md +255 -0
- package/website/guide/audio-output.md +135 -0
- package/website/guide/automotive.md +86 -0
- package/website/guide/basic-usage.md +151 -0
- package/website/guide/battery.md +115 -0
- package/website/guide/browser.md +581 -0
- package/website/guide/carplay.md +620 -0
- package/website/guide/configuration.md +173 -0
- package/website/guide/equalizer.md +231 -0
- package/website/guide/errors.md +310 -0
- package/website/guide/favorites.md +154 -0
- package/website/guide/gate.md +143 -0
- package/website/guide/getting-started.md +46 -0
- package/website/guide/hooks.md +238 -0
- package/website/guide/live-streams.md +125 -0
- package/website/guide/metadata.md +239 -0
- package/website/guide/migrating-from-track-player.md +244 -0
- package/website/guide/native-callback-fetch.md +127 -0
- package/website/guide/network.md +86 -0
- package/website/guide/now-playing.md +175 -0
- package/website/guide/playback.md +328 -0
- package/website/guide/queue.md +300 -0
- package/website/guide/remote-controls.md +256 -0
- package/website/guide/search.md +255 -0
- package/website/guide/sleep-timer.md +268 -0
- package/website/guide/track.md +246 -0
- 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
|
@@ -2,18 +2,26 @@ import type { NavigationErrorType } from '../../features'
|
|
|
2
2
|
import type {
|
|
3
3
|
Track,
|
|
4
4
|
ResolvedTrack,
|
|
5
|
+
Section,
|
|
5
6
|
TransformableRequestConfig
|
|
6
7
|
} from '../../types'
|
|
7
8
|
import type { NativeBrowserConfiguration } from '../../types/browser-native'
|
|
8
9
|
import type { HttpClient } from '../http/HttpClient'
|
|
9
10
|
import type { FavoriteManager } from './FavoriteManager'
|
|
10
11
|
import type { NavigationErrorManager } from './NavigationErrorManager'
|
|
12
|
+
import { getTrackIdentity } from '../../utils/getTrackIdentity'
|
|
11
13
|
import { assertedNotNullish } from '../../utils/validation'
|
|
12
14
|
import { RequestConfigBuilder } from '../http/RequestConfigBuilder'
|
|
13
15
|
import { SimpleRouter } from '../SimpleRouter'
|
|
14
16
|
import { BrowserPathHelper } from '../util/BrowserPathHelper'
|
|
15
17
|
import { assertBrowsePageShape } from './assertBrowsePageShape'
|
|
16
18
|
import { parseSearchResponse } from './parseSearchResponse'
|
|
19
|
+
import {
|
|
20
|
+
flattenSections,
|
|
21
|
+
flattenedChildren,
|
|
22
|
+
normalizePage,
|
|
23
|
+
sectionContaining
|
|
24
|
+
} from './sections'
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
27
|
* Manages browser navigation, route resolution, and content loading.
|
|
@@ -158,14 +166,13 @@ export class BrowserManager {
|
|
|
158
166
|
// Determine initial path
|
|
159
167
|
let initialPath = value.path
|
|
160
168
|
|
|
161
|
-
// If no path specified, try to get first tab
|
|
169
|
+
// If no path specified, try to get first tab path
|
|
162
170
|
if (!initialPath) {
|
|
163
171
|
const tabsRoute = value.routes?.find((r) => r.path === '__tabs__')
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
172
|
+
const staticTabs = tabsRoute?.browseStatic
|
|
173
|
+
? flattenedChildren(tabsRoute.browseStatic)
|
|
174
|
+
: []
|
|
175
|
+
initialPath = staticTabs[0]?.path ?? '/'
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
if (initialPath) {
|
|
@@ -182,16 +189,16 @@ export class BrowserManager {
|
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
/**
|
|
185
|
-
* Navigates to a track's
|
|
192
|
+
* Navigates to a track's path.
|
|
186
193
|
*/
|
|
187
194
|
async navigateTrack(track: Track): Promise<void> {
|
|
188
195
|
this.navigationErrorManager.clearNavigationError()
|
|
189
|
-
const
|
|
190
|
-
if (!
|
|
191
|
-
console.warn('Track has no
|
|
196
|
+
const path = track.path
|
|
197
|
+
if (!path) {
|
|
198
|
+
console.warn('Track has no path to navigate to')
|
|
192
199
|
return
|
|
193
200
|
}
|
|
194
|
-
await this.navigate(
|
|
201
|
+
await this.navigate(path)
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
/**
|
|
@@ -247,23 +254,15 @@ export class BrowserManager {
|
|
|
247
254
|
return
|
|
248
255
|
}
|
|
249
256
|
|
|
250
|
-
//
|
|
251
|
-
// search
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
|
|
257
|
+
// Resolution returns canonical sectioned pages; add contextual paths
|
|
258
|
+
// to non-search content (matches Android behavior where search()
|
|
259
|
+
// bypasses the resolve() contextual-path logic). Copies avoid mutating
|
|
260
|
+
// the original config object (e.g., browseStatic from routes), which
|
|
261
|
+
// would break search that reads from the same source.
|
|
262
|
+
if (content?.sections && !isSearchPath) {
|
|
255
263
|
content = {
|
|
256
264
|
...content,
|
|
257
|
-
|
|
258
|
-
// If track has src, always add/update contextual URL with current path context
|
|
259
|
-
// This matches Android's BrowserManager.kt:436-441
|
|
260
|
-
if (track.src) {
|
|
261
|
-
const contextualUrl = `${path}?__trackId=${encodeURIComponent(track.src)}`
|
|
262
|
-
return { ...track, url: contextualUrl }
|
|
263
|
-
}
|
|
264
|
-
// Return a shallow copy in attempt to avoid mutating original config objects
|
|
265
|
-
return { ...track }
|
|
266
|
-
})
|
|
265
|
+
sections: this.addContextualPaths(path, content.sections)
|
|
267
266
|
}
|
|
268
267
|
}
|
|
269
268
|
|
|
@@ -314,13 +313,40 @@ export class BrowserManager {
|
|
|
314
313
|
}
|
|
315
314
|
}
|
|
316
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Stamps contextual paths onto a page's sections. A playable track gets a
|
|
318
|
+
* contextual path carrying its identity (id ?? src) so the queue can be
|
|
319
|
+
* re-expanded from it later, plus its flat page position (children
|
|
320
|
+
* concatenated in section order — ADR 0009/0010) as a duplicate-identity
|
|
321
|
+
* tie-breaker. Non-playable tracks are shallow-copied to avoid mutating
|
|
322
|
+
* original config objects (e.g., browseStatic from routes).
|
|
323
|
+
*/
|
|
324
|
+
private addContextualPaths(path: string, sections: Section[]): Section[] {
|
|
325
|
+
let index = 0
|
|
326
|
+
return sections.map((section) => ({
|
|
327
|
+
...section,
|
|
328
|
+
children: section.children.map((track) => {
|
|
329
|
+
const flatIndex = index++
|
|
330
|
+
const identity = getTrackIdentity(track)
|
|
331
|
+
if (track.src && identity) {
|
|
332
|
+
return {
|
|
333
|
+
...track,
|
|
334
|
+
path: BrowserPathHelper.build(path, identity, flatIndex)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return { ...track }
|
|
338
|
+
})
|
|
339
|
+
}))
|
|
340
|
+
}
|
|
341
|
+
|
|
317
342
|
/**
|
|
318
343
|
* Resolves search content from a search path.
|
|
319
|
-
* Returns a
|
|
344
|
+
* Returns a canonical sectioned page — one untitled section of raw search
|
|
345
|
+
* results (no contextual URLs), like the native `makeSearchResult`.
|
|
320
346
|
* Matches Android's BrowserManager.search() which bypasses resolve() entirely.
|
|
321
347
|
*
|
|
322
348
|
* @param searchPath The search path (format: /__search?q=query)
|
|
323
|
-
* @returns ResolvedTrack containing search results as
|
|
349
|
+
* @returns ResolvedTrack containing search results as one untitled section
|
|
324
350
|
*/
|
|
325
351
|
private async resolveSearchContent(
|
|
326
352
|
searchPath: string
|
|
@@ -366,13 +392,12 @@ export class BrowserManager {
|
|
|
366
392
|
}
|
|
367
393
|
}
|
|
368
394
|
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
url: searchPath,
|
|
395
|
+
// Search callbacks should return fresh tracks without contextual paths (like Android)
|
|
396
|
+
return normalizePage({
|
|
397
|
+
path: searchPath,
|
|
373
398
|
title: `Search: ${query}`,
|
|
374
399
|
children: searchResults
|
|
375
|
-
}
|
|
400
|
+
})
|
|
376
401
|
}
|
|
377
402
|
|
|
378
403
|
/**
|
|
@@ -400,35 +425,40 @@ export class BrowserManager {
|
|
|
400
425
|
artworkConfig
|
|
401
426
|
)
|
|
402
427
|
|
|
403
|
-
// Transform children artwork
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
428
|
+
// Transform children artwork, section by section
|
|
429
|
+
const transformTrack = async (track: Track): Promise<Track> => {
|
|
430
|
+
const artworkSource =
|
|
431
|
+
await RequestConfigBuilder.resolveArtworkSourceAsync(
|
|
432
|
+
track,
|
|
433
|
+
requestConfig,
|
|
434
|
+
artworkConfig
|
|
435
|
+
)
|
|
436
|
+
if (artworkSource && !track.artworkSource) {
|
|
437
|
+
return { ...track, artworkSource }
|
|
438
|
+
}
|
|
439
|
+
return track
|
|
440
|
+
}
|
|
441
|
+
let transformedSections: Section[] | undefined
|
|
442
|
+
if (content.sections) {
|
|
443
|
+
transformedSections = await Promise.all(
|
|
444
|
+
content.sections.map(async (section) => ({
|
|
445
|
+
...section,
|
|
446
|
+
children: await Promise.all(section.children.map(transformTrack))
|
|
447
|
+
}))
|
|
419
448
|
)
|
|
420
449
|
}
|
|
421
450
|
|
|
422
451
|
return {
|
|
423
452
|
...content,
|
|
424
453
|
artworkSource: parentArtworkSource ?? content.artworkSource,
|
|
425
|
-
|
|
454
|
+
sections: transformedSections ?? content.sections
|
|
426
455
|
}
|
|
427
456
|
}
|
|
428
457
|
|
|
429
458
|
/**
|
|
430
459
|
* Resolves content from a route using browseCallback, browseStatic, or browseConfig.
|
|
431
|
-
* Single source of truth for route resolution logic.
|
|
460
|
+
* Single source of truth for route resolution logic. Returns canonical
|
|
461
|
+
* sectioned pages (normalizePage) — consumers never see authored `children`.
|
|
432
462
|
*
|
|
433
463
|
* @param route The route configuration to resolve
|
|
434
464
|
* @param path The path being navigated to
|
|
@@ -470,23 +500,26 @@ export class BrowserManager {
|
|
|
470
500
|
)
|
|
471
501
|
return undefined
|
|
472
502
|
}
|
|
473
|
-
return result
|
|
503
|
+
return normalizePage(result)
|
|
474
504
|
}
|
|
475
505
|
|
|
476
|
-
// Handle static ResolvedTrack route
|
|
506
|
+
// Handle static ResolvedTrack route (normalizePage also copies, so the
|
|
507
|
+
// config object itself is never handed out)
|
|
477
508
|
if (route.browseStatic) {
|
|
478
|
-
return route.browseStatic
|
|
509
|
+
return normalizePage(route.browseStatic)
|
|
479
510
|
}
|
|
480
511
|
|
|
481
512
|
// Handle request config-based route via the layered request → browse → route
|
|
482
513
|
// chain (matches native's resolveRouteEntry → resolveFromConfig).
|
|
483
514
|
if (route.browseConfig) {
|
|
484
515
|
try {
|
|
485
|
-
return
|
|
486
|
-
this.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
516
|
+
return normalizePage(
|
|
517
|
+
await this.resolveFromConfig(
|
|
518
|
+
this._configuration.browse,
|
|
519
|
+
route.browseConfig,
|
|
520
|
+
path,
|
|
521
|
+
routeParams
|
|
522
|
+
)
|
|
490
523
|
)
|
|
491
524
|
} catch (error: unknown) {
|
|
492
525
|
console.error(`Failed to resolve ${errorContext}:`, error)
|
|
@@ -592,6 +625,8 @@ export class BrowserManager {
|
|
|
592
625
|
|
|
593
626
|
/**
|
|
594
627
|
* Resolves content for a specific path using configured routes.
|
|
628
|
+
* Like the native resolve pipelines, the returned page is canonical
|
|
629
|
+
* sectioned shape — `children` sugar is normalized away at this seam.
|
|
595
630
|
*/
|
|
596
631
|
private async resolveContent(
|
|
597
632
|
path: string
|
|
@@ -658,11 +693,13 @@ export class BrowserManager {
|
|
|
658
693
|
this._configuration.browseResolver
|
|
659
694
|
) {
|
|
660
695
|
try {
|
|
661
|
-
return
|
|
662
|
-
this.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
696
|
+
return normalizePage(
|
|
697
|
+
await this.resolveFromConfig(
|
|
698
|
+
this._configuration.browse,
|
|
699
|
+
undefined,
|
|
700
|
+
path,
|
|
701
|
+
{ path }
|
|
702
|
+
)
|
|
666
703
|
)
|
|
667
704
|
} catch (error: unknown) {
|
|
668
705
|
console.error('Failed to resolve default browse content:', error)
|
|
@@ -686,7 +723,8 @@ export class BrowserManager {
|
|
|
686
723
|
}
|
|
687
724
|
|
|
688
725
|
const result = await this.resolveRouteContent(tabsRoute, '/', {}, 'Tabs')
|
|
689
|
-
|
|
726
|
+
// Tabs are a flat list, not a page — a sectioned tabs source flattens.
|
|
727
|
+
const tabs = result ? flattenSections(result.sections) : []
|
|
690
728
|
|
|
691
729
|
// Transform artwork URLs on tabs via the async resolver so the shared
|
|
692
730
|
// request layer (incl. its transform) applies, matching content/search.
|
|
@@ -724,44 +762,79 @@ export class BrowserManager {
|
|
|
724
762
|
/**
|
|
725
763
|
* Expands a queue from a contextual URL.
|
|
726
764
|
* Resolves the parent container to get all siblings.
|
|
727
|
-
* Matches Android's BrowserManager.
|
|
765
|
+
* Matches Android's BrowserManager.expandQueueFromContextualPath() behavior.
|
|
728
766
|
*
|
|
729
|
-
* @param
|
|
767
|
+
* @param contextualPath The contextual path (format: /path?__trackId=trackSrc)
|
|
730
768
|
* @returns Object with tracks and selectedIndex, or undefined if expansion fails
|
|
731
769
|
*/
|
|
732
|
-
async
|
|
733
|
-
|
|
770
|
+
async expandQueueFromContextualPath(
|
|
771
|
+
contextualPath: string
|
|
734
772
|
): Promise<{ tracks: Track[]; selectedIndex: number } | undefined> {
|
|
735
|
-
const trackId = BrowserPathHelper.extractTrackId(
|
|
773
|
+
const trackId = BrowserPathHelper.extractTrackId(contextualPath)
|
|
736
774
|
if (!trackId) return undefined
|
|
737
775
|
|
|
738
776
|
try {
|
|
739
777
|
// Resolve the parent container to get all siblings
|
|
740
|
-
const parentPath = BrowserPathHelper.stripTrackId(
|
|
778
|
+
const parentPath = BrowserPathHelper.stripTrackId(contextualPath)
|
|
741
779
|
const parentResolvedTrack = await this.resolveContent(parentPath)
|
|
742
|
-
const
|
|
780
|
+
const rawSections = parentResolvedTrack?.sections
|
|
743
781
|
|
|
744
|
-
if (!
|
|
782
|
+
if (!rawSections || rawSections.every((s) => s.children.length === 0)) {
|
|
745
783
|
console.warn('Parent has no children, cannot expand queue')
|
|
746
784
|
return undefined
|
|
747
785
|
}
|
|
748
786
|
|
|
749
|
-
//
|
|
750
|
-
|
|
787
|
+
// Stamp contextual paths so queued tracks carry their own queue context
|
|
788
|
+
// (navigate() stamps the displayed page the same way).
|
|
789
|
+
const sections = this.addContextualPaths(parentPath, rawSections)
|
|
790
|
+
|
|
791
|
+
// Queue scope is the tapped section, not the whole page (ADR 0006).
|
|
792
|
+
// The stamped flat index pins which section (and which copy) was
|
|
793
|
+
// tapped when the same identity appears more than once; a stale index
|
|
794
|
+
// falls back to the first identity match. An identity that no longer
|
|
795
|
+
// appears on the page aborts the expansion — the caller falls back to
|
|
796
|
+
// the single track.
|
|
797
|
+
const tappedIndex = BrowserPathHelper.extractIndex(contextualPath)
|
|
798
|
+
const scoped = sectionContaining(sections, trackId, tappedIndex)
|
|
799
|
+
if (!scoped) {
|
|
800
|
+
console.warn(
|
|
801
|
+
`Track with identity='${trackId}' not found on parent page`
|
|
802
|
+
)
|
|
803
|
+
return undefined
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Filter to only playable tracks (tracks with src). A disabled track is
|
|
807
|
+
// unavailable — queue expansion excludes it, so auto-advance never
|
|
808
|
+
// meets one (Track.disabled).
|
|
809
|
+
const isQueueable = (track: Track) =>
|
|
810
|
+
track.src != null && track.disabled !== true
|
|
811
|
+
const playableTracks = scoped.tracks.filter(isQueueable)
|
|
751
812
|
|
|
752
813
|
if (playableTracks.length === 0) {
|
|
753
|
-
console.warn('
|
|
814
|
+
console.warn('Section has no playable tracks, cannot expand queue')
|
|
754
815
|
return undefined
|
|
755
816
|
}
|
|
756
817
|
|
|
757
|
-
//
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
)
|
|
818
|
+
// Selected position: the pinned copy when the stamp survived, else
|
|
819
|
+
// the first identity match.
|
|
820
|
+
let selectedIndex = -1
|
|
821
|
+
if (scoped.tappedOffset !== undefined) {
|
|
822
|
+
const tapped = scoped.tracks[scoped.tappedOffset]
|
|
823
|
+
if (tapped != null && isQueueable(tapped)) {
|
|
824
|
+
selectedIndex =
|
|
825
|
+
scoped.tracks.slice(0, scoped.tappedOffset + 1).filter(isQueueable)
|
|
826
|
+
.length - 1
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (selectedIndex < 0) {
|
|
830
|
+
selectedIndex = playableTracks.findIndex(
|
|
831
|
+
(track) => getTrackIdentity(track) === trackId
|
|
832
|
+
)
|
|
833
|
+
}
|
|
761
834
|
|
|
762
835
|
if (selectedIndex < 0) {
|
|
763
836
|
console.warn(
|
|
764
|
-
`Track with
|
|
837
|
+
`Track with identity='${trackId}' not found in playable children`
|
|
765
838
|
)
|
|
766
839
|
return undefined
|
|
767
840
|
}
|
|
@@ -776,7 +849,7 @@ export class BrowserManager {
|
|
|
776
849
|
return { tracks: playableTracks, selectedIndex }
|
|
777
850
|
} catch (error) {
|
|
778
851
|
console.error(
|
|
779
|
-
`Error expanding queue from contextual
|
|
852
|
+
`Error expanding queue from contextual path: ${contextualPath}`,
|
|
780
853
|
error
|
|
781
854
|
)
|
|
782
855
|
return undefined
|
|
@@ -800,23 +873,31 @@ export class BrowserManager {
|
|
|
800
873
|
startPositionMs: number,
|
|
801
874
|
searchQuery?: string
|
|
802
875
|
): Promise<{ tracks: Track[]; startIndex: number; startPositionMs: number }> {
|
|
803
|
-
// Single track: check for search context or contextual
|
|
876
|
+
// Single track: check for search context or contextual path
|
|
804
877
|
if (tracks.length === 1) {
|
|
805
878
|
const track = assertedNotNullish(tracks[0])
|
|
806
|
-
const
|
|
879
|
+
const trackPath = track.path
|
|
807
880
|
|
|
808
881
|
// If search query present, expand search results
|
|
809
882
|
if (searchQuery) {
|
|
810
|
-
// Execute search (will hit cache if already performed)
|
|
883
|
+
// Execute search (will hit cache if already performed). Disabled
|
|
884
|
+
// tracks never play, so they never enter a search-built queue
|
|
885
|
+
// (Track.disabled — mirrors both native platforms).
|
|
811
886
|
const searchResults = await this.resolveSearchContent(
|
|
812
887
|
BrowserPathHelper.createSearchPath(searchQuery)
|
|
813
888
|
)
|
|
814
|
-
const searchTracks = searchResults?.
|
|
889
|
+
const searchTracks = flattenSections(searchResults?.sections).filter(
|
|
890
|
+
(t) => t.disabled !== true
|
|
891
|
+
)
|
|
815
892
|
|
|
816
|
-
if (searchTracks
|
|
817
|
-
// Find the selected track in search results
|
|
893
|
+
if (searchTracks.length > 0) {
|
|
894
|
+
// Find the selected track in search results by path or identity
|
|
895
|
+
// (matches Android's three-way mediaId match)
|
|
896
|
+
const identity = getTrackIdentity(track)
|
|
818
897
|
const selectedIdx = searchTracks.findIndex(
|
|
819
|
-
(t) =>
|
|
898
|
+
(t) =>
|
|
899
|
+
t.path === trackPath ||
|
|
900
|
+
(identity !== undefined && getTrackIdentity(t) === identity)
|
|
820
901
|
)
|
|
821
902
|
|
|
822
903
|
if (selectedIdx >= 0) {
|
|
@@ -829,14 +910,14 @@ export class BrowserManager {
|
|
|
829
910
|
}
|
|
830
911
|
}
|
|
831
912
|
|
|
832
|
-
// Check if contextual
|
|
913
|
+
// Check if contextual path - expand from parent
|
|
833
914
|
// Note: The queueSourcePath optimization is handled by the caller (NativeAudioBrowser.navigateTrackAsync)
|
|
834
915
|
// which has access to the existing queue and can skip to the track directly.
|
|
835
916
|
// This matches Android where MediaSessionCallback handles the optimization before calling
|
|
836
917
|
// BrowserManager.resolveMediaItemsForPlayback.
|
|
837
|
-
if (
|
|
838
|
-
const parentPath = BrowserPathHelper.stripTrackId(
|
|
839
|
-
const expanded = await this.
|
|
918
|
+
if (trackPath && BrowserPathHelper.isContextual(trackPath)) {
|
|
919
|
+
const parentPath = BrowserPathHelper.stripTrackId(trackPath)
|
|
920
|
+
const expanded = await this.expandQueueFromContextualPath(trackPath)
|
|
840
921
|
|
|
841
922
|
if (expanded) {
|
|
842
923
|
// Store source path for optimization by caller on next invocation
|
|
@@ -855,13 +936,13 @@ export class BrowserManager {
|
|
|
855
936
|
}
|
|
856
937
|
|
|
857
938
|
/**
|
|
858
|
-
* Gets the default navigation path (first tab
|
|
939
|
+
* Gets the default navigation path (first tab path or '/').
|
|
859
940
|
*/
|
|
860
941
|
private getDefaultPath(): string {
|
|
861
942
|
// Try to get first tab as default path
|
|
862
943
|
if (this._tabs && this._tabs.length > 0) {
|
|
863
944
|
const firstTab = this._tabs[0]
|
|
864
|
-
return firstTab?.
|
|
945
|
+
return firstTab?.path ?? '/'
|
|
865
946
|
}
|
|
866
947
|
return '/'
|
|
867
948
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { Track } from '../../types'
|
|
3
|
+
import { getTrackIdentity } from '../../utils/getTrackIdentity'
|
|
4
|
+
import { FavoriteManager } from './FavoriteManager'
|
|
5
|
+
|
|
6
|
+
const withId: Track = {
|
|
7
|
+
id: 'abc123',
|
|
8
|
+
src: 'https://cdn.example.com/streams/abc123.mp3?token=x',
|
|
9
|
+
title: 'With id'
|
|
10
|
+
}
|
|
11
|
+
const srcOnly: Track = {
|
|
12
|
+
src: 'https://cdn.example.com/streams/plain.mp3',
|
|
13
|
+
title: 'Src only'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('getTrackIdentity', () => {
|
|
17
|
+
it('is the id when set', () => {
|
|
18
|
+
expect(getTrackIdentity(withId)).toBe('abc123')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('falls back to src when id is absent or blank', () => {
|
|
22
|
+
expect(getTrackIdentity(srcOnly)).toBe(srcOnly.src)
|
|
23
|
+
expect(getTrackIdentity({ id: '', src: 's' })).toBe('s')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('is undefined for a browsable-only track', () => {
|
|
27
|
+
expect(getTrackIdentity({ title: 'Folder' } as Track)).toBeUndefined()
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('FavoriteManager identity matching', () => {
|
|
32
|
+
it('matches a stored id against track.id, not src', () => {
|
|
33
|
+
const manager = new FavoriteManager()
|
|
34
|
+
manager.setFavorites(['abc123'])
|
|
35
|
+
expect(manager.hydrateFavorite(withId).favorited).toBe(true)
|
|
36
|
+
// The id does not substring-match into another track's src
|
|
37
|
+
expect(manager.hydrateFavorite(srcOnly).favorited).toBeUndefined()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('matches a stored src for id-less tracks', () => {
|
|
41
|
+
const manager = new FavoriteManager()
|
|
42
|
+
manager.setFavorites([srcOnly.src!])
|
|
43
|
+
expect(manager.hydrateFavorite(srcOnly).favorited).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('does not src-match a track whose id takes identity precedence', () => {
|
|
47
|
+
const manager = new FavoriteManager()
|
|
48
|
+
manager.setFavorites([withId.src!])
|
|
49
|
+
// identity of withId is its id, so a stored src must NOT match
|
|
50
|
+
expect(manager.hydrateFavorite(withId).favorited).toBeUndefined()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('never overwrites a caller-set favorited flag', () => {
|
|
54
|
+
const manager = new FavoriteManager()
|
|
55
|
+
manager.setFavorites(['abc123'])
|
|
56
|
+
const preset: Track = { ...withId, favorited: false }
|
|
57
|
+
expect(manager.hydrateFavorite(preset).favorited).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('add/remove key the cache by identity', () => {
|
|
61
|
+
const manager = new FavoriteManager()
|
|
62
|
+
manager.addFavorite(withId)
|
|
63
|
+
expect(manager.isFavorited(withId)).toBe(true)
|
|
64
|
+
// Same identity, different src (volatile query param) still matches
|
|
65
|
+
expect(
|
|
66
|
+
manager.isFavorited({ ...withId, src: 'https://other.example/x.mp3' })
|
|
67
|
+
).toBe(true)
|
|
68
|
+
manager.removeFavorite(withId)
|
|
69
|
+
expect(manager.isFavorited(withId)).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('hydrateChildren stamps favorited across a normalized page', () => {
|
|
73
|
+
const manager = new FavoriteManager()
|
|
74
|
+
manager.setFavorites(['abc123'])
|
|
75
|
+
const page = manager.hydrateChildren({
|
|
76
|
+
path: '/list',
|
|
77
|
+
title: 'List',
|
|
78
|
+
sections: [{ children: [withId, srcOnly] }]
|
|
79
|
+
})
|
|
80
|
+
const tracks = page.sections?.[0]?.children
|
|
81
|
+
expect(tracks?.[0]?.favorited).toBe(true)
|
|
82
|
+
expect(tracks?.[1]?.favorited).toBeUndefined()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { Track, ResolvedTrack } from '../../types'
|
|
2
|
+
import { getTrackIdentity } from '../../utils/getTrackIdentity'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* Manages favorite state for tracks
|
|
5
|
+
* Manages favorite state for tracks, keyed by track identity (`id` when set,
|
|
6
|
+
* falling back to `src` — see getTrackIdentity / ADR 0008).
|
|
5
7
|
* Matches Android's favorite hydration behavior.
|
|
6
8
|
*/
|
|
7
9
|
export class FavoriteManager {
|
|
8
10
|
private favorites = new Set<string>()
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* Sets the favorites cache from an array of track
|
|
13
|
+
* Sets the favorites cache from an array of track identities.
|
|
12
14
|
* This is typically called when favorites are loaded from storage.
|
|
13
15
|
*/
|
|
14
16
|
setFavorites(favorites: string[]): void {
|
|
@@ -16,24 +18,27 @@ export class FavoriteManager {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
|
-
* Checks if a track is favorited based on its
|
|
21
|
+
* Checks if a track is favorited based on its identity.
|
|
20
22
|
*/
|
|
21
|
-
isFavorited(
|
|
22
|
-
|
|
23
|
+
isFavorited(track: Track): boolean {
|
|
24
|
+
const identity = getTrackIdentity(track)
|
|
25
|
+
return identity !== undefined && this.favorites.has(identity)
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
|
-
* Adds a track
|
|
29
|
+
* Adds a track to the favorites cache under its identity.
|
|
27
30
|
*/
|
|
28
|
-
addFavorite(
|
|
29
|
-
|
|
31
|
+
addFavorite(track: Track): void {
|
|
32
|
+
const identity = getTrackIdentity(track)
|
|
33
|
+
if (identity !== undefined) this.favorites.add(identity)
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/**
|
|
33
|
-
* Removes a track
|
|
37
|
+
* Removes a track from the favorites cache under its identity.
|
|
34
38
|
*/
|
|
35
|
-
removeFavorite(
|
|
36
|
-
|
|
39
|
+
removeFavorite(track: Track): void {
|
|
40
|
+
const identity = getTrackIdentity(track)
|
|
41
|
+
if (identity !== undefined) this.favorites.delete(identity)
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/**
|
|
@@ -48,8 +53,7 @@ export class FavoriteManager {
|
|
|
48
53
|
if (track.favorited !== undefined && track.favorited !== null) return track
|
|
49
54
|
if (this.favorites.size === 0) return track
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
if (!isFavorited) return track
|
|
56
|
+
if (!this.isFavorited(track)) return track
|
|
53
57
|
|
|
54
58
|
return {
|
|
55
59
|
...track,
|
|
@@ -58,22 +62,23 @@ export class FavoriteManager {
|
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/**
|
|
61
|
-
* Hydrates favorites on all
|
|
62
|
-
* Matches Android's hydrateChildren behavior.
|
|
65
|
+
* Hydrates favorites on all tracks of a resolved page.
|
|
66
|
+
* Matches Android's hydrateChildren behavior. Pages reach hydration
|
|
67
|
+
* normalized (ADR 0010), so `sections` is the only structure to walk.
|
|
63
68
|
*
|
|
64
|
-
* @param resolvedTrack
|
|
65
|
-
* @returns
|
|
69
|
+
* @param resolvedTrack Resolved page to hydrate
|
|
70
|
+
* @returns Resolved page with favorited properties set on its tracks
|
|
66
71
|
*/
|
|
67
72
|
hydrateChildren(resolvedTrack: ResolvedTrack): ResolvedTrack {
|
|
68
|
-
const
|
|
69
|
-
if (!
|
|
73
|
+
const { sections } = resolvedTrack
|
|
74
|
+
if (!sections) return resolvedTrack
|
|
70
75
|
|
|
71
|
-
const hydratedChildren = children.map((track) =>
|
|
72
|
-
this.hydrateFavorite(track)
|
|
73
|
-
)
|
|
74
76
|
return {
|
|
75
77
|
...resolvedTrack,
|
|
76
|
-
|
|
78
|
+
sections: sections.map((section) => ({
|
|
79
|
+
...section,
|
|
80
|
+
children: section.children.map((track) => this.hydrateFavorite(track))
|
|
81
|
+
}))
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
}
|
|
@@ -3,7 +3,7 @@ import { assertBrowsePageShape } from './assertBrowsePageShape'
|
|
|
3
3
|
|
|
4
4
|
describe('assertBrowsePageShape', () => {
|
|
5
5
|
it('passes a page object through unchanged', () => {
|
|
6
|
-
const page = {
|
|
6
|
+
const page = { path: '/x', title: 'X', children: [] }
|
|
7
7
|
expect(assertBrowsePageShape(page, '/x')).toBe(page)
|
|
8
8
|
})
|
|
9
9
|
|