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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/README.md +4 -4
  2. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
  3. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
  4. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
  5. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -58
  6. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
  7. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +49 -0
  8. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -7
  9. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  10. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  11. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
  12. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
  13. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +93 -73
  14. package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
  15. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  16. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
  17. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  18. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +35 -0
  19. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
  20. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +26 -39
  21. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  22. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +228 -50
  23. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  24. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
  25. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
  26. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
  27. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -0
  28. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  29. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  30. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
  31. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  32. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  33. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  34. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
  35. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  36. package/ios/Browser/BrowserConfig.swift +102 -5
  37. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  38. package/ios/Browser/BrowserManager.swift +131 -143
  39. package/ios/Browser/BrowserPathHelper.swift +44 -44
  40. package/ios/Browser/JsonModels.swift +187 -106
  41. package/ios/Browser/SearchDrillIn.swift +6 -6
  42. package/ios/Browser/SectionPresentation.swift +34 -0
  43. package/ios/Browser/SectionScope.swift +58 -24
  44. package/ios/Browser/StyleResolver.swift +47 -0
  45. package/ios/Browser/TabBarEntries.swift +2 -2
  46. package/ios/Browser/TrackIdTemplate.swift +37 -0
  47. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  48. package/ios/CarPlay/CarPlayController.swift +105 -67
  49. package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
  50. package/ios/CarPlay/CarPlayListItemFactory.swift +217 -84
  51. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  52. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  53. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  54. package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
  55. package/ios/Extension/Section+Copying.swift +31 -0
  56. package/ios/Extension/Track+Copying.swift +6 -12
  57. package/ios/Extension/Track+Identity.swift +18 -0
  58. package/ios/HybridAudioBrowser.swift +113 -35
  59. package/ios/Model/NitroTypeStubs.swift +46 -21
  60. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  61. package/ios/Player/NowPlayingUpdater.swift +1 -1
  62. package/ios/Player/PlaybackCoordinator.swift +2 -2
  63. package/ios/Player/RetryManager.swift +4 -3
  64. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  65. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  66. package/ios/Tests/JsonModelsTests.swift +75 -1
  67. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  68. package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
  69. package/ios/Tests/SearchDrillInTests.swift +16 -16
  70. package/ios/Tests/SectionPresentationTests.swift +61 -0
  71. package/ios/Tests/SectionScopeTests.swift +87 -55
  72. package/ios/Tests/StyleResolverTests.swift +67 -0
  73. package/ios/Tests/TabBarEntriesTests.swift +18 -18
  74. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  75. package/ios/Tests/TrackIdentityTests.swift +20 -0
  76. package/ios/Tests/TrackSelectorTests.swift +168 -33
  77. package/ios/TrackSelector.swift +45 -30
  78. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  79. package/lib/commonjs/AudioBrowser.js +11 -0
  80. package/lib/commonjs/AudioBrowser.js.map +1 -1
  81. package/lib/commonjs/features/browser-config.js +11 -8
  82. package/lib/commonjs/features/browser-config.js.map +1 -1
  83. package/lib/commonjs/features/browser.js +2 -2
  84. package/lib/commonjs/features/favorites.js +13 -10
  85. package/lib/commonjs/features/favorites.js.map +1 -1
  86. package/lib/commonjs/features/playback/controls.js +4 -2
  87. package/lib/commonjs/features/playback/controls.js.map +1 -1
  88. package/lib/commonjs/features/playback/state.js +2 -3
  89. package/lib/commonjs/features/playback/state.js.map +1 -1
  90. package/lib/commonjs/features/player/options.js.map +1 -1
  91. package/lib/commonjs/features/player/setup.js +3 -1
  92. package/lib/commonjs/features/player/setup.js.map +1 -1
  93. package/lib/commonjs/features/player/validateOptions.js +2 -0
  94. package/lib/commonjs/features/player/validateOptions.js.map +1 -1
  95. package/lib/commonjs/features/queue/activeTrack.js +14 -3
  96. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  97. package/lib/commonjs/features/queue/queue.js +11 -4
  98. package/lib/commonjs/features/queue/queue.js.map +1 -1
  99. package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
  100. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  101. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  102. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  103. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  104. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  105. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  106. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  107. package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
  108. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  109. package/lib/commonjs/web/browser/BrowserManager.js +144 -87
  110. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  111. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  112. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  113. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  114. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  115. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  116. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  117. package/lib/commonjs/web/browser/sections.js +89 -0
  118. package/lib/commonjs/web/browser/sections.js.map +1 -0
  119. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  120. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  121. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  122. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  123. package/lib/module/AudioBrowser.js +1 -0
  124. package/lib/module/AudioBrowser.js.map +1 -1
  125. package/lib/module/features/browser-config.js +11 -8
  126. package/lib/module/features/browser-config.js.map +1 -1
  127. package/lib/module/features/browser.js +2 -2
  128. package/lib/module/features/favorites.js +13 -10
  129. package/lib/module/features/favorites.js.map +1 -1
  130. package/lib/module/features/playback/controls.js +4 -2
  131. package/lib/module/features/playback/controls.js.map +1 -1
  132. package/lib/module/features/playback/state.js +2 -3
  133. package/lib/module/features/playback/state.js.map +1 -1
  134. package/lib/module/features/player/options.js.map +1 -1
  135. package/lib/module/features/player/setup.js +3 -1
  136. package/lib/module/features/player/setup.js.map +1 -1
  137. package/lib/module/features/player/validateOptions.js +2 -0
  138. package/lib/module/features/player/validateOptions.js.map +1 -1
  139. package/lib/module/features/queue/activeTrack.js +14 -3
  140. package/lib/module/features/queue/activeTrack.js.map +1 -1
  141. package/lib/module/features/queue/queue.js +11 -4
  142. package/lib/module/features/queue/queue.js.map +1 -1
  143. package/lib/module/utils/LazyNativeEmitter.js +21 -10
  144. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  145. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  146. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  147. package/lib/module/utils/getTrackIdentity.js +20 -0
  148. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  149. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  150. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  151. package/lib/module/web/NativeAudioBrowser.js +32 -24
  152. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  153. package/lib/module/web/browser/BrowserManager.js +144 -87
  154. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  155. package/lib/module/web/browser/FavoriteManager.js +31 -21
  156. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  157. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  158. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  159. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  160. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  161. package/lib/module/web/browser/sections.js +83 -0
  162. package/lib/module/web/browser/sections.js.map +1 -0
  163. package/lib/module/web/player/NowPlayingManager.js +4 -3
  164. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  165. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  166. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  167. package/lib/typescript/src/AudioBrowser.d.ts +1 -0
  168. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  169. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  170. package/lib/typescript/src/features/browser.d.ts +2 -2
  171. package/lib/typescript/src/features/favorites.d.ts +13 -10
  172. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  173. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  174. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  175. package/lib/typescript/src/features/playback/state.d.ts +2 -3
  176. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  177. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  178. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  179. package/lib/typescript/src/features/player/options.d.ts +21 -19
  180. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  181. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  182. package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
  183. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
  184. package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
  185. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  186. package/lib/typescript/src/features/queue/queue.d.ts +5 -2
  187. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  188. package/lib/typescript/src/types/browser-native.d.ts +2 -2
  189. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  190. package/lib/typescript/src/types/browser-nodes.d.ts +260 -127
  191. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  192. package/lib/typescript/src/types/browser.d.ts +33 -53
  193. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  194. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
  195. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  196. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  197. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  198. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  199. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  200. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  201. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  202. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
  203. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  204. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -3
  205. package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
  206. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  207. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
  208. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
  209. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
  210. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
  211. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
  212. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
  213. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
  214. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -2
  215. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +10 -4
  216. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
  217. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
  218. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
  219. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
  220. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
  221. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
  222. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  223. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
  224. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
  225. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
  226. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -7
  227. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
  228. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  229. package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
  230. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -3
  231. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  232. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
  233. package/nitrogen/generated/android/c++/JSection.hpp +114 -0
  234. package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
  235. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  236. package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
  237. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
  238. package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
  239. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -4
  240. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  241. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  242. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  243. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
  244. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  245. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  246. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
  247. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +50 -0
  248. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
  249. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  250. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
  251. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +23 -5
  252. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
  253. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
  254. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
  255. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  256. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  257. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  258. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
  259. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  260. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  261. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
  262. package/nitrogen/generated/ios/swift/Section.swift +100 -0
  263. package/nitrogen/generated/ios/swift/SectionStyle.swift +64 -0
  264. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  265. package/nitrogen/generated/ios/swift/Track.swift +20 -73
  266. package/nitrogen/generated/ios/swift/TrackStyle.swift +28 -22
  267. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  268. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  269. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  270. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
  271. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  272. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  273. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
  274. package/nitrogen/generated/shared/c++/Section.hpp +106 -0
  275. package/nitrogen/generated/shared/c++/SectionStyle.hpp +96 -0
  276. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  277. package/nitrogen/generated/shared/c++/Track.hpp +15 -31
  278. package/nitrogen/generated/shared/c++/TrackStyle.hpp +50 -34
  279. package/package.json +1 -1
  280. package/src/AudioBrowser.ts +1 -0
  281. package/src/features/browser-config.test.ts +21 -6
  282. package/src/features/browser-config.ts +11 -8
  283. package/src/features/browser.test.ts +1 -1
  284. package/src/features/browser.ts +2 -2
  285. package/src/features/favorites.ts +13 -10
  286. package/src/features/hook-wiring.test.ts +63 -15
  287. package/src/features/playback/controls.ts +4 -2
  288. package/src/features/playback/state.ts +2 -3
  289. package/src/features/playback/trackPlaybackTime.ts +1 -1
  290. package/src/features/player/options.ts +27 -19
  291. package/src/features/player/setup.test.ts +12 -3
  292. package/src/features/player/setup.ts +3 -1
  293. package/src/features/player/validateOptions.ts +2 -0
  294. package/src/features/queue/activeTrack.ts +14 -3
  295. package/src/features/queue/queue.ts +11 -4
  296. package/src/nitro-stub-drift.test.ts +40 -0
  297. package/src/public-surface.test.ts +40 -0
  298. package/src/specs/audio-browser.nitro.ts +4 -3
  299. package/src/style-resolution.test.ts +139 -0
  300. package/src/types/browser-native.ts +2 -2
  301. package/src/types/browser-nodes.ts +268 -136
  302. package/src/types/browser.ts +34 -55
  303. package/src/utils/LazyNativeEmitter.ts +20 -10
  304. package/src/utils/NativeUpdatedValue.ts +29 -13
  305. package/src/utils/getTrackIdentity.ts +20 -0
  306. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  307. package/src/web/NativeAudioBrowser.ts +37 -22
  308. package/src/web/browser/BrowserManager.ts +176 -95
  309. package/src/web/browser/FavoriteManager.test.ts +84 -0
  310. package/src/web/browser/FavoriteManager.ts +28 -23
  311. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  312. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  313. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  314. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  315. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  316. package/src/web/browser/sections.test.ts +91 -0
  317. package/src/web/browser/sections.ts +72 -0
  318. package/src/web/player/NowPlayingManager.ts +4 -3
  319. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  320. package/src/web/util/BrowserPathHelper.ts +61 -13
  321. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  322. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  323. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  324. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  325. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  326. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  327. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  328. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  329. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  330. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  331. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  332. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  333. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
