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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (416) hide show
  1. package/README.md +14 -4
  2. package/android/build.gradle +20 -1
  3. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +115 -26
  4. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +262 -188
  5. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
  6. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
  7. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +95 -58
  8. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
  9. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
  10. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +19 -7
  11. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  12. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  13. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +1 -1
  14. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
  15. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
  16. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +109 -74
  17. package/android/src/main/java/com/audiobrowser/player/Player.kt +51 -30
  18. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  19. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  20. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
  21. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  22. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
  23. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +52 -53
  24. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +74 -41
  25. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  26. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +229 -50
  27. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  28. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
  29. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
  30. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
  31. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
  32. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
  33. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  34. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  35. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
  36. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  37. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  38. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  39. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  40. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
  41. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  42. package/ios/Browser/BrowserConfig.swift +102 -5
  43. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  44. package/ios/Browser/BrowserManager.swift +229 -180
  45. package/ios/Browser/BrowserPathHelper.swift +44 -44
  46. package/ios/Browser/InertStyleDiagnostic.swift +279 -0
  47. package/ios/Browser/JsonModels.swift +258 -106
  48. package/ios/Browser/SearchDrillIn.swift +6 -6
  49. package/ios/Browser/SectionPresentation.swift +64 -0
  50. package/ios/Browser/SectionScope.swift +58 -24
  51. package/ios/Browser/StyleResolver.swift +58 -0
  52. package/ios/Browser/TabBarEntries.swift +9 -3
  53. package/ios/Browser/TrackIdTemplate.swift +37 -0
  54. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  55. package/ios/CarPlay/CarPlayController.swift +227 -85
  56. package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
  57. package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
  58. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  59. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  60. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  61. package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
  62. package/ios/Extension/Section+Copying.swift +31 -0
  63. package/ios/Extension/Track+Copying.swift +6 -12
  64. package/ios/Extension/Track+Identity.swift +18 -0
  65. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  66. package/ios/Http/HttpClient.swift +8 -2
  67. package/ios/HybridAudioBrowser.swift +166 -45
  68. package/ios/Model/NitroTypeStubs.swift +106 -21
  69. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  70. package/ios/Player/MediaIntentCriteria.swift +13 -5
  71. package/ios/Player/MediaLoader.swift +33 -9
  72. package/ios/Player/NowPlayingUpdater.swift +54 -6
  73. package/ios/Player/PlaybackCoordinator.swift +13 -6
  74. package/ios/Player/PlaybackTimer.swift +8 -2
  75. package/ios/Player/QueueManager.swift +36 -10
  76. package/ios/Player/RetryManager.swift +4 -3
  77. package/ios/Player/ShuffleOrder.swift +23 -0
  78. package/ios/Player/SleepTimerHandling.swift +13 -0
  79. package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
  80. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  81. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  82. package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
  83. package/ios/Tests/JsonModelsTests.swift +150 -1
  84. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  85. package/ios/Tests/QueueManagerTests.swift +72 -6
  86. package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
  87. package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
  88. package/ios/Tests/SearchDrillInTests.swift +16 -16
  89. package/ios/Tests/SectionPresentationTests.swift +90 -0
  90. package/ios/Tests/SectionScopeTests.swift +87 -55
  91. package/ios/Tests/StyleResolverTests.swift +127 -0
  92. package/ios/Tests/Support/TestTypes.swift +7 -0
  93. package/ios/Tests/TabBarEntriesTests.swift +18 -18
  94. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  95. package/ios/Tests/TrackIdentityTests.swift +20 -0
  96. package/ios/Tests/TrackSelectorTests.swift +168 -33
  97. package/ios/TrackPlayer.swift +57 -34
  98. package/ios/TrackSelector.swift +45 -30
  99. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  100. package/ios/Util/NetworkMonitor.swift +43 -26
  101. package/ios/Util/SFSymbolRenderer.swift +24 -2
  102. package/ios/Util/SVGProcessor.swift +9 -2
  103. package/lib/commonjs/AudioBrowser.js +11 -0
  104. package/lib/commonjs/AudioBrowser.js.map +1 -1
  105. package/lib/commonjs/features/browser-config.js +11 -8
  106. package/lib/commonjs/features/browser-config.js.map +1 -1
  107. package/lib/commonjs/features/browser.js +2 -2
  108. package/lib/commonjs/features/favorites.js +13 -10
  109. package/lib/commonjs/features/favorites.js.map +1 -1
  110. package/lib/commonjs/features/playback/controls.js +4 -2
  111. package/lib/commonjs/features/playback/controls.js.map +1 -1
  112. package/lib/commonjs/features/playback/progress.js +2 -1
  113. package/lib/commonjs/features/playback/progress.js.map +1 -1
  114. package/lib/commonjs/features/playback/rate.js +8 -1
  115. package/lib/commonjs/features/playback/rate.js.map +1 -1
  116. package/lib/commonjs/features/playback/state.js +2 -3
  117. package/lib/commonjs/features/playback/state.js.map +1 -1
  118. package/lib/commonjs/features/player/options.js.map +1 -1
  119. package/lib/commonjs/features/player/setup.js +3 -1
  120. package/lib/commonjs/features/player/setup.js.map +1 -1
  121. package/lib/commonjs/features/player/validateOptions.js +2 -0
  122. package/lib/commonjs/features/player/validateOptions.js.map +1 -1
  123. package/lib/commonjs/features/queue/activeTrack.js +14 -3
  124. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  125. package/lib/commonjs/features/queue/queue.js +11 -4
  126. package/lib/commonjs/features/queue/queue.js.map +1 -1
  127. package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
  128. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  129. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  130. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  131. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  132. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  133. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  134. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  135. package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
  136. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  137. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  138. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  139. package/lib/commonjs/web/browser/BrowserManager.js +144 -87
  140. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  141. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  142. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  143. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  144. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  145. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  146. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  147. package/lib/commonjs/web/browser/sections.js +89 -0
  148. package/lib/commonjs/web/browser/sections.js.map +1 -0
  149. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  150. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  151. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  152. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  153. package/lib/module/AudioBrowser.js +1 -0
  154. package/lib/module/AudioBrowser.js.map +1 -1
  155. package/lib/module/features/browser-config.js +11 -8
  156. package/lib/module/features/browser-config.js.map +1 -1
  157. package/lib/module/features/browser.js +2 -2
  158. package/lib/module/features/favorites.js +13 -10
  159. package/lib/module/features/favorites.js.map +1 -1
  160. package/lib/module/features/playback/controls.js +4 -2
  161. package/lib/module/features/playback/controls.js.map +1 -1
  162. package/lib/module/features/playback/progress.js +2 -1
  163. package/lib/module/features/playback/progress.js.map +1 -1
  164. package/lib/module/features/playback/rate.js +8 -1
  165. package/lib/module/features/playback/rate.js.map +1 -1
  166. package/lib/module/features/playback/state.js +2 -3
  167. package/lib/module/features/playback/state.js.map +1 -1
  168. package/lib/module/features/player/options.js.map +1 -1
  169. package/lib/module/features/player/setup.js +3 -1
  170. package/lib/module/features/player/setup.js.map +1 -1
  171. package/lib/module/features/player/validateOptions.js +2 -0
  172. package/lib/module/features/player/validateOptions.js.map +1 -1
  173. package/lib/module/features/queue/activeTrack.js +14 -3
  174. package/lib/module/features/queue/activeTrack.js.map +1 -1
  175. package/lib/module/features/queue/queue.js +11 -4
  176. package/lib/module/features/queue/queue.js.map +1 -1
  177. package/lib/module/utils/LazyNativeEmitter.js +21 -10
  178. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  179. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  180. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  181. package/lib/module/utils/getTrackIdentity.js +20 -0
  182. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  183. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  184. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  185. package/lib/module/web/NativeAudioBrowser.js +32 -24
  186. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  187. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  188. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  189. package/lib/module/web/browser/BrowserManager.js +144 -87
  190. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  191. package/lib/module/web/browser/FavoriteManager.js +31 -21
  192. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  193. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  194. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  195. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  196. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  197. package/lib/module/web/browser/sections.js +83 -0
  198. package/lib/module/web/browser/sections.js.map +1 -0
  199. package/lib/module/web/player/NowPlayingManager.js +4 -3
  200. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  201. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  202. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  203. package/lib/typescript/src/AudioBrowser.d.ts +1 -0
  204. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  205. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  206. package/lib/typescript/src/features/browser.d.ts +2 -2
  207. package/lib/typescript/src/features/favorites.d.ts +13 -10
  208. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  209. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  210. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  211. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  212. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  213. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  214. package/lib/typescript/src/features/playback/state.d.ts +2 -3
  215. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  216. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  217. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  218. package/lib/typescript/src/features/player/options.d.ts +21 -19
  219. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  220. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  221. package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
  222. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
  223. package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
  224. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  225. package/lib/typescript/src/features/queue/queue.d.ts +5 -2
  226. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  227. package/lib/typescript/src/types/browser-native.d.ts +2 -2
  228. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  229. package/lib/typescript/src/types/browser-nodes.d.ts +361 -126
  230. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  231. package/lib/typescript/src/types/browser.d.ts +33 -53
  232. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  233. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
  234. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  235. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  236. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  237. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  238. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  239. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  240. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  241. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
  242. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  243. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
  244. package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
  245. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  246. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  247. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
  248. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
  249. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
  250. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
  251. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
  252. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
  253. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
  254. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
  255. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +16 -4
  256. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
  257. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
  258. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
  259. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
  260. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
  261. package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
  262. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +33 -14
  263. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  264. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  265. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
  266. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +22 -10
  267. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
  268. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +10 -7
  269. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
  270. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  271. package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
  272. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
  273. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  274. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +55 -51
  275. package/nitrogen/generated/android/c++/JSection.hpp +120 -0
  276. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  277. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  278. package/nitrogen/generated/android/c++/JTrack.hpp +23 -48
  279. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
  280. package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
  281. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
  282. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  283. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  284. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  285. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +18 -6
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
  297. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +158 -82
  298. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +25 -10
  299. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +25 -10
  300. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  301. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  302. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  303. package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
  304. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  305. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  306. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
  307. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  308. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  309. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
  310. package/nitrogen/generated/ios/swift/Section.swift +100 -0
  311. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  312. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  313. package/nitrogen/generated/ios/swift/Track.swift +20 -73
  314. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  315. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  316. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  317. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  318. package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
  319. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  320. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  321. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
  322. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  323. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  324. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
  325. package/nitrogen/generated/shared/c++/Section.hpp +106 -0
  326. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  327. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  328. package/nitrogen/generated/shared/c++/Track.hpp +15 -31
  329. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  330. package/package.json +7 -2
  331. package/src/AudioBrowser.ts +1 -0
  332. package/src/features/browser-config.test.ts +21 -6
  333. package/src/features/browser-config.ts +11 -8
  334. package/src/features/browser.test.ts +1 -1
  335. package/src/features/browser.ts +2 -2
  336. package/src/features/favorites.ts +13 -10
  337. package/src/features/hook-wiring.test.ts +63 -15
  338. package/src/features/playback/controls.ts +4 -2
  339. package/src/features/playback/progress.ts +2 -1
  340. package/src/features/playback/rate.ts +8 -1
  341. package/src/features/playback/state.ts +2 -3
  342. package/src/features/playback/trackPlaybackTime.ts +1 -1
  343. package/src/features/player/options.ts +27 -19
  344. package/src/features/player/setup.test.ts +12 -3
  345. package/src/features/player/setup.ts +3 -1
  346. package/src/features/player/validateOptions.ts +2 -0
  347. package/src/features/queue/activeTrack.ts +14 -3
  348. package/src/features/queue/queue.ts +11 -4
  349. package/src/nitro-stub-drift.test.ts +40 -0
  350. package/src/public-surface.test.ts +40 -0
  351. package/src/specs/audio-browser.nitro.ts +4 -3
  352. package/src/style-resolution.test.ts +210 -0
  353. package/src/types/browser-native.ts +2 -2
  354. package/src/types/browser-nodes.ts +377 -135
  355. package/src/types/browser.ts +34 -55
  356. package/src/utils/LazyNativeEmitter.ts +20 -10
  357. package/src/utils/NativeUpdatedValue.ts +29 -13
  358. package/src/utils/getTrackIdentity.ts +20 -0
  359. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  360. package/src/web/NativeAudioBrowser.ts +37 -22
  361. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  362. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  363. package/src/web/browser/BrowserManager.ts +176 -95
  364. package/src/web/browser/FavoriteManager.test.ts +84 -0
  365. package/src/web/browser/FavoriteManager.ts +28 -23
  366. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  367. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  368. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  369. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  370. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  371. package/src/web/browser/sections.test.ts +91 -0
  372. package/src/web/browser/sections.ts +72 -0
  373. package/src/web/player/NowPlayingManager.ts +4 -3
  374. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  375. package/src/web/util/BrowserPathHelper.ts +61 -13
  376. package/website/guide/android-auto.md +114 -0
  377. package/website/guide/android-certificates.md +409 -0
  378. package/website/guide/artwork.md +255 -0
  379. package/website/guide/audio-output.md +135 -0
  380. package/website/guide/automotive.md +86 -0
  381. package/website/guide/basic-usage.md +151 -0
  382. package/website/guide/battery.md +115 -0
  383. package/website/guide/browser.md +581 -0
  384. package/website/guide/carplay.md +620 -0
  385. package/website/guide/configuration.md +173 -0
  386. package/website/guide/equalizer.md +231 -0
  387. package/website/guide/errors.md +310 -0
  388. package/website/guide/favorites.md +154 -0
  389. package/website/guide/gate.md +143 -0
  390. package/website/guide/getting-started.md +46 -0
  391. package/website/guide/hooks.md +238 -0
  392. package/website/guide/live-streams.md +125 -0
  393. package/website/guide/metadata.md +239 -0
  394. package/website/guide/migrating-from-track-player.md +244 -0
  395. package/website/guide/native-callback-fetch.md +127 -0
  396. package/website/guide/network.md +86 -0
  397. package/website/guide/now-playing.md +175 -0
  398. package/website/guide/playback.md +328 -0
  399. package/website/guide/queue.md +300 -0
  400. package/website/guide/remote-controls.md +256 -0
  401. package/website/guide/search.md +255 -0
  402. package/website/guide/sleep-timer.md +268 -0
  403. package/website/guide/track.md +246 -0
  404. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  405. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  406. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  407. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  408. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  409. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  410. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  411. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  412. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  413. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  414. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  415. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  416. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
