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
|
@@ -13,14 +13,16 @@ export function assertBrowsePageShape(
|
|
|
13
13
|
if (Array.isArray(response)) {
|
|
14
14
|
throw new Error(
|
|
15
15
|
`Browse endpoint for "${path}" returned a JSON array; expected a ` +
|
|
16
|
-
`page object { title, children?: Track[] }.
|
|
17
|
-
|
|
16
|
+
`page object { title, sections?: Section[], children?: Track[] }. ` +
|
|
17
|
+
`Wrap the rows in a \`children\` array (one untitled section) or ` +
|
|
18
|
+
`group them into \`sections\`.`
|
|
18
19
|
)
|
|
19
20
|
}
|
|
20
21
|
if (response === null || typeof response !== 'object') {
|
|
21
22
|
throw new Error(
|
|
22
23
|
`Browse endpoint for "${path}" returned ${JSON.stringify(response)}; ` +
|
|
23
|
-
`expected a page object { title,
|
|
24
|
+
`expected a page object { title, sections?: Section[], ` +
|
|
25
|
+
`children?: Track[] }.`
|
|
24
26
|
)
|
|
25
27
|
}
|
|
26
28
|
return response as ResolvedTrack
|
|
@@ -5,10 +5,10 @@ import { classifyTrackNavigation } from './classifyTrackNavigation'
|
|
|
5
5
|
const track = (fields: Partial<Track>): Track => fields as Track
|
|
6
6
|
|
|
7
7
|
describe('classifyTrackNavigation', () => {
|
|
8
|
-
it('classifies a contextual
|
|
8
|
+
it('classifies a contextual path, exposing parent path and track id', () => {
|
|
9
9
|
expect(
|
|
10
10
|
classifyTrackNavigation(
|
|
11
|
-
track({
|
|
11
|
+
track({ path: '/library/radio?__trackId=song.mp3' })
|
|
12
12
|
)
|
|
13
13
|
).toEqual({
|
|
14
14
|
kind: 'contextual',
|
|
@@ -17,25 +17,27 @@ describe('classifyTrackNavigation', () => {
|
|
|
17
17
|
})
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
it('classifies a non-contextual
|
|
21
|
-
expect(classifyTrackNavigation(track({
|
|
20
|
+
it('classifies a non-contextual path as a browse navigation', () => {
|
|
21
|
+
expect(classifyTrackNavigation(track({ path: '/library/radio' }))).toEqual({
|
|
22
22
|
kind: 'browse'
|
|
23
23
|
})
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
it('classifies a track with src but no
|
|
26
|
+
it('classifies a track with src but no path as playable', () => {
|
|
27
27
|
expect(classifyTrackNavigation(track({ src: 'song.mp3' }))).toEqual({
|
|
28
28
|
kind: 'playable'
|
|
29
29
|
})
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
it('prefers browse over playable when a
|
|
32
|
+
it('prefers browse over playable when a path is present', () => {
|
|
33
33
|
expect(
|
|
34
|
-
classifyTrackNavigation(
|
|
34
|
+
classifyTrackNavigation(
|
|
35
|
+
track({ path: '/library/radio', src: 'song.mp3' })
|
|
36
|
+
)
|
|
35
37
|
).toEqual({ kind: 'browse' })
|
|
36
38
|
})
|
|
37
39
|
|
|
38
|
-
it('classifies a track with neither
|
|
40
|
+
it('classifies a track with neither path nor src as invalid', () => {
|
|
39
41
|
expect(classifyTrackNavigation(track({}))).toEqual({ kind: 'invalid' })
|
|
40
42
|
})
|
|
41
43
|
})
|
|
@@ -4,10 +4,10 @@ import { BrowserPathHelper } from '../util/BrowserPathHelper'
|
|
|
4
4
|
/**
|
|
5
5
|
* How a selected track should be navigated, decided purely from its shape:
|
|
6
6
|
* - `contextual` — a playable track carrying its parent queue context in a
|
|
7
|
-
* contextual
|
|
8
|
-
* - `browse` — a browsable track (has a `
|
|
9
|
-
* - `playable` — a bare playable track (`src`, no `
|
|
10
|
-
* - `invalid` — neither `
|
|
7
|
+
* contextual path (`{parentPath}?__trackId={src}`); expand/skip within that queue.
|
|
8
|
+
* - `browse` — a browsable track (has a `path` that isn't contextual); drill in.
|
|
9
|
+
* - `playable` — a bare playable track (`src`, no `path`); load and play it alone.
|
|
10
|
+
* - `invalid` — neither `path` nor `src`; nothing to do.
|
|
11
11
|
*/
|
|
12
12
|
export type TrackNavigation =
|
|
13
13
|
| { kind: 'contextual'; parentPath: string; trackId: string | undefined }
|
|
@@ -21,15 +21,15 @@ export type TrackNavigation =
|
|
|
21
21
|
* caller; this only decides which action applies.
|
|
22
22
|
*/
|
|
23
23
|
export function classifyTrackNavigation(track: Track): TrackNavigation {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
24
|
+
const path = track.path
|
|
25
|
+
if (path && BrowserPathHelper.isContextual(path)) {
|
|
26
26
|
return {
|
|
27
27
|
kind: 'contextual',
|
|
28
|
-
parentPath: BrowserPathHelper.stripTrackId(
|
|
29
|
-
trackId: BrowserPathHelper.extractTrackId(
|
|
28
|
+
parentPath: BrowserPathHelper.stripTrackId(path),
|
|
29
|
+
trackId: BrowserPathHelper.extractTrackId(path)
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
if (
|
|
32
|
+
if (path) return { kind: 'browse' }
|
|
33
33
|
if (track.src) return { kind: 'playable' }
|
|
34
34
|
return { kind: 'invalid' }
|
|
35
35
|
}
|
|
@@ -5,7 +5,11 @@ describe('parseSearchResponse', () => {
|
|
|
5
5
|
it('extracts children from a page-object response', () => {
|
|
6
6
|
const tracks = [{ src: '/a', title: 'A' }]
|
|
7
7
|
expect(
|
|
8
|
-
parseSearchResponse({
|
|
8
|
+
parseSearchResponse({
|
|
9
|
+
path: '/search',
|
|
10
|
+
title: 'Search',
|
|
11
|
+
children: tracks
|
|
12
|
+
})
|
|
9
13
|
).toEqual(tracks)
|
|
10
14
|
})
|
|
11
15
|
|
|
@@ -15,7 +19,9 @@ describe('parseSearchResponse', () => {
|
|
|
15
19
|
})
|
|
16
20
|
|
|
17
21
|
it('returns [] for a page object with no children', () => {
|
|
18
|
-
expect(parseSearchResponse({
|
|
22
|
+
expect(parseSearchResponse({ path: '/search', title: 'Search' })).toEqual(
|
|
23
|
+
[]
|
|
24
|
+
)
|
|
19
25
|
})
|
|
20
26
|
|
|
21
27
|
it('returns [] for null / non-object responses', () => {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import type { ResolvedTrack, Track } from '../../types'
|
|
3
|
+
import { flattenSections, normalizePage, sectionContaining } from './sections'
|
|
4
|
+
|
|
5
|
+
const track = (src: string): Track => ({ title: src, src })
|
|
6
|
+
|
|
7
|
+
describe('normalizePage', () => {
|
|
8
|
+
it('wraps plain children into one untitled section', () => {
|
|
9
|
+
const page: ResolvedTrack = {
|
|
10
|
+
path: '/p',
|
|
11
|
+
title: 'P',
|
|
12
|
+
children: [track('a'), track('b')]
|
|
13
|
+
}
|
|
14
|
+
const normalized = normalizePage(page)
|
|
15
|
+
expect(normalized.sections).toEqual([
|
|
16
|
+
{ children: [track('a'), track('b')] }
|
|
17
|
+
])
|
|
18
|
+
expect(normalized.children).toBeUndefined()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('prefers sections when present and strips children', () => {
|
|
22
|
+
const page: ResolvedTrack = {
|
|
23
|
+
path: '/p',
|
|
24
|
+
title: 'P',
|
|
25
|
+
sections: [{ title: 'S', children: [track('a')] }],
|
|
26
|
+
children: [track('zzz')]
|
|
27
|
+
}
|
|
28
|
+
const normalized = normalizePage(page)
|
|
29
|
+
expect(normalized.sections).toEqual([
|
|
30
|
+
{ title: 'S', children: [track('a')] }
|
|
31
|
+
])
|
|
32
|
+
expect(normalized.children).toBeUndefined()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('leaves a childless page without sections', () => {
|
|
36
|
+
expect(normalizePage({ path: '/p', title: 'P' }).sections).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('flattenSections', () => {
|
|
41
|
+
it('concatenates children in section order', () => {
|
|
42
|
+
const flat = flattenSections([
|
|
43
|
+
{ title: 'A', children: [track('a1'), track('a2')] },
|
|
44
|
+
{ children: [track('b1')] }
|
|
45
|
+
])
|
|
46
|
+
expect(flat.map((t) => t.src)).toEqual(['a1', 'a2', 'b1'])
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('sectionContaining', () => {
|
|
51
|
+
const sections = [
|
|
52
|
+
{ title: 'First', children: [track('dup'), track('x')] },
|
|
53
|
+
{ title: 'Second', children: [track('y'), track('dup'), track('z')] }
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
it('falls back to the first section containing the identity', () => {
|
|
57
|
+
const scoped = sectionContaining(sections, 'dup', undefined)
|
|
58
|
+
expect(scoped?.tracks.map((t) => t.src)).toEqual(['dup', 'x'])
|
|
59
|
+
expect(scoped?.tappedOffset).toBeUndefined()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('pins the tapped section via the flat index', () => {
|
|
63
|
+
// Flat index 3 = second section, offset 1.
|
|
64
|
+
const scoped = sectionContaining(sections, 'dup', 3)
|
|
65
|
+
expect(scoped?.tracks.map((t) => t.src)).toEqual(['y', 'dup', 'z'])
|
|
66
|
+
expect(scoped?.tappedOffset).toBe(1)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('ignores a stale index whose child no longer matches', () => {
|
|
70
|
+
const scoped = sectionContaining(sections, 'dup', 1)
|
|
71
|
+
expect(scoped?.tracks.map((t) => t.src)).toEqual(['dup', 'x'])
|
|
72
|
+
expect(scoped?.tappedOffset).toBeUndefined()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('ignores an out-of-range index', () => {
|
|
76
|
+
const scoped = sectionContaining(sections, 'dup', 99)
|
|
77
|
+
expect(scoped?.tracks.map((t) => t.src)).toEqual(['dup', 'x'])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('returns undefined for a vanished identity', () => {
|
|
81
|
+
expect(sectionContaining(sections, 'gone', 0)).toBeUndefined()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('pins the exact copy of a within-section duplicate', () => {
|
|
85
|
+
const playlist = [
|
|
86
|
+
{ children: [track('a'), track('b'), track('a'), track('c')] }
|
|
87
|
+
]
|
|
88
|
+
const scoped = sectionContaining(playlist, 'a', 2)
|
|
89
|
+
expect(scoped?.tappedOffset).toBe(2)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ResolvedTrack, Section, Track } from '../../types'
|
|
2
|
+
import { getTrackIdentity } from '../../utils/getTrackIdentity'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normalizes a resolved page to the canonical sectioned shape (ADR 0010):
|
|
6
|
+
* `sections` wins when present; plain `children` is authoring sugar for one
|
|
7
|
+
* untitled section. The output never carries `children` — there is exactly
|
|
8
|
+
* one structure downstream code hydrates, transforms, and scopes against.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizePage(content: ResolvedTrack): ResolvedTrack {
|
|
11
|
+
const { children, sections, ...rest } = content
|
|
12
|
+
const normalized =
|
|
13
|
+
sections ?? (children !== undefined ? [{ children }] : undefined)
|
|
14
|
+
return { ...rest, sections: normalized }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The page's children concatenated in section order — the flattening that
|
|
19
|
+
* defines the contextual `__index` positions (ADR 0009/0010) and the flat
|
|
20
|
+
* views (tabs, search) of a sectioned page.
|
|
21
|
+
*/
|
|
22
|
+
export function flattenSections(sections: Section[] | undefined): Track[] {
|
|
23
|
+
return sections?.flatMap((section) => section.children) ?? []
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The page's tracks as one flat list, normalizing first — the flat view
|
|
28
|
+
* (tabs, search) of a page in either authored shape. Mirrors the native
|
|
29
|
+
* `flattenedChildren` accessors.
|
|
30
|
+
*/
|
|
31
|
+
export function flattenedChildren(page: ResolvedTrack): Track[] {
|
|
32
|
+
return flattenSections(normalizePage(page).sections)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The section of the page containing the playable `trackId` (a track
|
|
37
|
+
* identity: id when non-blank, else src), or undefined when not found.
|
|
38
|
+
*
|
|
39
|
+
* `tappedIndex` — the flat page position stamped into the contextual URL —
|
|
40
|
+
* is a tie-breaker, never an identifier: when the child at that position
|
|
41
|
+
* still carries the tapped identity, it pins which section (and which copy,
|
|
42
|
+
* via `tappedOffset`) was tapped; when it doesn't (the list shifted),
|
|
43
|
+
* resolution falls back to the first section containing the identity. A
|
|
44
|
+
* stale index can therefore never select a different track — at worst a
|
|
45
|
+
* different copy of the same one.
|
|
46
|
+
*/
|
|
47
|
+
export function sectionContaining(
|
|
48
|
+
sections: Section[],
|
|
49
|
+
trackId: string,
|
|
50
|
+
tappedIndex: number | undefined
|
|
51
|
+
): { tracks: Track[]; tappedOffset?: number } | undefined {
|
|
52
|
+
if (tappedIndex !== undefined && tappedIndex >= 0) {
|
|
53
|
+
let start = 0
|
|
54
|
+
for (const section of sections) {
|
|
55
|
+
const offset = tappedIndex - start
|
|
56
|
+
if (offset < section.children.length) {
|
|
57
|
+
const tapped = section.children[offset]
|
|
58
|
+
if (tapped && getTrackIdentity(tapped) === trackId) {
|
|
59
|
+
return { tracks: section.children, tappedOffset: offset }
|
|
60
|
+
}
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
start += section.children.length
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const section of sections) {
|
|
67
|
+
if (section.children.some((track) => getTrackIdentity(track) === trackId)) {
|
|
68
|
+
return { tracks: section.children }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { NowPlayingUpdate, NowPlayingMetadata } from '../../features'
|
|
2
2
|
import type { Track } from '../../types'
|
|
3
3
|
import { artworkUrl } from '../../utils/artwork'
|
|
4
|
+
import { getTrackIdentity } from '../../utils/getTrackIdentity'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Manages now playing metadata overrides.
|
|
@@ -37,7 +38,7 @@ export class NowPlayingManager {
|
|
|
37
38
|
album: update.album ?? currentTrack.album,
|
|
38
39
|
artwork: artworkUrl(currentTrack.artwork),
|
|
39
40
|
description: currentTrack.description,
|
|
40
|
-
mediaId: currentTrack
|
|
41
|
+
mediaId: getTrackIdentity(currentTrack) ?? currentTrack.path,
|
|
41
42
|
genre: currentTrack.genre,
|
|
42
43
|
duration
|
|
43
44
|
}
|
|
@@ -47,7 +48,7 @@ export class NowPlayingManager {
|
|
|
47
48
|
album: currentTrack.album,
|
|
48
49
|
artwork: artworkUrl(currentTrack.artwork),
|
|
49
50
|
description: currentTrack.description,
|
|
50
|
-
mediaId: currentTrack
|
|
51
|
+
mediaId: getTrackIdentity(currentTrack) ?? currentTrack.path,
|
|
51
52
|
genre: currentTrack.genre,
|
|
52
53
|
duration
|
|
53
54
|
}
|
|
@@ -74,7 +75,7 @@ export class NowPlayingManager {
|
|
|
74
75
|
album: this.override?.album ?? currentTrack.album,
|
|
75
76
|
artwork: artworkUrl(currentTrack.artwork),
|
|
76
77
|
description: currentTrack.description,
|
|
77
|
-
mediaId: currentTrack
|
|
78
|
+
mediaId: getTrackIdentity(currentTrack) ?? currentTrack.path,
|
|
78
79
|
genre: currentTrack.genre,
|
|
79
80
|
duration
|
|
80
81
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { BrowserPathHelper } from './BrowserPathHelper'
|
|
3
|
+
|
|
4
|
+
describe('BrowserPathHelper contextual URL round-trip', () => {
|
|
5
|
+
it('round-trips a src carrying its own query params', () => {
|
|
6
|
+
// A src carrying its own query string (signed CDN URL) must survive the
|
|
7
|
+
// build → extract/strip round-trip: an unescaped `&` would split the src
|
|
8
|
+
// into stray query params, truncating the trackId and polluting the
|
|
9
|
+
// parent path.
|
|
10
|
+
const src = 'https://cdn.example.com/stream.mp3?token=abc&exp=1699999999'
|
|
11
|
+
const url = BrowserPathHelper.build('/library', src)
|
|
12
|
+
expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
|
|
13
|
+
expect(BrowserPathHelper.stripTrackId(url)).toBe('/library')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('round-trips a src with equals and plus', () => {
|
|
17
|
+
const src = 'https://cdn.example.com/a+b.mp3?sig=x=y'
|
|
18
|
+
const url = BrowserPathHelper.build('/library', src)
|
|
19
|
+
expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
|
|
20
|
+
expect(BrowserPathHelper.stripTrackId(url)).toBe('/library')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('preserves unrelated parent query params when stripping', () => {
|
|
24
|
+
const src = 'https://cdn.example.com/stream.mp3?token=abc&exp=1'
|
|
25
|
+
const url = BrowserPathHelper.build('/search?q=jazz', src)
|
|
26
|
+
expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
|
|
27
|
+
expect(BrowserPathHelper.stripTrackId(url)).toBe('/search?q=jazz')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('round-trips the stamped index alongside the trackId', () => {
|
|
31
|
+
const src = 'https://cdn.example.com/stream.mp3?token=abc&exp=1'
|
|
32
|
+
const url = BrowserPathHelper.build('/library', src, 3)
|
|
33
|
+
expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
|
|
34
|
+
expect(BrowserPathHelper.extractIndex(url)).toBe(3)
|
|
35
|
+
expect(BrowserPathHelper.stripTrackId(url)).toBe('/library')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('extracts no index from an index-less contextual URL', () => {
|
|
39
|
+
const url = BrowserPathHelper.build('/library', 'song.mp3')
|
|
40
|
+
expect(BrowserPathHelper.extractIndex(url)).toBeUndefined()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('extracts no index from a non-contextual or malformed-index URL', () => {
|
|
44
|
+
expect(BrowserPathHelper.extractIndex('/library')).toBeUndefined()
|
|
45
|
+
expect(
|
|
46
|
+
BrowserPathHelper.extractIndex('/library?__trackId=song.mp3&__index=x')
|
|
47
|
+
).toBeUndefined()
|
|
48
|
+
expect(
|
|
49
|
+
BrowserPathHelper.extractIndex('/library?__trackId=song.mp3&__index=-1')
|
|
50
|
+
).toBeUndefined()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
* 1. System paths (prefixed with `/__`): Root, recent, and search paths
|
|
7
7
|
* 2. Contextual URLs: Embed parent context in track identifiers for Media3 integration
|
|
8
8
|
*
|
|
9
|
-
* Contextual URL format: `{parentPath}?__trackId={
|
|
10
|
-
* Example: "/library/radio?__trackId=song.mp3"
|
|
9
|
+
* Contextual URL format: `{parentPath}?__trackId={trackIdentity}&__index={childIndex}`
|
|
10
|
+
* Example: "/library/radio?__trackId=song.mp3&__index=2"
|
|
11
|
+
*
|
|
12
|
+
* `__trackId` is the identity check; `__index` (the child's position on the
|
|
13
|
+
* page at stamp time) is only a tie-breaker between surfaces that carry the
|
|
14
|
+
* same identity — a stale index never selects a different track.
|
|
11
15
|
*
|
|
12
16
|
* This allows:
|
|
13
17
|
* - Media3 to reference playable-only tracks (tracks with `src` but no `url`)
|
|
@@ -21,6 +25,9 @@ export const BrowserPathHelper = {
|
|
|
21
25
|
/** Query parameter name for contextual track identifiers */
|
|
22
26
|
CONTEXTUAL_TRACK_PARAM: '__trackId',
|
|
23
27
|
|
|
28
|
+
/** Query parameter name for the tapped child's page position (tie-breaker) */
|
|
29
|
+
CONTEXTUAL_INDEX_PARAM: '__index',
|
|
30
|
+
|
|
24
31
|
/**
|
|
25
32
|
* Create a search path for a given query
|
|
26
33
|
*/
|
|
@@ -43,13 +50,13 @@ export const BrowserPathHelper = {
|
|
|
43
50
|
},
|
|
44
51
|
|
|
45
52
|
/**
|
|
46
|
-
* Strips the __trackId
|
|
47
|
-
* If the URL is not contextual, returns it unchanged.
|
|
53
|
+
* Strips the contextual parameters (__trackId and __index) from a contextual
|
|
54
|
+
* URL to get the parent path. If the URL is not contextual, returns it unchanged.
|
|
48
55
|
*
|
|
49
56
|
* @param url The URL to process
|
|
50
|
-
* @returns The URL without the
|
|
57
|
+
* @returns The URL without the contextual parameters
|
|
51
58
|
*
|
|
52
|
-
* Example: "/library/radio?__trackId=song.mp3" → "/library/radio"
|
|
59
|
+
* Example: "/library/radio?__trackId=song.mp3&__index=2" → "/library/radio"
|
|
53
60
|
* Example: "/search?q=jazz&__trackId=song.mp3" → "/search?q=jazz"
|
|
54
61
|
*/
|
|
55
62
|
stripTrackId(url: string): string {
|
|
@@ -62,8 +69,9 @@ export const BrowserPathHelper = {
|
|
|
62
69
|
const isFullUrl = url.startsWith('http://') || url.startsWith('https://')
|
|
63
70
|
const urlObj = new URL(url, isFullUrl ? undefined : 'http://placeholder')
|
|
64
71
|
|
|
65
|
-
// Remove the
|
|
72
|
+
// Remove the contextual parameters
|
|
66
73
|
urlObj.searchParams.delete(this.CONTEXTUAL_TRACK_PARAM)
|
|
74
|
+
urlObj.searchParams.delete(this.CONTEXTUAL_INDEX_PARAM)
|
|
67
75
|
|
|
68
76
|
// Return the appropriate format
|
|
69
77
|
if (isFullUrl) {
|
|
@@ -75,7 +83,7 @@ export const BrowserPathHelper = {
|
|
|
75
83
|
} catch {
|
|
76
84
|
// Fallback: simple string manipulation
|
|
77
85
|
const paramPattern = new RegExp(
|
|
78
|
-
`[?&]
|
|
86
|
+
`[?&](?:${this.CONTEXTUAL_TRACK_PARAM}|${this.CONTEXTUAL_INDEX_PARAM})=[^&]*`,
|
|
79
87
|
'g'
|
|
80
88
|
)
|
|
81
89
|
let result = url.replace(paramPattern, '')
|
|
@@ -87,6 +95,24 @@ export const BrowserPathHelper = {
|
|
|
87
95
|
}
|
|
88
96
|
},
|
|
89
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Builds a contextual URL by appending a track identifier — and optionally
|
|
100
|
+
* the tapped child's page position — to a parent path.
|
|
101
|
+
*
|
|
102
|
+
* @param parentPath The parent container path
|
|
103
|
+
* @param trackId The track identity (`id` when non-blank, else `src`)
|
|
104
|
+
* @param index The child's position on the page at stamp time (tie-breaker)
|
|
105
|
+
* @returns A contextual URL combining parent path, track ID, and index
|
|
106
|
+
*
|
|
107
|
+
* Example: build("/library", "song.mp3", 2) → "/library?__trackId=song.mp3&__index=2"
|
|
108
|
+
*/
|
|
109
|
+
build(parentPath: string, trackId: string, index?: number): string {
|
|
110
|
+
const separator = parentPath.includes('?') ? '&' : '?'
|
|
111
|
+
const indexParam =
|
|
112
|
+
index === undefined ? '' : `&${this.CONTEXTUAL_INDEX_PARAM}=${index}`
|
|
113
|
+
return `${parentPath}${separator}${this.CONTEXTUAL_TRACK_PARAM}=${encodeURIComponent(trackId)}${indexParam}`
|
|
114
|
+
},
|
|
115
|
+
|
|
90
116
|
/**
|
|
91
117
|
* Extracts the track ID from a contextual URL.
|
|
92
118
|
* Returns undefined if the URL is not contextual or doesn't contain the track ID parameter.
|
|
@@ -100,18 +126,40 @@ export const BrowserPathHelper = {
|
|
|
100
126
|
if (!this.isContextual(path)) {
|
|
101
127
|
return undefined
|
|
102
128
|
}
|
|
129
|
+
return this.extractParam(path, this.CONTEXTUAL_TRACK_PARAM)
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extracts the stamped page index from a contextual URL, or undefined when
|
|
134
|
+
* the URL is not contextual or carries no (valid, non-negative) index.
|
|
135
|
+
*
|
|
136
|
+
* Example: "/library/radio?__trackId=song.mp3&__index=2" → 2
|
|
137
|
+
*/
|
|
138
|
+
extractIndex(path: string): number | undefined {
|
|
139
|
+
if (!this.isContextual(path)) {
|
|
140
|
+
return undefined
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const raw = this.extractParam(path, this.CONTEXTUAL_INDEX_PARAM)
|
|
144
|
+
if (raw === undefined || !/^\d+$/.test(raw)) {
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
return Number.parseInt(raw, 10)
|
|
148
|
+
},
|
|
103
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Extracts a single query parameter, handling both full URLs and paths,
|
|
152
|
+
* with a regex fallback for input the URL parser rejects.
|
|
153
|
+
*/
|
|
154
|
+
extractParam(path: string, param: string): string | undefined {
|
|
104
155
|
try {
|
|
105
|
-
// Parse the URL - handle both full URLs and paths
|
|
106
156
|
const isFullUrl =
|
|
107
157
|
path.startsWith('http://') || path.startsWith('https://')
|
|
108
158
|
const urlObj = new URL(path, isFullUrl ? undefined : 'http://placeholder')
|
|
109
|
-
return urlObj.searchParams.get(
|
|
159
|
+
return urlObj.searchParams.get(param) ?? undefined
|
|
110
160
|
} catch {
|
|
111
161
|
// Fallback: regex extraction
|
|
112
|
-
const match = path.match(
|
|
113
|
-
new RegExp(`[?&]${this.CONTEXTUAL_TRACK_PARAM}=([^&]*)`)
|
|
114
|
-
)
|
|
162
|
+
const match = path.match(new RegExp(`[?&]${param}=([^&]*)`))
|
|
115
163
|
return match?.[1] ? decodeURIComponent(match[1]) : undefined
|
|
116
164
|
}
|
|
117
165
|
},
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import NitroModules
|
|
3
|
-
|
|
4
|
-
// MARK: - ImageRowItem → Track Conversion
|
|
5
|
-
|
|
6
|
-
extension ImageRowItem {
|
|
7
|
-
/// Creates a minimal Track from an ImageRowItem for reuse with item selection and artwork loading.
|
|
8
|
-
/// A `src`-bearing item becomes a playable track (thumbnail tap plays it); otherwise the
|
|
9
|
-
/// track only navigates via `url`.
|
|
10
|
-
func toTrack() -> Track {
|
|
11
|
-
Track(
|
|
12
|
-
id: id, url: url, src: src, artwork: artwork.map { .first($0) },
|
|
13
|
-
artworkSource: artworkSource, request: request,
|
|
14
|
-
artworkCarPlayTinted: nil, title: title, subtitle: nil, artist: artist,
|
|
15
|
-
albumUrl: albumUrl, album: album, description: nil, genre: nil, duration: nil, style: nil,
|
|
16
|
-
childrenStyle: nil, favorited: nil, groupTitle: nil, live: live, imageRow: nil,
|
|
17
|
-
)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
///
|
|
2
|
-
/// JFavoriteConfig.hpp
|
|
3
|
-
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
-
/// https://github.com/mrousavy/nitro
|
|
5
|
-
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
-
///
|
|
7
|
-
|
|
8
|
-
#pragma once
|
|
9
|
-
|
|
10
|
-
#include <fbjni/fbjni.h>
|
|
11
|
-
#include "FavoriteConfig.hpp"
|
|
12
|
-
|
|
13
|
-
#include "FavoritesMatchMode.hpp"
|
|
14
|
-
#include "JFavoritesMatchMode.hpp"
|
|
15
|
-
|
|
16
|
-
namespace margelo::nitro::audiobrowser {
|
|
17
|
-
|
|
18
|
-
using namespace facebook;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The C++ JNI bridge between the C++ struct "FavoriteConfig" and the the Kotlin data class "FavoriteConfig".
|
|
22
|
-
*/
|
|
23
|
-
struct JFavoriteConfig final: public jni::JavaClass<JFavoriteConfig> {
|
|
24
|
-
public:
|
|
25
|
-
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/audiobrowser/FavoriteConfig;";
|
|
26
|
-
|
|
27
|
-
public:
|
|
28
|
-
/**
|
|
29
|
-
* Convert this Java/Kotlin-based struct to the C++ struct FavoriteConfig by copying all values to C++.
|
|
30
|
-
*/
|
|
31
|
-
[[maybe_unused]]
|
|
32
|
-
[[nodiscard]]
|
|
33
|
-
FavoriteConfig toCpp() const {
|
|
34
|
-
static const auto clazz = javaClassStatic();
|
|
35
|
-
static const auto fieldMatch = clazz->getField<JFavoritesMatchMode>("match");
|
|
36
|
-
jni::local_ref<JFavoritesMatchMode> match = this->getFieldValue(fieldMatch);
|
|
37
|
-
return FavoriteConfig(
|
|
38
|
-
match->toCpp()
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
public:
|
|
43
|
-
/**
|
|
44
|
-
* Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java.
|
|
45
|
-
*/
|
|
46
|
-
[[maybe_unused]]
|
|
47
|
-
static jni::local_ref<JFavoriteConfig::javaobject> fromCpp(const FavoriteConfig& value) {
|
|
48
|
-
using JSignature = JFavoriteConfig(jni::alias_ref<JFavoritesMatchMode>);
|
|
49
|
-
static const auto clazz = javaClassStatic();
|
|
50
|
-
static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
|
|
51
|
-
return create(
|
|
52
|
-
clazz,
|
|
53
|
-
JFavoritesMatchMode::fromCpp(value.match)
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
} // namespace margelo::nitro::audiobrowser
|