@@ -62,41 +62,125 @@ extension JsonArtwork {
62
62
  }
63
63
  }
64
64
 
65
- /// JSON model for image row items (horizontal thumbnail row).
66
- struct JsonImageRowItem: Codable {
67
- let id: String?
68
- let url: String?
69
- let src: String?
70
- let artwork: String?
71
- let title: String
72
- let artist: String?
73
- let album: String?
74
- let albumUrl: String?
75
- let live: Bool?
76
- let request: JsonTrackRequest?
65
+ /// A style declaration block on the wire (ADR 0011):
66
+ /// `{ display?, gridWrap?, artworkRendering? }`.
67
+ ///
68
+ /// One JSON model serves both the `TrackStyle` and `SectionStyle` positions —
69
+ /// keys that don't exist at the narrower position simply don't map.
70
+ ///
71
+ /// Decoding is tolerant by design: a stale payload — the retired string
72
+ /// styles, a wrong-typed field, an unknown enum value — must degrade to "no
73
+ /// declaration", never fail the page or the persisted playback state (the
74
+ /// decoders above this are strict, so a throw here would kill the whole
75
+ /// payload). `init(from:)` therefore never throws, and unknown enum values
76
+ /// map to nil at `toNitro` time.
77
+ struct JsonStyle: Codable, Equatable {
78
+ let display: String?
79
+ let gridWrap: Bool?
80
+ let artworkRendering: String?
77
81
 
78
82
  init(
79
- id: String? = nil,
80
- url: String? = nil,
81
- src: String? = nil,
82
- artwork: String? = nil,
83
- title: String,
84
- artist: String? = nil,
85
- album: String? = nil,
86
- albumUrl: String? = nil,
87
- live: Bool? = nil,
88
- request: JsonTrackRequest? = nil,
83
+ display: String? = nil,
84
+ gridWrap: Bool? = nil,
85
+ artworkRendering: String? = nil,
86
+ ) {
87
+ self.display = display
88
+ self.gridWrap = gridWrap
89
+ self.artworkRendering = artworkRendering
90
+ }
91
+
92
+ init(from decoder: Decoder) throws {
93
+ // A non-object value (the retired `style: 'grid'` string) decodes as an
94
+ // empty block rather than failing the parent.
95
+ guard let container = try? decoder.container(keyedBy: CodingKeys.self) else {
96
+ display = nil
97
+ gridWrap = nil
98
+ artworkRendering = nil
99
+ return
100
+ }
101
+ display = (try? container.decodeIfPresent(String.self, forKey: .display)) ?? nil
102
+ gridWrap = (try? container.decodeIfPresent(Bool.self, forKey: .gridWrap)) ?? nil
103
+ artworkRendering = (try? container.decodeIfPresent(String.self, forKey: .artworkRendering)) ?? nil
104
+ }
105
+ }
106
+
107
+ extension JsonStyle {
108
+ /// The wire strings map to Nitro enums here, tolerantly: an unknown value
109
+ /// (a future `display` mode, say) is no declaration, not an error. A block
110
+ /// that resolves to no declarations at all — `{}`, the tolerant decoder's
111
+ /// empty result for the retired string vocabulary, or all-unknown values —
112
+ /// maps to nil, so "no declaration" has one shape on every platform
113
+ /// (Android's lenient mapper makes the same collapse).
114
+ func toTrackStyle() -> TrackStyle? {
115
+ let display = display.flatMap { StyleDisplay(fromString: $0.lowercased()) }
116
+ let artworkRendering = artworkRendering.flatMap { ArtworkRendering(fromString: $0.lowercased()) }
117
+ guard display != nil || artworkRendering != nil else { return nil }
118
+ return TrackStyle(display: display, artworkRendering: artworkRendering)
119
+ }
120
+
121
+ func toSectionStyle() -> SectionStyle? {
122
+ let display = display.flatMap { StyleDisplay(fromString: $0.lowercased()) }
123
+ let artworkRendering = artworkRendering.flatMap { ArtworkRendering(fromString: $0.lowercased()) }
124
+ guard display != nil || artworkRendering != nil || gridWrap != nil else { return nil }
125
+ // Argument order is the generated init's (own properties before
126
+ // inherited ones — Nitro flattens `extends` own-props-first).
127
+ return SectionStyle(
128
+ gridWrap: gridWrap,
129
+ display: display,
130
+ artworkRendering: artworkRendering,
131
+ )
132
+ }
133
+
134
+ /// Snapshot a live track block back to its JSON model (for persistence).
135
+ /// An all-nil block snapshots as no block — the decoders collapse it to
136
+ /// "no declaration" anyway, so never persist the distinction.
137
+ init?(_ style: TrackStyle?) {
138
+ guard let style, style.display != nil || style.artworkRendering != nil else { return nil }
139
+ self.init(
140
+ display: Self.string(style.display),
141
+ artworkRendering: Self.string(style.artworkRendering),
142
+ )
143
+ }
144
+
145
+ private static func string(_ display: StyleDisplay?) -> String? {
146
+ switch display {
147
+ case .list: "list"
148
+ case .grid: "grid"
149
+ case nil: nil
150
+ }
151
+ }
152
+
153
+ private static func string(_ rendering: ArtworkRendering?) -> String? {
154
+ switch rendering {
155
+ case .original: "original"
156
+ case .stencil: "stencil"
157
+ case nil: nil
158
+ }
159
+ }
160
+ }
161
+
162
+ /// JSON model for a page section (ADR 0010). Legacy `groupTitle`/`imageRow`
163
+ /// keys in payloads are simply unknown to these models and decode as
164
+ /// ignored dead weight.
165
+ struct JsonSection: Codable {
166
+ let title: String?
167
+ let subtitle: String?
168
+ let style: JsonStyle?
169
+ let path: String?
170
+ let children: [JsonTrack]
171
+
172
+ init(
173
+ title: String? = nil,
174
+ subtitle: String? = nil,
175
+ style: JsonStyle? = nil,
176
+ path: String? = nil,
177
+ children: [JsonTrack],
89
178
  ) {
90
- self.id = id
91
- self.url = url
92
- self.src = src
93
- self.artwork = artwork
94
179
  self.title = title
95
- self.artist = artist
96
- self.album = album
97
- self.albumUrl = albumUrl
98
- self.live = live
99
- self.request = request
180
+ self.subtitle = subtitle
181
+ self.style = style
182
+ self.path = path
183
+ self.children = children
100
184
  }
101
185
  }
102
186
 
@@ -116,59 +200,59 @@ struct JsonTrackRequest: Codable {
116
200
  /// JSON model for resolved track (container with children).
117
201
  struct JsonResolvedTrack: Codable {
118
202
  let id: String?
119
- let url: String
203
+ let path: String
120
204
  let title: String
121
205
  let subtitle: String?
122
206
  let artwork: JsonArtwork?
123
207
  let artist: String?
124
- let albumUrl: String?
208
+ let albumPath: String?
125
209
  let album: String?
126
210
  let description: String?
127
211
  let genre: String?
128
212
  let duration: Double?
213
+ let sections: [JsonSection]?
129
214
  let children: [JsonTrack]?
130
215
  let src: String?
131
- let style: String?
132
- let childrenStyle: String?
133
- let groupTitle: String?
216
+ let style: JsonStyle?
217
+ let disabled: Bool?
134
218
  let live: Bool?
135
219
  let carPlaySiriListButton: String?
136
220
 
137
221
  init(
138
222
  id: String? = nil,
139
- url: String,
223
+ path: String,
140
224
  title: String,
141
225
  subtitle: String? = nil,
142
226
  artwork: JsonArtwork? = nil,
143
227
  artist: String? = nil,
144
- albumUrl: String? = nil,
228
+ albumPath: String? = nil,
145
229
  album: String? = nil,
146
230
  description: String? = nil,
147
231
  genre: String? = nil,
148
232
  duration: Double? = nil,
233
+ sections: [JsonSection]? = nil,
149
234
  children: [JsonTrack]? = nil,
150
235
  src: String? = nil,
151
- style: String? = nil,
152
- childrenStyle: String? = nil,
153
- groupTitle: String? = nil,
236
+ style: JsonStyle? = nil,
237
+ disabled: Bool? = nil,
154
238
  live: Bool? = nil,
155
239
  carPlaySiriListButton: String? = nil,
156
240
  ) {
157
- self.url = url
241
+ self.path = path
158
242
  self.title = title
159
243
  self.subtitle = subtitle
160
244
  self.artwork = artwork
161
245
  self.artist = artist
162
- self.albumUrl = albumUrl
246
+ self.albumPath = albumPath
163
247
  self.album = album
164
248
  self.description = description
165
249
  self.genre = genre
166
250
  self.duration = duration
251
+ self.sections = sections
167
252
  self.children = children
168
253
  self.src = src
169
254
  self.style = style
170
- self.childrenStyle = childrenStyle
171
- self.groupTitle = groupTitle
255
+ self.disabled = disabled
172
256
  self.live = live
173
257
  self.carPlaySiriListButton = carPlaySiriListButton
174
258
  self.id = id
@@ -178,50 +262,46 @@ struct JsonResolvedTrack: Codable {
178
262
  /// JSON model for individual tracks.
179
263
  struct JsonTrack: Codable {
180
264
  let id: String?
181
- let url: String?
265
+ let path: String?
182
266
  let title: String
183
267
  let subtitle: String?
184
268
  let artwork: JsonArtwork?
185
269
  let artist: String?
186
- let albumUrl: String?
270
+ let albumPath: String?
187
271
  let album: String?
188
272
  let description: String?
189
273
  let genre: String?
190
274
  let duration: Double?
191
275
  let src: String?
192
276
  let request: JsonTrackRequest?
193
- let style: String?
194
- let childrenStyle: String?
195
- let groupTitle: String?
277
+ let style: JsonStyle?
278
+ let disabled: Bool?
196
279
  let live: Bool?
197
- let imageRow: [JsonImageRowItem]?
198
280
 
199
281
  init(
200
282
  id: String? = nil,
201
- url: String? = nil,
283
+ path: String? = nil,
202
284
  title: String,
203
285
  subtitle: String? = nil,
204
286
  artwork: JsonArtwork? = nil,
205
287
  artist: String? = nil,
206
- albumUrl: String? = nil,
288
+ albumPath: String? = nil,
207
289
  album: String? = nil,
208
290
  description: String? = nil,
209
291
  genre: String? = nil,
210
292
  duration: Double? = nil,
211
293
  src: String? = nil,
212
294
  request: JsonTrackRequest? = nil,
213
- style: String? = nil,
214
- childrenStyle: String? = nil,
215
- groupTitle: String? = nil,
295
+ style: JsonStyle? = nil,
296
+ disabled: Bool? = nil,
216
297
  live: Bool? = nil,
217
- imageRow: [JsonImageRowItem]? = nil,
218
298
  ) {
219
- self.url = url
299
+ self.path = path
220
300
  self.title = title
221
301
  self.subtitle = subtitle
222
302
  self.artwork = artwork
223
303
  self.artist = artist
224
- self.albumUrl = albumUrl
304
+ self.albumPath = albumPath
225
305
  self.album = album
226
306
  self.description = description
227
307
  self.genre = genre
@@ -229,10 +309,8 @@ struct JsonTrack: Codable {
229
309
  self.src = src
230
310
  self.request = request
231
311
  self.style = style
232
- self.childrenStyle = childrenStyle
233
- self.groupTitle = groupTitle
312
+ self.disabled = disabled
234
313
  self.live = live
235
- self.imageRow = imageRow
236
314
  self.id = id
237
315
  }
238
316
  }
@@ -250,26 +328,27 @@ struct JsonTrack: Codable {
250
328
 
251
329
  extension JsonTrack {
252
330
  /// Snapshot the persistable subset of a live Track (inverse of `toNitro()`).
331
+ /// The track's own declared style block and `disabled` persist (ADR 0011);
332
+ /// section/page-inherited values are never stamped onto tracks and
333
+ /// re-resolve at render, exactly as they do live.
253
334
  init(from track: Track) {
254
335
  self.init(
255
336
  id: track.id,
256
- url: track.url,
337
+ path: track.path,
257
338
  title: track.title,
258
339
  subtitle: track.subtitle,
259
340
  artwork: JsonArtwork(track.artwork),
260
341
  artist: track.artist,
261
- albumUrl: track.albumUrl,
342
+ albumPath: track.albumPath,
262
343
  album: track.album,
263
344
  description: track.description,
264
345
  genre: track.genre,
265
346
  duration: track.duration,
266
347
  src: track.src,
267
348
  request: track.request.map(JsonTrackRequest.init(from:)),
268
- style: nil,
269
- childrenStyle: nil,
270
- groupTitle: track.groupTitle,
349
+ style: JsonStyle(track.style),
350
+ disabled: track.disabled,
271
351
  live: track.live,
272
- imageRow: nil,
273
352
  )
274
353
  }
275
354
  }
@@ -289,14 +368,16 @@ struct JsonTrack: Codable {
289
368
  init(from track: Track) {
290
369
  self.init(
291
370
  id: track.id,
292
- url: track.url,
371
+ path: track.path,
293
372
  title: track.title,
294
373
  artwork: JsonArtwork(track.artwork),
295
374
  artist: track.artist,
296
- albumUrl: track.albumUrl,
375
+ albumPath: track.albumPath,
297
376
  album: track.album,
298
377
  src: track.src,
299
378
  request: track.request.map(JsonTrackRequest.init(from:)),
379
+ style: JsonStyle(track.style),
380
+ disabled: track.disabled,
300
381
  live: track.live,
301
382
  )
302
383
  }
@@ -308,26 +389,14 @@ struct JsonTrack: Codable {
308
389
 
309
390
  #if canImport(NitroModules)
310
391
 
311
- private extension String {
312
- func toTrackStyle() -> TrackStyle? {
313
- TrackStyle(fromString: lowercased())
314
- }
315
- }
316
-
317
- extension JsonImageRowItem {
318
- func toNitro() -> ImageRowItem {
319
- ImageRowItem(
320
- id: id,
321
- url: url,
322
- src: src,
323
- artwork: artwork,
324
- artworkSource: nil,
392
+ extension JsonSection {
393
+ func toNitro() -> Section {
394
+ Section(
325
395
  title: title,
326
- artist: artist,
327
- album: album,
328
- albumUrl: albumUrl,
329
- live: live,
330
- request: request?.toNitro(),
396
+ subtitle: subtitle,
397
+ style: style?.toSectionStyle(),
398
+ path: path,
399
+ children: children.map { $0.toNitro() },
331
400
  )
332
401
  }
333
402
  }
@@ -335,28 +404,26 @@ struct JsonTrack: Codable {
335
404
  extension JsonResolvedTrack {
336
405
  func toNitro() -> ResolvedTrack {
337
406
  ResolvedTrack(
338
- url: url,
407
+ path: path,
408
+ style: style?.toSectionStyle(),
409
+ sections: sections?.map { $0.toNitro() },
339
410
  children: children?.map { $0.toNitro() },
340
411
  carPlaySiriListButton: carPlaySiriListButton.flatMap { CarPlaySiriListButtonPosition(fromString: $0) },
341
412
  id: id,
342
413
  src: src,
343
414
  artwork: artwork?.toNitro(),
344
415
  artworkSource: nil, request: nil,
345
- artworkCarPlayTinted: nil,
346
416
  title: title,
347
417
  subtitle: subtitle,
348
418
  artist: artist,
349
- albumUrl: albumUrl,
419
+ albumPath: albumPath,
350
420
  album: album,
351
421
  description: description,
352
422
  genre: genre,
353
423
  duration: duration,
354
- style: style?.toTrackStyle(),
355
- childrenStyle: childrenStyle?.toTrackStyle(),
424
+ disabled: disabled,
356
425
  favorited: nil,
357
- groupTitle: groupTitle,
358
426
  live: live,
359
- imageRow: nil,
360
427
  )
361
428
  }
362
429
  }
@@ -365,26 +432,23 @@ struct JsonTrack: Codable {
365
432
  func toNitro() -> Track {
366
433
  Track(
367
434
  id: id,
368
- url: url,
435
+ path: path,
369
436
  src: src,
370
437
  artwork: artwork?.toNitro(),
371
438
  artworkSource: nil,
372
439
  request: request?.toNitro(),
373
- artworkCarPlayTinted: nil,
374
440
  title: title,
375
441
  subtitle: subtitle,
376
442
  artist: artist,
377
- albumUrl: albumUrl,
443
+ albumPath: albumPath,
378
444
  album: album,
379
445
  description: description,
380
446
  genre: genre,
381
447
  duration: duration,
382
448
  style: style?.toTrackStyle(),
383
- childrenStyle: childrenStyle?.toTrackStyle(),
449
+ disabled: disabled,
384
450
  favorited: nil,
385
- groupTitle: groupTitle,
386
451
  live: live,
387
- imageRow: imageRow?.map { $0.toNitro() },
388
452
  )
389
453
  }
390
454
  }
@@ -396,15 +460,29 @@ struct JsonTrack: Codable {
396
460
  func toNitro() -> Track {
397
461
  Track(
398
462
  id: id ?? "",
399
- url: url,
463
+ path: path,
400
464
  src: src,
465
+ artwork: artwork?.toNitro(),
401
466
  request: request.map { TrackRequest(userAgent: $0.userAgent, headers: $0.headers, query: $0.query) },
402
467
  title: title,
403
468
  artist: artist,
404
- albumUrl: albumUrl,
469
+ albumPath: albumPath,
405
470
  album: album,
471
+ style: style?.toTrackStyle(),
472
+ disabled: disabled,
406
473
  live: live,
407
- artwork: artwork?.toNitro(),
474
+ )
475
+ }
476
+ }
477
+
478
+ extension JsonSection {
479
+ func toNitro() -> Section {
480
+ Section(
481
+ title: title,
482
+ subtitle: subtitle,
483
+ style: style?.toSectionStyle(),
484
+ path: path,
485
+ children: children.map { $0.toNitro() },
408
486
  )
409
487
  }
410
488
  }
@@ -412,10 +490,13 @@ struct JsonTrack: Codable {
412
490
  extension JsonResolvedTrack {
413
491
  func toNitro() -> ResolvedTrack {
414
492
  ResolvedTrack(
415
- url: url,
493
+ path: path,
494
+ style: style?.toSectionStyle(),
495
+ sections: sections?.map { $0.toNitro() },
416
496
  children: children?.map { $0.toNitro() },
417
497
  artwork: artwork?.toNitro(),
418
498
  title: title,
499
+ disabled: disabled,
419
500
  )
420
501
  }
421
502
  }
@@ -1,18 +1,18 @@
1
1
  import Foundation
2
2
 
3
3
  /// A search result that may or may not be directly playable. `src` is the
4
- /// playable stream; `url` is a browsable container path. Abstracting these two
4
+ /// playable stream; `path` is a browsable container path. Abstracting these two
5
5
  /// fields keeps the drill-in decision free of the Nitro `Track` type so it can
6
6
  /// be unit-tested.
7
7
  protocol PlayableSearchItem {
8
8
  var src: String? { get }
9
- var url: String? { get }
9
+ var path: String? { get }
10
10
  }
11
11
 
12
12
  enum SearchDrillIn {
13
13
  /// Picks the playable items for a "play «X»" voice search.
14
14
  ///
15
- /// If the first result is browsable-only (a `url` container with no `src` — a
15
+ /// If the first result is browsable-only (a `path` container with no `src` — a
16
16
  /// place/genre page), drills into it via `resolveChildren` and uses its
17
17
  /// playable children, so we play the first station *inside* the page rather
18
18
  /// than skipping the page for a later inline station. Falls back to the flat
@@ -25,8 +25,8 @@ enum SearchDrillIn {
25
25
  guard !children.isEmpty else { return nil }
26
26
 
27
27
  let candidates: [Item]
28
- if let first = children.first, first.src == nil, let url = first.url {
29
- let drilled = try await resolveChildren(url).filter { $0.src != nil }
28
+ if let first = children.first, first.src == nil, let path = first.path {
29
+ let drilled = try await resolveChildren(path).filter { $0.src != nil }
30
30
  candidates = drilled.isEmpty ? children : drilled
31
31
  } else {
32
32
  candidates = children
@@ -37,7 +37,7 @@ enum SearchDrillIn {
37
37
  }
38
38
  }
39
39
 
40
- // The real Nitro `Track` already carries `src`/`url`; conform it for the app
40
+ // The real Nitro `Track` already carries `src`/`path`; conform it for the app
41
41
  // build. (The testable target builds without NitroModules and uses a stub item.)
42
42
  #if canImport(NitroModules)
43
43
  extension Track: PlayableSearchItem {}
@@ -0,0 +1,34 @@
1
+ #if canImport(NitroModules)
2
+ import NitroModules
3
+ #endif
4
+
5
+ /// The rendered form of a section — the single place where (resolved style,
6
+ /// platform capability) maps to what CarPlay draws. Degradation drops
7
+ /// decorations before layout (ADR 0011): a *wrapping* grid can only reach the
8
+ /// tile path where the platform has a wrapping tile container (CarPlay:
9
+ /// iOS 26+'s element API); without one it becomes a list, where every item
10
+ /// stays reachable instead of truncating at an unknowable width. A
11
+ /// single-line grid (`gridWrap: false`) renders everywhere — the legacy image
12
+ /// row IS the single-line presentation.
13
+ enum SectionPresentation: Equatable {
14
+ case list
15
+ case singleLineRow
16
+ case wrappingGrid
17
+
18
+ /// - Parameter supportsWrappingGrid: whether the platform has a wrapping
19
+ /// tile container. Injected (rather than read via `#available` here) so
20
+ /// the mapping is testable off-device.
21
+ init(for style: SectionStyle?, supportsWrappingGrid: Bool) {
22
+ guard style?.display == .grid else {
23
+ self = .list
24
+ return
25
+ }
26
+ if style?.gridWrap == false {
27
+ self = .singleLineRow
28
+ } else if supportsWrappingGrid {
29
+ self = .wrappingGrid
30
+ } else {
31
+ self = .list
32
+ }
33
+ }
34
+ }
@@ -1,35 +1,69 @@
1
1
  // Queue scope is the tapped section, not the whole page (ADR 0006): the
2
2
  // playback context a listener expects is the list they tapped in — a page
3
3
  // aggregating several sections must not leak next/previous across them.
4
+ // Sections are structural (ADR 0010), so scoping is a lookup, not a
5
+ // groupTitle-run derivation.
4
6
  enum SectionScope {
5
- enum Section {
6
- /// The tapped id lives inside an image row's items.
7
- case imageRow([ImageRowItem])
8
- /// The contiguous `groupTitle` run around the tapped child (items with
9
- /// no group title form runs of their own).
10
- case run([Track])
7
+ /// The section containing the tapped identity, plus the tapped child's
8
+ /// offset within it — non-nil only when the stamped flat index pinned the
9
+ /// exact copy.
10
+ struct Scoped {
11
+ let section: Section
12
+ let tappedOffset: Int?
11
13
  }
12
14
 
13
- /// The section of `children` containing the playable `trackId`, or nil
14
- /// when the id is not found.
15
- static func section(of children: [Track], containing trackId: String) -> Section? {
16
- for child in children {
17
- if let items = child.imageRow, items.contains(where: { $0.src == trackId }) {
18
- return .imageRow(items)
15
+ /// The section of `sections` containing the playable `trackId` (a track
16
+ /// identity: id when non-blank, else src), or nil when not found.
17
+ ///
18
+ /// `tappedIndex` — the flat page position stamped into the contextual URL
19
+ /// (children concatenated in section order) — is a tie-breaker, never an
20
+ /// identifier: when the child at that position still carries the tapped
21
+ /// identity, it pins which section (and which copy) was tapped; when it
22
+ /// doesn't (the list shifted), resolution falls back to the first section
23
+ /// containing the identity. A stale index can therefore never select a
24
+ /// different track — at worst a different copy of the same one.
25
+ static func scoped(
26
+ in sections: [Section],
27
+ containing trackId: String,
28
+ tappedIndex: Int? = nil,
29
+ ) -> Scoped? {
30
+ if let tappedIndex, tappedIndex >= 0 {
31
+ var start = 0
32
+ for section in sections {
33
+ let offset = tappedIndex - start
34
+ if offset < section.children.count {
35
+ if section.children[offset].identity == trackId {
36
+ return Scoped(section: section, tappedOffset: offset)
37
+ }
38
+ break
39
+ }
40
+ start += section.children.count
19
41
  }
20
42
  }
21
- guard let index = children.firstIndex(where: { $0.src == trackId }) else {
22
- return nil
23
- }
24
- let group = children[index].groupTitle
25
- var start = index
26
- while start > 0, children[start - 1].groupTitle == group {
27
- start -= 1
28
- }
29
- var end = index
30
- while end + 1 < children.count, children[end + 1].groupTitle == group {
31
- end += 1
43
+ for section in sections {
44
+ if section.children.contains(where: { $0.identity == trackId }) {
45
+ return Scoped(section: section, tappedOffset: nil)
46
+ }
32
47
  }
33
- return .run(Array(children[start ... end]))
48
+ return nil
49
+ }
50
+ }
51
+
52
+ // MARK: - Page shape helpers (ADR 0010)
53
+
54
+ extension ResolvedTrack {
55
+ /// The canonical sectioned shape: `sections` wins when present; plain
56
+ /// `children` is authoring sugar for one untitled section.
57
+ var normalizedSections: [Section]? {
58
+ if let sections { return sections }
59
+ guard let children else { return nil }
60
+ return [.untitled(children)]
61
+ }
62
+
63
+ /// The page's children concatenated in section order — the flattening
64
+ /// that defines contextual `__index` positions and the flat views (tabs,
65
+ /// search) of a sectioned page.
66
+ var flattenedChildren: [Track]? {
67
+ normalizedSections.map { $0.flatMap(\.children) }
34
68
  }
35
69
  }