react-native-audio-browser 0.2.0-next.10 → 0.2.0-next.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/android/build.gradle +6 -5
- package/android/gradle.properties +1 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +49 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -7
- package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
- package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +93 -73
- package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +46 -5
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +86 -27
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +20 -6
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +249 -86
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +35 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +69 -30
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +345 -8
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
- package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +152 -8
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +218 -10
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
- package/ios/Browser/BrowserConfig.swift +102 -5
- package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
- package/ios/Browser/BrowserManager.swift +131 -143
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/JsonModels.swift +187 -106
- package/ios/Browser/SearchDrillIn.swift +6 -6
- package/ios/Browser/SectionPresentation.swift +34 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +47 -0
- package/ios/Browser/TabBarEntries.swift +2 -2
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +105 -67
- package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
- package/ios/CarPlay/CarPlayListItemFactory.swift +217 -84
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
- package/ios/Extension/Section+Copying.swift +31 -0
- package/ios/Extension/Track+Copying.swift +6 -12
- package/ios/Extension/Track+Identity.swift +18 -0
- package/ios/HybridAudioBrowser.swift +113 -35
- package/ios/Model/NitroTypeStubs.swift +64 -23
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Player/NowPlayingUpdater.swift +1 -1
- package/ios/Player/PlaybackCoordinator.swift +2 -2
- package/ios/Player/RetryManager.swift +4 -3
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/JsonModelsTests.swift +75 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/QueueManagerTests.swift +4 -4
- package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +61 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +67 -0
- package/ios/Tests/TabBarEntriesTests.swift +18 -18
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- package/lib/commonjs/AudioBrowser.js +11 -0
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/features/browser-config.js +11 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +2 -2
- package/lib/commonjs/features/favorites.js +13 -10
- package/lib/commonjs/features/favorites.js.map +1 -1
- package/lib/commonjs/features/playback/controls.js +4 -2
- package/lib/commonjs/features/playback/controls.js.map +1 -1
- package/lib/commonjs/features/playback/state.js +2 -3
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +3 -1
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +2 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -1
- package/lib/commonjs/features/queue/activeTrack.js +14 -3
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +11 -4
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
- package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/commonjs/utils/getTrackIdentity.js +24 -0
- package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +144 -87
- package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
- package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
- package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
- package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/commonjs/web/browser/sections.js +89 -0
- package/lib/commonjs/web/browser/sections.js.map +1 -0
- package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/AudioBrowser.js +1 -0
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/features/browser-config.js +11 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +2 -2
- package/lib/module/features/favorites.js +13 -10
- package/lib/module/features/favorites.js.map +1 -1
- package/lib/module/features/playback/controls.js +4 -2
- package/lib/module/features/playback/controls.js.map +1 -1
- package/lib/module/features/playback/state.js +2 -3
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +3 -1
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +2 -0
- package/lib/module/features/player/validateOptions.js.map +1 -1
- package/lib/module/features/queue/activeTrack.js +14 -3
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +11 -4
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/utils/LazyNativeEmitter.js +21 -10
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/utils/NativeUpdatedValue.js +31 -13
- package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/module/utils/getTrackIdentity.js +20 -0
- package/lib/module/utils/getTrackIdentity.js.map +1 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +32 -24
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +144 -87
- package/lib/module/web/browser/BrowserManager.js.map +1 -1
- package/lib/module/web/browser/FavoriteManager.js +31 -21
- package/lib/module/web/browser/FavoriteManager.js.map +1 -1
- package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/module/web/browser/sections.js +83 -0
- package/lib/module/web/browser/sections.js.map +1 -0
- package/lib/module/web/player/NowPlayingManager.js +4 -3
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js +56 -11
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts +1 -0
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/browser.d.ts +2 -2
- package/lib/typescript/src/features/favorites.d.ts +13 -10
- package/lib/typescript/src/features/favorites.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/controls.d.ts +4 -2
- package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +2 -3
- package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
- package/lib/typescript/src/features/player/options.d.ts +21 -19
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +5 -2
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-native.d.ts +2 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +260 -127
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +33 -53
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
- package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
- package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +10 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -7
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
- package/nitrogen/generated/android/c++/JSection.hpp +114 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +23 -5
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
- package/nitrogen/generated/ios/swift/Section.swift +100 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +64 -0
- package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +20 -73
- package/nitrogen/generated/ios/swift/TrackStyle.swift +28 -22
- package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
- package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
- package/nitrogen/generated/shared/c++/Section.hpp +106 -0
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +96 -0
- package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +15 -31
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +50 -34
- package/package.json +3 -2
- package/src/AudioBrowser.ts +1 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +11 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +2 -2
- package/src/features/favorites.ts +13 -10
- package/src/features/hook-wiring.test.ts +63 -15
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/state.ts +2 -3
- package/src/features/playback/trackPlaybackTime.ts +1 -1
- package/src/features/player/options.ts +27 -19
- package/src/features/player/setup.test.ts +12 -3
- package/src/features/player/setup.ts +3 -1
- package/src/features/player/validateOptions.ts +2 -0
- package/src/features/queue/activeTrack.ts +14 -3
- package/src/features/queue/queue.ts +11 -4
- package/src/nitro-stub-drift.test.ts +277 -0
- package/src/public-surface.test.ts +40 -0
- package/src/specs/audio-browser.nitro.ts +4 -3
- package/src/style-resolution.test.ts +139 -0
- package/src/types/browser-native.ts +2 -2
- package/src/types/browser-nodes.ts +268 -136
- package/src/types/browser.ts +34 -55
- package/src/utils/LazyNativeEmitter.ts +20 -10
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.ts +37 -22
- package/src/web/browser/BrowserManager.ts +176 -95
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
- package/src/web/browser/assertBrowsePageShape.ts +5 -3
- package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
- package/src/web/browser/classifyTrackNavigation.ts +9 -9
- package/src/web/browser/parseSearchResponse.test.ts +8 -2
- package/src/web/browser/sections.test.ts +91 -0
- package/src/web/browser/sections.ts +72 -0
- package/src/web/player/NowPlayingManager.ts +4 -3
- package/src/web/util/BrowserPathHelper.test.ts +52 -0
- package/src/web/util/BrowserPathHelper.ts +61 -13
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
- package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
- package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
- package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
- package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
|
@@ -11,49 +11,43 @@ extension Track {
|
|
|
11
11
|
/// `title` uses single-optional (`String?`) since it is non-optional on Track.
|
|
12
12
|
func copying(
|
|
13
13
|
id: String?? = nil,
|
|
14
|
-
|
|
14
|
+
path: String?? = nil,
|
|
15
15
|
src: String?? = nil,
|
|
16
16
|
artwork: Variant_String_ArtworkVariants?? = nil,
|
|
17
17
|
artworkSource: ImageSource?? = nil,
|
|
18
18
|
request: TrackRequest?? = nil,
|
|
19
|
-
artworkCarPlayTinted: Bool?? = nil,
|
|
20
19
|
title: String? = nil,
|
|
21
20
|
subtitle: String?? = nil,
|
|
22
21
|
artist: String?? = nil,
|
|
23
|
-
|
|
22
|
+
albumPath: String?? = nil,
|
|
24
23
|
album: String?? = nil,
|
|
25
24
|
description: String?? = nil,
|
|
26
25
|
genre: String?? = nil,
|
|
27
26
|
duration: Double?? = nil,
|
|
28
27
|
style: TrackStyle?? = nil,
|
|
29
|
-
|
|
28
|
+
disabled: Bool?? = nil,
|
|
30
29
|
favorited: Bool?? = nil,
|
|
31
|
-
groupTitle: String?? = nil,
|
|
32
30
|
live: Bool?? = nil,
|
|
33
|
-
imageRow: [ImageRowItem]?? = nil,
|
|
34
31
|
) -> Track {
|
|
35
32
|
Track(
|
|
36
33
|
id: id ?? self.id,
|
|
37
|
-
|
|
34
|
+
path: path ?? self.path,
|
|
38
35
|
src: src ?? self.src,
|
|
39
36
|
artwork: artwork ?? self.artwork,
|
|
40
37
|
artworkSource: artworkSource ?? self.artworkSource,
|
|
41
38
|
request: request ?? self.request,
|
|
42
|
-
artworkCarPlayTinted: artworkCarPlayTinted ?? self.artworkCarPlayTinted,
|
|
43
39
|
title: title ?? self.title,
|
|
44
40
|
subtitle: subtitle ?? self.subtitle,
|
|
45
41
|
artist: artist ?? self.artist,
|
|
46
|
-
|
|
42
|
+
albumPath: albumPath ?? self.albumPath,
|
|
47
43
|
album: album ?? self.album,
|
|
48
44
|
description: description ?? self.description,
|
|
49
45
|
genre: genre ?? self.genre,
|
|
50
46
|
duration: duration ?? self.duration,
|
|
51
47
|
style: style ?? self.style,
|
|
52
|
-
|
|
48
|
+
disabled: disabled ?? self.disabled,
|
|
53
49
|
favorited: favorited ?? self.favorited,
|
|
54
|
-
groupTitle: groupTitle ?? self.groupTitle,
|
|
55
50
|
live: live ?? self.live,
|
|
56
|
-
imageRow: imageRow ?? self.imageRow,
|
|
57
51
|
)
|
|
58
52
|
}
|
|
59
53
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#if canImport(NitroModules)
|
|
2
|
+
import NitroModules
|
|
3
|
+
#endif
|
|
4
|
+
|
|
5
|
+
/// A track's identity: the opaque `id` when present (non-blank), else the
|
|
6
|
+
/// playable `src`. Two tracks refer to the same item iff their identities are
|
|
7
|
+
/// equal. Browsable-only tracks (neither `id` nor `src`) have no identity —
|
|
8
|
+
/// they are addressed by `path` instead.
|
|
9
|
+
///
|
|
10
|
+
/// This is THE comparison rule for favorites matching, section scoping,
|
|
11
|
+
/// skip-in-place, the car now-playing row indicator, and the contextual
|
|
12
|
+
/// `__trackId` — see ADR 0008. Mirrors `trackIdentity` on the TS side.
|
|
13
|
+
extension Track {
|
|
14
|
+
var identity: String? {
|
|
15
|
+
if let id, !id.isEmpty { return id }
|
|
16
|
+
return src
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -34,6 +34,24 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
34
34
|
/// silent.
|
|
35
35
|
static let instanceChangedEmitter = Emitter<Void>()
|
|
36
36
|
|
|
37
|
+
/// Installed once, on first instance creation. React Native posts this
|
|
38
|
+
/// notification (from `RCTReloadCommand`) when a JS reload — dev or
|
|
39
|
+
/// OTA-update restart — is triggered, *before* the old runtime is destroyed:
|
|
40
|
+
/// the earliest signal that this instance's JS callbacks are about to go
|
|
41
|
+
/// stale. `init` of the replacement instance drops them too, but only after
|
|
42
|
+
/// the new runtime is up; this observer closes the window in between.
|
|
43
|
+
/// Observed by name so the library needs no React-Core dependency (and
|
|
44
|
+
/// stays inert under SPM tests, where nothing posts it).
|
|
45
|
+
@MainActor private static let reloadObserver: NSObjectProtocol = NotificationCenter.default.addObserver(
|
|
46
|
+
forName: Notification.Name("RCTTriggerReloadCommandNotification"),
|
|
47
|
+
object: nil,
|
|
48
|
+
queue: .main,
|
|
49
|
+
) { _ in
|
|
50
|
+
MainActor.assumeIsolated {
|
|
51
|
+
HybridAudioBrowser.shared?.dropStaleJSCallbacks()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
// MARK: - Private Properties
|
|
38
56
|
|
|
39
57
|
/// Main-confined by convention, not annotation: `deinit` reads it as a
|
|
@@ -70,6 +88,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
70
88
|
playerOptions.carPlayNowPlayingButtons
|
|
71
89
|
}
|
|
72
90
|
|
|
91
|
+
@MainActor var carPlayPlayingIndicatorLocation: CarPlayPlayingIndicatorLocation {
|
|
92
|
+
playerOptions.carPlayPlayingIndicatorLocation
|
|
93
|
+
}
|
|
94
|
+
|
|
73
95
|
/// Written from the JS thread, the cooperative pool and MainActor, with a
|
|
74
96
|
/// `didSet` that calls back into Nitro. MainActor-isolated so the compiler
|
|
75
97
|
/// requires an `onMainActor` hop at every access.
|
|
@@ -164,7 +186,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
164
186
|
public var configuration: NativeBrowserConfiguration = .init(
|
|
165
187
|
path: nil, request: nil, requestResolver: nil, browse: nil, browseResolver: nil, media: nil, artwork: nil, nowPlayingArtwork: nil, routes: nil,
|
|
166
188
|
singleTrack: nil, handleTrackLoad: nil,
|
|
167
|
-
androidControllerOfflineError: nil, carPlayLoadingTitle: nil,
|
|
189
|
+
androidControllerOfflineError: nil, carPlayLoadingTitle: nil, resolveAlbumPath: nil, formatNavigationError: nil,
|
|
168
190
|
) {
|
|
169
191
|
didSet {
|
|
170
192
|
// Copied here, on the setter's thread: the Task below outlives this call,
|
|
@@ -181,7 +203,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
181
203
|
Task {
|
|
182
204
|
let tabs = try? await browserManager.queryTabs()
|
|
183
205
|
// Navigate to configured path, first tab, or "/"
|
|
184
|
-
let initialPath = configuredPath ?? tabs?.first?.
|
|
206
|
+
let initialPath = configuredPath ?? tabs?.first?.path ?? "/"
|
|
185
207
|
// Clear error before navigation (matches Kotlin clearNavigationError())
|
|
186
208
|
clearNavigationError()
|
|
187
209
|
do {
|
|
@@ -213,8 +235,14 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
213
235
|
/// state, e.g. CarPlay re-seeding `isGated` at scene connect). Serving content
|
|
214
236
|
/// in that window would be a fail-open leak — same direction as the
|
|
215
237
|
/// resolver-error path in `gateDecision`. Matches Android's default.
|
|
216
|
-
public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> =
|
|
217
|
-
|
|
238
|
+
public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = HybridAudioBrowser.defaultResolveGate
|
|
239
|
+
|
|
240
|
+
// Computed, not stored: a nonisolated static holding a non-Sendable closure
|
|
241
|
+
// is rejected under Swift 6 concurrency checking.
|
|
242
|
+
static var defaultResolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> {
|
|
243
|
+
{ _ in
|
|
244
|
+
Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
|
|
245
|
+
}
|
|
218
246
|
}
|
|
219
247
|
|
|
220
248
|
/// Fired when a request is gated (served the gate). Set by JS.
|
|
@@ -306,7 +334,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
306
334
|
// playerAndConfiguredBrowser OnceValue, so we must explicitly stop
|
|
307
335
|
// its player to prevent two audio streams running simultaneously.
|
|
308
336
|
onMainActor {
|
|
337
|
+
// Statics are lazy — touch the observer so it's installed from the first
|
|
338
|
+
// instance onward.
|
|
339
|
+
_ = HybridAudioBrowser.reloadObserver
|
|
309
340
|
HybridAudioBrowser.shared?.player?.destroy()
|
|
341
|
+
HybridAudioBrowser.shared?.dropStaleJSCallbacks()
|
|
310
342
|
playerAndConfiguredBrowser.reset()
|
|
311
343
|
HybridAudioBrowser.shared = self
|
|
312
344
|
// Tell connected external controllers (CarPlay) to re-subscribe against
|
|
@@ -319,6 +351,38 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
319
351
|
setupBrowserCallbacks()
|
|
320
352
|
}
|
|
321
353
|
|
|
354
|
+
/// Drops every JS-provided callback that native code can invoke on its own
|
|
355
|
+
/// initiative (browse resolvers, `handleTrackLoad`, `resolveAlbumPath`,
|
|
356
|
+
/// `formatNavigationError`, `resolveGate`, the now-playing formatter).
|
|
357
|
+
///
|
|
358
|
+
/// Called on the *previous* instance when a new one takes over: its JS runtime
|
|
359
|
+
/// is gone, and invoking a callback from a dead runtime aborts the process —
|
|
360
|
+
/// Nitro's `AsyncJSCallback` throws on a destroyed Dispatcher, and the throw
|
|
361
|
+
/// crosses the generated bridge wrapper's `noexcept` boundary. Native code
|
|
362
|
+
/// (e.g. a CarPlay controller still bound to this instance mid-reload) keeps
|
|
363
|
+
/// working off the data-only config with built-in fallbacks; `resolveGate`
|
|
364
|
+
/// reverts to the fail-closed default it documents for exactly this window.
|
|
365
|
+
/// One-shot: on a normal reload both triggers hit the same dying instance
|
|
366
|
+
/// (the reload notification, then the replacement's `init`), and the second
|
|
367
|
+
/// strip would re-fire the config-setter cascade — another `onConfigChanged`
|
|
368
|
+
/// → CarPlay now-playing button rebuild — for nothing.
|
|
369
|
+
@MainActor private var didDropJSCallbacks = false
|
|
370
|
+
|
|
371
|
+
@MainActor
|
|
372
|
+
private func dropStaleJSCallbacks() {
|
|
373
|
+
guard !didDropJSCallbacks else { return }
|
|
374
|
+
didDropJSCallbacks = true
|
|
375
|
+
// Guarded: assigning `config` marks the browser configured (the setter's
|
|
376
|
+
// didSet), and a never-configured browser must keep cold-start waiters
|
|
377
|
+
// (CarPlay, Siri) blocked on the readiness gate rather than resolve them
|
|
378
|
+
// against an empty config.
|
|
379
|
+
if browserManager.isConfigured {
|
|
380
|
+
browserManager.config = browserManager.config.strippingJSCallbacks()
|
|
381
|
+
}
|
|
382
|
+
resolveGate = Self.defaultResolveGate
|
|
383
|
+
nowPlayingMetadataFormatter = nil
|
|
384
|
+
}
|
|
385
|
+
|
|
322
386
|
deinit {
|
|
323
387
|
// Safety net: ensure the AVPlayer is stopped if this instance is deallocated.
|
|
324
388
|
let player = self.player
|
|
@@ -447,8 +511,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
447
511
|
executePlayback(intent)
|
|
448
512
|
case .intercepted:
|
|
449
513
|
break
|
|
450
|
-
case let .browse(
|
|
451
|
-
|
|
514
|
+
case let .browse(path):
|
|
515
|
+
navigateToBrowsablePath(path)
|
|
452
516
|
case .none:
|
|
453
517
|
break
|
|
454
518
|
}
|
|
@@ -472,13 +536,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
472
536
|
}
|
|
473
537
|
}
|
|
474
538
|
|
|
475
|
-
private func
|
|
539
|
+
private func navigateToBrowsablePath(_ path: String) {
|
|
476
540
|
clearNavigationError()
|
|
477
541
|
Task {
|
|
478
542
|
do {
|
|
479
|
-
try await browserManager.navigate(
|
|
543
|
+
try await browserManager.navigate(path)
|
|
480
544
|
} catch {
|
|
481
|
-
handleNavigationError(error, path:
|
|
545
|
+
handleNavigationError(error, path: path)
|
|
482
546
|
}
|
|
483
547
|
}
|
|
484
548
|
}
|
|
@@ -487,7 +551,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
487
551
|
Promise.async { [weak self] in
|
|
488
552
|
guard let self else { return [] }
|
|
489
553
|
let resolved = try await browserManager.search(query)
|
|
490
|
-
return resolved.
|
|
554
|
+
return resolved.flattenedChildren ?? []
|
|
491
555
|
}
|
|
492
556
|
}
|
|
493
557
|
|
|
@@ -739,11 +803,16 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
739
803
|
return await browserManager.resolveMediaUrl(src, track: track)
|
|
740
804
|
}
|
|
741
805
|
|
|
742
|
-
// Configure artwork URL resolver for Now Playing (with size context)
|
|
806
|
+
// Configure artwork URL resolver for Now Playing (with size context). The
|
|
807
|
+
// `nowPlayingArtwork` config runs for EVERY playing track, id or not — an id-less track
|
|
808
|
+
// leaves any `{id}` token unfilled (resolveArtworkUrl warns and the request visibly
|
|
809
|
+
// 404s) rather than skipping the config.
|
|
743
810
|
player?.nowPlayingUpdater.artworkUrlResolver = { [weak self] track, imageContext in
|
|
744
811
|
guard let self else { return nil }
|
|
745
|
-
|
|
746
|
-
|
|
812
|
+
return await browserManager.resolveArtworkUrl(
|
|
813
|
+
track: track, perRouteConfig: browserManager.config.nowPlayingArtwork,
|
|
814
|
+
imageContext: imageContext,
|
|
815
|
+
)
|
|
747
816
|
}
|
|
748
817
|
|
|
749
818
|
// Emit the JS `onNowPlayingChanged` event when the rendered text lines change. The updater
|
|
@@ -813,9 +882,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
813
882
|
// Update stored options
|
|
814
883
|
playerOptions.update(from: options)
|
|
815
884
|
|
|
816
|
-
// Propagate the favorite
|
|
885
|
+
// Propagate the favorite flag to the browser so it can hydrate
|
|
817
886
|
// row hearts (the `favorite` capability is the single favoriting switch).
|
|
818
|
-
browserManager.
|
|
887
|
+
browserManager.setFavoriteEnabled(playerOptions.capabilities.favoriteEnabled)
|
|
819
888
|
|
|
820
889
|
// Apply remote commands to player
|
|
821
890
|
applyRemoteCommands()
|
|
@@ -1064,23 +1133,24 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1064
1133
|
|
|
1065
1134
|
public func setActiveTrackFavorited(favorited: Bool) throws {
|
|
1066
1135
|
onMainActor {
|
|
1067
|
-
guard let track = player?.currentTrack, let
|
|
1136
|
+
guard let track = player?.currentTrack, let identity = track.identity else { return }
|
|
1068
1137
|
guard let index = player?.currentIndex, index >= 0 else { return }
|
|
1069
|
-
// Optimistically reflect in the authoritative
|
|
1070
|
-
// heart (hydrated from it in getActiveTrack) flips
|
|
1071
|
-
// reconciles to its canonical
|
|
1072
|
-
|
|
1138
|
+
// Optimistically reflect in the authoritative favorite set so the
|
|
1139
|
+
// now-playing heart (hydrated from it in getActiveTrack) flips
|
|
1140
|
+
// immediately; the consumer reconciles to its canonical identities on
|
|
1141
|
+
// the next setFavorites.
|
|
1142
|
+
browserManager.setFavorited(identity: identity, favorited: favorited)
|
|
1073
1143
|
let updatedTrack = track.copying(favorited: favorited)
|
|
1144
|
+
// Hydration never overrides a caller-set `favorited`, so the queued
|
|
1145
|
+
// track must itself carry the latest explicit value (as Android's tag
|
|
1146
|
+
// swap and web's replaceTrack do).
|
|
1147
|
+
player?.replace(index, updatedTrack)
|
|
1148
|
+
// The ONLY emit: a favorite toggle is an in-place mutation of the active
|
|
1149
|
+
// track, not a transition — onPlaybackActiveTrackChanged stays
|
|
1150
|
+
// transition-only, and the useActiveTrack / useQueue hooks subscribe to
|
|
1151
|
+
// onFavoriteChanged themselves. Native surfaces (CarPlay heart) observe
|
|
1152
|
+
// favoriteChangedEmitter / onFavoritesChanged directly.
|
|
1074
1153
|
favoriteChangedEmitter.emit(FavoriteChangedEvent(track: updatedTrack, favorited: favorited))
|
|
1075
|
-
// Fire active track changed so the now-playing heart + useActiveTrack() refresh.
|
|
1076
|
-
let position = player?.currentTime ?? 0
|
|
1077
|
-
activeTrackChangedEmitter.emit(PlaybackActiveTrackChangedEvent(
|
|
1078
|
-
lastIndex: Double(index),
|
|
1079
|
-
lastTrack: track,
|
|
1080
|
-
lastPosition: position,
|
|
1081
|
-
index: Double(index),
|
|
1082
|
-
track: updatedTrack,
|
|
1083
|
-
))
|
|
1084
1154
|
}
|
|
1085
1155
|
}
|
|
1086
1156
|
|
|
@@ -1220,7 +1290,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1220
1290
|
duration: track.duration,
|
|
1221
1291
|
artwork: nowPlayingArtwork(for: track),
|
|
1222
1292
|
description: track.description,
|
|
1223
|
-
mediaId: track.src ?? track.
|
|
1293
|
+
mediaId: track.src ?? track.path,
|
|
1224
1294
|
genre: track.genre,
|
|
1225
1295
|
)
|
|
1226
1296
|
}
|
|
@@ -1809,14 +1879,22 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1809
1879
|
browser.showNowPlayingRequestedEmitter.emit(())
|
|
1810
1880
|
completion(true)
|
|
1811
1881
|
} else if let state = browser.playbackStateStore.load() {
|
|
1812
|
-
browser.logger.info("resume: cold — restoring persisted track")
|
|
1813
1882
|
let track = state.track.toNitro()
|
|
1883
|
+
// The persisted track can carry `disabled` — it became unavailable
|
|
1884
|
+
// after it was saved. A disabled track never plays, resumption
|
|
1885
|
+
// included (Track.disabled).
|
|
1886
|
+
if track.disabled == true {
|
|
1887
|
+
browser.logger.info("resume: persisted track is disabled — refusing")
|
|
1888
|
+
completion(false)
|
|
1889
|
+
return
|
|
1890
|
+
}
|
|
1891
|
+
browser.logger.info("resume: cold — restoring persisted track")
|
|
1814
1892
|
let startMs = (track.live == true) ? nil : state.positionMs
|
|
1815
1893
|
// Match Android resume: re-expand the full queue from the track's contextual
|
|
1816
|
-
//
|
|
1817
|
-
// track when the
|
|
1818
|
-
if let
|
|
1819
|
-
let expanded = try? await browser.browserManager.
|
|
1894
|
+
// path (parent container → siblings + selected index). Fall back to the single
|
|
1895
|
+
// track when the path isn't contextual or expansion fails.
|
|
1896
|
+
if let path = state.track.path,
|
|
1897
|
+
let expanded = try? await browser.browserManager.expandQueueFromContextualPath(path)
|
|
1820
1898
|
{
|
|
1821
1899
|
player.setQueue(expanded.tracks, initialIndex: expanded.selectedIndex, startPositionMs: startMs, playWhenReady: true)
|
|
1822
1900
|
} else {
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
/// These stubs declare `Equatable` for test ergonomics — the generated
|
|
6
6
|
/// Nitro types do NOT. Shared `ios/` sources must not rely on conformances
|
|
7
7
|
/// that exist only here: `swift test` compiles, the app build breaks.
|
|
8
|
+
///
|
|
9
|
+
/// Property names and types ARE checked against `nitrogen/generated/ios/swift`
|
|
10
|
+
/// by `src/nitro-stub-drift.test.ts`, which runs in the JS CI job — neither
|
|
11
|
+
/// native job can see both files at once. Declaring fewer properties than the
|
|
12
|
+
/// real type is fine and deliberate; spelling one differently is what that
|
|
13
|
+
/// test catches. Conformances and default values are still on you.
|
|
8
14
|
|
|
9
15
|
struct ArtworkVariants: Equatable {
|
|
10
16
|
var light: String
|
|
@@ -23,19 +29,19 @@
|
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
struct Track: Equatable {
|
|
26
|
-
var id: String
|
|
27
|
-
var
|
|
32
|
+
var id: String?
|
|
33
|
+
var path: String?
|
|
28
34
|
var src: String?
|
|
35
|
+
var artwork: Variant_String_ArtworkVariants?
|
|
36
|
+
var artworkSource: ImageSource?
|
|
29
37
|
var request: TrackRequest?
|
|
30
38
|
var title: String = ""
|
|
31
39
|
var artist: String?
|
|
32
|
-
var
|
|
40
|
+
var albumPath: String?
|
|
33
41
|
var album: String?
|
|
42
|
+
var style: TrackStyle?
|
|
43
|
+
var disabled: Bool?
|
|
34
44
|
var live: Bool?
|
|
35
|
-
var artwork: Variant_String_ArtworkVariants?
|
|
36
|
-
var artworkSource: ImageSource?
|
|
37
|
-
var groupTitle: String?
|
|
38
|
-
var imageRow: [ImageRowItem]?
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
struct TrackLoadEvent {
|
|
@@ -63,14 +69,24 @@
|
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
enum HttpMethod {
|
|
73
|
+
case get
|
|
74
|
+
case post
|
|
75
|
+
case put
|
|
76
|
+
case patch
|
|
77
|
+
case delete
|
|
78
|
+
case head
|
|
79
|
+
case options
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
struct ImageSource: Equatable {
|
|
67
83
|
var uri: String
|
|
68
|
-
var method:
|
|
84
|
+
var method: HttpMethod?
|
|
69
85
|
var headers: [String: String]?
|
|
70
86
|
var body: String?
|
|
71
87
|
}
|
|
72
88
|
|
|
73
|
-
enum
|
|
89
|
+
enum StyleDisplay: Equatable {
|
|
74
90
|
case list
|
|
75
91
|
case grid
|
|
76
92
|
|
|
@@ -83,17 +99,46 @@
|
|
|
83
99
|
}
|
|
84
100
|
}
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
enum ArtworkRendering: Equatable {
|
|
103
|
+
case original
|
|
104
|
+
case stencil
|
|
105
|
+
|
|
106
|
+
init?(fromString string: String) {
|
|
107
|
+
switch string {
|
|
108
|
+
case "original": self = .original
|
|
109
|
+
case "stencil": self = .stencil
|
|
110
|
+
default: return nil
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
struct TrackStyle: Equatable {
|
|
116
|
+
var display: StyleDisplay?
|
|
117
|
+
var artworkRendering: ArtworkRendering?
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Member order matches the generated init (own properties before inherited
|
|
121
|
+
// ones — Nitro flattens `extends` own-props-first): the memberwise init this
|
|
122
|
+
// produces must accept the same argument order as the real type, or
|
|
123
|
+
// `swift test` passes while the app build breaks on argument order.
|
|
124
|
+
struct SectionStyle: Equatable {
|
|
125
|
+
var gridWrap: Bool?
|
|
126
|
+
var display: StyleDisplay?
|
|
127
|
+
var artworkRendering: ArtworkRendering?
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
struct Section: Equatable {
|
|
131
|
+
var title: String?
|
|
132
|
+
var subtitle: String?
|
|
133
|
+
var style: SectionStyle?
|
|
134
|
+
var path: String?
|
|
135
|
+
var children: [Track]
|
|
93
136
|
}
|
|
94
137
|
|
|
95
138
|
struct ResolvedTrack: Equatable {
|
|
96
|
-
var
|
|
139
|
+
var path: String
|
|
140
|
+
var style: SectionStyle?
|
|
141
|
+
var sections: [Section]?
|
|
97
142
|
var children: [Track]?
|
|
98
143
|
var carPlaySiriListButton: CarPlaySiriListButtonPosition?
|
|
99
144
|
var id: String?
|
|
@@ -101,21 +146,17 @@
|
|
|
101
146
|
var artwork: Variant_String_ArtworkVariants?
|
|
102
147
|
var artworkSource: ImageSource?
|
|
103
148
|
var request: TrackRequest?
|
|
104
|
-
var artworkCarPlayTinted: Bool?
|
|
105
149
|
var title: String
|
|
106
150
|
var subtitle: String?
|
|
107
151
|
var artist: String?
|
|
108
|
-
var
|
|
152
|
+
var albumPath: String?
|
|
109
153
|
var album: String?
|
|
110
154
|
var description: String?
|
|
111
155
|
var genre: String?
|
|
112
156
|
var duration: Double?
|
|
113
|
-
var
|
|
114
|
-
var childrenStyle: TrackStyle?
|
|
157
|
+
var disabled: Bool?
|
|
115
158
|
var favorited: Bool?
|
|
116
|
-
var groupTitle: String?
|
|
117
159
|
var live: Bool?
|
|
118
|
-
var imageRow: [ImageRowItem]?
|
|
119
160
|
}
|
|
120
161
|
|
|
121
162
|
enum PlaybackState: Equatable {
|
|
@@ -32,6 +32,9 @@ class PlayerUpdateOptions {
|
|
|
32
32
|
/// Custom buttons for the CarPlay Now Playing screen
|
|
33
33
|
var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] = []
|
|
34
34
|
|
|
35
|
+
/// Where the now-playing indicator draws on CarPlay list rows
|
|
36
|
+
var carPlayPlayingIndicatorLocation: CarPlayPlayingIndicatorLocation = .leading
|
|
37
|
+
|
|
35
38
|
// MARK: - Initialization
|
|
36
39
|
|
|
37
40
|
init() {}
|
|
@@ -74,6 +77,9 @@ class PlayerUpdateOptions {
|
|
|
74
77
|
if let buttons = ios.carPlayNowPlayingButtons {
|
|
75
78
|
carPlayNowPlayingButtons = buttons
|
|
76
79
|
}
|
|
80
|
+
if let location = ios.carPlayPlayingIndicatorLocation {
|
|
81
|
+
carPlayPlayingIndicatorLocation = location
|
|
82
|
+
}
|
|
77
83
|
}
|
|
78
84
|
}
|
|
79
85
|
|
|
@@ -92,6 +98,7 @@ class PlayerUpdateOptions {
|
|
|
92
98
|
playbackRates: playbackRates,
|
|
93
99
|
carPlayUpNextButton: carPlayUpNextButton,
|
|
94
100
|
carPlayNowPlayingButtons: carPlayNowPlayingButtons,
|
|
101
|
+
carPlayPlayingIndicatorLocation: carPlayPlayingIndicatorLocation,
|
|
95
102
|
),
|
|
96
103
|
)
|
|
97
104
|
}
|
|
@@ -130,7 +130,7 @@ final class NowPlayingUpdater {
|
|
|
130
130
|
let resolvedArtist = artist ?? track.artist
|
|
131
131
|
let resolvedAlbum = album ?? track.album
|
|
132
132
|
let published = Published(
|
|
133
|
-
trackId: track.
|
|
133
|
+
trackId: track.identity ?? track.path,
|
|
134
134
|
title: resolvedTitle,
|
|
135
135
|
artist: resolvedArtist,
|
|
136
136
|
album: resolvedAlbum,
|
|
@@ -691,14 +691,14 @@ class PlaybackCoordinator {
|
|
|
691
691
|
guard let src = currentTrack.src else {
|
|
692
692
|
logger.error("Failed to load track - no src")
|
|
693
693
|
logger.error(" track.title: \(currentTrack.title)")
|
|
694
|
-
logger.error(" track.
|
|
694
|
+
logger.error(" track.path: \(currentTrack.path ?? "nil")")
|
|
695
695
|
effectHandler?.clearCurrentItem()
|
|
696
696
|
transition(.errorOccurred(.invalidSourceUrl("nil")))
|
|
697
697
|
return
|
|
698
698
|
}
|
|
699
699
|
|
|
700
700
|
logger.debug("Loading track: \(currentTrack.title)")
|
|
701
|
-
logger.debug(" track.
|
|
701
|
+
logger.debug(" track.path: \(currentTrack.path ?? "nil")")
|
|
702
702
|
logger.debug(" track.src: \(src)")
|
|
703
703
|
|
|
704
704
|
effectHandler?.loadTrack(src: src, track: currentTrack)
|
|
@@ -123,9 +123,10 @@ class RetryManager {
|
|
|
123
123
|
|
|
124
124
|
// MARK: - Error Classification
|
|
125
125
|
|
|
126
|
-
/// HTTP statuses worth another attempt
|
|
127
|
-
/// `RetryLoadErrorHandlingPolicy
|
|
128
|
-
///
|
|
126
|
+
/// HTTP statuses worth another attempt, beyond the 500–599 range checked in
|
|
127
|
+
/// `isRetryable`. Android's `RetryLoadErrorHandlingPolicy` shares 408/429 but
|
|
128
|
+
/// enumerates only 500/502/503/504 of the 5xx range, so uncommon 5xx codes
|
|
129
|
+
/// (501, 505…) retry here and not there.
|
|
129
130
|
private static let retryableHTTPStatusCodes: Set<Int> = [408, 429]
|
|
130
131
|
|
|
131
132
|
/// AVFoundation failures a retry cannot fix: the media is unusable rather
|
|
@@ -74,6 +74,56 @@ import Testing
|
|
|
74
74
|
#expect(result == "/items?a=1&m=2&z=3")
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// MARK: - build
|
|
78
|
+
|
|
79
|
+
@Test func buildAppendsTrackIdWithQuestionMark() {
|
|
80
|
+
let result = BrowserPathHelper.build(parentPath: "/library", trackId: "song.mp3")
|
|
81
|
+
#expect(result == "/library?__trackId=song.mp3")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test func buildAppendsTrackIdWithAmpersandWhenParentHasQuery() {
|
|
85
|
+
let result = BrowserPathHelper.build(parentPath: "/search?q=jazz", trackId: "song.mp3")
|
|
86
|
+
#expect(result == "/search?q=jazz&__trackId=song.mp3")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Test func buildRoundTripsSrcWithQueryParams() {
|
|
90
|
+
// A src carrying its own query string (signed CDN URL) must survive the
|
|
91
|
+
// build → extract/strip round-trip: an unescaped `&` splits the src into
|
|
92
|
+
// stray query params, truncating the trackId and polluting the parent path.
|
|
93
|
+
let src = "https://cdn.example.com/stream.mp3?token=abc&exp=1699999999"
|
|
94
|
+
let url = BrowserPathHelper.build(parentPath: "/library", trackId: src)
|
|
95
|
+
#expect(BrowserPathHelper.extractTrackId(url) == src)
|
|
96
|
+
#expect(BrowserPathHelper.stripTrackId(url) == "/library")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Test func buildRoundTripsSrcWithEqualsAndPlus() {
|
|
100
|
+
let src = "https://cdn.example.com/a+b.mp3?sig=x=y"
|
|
101
|
+
let url = BrowserPathHelper.build(parentPath: "/library", trackId: src)
|
|
102
|
+
#expect(BrowserPathHelper.extractTrackId(url) == src)
|
|
103
|
+
#expect(BrowserPathHelper.stripTrackId(url) == "/library")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Test func buildStampsTheIndexAndRoundTripsIt() {
|
|
107
|
+
let src = "https://cdn.example.com/stream.mp3?token=abc&exp=1"
|
|
108
|
+
let url = BrowserPathHelper.build(parentPath: "/library", trackId: src, index: 3)
|
|
109
|
+
#expect(BrowserPathHelper.extractTrackId(url) == src)
|
|
110
|
+
#expect(BrowserPathHelper.extractIndex(url) == 3)
|
|
111
|
+
#expect(BrowserPathHelper.stripTrackId(url) == "/library")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// MARK: - extractIndex
|
|
115
|
+
|
|
116
|
+
@Test func extractIndexReturnsNilWithoutAStampedIndex() {
|
|
117
|
+
let url = BrowserPathHelper.build(parentPath: "/library", trackId: "song.mp3")
|
|
118
|
+
#expect(BrowserPathHelper.extractIndex(url) == nil)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Test func extractIndexReturnsNilForNonContextualOrMalformedIndex() {
|
|
122
|
+
#expect(BrowserPathHelper.extractIndex("/library") == nil)
|
|
123
|
+
#expect(BrowserPathHelper.extractIndex("/library?__trackId=song.mp3&__index=x") == nil)
|
|
124
|
+
#expect(BrowserPathHelper.extractIndex("/library?__trackId=song.mp3&__index=-1") == nil)
|
|
125
|
+
}
|
|
126
|
+
|
|
77
127
|
// MARK: - stripTrackId
|
|
78
128
|
|
|
79
129
|
@Test func stripTrackIdRemovesTrackParam() {
|
|
@@ -116,23 +166,3 @@ import Testing
|
|
|
116
166
|
@Test func isContextualReturnsTrueWithAmpersandSeparator() {
|
|
117
167
|
#expect(BrowserPathHelper.isContextual("/lib?q=1&__trackId=x") == true)
|
|
118
168
|
}
|
|
119
|
-
|
|
120
|
-
// MARK: - containsSegment
|
|
121
|
-
|
|
122
|
-
@Test func containsSegmentMatchesFullSegments() {
|
|
123
|
-
let path = "/listen/amsterdam-funk-channel/Gw0LGB8j"
|
|
124
|
-
#expect(BrowserPathHelper.containsSegment(path, "Gw0LGB8j"))
|
|
125
|
-
#expect(BrowserPathHelper.containsSegment(path, "amsterdam-funk-channel"))
|
|
126
|
-
#expect(BrowserPathHelper.containsSegment(path, "listen"))
|
|
127
|
-
#expect(BrowserPathHelper.containsSegment("/listen/x/Gw0LGB8j?hl=en", "Gw0LGB8j"))
|
|
128
|
-
#expect(BrowserPathHelper.containsSegment("/listen/x/Gw0LGB8j#frag", "Gw0LGB8j"))
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
@Test func containsSegmentRejectsPartialAndMissing() {
|
|
132
|
-
let path = "/listen/amsterdam-funk-channel/Gw0LGB8j"
|
|
133
|
-
#expect(!BrowserPathHelper.containsSegment(path, "Gw0LGB8"))
|
|
134
|
-
#expect(!BrowserPathHelper.containsSegment(path, "funk"))
|
|
135
|
-
#expect(!BrowserPathHelper.containsSegment(path, "msterdam-funk-channe"))
|
|
136
|
-
#expect(!BrowserPathHelper.containsSegment(path, "NopeNope"))
|
|
137
|
-
#expect(!BrowserPathHelper.containsSegment(path, ""))
|
|
138
|
-
}
|