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
@@ -0,0 +1,127 @@
1
+ import Testing
2
+
3
+ @testable import AudioBrowserTestable
4
+
5
+ // Resolution semantics (ADR 0011): container properties and `display` resolve
6
+ // by scope override (`section ?? page`), inherited item properties resolve
7
+ // `track ?? section ?? page` — and `display` never inherits onto an item.
8
+
9
+ @Suite("StyleResolver.sectionStyle")
10
+ struct SectionStyleResolutionTests {
11
+ @Test func sectionOverridesThePagePerProperty() {
12
+ let resolved = StyleResolver.sectionStyle(
13
+ section: SectionStyle(gridWrap: nil, display: .list, artworkRendering: nil),
14
+ page: SectionStyle(gridWrap: false, display: .grid, artworkRendering: .stencil),
15
+ )
16
+ // Declared on the section: wins. Undeclared: the page's scope-wide value.
17
+ #expect(resolved.display == .list)
18
+ #expect(resolved.artworkRendering == .stencil)
19
+ #expect(resolved.gridWrap == false)
20
+ }
21
+
22
+ @Test func aStylelessSectionTakesThePageBlock() {
23
+ let resolved = StyleResolver.sectionStyle(
24
+ section: nil,
25
+ page: SectionStyle(gridWrap: true, display: .grid, artworkRendering: nil),
26
+ )
27
+ #expect(resolved.display == .grid)
28
+ #expect(resolved.gridWrap == true)
29
+ }
30
+
31
+ @Test func noDeclarationsResolveToAnEmptyBlock() {
32
+ let resolved = StyleResolver.sectionStyle(section: nil, page: nil)
33
+ #expect(resolved.display == nil)
34
+ #expect(resolved.artworkRendering == nil)
35
+ #expect(resolved.gridWrap == nil)
36
+ #expect(resolved.gridTile == nil)
37
+ }
38
+
39
+ @Test func gridTileResolvesByScopeOverride() {
40
+ // A page-wide card treatment; one section opts back to plain tiles.
41
+ let inherited = StyleResolver.sectionStyle(
42
+ section: nil,
43
+ page: SectionStyle(gridTile: .card),
44
+ )
45
+ #expect(inherited.gridTile == .card)
46
+ let overridden = StyleResolver.sectionStyle(
47
+ section: SectionStyle(gridTile: .plain),
48
+ page: SectionStyle(gridTile: .card),
49
+ )
50
+ #expect(overridden.gridTile == .plain)
51
+ }
52
+ }
53
+
54
+ @Suite("StyleResolver.trackStyle")
55
+ struct TrackStyleResolutionTests {
56
+ @Test func trackOverridesTheSectionPerProperty() {
57
+ let resolved = StyleResolver.trackStyle(
58
+ track: TrackStyle(display: nil, artworkRendering: .original),
59
+ section: SectionStyle(gridWrap: nil, display: .grid, artworkRendering: .stencil),
60
+ )
61
+ #expect(resolved.artworkRendering == .original)
62
+ }
63
+
64
+ @Test func aStylelessTrackInheritsTheSectionValue() {
65
+ let resolved = StyleResolver.trackStyle(
66
+ track: nil,
67
+ section: SectionStyle(gridWrap: nil, display: nil, artworkRendering: .stencil),
68
+ )
69
+ #expect(resolved.artworkRendering == .stencil)
70
+ }
71
+
72
+ @Test func displayNeverInheritsOntoAnItem() {
73
+ // The track's own `display` is the page promise for the page IT opens;
74
+ // the section's describes its children's layout. Neither may surface as
75
+ // the item's resolved display.
76
+ let resolved = StyleResolver.trackStyle(
77
+ track: TrackStyle(display: .grid, artworkRendering: nil),
78
+ section: SectionStyle(gridWrap: nil, display: .grid, artworkRendering: nil),
79
+ )
80
+ #expect(resolved.display == nil)
81
+ }
82
+
83
+ @Test func imageShapeInheritsAndOverridesPerItem() {
84
+ // Section-wide circular (an artists shelf); one album overrides.
85
+ let inherited = StyleResolver.trackStyle(
86
+ track: nil,
87
+ section: SectionStyle(imageShape: .circular),
88
+ )
89
+ #expect(inherited.imageShape == .circular)
90
+ let overridden = StyleResolver.trackStyle(
91
+ track: TrackStyle(imageShape: .roundedRectangle),
92
+ section: SectionStyle(imageShape: .circular),
93
+ )
94
+ #expect(overridden.imageShape == .roundedRectangle)
95
+ }
96
+
97
+ @Test func accessorySymbolInheritsAndNoneResolvesAsAValue() {
98
+ let inherited = StyleResolver.trackStyle(
99
+ track: nil,
100
+ section: SectionStyle(accessorySymbol: "lock.fill"),
101
+ )
102
+ #expect(inherited.accessorySymbol == "lock.fill")
103
+ // 'none' is the inheritance escape — it must survive resolution intact
104
+ // (the renderer, not the resolver, maps it to "no accessory").
105
+ let escaped = StyleResolver.trackStyle(
106
+ track: TrackStyle(accessorySymbol: "none"),
107
+ section: SectionStyle(accessorySymbol: "lock.fill"),
108
+ )
109
+ #expect(escaped.accessorySymbol == "none")
110
+ }
111
+
112
+ @Test func cardPropertiesInheritAndOverridePerItem() {
113
+ // Section-wide tint/mode; one card overrides its image mode.
114
+ let inherited = StyleResolver.trackStyle(
115
+ track: nil,
116
+ section: SectionStyle(accessorySymbol: nil, cardTint: "#1e3a8a", cardImage: .background),
117
+ )
118
+ #expect(inherited.cardTint == "#1e3a8a")
119
+ #expect(inherited.cardImage == .background)
120
+ let overridden = StyleResolver.trackStyle(
121
+ track: TrackStyle(cardImage: .normal),
122
+ section: SectionStyle(cardTint: "#1e3a8a", cardImage: .background),
123
+ )
124
+ #expect(overridden.cardImage == .normal)
125
+ #expect(overridden.cardTint == "#1e3a8a")
126
+ }
127
+ }
@@ -185,6 +185,7 @@ final class MockSleepTimerHandling: SleepTimerHandling {
185
185
  var onComplete: (() -> Void)?
186
186
  var onFadeStart: ((_ duration: TimeInterval) -> Void)?
187
187
  var onFadeCancel: (() -> Void)?
188
+ var onChanged: ((SleepTimerState) -> Void)?
188
189
  var trackChangedCallCount = 0
189
190
  var trackPlayedToEndCallCount = 0
190
191
  var clearCallCount = 0
@@ -196,6 +197,12 @@ final class MockSleepTimerHandling: SleepTimerHandling {
196
197
 
197
198
  func onTrackChanged() { trackChangedCallCount += 1 }
198
199
  func onTrackPlayedToEnd() { trackPlayedToEndCallCount += 1 }
200
+
201
+ // Consumer-facing surface — unused by the coordinator tests, present only to
202
+ // satisfy the protocol. Add recorders alongside the tests that need them.
203
+ func get() -> SleepTimerState? { nil }
204
+ func set(seconds _: TimeInterval, fadeDuration _: TimeInterval?) {}
205
+ func setToEndOfTrack() {}
199
206
  }
200
207
 
201
208
  // MARK: - Mock RetryHandling
@@ -4,59 +4,59 @@ import Testing
4
4
 
5
5
  struct TabBarEntriesTests {
6
6
  private func tab(
7
- _ title: String, url: String, artwork: String? = nil,
7
+ _ title: String, path: String, artwork: String? = nil,
8
8
  artworkSource: ImageSource? = nil, artist: String? = nil,
9
9
  ) -> Track {
10
10
  Track(
11
- id: url, url: url, title: title, artist: artist, artwork: artwork.map { .first($0) },
12
- artworkSource: artworkSource,
11
+ id: path, path: path, artwork: artwork.map { .first($0) },
12
+ artworkSource: artworkSource, title: title, artist: artist,
13
13
  )
14
14
  }
15
15
 
16
16
  @Test func identicalListsAreSame() {
17
- let tabs = [tab("Explore", url: "/explore"), tab("Browse", url: "/browse")]
17
+ let tabs = [tab("Explore", path: "/explore"), tab("Browse", path: "/browse")]
18
18
  #expect(TabBarEntries.same(tabs, tabs))
19
19
  }
20
20
 
21
21
  @Test func titleChangeDiffers() {
22
22
  #expect(
23
23
  !TabBarEntries.same(
24
- [tab("Explore", url: "/explore")],
25
- [tab("Verkennen", url: "/explore")],
24
+ [tab("Explore", path: "/explore")],
25
+ [tab("Verkennen", path: "/explore")],
26
26
  ))
27
27
  }
28
28
 
29
- @Test func urlChangeDiffers() {
29
+ @Test func pathChangeDiffers() {
30
30
  #expect(
31
31
  !TabBarEntries.same(
32
- [tab("Explore", url: "/explore")],
33
- [tab("Explore", url: "/discover")],
32
+ [tab("Explore", path: "/explore")],
33
+ [tab("Explore", path: "/discover")],
34
34
  ))
35
35
  }
36
36
 
37
37
  @Test func artworkChangeDiffers() {
38
38
  #expect(
39
39
  !TabBarEntries.same(
40
- [tab("Explore", url: "/explore", artwork: "sf:globe")],
41
- [tab("Explore", url: "/explore", artwork: "sf:map")],
40
+ [tab("Explore", path: "/explore", artwork: "sf:globe")],
41
+ [tab("Explore", path: "/explore", artwork: "sf:map")],
42
42
  ))
43
43
  }
