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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (416) hide show
  1. package/README.md +14 -4
  2. package/android/build.gradle +20 -1
  3. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +115 -26
  4. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +262 -188
  5. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
  6. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
  7. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +95 -58
  8. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
  9. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
  10. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +19 -7
  11. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  12. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  13. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +1 -1
  14. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
  15. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
  16. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +109 -74
  17. package/android/src/main/java/com/audiobrowser/player/Player.kt +51 -30
  18. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  19. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  20. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
  21. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  22. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
  23. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +52 -53
  24. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +74 -41
  25. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  26. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +229 -50
  27. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  28. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
  29. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
  30. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
  31. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
  32. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
  33. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  34. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  35. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
  36. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  37. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  38. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  39. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  40. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
  41. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  42. package/ios/Browser/BrowserConfig.swift +102 -5
  43. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  44. package/ios/Browser/BrowserManager.swift +229 -180
  45. package/ios/Browser/BrowserPathHelper.swift +44 -44
  46. package/ios/Browser/InertStyleDiagnostic.swift +279 -0
  47. package/ios/Browser/JsonModels.swift +258 -106
  48. package/ios/Browser/SearchDrillIn.swift +6 -6
  49. package/ios/Browser/SectionPresentation.swift +64 -0
  50. package/ios/Browser/SectionScope.swift +58 -24
  51. package/ios/Browser/StyleResolver.swift +58 -0
  52. package/ios/Browser/TabBarEntries.swift +9 -3
  53. package/ios/Browser/TrackIdTemplate.swift +37 -0
  54. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  55. package/ios/CarPlay/CarPlayController.swift +227 -85
  56. package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
  57. package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
  58. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  59. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  60. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  61. package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
  62. package/ios/Extension/Section+Copying.swift +31 -0
  63. package/ios/Extension/Track+Copying.swift +6 -12
  64. package/ios/Extension/Track+Identity.swift +18 -0
  65. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  66. package/ios/Http/HttpClient.swift +8 -2
  67. package/ios/HybridAudioBrowser.swift +166 -45
  68. package/ios/Model/NitroTypeStubs.swift +106 -21
  69. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  70. package/ios/Player/MediaIntentCriteria.swift +13 -5
  71. package/ios/Player/MediaLoader.swift +33 -9
  72. package/ios/Player/NowPlayingUpdater.swift +54 -6
  73. package/ios/Player/PlaybackCoordinator.swift +13 -6
  74. package/ios/Player/PlaybackTimer.swift +8 -2
  75. package/ios/Player/QueueManager.swift +36 -10
  76. package/ios/Player/RetryManager.swift +4 -3
  77. package/ios/Player/ShuffleOrder.swift +23 -0
  78. package/ios/Player/SleepTimerHandling.swift +13 -0
  79. package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
  80. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  81. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  82. package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
  83. package/ios/Tests/JsonModelsTests.swift +150 -1
  84. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  85. package/ios/Tests/QueueManagerTests.swift +72 -6
  86. package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
  87. package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
  88. package/ios/Tests/SearchDrillInTests.swift +16 -16
  89. package/ios/Tests/SectionPresentationTests.swift +90 -0
  90. package/ios/Tests/SectionScopeTests.swift +87 -55
  91. package/ios/Tests/StyleResolverTests.swift +127 -0
  92. package/ios/Tests/Support/TestTypes.swift +7 -0
  93. package/ios/Tests/TabBarEntriesTests.swift +18 -18
  94. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  95. package/ios/Tests/TrackIdentityTests.swift +20 -0
  96. package/ios/Tests/TrackSelectorTests.swift +168 -33
  97. package/ios/TrackPlayer.swift +57 -34
  98. package/ios/TrackSelector.swift +45 -30
  99. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  100. package/ios/Util/NetworkMonitor.swift +43 -26
  101. package/ios/Util/SFSymbolRenderer.swift +24 -2
  102. package/ios/Util/SVGProcessor.swift +9 -2
  103. package/lib/commonjs/AudioBrowser.js +11 -0
  104. package/lib/commonjs/AudioBrowser.js.map +1 -1
  105. package/lib/commonjs/features/browser-config.js +11 -8
  106. package/lib/commonjs/features/browser-config.js.map +1 -1
  107. package/lib/commonjs/features/browser.js +2 -2
  108. package/lib/commonjs/features/favorites.js +13 -10
  109. package/lib/commonjs/features/favorites.js.map +1 -1
  110. package/lib/commonjs/features/playback/controls.js +4 -2
  111. package/lib/commonjs/features/playback/controls.js.map +1 -1
  112. package/lib/commonjs/features/playback/progress.js +2 -1
  113. package/lib/commonjs/features/playback/progress.js.map +1 -1
  114. package/lib/commonjs/features/playback/rate.js +8 -1
  115. package/lib/commonjs/features/playback/rate.js.map +1 -1
  116. package/lib/commonjs/features/playback/state.js +2 -3
  117. package/lib/commonjs/features/playback/state.js.map +1 -1
  118. package/lib/commonjs/features/player/options.js.map +1 -1
  119. package/lib/commonjs/features/player/setup.js +3 -1
  120. package/lib/commonjs/features/player/setup.js.map +1 -1
  121. package/lib/commonjs/features/player/validateOptions.js +2 -0
  122. package/lib/commonjs/features/player/validateOptions.js.map +1 -1
  123. package/lib/commonjs/features/queue/activeTrack.js +14 -3
  124. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  125. package/lib/commonjs/features/queue/queue.js +11 -4
  126. package/lib/commonjs/features/queue/queue.js.map +1 -1
  127. package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
  128. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  129. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  130. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  131. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  132. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  133. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  134. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  135. package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
  136. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  137. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  138. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  139. package/lib/commonjs/web/browser/BrowserManager.js +144 -87
  140. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  141. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  142. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  143. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  144. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  145. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  146. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  147. package/lib/commonjs/web/browser/sections.js +89 -0
  148. package/lib/commonjs/web/browser/sections.js.map +1 -0
  149. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  150. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  151. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  152. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  153. package/lib/module/AudioBrowser.js +1 -0
  154. package/lib/module/AudioBrowser.js.map +1 -1
  155. package/lib/module/features/browser-config.js +11 -8
  156. package/lib/module/features/browser-config.js.map +1 -1
  157. package/lib/module/features/browser.js +2 -2
  158. package/lib/module/features/favorites.js +13 -10
  159. package/lib/module/features/favorites.js.map +1 -1
  160. package/lib/module/features/playback/controls.js +4 -2
  161. package/lib/module/features/playback/controls.js.map +1 -1
  162. package/lib/module/features/playback/progress.js +2 -1
  163. package/lib/module/features/playback/progress.js.map +1 -1
  164. package/lib/module/features/playback/rate.js +8 -1
  165. package/lib/module/features/playback/rate.js.map +1 -1
  166. package/lib/module/features/playback/state.js +2 -3
  167. package/lib/module/features/playback/state.js.map +1 -1
  168. package/lib/module/features/player/options.js.map +1 -1
  169. package/lib/module/features/player/setup.js +3 -1
  170. package/lib/module/features/player/setup.js.map +1 -1
  171. package/lib/module/features/player/validateOptions.js +2 -0
  172. package/lib/module/features/player/validateOptions.js.map +1 -1
  173. package/lib/module/features/queue/activeTrack.js +14 -3
  174. package/lib/module/features/queue/activeTrack.js.map +1 -1
  175. package/lib/module/features/queue/queue.js +11 -4
  176. package/lib/module/features/queue/queue.js.map +1 -1
  177. package/lib/module/utils/LazyNativeEmitter.js +21 -10
  178. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  179. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  180. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  181. package/lib/module/utils/getTrackIdentity.js +20 -0
  182. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  183. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  184. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  185. package/lib/module/web/NativeAudioBrowser.js +32 -24
  186. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  187. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  188. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  189. package/lib/module/web/browser/BrowserManager.js +144 -87
  190. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  191. package/lib/module/web/browser/FavoriteManager.js +31 -21
  192. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  193. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  194. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  195. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  196. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  197. package/lib/module/web/browser/sections.js +83 -0
  198. package/lib/module/web/browser/sections.js.map +1 -0
  199. package/lib/module/web/player/NowPlayingManager.js +4 -3
  200. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  201. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  202. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  203. package/lib/typescript/src/AudioBrowser.d.ts +1 -0
  204. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  205. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  206. package/lib/typescript/src/features/browser.d.ts +2 -2
  207. package/lib/typescript/src/features/favorites.d.ts +13 -10
  208. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  209. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  210. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  211. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  212. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  213. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  214. package/lib/typescript/src/features/playback/state.d.ts +2 -3
  215. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  216. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  217. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  218. package/lib/typescript/src/features/player/options.d.ts +21 -19
  219. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  220. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  221. package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
  222. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
  223. package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
  224. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  225. package/lib/typescript/src/features/queue/queue.d.ts +5 -2
  226. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  227. package/lib/typescript/src/types/browser-native.d.ts +2 -2
  228. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  229. package/lib/typescript/src/types/browser-nodes.d.ts +361 -126
  230. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  231. package/lib/typescript/src/types/browser.d.ts +33 -53
  232. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  233. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
  234. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  235. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  236. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  237. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  238. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  239. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  240. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  241. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
  242. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  243. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
  244. package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
  245. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  246. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  247. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
  248. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
  249. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
  250. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
  251. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
  252. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
  253. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
  254. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
  255. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +16 -4
  256. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
  257. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
  258. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
  259. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
  260. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
  261. package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
  262. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +33 -14
  263. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  264. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  265. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
  266. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +22 -10
  267. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
  268. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +10 -7
  269. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
  270. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  271. package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
  272. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
  273. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  274. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +55 -51
  275. package/nitrogen/generated/android/c++/JSection.hpp +120 -0
  276. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  277. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  278. package/nitrogen/generated/android/c++/JTrack.hpp +23 -48
  279. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
  280. package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
  281. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
  282. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  283. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  284. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  285. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +18 -6
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
  297. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +158 -82
  298. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +25 -10
  299. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +25 -10
  300. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  301. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  302. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  303. package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
  304. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  305. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  306. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
  307. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  308. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  309. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
  310. package/nitrogen/generated/ios/swift/Section.swift +100 -0
  311. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  312. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  313. package/nitrogen/generated/ios/swift/Track.swift +20 -73
  314. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  315. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  316. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  317. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  318. package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
  319. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  320. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  321. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
  322. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  323. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  324. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
  325. package/nitrogen/generated/shared/c++/Section.hpp +106 -0
  326. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  327. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  328. package/nitrogen/generated/shared/c++/Track.hpp +15 -31
  329. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  330. package/package.json +7 -2
  331. package/src/AudioBrowser.ts +1 -0
  332. package/src/features/browser-config.test.ts +21 -6
  333. package/src/features/browser-config.ts +11 -8
  334. package/src/features/browser.test.ts +1 -1
  335. package/src/features/browser.ts +2 -2
  336. package/src/features/favorites.ts +13 -10
  337. package/src/features/hook-wiring.test.ts +63 -15
  338. package/src/features/playback/controls.ts +4 -2
  339. package/src/features/playback/progress.ts +2 -1
  340. package/src/features/playback/rate.ts +8 -1
  341. package/src/features/playback/state.ts +2 -3
  342. package/src/features/playback/trackPlaybackTime.ts +1 -1
  343. package/src/features/player/options.ts +27 -19
  344. package/src/features/player/setup.test.ts +12 -3
  345. package/src/features/player/setup.ts +3 -1
  346. package/src/features/player/validateOptions.ts +2 -0
  347. package/src/features/queue/activeTrack.ts +14 -3
  348. package/src/features/queue/queue.ts +11 -4
  349. package/src/nitro-stub-drift.test.ts +40 -0
  350. package/src/public-surface.test.ts +40 -0
  351. package/src/specs/audio-browser.nitro.ts +4 -3
  352. package/src/style-resolution.test.ts +210 -0
  353. package/src/types/browser-native.ts +2 -2
  354. package/src/types/browser-nodes.ts +377 -135
  355. package/src/types/browser.ts +34 -55
  356. package/src/utils/LazyNativeEmitter.ts +20 -10
  357. package/src/utils/NativeUpdatedValue.ts +29 -13
  358. package/src/utils/getTrackIdentity.ts +20 -0
  359. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  360. package/src/web/NativeAudioBrowser.ts +37 -22
  361. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  362. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  363. package/src/web/browser/BrowserManager.ts +176 -95
  364. package/src/web/browser/FavoriteManager.test.ts +84 -0
  365. package/src/web/browser/FavoriteManager.ts +28 -23
  366. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  367. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  368. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  369. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  370. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  371. package/src/web/browser/sections.test.ts +91 -0
  372. package/src/web/browser/sections.ts +72 -0
  373. package/src/web/player/NowPlayingManager.ts +4 -3
  374. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  375. package/src/web/util/BrowserPathHelper.ts +61 -13
  376. package/website/guide/android-auto.md +114 -0
  377. package/website/guide/android-certificates.md +409 -0
  378. package/website/guide/artwork.md +255 -0
  379. package/website/guide/audio-output.md +135 -0
  380. package/website/guide/automotive.md +86 -0
  381. package/website/guide/basic-usage.md +151 -0
  382. package/website/guide/battery.md +115 -0
  383. package/website/guide/browser.md +581 -0
  384. package/website/guide/carplay.md +620 -0
  385. package/website/guide/configuration.md +173 -0
  386. package/website/guide/equalizer.md +231 -0
  387. package/website/guide/errors.md +310 -0
  388. package/website/guide/favorites.md +154 -0
  389. package/website/guide/gate.md +143 -0
  390. package/website/guide/getting-started.md +46 -0
  391. package/website/guide/hooks.md +238 -0
  392. package/website/guide/live-streams.md +125 -0
  393. package/website/guide/metadata.md +239 -0
  394. package/website/guide/migrating-from-track-player.md +244 -0
  395. package/website/guide/native-callback-fetch.md +127 -0
  396. package/website/guide/network.md +86 -0
  397. package/website/guide/now-playing.md +175 -0
  398. package/website/guide/playback.md +328 -0
  399. package/website/guide/queue.md +300 -0
  400. package/website/guide/remote-controls.md +256 -0
  401. package/website/guide/search.md +255 -0
  402. package/website/guide/sleep-timer.md +268 -0
  403. package/website/guide/track.md +246 -0
  404. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  405. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  406. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  407. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  408. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  409. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  410. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  411. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  412. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  413. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  414. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  415. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  416. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