@@ -120,6 +120,41 @@ struct NextTests {
120
120
  #expect(q.shuffleOrder.shuffled == order)
121
121
  }
122
122
 
123
+ /// A queue reorder permutes positions, not identities, so it must leave the
124
+ /// shuffled playback order alone. Re-inserting at a random shuffle position
125
+ /// would silently reshuffle under the listener.
126
+ @Test func shuffle_moveKeepsThePlaybackOrder() {
127
+ for target in 0 ..< 5 {
128
+ let q = QueueManager()
129
+ q.setQueue(tracks("a", "b", "c", "d", "e"))
130
+ q.shuffleEnabled = true
131
+ let before = q.shuffleOrder.shuffled.map { q.tracks[$0].id }
132
+
133
+ _ = try? q.move(fromIndex: 0, toIndex: target)
134
+
135
+ #expect(q.shuffleOrder.shuffled.map { q.tracks[$0].id } == before)
136
+ }
137
+ }
138
+
139
+ /// The same hazard as `shuffle_startTrackLeadsTheOrder`, reached by moving the
140
+ /// *playing* track: a random re-insert can drop it last in the shuffle order,
141
+ /// which reads as `isLastInPlaybackOrder` and ends the queue at the next track
142
+ /// boundary with tracks still unplayed.
143
+ @Test func shuffle_movingCurrentTrackDoesNotEndTheQueue() {
144
+ for target in 1 ..< 6 {
145
+ let q = QueueManager()
146
+ q.setQueue(tracks("a", "b", "c", "d", "e", "f"), initialIndex: 0)
147
+ q.shuffleEnabled = true // pins "a" first in the order
148
+ #expect(!q.isLastInPlaybackOrder)
149
+
150
+ _ = try? q.move(fromIndex: 0, toIndex: target)
151
+
152
+ #expect(q.currentTrack?.id == "a")
153
+ #expect(!q.isLastInPlaybackOrder)
154
+ #expect(q.nextTracks.count == 5)
155
+ }
156
+ }
157
+
123
158
  @Test func shuffle_visitsAllTracks() {
124
159
  let q = QueueManager()
125
160
  q.setQueue(tracks("a", "b", "c", "d"))
@@ -382,6 +417,25 @@ struct AddTests {
382
417
  #expect(q.tracks[2].id == "c")
383
418
  #expect(q.tracks[3].id == "d")
384
419
  }
420
+
421
+ /// Filling an empty queue must pin the start track to the head of the shuffle
422
+ /// order, exactly as `setQueue` does — `insert(at:count:)` places new indices
423
+ /// at random positions, so without the pin the start track can land last and
424
+ /// the queue reports end-of-queue as soon as it finishes. `shuffleEnabled`'s
425
+ /// didSet only re-pins on the off→on edge, so shuffle-already-on gets nothing
426
+ /// from it.
427
+ @Test func toEmptyQueue_withShuffleAlreadyOn_startTrackLeadsTheOrder() {
428
+ for start in 0 ..< 6 {
429
+ let q = QueueManager()
430
+ q.shuffleEnabled = true
431
+ q.add(tracks("a", "b", "c", "d", "e", "f"), initialIndex: start)
432
+
433
+ #expect(q.shuffleOrder.isFirst(start))
434
+ #expect(!q.isLastInPlaybackOrder)
435
+ #expect(q.nextTracks.count == 5)
436
+ #expect(q.previousTracks.isEmpty)
437
+ }
438
+ }
385
439
  }
