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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/README.md +4 -4
  2. package/android/build.gradle +6 -5
  3. package/android/gradle.properties +1 -1
  4. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
  5. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
  6. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
  7. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -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 +49 -0
  10. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -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/player/MediaSessionCallback.kt +120 -67
  14. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
  15. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +93 -73
  16. package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
  17. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  18. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +46 -5
  19. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +86 -27
  20. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  21. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +20 -6
  22. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +249 -86
  23. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
  24. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  25. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +35 -0
  26. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
  27. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +69 -30
  28. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  29. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
  30. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +345 -8
  31. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  32. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
  33. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
  34. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
  35. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -0
  36. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  37. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  38. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
  39. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  40. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  41. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +152 -8
  42. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +218 -10
  43. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  44. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  45. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
  46. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  47. package/ios/Browser/BrowserConfig.swift +102 -5
  48. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  49. package/ios/Browser/BrowserManager.swift +131 -143
  50. package/ios/Browser/BrowserPathHelper.swift +44 -44
  51. package/ios/Browser/JsonModels.swift +187 -106
  52. package/ios/Browser/SearchDrillIn.swift +6 -6
  53. package/ios/Browser/SectionPresentation.swift +34 -0
  54. package/ios/Browser/SectionScope.swift +58 -24
  55. package/ios/Browser/StyleResolver.swift +47 -0
  56. package/ios/Browser/TabBarEntries.swift +2 -2
  57. package/ios/Browser/TrackIdTemplate.swift +37 -0
  58. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  59. package/ios/CarPlay/CarPlayController.swift +105 -67
  60. package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
  61. package/ios/CarPlay/CarPlayListItemFactory.swift +217 -84
  62. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  63. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  64. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  65. package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
  66. package/ios/Extension/Section+Copying.swift +31 -0
  67. package/ios/Extension/Track+Copying.swift +6 -12
  68. package/ios/Extension/Track+Identity.swift +18 -0
  69. package/ios/HybridAudioBrowser.swift +113 -35
  70. package/ios/Model/NitroTypeStubs.swift +64 -23
  71. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  72. package/ios/Player/NowPlayingUpdater.swift +1 -1
  73. package/ios/Player/PlaybackCoordinator.swift +2 -2
  74. package/ios/Player/RetryManager.swift +4 -3
  75. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  76. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  77. package/ios/Tests/JsonModelsTests.swift +75 -1
  78. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  79. package/ios/Tests/QueueManagerTests.swift +4 -4
  80. package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
  81. package/ios/Tests/SearchDrillInTests.swift +16 -16
  82. package/ios/Tests/SectionPresentationTests.swift +61 -0
  83. package/ios/Tests/SectionScopeTests.swift +87 -55
  84. package/ios/Tests/StyleResolverTests.swift +67 -0
  85. package/ios/Tests/TabBarEntriesTests.swift +18 -18
  86. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  87. package/ios/Tests/TrackIdentityTests.swift +20 -0
  88. package/ios/Tests/TrackSelectorTests.swift +168 -33
  89. package/ios/TrackSelector.swift +45 -30
  90. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  91. package/lib/commonjs/AudioBrowser.js +11 -0
  92. package/lib/commonjs/AudioBrowser.js.map +1 -1
  93. package/lib/commonjs/features/browser-config.js +11 -8
  94. package/lib/commonjs/features/browser-config.js.map +1 -1
  95. package/lib/commonjs/features/browser.js +2 -2
  96. package/lib/commonjs/features/favorites.js +13 -10
  97. package/lib/commonjs/features/favorites.js.map +1 -1
  98. package/lib/commonjs/features/playback/controls.js +4 -2
  99. package/lib/commonjs/features/playback/controls.js.map +1 -1
  100. package/lib/commonjs/features/playback/state.js +2 -3
  101. package/lib/commonjs/features/playback/state.js.map +1 -1
  102. package/lib/commonjs/features/player/options.js.map +1 -1
  103. package/lib/commonjs/features/player/setup.js +3 -1
  104. package/lib/commonjs/features/player/setup.js.map +1 -1
  105. package/lib/commonjs/features/player/validateOptions.js +2 -0
  106. package/lib/commonjs/features/player/validateOptions.js.map +1 -1
  107. package/lib/commonjs/features/queue/activeTrack.js +14 -3
  108. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  109. package/lib/commonjs/features/queue/queue.js +11 -4
  110. package/lib/commonjs/features/queue/queue.js.map +1 -1
  111. package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
  112. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  113. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  114. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  115. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  116. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  117. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  118. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  119. package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
  120. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  121. package/lib/commonjs/web/browser/BrowserManager.js +144 -87
  122. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  123. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  124. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  125. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  126. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  127. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  128. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  129. package/lib/commonjs/web/browser/sections.js +89 -0
  130. package/lib/commonjs/web/browser/sections.js.map +1 -0
  131. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  132. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  133. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  134. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  135. package/lib/module/AudioBrowser.js +1 -0
  136. package/lib/module/AudioBrowser.js.map +1 -1
  137. package/lib/module/features/browser-config.js +11 -8
  138. package/lib/module/features/browser-config.js.map +1 -1
  139. package/lib/module/features/browser.js +2 -2
  140. package/lib/module/features/favorites.js +13 -10
  141. package/lib/module/features/favorites.js.map +1 -1
  142. package/lib/module/features/playback/controls.js +4 -2
  143. package/lib/module/features/playback/controls.js.map +1 -1
  144. package/lib/module/features/playback/state.js +2 -3
  145. package/lib/module/features/playback/state.js.map +1 -1
  146. package/lib/module/features/player/options.js.map +1 -1
  147. package/lib/module/features/player/setup.js +3 -1
  148. package/lib/module/features/player/setup.js.map +1 -1
  149. package/lib/module/features/player/validateOptions.js +2 -0
  150. package/lib/module/features/player/validateOptions.js.map +1 -1
  151. package/lib/module/features/queue/activeTrack.js +14 -3
  152. package/lib/module/features/queue/activeTrack.js.map +1 -1
  153. package/lib/module/features/queue/queue.js +11 -4
  154. package/lib/module/features/queue/queue.js.map +1 -1
  155. package/lib/module/utils/LazyNativeEmitter.js +21 -10
  156. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  157. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  158. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  159. package/lib/module/utils/getTrackIdentity.js +20 -0
  160. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  161. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  162. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  163. package/lib/module/web/NativeAudioBrowser.js +32 -24
  164. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  165. package/lib/module/web/browser/BrowserManager.js +144 -87
  166. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  167. package/lib/module/web/browser/FavoriteManager.js +31 -21
  168. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  169. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  170. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  171. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  172. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  173. package/lib/module/web/browser/sections.js +83 -0
  174. package/lib/module/web/browser/sections.js.map +1 -0
  175. package/lib/module/web/player/NowPlayingManager.js +4 -3
  176. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  177. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  178. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  179. package/lib/typescript/src/AudioBrowser.d.ts +1 -0
  180. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  181. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  182. package/lib/typescript/src/features/browser.d.ts +2 -2
  183. package/lib/typescript/src/features/favorites.d.ts +13 -10
  184. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  185. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  186. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  187. package/lib/typescript/src/features/playback/state.d.ts +2 -3
  188. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  189. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  190. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  191. package/lib/typescript/src/features/player/options.d.ts +21 -19
  192. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  193. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  194. package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
  195. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
  196. package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
  197. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  198. package/lib/typescript/src/features/queue/queue.d.ts +5 -2
  199. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  200. package/lib/typescript/src/types/browser-native.d.ts +2 -2
  201. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  202. package/lib/typescript/src/types/browser-nodes.d.ts +260 -127
  203. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  204. package/lib/typescript/src/types/browser.d.ts +33 -53
  205. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  206. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
  207. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  208. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  209. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  210. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  211. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  212. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  213. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  214. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
  215. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  216. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -3
  217. package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
  218. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  219. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
  220. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
  221. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
  222. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
  223. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
  224. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
  225. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
  226. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -2
  227. 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
  228. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
  229. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
  230. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
  231. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
  232. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
  233. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
  234. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  235. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
  236. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
  237. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
  238. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -7
  239. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
  240. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  241. package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
  242. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -3
  243. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  244. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
  245. package/nitrogen/generated/android/c++/JSection.hpp +114 -0
  246. package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
  247. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  248. package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
  249. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
  250. package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
  251. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -4
  252. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  253. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  254. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  255. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
  256. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  257. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  258. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
  259. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +50 -0
  260. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
  261. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  262. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
  263. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +23 -5
  264. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
  265. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
  266. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
  267. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  268. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  269. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  270. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
  271. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  272. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  273. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
  274. package/nitrogen/generated/ios/swift/Section.swift +100 -0
  275. package/nitrogen/generated/ios/swift/SectionStyle.swift +64 -0
  276. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  277. package/nitrogen/generated/ios/swift/Track.swift +20 -73
  278. package/nitrogen/generated/ios/swift/TrackStyle.swift +28 -22
  279. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  280. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  281. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  282. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
  283. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  284. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  285. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
  286. package/nitrogen/generated/shared/c++/Section.hpp +106 -0
  287. package/nitrogen/generated/shared/c++/SectionStyle.hpp +96 -0
  288. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  289. package/nitrogen/generated/shared/c++/Track.hpp +15 -31
  290. package/nitrogen/generated/shared/c++/TrackStyle.hpp +50 -34
  291. package/package.json +3 -2
  292. package/src/AudioBrowser.ts +1 -0
  293. package/src/features/browser-config.test.ts +21 -6
  294. package/src/features/browser-config.ts +11 -8
  295. package/src/features/browser.test.ts +1 -1
  296. package/src/features/browser.ts +2 -2
  297. package/src/features/favorites.ts +13 -10
  298. package/src/features/hook-wiring.test.ts +63 -15
  299. package/src/features/playback/controls.ts +4 -2
  300. package/src/features/playback/state.ts +2 -3
  301. package/src/features/playback/trackPlaybackTime.ts +1 -1
  302. package/src/features/player/options.ts +27 -19
  303. package/src/features/player/setup.test.ts +12 -3
  304. package/src/features/player/setup.ts +3 -1
  305. package/src/features/player/validateOptions.ts +2 -0
  306. package/src/features/queue/activeTrack.ts +14 -3
  307. package/src/features/queue/queue.ts +11 -4
  308. package/src/nitro-stub-drift.test.ts +277 -0
  309. package/src/public-surface.test.ts +40 -0
  310. package/src/specs/audio-browser.nitro.ts +4 -3
  311. package/src/style-resolution.test.ts +139 -0
  312. package/src/types/browser-native.ts +2 -2
  313. package/src/types/browser-nodes.ts +268 -136
  314. package/src/types/browser.ts +34 -55
  315. package/src/utils/LazyNativeEmitter.ts +20 -10
  316. package/src/utils/NativeUpdatedValue.ts +29 -13
  317. package/src/utils/getTrackIdentity.ts +20 -0
  318. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  319. package/src/web/NativeAudioBrowser.ts +37 -22
  320. package/src/web/browser/BrowserManager.ts +176 -95
  321. package/src/web/browser/FavoriteManager.test.ts +84 -0
  322. package/src/web/browser/FavoriteManager.ts +28 -23
  323. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  324. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  325. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  326. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  327. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  328. package/src/web/browser/sections.test.ts +91 -0
  329. package/src/web/browser/sections.ts +72 -0
  330. package/src/web/player/NowPlayingManager.ts +4 -3
  331. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  332. package/src/web/util/BrowserPathHelper.ts +61 -13
  333. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  334. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  335. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  336. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  337. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  338. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  339. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  340. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  341. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  342. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  343. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  344. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  345. 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>?,
