react-native-audio-browser 0.2.0-next.11 → 0.2.0-next.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/android/build.gradle +20 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +115 -26
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +262 -188
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +95 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +19 -7
- package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
- package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +109 -74
- package/android/src/main/java/com/audiobrowser/player/Player.kt +51 -30
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +52 -53
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +74 -41
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +229 -50
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
- package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
- package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
- package/ios/Browser/BrowserConfig.swift +102 -5
- package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
- package/ios/Browser/BrowserManager.swift +229 -180
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +258 -106
- package/ios/Browser/SearchDrillIn.swift +6 -6
- package/ios/Browser/SectionPresentation.swift +64 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +58 -0
- package/ios/Browser/TabBarEntries.swift +9 -3
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +227 -85
- package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
- package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
- package/ios/Extension/Section+Copying.swift +31 -0
- package/ios/Extension/Track+Copying.swift +6 -12
- package/ios/Extension/Track+Identity.swift +18 -0
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +166 -45
- package/ios/Model/NitroTypeStubs.swift +106 -21
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Player/MediaIntentCriteria.swift +13 -5
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +54 -6
- package/ios/Player/PlaybackCoordinator.swift +13 -6
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +36 -10
- package/ios/Player/RetryManager.swift +4 -3
- package/ios/Player/ShuffleOrder.swift +23 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
- package/ios/Tests/JsonModelsTests.swift +150 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/QueueManagerTests.swift +72 -6
- package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +90 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +127 -0
- package/ios/Tests/Support/TestTypes.swift +7 -0
- package/ios/Tests/TabBarEntriesTests.swift +18 -18
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/TrackPlayer.swift +57 -34
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/SFSymbolRenderer.swift +24 -2
- package/ios/Util/SVGProcessor.swift +9 -2
- package/lib/commonjs/AudioBrowser.js +11 -0
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/features/browser-config.js +11 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +2 -2
- package/lib/commonjs/features/favorites.js +13 -10
- package/lib/commonjs/features/favorites.js.map +1 -1
- package/lib/commonjs/features/playback/controls.js +4 -2
- package/lib/commonjs/features/playback/controls.js.map +1 -1
- package/lib/commonjs/features/playback/progress.js +2 -1
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/rate.js +8 -1
- package/lib/commonjs/features/playback/rate.js.map +1 -1
- package/lib/commonjs/features/playback/state.js +2 -3
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +3 -1
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +2 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -1
- package/lib/commonjs/features/queue/activeTrack.js +14 -3
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +11 -4
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
- package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/commonjs/utils/getTrackIdentity.js +24 -0
- package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +144 -87
- package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
- package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
- package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
- package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/commonjs/web/browser/sections.js +89 -0
- package/lib/commonjs/web/browser/sections.js.map +1 -0
- package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/AudioBrowser.js +1 -0
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/features/browser-config.js +11 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +2 -2
- package/lib/module/features/favorites.js +13 -10
- package/lib/module/features/favorites.js.map +1 -1
- package/lib/module/features/playback/controls.js +4 -2
- package/lib/module/features/playback/controls.js.map +1 -1
- package/lib/module/features/playback/progress.js +2 -1
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/rate.js +8 -1
- package/lib/module/features/playback/rate.js.map +1 -1
- package/lib/module/features/playback/state.js +2 -3
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +3 -1
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +2 -0
- package/lib/module/features/player/validateOptions.js.map +1 -1
- package/lib/module/features/queue/activeTrack.js +14 -3
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +11 -4
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/utils/LazyNativeEmitter.js +21 -10
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/utils/NativeUpdatedValue.js +31 -13
- package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/module/utils/getTrackIdentity.js +20 -0
- package/lib/module/utils/getTrackIdentity.js.map +1 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +32 -24
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +144 -87
- package/lib/module/web/browser/BrowserManager.js.map +1 -1
- package/lib/module/web/browser/FavoriteManager.js +31 -21
- package/lib/module/web/browser/FavoriteManager.js.map +1 -1
- package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/module/web/browser/sections.js +83 -0
- package/lib/module/web/browser/sections.js.map +1 -0
- package/lib/module/web/player/NowPlayingManager.js +4 -3
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js +56 -11
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts +1 -0
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/browser.d.ts +2 -2
- package/lib/typescript/src/features/favorites.d.ts +13 -10
- package/lib/typescript/src/features/favorites.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/controls.d.ts +4 -2
- package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/rate.d.ts +8 -1
- package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +2 -3
- package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
- package/lib/typescript/src/features/player/options.d.ts +21 -19
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +5 -2
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-native.d.ts +2 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +361 -126
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +33 -53
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
- package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
- package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +16 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
- package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +33 -14
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +22 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +10 -7
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +55 -51
- package/nitrogen/generated/android/c++/JSection.hpp +120 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +23 -48
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +18 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +158 -82
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +25 -10
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +25 -10
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
- package/nitrogen/generated/ios/swift/Section.swift +100 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
- package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +20 -73
- package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
- package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
- package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
- package/nitrogen/generated/shared/c++/Section.hpp +106 -0
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
- package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +15 -31
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
- package/package.json +7 -2
- package/src/AudioBrowser.ts +1 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +11 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +2 -2
- package/src/features/favorites.ts +13 -10
- package/src/features/hook-wiring.test.ts +63 -15
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/progress.ts +2 -1
- package/src/features/playback/rate.ts +8 -1
- package/src/features/playback/state.ts +2 -3
- package/src/features/playback/trackPlaybackTime.ts +1 -1
- package/src/features/player/options.ts +27 -19
- package/src/features/player/setup.test.ts +12 -3
- package/src/features/player/setup.ts +3 -1
- package/src/features/player/validateOptions.ts +2 -0
- package/src/features/queue/activeTrack.ts +14 -3
- package/src/features/queue/queue.ts +11 -4
- package/src/nitro-stub-drift.test.ts +40 -0
- package/src/public-surface.test.ts +40 -0
- package/src/specs/audio-browser.nitro.ts +4 -3
- package/src/style-resolution.test.ts +210 -0
- package/src/types/browser-native.ts +2 -2
- package/src/types/browser-nodes.ts +377 -135
- package/src/types/browser.ts +34 -55
- package/src/utils/LazyNativeEmitter.ts +20 -10
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.ts +37 -22
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- package/src/web/browser/BrowserManager.ts +176 -95
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
- package/src/web/browser/assertBrowsePageShape.ts +5 -3
- package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
- package/src/web/browser/classifyTrackNavigation.ts +9 -9
- package/src/web/browser/parseSearchResponse.test.ts +8 -2
- package/src/web/browser/sections.test.ts +91 -0
- package/src/web/browser/sections.ts +72 -0
- package/src/web/player/NowPlayingManager.ts +4 -3
- package/src/web/util/BrowserPathHelper.test.ts +52 -0
- package/src/web/util/BrowserPathHelper.ts +61 -13
- package/website/guide/android-auto.md +114 -0
- package/website/guide/android-certificates.md +409 -0
- package/website/guide/artwork.md +255 -0
- package/website/guide/audio-output.md +135 -0
- package/website/guide/automotive.md +86 -0
- package/website/guide/basic-usage.md +151 -0
- package/website/guide/battery.md +115 -0
- package/website/guide/browser.md +581 -0
- package/website/guide/carplay.md +620 -0
- package/website/guide/configuration.md +173 -0
- package/website/guide/equalizer.md +231 -0
- package/website/guide/errors.md +310 -0
- package/website/guide/favorites.md +154 -0
- package/website/guide/gate.md +143 -0
- package/website/guide/getting-started.md +46 -0
- package/website/guide/hooks.md +238 -0
- package/website/guide/live-streams.md +125 -0
- package/website/guide/metadata.md +239 -0
- package/website/guide/migrating-from-track-player.md +244 -0
- package/website/guide/native-callback-fetch.md +127 -0
- package/website/guide/network.md +86 -0
- package/website/guide/now-playing.md +175 -0
- package/website/guide/playback.md +328 -0
- package/website/guide/queue.md +300 -0
- package/website/guide/remote-controls.md +256 -0
- package/website/guide/search.md +255 -0
- package/website/guide/sleep-timer.md +268 -0
- package/website/guide/track.md +246 -0
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
- package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
- package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
- package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
- package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
|
@@ -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
|
}
|
|
@@ -4,7 +4,13 @@ import android.content.Context
|
|
|
4
4
|
import android.content.SharedPreferences
|
|
5
5
|
import androidx.core.content.edit
|
|
6
6
|
import androidx.media3.common.C
|
|
7
|
+
import com.audiobrowser.util.EMPTY_TRACK_STYLE
|
|
7
8
|
import com.audiobrowser.util.artworkOf
|
|
9
|
+
import com.audiobrowser.util.toArtworkRendering
|
|
10
|
+
import com.audiobrowser.util.toCardImage
|
|
11
|
+
import com.audiobrowser.util.toImageShape
|
|
12
|
+
import com.audiobrowser.util.toStyleDisplay
|
|
13
|
+
import com.audiobrowser.util.toWireString
|
|
8
14
|
import com.audiobrowser.util.url
|
|
9
15
|
import com.margelo.nitro.audiobrowser.PlaybackState
|
|
10
16
|
import com.margelo.nitro.audiobrowser.RepeatMode
|
|
@@ -18,12 +24,12 @@ import org.json.JSONObject
|
|
|
18
24
|
import timber.log.Timber
|
|
19
25
|
|
|
20
26
|
/**
|
|
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.).
|
|
27
|
+
* Persists playback state for resumption after app restart. Stores the current track (including its
|
|
28
|
+
* path), position, and player settings to enable playback resumption via MediaButtonReceiver
|
|
29
|
+
* (Bluetooth play button, etc.).
|
|
24
30
|
*
|
|
25
31
|
* Settings (repeatMode, shuffleEnabled, playbackSpeed) are auto-persisted when set via properties.
|
|
26
|
-
* Position/
|
|
32
|
+
* Position/track are persisted via save() on pause or track change.
|
|
27
33
|
*/
|
|
28
34
|
class PlaybackStateStore(private val player: Player) {
|
|
29
35
|
private val prefs: SharedPreferences =
|
|
@@ -32,7 +38,7 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
32
38
|
/**
|
|
33
39
|
* Persisted playback state for resumption.
|
|
34
40
|
*
|
|
35
|
-
* @param track The track being played (includes
|
|
41
|
+
* @param track The track being played (includes path for queue expansion)
|
|
36
42
|
* @param positionMs The playback position in milliseconds (C.TIME_UNSET for default/live edge)
|
|
37
43
|
* @param repeatMode The repeat mode setting
|
|
38
44
|
* @param shuffleEnabled Whether shuffle mode was enabled
|
|
@@ -148,78 +154,11 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
148
154
|
Timber.d("Saved position: positionMs=0 (queue ended)")
|
|
149
155
|
}
|
|
150
156
|
|
|
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
157
|
/**
|
|
219
158
|
* Restores player settings from persisted state and returns the state for queue setup.
|
|
220
159
|
*
|
|
221
160
|
* Applies repeatMode, shuffleMode, and playbackSpeed to the player. The caller is responsible for
|
|
222
|
-
* using the returned
|
|
161
|
+
* using the returned path and positionMs to set up the playback queue.
|
|
223
162
|
*
|
|
224
163
|
* @return PersistedState if available, null otherwise
|
|
225
164
|
*/
|
|
@@ -228,7 +167,11 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
228
167
|
// Apply player settings from persisted state
|
|
229
168
|
player.repeatMode = state.repeatMode
|
|
230
169
|
player.shuffleMode = state.shuffleEnabled
|
|
231
|
-
|
|
170
|
+
// Guarded because this input is untrusted: ExoPlayer's setPlaybackSpeed rejects a
|
|
171
|
+
// non-positive speed with IllegalArgumentException, and a snapshot holding one (a
|
|
172
|
+
// corrupted prefs file — the normal write path can't produce it) would then throw on
|
|
173
|
+
// every resume, permanently, since the bad value stays persisted.
|
|
174
|
+
if (state.playbackSpeed > 0f) player.playbackSpeed = state.playbackSpeed
|
|
232
175
|
|
|
233
176
|
Timber.d(
|
|
234
177
|
"Restored player settings: repeatMode=${state.repeatMode}, shuffle=${state.shuffleEnabled}, speed=${state.playbackSpeed}"
|
|
@@ -255,6 +198,98 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
255
198
|
}
|
|
256
199
|
|
|
257
200
|
companion object {
|
|
201
|
+
// Internal (not private) so the round-trip is testable without a Player.
|
|
202
|
+
internal fun trackToJson(track: Track): String =
|
|
203
|
+
JSONObject()
|
|
204
|
+
.apply {
|
|
205
|
+
put("id", track.id)
|
|
206
|
+
put("path", track.path)
|
|
207
|
+
put("src", track.src)
|
|
208
|
+
put("title", track.title)
|
|
209
|
+
put("subtitle", track.subtitle)
|
|
210
|
+
put("artist", track.artist)
|
|
211
|
+
put("albumPath", track.albumPath)
|
|
212
|
+
put("album", track.album)
|
|
213
|
+
put("artwork", track.artwork?.url)
|
|
214
|
+
put("description", track.description)
|
|
215
|
+
put("genre", track.genre)
|
|
216
|
+
put("duration", track.duration)
|
|
217
|
+
// The style block persists as its wire shape (nested object, wire
|
|
218
|
+
// strings) and `disabled` beside it: a restored queue keeps its
|
|
219
|
+
// visible fidelity across process death (ADR 0011).
|
|
220
|
+
put(
|
|
221
|
+
"style",
|
|
222
|
+
track.style?.let { style ->
|
|
223
|
+
JSONObject().apply {
|
|
224
|
+
put("display", style.display?.toWireString())
|
|
225
|
+
put("artworkRendering", style.artworkRendering?.toWireString())
|
|
226
|
+
put("imageShape", style.imageShape?.toWireString())
|
|
227
|
+
put("accessorySymbol", style.accessorySymbol)
|
|
228
|
+
put("cardTint", style.cardTint)
|
|
229
|
+
put("cardImage", style.cardImage?.toWireString())
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
put("disabled", track.disabled)
|
|
234
|
+
put("favorited", track.favorited)
|
|
235
|
+
put("live", track.live)
|
|
236
|
+
}
|
|
237
|
+
.toString()
|
|
238
|
+
|
|
239
|
+
internal fun trackFromJson(json: String): Track? =
|
|
240
|
+
runCatching {
|
|
241
|
+
val obj = JSONObject(json)
|
|
242
|
+
Track(
|
|
243
|
+
id = obj.optString("id").takeIf { it.isNotEmpty() },
|
|
244
|
+
path = obj.optString("path").takeIf { it.isNotEmpty() },
|
|
245
|
+
src = obj.optString("src").takeIf { it.isNotEmpty() },
|
|
246
|
+
title = obj.getString("title"),
|
|
247
|
+
subtitle = obj.optString("subtitle").takeIf { it.isNotEmpty() },
|
|
248
|
+
artist = obj.optString("artist").takeIf { it.isNotEmpty() },
|
|
249
|
+
albumPath = obj.optString("albumPath").takeIf { it.isNotEmpty() },
|
|
250
|
+
album = obj.optString("album").takeIf { it.isNotEmpty() },
|
|
251
|
+
artwork = artworkOf(obj.optString("artwork").takeIf { it.isNotEmpty() }),
|
|
252
|
+
artworkSource = null, // Not persisted - will be re-transformed on browse
|
|
253
|
+
description = obj.optString("description").takeIf { it.isNotEmpty() },
|
|
254
|
+
genre = obj.optString("genre").takeIf { it.isNotEmpty() },
|
|
255
|
+
duration =
|
|
256
|
+
if (obj.has("duration") && !obj.isNull("duration")) obj.getDouble("duration")
|
|
257
|
+
else null,
|
|
258
|
+
// Tolerant (ADR 0011): a snapshot written before the block model —
|
|
259
|
+
// `style` as an enum-name string — is not an object, so optJSONObject
|
|
260
|
+
// yields null and the track restores without a declaration.
|
|
261
|
+
style =
|
|
262
|
+
obj
|
|
263
|
+
.optJSONObject("style")
|
|
264
|
+
?.let { style ->
|
|
265
|
+
TrackStyle(
|
|
266
|
+
display = style.optString("display").toStyleDisplay(),
|
|
267
|
+
artworkRendering = style.optString("artworkRendering").toArtworkRendering(),
|
|
268
|
+
imageShape = style.optString("imageShape").toImageShape(),
|
|
269
|
+
accessorySymbol = style.optString("accessorySymbol").takeIf { it.isNotEmpty() },
|
|
270
|
+
cardTint = style.optString("cardTint").takeIf { it.isNotEmpty() },
|
|
271
|
+
cardImage = style.optString("cardImage").toCardImage(),
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
// An empty block collapses to "no declaration" — one shape on
|
|
275
|
+
// every platform, matching the browse decoders.
|
|
276
|
+
?.takeIf { it != EMPTY_TRACK_STYLE },
|
|
277
|
+
disabled =
|
|
278
|
+
if (obj.has("disabled") && !obj.isNull("disabled")) obj.getBoolean("disabled")
|
|
279
|
+
else null,
|
|
280
|
+
favorited =
|
|
281
|
+
if (obj.has("favorited") && !obj.isNull("favorited")) obj.getBoolean("favorited")
|
|
282
|
+
else null,
|
|
283
|
+
live = if (obj.has("live") && !obj.isNull("live")) obj.getBoolean("live") else null,
|
|
284
|
+
// Not persisted: on resumption the contextual path is re-browsed
|
|
285
|
+
// (expandQueueFromContextualPath), which re-parses each track's request
|
|
286
|
+
// from the API's current response and re-caches it.
|
|
287
|
+
request = null,
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
.onFailure { e -> Timber.w(e, "Failed to parse persisted track JSON") }
|
|
291
|
+
.getOrNull()
|
|
292
|
+
|
|
258
293
|
private const val PREFS_NAME = "audio_browser_playback_state_v3"
|
|
259
294
|
private const val KEY_POSITION_MS = "position_ms"
|
|
260
295
|
private const val KEY_REPEAT_MODE = "repeat_mode"
|
|
@@ -20,6 +20,7 @@ import com.audiobrowser.browser.displayArtworkSource
|
|
|
20
20
|
import com.audiobrowser.browser.resolveArtworkUrl
|
|
21
21
|
import com.audiobrowser.browser.unattributedArtworkSource
|
|
22
22
|
import com.audiobrowser.extension.NumberExt.Companion.toSeconds
|
|
23
|
+
import com.audiobrowser.extension.identity
|
|
23
24
|
import com.audiobrowser.model.PlayerSetupOptions
|
|
24
25
|
import com.audiobrowser.model.PlayerUpdateOptions
|
|
25
26
|
import com.audiobrowser.util.CoilBitmapLoader
|
|
@@ -36,7 +37,6 @@ import com.margelo.nitro.audiobrowser.ImageSource
|
|
|
36
37
|
import com.margelo.nitro.audiobrowser.NativeGateRequest
|
|
37
38
|
import com.margelo.nitro.audiobrowser.NowPlayingMetadata
|
|
38
39
|
import com.margelo.nitro.audiobrowser.Playback
|
|
39
|
-
import com.margelo.nitro.audiobrowser.PlaybackActiveTrackChangedEvent
|
|
40
40
|
import com.margelo.nitro.audiobrowser.PlaybackError
|
|
41
41
|
import com.margelo.nitro.audiobrowser.PlaybackPlayWhenReadyChangedEvent
|
|
42
42
|
import com.margelo.nitro.audiobrowser.PlaybackProgressUpdatedEvent
|
|
@@ -351,7 +351,7 @@ class Player(internal val context: Context) {
|
|
|
351
351
|
get() = exoPlayer.nextMediaItemIndex == C.INDEX_UNSET
|
|
352
352
|
|
|
353
353
|
/**
|
|
354
|
-
* The source path from which the current queue was expanded (e.g., from a contextual
|
|
354
|
+
* The source path from which the current queue was expanded (e.g., from a contextual path). Used
|
|
355
355
|
* to avoid re-expanding the queue when selecting tracks from the same source.
|
|
356
356
|
*/
|
|
357
357
|
var queueSourcePath: String? = null
|
|
@@ -657,7 +657,7 @@ class Player(internal val context: Context) {
|
|
|
657
657
|
* @param tracks The tracks to set as the new queue.
|
|
658
658
|
* @param startIndex The index to start playback from (default: 0).
|
|
659
659
|
* @param startPositionMs The position in milliseconds to start from (default: 0).
|
|
660
|
-
* @param sourcePath Optional path from which this queue was expanded (for contextual
|
|
660
|
+
* @param sourcePath Optional path from which this queue was expanded (for contextual path
|
|
661
661
|
* optimization).
|
|
662
662
|
*/
|
|
663
663
|
fun setQueue(
|
|
@@ -668,6 +668,9 @@ class Player(internal val context: Context) {
|
|
|
668
668
|
) {
|
|
669
669
|
val mediaItems = TrackFactory.toMedia3(tracks).toMutableList()
|
|
670
670
|
exoPlayer.setMediaItems(mediaItems, startIndex, startPositionMs)
|
|
671
|
+
if (mediaItems.isNotEmpty()) {
|
|
672
|
+
exoPlayer.setShuffleOrder(shuffleOrderLedBy(startIndex, mediaItems.size))
|
|
673
|
+
}
|
|
671
674
|
queueSourcePath = sourcePath
|
|
672
675
|
exoPlayer.prepare()
|
|
673
676
|
}
|
|
@@ -685,17 +688,27 @@ class Player(internal val context: Context) {
|
|
|
685
688
|
/**
|
|
686
689
|
* Sets the favorited state of the currently playing track. Updates the heart icon in media
|
|
687
690
|
* controllers without interrupting playback.
|
|
691
|
+
*
|
|
692
|
+
* @return whether the gesture applied — false only when there is no current track (nothing to
|
|
693
|
+
* favorite), so MediaSession callers can report an invalid state instead of a success the
|
|
694
|
+
* controller then watches revert.
|
|
688
695
|
*/
|
|
689
|
-
fun setActiveTrackFavorited(favorited: Boolean) {
|
|
690
|
-
val currentTrack = this.currentTrack ?: return
|
|
696
|
+
fun setActiveTrackFavorited(favorited: Boolean): Boolean {
|
|
697
|
+
val currentTrack = this.currentTrack ?: return false
|
|
691
698
|
|
|
692
699
|
// Persist to the native favorites cache regardless of player state — the
|
|
693
700
|
// user's gesture is durable even when there's no active media item to
|
|
694
|
-
// update (queue tear-down, between-track gaps, etc.).
|
|
695
|
-
|
|
701
|
+
// update (queue tear-down, between-track gaps, etc.). Keyed by the track
|
|
702
|
+
// identity (id when non-blank, else src).
|
|
703
|
+
currentTrack.identity?.let { identity ->
|
|
704
|
+
browser?.browserManager?.updateFavorite(identity, favorited)
|
|
705
|
+
}
|
|
696
706
|
|
|
707
|
+
// Cache written above: the gesture is durably recorded even without a media
|
|
708
|
+
// item to restamp (queue tear-down, between-track gaps) — that counts as
|
|
709
|
+
// applied; there is also no visible heart to revert in that state.
|
|
697
710
|
val index = exoPlayer.currentMediaItemIndex
|
|
698
|
-
if (index == C.INDEX_UNSET) return
|
|
711
|
+
if (index == C.INDEX_UNSET) return true
|
|
699
712
|
|
|
700
713
|
// Create updated Track with new favorited state
|
|
701
714
|
val updatedTrack = currentTrack.copy(favorited = favorited)
|
|
@@ -719,28 +732,23 @@ class Player(internal val context: Context) {
|
|
|
719
732
|
// Update the heart button icon in notification/Android Auto
|
|
720
733
|
updateFavoriteButtonState(favorited)
|
|
721
734
|
|
|
722
|
-
// Notify JS of the favorite state change
|
|
735
|
+
// Notify JS of the favorite state change. This is the ONLY emit: a favorite
|
|
736
|
+
// toggle is an in-place mutation of the active track, not a transition —
|
|
737
|
+
// onPlaybackActiveTrackChanged / onPlaybackQueueChanged stay
|
|
738
|
+
// transition-only, and the useActiveTrack / useQueue hooks subscribe to
|
|
739
|
+
// onFavoriteChanged themselves.
|
|
723
740
|
callbacks?.onFavoriteChanged(FavoriteChangedEvent(updatedTrack, favorited))
|
|
724
|
-
|
|
725
|
-
// Emit active track changed so useActiveTrack() hook updates
|
|
726
|
-
val activeTrackEvent =
|
|
727
|
-
PlaybackActiveTrackChangedEvent(
|
|
728
|
-
lastIndex = index.toDouble(),
|
|
729
|
-
lastTrack = currentTrack,
|
|
730
|
-
lastPosition = exoPlayer.currentPosition.toSeconds(),
|
|
731
|
-
index = index.toDouble(),
|
|
732
|
-
track = updatedTrack,
|
|
733
|
-
)
|
|
734
|
-
callbacks?.onPlaybackActiveTrackChanged(activeTrackEvent)
|
|
735
|
-
|
|
736
|
-
// Emit queue changed so useQueue() hook updates
|
|
737
|
-
callbacks?.onPlaybackQueueChanged(tracks)
|
|
741
|
+
return true
|
|
738
742
|
}
|
|
739
743
|
|
|
740
|
-
/**
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
+
/**
|
|
745
|
+
* Toggles the favorited state of the currently playing track.
|
|
746
|
+
*
|
|
747
|
+
* @return whether the toggle applied — false when there is no current track.
|
|
748
|
+
*/
|
|
749
|
+
fun toggleActiveTrackFavorited(): Boolean {
|
|
750
|
+
val currentTrack = this.currentTrack ?: return false
|
|
751
|
+
return setActiveTrackFavorited(currentTrack.favorited != true)
|
|
744
752
|
}
|
|
745
753
|
|
|
746
754
|
// MARK: - Now Playing Metadata
|
|
@@ -926,10 +934,23 @@ class Player(internal val context: Context) {
|
|
|
926
934
|
}
|
|
927
935
|
}
|
|
928
936
|
|
|
929
|
-
/**
|
|
937
|
+
/**
|
|
938
|
+
* Jump to the live edge of the current track; no-op unless it declares `live` — the track's own
|
|
939
|
+
* declaration is the gate (matching iOS and the spec), not ExoPlayer's stream-derived liveness,
|
|
940
|
+
* which never marks progressive ICY radio live. A stream with a live window seeks to its edge;
|
|
941
|
+
* anything else reconnects — for a non-seekable stream a fresh connection IS the live edge. A
|
|
942
|
+
* stale URL recovers through the failure-armed re-resolve in [TransformingDataSource].
|
|
943
|
+
*/
|
|
930
944
|
fun seekToLiveEdge() {
|
|
945
|
+
if (currentTrack?.live != true) return
|
|
931
946
|
if (exoPlayer.isCurrentMediaItemLive) {
|
|
932
947
|
exoPlayer.seekToDefaultPosition()
|
|
948
|
+
} else {
|
|
949
|
+
// ExoPlayer.prepare() only re-prepares from STATE_IDLE, hence the raw stop() — not
|
|
950
|
+
// Player.stop(), which would drop the play intent this reconnect must preserve.
|
|
951
|
+
exoPlayer.stop()
|
|
952
|
+
exoPlayer.seekToDefaultPosition()
|
|
953
|
+
exoPlayer.prepare()
|
|
933
954
|
}
|
|
934
955
|
}
|
|
935
956
|
|
|
@@ -1249,8 +1270,8 @@ class Player(internal val context: Context) {
|
|
|
1249
1270
|
|
|
1250
1271
|
if (capabilitiesChanged) {
|
|
1251
1272
|
// The `favorite` capability is the single favoriting switch — propagate
|
|
1252
|
-
//
|
|
1253
|
-
browser?.browserManager?.
|
|
1273
|
+
// it to the browser so it can hydrate row hearts.
|
|
1274
|
+
browser?.browserManager?.setFavoriteEnabled(options.capabilities.favoriteEnabled)
|
|
1254
1275
|
}
|
|
1255
1276
|
|
|
1256
1277
|
if (progressUpdateEventIntervalChanged) {
|
|
@@ -1,20 +1,7 @@
|
|
|
1
1
|
package com.audiobrowser.player
|
|
2
2
|
|
|
3
|
-
import com.margelo.nitro.audiobrowser.FavoritesMatchMode
|
|
4
3
|
import com.margelo.nitro.audiobrowser.PlayerCapabilities
|
|
5
4
|
|
|
6
|
-
/**
|
|
7
|
-
* Resolved favorite match mode, or null when favoriting is disabled.
|
|
8
|
-
*
|
|
9
|
-
* `false`/unset → null; `true` → EXACT; `{ match }` → that match.
|
|
10
|
-
*/
|
|
11
|
-
val PlayerCapabilities.favoriteMatch: FavoritesMatchMode?
|
|
12
|
-
get() =
|
|
13
|
-
favorite?.match(
|
|
14
|
-
first = { enabled -> if (enabled) FavoritesMatchMode.EXACT else null },
|
|
15
|
-
second = { config -> config.match },
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
/** Whether the favorite/like control is enabled. */
|
|
5
|
+
/** Whether the favorite/like control is enabled. Opt-in: only an explicit `true` enables it. */
|
|
19
6
|
val PlayerCapabilities.favoriteEnabled: Boolean
|
|
20
|
-
get() =
|
|
7
|
+
get() = favorite == true
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.audiobrowser.player
|
|
2
|
+
|
|
3
|
+
import androidx.media3.exoplayer.source.ShuffleOrder
|
|
4
|
+
import kotlin.random.Random
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A shuffle order over [count] items with [startIndex] pinned to shuffle position 0.
|
|
8
|
+
*
|
|
9
|
+
* Media3 does not do this for us. `ExoPlayerImpl.setMediaSources` hands the start index to
|
|
10
|
+
* `ShuffleOrder.cloneAndSet(insertionCount, startIndex)`, but that is an interface default of
|
|
11
|
+
* `cloneAndClear().cloneAndInsert(0, insertionCount)` — the start index is discarded — and
|
|
12
|
+
* `DefaultShuffleOrder` does not override it (verified against the pinned media3 build). So with
|
|
13
|
+
* shuffle on, the track playback starts from lands at a random position in the order, and a 1-in-N
|
|
14
|
+
* share of queues put it last: `nextMediaItemIndex` is then `INDEX_UNSET` from the very first
|
|
15
|
+
* track, so auto-advance ends the queue after one track and fires `onQueueEnded`. The
|
|
16
|
+
* voice-assistant cold resume expands a queue exactly this way, so it sits squarely on that path.
|
|
17
|
+
*
|
|
18
|
+
* Applied unconditionally by `Player.setQueue`, like the Swift twin's `adoptInitialCurrent`:
|
|
19
|
+
* enabling shuffle later does not regenerate the order, so it has to be sane whether or not shuffle
|
|
20
|
+
* is on right now.
|
|
21
|
+
*/
|
|
22
|
+
internal fun shuffleOrderLedBy(startIndex: Int, count: Int, random: Random = Random): ShuffleOrder {
|
|
23
|
+
val first = startIndex.coerceIn(0, (count - 1).coerceAtLeast(0))
|
|
24
|
+
val indices = IntArray(count)
|
|
25
|
+
if (count > 0) {
|
|
26
|
+
indices[0] = first
|
|
27
|
+
((0 until count) - first).shuffled().forEachIndexed { i, index -> indices[i + 1] = index }
|
|
28
|
+
}
|
|
29
|
+
return ShuffleOrder.DefaultShuffleOrder(indices, random.nextLong())
|
|
30
|
+
}
|