@@ -176,15 +176,22 @@ suspend fun BrowserManager.resolveArtworkUrl(
176
176
  )
177
177
  }
178
178
 
179
- // `{id}` token substitution, only for a non-empty id. Mirrors iOS substituteTrackId.
179
+ // `{id}` token substitution. A track without a non-blank id leaves the token LITERALLY in
180
+ // place — the request then visibly 404s, which is intended (self-describing error trail) —
181
+ // with a single warning logged below once the URL is built. Mirrors iOS substituteTrackId.
180
182
  track.id
181
183
  ?.takeIf { it.isNotEmpty() }
182
184
  ?.let { transformedConfig = substituteTrackId(transformedConfig, it) }
185
+ val hasUnfilledIdToken = configContainsIdToken(transformedConfig)
183
186
 
184
187
  val uri = RequestConfigBuilder.buildUrl(transformedConfig)
185
188
  // If URI is empty, there's no valid artwork path
186
189
  if (uri.isEmpty()) return null
187
190
 
191
+ if (hasUnfilledIdToken) {
192
+ Timber.w("nowPlayingArtwork: track has no id — '{id}' left unfilled in %s", uri)
193
+ }
194
+
188
195
  ImageSource(
189
196
  uri = uri,
190
197
  method = transformedConfig.method,
@@ -265,15 +272,18 @@ private fun applyImageQueryParams(
265
272
  return config.copy(query = (config.query ?: emptyMap()) + contextQuery)
266
273
  }
267
274
 
275
+ /** The `{id}` template token — see [substituteTrackId] / [configContainsIdToken]. */
276
+ private const val ID_TOKEN = "{id}"
277
+
268
278
  /**
269
279
  * Replaces the `{id}` token with the track id in a request config's path, query values, and header
270
280
  * values. Used so a `nowPlayingArtwork` like `{ path: "/artwork/{id}" }` resolves. Configs without
271
281
  * the token are returned unchanged. Mirrors the iOS `substituteTrackId` helper.
272
282
  */
273
283
  private fun substituteTrackId(config: RequestConfig, id: String): RequestConfig {
274
- fun sub(s: String?): String? = s?.replace("{id}", id)
284
+ fun sub(s: String?): String? = s?.replace(ID_TOKEN, id)
275
285
  fun subMap(m: Map<String, String>?): Map<String, String>? =
276
- m?.mapValues { (_, value) -> value.replace("{id}", id) }
286
+ m?.mapValues { (_, value) -> value.replace(ID_TOKEN, id) }
277
287
  return config.copy(
278
288
  path = sub(config.path),
279
289
  headers = subMap(config.headers),
@@ -281,6 +291,16 @@ private fun substituteTrackId(config: RequestConfig, id: String): RequestConfig
281
291
  )
282
292
  }
283
293
 
294
+ /**
295
+ * Whether the config still carries an unfilled `{id}` token in its path, query values, or header
296
+ * values (checked AFTER substitution, so it matches exactly when the track had no non-blank id and
297
+ * the config uses the token). Mirrors iOS `TrackIdTemplate.containsToken`.
298
+ */
299
+ private fun configContainsIdToken(config: RequestConfig): Boolean =
300
+ config.path?.contains(ID_TOKEN) == true ||
301
+ config.query?.values?.any { it.contains(ID_TOKEN) } == true ||
302
+ config.headers?.values?.any { it.contains(ID_TOKEN) } == true
303
+
284
304
  /** Builds a headers map, merging explicit headers with userAgent and contentType. */
285
305
  private fun buildHeadersMap(
286
306
  headers: Map<String, String>?,
@@ -0,0 +1,267 @@
1
+ package com.audiobrowser.browser
2
+
3
+ import com.audiobrowser.BuildConfig
4
+ import com.audiobrowser.util.toWireString
5
+ import com.margelo.nitro.audiobrowser.GridTile
6
+ import com.margelo.nitro.audiobrowser.Section
7
+ import com.margelo.nitro.audiobrowser.SectionStyle
8
+ import com.margelo.nitro.audiobrowser.StyleDisplay
9
+ import com.margelo.nitro.audiobrowser.TrackStyle
10
+ import timber.log.Timber
11
+
12
+ /**
13
+ * Dev diagnostic (ADR 0011): the declarations on a resolved page that can never render.
14
+ *
15
+ * The block model deliberately gave up compile-time invalid-combination errors — combinations are
16
+ * not invalid, they are *inert*, the way `grid-template` on a non-grid element is inert in CSS.
17
+ * This is the recovery: at page resolution every declaration its own block makes unreadable is
18
+ * reported once, with the effective context and the fix.
19
+ *
20
+ * The line it draws, and never crosses: only **structural** inertness — a declaration the resolved
21
+ * block itself renders unreadable, on every surface (`gridWrap` outside a grid, `cardTint` with no
22
+ * card treatment in scope). *Surface* inertness is never reported, because declarations are
23
+ * aspirational: `imageShape` on Android Auto, or a card treatment on a pre-26 CarPlay, is intended
24
+ * usage — the renderer drops what it can't draw.
25
+ *
26
+ * Runs where every page passes regardless of its source (static config, route callback, or fetched
27
+ * browse JSON), and only on a cache miss, so a mistake is reported once per resolution rather than
28
+ * once per serve. Mirrors `ios/Browser/InertStyleDiagnostic.swift`.
29
+ *
30
+ * Debug builds only: [warn] is the whole seam, so a release build never even walks the page (the
31
+ * trade is that a page served only to a release build — an internal track, production browse JSON —
32
+ * reports nothing; reproduce it against a debug build).
33
+ */
34
+ object InertStyleDiagnostic {
35
+ /**
36
+ * Reports every inert declaration on a resolved page. The one gate — the [BuildConfig] check is
37
+ * here rather than at the call sites so callers read unconditionally and the rule set stays free
38
+ * of build-configuration knowledge.
39
+ */
40
+ fun warn(path: String, pageStyle: SectionStyle?, sections: List<Section>) {
41
+ if (!BuildConfig.DEBUG) return
42
+ findings(path, pageStyle, sections).forEach { Timber.w(it) }
43
+ }
44
+
45
+ /**
46
+ * The declarations that carry a structural inertness condition.
47
+ *
48
+ * Two keys of the block are deliberately absent — `display` and `artworkRendering`, dispositioned
49
+ * below instead.
50
+ */
51
+ private enum class Property(val key: String, val isItemProperty: Boolean) {
52
+ // Container properties resolve by scope override (`section ?? page`) and never reach an item;
53
+ // item properties inherit (`track ?? section ?? page`), so they may also be declared per track.
54
+ GRID_WRAP("gridWrap", isItemProperty = false),
55
+ GRID_TILE("gridTile", isItemProperty = false),
56
+ IMAGE_SHAPE("imageShape", isItemProperty = true),
57
+ ACCESSORY_SYMBOL("accessorySymbol", isItemProperty = true),
58
+ CARD_TINT("cardTint", isItemProperty = true),
59
+ CARD_IMAGE("cardImage", isItemProperty = true);
60
+
61
+ /**
62
+ * Whether a value of this property, resolved into [context], has any rendering to affect. Each
63
+ * branch is the spec's own "inert unless" clause.
64
+ */
65
+ fun isInert(context: Context): Boolean =
66
+ when (this) {
67
+ GRID_WRAP,
68
+ GRID_TILE -> !context.isGrid
69
+ IMAGE_SHAPE -> !context.isGrid || context.isCard
70
+ ACCESSORY_SYMBOL -> context.isCard
71
+ CARD_TINT,
72
+ CARD_IMAGE -> !context.isCard
73
+ }
74
+
75
+ /** The rule the declaration falls foul of, stated positively. */
76
+ val rule: String
77
+ get() =
78
+ when (this) {
79
+ GRID_WRAP,
80
+ GRID_TILE -> "`$key` renders only in a grid"
81
+ IMAGE_SHAPE -> "only plain and condensed tiles have a shape"
82
+ ACCESSORY_SYMBOL -> "card tiles have no accessory slot"
83
+ CARD_TINT,
84
+ CARD_IMAGE -> "the card properties render only under `gridTile: 'card'`"
85
+ }
86
+
87
+ val hint: String
88
+ get() =
89
+ when (this) {
90
+ GRID_WRAP,
91
+ GRID_TILE -> "Declare `display: 'grid'` on the section or the page, or drop `$key`."
92
+ IMAGE_SHAPE -> "Render the items as plain or condensed grid tiles, or drop `imageShape`."
93
+ ACCESSORY_SYMBOL -> "Use `gridTile: 'plain'` or `'condensed'`, or drop `accessorySymbol`."
94
+ CARD_TINT,
95
+ CARD_IMAGE -> "Declare `display: 'grid'` with `gridTile: 'card'`, or drop `$key`."
96
+ }
97
+
98
+ fun declaredIn(style: SectionStyle?): Boolean =
99
+ when (this) {
100
+ GRID_WRAP -> style?.gridWrap != null
101
+ GRID_TILE -> style?.gridTile != null
102
+ IMAGE_SHAPE -> style?.imageShape != null
103
+ ACCESSORY_SYMBOL -> style?.accessorySymbol != null
104
+ CARD_TINT -> style?.cardTint != null
105
+ CARD_IMAGE -> style?.cardImage != null
106
+ }
107
+
108
+ // Nitro flattens the spec's `extends`, so the two blocks share no type — hence two lookups over
109
+ // the same key set rather than one interface.
110
+ fun declaredIn(style: TrackStyle?): Boolean =
111
+ when (this) {
112
+ // Container properties have no Track slot to be declared in.
113
+ GRID_WRAP,
114
+ GRID_TILE -> false
115
+ IMAGE_SHAPE -> style?.imageShape != null
116
+ ACCESSORY_SYMBOL -> style?.accessorySymbol != null
117
+ CARD_TINT -> style?.cardTint != null
118
+ CARD_IMAGE -> style?.cardImage != null
119
+ }
120
+ }
121
+
122
+ /**
123
+ * The rest of the block's keys, dispositioned rather than ruled on: `artworkRendering` has no
124
+ * structural inertness condition (artwork renders in every layout), and `display` is positional —
125
+ * inert only in the one position that opens no page (see [playableDisplayFinding]). Declared here
126
+ * so the spec-level completeness test can hold this file to the whole vocabulary: every key of
127
+ * the block is a rule, always renderable, or positional, and a newly added property is a decision
128
+ * rather than an omission.
129
+ */
130
+ val ALWAYS_RENDERABLE = listOf("artworkRendering")
131
+
132
+ val POSITIONAL = listOf("display")
133
+
134
+ /**
135
+ * A section's effective container context: the two container values every inertness condition is
136
+ * stated against.
137
+ *
138
+ * Folded through [StyleResolver] rather than re-deriving the scope override: the diagnostic
139
+ * predicts what a renderer will do with these declarations, so it has to resolve them the way the
140
+ * renderers do.
141
+ */
142
+ private class Context(section: SectionStyle?, page: SectionStyle?) {
143
+ private val folded = StyleResolver.sectionStyle(section, page)
144
+ val isGrid = folded.display == StyleDisplay.GRID
145
+ val tile = folded.gridTile ?: GridTile.PLAIN
146
+
147
+ /**
148
+ * Whether the card treatment is what this container renders — the `card*` family's condition.
149
+ */
150
+ val isCard: Boolean
151
+ get() = isGrid && tile == GridTile.CARD
152
+
153
+ /**
154
+ * The effective style, for the message — in the wire spelling the author types ([toWireString]
155
+ * is the decoder's own mapping). `gridTile` is quoted only where a grid gives it meaning:
156
+ * naming it on a list would be advice to fix the wrong property.
157
+ */
158
+ val described: String
159
+ get() = if (!isGrid) "display 'list'" else "display 'grid', gridTile '${tile.toWireString()}'"
160
+ }
161
+
162
+ /**
163
+ * Every inert declaration on a resolved page, as ready-to-log messages — pure, so the whole rule
164
+ * set is testable off-device; the caller logs.
165
+ *
166
+ * @param path the resolved path, named in every message.
167
+ * @param pageStyle the page's own block (`ResolvedTrack.style`).
168
+ * @param sections the page's normalized sections, blocks *unfolded* — the diagnostic attributes
169
+ * each finding to the level that declared it, so it must see the levels before [StyleResolver]
170
+ * merges them.
171
+ */
172
+ fun findings(path: String, pageStyle: SectionStyle?, sections: List<Section>): List<String> {
173
+ val findings = mutableListOf<String>()
174
+ // A page-level declaration is judged across the whole page: it is live if it renders in any
175
+ // section it reaches, however many others ignore it.
176
+ val pageReached = mutableSetOf<Property>()
177
+ val pageRendered = mutableSetOf<Property>()
178
+
179
+ sections.forEachIndexed { index, section ->
180
+ val context = Context(section.style, pageStyle)
181
+ val sectionLabel = label(section, index)
182
+
183
+ for (property in Property.entries) {
184
+ val inert = property.isInert(context)
185
+
186
+ if (property.declaredIn(section.style)) {
187
+ if (inert) {
188
+ findings +=
189
+ message(property, path, sectionLabel, "its effective style is ${context.described}")
190
+ }
191
+ } else if (property.declaredIn(pageStyle)) {
192
+ // The section takes the page's value (nothing closer overrode it).
193
+ pageReached += property
194
+ if (!inert) pageRendered += property
195
+ }
196
+
197
+ if (!property.isItemProperty || !inert) continue
198
+ val tracks = section.children.count { property.declaredIn(it.style) }
199
+ if (tracks > 0) {
200
+ findings +=
201
+ message(
202
+ property,
203
+ path,
204
+ "${trackCount(tracks)} in $sectionLabel",
205
+ "the section's effective style is ${context.described}",
206
+ )
207
+ }
208
+ }
209
+
210
+ playableDisplayFinding(section, path, sectionLabel)?.let { findings += it }
211
+ }
212
+
213
+ for (property in Property.entries) {
214
+ if (property in pageReached && property !in pageRendered) {
215
+ findings +=
216
+ message(property, path, "the page block", "no section that inherits it renders one")
217
+ }
218
+ }
219
+
220
+ return findings
221
+ }
222
+
223
+ /**
224
+ * `display` is positional, so it is never inert *in* a container — but on a track that renders
225
+ * playable (`src` wins the rendering) it promises the layout of a page that never opens.
226
+ */
227
+ private fun playableDisplayFinding(section: Section, path: String, label: String): String? {
228
+ val tracks = section.children.count { it.src != null && it.style?.display != null }
229
+ if (tracks == 0) return null
230
+ return message(
231
+ path = path,
232
+ owner = "${trackCount(tracks)} in $label",
233
+ key = "display",
234
+ reason =
235
+ "`display` is the layout promise for the page a track opens, and a track rendered playable " +
236
+ "opens none",
237
+ hint = "Drop `display`, or give the track a `path` if it should be browsable.",
238
+ )
239
+ }
240
+
241
+ private fun message(property: Property, path: String, owner: String, scope: String): String =
242
+ message(
243
+ path = path,
244
+ owner = owner,
245
+ key = property.key,
246
+ reason = "${property.rule}, but $scope",
247
+ hint = property.hint,
248
+ )
249
+
250
+ /**
251
+ * The one message skeleton — every finding names the page, the declaration, who declared it, why
252
+ * it can't render, and what to do about it.
253
+ */
254
+ private fun message(
255
+ path: String,
256
+ owner: String,
257
+ key: String,
258
+ reason: String,
259
+ hint: String,
260
+ ): String =
261
+ "Inert style declaration on page '$path': `style.$key` on $owner never renders — $reason. $hint"
262
+
263
+ private fun trackCount(count: Int): String = "$count ${if (count == 1) "track" else "tracks"}"
264
+
265
+ private fun label(section: Section, index: Int): String =
266
+ section.title?.takeIf { it.isNotEmpty() }?.let { "section '$it'" } ?: "section #${index + 1}"
267
+ }
@@ -1,30 +1,30 @@
1
1
  package com.audiobrowser.browser
2
2
 
3
+ import com.audiobrowser.util.EMPTY_SECTION_STYLE
4
+ import com.audiobrowser.util.EMPTY_TRACK_STYLE
5
+ import com.audiobrowser.util.toArtworkRendering
6
+ import com.audiobrowser.util.toCardImage
7
+ import com.audiobrowser.util.toGridTile
8
+ import com.audiobrowser.util.toImageShape
9
+ import com.audiobrowser.util.toStyleDisplay
3
10
  import com.margelo.nitro.audiobrowser.CarPlaySiriListButtonPosition
4
- import com.margelo.nitro.audiobrowser.ImageRowItem
5
11
  import com.margelo.nitro.audiobrowser.ResolvedTrack
12
+ import com.margelo.nitro.audiobrowser.Section
13
+ import com.margelo.nitro.audiobrowser.SectionStyle
6
14
  import com.margelo.nitro.audiobrowser.Track
7
15
  import com.margelo.nitro.audiobrowser.TrackRequest
8
16
  import com.margelo.nitro.audiobrowser.TrackStyle
9
17
  import kotlinx.serialization.Serializable
18
+ import kotlinx.serialization.json.JsonElement
19
+ import kotlinx.serialization.json.JsonObject
20
+ import kotlinx.serialization.json.JsonPrimitive
21
+ import kotlinx.serialization.json.booleanOrNull
10
22
 
11
23
  /**
12
24
  * JSON serializable models for parsing API responses. These will be converted to Nitro types after
13
- * parsing.
25
+ * parsing. Legacy `groupTitle`/`imageRow` keys in payloads are simply unknown to these models and
26
+ * decode as ignored dead weight (ADR 0010).
14
27
  */
15
- @Serializable
16
- data class JsonImageRowItem(
17
- val id: String? = null,
18
- val url: String? = null,
19
- val src: String? = null,
20
- val artwork: String? = null,
21
- val title: String,
22
- val artist: String? = null,
23
- val album: String? = null,
24
- val albumUrl: String? = null,
25
- val live: Boolean? = null,
26
- val request: JsonTrackRequest? = null,
27
- )
28
28
 
29
29
  /** JSON model for a track's per-request HTTP override (identity/auth/signed-url). */
30
30
  @Serializable
@@ -36,25 +36,35 @@ data class JsonTrackRequest(
36
36
  fun toNitro(): TrackRequest = TrackRequest(userAgent, headers, query)
37
37
  }
38
38
 
39
+ /** JSON model for a page section (ADR 0010). */
40
+ @Serializable
41
+ data class JsonSection(
42
+ val title: String? = null,
43
+ val subtitle: String? = null,
44
+ val style: JsonElement? = null,
45
+ val path: String? = null,
46
+ val children: List<JsonTrack>,
47
+ )
48
+
39
49
  @Serializable
40
50
  data class JsonResolvedTrack(
41
51
  val id: String? = null,
42
- val url: String,
52
+ val path: String,
43
53
  val title: String,
44
54
  val subtitle: String? = null,
45
55
  val icon: String? = null,
46
56
  val artwork: JsonArtwork? = null,
47
57
  val artist: String? = null,
48
- val albumUrl: String? = null,
58
+ val albumPath: String? = null,
49
59
  val album: String? = null,
50
60
  val description: String? = null,
51
61
  val genre: String? = null,
52
62
  val duration: Double? = null,
63
+ val sections: List<JsonSection>? = null,
53
64
  val children: List<JsonTrack>? = null,
54
65
  val src: String? = null,
55
- val style: String? = null,
56
- val childrenStyle: String? = null,
57
- val groupTitle: String? = null,
66
+ val style: JsonElement? = null,
67
+ val disabled: Boolean? = null,
58
68
  val live: Boolean? = null,
59
69
  val carPlaySiriListButton: String? = null,
60
70
  )
@@ -62,34 +72,72 @@ data class JsonResolvedTrack(
62
72
  @Serializable
63
73
  data class JsonTrack(
64
74
  val id: String? = null,
65
- val url: String? = null,
75
+ val path: String? = null,
66
76
  val title: String,
67
77
  val subtitle: String? = null,
68
78
  val icon: String? = null,
69
79
  val artwork: JsonArtwork? = null,
70
80
  val artist: String? = null,
71
- val albumUrl: String? = null,
81
+ val albumPath: String? = null,
72
82
  val album: String? = null,
73
83
  val description: String? = null,
74
84
  val genre: String? = null,
75
85
  val duration: Double? = null,
76
86
  val src: String? = null,
77
87
  val request: JsonTrackRequest? = null,
78
- val style: String? = null,
79
- val childrenStyle: String? = null,
80
- val groupTitle: String? = null,
88
+ val style: JsonElement? = null,
89
+ val disabled: Boolean? = null,
81
90
  val live: Boolean? = null,
82
- val imageRow: List<JsonImageRowItem>? = null,
83
91
  )
84
92
 
85
- /** Convert JSON models to Nitro types */
86
- private fun String?.toTrackStyle(): TrackStyle? {
87
- return when (this?.lowercase()) {
88
- "list" -> TrackStyle.LIST
89
- "grid" -> TrackStyle.GRID
90
- else -> null
93
+ /**
94
+ * Convert JSON models to Nitro types.
95
+ *
96
+ * `style` rides the models as a raw [JsonElement] and maps leniently here — tolerant decoding by
97
+ * design (ADR 0011): a stale value (the retired string vocabulary), a wrong-typed field, or an
98
+ * unknown enum value is "no declaration", never a parse failure. A typed field would fail the whole
99
+ * page, since this decoder is otherwise strict.
100
+ */
101
+ private fun JsonObject.stringField(key: String): String? =
102
+ (get(key) as? JsonPrimitive)?.takeIf { it.isString }?.content
103
+
104
+ /** A strict JSON boolean — a quoted "false" is a wrong-typed field, i.e. no declaration. */
105
+ private fun JsonObject.booleanField(key: String): Boolean? =
106
+ (get(key) as? JsonPrimitive)?.takeIf { !it.isString }?.booleanOrNull
107
+
108
+ /**
109
+ * A block that resolves to no declarations at all — `{}` or all-unknown values — maps to null, so
110
+ * "no declaration" has one shape on every platform (iOS makes the same collapse).
111
+ */
112
+ private fun JsonElement?.toTrackStyle(): TrackStyle? =
113
+ (this as? JsonObject)?.let { obj ->
114
+ TrackStyle(
115
+ display = obj.stringField("display").toStyleDisplay(),
116
+ artworkRendering = obj.stringField("artworkRendering").toArtworkRendering(),
117
+ imageShape = obj.stringField("imageShape").toImageShape(),
118
+ // 'none' is a legitimate accessory value (the inheritance escape);
119
+ // only emptiness collapses.
120
+ accessorySymbol = obj.stringField("accessorySymbol")?.takeIf { it.isNotEmpty() },
121
+ cardTint = obj.stringField("cardTint")?.takeIf { it.isNotEmpty() },
122
+ cardImage = obj.stringField("cardImage").toCardImage(),
123
+ )
124
+ .takeIf { it != EMPTY_TRACK_STYLE }
125
+ }
126
+
127
+ private fun JsonElement?.toSectionStyle(): SectionStyle? =
128
+ (this as? JsonObject)?.let { obj ->
129
+ SectionStyle(
130
+ display = obj.stringField("display").toStyleDisplay(),
131
+ artworkRendering = obj.stringField("artworkRendering").toArtworkRendering(),
132
+ imageShape = obj.stringField("imageShape").toImageShape(),
133
+ accessorySymbol = obj.stringField("accessorySymbol")?.takeIf { it.isNotEmpty() },
134
+ cardTint = obj.stringField("cardTint")?.takeIf { it.isNotEmpty() },
135
+ cardImage = obj.stringField("cardImage").toCardImage(),
136
+ gridWrap = obj.booleanField("gridWrap"),
137
+ gridTile = obj.stringField("gridTile").toGridTile(),
138
+ )
139
+ .takeIf { it != EMPTY_SECTION_STYLE }
91
140
  }
92
- }
93
141
 
94
142
  private fun String?.toCarPlaySiriListButtonPosition(): CarPlaySiriListButtonPosition? {
95
143
  return when (this?.lowercase()) {
@@ -99,26 +147,21 @@ private fun String?.toCarPlaySiriListButtonPosition(): CarPlaySiriListButtonPosi
99
147
  }
100
148
  }
101
149
 
102
- private fun JsonImageRowItem.toNitro(): ImageRowItem {
103
- return ImageRowItem(
104
- id = id,
105
- url = url,
106
- src = src,
107
- artwork = artwork,
108
- artworkSource = null,
150
+ fun JsonSection.toNitro(): Section {
151
+ return Section(
109
152
  title = title,
110
- artist = artist,
111
- album = album,
112
- albumUrl = albumUrl,
113
- live = live,
114
- request = request?.toNitro(),
153
+ subtitle = subtitle,
154
+ style = style.toSectionStyle(),
155
+ path = path,
156
+ children = children.map { it.toNitro() }.toTypedArray(),
115
157
  )
116
158
  }
117
159
 
118
160
  fun JsonResolvedTrack.toNitro(): ResolvedTrack {
119
161
  return ResolvedTrack(
120
162
  id = id,
121
- url = url,
163
+ path = path,
164
+ sections = sections?.map { it.toNitro() }?.toTypedArray(),
122
165
  children = children?.map { it.toNitro() }?.toTypedArray(),
123
166
  carPlaySiriListButton = carPlaySiriListButton.toCarPlaySiriListButtonPosition(),
124
167
  title = title,
@@ -127,45 +170,39 @@ fun JsonResolvedTrack.toNitro(): ResolvedTrack {
127
170
  artworkSource = null,
128
171
  // Containers carry no per-track media-request override (mirrors iOS JsonResolvedTrack).
129
172
  request = null,
130
- artworkCarPlayTinted = null,
131
173
  artist = artist,
132
- albumUrl = albumUrl,
174
+ albumPath = albumPath,
133
175
  album = album,
134
176
  description = description,
135
177
  genre = genre,
136
178
  duration = duration,
137
179
  src = src,
138
- style = style.toTrackStyle(),
139
- childrenStyle = childrenStyle.toTrackStyle(),
180
+ style = style.toSectionStyle(),
181
+ disabled = disabled,
140
182
  favorited = null,
141
- groupTitle = groupTitle,
142
183
  live = live,
143
- imageRow = null,
144
184
  )
145
185
  }
146
186
 
147
187
  fun JsonTrack.toNitro(): Track {
148
188
  return Track(
149
189
  id = id,
150
- url = url,
190
+ path = path,
151
191
  title = title,
152
192
  subtitle = subtitle,
153
193
  artwork = artwork?.toNitro(),
154
194
  artworkSource = null,
155
195
  request = request?.toNitro(),
156
- artworkCarPlayTinted = null,
157
196
  artist = artist,
158
- albumUrl = albumUrl,
197
+ albumPath = albumPath,
159
198
  album = album,
160
199
  description = description,
161
200
  genre = genre,
162
201
  duration = duration,
163
202
  src = src,
164
203
  style = style.toTrackStyle(),
165
- childrenStyle = childrenStyle.toTrackStyle(),
204
+ disabled = disabled,
166
205
  favorited = null,
167
- groupTitle = groupTitle,
168
206
  live = live,
169
- imageRow = imageRow?.map { it.toNitro() }?.toTypedArray(),
170
207
  )
171
208
  }