react-native-audio-browser 0.2.0-next.11 → 0.2.0-next.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/README.md +4 -4
  2. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
  3. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
  4. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
  5. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -58
  6. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
  7. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +49 -0
  8. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -7
  9. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  10. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  11. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
  12. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
  13. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +93 -73
  14. package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
  15. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  16. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
  17. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  18. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +35 -0
  19. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
  20. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +26 -39
  21. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  22. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +228 -50
  23. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  24. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
  25. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
  26. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
  27. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -0
  28. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  29. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  30. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
  31. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  32. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  33. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  34. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
  35. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  36. package/ios/Browser/BrowserConfig.swift +102 -5
  37. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  38. package/ios/Browser/BrowserManager.swift +131 -143
  39. package/ios/Browser/BrowserPathHelper.swift +44 -44
  40. package/ios/Browser/JsonModels.swift +187 -106
  41. package/ios/Browser/SearchDrillIn.swift +6 -6
  42. package/ios/Browser/SectionPresentation.swift +34 -0
  43. package/ios/Browser/SectionScope.swift +58 -24
  44. package/ios/Browser/StyleResolver.swift +47 -0
  45. package/ios/Browser/TabBarEntries.swift +2 -2
  46. package/ios/Browser/TrackIdTemplate.swift +37 -0
  47. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  48. package/ios/CarPlay/CarPlayController.swift +105 -67
  49. package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
  50. package/ios/CarPlay/CarPlayListItemFactory.swift +217 -84
  51. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  52. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  53. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  54. package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
  55. package/ios/Extension/Section+Copying.swift +31 -0
  56. package/ios/Extension/Track+Copying.swift +6 -12
  57. package/ios/Extension/Track+Identity.swift +18 -0
  58. package/ios/HybridAudioBrowser.swift +113 -35
  59. package/ios/Model/NitroTypeStubs.swift +46 -21
  60. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  61. package/ios/Player/NowPlayingUpdater.swift +1 -1
  62. package/ios/Player/PlaybackCoordinator.swift +2 -2
  63. package/ios/Player/RetryManager.swift +4 -3
  64. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  65. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  66. package/ios/Tests/JsonModelsTests.swift +75 -1
  67. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  68. package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
  69. package/ios/Tests/SearchDrillInTests.swift +16 -16
  70. package/ios/Tests/SectionPresentationTests.swift +61 -0
  71. package/ios/Tests/SectionScopeTests.swift +87 -55
  72. package/ios/Tests/StyleResolverTests.swift +67 -0
  73. package/ios/Tests/TabBarEntriesTests.swift +18 -18
  74. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  75. package/ios/Tests/TrackIdentityTests.swift +20 -0
  76. package/ios/Tests/TrackSelectorTests.swift +168 -33
  77. package/ios/TrackSelector.swift +45 -30
  78. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  79. package/lib/commonjs/AudioBrowser.js +11 -0
  80. package/lib/commonjs/AudioBrowser.js.map +1 -1
  81. package/lib/commonjs/features/browser-config.js +11 -8
  82. package/lib/commonjs/features/browser-config.js.map +1 -1
  83. package/lib/commonjs/features/browser.js +2 -2
  84. package/lib/commonjs/features/favorites.js +13 -10
  85. package/lib/commonjs/features/favorites.js.map +1 -1
  86. package/lib/commonjs/features/playback/controls.js +4 -2
  87. package/lib/commonjs/features/playback/controls.js.map +1 -1
  88. package/lib/commonjs/features/playback/state.js +2 -3
  89. package/lib/commonjs/features/playback/state.js.map +1 -1
  90. package/lib/commonjs/features/player/options.js.map +1 -1
  91. package/lib/commonjs/features/player/setup.js +3 -1
  92. package/lib/commonjs/features/player/setup.js.map +1 -1
  93. package/lib/commonjs/features/player/validateOptions.js +2 -0
  94. package/lib/commonjs/features/player/validateOptions.js.map +1 -1
  95. package/lib/commonjs/features/queue/activeTrack.js +14 -3
  96. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  97. package/lib/commonjs/features/queue/queue.js +11 -4
  98. package/lib/commonjs/features/queue/queue.js.map +1 -1
  99. package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
  100. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  101. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  102. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  103. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  104. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  105. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  106. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  107. package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
  108. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  109. package/lib/commonjs/web/browser/BrowserManager.js +144 -87
  110. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  111. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  112. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  113. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  114. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  115. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  116. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  117. package/lib/commonjs/web/browser/sections.js +89 -0
  118. package/lib/commonjs/web/browser/sections.js.map +1 -0
  119. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  120. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  121. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  122. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  123. package/lib/module/AudioBrowser.js +1 -0
  124. package/lib/module/AudioBrowser.js.map +1 -1
  125. package/lib/module/features/browser-config.js +11 -8
  126. package/lib/module/features/browser-config.js.map +1 -1
  127. package/lib/module/features/browser.js +2 -2
  128. package/lib/module/features/favorites.js +13 -10
  129. package/lib/module/features/favorites.js.map +1 -1
  130. package/lib/module/features/playback/controls.js +4 -2
  131. package/lib/module/features/playback/controls.js.map +1 -1
  132. package/lib/module/features/playback/state.js +2 -3
  133. package/lib/module/features/playback/state.js.map +1 -1
  134. package/lib/module/features/player/options.js.map +1 -1
  135. package/lib/module/features/player/setup.js +3 -1
  136. package/lib/module/features/player/setup.js.map +1 -1
  137. package/lib/module/features/player/validateOptions.js +2 -0
  138. package/lib/module/features/player/validateOptions.js.map +1 -1
  139. package/lib/module/features/queue/activeTrack.js +14 -3
  140. package/lib/module/features/queue/activeTrack.js.map +1 -1
  141. package/lib/module/features/queue/queue.js +11 -4
  142. package/lib/module/features/queue/queue.js.map +1 -1
  143. package/lib/module/utils/LazyNativeEmitter.js +21 -10
  144. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  145. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  146. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  147. package/lib/module/utils/getTrackIdentity.js +20 -0
  148. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  149. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  150. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  151. package/lib/module/web/NativeAudioBrowser.js +32 -24
  152. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  153. package/lib/module/web/browser/BrowserManager.js +144 -87
  154. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  155. package/lib/module/web/browser/FavoriteManager.js +31 -21
  156. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  157. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  158. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  159. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  160. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  161. package/lib/module/web/browser/sections.js +83 -0
  162. package/lib/module/web/browser/sections.js.map +1 -0
  163. package/lib/module/web/player/NowPlayingManager.js +4 -3
  164. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  165. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  166. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  167. package/lib/typescript/src/AudioBrowser.d.ts +1 -0
  168. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  169. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  170. package/lib/typescript/src/features/browser.d.ts +2 -2
  171. package/lib/typescript/src/features/favorites.d.ts +13 -10
  172. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  173. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  174. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  175. package/lib/typescript/src/features/playback/state.d.ts +2 -3
  176. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  177. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  178. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  179. package/lib/typescript/src/features/player/options.d.ts +21 -19
  180. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  181. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  182. package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
  183. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
  184. package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
  185. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  186. package/lib/typescript/src/features/queue/queue.d.ts +5 -2
  187. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  188. package/lib/typescript/src/types/browser-native.d.ts +2 -2
  189. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  190. package/lib/typescript/src/types/browser-nodes.d.ts +260 -127
  191. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  192. package/lib/typescript/src/types/browser.d.ts +33 -53
  193. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  194. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
  195. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  196. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  197. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  198. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  199. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  200. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  201. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  202. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
  203. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  204. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -3
  205. package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
  206. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  207. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
  208. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
  209. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
  210. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
  211. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
  212. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
  213. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
  214. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -2
  215. 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
  216. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
  217. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
  218. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
  219. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
  220. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
  221. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
  222. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  223. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
  224. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
  225. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
  226. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -7
  227. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
  228. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  229. package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
  230. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -3
  231. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  232. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
  233. package/nitrogen/generated/android/c++/JSection.hpp +114 -0
  234. package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
  235. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  236. package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
  237. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
  238. package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
  239. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -4
  240. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  241. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  242. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  243. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
  244. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  245. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  246. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
  247. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +50 -0
  248. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
  249. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  250. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
  251. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +23 -5
  252. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
  253. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
  254. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
  255. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  256. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  257. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  258. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
  259. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  260. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  261. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
  262. package/nitrogen/generated/ios/swift/Section.swift +100 -0
  263. package/nitrogen/generated/ios/swift/SectionStyle.swift +64 -0
  264. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  265. package/nitrogen/generated/ios/swift/Track.swift +20 -73
  266. package/nitrogen/generated/ios/swift/TrackStyle.swift +28 -22
  267. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  268. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  269. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  270. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
  271. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  272. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  273. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
  274. package/nitrogen/generated/shared/c++/Section.hpp +106 -0
  275. package/nitrogen/generated/shared/c++/SectionStyle.hpp +96 -0
  276. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  277. package/nitrogen/generated/shared/c++/Track.hpp +15 -31
  278. package/nitrogen/generated/shared/c++/TrackStyle.hpp +50 -34
  279. package/package.json +1 -1
  280. package/src/AudioBrowser.ts +1 -0
  281. package/src/features/browser-config.test.ts +21 -6
  282. package/src/features/browser-config.ts +11 -8
  283. package/src/features/browser.test.ts +1 -1
  284. package/src/features/browser.ts +2 -2
  285. package/src/features/favorites.ts +13 -10
  286. package/src/features/hook-wiring.test.ts +63 -15
  287. package/src/features/playback/controls.ts +4 -2
  288. package/src/features/playback/state.ts +2 -3
  289. package/src/features/playback/trackPlaybackTime.ts +1 -1
  290. package/src/features/player/options.ts +27 -19
  291. package/src/features/player/setup.test.ts +12 -3
  292. package/src/features/player/setup.ts +3 -1
  293. package/src/features/player/validateOptions.ts +2 -0
  294. package/src/features/queue/activeTrack.ts +14 -3
  295. package/src/features/queue/queue.ts +11 -4
  296. package/src/nitro-stub-drift.test.ts +40 -0
  297. package/src/public-surface.test.ts +40 -0
  298. package/src/specs/audio-browser.nitro.ts +4 -3
  299. package/src/style-resolution.test.ts +139 -0
  300. package/src/types/browser-native.ts +2 -2
  301. package/src/types/browser-nodes.ts +268 -136
  302. package/src/types/browser.ts +34 -55
  303. package/src/utils/LazyNativeEmitter.ts +20 -10
  304. package/src/utils/NativeUpdatedValue.ts +29 -13
  305. package/src/utils/getTrackIdentity.ts +20 -0
  306. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  307. package/src/web/NativeAudioBrowser.ts +37 -22
  308. package/src/web/browser/BrowserManager.ts +176 -95
  309. package/src/web/browser/FavoriteManager.test.ts +84 -0
  310. package/src/web/browser/FavoriteManager.ts +28 -23
  311. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  312. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  313. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  314. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  315. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  316. package/src/web/browser/sections.test.ts +91 -0
  317. package/src/web/browser/sections.ts +72 -0
  318. package/src/web/player/NowPlayingManager.ts +4 -3
  319. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  320. package/src/web/util/BrowserPathHelper.ts +61 -13
  321. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  322. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  323. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  324. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  325. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  326. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  327. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  328. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  329. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  330. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  331. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  332. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  333. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