@@ -1,30 +1,25 @@
1
1
  package com.audiobrowser.browser
2
2
 
3
+ import com.audiobrowser.util.toArtworkRendering
4
+ import com.audiobrowser.util.toStyleDisplay
3
5
  import com.margelo.nitro.audiobrowser.CarPlaySiriListButtonPosition
4
- import com.margelo.nitro.audiobrowser.ImageRowItem
5
6
  import com.margelo.nitro.audiobrowser.ResolvedTrack
7
+ import com.margelo.nitro.audiobrowser.Section
8
+ import com.margelo.nitro.audiobrowser.SectionStyle
6
9
  import com.margelo.nitro.audiobrowser.Track
7
10
  import com.margelo.nitro.audiobrowser.TrackRequest
8
11
  import com.margelo.nitro.audiobrowser.TrackStyle
9
12
  import kotlinx.serialization.Serializable
13
+ import kotlinx.serialization.json.JsonElement
14
+ import kotlinx.serialization.json.JsonObject
15
+ import kotlinx.serialization.json.JsonPrimitive
16
+ import kotlinx.serialization.json.booleanOrNull
10
17
 
11
18
  /**
12
19
  * JSON serializable models for parsing API responses. These will be converted to Nitro types after
13
- * parsing.
20
+ * parsing. Legacy `groupTitle`/`imageRow` keys in payloads are simply unknown to these models and
21
+ * decode as ignored dead weight (ADR 0010).
14
22
  */
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
23
 