44
44
 
45
45
  @Test func artworkSourceUriChangeDiffers() {
46
46
  #expect(
47
47
  !TabBarEntries.same(
48
- [tab("Explore", url: "/explore", artworkSource: ImageSource(uri: "https://a/1.png"))],
49
- [tab("Explore", url: "/explore", artworkSource: ImageSource(uri: "https://a/2.png"))],
48
+ [tab("Explore", path: "/explore", artworkSource: ImageSource(uri: "https://a/1.png"))],
49
+ [tab("Explore", path: "/explore", artworkSource: ImageSource(uri: "https://a/2.png"))],
50
50
  ))
51
51
  }
52
52
 
53
53
  @Test func countChangeDiffers() {
54
- let tabs = [tab("Explore", url: "/explore")]
55
- #expect(!TabBarEntries.same(tabs, tabs + [tab("Browse", url: "/browse")]))
54
+ let tabs = [tab("Explore", path: "/explore")]
55
+ #expect(!TabBarEntries.same(tabs, tabs + [tab("Browse", path: "/browse")]))
56
56
  }
57
57
 
58
58
  @Test func nilOldDiffers() {
59
- #expect(!TabBarEntries.same(nil, [tab("Explore", url: "/explore")]))
59
+ #expect(!TabBarEntries.same(nil, [tab("Explore", path: "/explore")]))
60
60
  }
