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
@@ -1,7 +1,9 @@
1
1
  package com.audiobrowser.browser
2
2
 
3
- import com.audiobrowser.TestFixtures.imageRowItem
3
+ import androidx.media3.common.C
4
+ import androidx.media3.common.MediaItem
4
5
  import com.audiobrowser.TestFixtures.resolvedTrack
6
+ import com.audiobrowser.TestFixtures.section
5
7
  import com.audiobrowser.TestFixtures.staticRoute
6
8
  import com.audiobrowser.TestFixtures.track
7
9
  import com.audiobrowser.util.BrowserPathHelper
@@ -17,14 +19,14 @@ import org.robolectric.RobolectricTestRunner
17
19
  /**
18
20
  * Navigation and queue-expansion glue.
19
21
  *
20
- * [BrowserManager.expandQueueFromContextualUrl] is the seam between the page a listener tapped in
22
+ * [BrowserManager.expandQueueFromContextualPath] is the seam between the page a listener tapped in
21
23
  * and the queue that starts playing. Its two halves are covered on their own — the section logic in
22
24
  * [SectionScopeTest], the caller's single-track fallback in the player tests — but the wiring
23
25
  * between them is what decides whether tapping row 3 of a page resumes the right station, and only
24
26
  * these tests exercise it.
25
27
  *
26
28
  * Routes serve `browseStatic`, the one browse arm that needs neither the JNI bridge nor HTTP.
27
- * Robolectric is required for `android.net.Uri` (contextual-URL parsing) and
29
+ * Robolectric is required for `android.net.Uri` (contextual-path parsing) and
28
30
  * `android.util.LruCache` (the content cache).
29
31
  */
30
32
  @RunWith(RobolectricTestRunner::class)
@@ -46,11 +48,20 @@ class BrowserManagerTest {
46
48
  browserManager.config =
47
49
  BrowserConfig(
48
50
  routes =
49
- arrayOf(staticRoute(path, resolvedTrack(url = path, children = arrayOf(*children)))),
51
+ arrayOf(staticRoute(path, resolvedTrack(path = path, children = arrayOf(*children)))),
50
52
  singleTrack = singleTrack,
51
53
  )
52
54
  }
53
55
 
56
+ /** Configures a single static route at [path] serving [sections] as its page. */
57
+ private fun serveSections(path: String, vararg sections: com.margelo.nitro.audiobrowser.Section) {
58
+ browserManager.config =
59
+ BrowserConfig(
60
+ routes =
61
+ arrayOf(staticRoute(path, resolvedTrack(path = path, sections = arrayOf(*sections))))
62
+ )
63
+ }
64
+
54
65
  private fun contextual(path: String, trackId: String) = BrowserPathHelper.build(path, trackId)
55
66
 
56
67
  @Test
@@ -100,28 +111,30 @@ class BrowserManagerTest {
100
111
 
101
112
  val resolved = browserManager.navigate("/library")
102
113
 
103
- assertEquals(listOf("A", "B"), resolved.children?.map { it.title })
104
- // The tap target: a child's url is rewritten to point back at this page plus its own id.
114
+ assertEquals(listOf("A", "B"), resolved.flattenedChildren?.map { it.title })
115
+ // The tap target: a child's path is rewritten to point back at this page
116
+ // plus its own id and page position (the duplicate-identity tie-breaker).
105
117
  assertEquals(
106
- listOf(contextual("/library", "a"), contextual("/library", "b")),
107
- resolved.children?.map { it.url },
118
+ listOf(
119
+ BrowserPathHelper.build("/library", "a", 0),
120
+ BrowserPathHelper.build("/library", "b", 1),
121
+ ),
122
+ resolved.flattenedChildren?.map { it.path },
108
123
  )
109
124
  assertEquals("/library", browserManager.getPath())
110
125
  }
111
126
 
112
- // MARK: - expandQueueFromContextualUrl
127
+ // MARK: - expandQueueFromContextualPath
113
128
 
114
129
  @Test