29
24
  /** JSON model for a track's per-request HTTP override (identity/auth/signed-url). */
30
25
  @Serializable
@@ -36,25 +31,35 @@ data class JsonTrackRequest(
36
31
  fun toNitro(): TrackRequest = TrackRequest(userAgent, headers, query)
37
32
  }
38
33
 
34
+ /** JSON model for a page section (ADR 0010). */
35
+ @Serializable
36
+ data class JsonSection(
37
+ val title: String? = null,
38
+ val subtitle: String? = null,
39
+ val style: JsonElement? = null,
40
+ val path: String? = null,
41
+ val children: List<JsonTrack>,
42
+ )
43
+
39
44
  @Serializable
40
45
  data class JsonResolvedTrack(
41
46
  val id: String? = null,
42
- val url: String,
47
+ val path: String,
43
48
  val title: String,
44
49
  val subtitle: String? = null,
45
50
  val icon: String? = null,
46
51
  val artwork: JsonArtwork? = null,
47
52
  val artist: String? = null,
48
- val albumUrl: String? = null,
53
+ val albumPath: String? = null,
49
54
  val album: String? = null,
50
55
  val description: String? = null,
51
56
  val genre: String? = null,
52
57
  val duration: Double? = null,
58
+ val sections: List<JsonSection>? = null,
53
59
  val children: List<JsonTrack>? = null,
54
60
  val src: String? = null,
55
- val style: String? = null,
56
- val childrenStyle: String? = null,
57
- val groupTitle: String? = null,
61
+ val style: JsonElement? = null,
62
+ val disabled: Boolean? = null,
58
63
  val live: Boolean? = null,
59
64
  val carPlaySiriListButton: String? = null,
60
65
  )