61
61
 
62
62
  @Test func nonRenderedFieldChangeIsSame() {
@@ -64,8 +64,8 @@ struct TabBarEntriesTests {
64
64
  // drift without churning the tab bar.
65
65
  #expect(
66
66
  TabBarEntries.same(
67
- [tab("Explore", url: "/explore", artist: "a")],
68
- [tab("Explore", url: "/explore", artist: "b")],
67
+ [tab("Explore", path: "/explore", artist: "a")],
68
+ [tab("Explore", path: "/explore", artist: "b")],
69
69
  ))
70
70
  }
71
71
  }
@@ -0,0 +1,67 @@
1
+ @testable import AudioBrowserTestable
2
+ import Testing
3
+
4
+ /// `{id}` template substitution used by artwork URL resolution
5
+ /// (BrowserManager+URLResolution.substituteTrackId). A track without a non-blank id must leave
6
+ /// the token LITERALLY in place — never substitute an empty string, never skip — so the
7
+ /// resulting request 404s with a self-describing URL (the resolver logs a warning keyed off
8
+ /// `containsToken`). Mirrors Android's `substituteTrackId` / `configContainsIdToken`.
9
+ struct TrackIdTemplateTests {
10
+ // MARK: substitution with an id
11
+
12
+ @Test func substitutesTokenInString() {
13
+ #expect(TrackIdTemplate.substitute("/artwork/{id}", id: "abc") == "/artwork/abc")
14
+ }
15
+
16
+ @Test func substitutesEveryOccurrence() {
17
+ #expect(TrackIdTemplate.substitute("/{id}/art/{id}.png", id: "x") == "/x/art/x.png")
18
+ }
19
+
20
+ @Test func substitutesTokenInDictionaryValues() {
21
+ let subbed = TrackIdTemplate.substitute(["x-track": "{id}", "plain": "v"], id: "abc")
22
+ #expect(subbed == ["x-track": "abc", "plain": "v"])
23
+ }
24
+
25
+ @Test func valueWithoutTokenIsUnchanged() {
26
+ #expect(TrackIdTemplate.substitute("/artwork/static.png", id: "abc") == "/artwork/static.png")
27
+ }
28
+
29
+ // MARK: token left literal without an id
30
+
31
+ @Test func nilIdLeavesTokenLiteral() {
32
+ #expect(TrackIdTemplate.substitute("/artwork/{id}", id: nil) == "/artwork/{id}")
33
+ }
34
+
35
+ @Test func blankIdLeavesTokenLiteral() {
36
+ #expect(TrackIdTemplate.substitute("/artwork/{id}", id: "") == "/artwork/{id}")
37
+ }
38
+
39
+ @Test func nilIdLeavesDictionaryValuesLiteral() {
40
+ #expect(TrackIdTemplate.substitute(["x-track": "{id}"], id: nil) == ["x-track": "{id}"])
41
+ }
42
+
43
+ @Test func nilValuePassesThrough() {
44
+ #expect(TrackIdTemplate.substitute(String?.none, id: "abc") == nil)
45
+ #expect(TrackIdTemplate.substitute([String: String]?.none, id: nil) == nil)
46
+ }
47
+
48
+ // MARK: unfilled-token detection (drives the single warning)
49
+
50
+ @Test func detectsUnfilledTokenInString() {
51
+ #expect(TrackIdTemplate.containsToken("/artwork/{id}"))
52
+ #expect(!TrackIdTemplate.containsToken("/artwork/abc"))
53
+ #expect(!TrackIdTemplate.containsToken(String?.none))
54
+ }
55
+
56
+ @Test func detectsUnfilledTokenInDictionaryValues() {
57
+ #expect(TrackIdTemplate.containsToken(["x-track": "{id}"]))
58
+ #expect(!TrackIdTemplate.containsToken(["x-track": "abc"]))
59
+ #expect(!TrackIdTemplate.containsToken([String: String]?.none))
60
+ }
61
+
62
+ @Test func substitutionThenDetectionNeverBothMatch() {
63
+ // With an id, substitution clears the token, so no warning fires.
64
+ let subbed = TrackIdTemplate.substitute("/artwork/{id}", id: "abc")
65
+ #expect(!TrackIdTemplate.containsToken(subbed))
66
+ }
67
+ }
@@ -0,0 +1,20 @@
1
+ @testable import AudioBrowserTestable
2
+ import Testing
3
+
4
+ struct TrackIdentityTests {
5
+ @Test func idWinsOverSrc() {
6
+ #expect(Track(id: "stable", src: "https://cdn.example/stream").identity == "stable")
7
+ }
8
+
9
+ @Test func blankIdFallsBackToSrc() {
10
+ #expect(Track(id: "", src: "https://cdn.example/stream").identity == "https://cdn.example/stream")
11
+ }
12
+
13
+ @Test func nilIdFallsBackToSrc() {
14
+ #expect(Track(src: "https://cdn.example/stream").identity == "https://cdn.example/stream")
15
+ }
16
+
17
+ @Test func neitherIdNorSrcHasNoIdentity() {
18
+ #expect(Track(path: "/browse/only").identity == nil)
19
+ }
20
+ }
@@ -12,15 +12,15 @@ private final class BrowserMock: TrackSelectionBrowser {
12
12
 
13
13
  // Record calls for verification
14
14
  var trackLoadEvents: [TrackLoadEvent] = []
15
- var expandedUrls: [String] = []
15
+ var expandedPaths: [String] = []
16
16
 
17
17
  func awaitTrackLoadHandler(event: TrackLoadEvent) async -> Bool {
18
18
  trackLoadEvents.append(event)
19
19
  return trackLoadHandlerResult
20
20
  }
21
21
 
22
- func expandQueueFromContextualUrl(_ url: String) async throws -> (tracks: [Track], selectedIndex: Int)? {
23
- expandedUrls.append(url)
22
+ func expandQueueFromContextualPath(_ path: String) async throws -> (tracks: [Track], selectedIndex: Int)? {
23
+ expandedPaths.append(path)
24
24
  if let error = expandQueueError {
25
25
  throw error
26
26
  }
@@ -53,23 +53,23 @@ private func makeSelector() -> (TrackSelector, BrowserMock, PlayerMock) {
53
53
  @Suite("browsable track")
54
54
  @MainActor
55
55
  struct BrowsableTrackTests {
56
- @Test func urlOnly_returnsBrowse() async {
56
+ @Test func pathOnly_returnsBrowse() async {
57
57
  let (selector, _, player) = makeSelector()
58
- let track = Track(id: "t1", url: "/some/path")
58
+ let track = Track(id: "t1", path: "/some/path")
59
59
  let result = await selector.select(track: track, player: player)
60
- guard case let .browse(url) = result else {
60
+ guard case let .browse(path) = result else {
61
61
  Issue.record("expected .browse, got \(result)")
62
62
  return
63
63
  }
64
- #expect(url == "/some/path")
64
+ #expect(path == "/some/path")
65
65
  }
66
66
  }
67
67
 
68
- // MARK: - No src or url
68
+ // MARK: - No src or path
69
69
 
70
- @Suite("no src or url")
70
+ @Suite("no src or path")
71
71
  @MainActor
72
- struct NoSrcOrUrlTests {
72
+ struct NoSrcOrPathTests {
73
73
  @Test func returnsNone() async {
74
74
  let (selector, _, player) = makeSelector()
75
75
  let track = Track(id: "t1")
@@ -115,25 +115,56 @@ struct PlayableTrackTests {
115
115
  }
116
116
  }
117
117
 
118
- // MARK: - Contextual URL Queue Reuse
118
+ // MARK: - Contextual path Queue Reuse
119
119
 
120
- @Suite("contextual URL queue reuse")
120
+ @Suite("contextual path queue reuse")
121
121
  @MainActor
122
- struct ContextualUrlQueueReuseTests {
122
+ struct ContextualPathQueueReuseTests {
123
123
  @Test func matchingQueueSourcePath_returnsSkipTo() async {
124
124
  let (selector, browser, player) = makeSelector()
125
125
  let parentPath = "/library/radio"
126
126
  let trackSrc = "song.mp3"
127
- let contextualUrl = BrowserPathHelper.build(parentPath: parentPath, trackId: trackSrc)
127
+ let contextualPath = BrowserPathHelper.build(parentPath: parentPath, trackId: trackSrc)
128
+
129
+ // Id-less queue tracks: identity falls back to src.
130
+ player.queueSourcePath = parentPath
131
+ player.tracks = [
132
+ Track(src: "other.mp3"),
133
+ Track(src: "song.mp3"),
134
+ Track(src: "another.mp3"),
135
+ ]
136
+
137
+ let track = Track(path: contextualPath, src: trackSrc)
138
+ let result = await selector.select(track: track, player: player)
139
+ guard case let .play(intent) = result else {
140
+ Issue.record("expected .play, got \(result)")
141
+ return
142
+ }
143
+ guard case let .skipTo(index) = intent else {
144
+ Issue.record("expected .skipTo, got \(intent)")
145
+ return
146
+ }
147
+ #expect(index == 1)
148
+ #expect(browser.trackLoadEvents.count == 1)
149
+ #expect(browser.expandedPaths.isEmpty)
150
+ }
151
+
152
+ // Skip-in-place matches by id when queue tracks carry ids: the contextual
153
+ // trackId is the row's identity (its id), and the queue row is found even
154
+ // though its src differs textually from the tapped track's.
155
+ @Test func matchingQueueSourcePath_skipsToTrackByIdWhenSrcDiffers() async {
156
+ let (selector, browser, player) = makeSelector()
157
+ let parentPath = "/library/radio"
158
+ let contextualPath = BrowserPathHelper.build(parentPath: parentPath, trackId: "b")
128
159
 
129
160
  player.queueSourcePath = parentPath
130
161
  player.tracks = [
131
162
  Track(id: "a", src: "other.mp3"),
132
- Track(id: "b", src: "song.mp3"),
163
+ Track(id: "b", src: "https://cdn.example/song.mp3?token=queue"),
133
164
  Track(id: "c", src: "another.mp3"),
134
165
  ]
135
166
 
136
- let track = Track(id: "t1", url: contextualUrl, src: trackSrc)
167
+ let track = Track(id: "b", path: contextualPath, src: "song.mp3")
137
168
  let result = await selector.select(track: track, player: player)
138
169
  guard case let .play(intent) = result else {
139
170
  Issue.record("expected .play, got \(result)")
@@ -144,8 +175,7 @@ struct ContextualUrlQueueReuseTests {
144
175
  return
145
176
  }
146
177
  #expect(index == 1)
147
- #expect(browser.trackLoadEvents.count == 1)
148
- #expect(browser.expandedUrls.isEmpty)
178
+ #expect(browser.expandedPaths.isEmpty)
149
179
  }
150
180
 
151
181
  @Test func handlerIntercepts_returnsIntercepted() async {
@@ -153,32 +183,93 @@ struct ContextualUrlQueueReuseTests {
153
183
  browser.trackLoadHandlerResult = true
154
184
  let parentPath = "/library/radio"
155
185
  let trackSrc = "song.mp3"
156
- let contextualUrl = BrowserPathHelper.build(parentPath: parentPath, trackId: trackSrc)
186
+ let contextualPath = BrowserPathHelper.build(parentPath: parentPath, trackId: trackSrc)
157
187
 
158
188
  player.queueSourcePath = parentPath
159
189
  player.tracks = [
160
- Track(id: "a", src: "song.mp3"),
190
+ Track(src: "song.mp3"),
161
191
  ]
162
192
 
163
- let track = Track(id: "t1", url: contextualUrl, src: trackSrc)
193
+ let track = Track(path: contextualPath, src: trackSrc)
164
194
  let result = await selector.select(track: track, player: player)
165
195
  guard case .intercepted = result else {
166
196
  Issue.record("expected .intercepted, got \(result)")
167
197
  return
168
198
  }
199
+ #expect(browser.expandedPaths.isEmpty)
200
+ }
201
+
202
+ // A queue holding the same identity twice (normal for music playlists):
203
+ // exact contextual-path equality — which carries the stamped page index —
204
+ // pins the tapped copy instead of the first one.
205
+ @Test func duplicateIdentityInQueue_pathMatchPinsTheTappedCopy() async {
206
+ let (selector, browser, player) = makeSelector()
207
+ let parentPath = "/playlist"
208
+ func stamped(_ id: String, _ index: Int) -> String {
209
+ BrowserPathHelper.build(parentPath: parentPath, trackId: id, index: index)
210
+ }
211
+
212
+ player.queueSourcePath = parentPath
213
+ player.tracks = [
214
+ Track(path: stamped("a", 0), src: "a"),
215
+ Track(path: stamped("b", 1), src: "b"),
216
+ Track(path: stamped("a", 2), src: "a"),
217
+ ]
218
+
219
+ let track = Track(path: stamped("a", 2), src: "a")
220
+ let result = await selector.select(track: track, player: player)
221
+ guard case let .play(.skipTo(index)) = result else {
222
+ Issue.record("expected .play(.skipTo), got \(result)")
223
+ return
224
+ }
225
+ #expect(index == 2)
226
+ #expect(browser.expandedPaths.isEmpty)
227
+ }
228
+
229
+ // A tap in section B whose identity also sits in the active section-A
230
+ // queue: the exact surface is not in the queue, so the selection
231
+ // re-expands (re-scoping to the tapped section) instead of silently
232
+ // skipping inside the old section's queue.
233
+ @Test func indexStampedPathNotInQueue_reExpandsInsteadOfIdentitySkip() async {
234
+ let (selector, browser, player) = makeSelector()
235
+ let parentPath = "/city"
236
+ func stamped(_ id: String, _ index: Int) -> String {
237
+ BrowserPathHelper.build(parentPath: parentPath, trackId: id, index: index)
238
+ }
239
+
240
+ player.queueSourcePath = parentPath
241
+ player.tracks = [
242
+ Track(path: stamped("dup", 0), src: "dup"),
243
+ Track(path: stamped("x", 1), src: "x"),
244
+ ]
245
+ browser.expandQueueResult = (
246
+ tracks: [Track(path: stamped("y", 4), src: "y"), Track(path: stamped("dup", 5), src: "dup")],
247
+ selectedIndex: 1,
248
+ )
249
+
250
+ let track = Track(path: stamped("dup", 5), src: "dup")
251
+ let result = await selector.select(track: track, player: player)
252
+ guard case let .play(.setQueue(tracks, startIndex, sourcePath)) = result else {
253
+ Issue.record("expected .play(.setQueue), got \(result)")
254
+ return
255
+ }
256
+ #expect(tracks.count == 2)
257
+ #expect(startIndex == 1)
258
+ #expect(sourcePath == parentPath)
259
+ #expect(browser.expandedPaths == [stamped("dup", 5)])
169
260
  }
170
261
  }
171
262
 
172
- // MARK: - Contextual URL Expansion
263
+ // MARK: - Contextual path Expansion
173
264
 
174
- @Suite("contextual URL expansion")
265
+ @Suite("contextual path expansion")
175
266
  @MainActor
176
- struct ContextualUrlExpansionTests {
267
+ struct ContextualPathExpansionTests {
177
268
  @Test func expands_returnsSetQueue() async {
178
269
  let (selector, browser, player) = makeSelector()
179
270
  let parentPath = "/library/radio"
180
271
  let trackSrc = "song.mp3"
181
- let contextualUrl = BrowserPathHelper.build(parentPath: parentPath, trackId: trackSrc)
272
+ let contextualPath = BrowserPathHelper.build(parentPath: parentPath, trackId: trackSrc)
182
273
 
183
274
  let expandedTracks = [
184
275
  Track(id: "a", src: "first.mp3"),
@@ -187,7 +278,7 @@ struct ContextualUrlExpansionTests {
187
278
  ]
188
279
  browser.expandQueueResult = (tracks: expandedTracks, selectedIndex: 1)
189
280
 
190
- let track = Track(id: "t1", url: contextualUrl, src: trackSrc)
281
+ let track = Track(id: "t1", path: contextualPath, src: trackSrc)
191
282
  let result = await selector.select(track: track, player: player)
192
283
  guard case let .play(intent) = result else {
193
284
  Issue.record("expected .play, got \(result)")
@@ -200,7 +291,7 @@ struct ContextualUrlExpansionTests {
200
291
  #expect(tracks.count == 3)
201
292
  #expect(startIndex == 1)
202
293
  #expect(sourcePath == parentPath)
203
- #expect(browser.expandedUrls == [contextualUrl])
294
+ #expect(browser.expandedPaths == [contextualPath])
204
295
  #expect(browser.trackLoadEvents.count == 1)
205
296
  }
206
297
 
@@ -209,14 +300,14 @@ struct ContextualUrlExpansionTests {
209
300
  browser.trackLoadHandlerResult = true
210
301
  let parentPath = "/library/radio"
211
302
  let trackSrc = "song.mp3"
212
- let contextualUrl = BrowserPathHelper.build(parentPath: parentPath, trackId: trackSrc)
303
+ let contextualPath = BrowserPathHelper.build(parentPath: parentPath, trackId: trackSrc)
213
304
 
214
305
  browser.expandQueueResult = (
215
306
  tracks: [Track(id: "a", src: "song.mp3")],
216
307
  selectedIndex: 0,
217
308
  )
218
309
 
219
- let track = Track(id: "t1", url: contextualUrl, src: trackSrc)
310
+ let track = Track(id: "t1", path: contextualPath, src: trackSrc)
220
311
  let result = await selector.select(track: track, player: player)
221
312
  guard case .intercepted = result else {
222
313
  Issue.record("expected .intercepted, got \(result)")
@@ -228,11 +319,11 @@ struct ContextualUrlExpansionTests {
228
319
  let (selector, browser, player) = makeSelector()
229
320
  let parentPath = "/library/radio"
230
321
  let trackSrc = "song.mp3"
231
- let contextualUrl = BrowserPathHelper.build(parentPath: parentPath, trackId: trackSrc)
322
+ let contextualPath = BrowserPathHelper.build(parentPath: parentPath, trackId: trackSrc)
232
323
 
233
324
  browser.expandQueueResult = nil
234
325
 
235
- let track = Track(id: "t1", url: contextualUrl, src: trackSrc)
326
+ let track = Track(id: "t1", path: contextualPath, src: trackSrc)
236
327
  let result = await selector.select(track: track, player: player)
237
328
  guard case let .play(intent) = result else {
238
329
  Issue.record("expected .play, got \(result)")
@@ -249,11 +340,11 @@ struct ContextualUrlExpansionTests {
249
340
  let (selector, browser, player) = makeSelector()
250
341
  let parentPath = "/library/radio"
251
342
  let trackSrc = "song.mp3"
252
- let contextualUrl = BrowserPathHelper.build(parentPath: parentPath, trackId: trackSrc)
343
+ let contextualPath = BrowserPathHelper.build(parentPath: parentPath, trackId: trackSrc)
253
344
 
254
345
  browser.expandQueueError = MockError.expansion
255
346
 
256
- let track = Track(id: "t1", url: contextualUrl, src: trackSrc)
347
+ let track = Track(id: "t1", path: contextualPath, src: trackSrc)
257
348
  let result = await selector.select(track: track, player: player)
258
349
  guard case let .play(intent) = result else {
259
350
  Issue.record("expected .play, got \(result)")
@@ -266,3 +357,47 @@ struct ContextualUrlExpansionTests {
266
357
  #expect(loaded.src == trackSrc)
267
358
  }
268
359
  }
360
+
361
+ // MARK: - Disabled Track
362
+
363
+ @Suite("disabled track")
364
+ @MainActor
365
+ struct DisabledTrackTests {
366
+ @Test func disabledPlayable_returnsNone() async {
367
+ let (selector, browser, player) = makeSelector()
368
+ let track = Track(id: "t1", src: "https://example.com/a.mp3", disabled: true)
369
+ let result = await selector.select(track: track, player: player)
370
+ guard case .none = result else {
371
+ Issue.record("Expected .none for a disabled track, got \(result)")
372
+ return
373
+ }
374
+ // Refused outright: not even the JS load handler hears about it.
375
+ #expect(browser.trackLoadEvents.isEmpty)
376
+ }
377
+
378
+ @Test func disabledBrowsable_returnsNone() async {
379
+ // Unavailable means unavailable — a disabled browsable doesn't navigate
380
+ // either (the ladder grays or hides it; a delivered tap stays inert).
381
+ let (selector, _, player) = makeSelector()
382
+ let track = Track(id: "t1", path: "/gone", disabled: true)
383
+ let result = await selector.select(track: track, player: player)
384
+ guard case .none = result else {
385
+ Issue.record("Expected .none for a disabled browsable, got \(result)")
386
+ return
387
+ }
388
+ }
389
+
390
+ @Test func disabledContextual_neverExpands() async {
391
+ let (selector, browser, player) = makeSelector()
392
+ browser.expandQueueResult = (tracks: [Track(id: "a", src: "a")], selectedIndex: 0)
393
+ let track = Track(
394
+ id: "t1", path: "/home?__trackId=t1", src: "https://example.com/a.mp3", disabled: true,
395
+ )
396
+ let result = await selector.select(track: track, player: player)
397
+ guard case .none = result else {
398
+ Issue.record("Expected .none, got \(result)")
399
+ return
400
+ }
401
+ #expect(browser.expandedPaths.isEmpty)
402
+ }
403
+ }