@@ -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 URL). Used
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 URL
660
+ * @param sourcePath Optional path from which this queue was expanded (for contextual path
661
661
  * optimization).
662
662
  */
663
663
  fun setQueue(
@@ -685,17 +685,27 @@ class Player(internal val context: Context) {
685
685
  /**
686
686
  * Sets the favorited state of the currently playing track. Updates the heart icon in media
687
687
  * controllers without interrupting playback.
688
+ *
689
+ * @return whether the gesture applied — false only when there is no current track (nothing to
690
+ * favorite), so MediaSession callers can report an invalid state instead of a success the
691
+ * controller then watches revert.
688
692
  */
689
- fun setActiveTrackFavorited(favorited: Boolean) {
690
- val currentTrack = this.currentTrack ?: return
693
+ fun setActiveTrackFavorited(favorited: Boolean): Boolean {
694
+ val currentTrack = this.currentTrack ?: return false
691
695
 
692
696
  // Persist to the native favorites cache regardless of player state — the
693
697
  // user's gesture is durable even when there's no active media item to
694
- // update (queue tear-down, between-track gaps, etc.).
695
- currentTrack.src?.let { src -> browser?.browserManager?.updateFavorite(src, favorited) }
698
+ // update (queue tear-down, between-track gaps, etc.). Keyed by the track
699
+ // identity (id when non-blank, else src).
700
+ currentTrack.identity?.let { identity ->
701
+ browser?.browserManager?.updateFavorite(identity, favorited)
702
+ }
696
703
 
704
+ // Cache written above: the gesture is durably recorded even without a media
705
+ // item to restamp (queue tear-down, between-track gaps) — that counts as
706
+ // applied; there is also no visible heart to revert in that state.
697
707
  val index = exoPlayer.currentMediaItemIndex
698
- if (index == C.INDEX_UNSET) return
708
+ if (index == C.INDEX_UNSET) return true
699
709
 
700
710
  // Create updated Track with new favorited state
701
711
  val updatedTrack = currentTrack.copy(favorited = favorited)
@@ -719,28 +729,23 @@ class Player(internal val context: Context) {
719
729
  // Update the heart button icon in notification/Android Auto
720
730
  updateFavoriteButtonState(favorited)
721
731
 
722
- // Notify JS of the favorite state change
732
+ // Notify JS of the favorite state change. This is the ONLY emit: a favorite
733
+ // toggle is an in-place mutation of the active track, not a transition —
734
+ // onPlaybackActiveTrackChanged / onPlaybackQueueChanged stay
735
+ // transition-only, and the useActiveTrack / useQueue hooks subscribe to
736
+ // onFavoriteChanged themselves.
723
737
  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)
738
+ return true
738
739
  }
739
740
 
740
- /** Toggles the favorited state of the currently playing track. */
741
- fun toggleActiveTrackFavorited() {
742
- val currentTrack = this.currentTrack ?: return
743
- setActiveTrackFavorited(currentTrack.favorited != true)
741
+ /**
742
+ * Toggles the favorited state of the currently playing track.
743
+ *
744
+ * @return whether the toggle applied — false when there is no current track.
745
+ */
746
+ fun toggleActiveTrackFavorited(): Boolean {
747
+ val currentTrack = this.currentTrack ?: return false
748
+ return setActiveTrackFavorited(currentTrack.favorited != true)
744
749
  }
745
750
 
746
751
  // MARK: - Now Playing Metadata
@@ -926,10 +931,23 @@ class Player(internal val context: Context) {
926
931
  }
927
932
  }
928
933
 
929
- /** Jump to the live edge (default position) of a live item; no-op otherwise. */
934
+ /**
935
+ * Jump to the live edge of the current track; no-op unless it declares `live` — the track's own
936
+ * declaration is the gate (matching iOS and the spec), not ExoPlayer's stream-derived liveness,
937
+ * which never marks progressive ICY radio live. A stream with a live window seeks to its edge;
938
+ * anything else reconnects — for a non-seekable stream a fresh connection IS the live edge. A
939
+ * stale URL recovers through the failure-armed re-resolve in [TransformingDataSource].
940
+ */
930
941
  fun seekToLiveEdge() {
942
+ if (currentTrack?.live != true) return
931
943
  if (exoPlayer.isCurrentMediaItemLive) {
932
944
  exoPlayer.seekToDefaultPosition()
945
+ } else {
946
+ // ExoPlayer.prepare() only re-prepares from STATE_IDLE, hence the raw stop() — not
947
+ // Player.stop(), which would drop the play intent this reconnect must preserve.
948
+ exoPlayer.stop()
949
+ exoPlayer.seekToDefaultPosition()
950
+ exoPlayer.prepare()
933
951
  }
934
952
  }
935
953
 
@@ -1249,8 +1267,8 @@ class Player(internal val context: Context) {
1249
1267
 
1250
1268
  if (capabilitiesChanged) {
1251
1269
  // The `favorite` capability is the single favoriting switch — propagate
1252
- // its match mode to the browser so it can hydrate row hearts.
1253
- browser?.browserManager?.setFavoriteMatch(options.capabilities.favoriteMatch)
1270
+ // it to the browser so it can hydrate row hearts.
1271
+ browser?.browserManager?.setFavoriteEnabled(options.capabilities.favoriteEnabled)
1254
1272
  }
1255
1273
 
1256
1274
  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() = favoriteMatch != null
7
+ get() = favorite == true
@@ -8,14 +8,19 @@ import java.net.URLEncoder
8
8
  * Utility for handling browser paths and contextual URLs in the media browser system.
9
9
  *
10
10
  * Handles two types of special paths:
11
- * 1. System paths (prefixed with `/__`): Root, recent, and search paths
11
+ * 1. System paths (prefixed with `/__`): root, search, offline, error, and gate paths
12
12
  * 2. Contextual URLs: Embed parent context in track identifiers for Media3 integration
13
13
  *
14
- * Contextual URL format: `{parentPath}?__trackId={trackSrc}` Example:
15
- * "/library/radio?__trackId=song.mp3"
14
+ * Contextual URL format: `{parentPath}?__trackId={trackIdentity}&__index={childIndex}` (the
15
+ * identity is the track's `id` when non-blank, else its `src`) Example:
16
+ * "/library/radio?__trackId=song.mp3&__index=2"
17
+ *
18
+ * `__trackId` is the identity check; `__index` (the child's position on the page at stamp time) is
19
+ * only a tie-breaker between surfaces that carry the same identity — a stale index never selects a
20
+ * different track.
16
21
  *
17
22
  * This allows:
18
- * - Media3 to reference playable-only tracks (tracks with `src` but no `url`)
23
+ * - Media3 to reference playable-only tracks (tracks with `src` but no `path`)
19
24
  * - Cache lookup to work consistently
20
25
  * - Parent context to be preserved for queue restoration
21
26
  */
@@ -23,9 +28,6 @@ object BrowserPathHelper {
23
28
  /** Root path for media browsing */
24
29
  const val ROOT_PATH = "/__root"
25
30
 
26
- /** Recent media path for playback resumption */
27
- const val RECENT_PATH = "/__recent"
28
-
29
31
  /** Search path prefix (full path is /__search?q=query) */
30
32
  const val SEARCH_PATH_PREFIX = "/__search"
31
33
 
@@ -41,9 +43,12 @@ object BrowserPathHelper {
41
43
  // Query parameter name for contextual track identifiers
42
44
  private const val CONTEXTUAL_TRACK_PARAM = "__trackId"
43
45
 
46
+ // Query parameter name for the tapped child's page position (tie-breaker)
47
+ private const val CONTEXTUAL_INDEX_PARAM = "__index"
48
+
44
49
  /** Check if a path is a special system path (not a regular navigation path) */
45
50
  fun isSpecialPath(path: String): Boolean {
46
- return path == ROOT_PATH || path == RECENT_PATH || path.startsWith("$SEARCH_PATH_PREFIX?")
51
+ return path == ROOT_PATH || path.startsWith("$SEARCH_PATH_PREFIX?")
47
52
  }
48
53
 
49
54
  /** Create a search path for a given query */
@@ -63,13 +68,13 @@ object BrowserPathHelper {
63
68
  }
64
69
 
65
70
  /**
66
- * Strips the __trackId parameter from a contextual URL to get the parent path. If the URL is not
67
- * contextual, returns it unchanged.
71
+ * Strips the contextual parameters (__trackId and __index) from a contextual URL to get the
72
+ * parent path. If the URL is not contextual, returns it unchanged.
68
73
  *
69
74
  * @param url The URL to process
70
- * @return The URL without the __trackId parameter
75
+ * @return The URL without the contextual parameters
71
76
  *
72
- * Example: "/library/radio?__trackId=song.mp3" → "/library/radio" Example:
77
+ * Example: "/library/radio?__trackId=song.mp3&__index=2" → "/library/radio" Example:
73
78
  * "/search?q=jazz&__trackId=song.mp3" → "/search?q=jazz"
74
79
  */
75
80
  fun stripTrackId(url: String): String {
@@ -79,13 +84,13 @@ object BrowserPathHelper {
79
84
 
80
85
  val uri = url.toUri()
81
86
 
82
- // Build URL preserving everything except __trackId parameter
87
+ // Build URL preserving everything except the contextual parameters
83
88
  val builder = uri.buildUpon()
84
89
  builder.clearQuery()
85
90
 
86
- // Re-add all query params except __trackId
91
+ // Re-add all query params except __trackId and __index
87
92
  uri.queryParameterNames.forEach { paramName ->
88
- if (paramName != CONTEXTUAL_TRACK_PARAM) {
93
+ if (paramName != CONTEXTUAL_TRACK_PARAM && paramName != CONTEXTUAL_INDEX_PARAM) {
89
94
  uri.getQueryParameters(paramName).forEach { value ->
90
95
  builder.appendQueryParameter(paramName, value)
91
96
  }
@@ -96,19 +101,21 @@ object BrowserPathHelper {
96
101
  }
97
102
 
98
103
  /**
99
- * Builds a contextual URL by appending a track identifier to a parent path. Handles existing
100
- * query parameters correctly.
104
+ * Builds a contextual URL by appending a track identifier — and optionally the tapped child's
105
+ * page position — to a parent path. Handles existing query parameters correctly.
101
106
  *
102
107
  * @param parentPath The parent container path
103
- * @param trackId The track identifier (typically the `src` value)
104
- * @return A contextual URL combining parent path and track ID
108
+ * @param trackId The track identity (`id` when non-blank, else `src` — see Track.identity)
109
+ * @param index The child's position on the page at stamp time (tie-breaker)
110
+ * @return A contextual URL combining parent path, track ID, and index
105
111
  *
106
- * Example: build("/library", "song.mp3") → "/library?__trackId=song.mp3" Example:
112
+ * Example: build("/library", "song.mp3", 2) → "/library?__trackId=song.mp3&__index=2" Example:
107
113
  * build("/search?q=jazz", "song.mp3") → "/search?q=jazz&__trackId=song.mp3"
108
114
  */
109
- fun build(parentPath: String, trackId: String): String {
115
+ fun build(parentPath: String, trackId: String, index: Int? = null): String {
110
116
  val separator = if (parentPath.contains('?')) '&' else '?'
111
- return "$parentPath$separator$CONTEXTUAL_TRACK_PARAM=${Uri.encode(trackId)}"
117
+ val indexParam = if (index != null) "&$CONTEXTUAL_INDEX_PARAM=$index" else ""
118
+ return "$parentPath$separator$CONTEXTUAL_TRACK_PARAM=${Uri.encode(trackId)}$indexParam"
112
119
  }
113
120
 
114
121
  /**
@@ -129,6 +136,21 @@ object BrowserPathHelper {
129
136
  return uri.getQueryParameter(CONTEXTUAL_TRACK_PARAM)
130
137
  }
131
138
 
139
+ /**
140
+ * Extracts the stamped page index from a contextual URL, or null when the URL is not contextual
141
+ * or carries no (valid, non-negative) index.
142
+ *
143
+ * Example: "/library/radio?__trackId=song.mp3&__index=2" → 2
144
+ */
145
+ fun extractIndex(path: String): Int? {
146
+ if (!isContextual(path)) {
147
+ return null
148
+ }
149
+
150
+ val raw = Uri.parse(path).getQueryParameter(CONTEXTUAL_INDEX_PARAM) ?: return null
151
+ return raw.toIntOrNull()?.takeIf { it >= 0 }
152
+ }
153
+
132
154
  /**
133
155
  * Combines a base URL with a path, ensuring proper slash handling.
134
156
  *
@@ -158,22 +180,4 @@ object BrowserPathHelper {
158
180
  val normalizedPath = path.trimStart('/')
159
181
  return "$normalizedBase$normalizedPath"
160
182
  }
161
-
162
- /**
163
- * True if [segment] appears in [path] as a complete path segment — bounded on the left by the
164
- * string start or `/`, and on the right by the string end or one of `/`, `?`, `#`.
165
- */
166
- fun containsSegment(path: String, segment: String): Boolean {
167
- if (segment.isEmpty()) return false
168
- var from = 0
169
- while (true) {
170
- val idx = path.indexOf(segment, from)
171
- if (idx < 0) return false
172
- val beforeOk = idx == 0 || path[idx - 1] == '/'
173
- val end = idx + segment.length
174
- val afterOk = end == path.length || path[end] == '/' || path[end] == '?' || path[end] == '#'
175
- if (beforeOk && afterOk) return true
176
- from = idx + 1
177
- }
178
- }
179
183
  }
@@ -2,30 +2,31 @@ package com.audiobrowser.util
2
2
 
3
3
  import android.os.Bundle
4
4
  import androidx.media.utils.MediaConstants
5
- import com.margelo.nitro.audiobrowser.ResolvedTrack
5
+ import com.margelo.nitro.audiobrowser.Section
6
+ import com.margelo.nitro.audiobrowser.StyleDisplay
6
7
  import com.margelo.nitro.audiobrowser.Track
7
- import com.margelo.nitro.audiobrowser.TrackStyle
8
8
 
9
9
  /** Builds MediaMetadata extras bundle for Android Auto/AAOS content styling. */
10
10
  object MediaExtrasBuilder {
11
11
 
12
12
  /**
13
- * Maps TrackStyle to MediaConstants content style value. When artwork is an android.resource://
14
- * URI, uses CATEGORY_* variants which add margins around icons (better for small vector icons).
13
+ * Maps a display mode to the MediaConstants content style value. When artwork is an
14
+ * android.resource:// URI, uses CATEGORY_* variants which add margins around icons (better for
15
+ * small vector icons).
15
16
  *
16
17
  * @see <a href="https://developer.android.com/training/cars/media#default-content-style">Default
17
18
  * content style</a>
18
19
  */
19
- private fun TrackStyle.toContentStyleValue(artwork: String?): Int {
20
+ private fun StyleDisplay.toContentStyleValue(artwork: String?): Int {
20
21
  val isAndroidResource = artwork?.startsWith("android.resource://") == true
21
22
  return when (this) {
22
- TrackStyle.LIST ->
23
+ StyleDisplay.LIST ->
23
24
  if (isAndroidResource) {
24
25
  MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM
25
26
  } else {
26
27
  MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
27
28
  }
28
- TrackStyle.GRID ->
29
+ StyleDisplay.GRID ->
29
30
  if (isAndroidResource) {
30
31
  MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM
31
32
  } else {
@@ -34,39 +35,43 @@ object MediaExtrasBuilder {
34
35
  }
35
36
  }
36
37
 
37
- fun build(track: Track): Bundle =
38
+ /**
39
+ * Extras for a track rendered on a browse surface. The owning [section] supplies the Android Auto
40
+ * group-title header hint (grouping is a per-item advisory in the flat MediaBrowser protocol —
41
+ * the section dies here, ADR 0010) and, from its page-folded style block, the per-item display
42
+ * hint: per-item hints derive from the section resolution, never from the track's own `display`,
43
+ * which is positional (ADR 0011).
44
+ *
45
+ * The track's own declared `display` is the page-layout *promise* for the page a browsable track
46
+ * opens — emitted as the parent-level BROWSABLE/PLAYABLE hint, the only below-root hint
47
+ * AOSP-derived AAOS media UIs honor. Declared or it doesn't exist: the library never derives a
48
+ * promise from pages it hasn't resolved, and never back-fills from ones it has.
49
+ */
50
+ fun build(track: Track, section: Section? = null): Bundle =
38
51
  build(
39
- groupTitle = track.groupTitle,
40
- style = track.style,
41
- childrenStyle = track.childrenStyle,
52
+ groupTitle = section?.title,
53
+ itemDisplay = section?.style?.display,
54
+ promisedDisplay = if (track.src == null) track.style?.display else null,
42
55
  artwork = track.artwork?.url,
43
56
  )
44
57
 
45
- fun build(resolvedTrack: ResolvedTrack): Bundle =
46
- build(
47
- groupTitle = resolvedTrack.groupTitle,
48
- style = resolvedTrack.style,
49
- childrenStyle = resolvedTrack.childrenStyle,
50
- artwork = resolvedTrack.artwork?.url,
51
- )
52
-
53
58
  private fun build(
54
59
  groupTitle: String?,
55
- style: TrackStyle?,
56
- childrenStyle: TrackStyle?,
60
+ itemDisplay: StyleDisplay?,
61
+ promisedDisplay: StyleDisplay?,
57
62
  artwork: String?,
58
63
  ): Bundle =
59
64
  Bundle().apply {
60
65
  groupTitle?.let {
61
66
  putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, it)
62
67
  }
63
- style?.let {
68
+ itemDisplay?.let {
64
69
  putInt(
65
70
  MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM,
66
71
  it.toContentStyleValue(artwork),
67
72
  )
68
73
  }
69
- childrenStyle?.let {
74
+ promisedDisplay?.let {
70
75
  val styleValue = it.toContentStyleValue(artwork)
71
76
  putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, styleValue)
72
77
  putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, styleValue)
@@ -0,0 +1,35 @@
1
+ package com.audiobrowser.util
2
+
3
+ import com.margelo.nitro.audiobrowser.ArtworkRendering
4
+ import com.margelo.nitro.audiobrowser.StyleDisplay
5
+
6
+ /**
7
+ * The style block's wire strings (ADR 0011) ↔ Nitro enums, shared by the browse decoder and the
8
+ * playback-state store. Inbound mapping is lenient: an unknown or empty value is "no declaration",
9
+ * never an error.
10
+ */
11
+ internal fun StyleDisplay.toWireString(): String =
12
+ when (this) {
13
+ StyleDisplay.LIST -> "list"
14
+ StyleDisplay.GRID -> "grid"
15
+ }
16
+
17
+ internal fun ArtworkRendering.toWireString(): String =
18
+ when (this) {
19
+ ArtworkRendering.ORIGINAL -> "original"
20
+ ArtworkRendering.STENCIL -> "stencil"
21
+ }
22
+
23
+ internal fun String?.toStyleDisplay(): StyleDisplay? =
24
+ when (this?.takeIf { it.isNotEmpty() }?.lowercase()) {
25
+ "list" -> StyleDisplay.LIST
26
+ "grid" -> StyleDisplay.GRID
27
+ else -> null
28
+ }
29
+
30
+ internal fun String?.toArtworkRendering(): ArtworkRendering? =
31
+ when (this?.takeIf { it.isNotEmpty() }?.lowercase()) {
32
+ "original" -> ArtworkRendering.ORIGINAL
33
+ "stencil" -> ArtworkRendering.STENCIL
34
+ else -> null
35
+ }
@@ -7,17 +7,16 @@ import androidx.media3.common.MediaItem
7
7
  import androidx.media3.common.MediaMetadata
8
8
  import com.audiobrowser.browser.BrowseArtworkRegistry
9
9
  import com.audiobrowser.browser.ResolvedArtwork
10
- import com.margelo.nitro.audiobrowser.ImageRowItem
10
+ import com.margelo.nitro.audiobrowser.Section
11
11
  import com.margelo.nitro.audiobrowser.Track
12
12
  import com.margelo.nitro.audiobrowser.TrackStyle
13
- import com.margelo.nitro.audiobrowser.Variant_String_ArtworkVariants
14
13
 
15
14
  /**
16
15
  * The single Track → Media3 [MediaItem] conversion. Owns the two easy-to-drift fallbacks: the
17
16
  * displayed artwork is the transformed `artworkSource.uri` falling back to the raw `artwork` field,
18
- * and the mediaId is the stable `id` for playable tracks, falling back to `url` then `src` (a Track
19
- * must have one of the latter two — see the `src` vs `url` note in CONTEXT.md). The list line
20
- * renders from `subtitle` (the now-playing line is re-stamped from `artist` by the now-playing
17
+ * and the mediaId is the stable `id` for playable tracks, falling back to `path` then `src` (a
18
+ * Track must have one of the latter two — see the `src` vs `path` note in CONTEXT.md). The list
19
+ * line renders from `subtitle` (the now-playing line is re-stamped from `artist` by the now-playing
21
20
  * pipeline; see the Now Playing Metadata guide).
22
21
  */
23
22
  object TrackFactory {
@@ -26,49 +25,35 @@ object TrackFactory {
26
25
  }
27
26
 
28
27
  /**
29
- * Android Auto has no image-row rendering; its closest equivalent is a grid of artwork tiles. A
30
- * track carrying `imageRow` expands into its items as grid-styled rows (the per-item
31
- * content-style hint — hosts that ignore it fall back to list rows) grouped under the row's
32
- * title, followed by the row itself as a browsable "view all" link when it has a `url` (a
33
- * url-less row is a pure preview and contributes only its items). Tracks without an `imageRow`
34
- * pass through unchanged.
28
+ * A synthetic browsable Track for a section's "view all" surface — the section has a path, title,
29
+ * and label, but no Track (ADR 0010). It carries the section's declared item block as its own,
30
+ * `display` included: the section's declaration about this content's layout is the only declared
31
+ * promise the "view all" page can ever have (there is no consumer-authored handle to declare one
32
+ * on), and without it Android Auto would always render that page as a list.
35
33
  */
36
- fun expandImageRows(tracks: List<Track>): List<Track> =
37
- tracks.flatMap { track ->
38
- val items = track.imageRow
39
- if (items.isNullOrEmpty()) return@flatMap listOf(track)
40
- val expanded = items.map { it.toTrack(groupTitle = track.title) }
41
- if (track.url != null) {
42
- expanded + track.copy(imageRow = null, groupTitle = track.title)
43
- } else {
44
- expanded
45
- }
46
- }
47
-
48
- /** The row-item equivalent of a full Track, for surfaces that render items as plain rows. */
49
- fun ImageRowItem.toTrack(groupTitle: String?): Track =
34
+ fun navigationTrack(section: Section): Track =
50
35
  Track(
51
- id = id,
52
- url = url,
53
- src = src,
54
- artwork = artwork?.let { Variant_String_ArtworkVariants.First(it) },
55
- artworkSource = artworkSource,
56
- request = request,
57
- artworkCarPlayTinted = null,
58
- title = title,
59
- subtitle = null,
60
- artist = artist,
61
- albumUrl = albumUrl,
62
- album = album,
36
+ id = null,
37
+ path = section.path,
38
+ src = null,
39
+ artwork = null,
40
+ artworkSource = null,
41
+ request = null,
42
+ title = section.title ?: "",
43
+ subtitle = section.subtitle,
44
+ artist = null,
45
+ albumPath = null,
46
+ album = null,
63
47
  description = null,
64
48
  genre = null,
65
49
  duration = null,
66
- style = TrackStyle.GRID,
67
- childrenStyle = null,
50
+ style =
51
+ section.style?.let {
52
+ TrackStyle(display = it.display, artworkRendering = it.artworkRendering)
53
+ },
54
+ disabled = null,
68
55
  favorited = null,
69
- groupTitle = groupTitle,
70
- live = live,
71
- imageRow = null,
56
+ live = null,
72
57
  )
73
58
 
74
59
  fun toMedia3(tracks: Array<Track>): List<MediaItem> {
@@ -90,10 +75,11 @@ object TrackFactory {
90
75
  track: Track,
91
76
  registry: BrowseArtworkRegistry,
92
77
  authority: String,
78
+ section: Section? = null,
93
79
  ): MediaItem {
94
80
  val rawUrl = artworkUri(track) // artworkSource.uri ?: artwork
95
81
  val scheme = rawUrl?.let { Uri.parse(it).scheme?.lowercase() }
96
- val builder = metadataBuilder(track)
82
+ val builder = metadataBuilder(track, section)
97
83
  if (rawUrl != null && (scheme == "http" || scheme == "https")) {
98
84
  val isSvg =
99
85
  SvgArtworkRenderer.isSvgUrl(rawUrl) || SvgArtworkRenderer.isSvgUrl(track.artwork?.url)
@@ -109,8 +95,11 @@ object TrackFactory {
109
95
  /** The transformed artworkSource wins over the raw artwork field. */
110
96
  private fun artworkUri(track: Track): String? = track.artworkSource?.uri ?: track.artwork?.url
111
97
 
112
- /** All metadata except artwork. */
113
- private fun metadataBuilder(track: Track): MediaMetadata.Builder =
98
+ /**
99
+ * All metadata except artwork. The owning [section] (browse surfaces only) supplies the
100
+ * group-title header hint and the per-item style default (ADR 0010).
101
+ */
102
+ private fun metadataBuilder(track: Track, section: Section? = null): MediaMetadata.Builder =
114
103
  MediaMetadata.Builder()
115
104
  .setTitle(track.title)
116
105
  .setArtist(track.subtitle)
@@ -119,26 +108,29 @@ object TrackFactory {
119
108
  .setGenre(track.genre)
120
109
  .setIsBrowsable(track.src == null)
121
110
  .setIsPlayable(track.src != null)
122
- .setExtras(MediaExtrasBuilder.build(track))
111
+ .setExtras(MediaExtrasBuilder.build(track, section))
123
112
  .apply { track.favorited?.let { setUserRating(HeartRating(it)) } }
124
113
 
125
114
  private fun buildMediaItem(track: Track, metadata: MediaMetadata): MediaItem {
126
- // A playable track's mediaId is the consumer's stable `id` when it has one.
127
- // Android Auto marks the "now playing" browse row by exact mediaId equality
128
- // between the row and the player's current item (media3 announces
129
- // `currentMediaItem.mediaId` in the legacy playback state), and a
115
+ // A playable track's mediaId is the consumer's stable `id` when it has one —
116
+ // the identity rule (id when non-blank, else src; see Track.identity /
117
+ // ADR 0008), except that an id-less playable track prefers its contextual
118
+ // `path` over `src` here, so this is deliberately NOT expressed via
119
+ // Track.identity. Android Auto marks the "now playing" browse row by exact
120
+ // mediaId equality between the row and the player's current item (media3
121
+ // announces `currentMediaItem.mediaId` in the legacy playback state), and a
130
122
  // consumer-loaded track's `src` can differ textually from the browse row's
131
123
  // for the same item (absolute vs relative, extra query params) — the id is
132
124
  // the one string both sides share. It is also context-free, so the same
133
125
  // station gets the same mediaId in every tab. Browsable-only tracks keep
134
- // `url` so navigation parentIds stay resolvable paths.
126
+ // `path` so navigation parentIds stay resolvable paths.
135
127
  val stableId = if (track.src != null) track.id?.takeUnless { it.isBlank() } else null
136
128
  val mediaId =
137
129
  stableId
138
- ?: track.url
130
+ ?: track.path
139
131
  ?: track.src
140
132
  ?: throw IllegalArgumentException(
141
- "Track must have either url or src defined. Track: title='${track.title}', artist='${track.artist}'"
133
+ "Track must have either path or src defined. Track: title='${track.title}', artist='${track.artist}'"
142
134
  )
143
135
  return MediaItem.Builder()
144
136
  .setMediaId(mediaId)