@@ -62,34 +67,59 @@ data class JsonResolvedTrack(
62
67
  @Serializable
63
68
  data class JsonTrack(
64
69
  val id: String? = null,
65
- val url: String? = null,
70
+ val path: String? = null,
66
71
  val title: String,
67
72
  val subtitle: String? = null,
68
73
  val icon: String? = null,
69
74
  val artwork: JsonArtwork? = null,
70
75
  val artist: String? = null,
71
- val albumUrl: String? = null,
76
+ val albumPath: String? = null,
72
77
  val album: String? = null,
73
78
  val description: String? = null,
74
79
  val genre: String? = null,
75
80
  val duration: Double? = null,
76
81
  val src: String? = null,
77
82
  val request: JsonTrackRequest? = null,
78
- val style: String? = null,
79
- val childrenStyle: String? = null,
80
- val groupTitle: String? = null,
83
+ val style: JsonElement? = null,
84
+ val disabled: Boolean? = null,
81
85
  val live: Boolean? = null,
82
- val imageRow: List<JsonImageRowItem>? = null,
83
86
  )
84
87
 
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
88
+ /**
89
+ * Convert JSON models to Nitro types.
90
+ *
91
+ * `style` rides the models as a raw [JsonElement] and maps leniently here — tolerant decoding by
92
+ * design (ADR 0011): a stale value (the retired string vocabulary), a wrong-typed field, or an
93
+ * unknown enum value is "no declaration", never a parse failure. A typed field would fail the whole
94
+ * page, since this decoder is otherwise strict.
95
+ */
96
+ private fun JsonObject.stringField(key: String): String? =
97
+ (get(key) as? JsonPrimitive)?.takeIf { it.isString }?.content
98
+
99
+ /** A strict JSON boolean — a quoted "false" is a wrong-typed field, i.e. no declaration. */
100
+ private fun JsonObject.booleanField(key: String): Boolean? =
101
+ (get(key) as? JsonPrimitive)?.takeIf { !it.isString }?.booleanOrNull
102
+
103
+ /**
104
+ * A block that resolves to no declarations at all — `{}` or all-unknown values — maps to null, so
105
+ * "no declaration" has one shape on every platform (iOS makes the same collapse).
106
+ */
107
+ private fun JsonElement?.toTrackStyle(): TrackStyle? =
108
+ (this as? JsonObject)?.let { obj ->
109
+ val display = obj.stringField("display").toStyleDisplay()
110
+ val artworkRendering = obj.stringField("artworkRendering").toArtworkRendering()
111
+ if (display == null && artworkRendering == null) null
112
+ else TrackStyle(display = display, artworkRendering = artworkRendering)
113
+ }
114
+
115
+ private fun JsonElement?.toSectionStyle(): SectionStyle? =
116
+ (this as? JsonObject)?.let { obj ->
117
+ val display = obj.stringField("display").toStyleDisplay()
118
+ val artworkRendering = obj.stringField("artworkRendering").toArtworkRendering()
119
+ val gridWrap = obj.booleanField("gridWrap")
120
+ if (display == null && artworkRendering == null && gridWrap == null) null
121
+ else SectionStyle(display = display, artworkRendering = artworkRendering, gridWrap = gridWrap)
91
122
  }
92
- }
93
123
 