115
- fun `expandQueueFromContextualUrl scopes the queue to the tapped section`() = runBlocking {
116
- servePage(
130
+ fun `expandQueueFromContextualPath scopes the queue to the tapped section`() = runBlocking {
131
+ serveSections(
117
132
  "/home",
118
- track(src = "a", groupTitle = "Recent"),
119
- track(src = "b", groupTitle = "Recent"),
120
- track(src = "c", groupTitle = "Popular"),
121
- track(src = "d", groupTitle = "Popular"),
133
+ section(title = "Recent", children = arrayOf(track(src = "a"), track(src = "b"))),
134
+ section(title = "Popular", children = arrayOf(track(src = "c"), track(src = "d"))),
122
135
  )
123
136
 
124
- val expanded = browserManager.expandQueueFromContextualUrl(contextual("/home", "c"))
137
+ val expanded = browserManager.expandQueueFromContextualPath(contextual("/home", "c"))
125
138
 
126
139
  // "Popular" only — a page aggregating sections must not leak next/previous across them.
127
140
  val (queue, index) = requireNotNull(expanded)
@@ -130,97 +143,262 @@ class BrowserManagerTest {
130
143
  }
131
144
 
132
145
  @Test
133
- fun `expandQueueFromContextualUrl indexes the tapped track within its section`() = runBlocking {
134
- servePage(
146
+ fun `expandQueueFromContextualPath indexes the tapped track within its section`() = runBlocking {
147
+ serveSections(
135
148
  "/home",
136
- track(src = "a", groupTitle = "Recent"),
137
- track(src = "b", groupTitle = "Popular"),
138
- track(src = "c", groupTitle = "Popular"),
149
+ section(title = "Recent", children = arrayOf(track(src = "a"))),
150
+ section(title = "Popular", children = arrayOf(track(src = "b"), track(src = "c"))),
139
151
  )
140
152
 
141
153
  val (queue, index) =
142
- requireNotNull(browserManager.expandQueueFromContextualUrl(contextual("/home", "c")))
154
+ requireNotNull(browserManager.expandQueueFromContextualPath(contextual("/home", "c")))
143
155
 
144
156
  assertEquals(listOf("b", "c"), queue.map { it.src })
145
157
  assertEquals(1, index)
146
158
  }
147
159
 
148
160
  @Test
149
- fun `expandQueueFromContextualUrl returns null when the id has vanished from the page`() =
161
+ fun `expandQueueFromContextualPath pins the tapped copy of a duplicate identity`() = runBlocking {
162
+ // The same src twice in one section (normal for music playlists): the
163
+ // stamped page index selects the tapped copy, so "next" continues from
164
+ // there instead of from the first copy.
165
+ servePage("/playlist", track(src = "a"), track(src = "b"), track(src = "a"), track(src = "c"))
166
+
167
+ val (queue, index) =
168
+ requireNotNull(
169
+ browserManager.expandQueueFromContextualPath(BrowserPathHelper.build("/playlist", "a", 2))
170
+ )
171
+
172
+ assertEquals(listOf("a", "b", "a", "c"), queue.map { it.src })
173
+ assertEquals(2, index)
174
+ }
175
+
176
+ @Test
177
+ fun `expandQueueFromContextualPath scopes a cross-section duplicate to the tapped section`() =
150
178
  runBlocking {
151
- // The stored url points at a track the page no longer lists — the page changed under a
179
+ // The same identity in a rail section AND the list section below it:
180
+ // the stamped flat index resolves the tap to the list section.
181
+ serveSections(
182
+ "/home",
183
+ section(
184
+ title = "Row",
185
+ style =
186
+ com.margelo.nitro.audiobrowser.SectionStyle(
187
+ display = com.margelo.nitro.audiobrowser.StyleDisplay.GRID,
188
+ artworkRendering = null,
189
+ gridWrap = false,
190
+ ),
191
+ children = arrayOf(track(src = "dup")),
192
+ ),
193
+ section(title = "Stations", children = arrayOf(track(src = "dup"), track(src = "b"))),
194
+ )
195
+
196
+ val (queue, index) =
197
+ requireNotNull(
198
+ browserManager.expandQueueFromContextualPath(BrowserPathHelper.build("/home", "dup", 1))
199
+ )
200
+
201
+ assertEquals(listOf("dup", "b"), queue.map { it.src })
202
+ assertEquals(0, index)
203
+ }
204
+
205
+ @Test
206
+ fun `expandQueueFromContextualPath falls back to the first match on a stale index`() =
207
+ runBlocking {
208
+ // The list shifted since the stamp: the child at the index no longer
209
+ // carries the identity, so resolution ignores the index and falls back
210
+ // to the first identity match instead of aborting.
211
+ servePage("/home", track(src = "x"), track(src = "a"), track(src = "b"))
212
+
213
+ val (queue, index) =
214
+ requireNotNull(
215
+ browserManager.expandQueueFromContextualPath(BrowserPathHelper.build("/home", "a", 0))
216
+ )
217
+
218
+ assertEquals(listOf("x", "a", "b"), queue.map { it.src })
219
+ assertEquals(1, index)
220
+ }
221
+
222
+ @Test
223
+ fun `expandQueueFromContextualPath returns null when the id has vanished from the page`() =
224
+ runBlocking {
225
+ // The stored path points at a track the page no longer lists — the page changed under a
152
226
  // resumed session. Queueing the current list would resume the wrong station, so expansion
153
227
  // aborts and the caller falls back to the stored single track.
154
228
  servePage("/home", track(src = "a"), track(src = "b"))
155
229
 
156
- assertNull(browserManager.expandQueueFromContextualUrl(contextual("/home", "gone")))
230
+ assertNull(browserManager.expandQueueFromContextualPath(contextual("/home", "gone")))
157
231
  }
158
232
 
159
233
  @Test
160
- fun `expandQueueFromContextualUrl returns null for a non-contextual url`() = runBlocking {
234
+ fun `expandQueueFromContextualPath returns null for a non-contextual path`() = runBlocking {
161
235
  servePage("/home", track(src = "a"))
162
236
 
163
- assertNull(browserManager.expandQueueFromContextualUrl("/home"))
237
+ assertNull(browserManager.expandQueueFromContextualPath("/home"))
164
238
  }
165
239
 
166
240
  @Test
167
- fun `expandQueueFromContextualUrl returns null when the page has no children`() = runBlocking {
241
+ fun `expandQueueFromContextualPath returns null when the page has no children`() = runBlocking {
168
242
  browserManager.config =
169
- BrowserConfig(routes = arrayOf(staticRoute("/home", resolvedTrack(url = "/home"))))
243
+ BrowserConfig(routes = arrayOf(staticRoute("/home", resolvedTrack(path = "/home"))))
170
244
 
171
- assertNull(browserManager.expandQueueFromContextualUrl(contextual("/home", "a")))
245
+ assertNull(browserManager.expandQueueFromContextualPath(contextual("/home", "a")))
172
246
  }
173
247
 
174
248
  @Test
175
- fun `expandQueueFromContextualUrl expands an image row into its items`() = runBlocking {
176
- servePage(
249
+ fun `expandQueueFromContextualPath scopes a rail section to its own tiles`() = runBlocking {
250
+ serveSections(
177
251
  "/home",
178
- track(
252
+ section(
179
253
  title = "Most Played",
180
- src = null,
181
- imageRow = arrayOf(imageRowItem("r1"), imageRowItem("r2")),
254
+ style =
255
+ com.margelo.nitro.audiobrowser.SectionStyle(
256
+ display = com.margelo.nitro.audiobrowser.StyleDisplay.GRID,
257
+ artworkRendering = null,
258
+ gridWrap = false,
259
+ ),
260
+ children = arrayOf(track(src = "r1"), track(src = "r2")),
182
261
  ),
183
- track(src = "a"),
262
+ section(children = arrayOf(track(src = "a"))),
184
263
  )
185
264
 
186
265
  val (queue, index) =
187
- requireNotNull(browserManager.expandQueueFromContextualUrl(contextual("/home", "r2")))
266
+ requireNotNull(browserManager.expandQueueFromContextualPath(contextual("/home", "r2")))
188
267
 
189
- // A tile tap queues its row, not the list rows beside it.
268
+ // A tile tap queues its section, not the list rows beside it.
190
269
  assertEquals(listOf("r1", "r2"), queue.map { it.src })
191
270
  assertEquals(1, index)
192
271
  }
193
272
 
194
273
  @Test
195
- fun `expandQueueFromContextualUrl drops unplayable siblings`() = runBlocking {
196
- servePage(
274
+ fun `expandQueueFromContextualPath drops unplayable siblings`() = runBlocking {
275
+ serveSections(
197
276
  "/home",
198
- track(src = "a", groupTitle = "Mixed"),
199
- // A browsable row sitting inside the same section — no src, so not queueable.
200
- track(src = null, groupTitle = "Mixed").copy(url = "/sub"),
201
- track(src = "b", groupTitle = "Mixed"),
277
+ section(
278
+ title = "Mixed",
279
+ children =
280
+ arrayOf(
281
+ track(src = "a"),
282
+ // A browsable row sitting inside the same section — no src, so not queueable.
283
+ track(src = null).copy(path = "/sub"),
284
+ track(src = "b"),
285
+ ),
286
+ ),
202
287
  )
203
288
 
204
289
  val (queue, index) =
205
- requireNotNull(browserManager.expandQueueFromContextualUrl(contextual("/home", "b")))
290
+ requireNotNull(browserManager.expandQueueFromContextualPath(contextual("/home", "b")))
206
291
 
207
292
  assertEquals(listOf("a", "b"), queue.map { it.src })
208
293
  assertEquals(1, index)
209
294
  }
210
295
 
211
296
  @Test
212
- fun `expandQueueFromContextualUrl honours singleTrack`() = runBlocking {
297
+ fun `navigate stamps the identity, not the src, into id-bearing children's contextual urls`() =
298
+ runBlocking {
299
+ servePage(
300
+ "/home",
301
+ track(id = "stable-a", src = "https://s/a.mp3", title = "A"),
302
+ track(src = "https://s/b.mp3", title = "B"),
303
+ )
304
+
305
+ val resolved = browserManager.navigate("/home")
306
+
307
+ // id when non-blank, else src — Track.identity (ADR 0008).
308
+ assertEquals(
309
+ listOf(
310
+ BrowserPathHelper.build("/home", "stable-a", 0),
311
+ BrowserPathHelper.build("/home", "https://s/b.mp3", 1),
312
+ ),
313
+ resolved.flattenedChildren?.map { it.path },
314
+ )
315
+ }
316
+
317
+ @Test
318
+ fun `expandQueueFromContextualPath finds the selected index by identity`() = runBlocking {
213
319
  servePage(
214
320
  "/home",
215
- track(src = "a", groupTitle = "Recent"),
216
- track(src = "b", groupTitle = "Recent"),
217
- singleTrack = true,
321
+ track(id = "stable-a", src = "https://s/a.mp3"),
322
+ track(id = "stable-b", src = "https://s/b.mp3"),
323
+ track(id = "stable-c", src = "https://s/c.mp3"),
218
324
  )
219
325
 
220
326
  val (queue, index) =
221
- requireNotNull(browserManager.expandQueueFromContextualUrl(contextual("/home", "b")))
327
+ requireNotNull(browserManager.expandQueueFromContextualPath(contextual("/home", "stable-c")))
328
+
329
+ assertEquals(listOf("stable-a", "stable-b", "stable-c"), queue.map { it.id })
330
+ assertEquals(2, index)
331
+ }
332
+
333
+ @Test
334
+ fun `getCachedTrack resolves an id-valued trackId through the cache`() = runBlocking {
335
+ servePage("/home", track(id = "stable-b", src = "https://s/b.mp3", title = "B"))
336
+ browserManager.navigate("/home")
337
+
338
+ // A contextual mediaId whose __trackId is the identity (the id), with a parent path that was
339
+ // never cached directly — the extract-then-lookup path must find it via the id key.
340
+ val mediaId = contextual("/elsewhere", "stable-b")
341
+
342
+ assertEquals("B", browserManager.getCachedTrack(mediaId)?.title)
343
+ }
344
+
345
+ @Test
346
+ fun `expandQueueFromContextualPath honours singleTrack`() = runBlocking {
347
+ servePage("/home", track(src = "a"), track(src = "b"), singleTrack = true)
348
+
349
+ val (queue, index) =
350
+ requireNotNull(browserManager.expandQueueFromContextualPath(contextual("/home", "b")))
222
351
 
223
352
  assertEquals(listOf("b"), queue.map { it.src })
224
353
  assertEquals(0, index)
225
354
  }
355
+
356
+ // --- Track.disabled: queue expansion excludes unavailable tracks ---
357
+
358
+ @Test
359
+ fun `expandQueueFromContextualPath excludes disabled tracks`() = runBlocking {
360
+ servePage(
361
+ "/home",
362
+ track(src = "a"),
363
+ track(src = "unavailable", disabled = true),
364
+ track(src = "b"),
365
+ )
366
+
367
+ val (queue, index) =
368
+ requireNotNull(browserManager.expandQueueFromContextualPath(contextual("/home", "b")))
369
+
370
+ assertEquals(listOf("a", "b"), queue.map { it.src })
371
+ assertEquals(1, index)
372
+ }
373
+
374
+ @Test
375
+ fun `expandQueueFromContextualPath aborts when the tapped track is disabled`() = runBlocking {
376
+ // A disabled track never plays: with the tapped identity excluded from the
377
+ // playable list, expansion aborts, and every caller's fallback refuses the
378
+ // disabled track itself — navigateTrack's guard on tap,
379
+ // onPlaybackResumption's refusal on resume.
380
+ servePage("/home", track(src = "a"), track(src = "unavailable", disabled = true))
381
+
382
+ assertNull(browserManager.expandQueueFromContextualPath(contextual("/home", "unavailable")))
383
+ }
384
+
385
+ @Test
386
+ fun `resolveMediaItemsForPlayback refuses a disabled track on the legacy INDEX_UNSET path`() =
387
+ runBlocking {
388
+ // Android Auto and Assistant request playback through Media3's legacy
389
+ // path, which passes C.INDEX_UNSET ("the default item") — the refusal
390
+ // must cover it, or a stale browse row for a now-disabled track plays.
391
+ servePage("/home", track(src = "unavailable", disabled = true))
392
+ browserManager.resolve("/home")
393
+
394
+ val item = MediaItem.Builder().setMediaId("unavailable").build()
395
+
396
+ try {
397
+ browserManager.resolveMediaItemsForPlayback(listOf(item), C.INDEX_UNSET, 0)
398
+ fail("Expected refusal of a disabled track")
399
+ } catch (expected: IllegalStateException) {
400
+ // Refused by failing the future: Media3 surfaces an error to the
401
+ // controller and live playback stays untouched.
402
+ }
403
+ }
226
404
  }
@@ -91,6 +91,44 @@ class BrowserUrlResolutionTest {
91
91
  assertEquals("https://api.example.com/artwork/abc", source?.uri)
92
92
  }
93
93
 
94
+ @Test
95
+ fun `resolveArtworkUrl leaves the id token literal for a track without an id`() = runTest {
96
+ bm.config =
97
+ BrowserConfig(
98
+ request = transformableConfig(baseUrl = "https://api.example.com"),
99
+ artwork = artworkConfig(path = "/artwork/{id}"),
100
+ )
101
+ // The config still runs (no blanket skip); the token stays literal so the resulting request
102
+ // 404s with a self-describing URL.
103
+ val source = bm.resolveArtworkUrl(track(artwork = null, id = null))
104
+ assertEquals("https://api.example.com/artwork/{id}", source?.uri)
105
+ }
106
+
107
+ @Test
108
+ fun `resolveArtworkUrl treats a blank id like a missing one`() = runTest {
109
+ bm.config =
110
+ BrowserConfig(
111
+ request = transformableConfig(baseUrl = "https://api.example.com"),
112
+ artwork = artworkConfig(path = "/artwork/{id}"),
113
+ )
114
+ val source = bm.resolveArtworkUrl(track(artwork = null, id = ""))
115
+ assertEquals("https://api.example.com/artwork/{id}", source?.uri)
116
+ }
117
+
118
+ @Test
119
+ fun `resolveArtworkUrl leaves the id token literal in header values without an id`() = runTest {
120
+ bm.config =
121
+ BrowserConfig(
122
+ request = transformableConfig(baseUrl = "https://api.example.com"),
123
+ artwork = artworkConfig(path = "/artwork", headers = mapOf("x-track" to "{id}")),
124
+ )
125
+ val source = bm.resolveArtworkUrl(track(artwork = null, id = null))
126
+ assertEquals("{id}", source?.headers?.get("x-track"))
127
+
128
+ val substituted = bm.resolveArtworkUrl(track(artwork = null, id = "abc"))
129
+ assertEquals("abc", substituted?.headers?.get("x-track"))
130
+ }
131
+
94
132
  @Test
95
133
  fun `resolveArtworkUrl applies image query params from the image context`() = runTest {
96
134
  bm.config =
@@ -35,7 +35,7 @@ class JsonArtworkTest {
35
35
  // so one unparseable row rejected the whole page rather than losing an image.
36
36
  val resolved =
37
37
  json.decodeFromString<JsonResolvedTrack>(
38
- """{"url":"/favorites","title":"Favorites","children":[
38
+ """{"path":"/favorites","title":"Favorites","children":[
39
39
  {"title":"Playlist","artwork":{"light":"https://e.example/l.png","dark":"https://e.example/d.png"}}
40
40
  ]}"""
41
41
  )
@@ -0,0 +1,91 @@
1
+ package com.audiobrowser.browser
2
+
3
+ import com.margelo.nitro.audiobrowser.ArtworkRendering
4
+ import com.margelo.nitro.audiobrowser.StyleDisplay
5
+ import kotlinx.serialization.json.Json
6
+ import org.junit.Assert.assertEquals
7
+ import org.junit.Assert.assertNull
8
+ import org.junit.Test
9
+
10
+ /** Style blocks on the wire, decoded tolerantly (ADR 0011). */
11
+ class JsonStyleTest {
12
+ private val json = Json { ignoreUnknownKeys = true }
13
+
14
+ @Test
15
+ fun `decodes a block onto TrackStyle`() {
16
+ val track =
17
+ json.decodeFromString<JsonTrack>(
18
+ """{"title":"X","style":{"display":"grid","artworkRendering":"stencil"}}"""
19
+ )
20
+ val style = track.toNitro().style
21
+ assertEquals(StyleDisplay.GRID, style?.display)
22
+ assertEquals(ArtworkRendering.STENCIL, style?.artworkRendering)
23
+ }
24
+
25
+ @Test
26
+ fun `decodes a block onto SectionStyle`() {
27
+ val resolved =
28
+ json.decodeFromString<JsonResolvedTrack>(
29
+ """{"path":"/home","title":"Home","sections":[
30
+ {"style":{"display":"grid","gridWrap":false},"children":[{"title":"C","src":"s"}]}
31
+ ]}"""
32
+ )
33
+ val style = resolved.toNitro().sections?.first()?.style
34
+ assertEquals(StyleDisplay.GRID, style?.display)
35
+ assertEquals(false, style?.gridWrap)
36
+ }
37
+
38
+ @Test
39
+ fun `a stale string style decodes as no declaration - never a parse failure`() {
40
+ // The retired string vocabulary must never kill the page (tolerant
41
+ // decoding, ADR 0011).
42
+ val resolved =
43
+ json.decodeFromString<JsonResolvedTrack>(
44
+ """{"path":"/home","title":"Home","style":"rail","children":[
45
+ {"title":"C","src":"s","style":"grid"}
46
+ ]}"""
47
+ )
48
+ val nitro = resolved.toNitro()
49
+ assertNull(nitro.style)
50
+ assertNull(nitro.children?.first()?.style)
51
+ }
52
+
53
+ @Test
54
+ fun `unknown enum values decode as no declaration`() {
55
+ // All-unknown declarations collapse to "no block" — one shape for "no
56
+ // declaration" on every platform.
57
+ val track =
58
+ json.decodeFromString<JsonTrack>(
59
+ """{"title":"X","style":{"display":"carousel","artworkRendering":"embossed","gridWrap":"yes"}}"""
60
+ )
61
+ assertNull(track.toNitro().style)
62
+ }
63
+
64
+ @Test
65
+ fun `an empty block decodes as no declaration`() {
66
+ val track = json.decodeFromString<JsonTrack>("""{"title":"X","style":{}}""")
67
+ assertNull(track.toNitro().style)
68
+ }
69
+
70
+ @Test
71
+ fun `a quoted boolean gridWrap is a wrong-typed field - no declaration`() {
72
+ // iOS's strict-typed tolerant decode drops a quoted "false"; the lenient
73
+ // Kotlin mapper must not read it either, or the same payload renders a
74
+ // teaser shelf on one platform and a wrapping grid on the other.
75
+ val resolved =
76
+ json.decodeFromString<JsonResolvedTrack>(
77
+ """{"path":"/home","title":"Home","sections":[
78
+ {"style":{"display":"grid","gridWrap":"false"},"children":[{"title":"C","src":"s"}]}
79
+ ]}"""
80
+ )
81
+ val style = resolved.toNitro().sections?.first()?.style
82
+ assertEquals(StyleDisplay.GRID, style?.display)
83
+ assertNull(style?.gridWrap)
84
+ }
85
+
86
+ @Test
87
+ fun `decodes disabled`() {
88
+ val track = json.decodeFromString<JsonTrack>("""{"title":"X","src":"s","disabled":true}""")
89
+ assertEquals(true, track.toNitro().disabled)
90
+ }
91
+ }