react-native-audio-browser 0.2.0-next.11 → 0.2.0-next.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +49 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -7
- package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
- package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +93 -73
- package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +35 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +26 -39
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +228 -50
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
- package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
- package/ios/Browser/BrowserConfig.swift +102 -5
- package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
- package/ios/Browser/BrowserManager.swift +131 -143
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/JsonModels.swift +187 -106
- package/ios/Browser/SearchDrillIn.swift +6 -6
- package/ios/Browser/SectionPresentation.swift +34 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +47 -0
- package/ios/Browser/TabBarEntries.swift +2 -2
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +105 -67
- package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
- package/ios/CarPlay/CarPlayListItemFactory.swift +217 -84
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
- package/ios/Extension/Section+Copying.swift +31 -0
- package/ios/Extension/Track+Copying.swift +6 -12
- package/ios/Extension/Track+Identity.swift +18 -0
- package/ios/HybridAudioBrowser.swift +113 -35
- package/ios/Model/NitroTypeStubs.swift +46 -21
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Player/NowPlayingUpdater.swift +1 -1
- package/ios/Player/PlaybackCoordinator.swift +2 -2
- package/ios/Player/RetryManager.swift +4 -3
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/JsonModelsTests.swift +75 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +61 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +67 -0
- package/ios/Tests/TabBarEntriesTests.swift +18 -18
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- package/lib/commonjs/AudioBrowser.js +11 -0
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/features/browser-config.js +11 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +2 -2
- package/lib/commonjs/features/favorites.js +13 -10
- package/lib/commonjs/features/favorites.js.map +1 -1
- package/lib/commonjs/features/playback/controls.js +4 -2
- package/lib/commonjs/features/playback/controls.js.map +1 -1
- package/lib/commonjs/features/playback/state.js +2 -3
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +3 -1
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +2 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -1
- package/lib/commonjs/features/queue/activeTrack.js +14 -3
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +11 -4
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
- package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/commonjs/utils/getTrackIdentity.js +24 -0
- package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +144 -87
- package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
- package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
- package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
- package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/commonjs/web/browser/sections.js +89 -0
- package/lib/commonjs/web/browser/sections.js.map +1 -0
- package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/AudioBrowser.js +1 -0
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/features/browser-config.js +11 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +2 -2
- package/lib/module/features/favorites.js +13 -10
- package/lib/module/features/favorites.js.map +1 -1
- package/lib/module/features/playback/controls.js +4 -2
- package/lib/module/features/playback/controls.js.map +1 -1
- package/lib/module/features/playback/state.js +2 -3
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +3 -1
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +2 -0
- package/lib/module/features/player/validateOptions.js.map +1 -1
- package/lib/module/features/queue/activeTrack.js +14 -3
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +11 -4
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/utils/LazyNativeEmitter.js +21 -10
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/utils/NativeUpdatedValue.js +31 -13
- package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/module/utils/getTrackIdentity.js +20 -0
- package/lib/module/utils/getTrackIdentity.js.map +1 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +32 -24
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +144 -87
- package/lib/module/web/browser/BrowserManager.js.map +1 -1
- package/lib/module/web/browser/FavoriteManager.js +31 -21
- package/lib/module/web/browser/FavoriteManager.js.map +1 -1
- package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/module/web/browser/sections.js +83 -0
- package/lib/module/web/browser/sections.js.map +1 -0
- package/lib/module/web/player/NowPlayingManager.js +4 -3
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js +56 -11
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts +1 -0
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/browser.d.ts +2 -2
- package/lib/typescript/src/features/favorites.d.ts +13 -10
- package/lib/typescript/src/features/favorites.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/controls.d.ts +4 -2
- package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +2 -3
- package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
- package/lib/typescript/src/features/player/options.d.ts +21 -19
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +5 -2
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-native.d.ts +2 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +260 -127
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +33 -53
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
- package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
- package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +10 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -7
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
- package/nitrogen/generated/android/c++/JSection.hpp +114 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +23 -5
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
- package/nitrogen/generated/ios/swift/Section.swift +100 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +64 -0
- package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +20 -73
- package/nitrogen/generated/ios/swift/TrackStyle.swift +28 -22
- package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
- package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
- package/nitrogen/generated/shared/c++/Section.hpp +106 -0
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +96 -0
- package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +15 -31
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +50 -34
- package/package.json +1 -1
- package/src/AudioBrowser.ts +1 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +11 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +2 -2
- package/src/features/favorites.ts +13 -10
- package/src/features/hook-wiring.test.ts +63 -15
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/state.ts +2 -3
- package/src/features/playback/trackPlaybackTime.ts +1 -1
- package/src/features/player/options.ts +27 -19
- package/src/features/player/setup.test.ts +12 -3
- package/src/features/player/setup.ts +3 -1
- package/src/features/player/validateOptions.ts +2 -0
- package/src/features/queue/activeTrack.ts +14 -3
- package/src/features/queue/queue.ts +11 -4
- package/src/nitro-stub-drift.test.ts +40 -0
- package/src/public-surface.test.ts +40 -0
- package/src/specs/audio-browser.nitro.ts +4 -3
- package/src/style-resolution.test.ts +139 -0
- package/src/types/browser-native.ts +2 -2
- package/src/types/browser-nodes.ts +268 -136
- package/src/types/browser.ts +34 -55
- package/src/utils/LazyNativeEmitter.ts +20 -10
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.ts +37 -22
- package/src/web/browser/BrowserManager.ts +176 -95
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
- package/src/web/browser/assertBrowsePageShape.ts +5 -3
- package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
- package/src/web/browser/classifyTrackNavigation.ts +9 -9
- package/src/web/browser/parseSearchResponse.test.ts +8 -2
- package/src/web/browser/sections.test.ts +91 -0
- package/src/web/browser/sections.ts +72 -0
- package/src/web/player/NowPlayingManager.ts +4 -3
- package/src/web/util/BrowserPathHelper.test.ts +52 -0
- package/src/web/util/BrowserPathHelper.ts +61 -13
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
- package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
- package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
- package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
- package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
|
@@ -12,6 +12,10 @@ import androidx.media3.session.SessionCommand
|
|
|
12
12
|
import androidx.media3.session.SessionError
|
|
13
13
|
import androidx.media3.session.SessionResult
|
|
14
14
|
import com.audiobrowser.browser.handleTrackLoad
|
|
15
|
+
import com.audiobrowser.browser.normalizedSections
|
|
16
|
+
import com.audiobrowser.browser.styleResolvedSections
|
|
17
|
+
import com.audiobrowser.browser.untitledSection
|
|
18
|
+
import com.audiobrowser.extension.indexOfTappedTrack
|
|
15
19
|
import com.audiobrowser.extension.toTrack
|
|
16
20
|
import com.audiobrowser.util.BrowserPathHelper
|
|
17
21
|
import com.audiobrowser.util.RatingFavorites
|
|
@@ -27,6 +31,8 @@ import com.margelo.nitro.audiobrowser.NativeGateRequest
|
|
|
27
31
|
import com.margelo.nitro.audiobrowser.PlayerCapabilities
|
|
28
32
|
import com.margelo.nitro.audiobrowser.RemoteButtonLayout
|
|
29
33
|
import com.margelo.nitro.audiobrowser.SearchParams
|
|
34
|
+
import com.margelo.nitro.audiobrowser.Section
|
|
35
|
+
import com.margelo.nitro.audiobrowser.StyleDisplay
|
|
30
36
|
import com.margelo.nitro.audiobrowser.Track
|
|
31
37
|
import kotlinx.coroutines.CancellationException
|
|
32
38
|
import kotlinx.coroutines.CoroutineScope
|
|
@@ -191,12 +197,17 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
191
197
|
command: SessionCommand,
|
|
192
198
|
args: Bundle,
|
|
193
199
|
): ListenableFuture<SessionResult> {
|
|
194
|
-
// Handle favorite button tap
|
|
200
|
+
// Handle favorite button tap. Report an honest result: a success here makes
|
|
201
|
+
// the controller flip its heart optimistically, so a no-op (no current
|
|
202
|
+
// track) must say INVALID_STATE instead of success-then-revert.
|
|
195
203
|
if (command.customAction == MediaSessionCommandManager.CUSTOM_ACTION_FAVORITE) {
|
|
196
|
-
val
|
|
197
|
-
Timber.d("Favorite button tapped -
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
val applied = player.toggleActiveTrackFavorited()
|
|
205
|
+
Timber.d("Favorite button tapped - toggle applied=$applied")
|
|
206
|
+
return Futures.immediateFuture(
|
|
207
|
+
SessionResult(
|
|
208
|
+
if (applied) SessionResult.RESULT_SUCCESS else SessionResult.RESULT_ERROR_INVALID_STATE
|
|
209
|
+
)
|
|
210
|
+
)
|
|
200
211
|
}
|
|
201
212
|
|
|
202
213
|
if (commandManager.handleCustomCommand(command, player)) {
|
|
@@ -212,8 +223,16 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
212
223
|
): ListenableFuture<SessionResult> {
|
|
213
224
|
// A heart rating from a controller (e.g. Google Assistant "I like this") toggles the
|
|
214
225
|
// now-playing favorite. setActiveTrackFavorited fires onFavoriteChanged so the consumer
|
|
215
|
-
// persists it — the same path as the notification / CarPlay heart button.
|
|
216
|
-
|
|
226
|
+
// persists it — the same path as the notification / CarPlay heart button. Report an honest
|
|
227
|
+
// result (see onCustomCommand): INVALID_STATE when there is no current track to favorite.
|
|
228
|
+
RatingFavorites.favoritedFor(rating)?.let { favorited ->
|
|
229
|
+
val applied = player.setActiveTrackFavorited(favorited)
|
|
230
|
+
return Futures.immediateFuture(
|
|
231
|
+
SessionResult(
|
|
232
|
+
if (applied) SessionResult.RESULT_SUCCESS else SessionResult.RESULT_ERROR_INVALID_STATE
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
}
|
|
217
236
|
return super.onSetRating(session, controller, rating)
|
|
218
237
|
}
|
|
219
238
|
|
|
@@ -238,13 +257,11 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
238
257
|
}
|
|
239
258
|
|
|
240
259
|
if (params?.isRecent == true) {
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
//
|
|
246
|
-
// an error of type 'RESULT_ERROR_NOT_SUPPORTED' for a `params.isRecent` request. See
|
|
247
|
-
// https://github.com/androidx/media/issues/355
|
|
260
|
+
// Playback resumption is served by media3 itself: it intercepts System UI's EXTRA_RECENT
|
|
261
|
+
// root request internally and answers through onPlaybackResumption, so that request never
|
|
262
|
+
// reaches this callback. Any other browser asking for a recent root gets
|
|
263
|
+
// ERROR_NOT_SUPPORTED — there is no browsable recents tree; recents are ordinary consumer
|
|
264
|
+
// content (a tab or route).
|
|
248
265
|
return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_NOT_SUPPORTED))
|
|
249
266
|
}
|
|
250
267
|
|
|
@@ -271,7 +288,7 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
271
288
|
params: MediaLibraryService.LibraryParams?,
|
|
272
289
|
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
|
273
290
|
Timber.d(
|
|
274
|
-
"onGetChildren: {parentId: $parentId, page: $page, pageSize: $pageSize,
|
|
291
|
+
"onGetChildren: {parentId: $parentId, page: $page, pageSize: $pageSize, isSpecialPath: ${BrowserPathHelper.isSpecialPath(parentId)} }"
|
|
275
292
|
)
|
|
276
293
|
return scope.future {
|
|
277
294
|
// Wait for browser to be registered if it's not available yet
|
|
@@ -325,30 +342,28 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
325
342
|
|
|
326
343
|
try {
|
|
327
344
|
val children =
|
|
328
|
-
if (parentId == BrowserPathHelper.
|
|
329
|
-
// TODO: implement recent media items
|
|
330
|
-
emptyList<MediaItem>()
|
|
331
|
-
} else if (parentId == BrowserPathHelper.ROOT_PATH) {
|
|
345
|
+
if (parentId == BrowserPathHelper.ROOT_PATH) {
|
|
332
346
|
// Return tabs as root children (limited to 4 for automotive platform compatibility)
|
|
333
347
|
// TODO: Check what Android Auto does with empty tabs list - may need to return error?
|
|
334
|
-
|
|
348
|
+
// Disabled tabs hide (Track.disabled) — filtered before the cap so
|
|
349
|
+
// an unavailable tab never costs an available one its slot.
|
|
350
|
+
val tabs = browserManager.queryTabs().filterNot { it.disabled == true }
|
|
335
351
|
if (tabs.size > 4) {
|
|
336
352
|
Timber.w(
|
|
337
353
|
"Root has ${tabs.size} tabs; dropping ${tabs.size - 4} (Android Auto root limit)"
|
|
338
354
|
)
|
|
339
355
|
}
|
|
340
|
-
|
|
356
|
+
toFlatMediaItems(tabs.take(4))
|
|
341
357
|
} else {
|
|
342
|
-
// Resolve the specific path and
|
|
358
|
+
// Resolve the specific path and flatten its sections to MediaItems
|
|
359
|
+
// (paths are already set to contextual paths). Section styles come
|
|
360
|
+
// page-folded (`section ?? page`, ADR 0011).
|
|
343
361
|
val resolvedTrack = browserManager.resolve(parentId)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
"Expected browsed ResolvedTrack to have a children array"
|
|
350
|
-
)
|
|
351
|
-
toMediaItems(trackChildren.toList())
|
|
362
|
+
val sections =
|
|
363
|
+
resolvedTrack.styleResolvedSections()
|
|
364
|
+
?: throw IllegalStateException("Expected browsed ResolvedTrack to have sections")
|
|
365
|
+
browserManager.warnIfGridPageLacksPromise(parentId, sections)
|
|
366
|
+
toMediaItems(sections)
|
|
352
367
|
}
|
|
353
368
|
|
|
354
369
|
LibraryResult.ofItemList(ImmutableList.copyOf(children.paginate(page, pageSize)), params)
|
|
@@ -402,19 +417,45 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
402
417
|
}
|
|
403
418
|
|
|
404
419
|
/**
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
420
|
+
* Flattens a page's sections to MediaItems for browse delivery — the Media3 boundary where
|
|
421
|
+
* sections die (ADR 0010): the MediaBrowser protocol has no section node, so each child is
|
|
422
|
+
* stamped with its owning section's title/style hints. A grid-displayed section with a `path`
|
|
423
|
+
* gains a browsable "view all" link under the same header. Http(s) artwork routes through the
|
|
424
|
+
* content:// provider so Android Auto can load it via the ArtworkContentProvider.
|
|
409
425
|
*/
|
|
410
|
-
private fun toMediaItems(
|
|
426
|
+
private fun toMediaItems(sections: List<Section>): List<MediaItem> {
|
|
411
427
|
val registry = player.browseArtworkRegistry
|
|
412
428
|
val authority = com.audiobrowser.util.ArtworkUris.authorityFor(player.context.packageName)
|
|
413
|
-
return
|
|
414
|
-
|
|
429
|
+
return sections.flatMap { section ->
|
|
430
|
+
// Android Auto has no disabled affordance, so an unavailable track hides
|
|
431
|
+
// — never a normal-looking dead row (Track.disabled's rendering ladder).
|
|
432
|
+
val visibleChildren = section.children.filter { it.disabled != true }
|
|
433
|
+
val items =
|
|
434
|
+
visibleChildren.map { TrackFactory.toBrowseMediaItem(it, registry, authority, section) }
|
|
435
|
+
val isTileSection = section.style?.display == StyleDisplay.GRID
|
|
436
|
+
// No "view all" under an empty section — CarPlay skips empty sections
|
|
437
|
+
// outright, and a lone navigation tile under a header is a dead end.
|
|
438
|
+
if (isTileSection && section.path != null && visibleChildren.isNotEmpty()) {
|
|
439
|
+
items +
|
|
440
|
+
TrackFactory.toBrowseMediaItem(
|
|
441
|
+
TrackFactory.navigationTrack(section),
|
|
442
|
+
registry,
|
|
443
|
+
authority,
|
|
444
|
+
section,
|
|
445
|
+
)
|
|
446
|
+
} else {
|
|
447
|
+
items
|
|
448
|
+
}
|
|
415
449
|
}
|
|
416
450
|
}
|
|
417
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Flat lists (tabs, search results) convert as one untitled list section — which contributes no
|
|
454
|
+
* group or style hints — so exactly one Track→MediaItem path exists at the Media3 boundary.
|
|
455
|
+
*/
|
|
456
|
+
private fun toFlatMediaItems(tracks: List<Track>): List<MediaItem> =
|
|
457
|
+
toMediaItems(listOf(untitledSection(tracks.toTypedArray())))
|
|
458
|
+
|
|
418
459
|
override fun onGetItem(
|
|
419
460
|
session: MediaLibraryService.MediaLibrarySession,
|
|
420
461
|
browser: MediaSession.ControllerInfo,
|
|
@@ -451,7 +492,7 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
451
492
|
return@future LibraryResult.ofItem(createGateMediaItem(outcome.chrome!!), null)
|
|
452
493
|
}
|
|
453
494
|
|
|
454
|
-
if (mediaId == BrowserPathHelper.ROOT_PATH
|
|
495
|
+
if (mediaId == BrowserPathHelper.ROOT_PATH) {
|
|
455
496
|
return@future LibraryResult.ofItem(
|
|
456
497
|
MediaItem.Builder()
|
|
457
498
|
.setMediaId(mediaId)
|
|
@@ -466,7 +507,7 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
466
507
|
// Wait for browser to be registered if it's not available yet
|
|
467
508
|
val browserManager = player.awaitBrowser().browserManager
|
|
468
509
|
|
|
469
|
-
// Serve tracks from the track cache first (keyed by
|
|
510
|
+
// Serve tracks from the track cache first (keyed by path and src). Besides avoiding an HTTP
|
|
470
511
|
// resolve, this keeps item identity correct for contextual mediaIds: resolve() strips
|
|
471
512
|
// __trackId and would return the *parent container's* metadata as the item.
|
|
472
513
|
val browseAuthority =
|
|
@@ -628,7 +669,12 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
628
669
|
try {
|
|
629
670
|
// Execute search (automatically caches results at /__search?q=query)
|
|
630
671
|
val searchResults = browserManager.search(query)
|
|
631
|
-
|
|
672
|
+
// Count what onGetSearchResult will actually serve: disabled tracks
|
|
673
|
+
// hide on Android Auto, so they must not inflate the announced count.
|
|
674
|
+
val resultCount =
|
|
675
|
+
searchResults.normalizedSections?.sumOf { section ->
|
|
676
|
+
section.children.count { it.disabled != true }
|
|
677
|
+
} ?: 0
|
|
632
678
|
|
|
633
679
|
Timber.d("Search completed: $resultCount results for query '$query'")
|
|
634
680
|
|
|
@@ -681,14 +727,9 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
681
727
|
try {
|
|
682
728
|
// Get cached search results from BrowserManager
|
|
683
729
|
browserManager.getCachedSearchResults(query)?.let { tracks ->
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
val mediaItems =
|
|
688
|
-
tracks.map { track ->
|
|
689
|
-
Timber.d("Search result: ${track.title} (url=${track.url}, src=${track.src})")
|
|
690
|
-
TrackFactory.toBrowseMediaItem(track, player.browseArtworkRegistry, searchAuthority)
|
|
691
|
-
}
|
|
730
|
+
// Through the shared browse conversion, so search results get the
|
|
731
|
+
// same artwork routing as browse rows.
|
|
732
|
+
val mediaItems = toFlatMediaItems(tracks.toList())
|
|
692
733
|
|
|
693
734
|
val paginatedItems = mediaItems.paginate(page, pageSize)
|
|
694
735
|
Timber.d("Returning ${paginatedItems.size} search results")
|
|
@@ -737,26 +778,28 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
737
778
|
|
|
738
779
|
val browserManager = audioBrowser.browserManager
|
|
739
780
|
|
|
740
|
-
// A single tapped item resolves to the contextual
|
|
781
|
+
// A single tapped item resolves to the contextual path of the list it was
|
|
741
782
|
// tapped in: directly for contextual mediaIds, via the track cache for
|
|
742
|
-
// stable-id mediaIds (see BrowserManager.
|
|
783
|
+
// stable-id mediaIds (see BrowserManager.contextualPathFor). A search
|
|
743
784
|
// selection is not a list tap — its queue comes from the search results
|
|
744
785
|
// (resolveMediaItemsForPlayback), never from a browsed container.
|
|
745
|
-
val
|
|
786
|
+
val singleContextualPath =
|
|
746
787
|
mediaItems
|
|
747
788
|
.singleOrNull()
|
|
748
789
|
?.takeIf { it.requestMetadata.searchQuery == null }
|
|
749
|
-
?.let { browserManager.
|
|
790
|
+
?.let { browserManager.contextualPathFor(it.mediaId) }
|
|
750
791
|
|
|
751
792
|
// Check if this is a single item from the current queue source
|
|
752
|
-
if (
|
|
753
|
-
val parentPath = BrowserPathHelper.stripTrackId(
|
|
754
|
-
val trackId = BrowserPathHelper.extractTrackId(
|
|
793
|
+
if (singleContextualPath != null) {
|
|
794
|
+
val parentPath = BrowserPathHelper.stripTrackId(singleContextualPath)
|
|
795
|
+
val trackId = BrowserPathHelper.extractTrackId(singleContextualPath)
|
|
755
796
|
|
|
756
|
-
// Check if queue already came from this parent path - just skip to the
|
|
797
|
+
// Check if queue already came from this parent path - just skip to the
|
|
798
|
+
// tapped surface (exact path first, identity for index-less paths —
|
|
799
|
+
// see indexOfTappedTrack)
|
|
757
800
|
if (trackId != null && parentPath == player.queueSourcePath) {
|
|
758
801
|
val queueTracks = withContext(Dispatchers.Main) { player.tracks }
|
|
759
|
-
val index = queueTracks.
|
|
802
|
+
val index = queueTracks.indexOfTappedTrack(singleContextualPath, trackId)
|
|
760
803
|
if (index >= 0) {
|
|
761
804
|
Timber.d("Queue already from $parentPath, skipping to index $index")
|
|
762
805
|
val track = queueTracks[index]
|
|
@@ -788,9 +831,10 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
788
831
|
result.startIndex.toDouble(),
|
|
789
832
|
::currentPlayerState,
|
|
790
833
|
) {
|
|
791
|
-
// If this was a contextual
|
|
792
|
-
|
|
793
|
-
|
|
834
|
+
// If this was a contextual path expansion, track the source path (only for default
|
|
835
|
+
// behavior)
|
|
836
|
+
if (singleContextualPath != null) {
|
|
837
|
+
val parentPath = BrowserPathHelper.stripTrackId(singleContextualPath)
|
|
794
838
|
withContext(Dispatchers.Main) { player.queueSourcePath = parentPath }
|
|
795
839
|
}
|
|
796
840
|
result
|
|
@@ -829,6 +873,15 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
829
873
|
throw IllegalStateException("No playback state to resume")
|
|
830
874
|
}
|
|
831
875
|
|
|
876
|
+
// The persisted track can carry `disabled` — it became unavailable after
|
|
877
|
+
// it was saved. A disabled track never plays, resumption included
|
|
878
|
+
// (Track.disabled); failing the future refuses without touching the
|
|
879
|
+
// player.
|
|
880
|
+
if (state.track.disabled == true) {
|
|
881
|
+
Timber.w("Refusing resumption of disabled track: ${state.track.title}")
|
|
882
|
+
throw IllegalStateException("Persisted track is disabled")
|
|
883
|
+
}
|
|
884
|
+
|
|
832
885
|
// isForPlayback == false is the device-boot-time notification case: network may be
|
|
833
886
|
// unavailable and we must not start playback. Return just the locally-stored track with its
|
|
834
887
|
// already-local metadata; skip the settings restore and network queue expansion below.
|
|
@@ -844,14 +897,14 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
844
897
|
// restore() sets player properties which must happen on main thread
|
|
845
898
|
withContext(Dispatchers.Main) { player.playbackStateStore.restore() }
|
|
846
899
|
|
|
847
|
-
val
|
|
848
|
-
Timber.d("Resuming from
|
|
900
|
+
val path = state.track.path
|
|
901
|
+
Timber.d("Resuming from path=$path, positionMs=${state.positionMs}")
|
|
849
902
|
|
|
850
903
|
// Wait for browser to be available (JS needs to have configured it)
|
|
851
904
|
val browserManager = player.awaitBrowser().browserManager
|
|
852
905
|
|
|
853
|
-
// Try to expand the
|
|
854
|
-
val expanded =
|
|
906
|
+
// Try to expand the path into a full queue
|
|
907
|
+
val expanded = path?.let { browserManager.expandQueueFromContextualPath(it) }
|
|
855
908
|
|
|
856
909
|
if (expanded != null) {
|
|
857
910
|
val (tracks, selectedIndex) = expanded
|
|
@@ -859,9 +912,9 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
859
912
|
"Restored ${tracks.size} tracks, starting at index $selectedIndex at ${state.positionMs}ms"
|
|
860
913
|
)
|
|
861
914
|
|
|
862
|
-
// Track the source path if this was a contextual
|
|
863
|
-
if (BrowserPathHelper.isContextual(
|
|
864
|
-
val parentPath = BrowserPathHelper.stripTrackId(
|
|
915
|
+
// Track the source path if this was a contextual path expansion
|
|
916
|
+
if (BrowserPathHelper.isContextual(path)) {
|
|
917
|
+
val parentPath = BrowserPathHelper.stripTrackId(path)
|
|
865
918
|
withContext(Dispatchers.Main) { player.queueSourcePath = parentPath }
|
|
866
919
|
}
|
|
867
920
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.audiobrowser.player
|
|
2
2
|
|
|
3
|
+
import com.audiobrowser.extension.identity
|
|
3
4
|
import com.audiobrowser.util.url
|
|
4
5
|
import com.margelo.nitro.audiobrowser.FormatNowPlayingParams
|
|
5
6
|
import com.margelo.nitro.audiobrowser.NowPlayingMetadata
|
|
@@ -105,10 +106,11 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
|
|
|
105
106
|
private var lastPublished: Published? = null
|
|
106
107
|
|
|
107
108
|
/**
|
|
108
|
-
* The track id
|
|
109
|
-
*
|
|
109
|
+
* The track identity (id-or-src, falling back to path — same key as [Published]) whose
|
|
110
|
+
* now-playing artwork has been resolved (or is being resolved), so a `nowPlayingArtwork` resolve
|
|
111
|
+
* runs once per track — id-less tracks included — instead of on every render.
|
|
110
112
|
*/
|
|
111
|
-
private var
|
|
113
|
+
private var artworkResolvedForTrack: String? = null
|
|
112
114
|
|
|
113
115
|
/** Size hint (px, square) for now-playing artwork. Mirrors iOS (screen-width-capped to 1200). */
|
|
114
116
|
private val artworkSizePx = 1200.0
|
|
@@ -164,7 +166,7 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
|
|
|
164
166
|
duration = track.duration,
|
|
165
167
|
artwork = track.artwork?.url,
|
|
166
168
|
description = track.description,
|
|
167
|
-
mediaId = track.src ?: track.
|
|
169
|
+
mediaId = track.src ?: track.path,
|
|
168
170
|
genre = track.genre,
|
|
169
171
|
)
|
|
170
172
|
}
|
|
@@ -227,7 +229,7 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
|
|
|
227
229
|
// default on null/throw.
|
|
228
230
|
val activeFormatter = if (enabled) formatter else null
|
|
229
231
|
if (activeFormatter != null) {
|
|
230
|
-
val capturedId = track.src ?: track.
|
|
232
|
+
val capturedId = track.src ?: track.path
|
|
231
233
|
// Gate the raw load-control signal to the buffering state so `stalled` is correct on its
|
|
232
234
|
// own: ExoPlayer's rebuffering flag is polled on a different cadence than state transitions
|
|
233
235
|
// and can linger true into the PLAYING transition as a rebuffer recovers. Classify by
|
|
@@ -266,7 +268,7 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
|
|
|
266
268
|
formatted != null &&
|
|
267
269
|
current != null &&
|
|
268
270
|
currentIdx != null &&
|
|
269
|
-
(current.src ?: current.
|
|
271
|
+
(current.src ?: current.path) == capturedId &&
|
|
270
272
|
renderGeneration == generation
|
|
271
273
|
) {
|
|
272
274
|
applyFields(
|
|
@@ -293,7 +295,7 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
|
|
|
293
295
|
// the same fields are recomputed often; an unconditional stamp would churn the MediaSession
|
|
294
296
|
// (and flicker Android Auto / now-playing) for no visible change. Keyed on the track identity
|
|
295
297
|
// so a new track with a coincidentally identical line still publishes.
|
|
296
|
-
val published = Published(index, track.
|
|
298
|
+
val published = Published(index, track.identity ?: track.path, title, secondaryLine, album)
|
|
297
299
|
if (published == lastPublished) return
|
|
298
300
|
lastPublished = published
|
|
299
301
|
|
|
@@ -307,28 +309,23 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
|
|
|
307
309
|
|
|
308
310
|
/**
|
|
309
311
|
* Resolves the playing track's now-playing artwork and stamps it onto the playing media item.
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
* artworkUri — which came from `artwork` (browse
|
|
312
|
+
* Runs for EVERY playing track when `nowPlayingArtwork` is configured — id or not, exactly like
|
|
313
|
+
* iOS (an id-less track leaves any `{id}` token unfilled; the resolver warns and the request
|
|
314
|
+
* visibly 404s). Without a config, the existing artworkUri — which came from `artwork` (browse
|
|
315
|
+
* list path) — is left in place.
|
|
313
316
|
*/
|
|
314
317
|
private fun maybeResolveArtwork(track: Track) {
|
|
315
|
-
val trackId =
|
|
316
|
-
track.id?.takeIf { it.isNotEmpty() }
|
|
317
|
-
?: run {
|
|
318
|
-
// No id → skip nowPlayingArtwork entirely; keep the existing (browse) artworkUri.
|
|
319
|
-
artworkResolvedForTrackId = null
|
|
320
|
-
return
|
|
321
|
-
}
|
|
322
|
-
|
|
323
318
|
if (!surface.hasNowPlayingArtworkConfig) {
|
|
324
319
|
// No now-playing artwork config (or no browser) → fall back to the existing artworkUri.
|
|
325
|
-
|
|
320
|
+
artworkResolvedForTrack = null
|
|
326
321
|
return
|
|
327
322
|
}
|
|
328
323
|
|
|
329
|
-
// Already resolved (or resolving) for this track
|
|
330
|
-
|
|
331
|
-
|
|
324
|
+
// Already resolved (or resolving) for this track — avoid a redundant resolve per render.
|
|
325
|
+
// Keyed on track identity (id-or-src, falling back to path) so id-less tracks dedupe too.
|
|
326
|
+
val trackKey = track.identity ?: track.path
|
|
327
|
+
if (trackKey != null && artworkResolvedForTrack == trackKey) return
|
|
328
|
+
artworkResolvedForTrack = trackKey
|
|
332
329
|
|
|
333
330
|
scope.launch {
|
|
334
331
|
val uri =
|
|
@@ -336,10 +333,10 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
|
|
|
336
333
|
?: return@launch
|
|
337
334
|
|
|
338
335
|
// Apply only if still the same track (a fast skip must not be overwritten by a stale
|
|
339
|
-
// result).
|
|
336
|
+
// result), compared on the same identity key the resolve was launched under.
|
|
340
337
|
val currentIdx = surface.currentIndex ?: return@launch
|
|
341
338
|
val current = surface.currentTrack ?: return@launch
|
|
342
|
-
if (current.
|
|
339
|
+
if ((current.identity ?: current.path) != trackKey) return@launch
|
|
343
340
|
|
|
344
341
|
surface.stampArtwork(currentIdx, current, uri)
|
|
345
342
|
}
|
|
@@ -5,6 +5,9 @@ import android.content.SharedPreferences
|
|
|
5
5
|
import androidx.core.content.edit
|
|
6
6
|
import androidx.media3.common.C
|
|
7
7
|
import com.audiobrowser.util.artworkOf
|
|
8
|
+
import com.audiobrowser.util.toArtworkRendering
|
|
9
|
+
import com.audiobrowser.util.toStyleDisplay
|
|
10
|
+
import com.audiobrowser.util.toWireString
|
|
8
11
|
import com.audiobrowser.util.url
|
|
9
12
|
import com.margelo.nitro.audiobrowser.PlaybackState
|
|
10
13
|
import com.margelo.nitro.audiobrowser.RepeatMode
|
|
@@ -18,12 +21,12 @@ import org.json.JSONObject
|
|
|
18
21
|
import timber.log.Timber
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
|
-
* Persists playback state for resumption after app restart. Stores the current track
|
|
22
|
-
* and player settings to enable playback resumption via MediaButtonReceiver
|
|
23
|
-
* etc.).
|
|
24
|
+
* Persists playback state for resumption after app restart. Stores the current track (including its
|
|
25
|
+
* path), position, and player settings to enable playback resumption via MediaButtonReceiver
|
|
26
|
+
* (Bluetooth play button, etc.).
|
|
24
27
|
*
|
|
25
28
|
* Settings (repeatMode, shuffleEnabled, playbackSpeed) are auto-persisted when set via properties.
|
|
26
|
-
* Position/
|
|
29
|
+
* Position/track are persisted via save() on pause or track change.
|
|
27
30
|
*/
|
|
28
31
|
class PlaybackStateStore(private val player: Player) {
|
|
29
32
|
private val prefs: SharedPreferences =
|
|
@@ -32,7 +35,7 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
32
35
|
/**
|
|
33
36
|
* Persisted playback state for resumption.
|
|
34
37
|
*
|
|
35
|
-
* @param track The track being played (includes
|
|
38
|
+
* @param track The track being played (includes path for queue expansion)
|
|
36
39
|
* @param positionMs The playback position in milliseconds (C.TIME_UNSET for default/live edge)
|
|
37
40
|
* @param repeatMode The repeat mode setting
|
|
38
41
|
* @param shuffleEnabled Whether shuffle mode was enabled
|
|
@@ -148,78 +151,11 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
148
151
|
Timber.d("Saved position: positionMs=0 (queue ended)")
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
private fun trackToJson(track: Track): String =
|
|
152
|
-
JSONObject()
|
|
153
|
-
.apply {
|
|
154
|
-
put("id", track.id)
|
|
155
|
-
put("url", track.url)
|
|
156
|
-
put("src", track.src)
|
|
157
|
-
put("title", track.title)
|
|
158
|
-
put("subtitle", track.subtitle)
|
|
159
|
-
put("artist", track.artist)
|
|
160
|
-
put("albumUrl", track.albumUrl)
|
|
161
|
-
put("album", track.album)
|
|
162
|
-
put("artwork", track.artwork?.url)
|
|
163
|
-
put("description", track.description)
|
|
164
|
-
put("genre", track.genre)
|
|
165
|
-
put("duration", track.duration)
|
|
166
|
-
put("style", track.style?.name)
|
|
167
|
-
put("childrenStyle", track.childrenStyle?.name)
|
|
168
|
-
put("favorited", track.favorited)
|
|
169
|
-
put("groupTitle", track.groupTitle)
|
|
170
|
-
put("live", track.live)
|
|
171
|
-
}
|
|
172
|
-
.toString()
|
|
173
|
-
|
|
174
|
-
private fun trackFromJson(json: String): Track? =
|
|
175
|
-
runCatching {
|
|
176
|
-
val obj = JSONObject(json)
|
|
177
|
-
Track(
|
|
178
|
-
id = obj.optString("id").takeIf { it.isNotEmpty() },
|
|
179
|
-
url = obj.optString("url").takeIf { it.isNotEmpty() },
|
|
180
|
-
src = obj.optString("src").takeIf { it.isNotEmpty() },
|
|
181
|
-
title = obj.getString("title"),
|
|
182
|
-
subtitle = obj.optString("subtitle").takeIf { it.isNotEmpty() },
|
|
183
|
-
artist = obj.optString("artist").takeIf { it.isNotEmpty() },
|
|
184
|
-
albumUrl = obj.optString("albumUrl").takeIf { it.isNotEmpty() },
|
|
185
|
-
album = obj.optString("album").takeIf { it.isNotEmpty() },
|
|
186
|
-
artwork = artworkOf(obj.optString("artwork").takeIf { it.isNotEmpty() }),
|
|
187
|
-
artworkSource = null, // Not persisted - will be re-transformed on browse
|
|
188
|
-
artworkCarPlayTinted = null, // iOS-only, not persisted
|
|
189
|
-
description = obj.optString("description").takeIf { it.isNotEmpty() },
|
|
190
|
-
genre = obj.optString("genre").takeIf { it.isNotEmpty() },
|
|
191
|
-
duration =
|
|
192
|
-
if (obj.has("duration") && !obj.isNull("duration")) obj.getDouble("duration") else null,
|
|
193
|
-
style =
|
|
194
|
-
obj
|
|
195
|
-
.optString("style")
|
|
196
|
-
.takeIf { it.isNotEmpty() }
|
|
197
|
-
?.let { runCatching { TrackStyle.valueOf(it) }.getOrNull() },
|
|
198
|
-
childrenStyle =
|
|
199
|
-
obj
|
|
200
|
-
.optString("childrenStyle")
|
|
201
|
-
.takeIf { it.isNotEmpty() }
|
|
202
|
-
?.let { runCatching { TrackStyle.valueOf(it) }.getOrNull() },
|
|
203
|
-
favorited =
|
|
204
|
-
if (obj.has("favorited") && !obj.isNull("favorited")) obj.getBoolean("favorited")
|
|
205
|
-
else null,
|
|
206
|
-
groupTitle = obj.optString("groupTitle").takeIf { it.isNotEmpty() },
|
|
207
|
-
live = if (obj.has("live") && !obj.isNull("live")) obj.getBoolean("live") else null,
|
|
208
|
-
// Not persisted: on resumption the contextual URL is re-browsed
|
|
209
|
-
// (expandQueueFromContextualUrl), which re-parses each track's request
|
|
210
|
-
// from the API's current response and re-caches it.
|
|
211
|
-
request = null,
|
|
212
|
-
imageRow = null, // Not persisted
|
|
213
|
-
)
|
|
214
|
-
}
|
|
215
|
-
.onFailure { e -> Timber.w(e, "Failed to parse persisted track JSON") }
|
|
216
|
-
.getOrNull()
|
|
217
|
-
|
|
218
154
|
/**
|
|
219
155
|
* Restores player settings from persisted state and returns the state for queue setup.
|
|
220
156
|
*
|
|
221
157
|
* Applies repeatMode, shuffleMode, and playbackSpeed to the player. The caller is responsible for
|
|
222
|
-
* using the returned
|
|
158
|
+
* using the returned path and positionMs to set up the playback queue.
|
|
223
159
|
*
|
|
224
160
|
* @return PersistedState if available, null otherwise
|
|
225
161
|
*/
|
|
@@ -255,6 +191,90 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
255
191
|
}
|
|
256
192
|
|
|
257
193
|
companion object {
|
|
194
|
+
// Internal (not private) so the round-trip is testable without a Player.
|
|
195
|
+
internal fun trackToJson(track: Track): String =
|
|
196
|
+
JSONObject()
|
|
197
|
+
.apply {
|
|
198
|
+
put("id", track.id)
|
|
199
|
+
put("path", track.path)
|
|
200
|
+
put("src", track.src)
|
|
201
|
+
put("title", track.title)
|
|
202
|
+
put("subtitle", track.subtitle)
|
|
203
|
+
put("artist", track.artist)
|
|
204
|
+
put("albumPath", track.albumPath)
|
|
205
|
+
put("album", track.album)
|
|
206
|
+
put("artwork", track.artwork?.url)
|
|
207
|
+
put("description", track.description)
|
|
208
|
+
put("genre", track.genre)
|
|
209
|
+
put("duration", track.duration)
|
|
210
|
+
// The style block persists as its wire shape (nested object, wire
|
|
211
|
+
// strings) and `disabled` beside it: a restored queue keeps its
|
|
212
|
+
// visible fidelity across process death (ADR 0011).
|
|
213
|
+
put(
|
|
214
|
+
"style",
|
|
215
|
+
track.style?.let { style ->
|
|
216
|
+
JSONObject().apply {
|
|
217
|
+
put("display", style.display?.toWireString())
|
|
218
|
+
put("artworkRendering", style.artworkRendering?.toWireString())
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
put("disabled", track.disabled)
|
|
223
|
+
put("favorited", track.favorited)
|
|
224
|
+
put("live", track.live)
|
|
225
|
+
}
|
|
226
|
+
.toString()
|
|
227
|
+
|
|
228
|
+
internal fun trackFromJson(json: String): Track? =
|
|
229
|
+
runCatching {
|
|
230
|
+
val obj = JSONObject(json)
|
|
231
|
+
Track(
|
|
232
|
+
id = obj.optString("id").takeIf { it.isNotEmpty() },
|
|
233
|
+
path = obj.optString("path").takeIf { it.isNotEmpty() },
|
|
234
|
+
src = obj.optString("src").takeIf { it.isNotEmpty() },
|
|
235
|
+
title = obj.getString("title"),
|
|
236
|
+
subtitle = obj.optString("subtitle").takeIf { it.isNotEmpty() },
|
|
237
|
+
artist = obj.optString("artist").takeIf { it.isNotEmpty() },
|
|
238
|
+
albumPath = obj.optString("albumPath").takeIf { it.isNotEmpty() },
|
|
239
|
+
album = obj.optString("album").takeIf { it.isNotEmpty() },
|
|
240
|
+
artwork = artworkOf(obj.optString("artwork").takeIf { it.isNotEmpty() }),
|
|
241
|
+
artworkSource = null, // Not persisted - will be re-transformed on browse
|
|
242
|
+
description = obj.optString("description").takeIf { it.isNotEmpty() },
|
|
243
|
+
genre = obj.optString("genre").takeIf { it.isNotEmpty() },
|
|
244
|
+
duration =
|
|
245
|
+
if (obj.has("duration") && !obj.isNull("duration")) obj.getDouble("duration")
|
|
246
|
+
else null,
|
|
247
|
+
// Tolerant (ADR 0011): a snapshot written before the block model —
|
|
248
|
+
// `style` as an enum-name string — is not an object, so optJSONObject
|
|
249
|
+
// yields null and the track restores without a declaration.
|
|
250
|
+
style =
|
|
251
|
+
obj
|
|
252
|
+
.optJSONObject("style")
|
|
253
|
+
?.let { style ->
|
|
254
|
+
TrackStyle(
|
|
255
|
+
display = style.optString("display").toStyleDisplay(),
|
|
256
|
+
artworkRendering = style.optString("artworkRendering").toArtworkRendering(),
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
// An empty block collapses to "no declaration" — one shape on
|
|
260
|
+
// every platform, matching the browse decoders.
|
|
261
|
+
?.takeIf { it.display != null || it.artworkRendering != null },
|
|
262
|
+
disabled =
|
|
263
|
+
if (obj.has("disabled") && !obj.isNull("disabled")) obj.getBoolean("disabled")
|
|
264
|
+
else null,
|
|
265
|
+
favorited =
|
|
266
|
+
if (obj.has("favorited") && !obj.isNull("favorited")) obj.getBoolean("favorited")
|
|
267
|
+
else null,
|
|
268
|
+
live = if (obj.has("live") && !obj.isNull("live")) obj.getBoolean("live") else null,
|
|
269
|
+
// Not persisted: on resumption the contextual path is re-browsed
|
|
270
|
+
// (expandQueueFromContextualPath), which re-parses each track's request
|
|
271
|
+
// from the API's current response and re-caches it.
|
|
272
|
+
request = null,
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
.onFailure { e -> Timber.w(e, "Failed to parse persisted track JSON") }
|
|
276
|
+
.getOrNull()
|
|
277
|
+
|
|
258
278
|
private const val PREFS_NAME = "audio_browser_playback_state_v3"
|
|
259
279
|
private const val KEY_POSITION_MS = "position_ms"
|
|
260
280
|
private const val KEY_REPEAT_MODE = "repeat_mode"
|