94
124
  private fun String?.toCarPlaySiriListButtonPosition(): CarPlaySiriListButtonPosition? {
95
125
  return when (this?.lowercase()) {
@@ -99,26 +129,21 @@ private fun String?.toCarPlaySiriListButtonPosition(): CarPlaySiriListButtonPosi
99
129
  }
100
130
  }
101
131
 
102
- private fun JsonImageRowItem.toNitro(): ImageRowItem {
103
- return ImageRowItem(
104
- id = id,
105
- url = url,
106
- src = src,
107
- artwork = artwork,
108
- artworkSource = null,
132
+ fun JsonSection.toNitro(): Section {
133
+ return Section(
109
134
  title = title,
110
- artist = artist,
111
- album = album,
112
- albumUrl = albumUrl,
113
- live = live,
114
- request = request?.toNitro(),
135
+ subtitle = subtitle,
136
+ style = style.toSectionStyle(),
137
+ path = path,
138
+ children = children.map { it.toNitro() }.toTypedArray(),
115
139
  )
116
140
  }
117
141
 
118
142
  fun JsonResolvedTrack.toNitro(): ResolvedTrack {
119
143
  return ResolvedTrack(
120
144
  id = id,
121
- url = url,
145
+ path = path,
146
+ sections = sections?.map { it.toNitro() }?.toTypedArray(),
122
147
  children = children?.map { it.toNitro() }?.toTypedArray(),
123
148
  carPlaySiriListButton = carPlaySiriListButton.toCarPlaySiriListButtonPosition(),
124
149
  title = title,
@@ -127,45 +152,39 @@ fun JsonResolvedTrack.toNitro(): ResolvedTrack {
127
152
  artworkSource = null,
128
153
  // Containers carry no per-track media-request override (mirrors iOS JsonResolvedTrack).
129
154
  request = null,
130
- artworkCarPlayTinted = null,
131
155
  artist = artist,
132
- albumUrl = albumUrl,
156
+ albumPath = albumPath,
133
157
  album = album,
134
158
  description = description,
135
159
  genre = genre,
136
160
  duration = duration,
137
161
  src = src,
138
- style = style.toTrackStyle(),
139
- childrenStyle = childrenStyle.toTrackStyle(),
162
+ style = style.toSectionStyle(),
163
+ disabled = disabled,
140
164
  favorited = null,
141
- groupTitle = groupTitle,
142
165
  live = live,
143
- imageRow = null,
144
166
  )
145
167
  }
146
168
 
147
169
  fun JsonTrack.toNitro(): Track {
148
170
  return Track(
149
171
  id = id,
150
- url = url,
172
+ path = path,
151
173
  title = title,
152
174
  subtitle = subtitle,
153
175
  artwork = artwork?.toNitro(),
154
176
  artworkSource = null,
155
177
  request = request?.toNitro(),
156
- artworkCarPlayTinted = null,
157
178
  artist = artist,
158
- albumUrl = albumUrl,
179
+ albumPath = albumPath,
159
180
  album = album,
160
181
  description = description,
161
182
  genre = genre,
162
183
  duration = duration,
163
184
  src = src,
164
185
  style = style.toTrackStyle(),
165
- childrenStyle = childrenStyle.toTrackStyle(),
186
+ disabled = disabled,
166
187
  favorited = null,
167
- groupTitle = groupTitle,
168
188
  live = live,
169
- imageRow = imageRow?.map { it.toNitro() }?.toTypedArray(),
170
189
  )
171
190
  }
@@ -1,42 +1,84 @@
1
1
  package com.audiobrowser.browser
2
2
 
3
- import com.margelo.nitro.audiobrowser.ImageRowItem
3
+ import com.audiobrowser.extension.identity
4
+ import com.margelo.nitro.audiobrowser.ResolvedTrack
5
+ import com.margelo.nitro.audiobrowser.Section
4
6
  import com.margelo.nitro.audiobrowser.Track
5
7
 
6
8
  /**
7
9
  * Queue scope is the tapped section, not the whole page (ADR 0006): the playback context a listener
8
10
  * expects is the list they tapped in — a page aggregating several sections must not leak
9
- * next/previous across them.
11
+ * next/previous across them. Sections are structural (ADR 0010), so scoping is a lookup, not a
12
+ * groupTitle-run derivation.
10
13
  */
11
14
  object SectionScope {
12
- sealed class Section {
13
- /** The tapped id lives inside an image row's items. */
14
- data class ImageRow(val items: List<ImageRowItem>) : Section()
15
-
16
- /**
17
- * The contiguous `groupTitle` run around the tapped child (items with no group title form runs
18
- * of their own).
19
- */
20
- data class Run(val tracks: List<Track>) : Section()
21
- }
15
+ /**
16
+ * The section containing the tapped identity, plus the tapped child's offset within it — non-null
17
+ * only when the stamped flat index pinned the exact copy.
18
+ */
19
+ data class Scoped(val section: Section, val tappedOffset: Int?)
22
20
 
23
21
  /**
24
- * The section of [children] containing the playable [trackId], or null when the id is not found.
22
+ * The section of [sections] containing the playable [trackId] (a track identity — id when
23
+ * non-blank, else src), or null when not found.
24
+ *
25
+ * [tappedIndex] — the flat page position stamped into the contextual URL (children concatenated
26
+ * in section order) — is a tie-breaker, never an identifier: when the child at that position
27
+ * still carries the tapped identity, it pins which section (and which copy) was tapped; when it
28
+ * doesn't (the list shifted), resolution falls back to the first section containing the identity.
29
+ * A stale index can therefore never select a different track — at worst a different copy of the
30
+ * same one.
25
31
  */
26
- fun section(children: List<Track>, trackId: String): Section? {
27
- for (child in children) {
28
- val items = child.imageRow
29
- if (items != null && items.any { it.src == trackId }) {
30
- return Section.ImageRow(items.toList())
32
+ fun scoped(sections: List<Section>, trackId: String, tappedIndex: Int? = null): Scoped? {
33
+ if (tappedIndex != null && tappedIndex >= 0) {
34
+ var start = 0
35
+ for (section in sections) {
36
+ val offset = tappedIndex - start
37
+ if (offset < section.children.size) {
38
+ if (section.children[offset].identity == trackId) {
39
+ return Scoped(section, offset)
40
+ }
41
+ break
42
+ }
43
+ start += section.children.size
44
+ }
45
+ }
46
+ for (section in sections) {
47
+ if (section.children.any { it.identity == trackId }) {
48
+ return Scoped(section, null)
31
49
  }
32
50
  }
33
- val index = children.indexOfFirst { it.src == trackId }
34
- if (index < 0) return null
35
- val group = children[index].groupTitle
36
- var start = index
37
- while (start > 0 && children[start - 1].groupTitle == group) start--
38
- var end = index
39
- while (end + 1 < children.size && children[end + 1].groupTitle == group) end++
40
- return Section.Run(children.subList(start, end + 1).toList())
51
+ return null
41
52
  }
42
53
  }
54
+
55
+ // Page shape helpers (ADR 0010)
56
+
57
+ /**
58
+ * One untitled section — the canonical wrap of a flat track list (`children` authoring sugar,
59
+ * search results — ADR 0010).
60
+ */
61
+ fun untitledSection(children: Array<Track>): Section =
62
+ Section(title = null, subtitle = null, style = null, path = null, children = children)
63
+
64
+ /**
65
+ * The canonical sectioned shape: `sections` wins when present; plain `children` is authoring sugar
66
+ * for one untitled section.
67
+ */
68
+ val ResolvedTrack.normalizedSections: List<Section>?
69
+ get() = sections?.toList() ?: children?.let { listOf(untitledSection(it)) }
70
+
71
+ /**
72
+ * [normalizedSections] with the page block folded in: each section's style resolves `section ??
73
+ * page` per property ([StyleResolver], ADR 0011), so everything downstream sees only effective
74
+ * section blocks — a page is a Track that is also the container of its sections.
75
+ */
76
+ fun ResolvedTrack.styleResolvedSections(): List<Section>? =
77
+ normalizedSections?.map { it.copy(style = StyleResolver.sectionStyle(it.style, style)) }
78
+
79
+ /**
80
+ * The page's children concatenated in section order — the flattening that defines contextual
81
+ * `__index` positions and the flat views (tabs, search) of a sectioned page.
82
+ */
83
+ val ResolvedTrack.flattenedChildren: List<Track>?
84
+ get() = normalizedSections?.flatMap { it.children.asList() }
@@ -0,0 +1,49 @@
1
+ package com.audiobrowser.browser
2
+
3
+ import com.margelo.nitro.audiobrowser.SectionStyle
4
+ import com.margelo.nitro.audiobrowser.TrackStyle
5
+
6
+ /**
7
+ * The one place style declarations resolve to effective values (ADR 0011).
8
+ *
9
+ * Two mechanisms, deliberately distinct:
10
+ * - **Inherited item properties** resolve `track ?? section ?? page`, per property — a track's own
11
+ * declaration wins over the blocks that contain it.
12
+ * - **Container properties and the positional `display`** resolve by scope override: `section ??
13
+ * page` — the page declares for its whole scope, a section overrides for its own children.
14
+ * `display` is never inherited item-to-container: each holder describes its own children, and a
15
+ * track's `display` is the promise for the page *it* opens, not its rendering inside this
16
+ * section.
17
+ *
18
+ * Every key of `SectionStyle` must be read by [sectionStyle] and every inherited key of
19
+ * `TrackStyle` by [trackStyle] — Nitro flattens the spec's `extends`, so
20
+ * `src/style-resolution.test.ts` enforces this completeness, not the type system.
21
+ */
22
+ object StyleResolver {
23
+ /**
24
+ * Folds the page block into a section's block: every `SectionStyle` key resolves `section ??
25
+ * page`. The result is the section's effective block; item resolution then only needs `track ??
26
+ * section`.
27
+ */
28
+ fun sectionStyle(section: SectionStyle?, page: SectionStyle?): SectionStyle =
29
+ SectionStyle(
30
+ display = section?.display ?: page?.display,
31
+ artworkRendering = section?.artworkRendering ?: page?.artworkRendering,
32
+ gridWrap = section?.gridWrap ?: page?.gridWrap,
33
+ )
34
+
35
+ /**
36
+ * Resolves a track's effective item properties against its (page-folded) section block.
37
+ *
38
+ * No Android renderer consumes the result yet — today's only inherited property
39
+ * (`artworkRendering`) is CarPlay-rendered — but the resolver ships on both platforms so the
40
+ * completeness test can hold every future inherited key to the same rule from day one.
41
+ */
42
+ fun trackStyle(track: TrackStyle?, section: SectionStyle?): TrackStyle =
43
+ TrackStyle(
44
+ // Positional deny-list: `display` is never inherited onto an item — resolved null, so no
45
+ // renderer can mistake the handle's page promise for this item's own layout.
46
+ display = null,
47
+ artworkRendering = track?.artworkRendering ?: section?.artworkRendering,
48
+ )
49
+ }
@@ -2,6 +2,7 @@ package com.audiobrowser.extension
2
2
 
3
3
  import com.margelo.nitro.audiobrowser.ResolvedTrack
4
4
  import com.margelo.nitro.audiobrowser.Track
5
+ import com.margelo.nitro.audiobrowser.TrackStyle
5
6
 
6
7
  /**
7
8
  * The Track view of a ResolvedTrack: every display/playback field carried over, the browse-only
@@ -12,24 +13,25 @@ import com.margelo.nitro.audiobrowser.Track
12
13
  fun ResolvedTrack.toTrack(): Track =
13
14
  Track(
14
15
  id = id,
15
- url = url,
16
+ path = path,
16
17
  src = src,
17
18
  artwork = artwork,
18
19
  artworkSource = artworkSource,
19
20
  request = request,
20
- artworkCarPlayTinted = artworkCarPlayTinted,
21
21
  title = title,
22
22
  subtitle = subtitle,
23
23
  artist = artist,
24
- albumUrl = albumUrl,
24
+ albumPath = albumPath,
25
25
  album = album,
26
26
  description = description,
27
27
  genre = genre,
28
28
  duration = duration,
29
- style = style,
30
- childrenStyle = childrenStyle,
29
+ // An explicit SectionStyle → TrackStyle projection: the page's container
30
+ // properties (gridWrap) have no meaning on a plain Track, so they drop —
31
+ // Nitro flattens the spec's `extends`, making this narrowing a hand-written
32
+ // field list rather than an upcast (ADR 0011).
33
+ style = style?.let { TrackStyle(display = it.display, artworkRendering = it.artworkRendering) },
34
+ disabled = disabled,
31
35
  favorited = favorited,
32
- groupTitle = groupTitle,
33
36
  live = live,
34
- imageRow = imageRow,
35
37
  )
@@ -0,0 +1,20 @@
1
+ package com.audiobrowser.extension
2
+
3
+ import com.audiobrowser.util.BrowserPathHelper
4
+ import com.margelo.nitro.audiobrowser.Track
5
+
6
+ /**
7
+ * The queue position of the tapped surface, for skip-in-place. Exact-surface match first: a
8
+ * contextual path carries the tapped page position (`__index`), so path equality pins the exact
9
+ * copy when the page holds the same identity more than once. The identity match remains for
10
+ * index-less paths (e.g. pre-stamp persisted state); an index-stamped path with no exact match
11
+ * returns -1, and the caller falls through to expansion, which re-scopes the queue to the tapped
12
+ * section (ADR 0009).
13
+ */
14
+ fun Array<Track>.indexOfTappedTrack(contextualPath: String, trackId: String?): Int {
15
+ val index = indexOfFirst { it.path == contextualPath }
16
+ if (index >= 0 || BrowserPathHelper.extractIndex(contextualPath) != null || trackId == null) {
17
+ return index
18
+ }
19
+ return indexOfFirst { it.identity == trackId }
20
+ }
@@ -0,0 +1,15 @@
1
+ package com.audiobrowser.extension
2
+
3
+ import com.margelo.nitro.audiobrowser.Track
4
+
5
+ /**
6
+ * A track's identity: the opaque `id` when present (non-blank), else the playable `src`. Two tracks
7
+ * refer to the same item iff their identities are equal. Browsable-only tracks (neither `id` nor
8
+ * `src`) have no identity — they are addressed by `path` instead.
9
+ *
10
+ * This is THE comparison rule for favorites matching, section scoping, skip-in-place, the car
11
+ * now-playing row indicator, and the contextual `__trackId` — see ADR 0008. Mirrors the TS
12
+ * `trackIdentity` helper.
13
+ */
14
+ val Track.identity: String?
15
+ get() = id?.takeUnless { it.isBlank() } ?: src