386
440
 
387
441
  // MARK: - Mutations: addAt()
@@ -414,6 +468,18 @@ struct AddAtTests {
414
468
  #expect(q.currentIndex == 0)
415
469
  }
416
470
 
471
+ /// Same pinning requirement as `add()` into an empty queue — see its twin.
472
+ @Test func toEmptyQueue_withShuffleAlreadyOn_startTrackLeadsTheOrder() throws {
473
+ let q = QueueManager()
474
+ q.shuffleEnabled = true
475
+ try q.addAt(tracks("a", "b", "c", "d", "e", "f"), at: 0)
476
+
477
+ #expect(q.shuffleOrder.isFirst(0))
478
+ #expect(!q.isLastInPlaybackOrder)
479
+ #expect(q.nextTracks.count == 5)
480
+ #expect(q.previousTracks.isEmpty)
481
+ }
482
+
417
483
  @Test func invalidIndex_throws() {
418
484
  let q = QueueManager()
419
485
  q.setQueue(tracks("a"))
@@ -691,10 +757,10 @@ struct ClearTests {
691
757
  @Suite("replace & removeUpcomingTracks")
692
758
  @MainActor
693
759
  struct OtherMutationTests {
694
- @Test func replace_updatesTrackAtIndex() {
760
+ @Test func replace_updatesTrackAtIndex() throws {
695
761
  let q = QueueManager()
696
762
  q.setQueue(tracks("a", "b", "c"))
697
- q.replace(1, Track(id: "B"))
763
+ try q.replace(1, Track(id: "B"))
698
764
  #expect(q.tracks[1].id == "B")
699
765
  }
700
766
 
@@ -738,10 +804,10 @@ struct OtherMutationTests {
738
804
  #expect(q.tracks[0].id == "a")
739
805
  }
740
806
 
741
- @Test func replace_currentTrack_reflectsInCurrentTrack() {
807
+ @Test func replace_currentTrack_reflectsInCurrentTrack() throws {
742
808
  let q = QueueManager()
743
809
  q.setQueue(tracks("a", "b", "c"), initialIndex: 1)
744
- q.replace(1, Track(id: "B"))
810
+ try q.replace(1, Track(id: "B"))
745
811
  #expect(q.currentTrack?.id == "B")
746
812
  #expect(q.currentIndex == 1)
747
813
  }
@@ -926,12 +992,12 @@ struct DelegateTests {
926
992
  #expect(spy.calls.count == 1)
927
993
  }
928
994
 
929
- @Test func queueDidChangeTracks_firesOnReplace() {
995
+ @Test func queueDidChangeTracks_firesOnReplace() throws {
930
996
  let q = QueueManager()
931
997
  q.setQueue(tracks("a", "b"))
932
998
  let spy = DelegateSpy()
933
999
  q.delegate = spy
934
- q.replace(0, Track(id: "A"))
1000
+ try q.replace(0, Track(id: "A"))
935
1001
  #expect(spy.calls.count == 1)
936
1002
  }
937
1003
 
@@ -136,6 +136,22 @@ struct RemoteCommandControllerTests {
136
136
  #expect(center.previousTrackCommand.isEnabled)
137
137
  }
138
138
 
139
+ /// The handler properties must not capture the controller strongly. Assigning a
140
+ /// bare method reference (`= handlePlayCommandDefault`) curries to
141
+ /// `{ [self] in self.method($0) }`, and storing that back on self is a permanent
142
+ /// cycle — one leaked controller per player instance, across every JS reload.
143
+ @Test func doesNotRetainItselfThroughHandlers() {
144
+ weak var weakController: RemoteCommandController?
145
+ autoreleasepool {
146
+ let (controller, _) = makeController()
147
+ // Forces every lazy handler to initialize, which is what closes the cycle.
148
+ controller.enable(commands: RemoteCommand.all())
149
+ controller.disableAll()
150
+ weakController = controller
151
+ }
152
+ #expect(weakController == nil)
153
+ }
154
+
139
155
  @Test func shuffleAndRepeatStateSurviveCommandCenterSwitch() {
140
156
  let (controller, _) = makeController()
141
157
  let center = MPRemoteCommandCenter.shared()
@@ -3,17 +3,18 @@ import Testing
3
3
  @testable import AudioBrowserTestable
4
4
 
5
5
  private func makeResolvedTrack(
6
- url: String = "/test",
6
+ path: String = "/test",
7
7
  title: String = "Test Track",
8
8
  ) -> ResolvedTrack {
9
9
  ResolvedTrack(
10
- url: url,
10
+ path: path,
11
+ style: nil,
12
+ sections: nil,
11
13
  children: nil,
12
14
  carPlaySiriListButton: nil,
13
15
  src: nil,
14
16
  artwork: nil,
15
17
  artworkSource: nil,
16
- artworkCarPlayTinted: nil,
17
18
  title: title,
18
19
  subtitle: nil,
19
20
  artist: nil,
@@ -21,25 +22,23 @@ private func makeResolvedTrack(
21
22
  description: nil,
22
23
  genre: nil,
23
24
  duration: nil,
24
- style: nil,
25
- childrenStyle: nil,
25
+ disabled: nil,
26
26
  favorited: nil,
27
- groupTitle: nil,
28
27
  live: nil,
29
- imageRow: nil,
30
28
  )
31
29
  }
32
30
 
33
31
  private func makeFullResolvedTrack() -> ResolvedTrack {
34
32
  ResolvedTrack(
35
- url: "/original",
36
- children: [Track(id: "t1", url: "/t1")],
33
+ path: "/original",
34
+ style: SectionStyle(gridWrap: false, display: .grid, artworkRendering: .stencil),
35
+ sections: [Section(title: "Group A", subtitle: nil, style: nil, path: nil, children: [])],
36
+ children: [Track(id: "t1", path: "/t1")],
37
37
  carPlaySiriListButton: .top,
38
38
  id: "original-id",
39
39
  src: "src.mp3",
40
40
  artwork: .first("art.jpg"),
41
41
  artworkSource: ImageSource(uri: "resolved-art.jpg"),
42
- artworkCarPlayTinted: true,
43
42
  title: "Original Title",
44
43
  subtitle: "Original Subtitle",
45
44
  artist: "Original Artist",
@@ -47,12 +46,9 @@ private func makeFullResolvedTrack() -> ResolvedTrack {
47
46
  description: "Original Description",
48
47
  genre: "Rock",
49
48
  duration: 180.0,
50
- style: .list,
51
- childrenStyle: .grid,
49
+ disabled: false,
52
50
  favorited: true,
53
- groupTitle: "Group A",
54
51
  live: false,
55
- imageRow: [ImageRowItem(title: "Row 1")],
56
52
  )
57
53
  }
58
54
 
@@ -64,12 +60,12 @@ private func makeFullResolvedTrack() -> ResolvedTrack {
64
60
  #expect(copy == original)
65
61
  }
66
62
 
67
- // MARK: - non-optional fields (url, title)
63
+ // MARK: - non-optional fields (path, title)
68
64
 
69
- @Test func copyingOverridesUrl() {
70
- let original = makeResolvedTrack(url: "/old")
71
- let copy = original.copying(url: "/new")
72
- #expect(copy.url == "/new")
65
+ @Test func copyingOverridesPath() {
66
+ let original = makeResolvedTrack(path: "/old")
67
+ let copy = original.copying(path: "/new")
68
+ #expect(copy.path == "/new")
73
69
  #expect(copy.title == "Test Track")
74
70
  }
75
71
 
@@ -77,14 +73,14 @@ private func makeFullResolvedTrack() -> ResolvedTrack {
77
73
  let original = makeResolvedTrack(title: "Old")
78
74
  let copy = original.copying(title: "New")
79
75
  #expect(copy.title == "New")
80
- #expect(copy.url == "/test")
76
+ #expect(copy.path == "/test")
81
77
  }
82
78
 
83
79
  // MARK: - optional fields: set value
84
80
 
85
81
  @Test func copyingOverridesChildren() {
86
82
  let original = makeResolvedTrack()
87
- let children = [Track(id: "c1", url: "/c1"), Track(id: "c2", url: "/c2")]
83
+ let children = [Track(id: "c1", path: "/c1"), Track(id: "c2", path: "/c2")]
88
84
  let copy = original.copying(children: children)
89
85
  #expect(copy.children?.count == 2)
90
86
  #expect(copy.children?[0].id == "c1")
@@ -135,8 +131,14 @@ private func makeFullResolvedTrack() -> ResolvedTrack {
135
131
 
136
132
  @Test func copyingOverridesStyle() {
137
133
  let original = makeResolvedTrack()
138
- let copy = original.copying(style: .grid)
139
- #expect(copy.style == .grid)
134
+ let copy = original.copying(style: SectionStyle(gridWrap: nil, display: .grid, artworkRendering: nil))
135
+ #expect(copy.style?.display == .grid)
136
+ }
137
+
138
+ @Test func copyingOverridesDisabled() {
139
+ let original = makeResolvedTrack()
140
+ let copy = original.copying(disabled: true)
141
+ #expect(copy.disabled == true)
140
142
  }
141
143
 
142
144
  @Test func copyingOverridesFavorited() {
@@ -157,11 +159,13 @@ private func makeFullResolvedTrack() -> ResolvedTrack {
157
159
  #expect(copy.carPlaySiriListButton == .bottom)
158
160
  }
159
161
 
160
- @Test func copyingOverridesImageRow() {
162
+ @Test func copyingOverridesSections() {
161
163
  let original = makeResolvedTrack()
162
- let items = [ImageRowItem(title: "Item 1"), ImageRowItem(title: "Item 2")]
163
- let copy = original.copying(imageRow: items)
164
- #expect(copy.imageRow == items)
164
+ let sections = [
165
+ Section(title: "A", subtitle: nil, style: nil, path: nil, children: [Track(id: "c1", src: "c1")]),
166
+ ]
167
+ let copy = original.copying(sections: sections)
168
+ #expect(copy.sections == sections)
165
169
  }
166
170
 
167
171
  // MARK: - optional fields: set to nil via .some(nil)
@@ -206,13 +210,13 @@ private func makeFullResolvedTrack() -> ResolvedTrack {
206
210
  @Test func copyingOverridesMultipleFields() {
207
211
  let original = makeResolvedTrack()
208
212
  let copy = original.copying(
209
- url: "/updated",
213
+ path: "/updated",
210
214
  title: "Updated Title",
211
215
  artist: "New Artist",
212
216
  duration: 240.0,
213
217
  favorited: true,
214
218
  )
215
- #expect(copy.url == "/updated")
219
+ #expect(copy.path == "/updated")
216
220
  #expect(copy.title == "Updated Title")
217
221
  #expect(copy.artist == "New Artist")
218
222
  #expect(copy.duration == 240.0)
@@ -230,23 +234,21 @@ private func makeFullResolvedTrack() -> ResolvedTrack {
230
234
  let copy = original.copying(title: "Changed Only Title")
231
235
  #expect(copy.title == "Changed Only Title")
232
236
  // All other fields unchanged
233
- #expect(copy.url == "/original")
237
+ #expect(copy.path == "/original")
234
238
  #expect(copy.children?.count == 1)
235
239
  #expect(copy.carPlaySiriListButton == .top)
236
240
  #expect(copy.src == "src.mp3")
237
241
  #expect(copy.artwork?.url == "art.jpg")
238
242
  #expect(copy.artworkSource == ImageSource(uri: "resolved-art.jpg"))
239
- #expect(copy.artworkCarPlayTinted == true)
240
243
  #expect(copy.subtitle == "Original Subtitle")
241
244
  #expect(copy.artist == "Original Artist")
242
245
  #expect(copy.album == "Original Album")
243
246
  #expect(copy.description == "Original Description")
244
247
  #expect(copy.genre == "Rock")
245
248
  #expect(copy.duration == 180.0)
246
- #expect(copy.style == .list)
247
- #expect(copy.childrenStyle == .grid)
249
+ #expect(copy.style == SectionStyle(gridWrap: false, display: .grid, artworkRendering: .stencil))
250
+ #expect(copy.disabled == false)
248
251
  #expect(copy.favorited == true)
249
- #expect(copy.groupTitle == "Group A")
252
+ #expect(copy.sections?.first?.title == "Group A")
250
253
  #expect(copy.live == false)
251
- #expect(copy.imageRow == [ImageRowItem(title: "Row 1")])
252
254
  }
@@ -4,14 +4,14 @@ import Testing
4
4
 
5
5
  private struct Item: PlayableSearchItem {
6
6
  let src: String?
7
- let url: String?
7
+ let path: String?
8
8
  }
9
9
 
10
10
  @Suite("SearchDrillIn.playable")
11
11
  struct SearchDrillInTests {
12
12
  /// A resolver that fails the test if the drill-in path calls it.
13
- private func unusedResolver(_ url: String) async throws -> [Item] {
14
- Issue.record("resolveChildren should not have been called (url: \(url))")
13
+ private func unusedResolver(_ path: String) async throws -> [Item] {
14
+ Issue.record("resolveChildren should not have been called (path: \(path))")
15
15
  return []
16
16
  }
17
17
 
@@ -22,9 +22,9 @@ struct SearchDrillInTests {
22
22
 
23
23
  @Test func firstPlayable_returnsFlatPlayable_inOrder_withoutResolving() async throws {
24
24
  let items = [
25
- Item(src: "/a.mp3", url: nil),
26
- Item(src: nil, url: "/page/x"), // browsable, filtered out
27
- Item(src: "/b.mp3", url: nil),
25
+ Item(src: "/a.mp3", path: nil),
26
+ Item(src: nil, path: "/page/x"), // browsable, filtered out
27
+ Item(src: "/b.mp3", path: nil),
28
28
  ]
29
29
  let result = try await SearchDrillIn.playable(from: items, resolveChildren: unusedResolver)
30
30
  #expect(result?.compactMap(\.src) == ["/a.mp3", "/b.mp3"])
@@ -32,31 +32,31 @@ struct SearchDrillInTests {
32
32
 
33
33
  @Test func firstBrowsable_drillsIntoIt_andPlaysItsChildren() async throws {
34
34
  let items = [
35
- Item(src: nil, url: "/page/place"), // browsable first result
36
- Item(src: "/later.mp3", url: nil), // would win without drill-in
35
+ Item(src: nil, path: "/page/place"), // browsable first result
36
+ Item(src: "/later.mp3", path: nil), // would win without drill-in
37
37
  ]
38
- let result = try await SearchDrillIn.playable(from: items) { url in
39
- #expect(url == "/page/place")
40
- return [Item(src: "/station1.mp3", url: nil), Item(src: "/station2.mp3", url: nil)]
38
+ let result = try await SearchDrillIn.playable(from: items) { path in
39
+ #expect(path == "/page/place")
40
+ return [Item(src: "/station1.mp3", path: nil), Item(src: "/station2.mp3", path: nil)]
41
41
  }
42
42
  #expect(result?.compactMap(\.src) == ["/station1.mp3", "/station2.mp3"])
43
43
  }
44
44
 
45
45
  @Test func browsableDrillYieldsNothing_fallsBackToFlat() async throws {
46
46
  let items = [
47
- Item(src: nil, url: "/page/empty"),
48
- Item(src: "/fallback.mp3", url: nil),
47
+ Item(src: nil, path: "/page/empty"),
48
+ Item(src: "/fallback.mp3", path: nil),
49
49
  ]
50
50
  let result = try await SearchDrillIn.playable(from: items) { _ in
51
- [Item(src: nil, url: "/page/nested")] // nothing playable inside
51
+ [Item(src: nil, path: "/page/nested")] // nothing playable inside
52
52
  }
53
53
  #expect(result?.compactMap(\.src) == ["/fallback.mp3"])
54
54
  }
55
55
 
56
56
  @Test func nothingPlayableAnywhere_returnsNil() async throws {
57
- let items = [Item(src: nil, url: "/page/a"), Item(src: nil, url: "/page/b")]
57
+ let items = [Item(src: nil, path: "/page/a"), Item(src: nil, path: "/page/b")]
58
58
  let result = try await SearchDrillIn.playable(from: items) { _ in
59
- [Item(src: nil, url: "/page/nested")]
59
+ [Item(src: nil, path: "/page/nested")]
60
60
  }
61
61
  #expect(result == nil)
62
62
  }
@@ -0,0 +1,90 @@
1
+ import Testing
2
+
3
+ @testable import AudioBrowserTestable
4
+
5
+ // The (resolved style, platform capability) → rendered-form mapping (ADR
6
+ // 0011): degradation drops decorations before layout, and the one layout drop
7
+ // is a wrapping grid on a platform without a wrapping tile container.
8
+
9
+ @Suite("SectionPresentation")
10
+ struct SectionPresentationTests {
11
+ private func style(display: StyleDisplay?, gridWrap: Bool? = nil) -> SectionStyle {
12
+ SectionStyle(gridWrap: gridWrap, display: display, artworkRendering: nil)
13
+ }
14
+
15
+ @Test func noStyleRendersAList() {
16
+ #expect(SectionPresentation(for: nil, supportsWrappingGrid: true) == .list)
17
+ #expect(SectionPresentation(for: nil, supportsWrappingGrid: false) == .list)
18
+ }
19
+
20
+ @Test func listDisplayRendersAList() {
21
+ #expect(SectionPresentation(for: style(display: .list), supportsWrappingGrid: true) == .list)
22
+ }
23
+
24
+ @Test func wrappingGridNeedsTheWrappingContainer() {
25
+ #expect(
26
+ SectionPresentation(for: style(display: .grid), supportsWrappingGrid: true) == .wrappingGrid)
27
+ // The one layout drop: a wrapping grid has nowhere to go but a list —
28
+ // every item stays reachable instead of truncating at an unknowable width.
29
+ #expect(SectionPresentation(for: style(display: .grid), supportsWrappingGrid: false) == .list)
30
+ }
31
+
32
+ @Test func singleLineGridRendersOnEveryOS() {
33
+ // gridWrap: false is the legacy image row's own shape, so it never
34
+ // degrades — declaring it INCREASES pre-26 fidelity.
35
+ #expect(
36
+ SectionPresentation(for: style(display: .grid, gridWrap: false), supportsWrappingGrid: true)
37
+ == .singleLineRow)
38
+ #expect(
39
+ SectionPresentation(for: style(display: .grid, gridWrap: false), supportsWrappingGrid: false)
40
+ == .singleLineRow)
41
+ }
42
+
43
+ @Test func explicitWrapTrueBehavesLikeTheDefault() {
44
+ #expect(
45
+ SectionPresentation(for: style(display: .grid, gridWrap: true), supportsWrappingGrid: true)
46
+ == .wrappingGrid)
47
+ #expect(
48
+ SectionPresentation(for: style(display: .grid, gridWrap: true), supportsWrappingGrid: false)
49
+ == .list)
50
+ }
51
+
52
+ @Test func gridWrapIsInertOnLists() {
53
+ // Declaring a decoration never changes the layout you'd get without it.
54
+ #expect(
55
+ SectionPresentation(for: style(display: .list, gridWrap: false), supportsWrappingGrid: true)
56
+ == .list)
57
+ #expect(
58
+ SectionPresentation(for: style(display: nil, gridWrap: false), supportsWrappingGrid: true)
59
+ == .list)
60
+ }
61
+
62
+ @Test func tileFamilyFollowsGridTileWithThePlainSplit() {
63
+ // Card/condensed follow the declaration in either wrap mode; the plain
64
+ // default splits on the wrap: single-line keeps row elements (the
65
+ // subtitle slot), wrapping renders the image grid.
66
+ #expect(
67
+ SectionPresentation.tileFamily(for: SectionStyle(gridTile: .card), singleLine: true)
68
+ == .cardElements)
69
+ #expect(
70
+ SectionPresentation.tileFamily(for: SectionStyle(gridTile: .condensed), singleLine: false)
71
+ == .condensedElements)
72
+ #expect(SectionPresentation.tileFamily(for: nil, singleLine: true) == .rowElements)
73
+ #expect(SectionPresentation.tileFamily(for: nil, singleLine: false) == .imageGridElements)
74
+ #expect(
75
+ SectionPresentation.tileFamily(for: SectionStyle(gridTile: .plain), singleLine: true)
76
+ == .rowElements)
77
+ }
78
+
79
+ @Test func effectiveAccessorySymbol_noneAndAbsenceDrawNothing() {
80
+ // A declared symbol draws; 'none' (the inheritance escape) and absence
81
+ // both fall back to the derived accessory.
82
+ #expect(
83
+ SectionPresentation.effectiveAccessorySymbol(TrackStyle(accessorySymbol: "lock.fill"))
84
+ == "lock.fill")
85
+ #expect(
86
+ SectionPresentation.effectiveAccessorySymbol(TrackStyle(accessorySymbol: "none")) == nil)
87
+ #expect(SectionPresentation.effectiveAccessorySymbol(TrackStyle()) == nil)
88
+ #expect(SectionPresentation.effectiveAccessorySymbol(nil) == nil)
89
+ }
90
+ }
@@ -2,81 +2,113 @@
2
2
  import Testing
3
3
 
4
4
  struct SectionScopeTests {
5
- private func track(_ src: String, group: String? = nil) -> Track {
6
- Track(id: src, src: src, groupTitle: group)
5
+ private func track(_ src: String, id: String? = nil) -> Track {
6
+ Track(id: id ?? src, src: src)
7
7
  }
8
8
 
9
- private func runSrcs(_ section: SectionScope.Section?) -> [String?]? {
10
- guard case let .run(tracks) = section else { return nil }
11
- return tracks.map(\.src)
9
+ private func section(_ title: String? = nil, _ tracks: [Track]) -> Section {
10
+ Section(title: title, children: tracks)
12
11
  }
13
12
 
14
- private func imageRowSrcs(_ section: SectionScope.Section?) -> [String?]? {
15
- guard case let .imageRow(items) = section else { return nil }
16
- return items.map(\.src)
13
+ private func srcs(_ scoped: SectionScope.Scoped?) -> [String?]? {
14
+ scoped.map { $0.section.children.map(\.src) }
17
15
  }
18
16
 
19
- @Test func scopesToTheContiguousGroupTitleRun() {
20
- let children = [
21
- track("a", group: "First"),
22
- track("b", group: "First"),
23
- track("c", group: "Second"),
24
- track("d", group: "Second"),
25
- track("e"),
17
+ // Two sections sharing an identity — the duplicate-heavy shape the flat
18
+ // model had to disambiguate by precedence; sections + the stamped flat
19
+ // index make it exact.
20
+ private var sections: [Section] {
21
+ [
22
+ section("First", [track("dup"), track("x")]),
23
+ section("Second", [track("y"), track("dup"), track("z")]),
26
24
  ]
27
- #expect(runSrcs(SectionScope.section(of: children, containing: "c")) == ["c", "d"])
28
- #expect(runSrcs(SectionScope.section(of: children, containing: "a")) == ["a", "b"])
29
25
  }
30
26
 
31
- @Test func ungroupedItemsFormTheirOwnRun() {
32
- let children = [
33
- track("a", group: "First"),
34
- track("b"),
35
- track("c"),
36
- track("d", group: "Second"),
37
- ]
38
- #expect(runSrcs(SectionScope.section(of: children, containing: "b")) == ["b", "c"])
27
+ @Test func fallsBackToTheFirstSectionContainingTheIdentity() {
28
+ let scoped = SectionScope.scoped(in: sections, containing: "dup")
29
+ #expect(srcs(scoped) == ["dup", "x"])
30
+ #expect(scoped?.tappedOffset == nil)
39
31
  }
40
32
 
41
- @Test func identicalTitlesInSeparateRunsStaySeparate() {
42
- let children = [
43
- track("a", group: "Same"),
44
- track("x", group: "Other"),
45
- track("b", group: "Same"),
46
- ]
47
- #expect(runSrcs(SectionScope.section(of: children, containing: "a")) == ["a"])
33
+ @Test func flatIndexPinsTheTappedSection() {
34
+ // Flat index 3 = second section, offset 1.
35
+ let scoped = SectionScope.scoped(in: sections, containing: "dup", tappedIndex: 3)
36
+ #expect(srcs(scoped) == ["y", "dup", "z"])
37
+ #expect(scoped?.tappedOffset == 1)
48
38
  }
49
39
 
50
- @Test func findsTheIdInsideAnImageRow() {
51
- let items = [
52
- ImageRowItem(src: "s1", title: "One"),
53
- ImageRowItem(src: "s2", title: "Two"),
54
- ]
55
- var row = Track(id: "row", title: "Most Played")
56
- row.imageRow = items
57
- let children = [row, track("a")]
58
- #expect(imageRowSrcs(SectionScope.section(of: children, containing: "s2")) == ["s1", "s2"])
40
+ @Test func flatIndexPinsTheExactCopyWithinASection() {
41
+ let playlist = [section(nil, [track("a"), track("b"), track("a"), track("c")])]
42
+ let scoped = SectionScope.scoped(in: playlist, containing: "a", tappedIndex: 2)
43
+ #expect(scoped?.tappedOffset == 2)
44
+ }
45
+
46
+ @Test func staleIndexFallsBackToTheFirstIdentityMatch() {
47
+ // The child at the stamped index no longer carries the tapped identity
48
+ // (the list shifted) — resolution ignores the index and pins nothing.
49
+ let scoped = SectionScope.scoped(in: sections, containing: "dup", tappedIndex: 1)
50
+ #expect(srcs(scoped) == ["dup", "x"])
51
+ #expect(scoped?.tappedOffset == nil)
59
52
  }
60
53
 
61
- @Test func unknownIdReturnsNil() {
62
- #expect(SectionScope.section(of: [track("a")], containing: "zz") == nil)
54
+ @Test func outOfRangeIndexIsIgnored() {
55
+ let scoped = SectionScope.scoped(in: sections, containing: "dup", tappedIndex: 99)
56
+ #expect(srcs(scoped) == ["dup", "x"])
57
+ #expect(scoped?.tappedOffset == nil)
63
58
  }
64
59
 
65
- // Pins the documented precedence: sections are located by src, and an id
66
- // present in both an image row and the flat list resolves to the row.
67
- @Test func imageRowWinsOverAFlatListDuplicate() {
68
- var row = Track(id: "row", title: "Row")
69
- row.imageRow = [ImageRowItem(src: "dup", title: "Dup")]
70
- let children = [row, track("dup"), track("b")]
71
- #expect(imageRowSrcs(SectionScope.section(of: children, containing: "dup")) == ["dup"])
60
+ @Test func vanishedIdentityReturnsNil() {
61
+ #expect(SectionScope.scoped(in: sections, containing: "gone") == nil)
72
62
  }
73
63
 
74
- @Test func duplicateSrcAcrossRunsResolvesToTheEarlierRun() {
64
+ // MARK: - Identity (id when non-blank, else src)
65
+
66
+ @Test func matchesChildByIdWhenSrcDiffers() {
75
67
  let children = [
76
- track("dup", group: "First"),
77
- track("x", group: "Second"),
78
- track("dup", group: "Second"),
68
+ Track(id: "stable-a", src: "https://cdn.example/a?token=1"),
69
+ Track(id: "stable-b", src: "https://cdn.example/b?token=2"),
79
70
  ]
80
- #expect(runSrcs(SectionScope.section(of: children, containing: "dup")) == ["dup"])
71
+ let scoped = SectionScope.scoped(in: [section(nil, children)], containing: "stable-b")
72
+ #expect(srcs(scoped) == ["https://cdn.example/a?token=1", "https://cdn.example/b?token=2"])
73
+ }
74
+
75
+ @Test func blankIdFallsBackToSrc() {
76
+ let children = [Track(id: "", src: "s1"), Track(id: "", src: "s2")]
77
+ let scoped = SectionScope.scoped(in: [section(nil, children)], containing: "s2")
78
+ #expect(srcs(scoped) == ["s1", "s2"])
79
+ }
80
+
81
+ // MARK: - Page shape helpers
82
+
83
+ @Test func childrenSugarNormalizesToOneUntitledSection() {
84
+ let page = ResolvedTrack(path: "/p", children: [track("a"), track("b")], title: "P")
85
+ let sections = page.normalizedSections
86
+ #expect(sections?.count == 1)
87
+ #expect(sections?.first?.title == nil)
88
+ #expect(sections?.first?.children.map(\.src) == ["a", "b"])
89
+ }
90
+
91
+ @Test func sectionsWinOverChildren() {
92
+ let page = ResolvedTrack(
93
+ path: "/p",
94
+ sections: [section("S", [track("a")])],
95
+ children: [track("zzz")],
96
+ title: "P",
97
+ )
98
+ #expect(page.normalizedSections?.count == 1)
99
+ #expect(page.normalizedSections?.first?.title == "S")
100
+ }
101
+
102
+ @Test func flattenedChildrenConcatenateInSectionOrder() {
103
+ let page = ResolvedTrack(
104
+ path: "/p",
105
+ sections: [section("A", [track("a1"), track("a2")]), section(nil, [track("b1")])],
106
+ title: "P",
107
+ )
108
+ #expect(page.flattenedChildren?.map(\.src) == ["a1", "a2", "b1"])
109
+ }
110
+
111
+ @Test func childlessPageHasNoSections() {
112
+ #expect(ResolvedTrack(path: "/p", title: "P").normalizedSections == nil)
81
113
  }
82
114
  }