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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/README.md +4 -4
  2. package/android/build.gradle +6 -5
  3. package/android/gradle.properties +1 -1
  4. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
  5. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
  6. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
  7. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -58
  8. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
  9. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +49 -0
  10. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -7
  11. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  12. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  13. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
  14. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
  15. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +93 -73
  16. package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
  17. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  18. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +46 -5
  19. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +86 -27
  20. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  21. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +20 -6
  22. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +249 -86
  23. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
  24. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  25. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +35 -0
  26. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
  27. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +69 -30
  28. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  29. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
  30. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +345 -8
  31. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  32. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
  33. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
  34. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
  35. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -0
  36. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  37. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  38. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
  39. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  40. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  41. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +152 -8
  42. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +218 -10
  43. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  44. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  45. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
  46. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  47. package/ios/Browser/BrowserConfig.swift +102 -5
  48. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  49. package/ios/Browser/BrowserManager.swift +131 -143
  50. package/ios/Browser/BrowserPathHelper.swift +44 -44
  51. package/ios/Browser/JsonModels.swift +187 -106
  52. package/ios/Browser/SearchDrillIn.swift +6 -6
  53. package/ios/Browser/SectionPresentation.swift +34 -0
  54. package/ios/Browser/SectionScope.swift +58 -24
  55. package/ios/Browser/StyleResolver.swift +47 -0
  56. package/ios/Browser/TabBarEntries.swift +2 -2
  57. package/ios/Browser/TrackIdTemplate.swift +37 -0
  58. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  59. package/ios/CarPlay/CarPlayController.swift +105 -67
  60. package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
  61. package/ios/CarPlay/CarPlayListItemFactory.swift +217 -84
  62. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  63. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  64. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  65. package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
  66. package/ios/Extension/Section+Copying.swift +31 -0
  67. package/ios/Extension/Track+Copying.swift +6 -12
  68. package/ios/Extension/Track+Identity.swift +18 -0
  69. package/ios/HybridAudioBrowser.swift +113 -35
  70. package/ios/Model/NitroTypeStubs.swift +64 -23
  71. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  72. package/ios/Player/NowPlayingUpdater.swift +1 -1
  73. package/ios/Player/PlaybackCoordinator.swift +2 -2
  74. package/ios/Player/RetryManager.swift +4 -3
  75. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  76. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  77. package/ios/Tests/JsonModelsTests.swift +75 -1
  78. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  79. package/ios/Tests/QueueManagerTests.swift +4 -4
  80. package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
  81. package/ios/Tests/SearchDrillInTests.swift +16 -16
  82. package/ios/Tests/SectionPresentationTests.swift +61 -0
  83. package/ios/Tests/SectionScopeTests.swift +87 -55
  84. package/ios/Tests/StyleResolverTests.swift +67 -0
  85. package/ios/Tests/TabBarEntriesTests.swift +18 -18
  86. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  87. package/ios/Tests/TrackIdentityTests.swift +20 -0
  88. package/ios/Tests/TrackSelectorTests.swift +168 -33
  89. package/ios/TrackSelector.swift +45 -30
  90. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  91. package/lib/commonjs/AudioBrowser.js +11 -0
  92. package/lib/commonjs/AudioBrowser.js.map +1 -1
  93. package/lib/commonjs/features/browser-config.js +11 -8
  94. package/lib/commonjs/features/browser-config.js.map +1 -1
  95. package/lib/commonjs/features/browser.js +2 -2
  96. package/lib/commonjs/features/favorites.js +13 -10
  97. package/lib/commonjs/features/favorites.js.map +1 -1
  98. package/lib/commonjs/features/playback/controls.js +4 -2
  99. package/lib/commonjs/features/playback/controls.js.map +1 -1
  100. package/lib/commonjs/features/playback/state.js +2 -3
  101. package/lib/commonjs/features/playback/state.js.map +1 -1
  102. package/lib/commonjs/features/player/options.js.map +1 -1
  103. package/lib/commonjs/features/player/setup.js +3 -1
  104. package/lib/commonjs/features/player/setup.js.map +1 -1
  105. package/lib/commonjs/features/player/validateOptions.js +2 -0
  106. package/lib/commonjs/features/player/validateOptions.js.map +1 -1
  107. package/lib/commonjs/features/queue/activeTrack.js +14 -3
  108. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  109. package/lib/commonjs/features/queue/queue.js +11 -4
  110. package/lib/commonjs/features/queue/queue.js.map +1 -1
  111. package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
  112. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  113. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  114. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  115. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  116. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  117. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  118. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  119. package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
  120. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  121. package/lib/commonjs/web/browser/BrowserManager.js +144 -87
  122. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  123. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  124. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  125. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  126. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  127. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  128. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  129. package/lib/commonjs/web/browser/sections.js +89 -0
  130. package/lib/commonjs/web/browser/sections.js.map +1 -0
  131. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  132. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  133. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  134. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  135. package/lib/module/AudioBrowser.js +1 -0
  136. package/lib/module/AudioBrowser.js.map +1 -1
  137. package/lib/module/features/browser-config.js +11 -8
  138. package/lib/module/features/browser-config.js.map +1 -1
  139. package/lib/module/features/browser.js +2 -2
  140. package/lib/module/features/favorites.js +13 -10
  141. package/lib/module/features/favorites.js.map +1 -1
  142. package/lib/module/features/playback/controls.js +4 -2
  143. package/lib/module/features/playback/controls.js.map +1 -1
  144. package/lib/module/features/playback/state.js +2 -3
  145. package/lib/module/features/playback/state.js.map +1 -1
  146. package/lib/module/features/player/options.js.map +1 -1
  147. package/lib/module/features/player/setup.js +3 -1
  148. package/lib/module/features/player/setup.js.map +1 -1
  149. package/lib/module/features/player/validateOptions.js +2 -0
  150. package/lib/module/features/player/validateOptions.js.map +1 -1
  151. package/lib/module/features/queue/activeTrack.js +14 -3
  152. package/lib/module/features/queue/activeTrack.js.map +1 -1
  153. package/lib/module/features/queue/queue.js +11 -4
  154. package/lib/module/features/queue/queue.js.map +1 -1
  155. package/lib/module/utils/LazyNativeEmitter.js +21 -10
  156. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  157. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  158. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  159. package/lib/module/utils/getTrackIdentity.js +20 -0
  160. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  161. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  162. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  163. package/lib/module/web/NativeAudioBrowser.js +32 -24
  164. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  165. package/lib/module/web/browser/BrowserManager.js +144 -87
  166. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  167. package/lib/module/web/browser/FavoriteManager.js +31 -21
  168. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  169. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  170. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  171. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  172. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  173. package/lib/module/web/browser/sections.js +83 -0
  174. package/lib/module/web/browser/sections.js.map +1 -0
  175. package/lib/module/web/player/NowPlayingManager.js +4 -3
  176. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  177. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  178. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  179. package/lib/typescript/src/AudioBrowser.d.ts +1 -0
  180. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  181. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  182. package/lib/typescript/src/features/browser.d.ts +2 -2
  183. package/lib/typescript/src/features/favorites.d.ts +13 -10
  184. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  185. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  186. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  187. package/lib/typescript/src/features/playback/state.d.ts +2 -3
  188. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  189. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  190. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  191. package/lib/typescript/src/features/player/options.d.ts +21 -19
  192. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  193. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  194. package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
  195. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
  196. package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
  197. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  198. package/lib/typescript/src/features/queue/queue.d.ts +5 -2
  199. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  200. package/lib/typescript/src/types/browser-native.d.ts +2 -2
  201. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  202. package/lib/typescript/src/types/browser-nodes.d.ts +260 -127
  203. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  204. package/lib/typescript/src/types/browser.d.ts +33 -53
  205. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  206. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
  207. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  208. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  209. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  210. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  211. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  212. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  213. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  214. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
  215. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  216. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -3
  217. package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
  218. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  219. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
  220. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
  221. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
  222. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
  223. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
  224. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
  225. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
  226. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -2
  227. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +10 -4
  228. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
  229. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
  230. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
  231. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
  232. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
  233. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
  234. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  235. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
  236. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
  237. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
  238. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -7
  239. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
  240. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  241. package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
  242. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -3
  243. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  244. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
  245. package/nitrogen/generated/android/c++/JSection.hpp +114 -0
  246. package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
  247. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  248. package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
  249. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
  250. package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
  251. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -4
  252. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  253. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  254. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  255. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
  256. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  257. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  258. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
  259. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +50 -0
  260. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
  261. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  262. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
  263. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +23 -5
  264. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
  265. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
  266. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
  267. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  268. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  269. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  270. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
  271. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  272. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  273. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
  274. package/nitrogen/generated/ios/swift/Section.swift +100 -0
  275. package/nitrogen/generated/ios/swift/SectionStyle.swift +64 -0
  276. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  277. package/nitrogen/generated/ios/swift/Track.swift +20 -73
  278. package/nitrogen/generated/ios/swift/TrackStyle.swift +28 -22
  279. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  280. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  281. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  282. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
  283. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  284. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  285. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
  286. package/nitrogen/generated/shared/c++/Section.hpp +106 -0
  287. package/nitrogen/generated/shared/c++/SectionStyle.hpp +96 -0
  288. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  289. package/nitrogen/generated/shared/c++/Track.hpp +15 -31
  290. package/nitrogen/generated/shared/c++/TrackStyle.hpp +50 -34
  291. package/package.json +3 -2
  292. package/src/AudioBrowser.ts +1 -0
  293. package/src/features/browser-config.test.ts +21 -6
  294. package/src/features/browser-config.ts +11 -8
  295. package/src/features/browser.test.ts +1 -1
  296. package/src/features/browser.ts +2 -2
  297. package/src/features/favorites.ts +13 -10
  298. package/src/features/hook-wiring.test.ts +63 -15
  299. package/src/features/playback/controls.ts +4 -2
  300. package/src/features/playback/state.ts +2 -3
  301. package/src/features/playback/trackPlaybackTime.ts +1 -1
  302. package/src/features/player/options.ts +27 -19
  303. package/src/features/player/setup.test.ts +12 -3
  304. package/src/features/player/setup.ts +3 -1
  305. package/src/features/player/validateOptions.ts +2 -0
  306. package/src/features/queue/activeTrack.ts +14 -3
  307. package/src/features/queue/queue.ts +11 -4
  308. package/src/nitro-stub-drift.test.ts +277 -0
  309. package/src/public-surface.test.ts +40 -0
  310. package/src/specs/audio-browser.nitro.ts +4 -3
  311. package/src/style-resolution.test.ts +139 -0
  312. package/src/types/browser-native.ts +2 -2
  313. package/src/types/browser-nodes.ts +268 -136
  314. package/src/types/browser.ts +34 -55
  315. package/src/utils/LazyNativeEmitter.ts +20 -10
  316. package/src/utils/NativeUpdatedValue.ts +29 -13
  317. package/src/utils/getTrackIdentity.ts +20 -0
  318. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  319. package/src/web/NativeAudioBrowser.ts +37 -22
  320. package/src/web/browser/BrowserManager.ts +176 -95
  321. package/src/web/browser/FavoriteManager.test.ts +84 -0
  322. package/src/web/browser/FavoriteManager.ts +28 -23
  323. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  324. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  325. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  326. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  327. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  328. package/src/web/browser/sections.test.ts +91 -0
  329. package/src/web/browser/sections.ts +72 -0
  330. package/src/web/player/NowPlayingManager.ts +4 -3
  331. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  332. package/src/web/util/BrowserPathHelper.ts +61 -13
  333. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  334. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  335. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  336. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  337. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  338. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  339. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  340. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  341. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  342. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  343. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  344. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  345. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
@@ -13,14 +13,16 @@ export function assertBrowsePageShape(
13
13
  if (Array.isArray(response)) {
14
14
  throw new Error(
15
15
  `Browse endpoint for "${path}" returned a JSON array; expected a ` +
16
- `page object { title, children?: Track[] }. Wrap the rows in a ` +
17
- `\`children\` array.`
16
+ `page object { title, sections?: Section[], children?: Track[] }. ` +
17
+ `Wrap the rows in a \`children\` array (one untitled section) or ` +
18
+ `group them into \`sections\`.`
18
19
  )
19
20
  }
20
21
  if (response === null || typeof response !== 'object') {
21
22
  throw new Error(
22
23
  `Browse endpoint for "${path}" returned ${JSON.stringify(response)}; ` +
23
- `expected a page object { title, children?: Track[] }.`
24
+ `expected a page object { title, sections?: Section[], ` +
25
+ `children?: Track[] }.`
24
26
  )
25
27
  }
26
28
  return response as ResolvedTrack
@@ -5,10 +5,10 @@ import { classifyTrackNavigation } from './classifyTrackNavigation'
5
5
  const track = (fields: Partial<Track>): Track => fields as Track
6
6
 
7
7
  describe('classifyTrackNavigation', () => {
8
- it('classifies a contextual url, exposing parent path and track id', () => {
8
+ it('classifies a contextual path, exposing parent path and track id', () => {
9
9
  expect(
10
10
  classifyTrackNavigation(
11
- track({ url: '/library/radio?__trackId=song.mp3' })
11
+ track({ path: '/library/radio?__trackId=song.mp3' })
12
12
  )
13
13
  ).toEqual({
14
14
  kind: 'contextual',
@@ -17,25 +17,27 @@ describe('classifyTrackNavigation', () => {
17
17
  })
18
18
  })
19
19
 
20
- it('classifies a non-contextual url as a browse navigation', () => {
21
- expect(classifyTrackNavigation(track({ url: '/library/radio' }))).toEqual({
20
+ it('classifies a non-contextual path as a browse navigation', () => {
21
+ expect(classifyTrackNavigation(track({ path: '/library/radio' }))).toEqual({
22
22
  kind: 'browse'
23
23
  })
24
24
  })
25
25
 
26
- it('classifies a track with src but no url as playable', () => {
26
+ it('classifies a track with src but no path as playable', () => {
27
27
  expect(classifyTrackNavigation(track({ src: 'song.mp3' }))).toEqual({
28
28
  kind: 'playable'
29
29
  })
30
30
  })
31
31
 
32
- it('prefers browse over playable when a url is present', () => {
32
+ it('prefers browse over playable when a path is present', () => {
33
33
  expect(
34
- classifyTrackNavigation(track({ url: '/library/radio', src: 'song.mp3' }))
34
+ classifyTrackNavigation(
35
+ track({ path: '/library/radio', src: 'song.mp3' })
36
+ )
35
37
  ).toEqual({ kind: 'browse' })
36
38
  })
37
39
 
38
- it('classifies a track with neither url nor src as invalid', () => {
40
+ it('classifies a track with neither path nor src as invalid', () => {
39
41
  expect(classifyTrackNavigation(track({}))).toEqual({ kind: 'invalid' })
40
42
  })
41
43
  })
@@ -4,10 +4,10 @@ import { BrowserPathHelper } from '../util/BrowserPathHelper'
4
4
  /**
5
5
  * How a selected track should be navigated, decided purely from its shape:
6
6
  * - `contextual` — a playable track carrying its parent queue context in a
7
- * contextual URL (`{parentPath}?__trackId={src}`); expand/skip within that queue.
8
- * - `browse` — a browsable track (has a `url` that isn't contextual); drill in.
9
- * - `playable` — a bare playable track (`src`, no `url`); load and play it alone.
10
- * - `invalid` — neither `url` nor `src`; nothing to do.
7
+ * contextual path (`{parentPath}?__trackId={src}`); expand/skip within that queue.
8
+ * - `browse` — a browsable track (has a `path` that isn't contextual); drill in.
9
+ * - `playable` — a bare playable track (`src`, no `path`); load and play it alone.
10
+ * - `invalid` — neither `path` nor `src`; nothing to do.
11
11
  */
12
12
  export type TrackNavigation =
13
13
  | { kind: 'contextual'; parentPath: string; trackId: string | undefined }
@@ -21,15 +21,15 @@ export type TrackNavigation =
21
21
  * caller; this only decides which action applies.
22
22
  */
23
23
  export function classifyTrackNavigation(track: Track): TrackNavigation {
24
- const url = track.url
25
- if (url && BrowserPathHelper.isContextual(url)) {
24
+ const path = track.path
25
+ if (path && BrowserPathHelper.isContextual(path)) {
26
26
  return {
27
27
  kind: 'contextual',
28
- parentPath: BrowserPathHelper.stripTrackId(url),
29
- trackId: BrowserPathHelper.extractTrackId(url)
28
+ parentPath: BrowserPathHelper.stripTrackId(path),
29
+ trackId: BrowserPathHelper.extractTrackId(path)
30
30
  }
31
31
  }
32
- if (url) return { kind: 'browse' }
32
+ if (path) return { kind: 'browse' }
33
33
  if (track.src) return { kind: 'playable' }
34
34
  return { kind: 'invalid' }
35
35
  }
@@ -5,7 +5,11 @@ describe('parseSearchResponse', () => {
5
5
  it('extracts children from a page-object response', () => {
6
6
  const tracks = [{ src: '/a', title: 'A' }]
7
7
  expect(
8
- parseSearchResponse({ url: '/search', title: 'Search', children: tracks })
8
+ parseSearchResponse({
9
+ path: '/search',
10
+ title: 'Search',
11
+ children: tracks
12
+ })
9
13
  ).toEqual(tracks)
10
14
  })
11
15
 
@@ -15,7 +19,9 @@ describe('parseSearchResponse', () => {
15
19
  })
16
20
 
17
21
  it('returns [] for a page object with no children', () => {
18
- expect(parseSearchResponse({ url: '/search', title: 'Search' })).toEqual([])
22
+ expect(parseSearchResponse({ path: '/search', title: 'Search' })).toEqual(
23
+ []
24
+ )
19
25
  })
20
26
 
21
27
  it('returns [] for null / non-object responses', () => {
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import type { ResolvedTrack, Track } from '../../types'
3
+ import { flattenSections, normalizePage, sectionContaining } from './sections'
4
+
5
+ const track = (src: string): Track => ({ title: src, src })
6
+
7
+ describe('normalizePage', () => {
8
+ it('wraps plain children into one untitled section', () => {
9
+ const page: ResolvedTrack = {
10
+ path: '/p',
11
+ title: 'P',
12
+ children: [track('a'), track('b')]
13
+ }
14
+ const normalized = normalizePage(page)
15
+ expect(normalized.sections).toEqual([
16
+ { children: [track('a'), track('b')] }
17
+ ])
18
+ expect(normalized.children).toBeUndefined()
19
+ })
20
+
21
+ it('prefers sections when present and strips children', () => {
22
+ const page: ResolvedTrack = {
23
+ path: '/p',
24
+ title: 'P',
25
+ sections: [{ title: 'S', children: [track('a')] }],
26
+ children: [track('zzz')]
27
+ }
28
+ const normalized = normalizePage(page)
29
+ expect(normalized.sections).toEqual([
30
+ { title: 'S', children: [track('a')] }
31
+ ])
32
+ expect(normalized.children).toBeUndefined()
33
+ })
34
+
35
+ it('leaves a childless page without sections', () => {
36
+ expect(normalizePage({ path: '/p', title: 'P' }).sections).toBeUndefined()
37
+ })
38
+ })
39
+
40
+ describe('flattenSections', () => {
41
+ it('concatenates children in section order', () => {
42
+ const flat = flattenSections([
43
+ { title: 'A', children: [track('a1'), track('a2')] },
44
+ { children: [track('b1')] }
45
+ ])
46
+ expect(flat.map((t) => t.src)).toEqual(['a1', 'a2', 'b1'])
47
+ })
48
+ })
49
+
50
+ describe('sectionContaining', () => {
51
+ const sections = [
52
+ { title: 'First', children: [track('dup'), track('x')] },
53
+ { title: 'Second', children: [track('y'), track('dup'), track('z')] }
54
+ ]
55
+
56
+ it('falls back to the first section containing the identity', () => {
57
+ const scoped = sectionContaining(sections, 'dup', undefined)
58
+ expect(scoped?.tracks.map((t) => t.src)).toEqual(['dup', 'x'])
59
+ expect(scoped?.tappedOffset).toBeUndefined()
60
+ })
61
+
62
+ it('pins the tapped section via the flat index', () => {
63
+ // Flat index 3 = second section, offset 1.
64
+ const scoped = sectionContaining(sections, 'dup', 3)
65
+ expect(scoped?.tracks.map((t) => t.src)).toEqual(['y', 'dup', 'z'])
66
+ expect(scoped?.tappedOffset).toBe(1)
67
+ })
68
+
69
+ it('ignores a stale index whose child no longer matches', () => {
70
+ const scoped = sectionContaining(sections, 'dup', 1)
71
+ expect(scoped?.tracks.map((t) => t.src)).toEqual(['dup', 'x'])
72
+ expect(scoped?.tappedOffset).toBeUndefined()
73
+ })
74
+
75
+ it('ignores an out-of-range index', () => {
76
+ const scoped = sectionContaining(sections, 'dup', 99)
77
+ expect(scoped?.tracks.map((t) => t.src)).toEqual(['dup', 'x'])
78
+ })
79
+
80
+ it('returns undefined for a vanished identity', () => {
81
+ expect(sectionContaining(sections, 'gone', 0)).toBeUndefined()
82
+ })
83
+
84
+ it('pins the exact copy of a within-section duplicate', () => {
85
+ const playlist = [
86
+ { children: [track('a'), track('b'), track('a'), track('c')] }
87
+ ]
88
+ const scoped = sectionContaining(playlist, 'a', 2)
89
+ expect(scoped?.tappedOffset).toBe(2)
90
+ })
91
+ })
@@ -0,0 +1,72 @@
1
+ import type { ResolvedTrack, Section, Track } from '../../types'
2
+ import { getTrackIdentity } from '../../utils/getTrackIdentity'
3
+
4
+ /**
5
+ * Normalizes a resolved page to the canonical sectioned shape (ADR 0010):
6
+ * `sections` wins when present; plain `children` is authoring sugar for one
7
+ * untitled section. The output never carries `children` — there is exactly
8
+ * one structure downstream code hydrates, transforms, and scopes against.
9
+ */
10
+ export function normalizePage(content: ResolvedTrack): ResolvedTrack {
11
+ const { children, sections, ...rest } = content
12
+ const normalized =
13
+ sections ?? (children !== undefined ? [{ children }] : undefined)
14
+ return { ...rest, sections: normalized }
15
+ }
16
+
17
+ /**
18
+ * The page's children concatenated in section order — the flattening that
19
+ * defines the contextual `__index` positions (ADR 0009/0010) and the flat
20
+ * views (tabs, search) of a sectioned page.
21
+ */
22
+ export function flattenSections(sections: Section[] | undefined): Track[] {
23
+ return sections?.flatMap((section) => section.children) ?? []
24
+ }
25
+
26
+ /**
27
+ * The page's tracks as one flat list, normalizing first — the flat view
28
+ * (tabs, search) of a page in either authored shape. Mirrors the native
29
+ * `flattenedChildren` accessors.
30
+ */
31
+ export function flattenedChildren(page: ResolvedTrack): Track[] {
32
+ return flattenSections(normalizePage(page).sections)
33
+ }
34
+
35
+ /**
36
+ * The section of the page containing the playable `trackId` (a track
37
+ * identity: id when non-blank, else src), or undefined when not found.
38
+ *
39
+ * `tappedIndex` — the flat page position stamped into the contextual URL —
40
+ * is a tie-breaker, never an identifier: when the child at that position
41
+ * still carries the tapped identity, it pins which section (and which copy,
42
+ * via `tappedOffset`) was tapped; when it doesn't (the list shifted),
43
+ * resolution falls back to the first section containing the identity. A
44
+ * stale index can therefore never select a different track — at worst a
45
+ * different copy of the same one.
46
+ */
47
+ export function sectionContaining(
48
+ sections: Section[],
49
+ trackId: string,
50
+ tappedIndex: number | undefined
51
+ ): { tracks: Track[]; tappedOffset?: number } | undefined {
52
+ if (tappedIndex !== undefined && tappedIndex >= 0) {
53
+ let start = 0
54
+ for (const section of sections) {
55
+ const offset = tappedIndex - start
56
+ if (offset < section.children.length) {
57
+ const tapped = section.children[offset]
58
+ if (tapped && getTrackIdentity(tapped) === trackId) {
59
+ return { tracks: section.children, tappedOffset: offset }
60
+ }
61
+ break
62
+ }
63
+ start += section.children.length
64
+ }
65
+ }
66
+ for (const section of sections) {
67
+ if (section.children.some((track) => getTrackIdentity(track) === trackId)) {
68
+ return { tracks: section.children }
69
+ }
70
+ }
71
+ return undefined
72
+ }
@@ -1,6 +1,7 @@
1
1
  import type { NowPlayingUpdate, NowPlayingMetadata } from '../../features'
2
2
  import type { Track } from '../../types'
3
3
  import { artworkUrl } from '../../utils/artwork'
4
+ import { getTrackIdentity } from '../../utils/getTrackIdentity'
4
5
 
5
6
  /**
6
7
  * Manages now playing metadata overrides.
@@ -37,7 +38,7 @@ export class NowPlayingManager {
37
38
  album: update.album ?? currentTrack.album,
38
39
  artwork: artworkUrl(currentTrack.artwork),
39
40
  description: currentTrack.description,
40
- mediaId: currentTrack.src ?? currentTrack.url,
41
+ mediaId: getTrackIdentity(currentTrack) ?? currentTrack.path,
41
42
  genre: currentTrack.genre,
42
43
  duration
43
44
  }
@@ -47,7 +48,7 @@ export class NowPlayingManager {
47
48
  album: currentTrack.album,
48
49
  artwork: artworkUrl(currentTrack.artwork),
49
50
  description: currentTrack.description,
50
- mediaId: currentTrack.src ?? currentTrack.url,
51
+ mediaId: getTrackIdentity(currentTrack) ?? currentTrack.path,
51
52
  genre: currentTrack.genre,
52
53
  duration
53
54
  }
@@ -74,7 +75,7 @@ export class NowPlayingManager {
74
75
  album: this.override?.album ?? currentTrack.album,
75
76
  artwork: artworkUrl(currentTrack.artwork),
76
77
  description: currentTrack.description,
77
- mediaId: currentTrack.src ?? currentTrack.url,
78
+ mediaId: getTrackIdentity(currentTrack) ?? currentTrack.path,
78
79
  genre: currentTrack.genre,
79
80
  duration
80
81
  }
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { BrowserPathHelper } from './BrowserPathHelper'
3
+
4
+ describe('BrowserPathHelper contextual URL round-trip', () => {
5
+ it('round-trips a src carrying its own query params', () => {
6
+ // A src carrying its own query string (signed CDN URL) must survive the
7
+ // build → extract/strip round-trip: an unescaped `&` would split the src
8
+ // into stray query params, truncating the trackId and polluting the
9
+ // parent path.
10
+ const src = 'https://cdn.example.com/stream.mp3?token=abc&exp=1699999999'
11
+ const url = BrowserPathHelper.build('/library', src)
12
+ expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
13
+ expect(BrowserPathHelper.stripTrackId(url)).toBe('/library')
14
+ })
15
+
16
+ it('round-trips a src with equals and plus', () => {
17
+ const src = 'https://cdn.example.com/a+b.mp3?sig=x=y'
18
+ const url = BrowserPathHelper.build('/library', src)
19
+ expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
20
+ expect(BrowserPathHelper.stripTrackId(url)).toBe('/library')
21
+ })
22
+
23
+ it('preserves unrelated parent query params when stripping', () => {
24
+ const src = 'https://cdn.example.com/stream.mp3?token=abc&exp=1'
25
+ const url = BrowserPathHelper.build('/search?q=jazz', src)
26
+ expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
27
+ expect(BrowserPathHelper.stripTrackId(url)).toBe('/search?q=jazz')
28
+ })
29
+
30
+ it('round-trips the stamped index alongside the trackId', () => {
31
+ const src = 'https://cdn.example.com/stream.mp3?token=abc&exp=1'
32
+ const url = BrowserPathHelper.build('/library', src, 3)
33
+ expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
34
+ expect(BrowserPathHelper.extractIndex(url)).toBe(3)
35
+ expect(BrowserPathHelper.stripTrackId(url)).toBe('/library')
36
+ })
37
+
38
+ it('extracts no index from an index-less contextual URL', () => {
39
+ const url = BrowserPathHelper.build('/library', 'song.mp3')
40
+ expect(BrowserPathHelper.extractIndex(url)).toBeUndefined()
41
+ })
42
+
43
+ it('extracts no index from a non-contextual or malformed-index URL', () => {
44
+ expect(BrowserPathHelper.extractIndex('/library')).toBeUndefined()
45
+ expect(
46
+ BrowserPathHelper.extractIndex('/library?__trackId=song.mp3&__index=x')
47
+ ).toBeUndefined()
48
+ expect(
49
+ BrowserPathHelper.extractIndex('/library?__trackId=song.mp3&__index=-1')
50
+ ).toBeUndefined()
51
+ })
52
+ })
@@ -6,8 +6,12 @@
6
6
  * 1. System paths (prefixed with `/__`): Root, recent, and search paths
7
7
  * 2. Contextual URLs: Embed parent context in track identifiers for Media3 integration
8
8
  *
9
- * Contextual URL format: `{parentPath}?__trackId={trackSrc}`
10
- * Example: "/library/radio?__trackId=song.mp3"
9
+ * Contextual URL format: `{parentPath}?__trackId={trackIdentity}&__index={childIndex}`
10
+ * Example: "/library/radio?__trackId=song.mp3&__index=2"
11
+ *
12
+ * `__trackId` is the identity check; `__index` (the child's position on the
13
+ * page at stamp time) is only a tie-breaker between surfaces that carry the
14
+ * same identity — a stale index never selects a different track.
11
15
  *
12
16
  * This allows:
13
17
  * - Media3 to reference playable-only tracks (tracks with `src` but no `url`)
@@ -21,6 +25,9 @@ export const BrowserPathHelper = {
21
25
  /** Query parameter name for contextual track identifiers */
22
26
  CONTEXTUAL_TRACK_PARAM: '__trackId',
23
27
 
28
+ /** Query parameter name for the tapped child's page position (tie-breaker) */
29
+ CONTEXTUAL_INDEX_PARAM: '__index',
30
+
24
31
  /**
25
32
  * Create a search path for a given query
26
33
  */
@@ -43,13 +50,13 @@ export const BrowserPathHelper = {
43
50
  },
44
51
 
45
52
  /**
46
- * Strips the __trackId parameter from a contextual URL to get the parent path.
47
- * If the URL is not contextual, returns it unchanged.
53
+ * Strips the contextual parameters (__trackId and __index) from a contextual
54
+ * URL to get the parent path. If the URL is not contextual, returns it unchanged.
48
55
  *
49
56
  * @param url The URL to process
50
- * @returns The URL without the __trackId parameter
57
+ * @returns The URL without the contextual parameters
51
58
  *
52
- * Example: "/library/radio?__trackId=song.mp3" → "/library/radio"
59
+ * Example: "/library/radio?__trackId=song.mp3&__index=2" → "/library/radio"
53
60
  * Example: "/search?q=jazz&__trackId=song.mp3" → "/search?q=jazz"
54
61
  */
55
62
  stripTrackId(url: string): string {
@@ -62,8 +69,9 @@ export const BrowserPathHelper = {
62
69
  const isFullUrl = url.startsWith('http://') || url.startsWith('https://')
63
70
  const urlObj = new URL(url, isFullUrl ? undefined : 'http://placeholder')
64
71
 
65
- // Remove the __trackId parameter
72
+ // Remove the contextual parameters
66
73
  urlObj.searchParams.delete(this.CONTEXTUAL_TRACK_PARAM)
74
+ urlObj.searchParams.delete(this.CONTEXTUAL_INDEX_PARAM)
67
75
 
68
76
  // Return the appropriate format
69
77
  if (isFullUrl) {
@@ -75,7 +83,7 @@ export const BrowserPathHelper = {
75
83
  } catch {
76
84
  // Fallback: simple string manipulation
77
85
  const paramPattern = new RegExp(
78
- `[?&]${this.CONTEXTUAL_TRACK_PARAM}=[^&]*`,
86
+ `[?&](?:${this.CONTEXTUAL_TRACK_PARAM}|${this.CONTEXTUAL_INDEX_PARAM})=[^&]*`,
79
87
  'g'
80
88
  )
81
89
  let result = url.replace(paramPattern, '')
@@ -87,6 +95,24 @@ export const BrowserPathHelper = {
87
95
  }
88
96
  },
89
97
 
98
+ /**
99
+ * Builds a contextual URL by appending a track identifier — and optionally
100
+ * the tapped child's page position — to a parent path.
101
+ *
102
+ * @param parentPath The parent container path
103
+ * @param trackId The track identity (`id` when non-blank, else `src`)
104
+ * @param index The child's position on the page at stamp time (tie-breaker)
105
+ * @returns A contextual URL combining parent path, track ID, and index
106
+ *
107
+ * Example: build("/library", "song.mp3", 2) → "/library?__trackId=song.mp3&__index=2"
108
+ */
109
+ build(parentPath: string, trackId: string, index?: number): string {
110
+ const separator = parentPath.includes('?') ? '&' : '?'
111
+ const indexParam =
112
+ index === undefined ? '' : `&${this.CONTEXTUAL_INDEX_PARAM}=${index}`
113
+ return `${parentPath}${separator}${this.CONTEXTUAL_TRACK_PARAM}=${encodeURIComponent(trackId)}${indexParam}`
114
+ },
115
+
90
116
  /**
91
117
  * Extracts the track ID from a contextual URL.
92
118
  * Returns undefined if the URL is not contextual or doesn't contain the track ID parameter.
@@ -100,18 +126,40 @@ export const BrowserPathHelper = {
100
126
  if (!this.isContextual(path)) {
101
127
  return undefined
102
128
  }
129
+ return this.extractParam(path, this.CONTEXTUAL_TRACK_PARAM)
130
+ },
131
+
132
+ /**
133
+ * Extracts the stamped page index from a contextual URL, or undefined when
134
+ * the URL is not contextual or carries no (valid, non-negative) index.
135
+ *
136
+ * Example: "/library/radio?__trackId=song.mp3&__index=2" → 2
137
+ */
138
+ extractIndex(path: string): number | undefined {
139
+ if (!this.isContextual(path)) {
140
+ return undefined
141
+ }
142
+
143
+ const raw = this.extractParam(path, this.CONTEXTUAL_INDEX_PARAM)
144
+ if (raw === undefined || !/^\d+$/.test(raw)) {
145
+ return undefined
146
+ }
147
+ return Number.parseInt(raw, 10)
148
+ },
103
149
 
150
+ /**
151
+ * Extracts a single query parameter, handling both full URLs and paths,
152
+ * with a regex fallback for input the URL parser rejects.
153
+ */
154
+ extractParam(path: string, param: string): string | undefined {
104
155
  try {
105
- // Parse the URL - handle both full URLs and paths
106
156
  const isFullUrl =
107
157
  path.startsWith('http://') || path.startsWith('https://')
108
158
  const urlObj = new URL(path, isFullUrl ? undefined : 'http://placeholder')
109
- return urlObj.searchParams.get(this.CONTEXTUAL_TRACK_PARAM) ?? undefined
159
+ return urlObj.searchParams.get(param) ?? undefined
110
160
  } catch {
111
161
  // Fallback: regex extraction
112
- const match = path.match(
113
- new RegExp(`[?&]${this.CONTEXTUAL_TRACK_PARAM}=([^&]*)`)
114
- )
162
+ const match = path.match(new RegExp(`[?&]${param}=([^&]*)`))
115
163
  return match?.[1] ? decodeURIComponent(match[1]) : undefined
116
164
  }
117
165
  },
@@ -1,19 +0,0 @@
1
- import Foundation
2
- import NitroModules
3
-
4
- // MARK: - ImageRowItem → Track Conversion
5
-
6
- extension ImageRowItem {
7
- /// Creates a minimal Track from an ImageRowItem for reuse with item selection and artwork loading.
8
- /// A `src`-bearing item becomes a playable track (thumbnail tap plays it); otherwise the
9
- /// track only navigates via `url`.
10
- func toTrack() -> Track {
11
- Track(
12
- id: id, url: url, src: src, artwork: artwork.map { .first($0) },
13
- artworkSource: artworkSource, request: request,
14
- artworkCarPlayTinted: nil, title: title, subtitle: nil, artist: artist,
15
- albumUrl: albumUrl, album: album, description: nil, genre: nil, duration: nil, style: nil,
16
- childrenStyle: nil, favorited: nil, groupTitle: nil, live: live, imageRow: nil,
17
- )
18
- }
19
- }
@@ -1,58 +0,0 @@
1
- ///
2
- /// JFavoriteConfig.hpp
3
- /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
- /// https://github.com/mrousavy/nitro
5
- /// Copyright © Marc Rousavy @ Margelo
6
- ///
7
-
8
- #pragma once
9
-
10
- #include <fbjni/fbjni.h>
11
- #include "FavoriteConfig.hpp"
12
-
13
- #include "FavoritesMatchMode.hpp"
14
- #include "JFavoritesMatchMode.hpp"
15
-
16
- namespace margelo::nitro::audiobrowser {
17
-
18
- using namespace facebook;
19
-
20
- /**
21
- * The C++ JNI bridge between the C++ struct "FavoriteConfig" and the the Kotlin data class "FavoriteConfig".
22
- */
23
- struct JFavoriteConfig final: public jni::JavaClass<JFavoriteConfig> {
24
- public:
25
- static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/audiobrowser/FavoriteConfig;";
26
-
27
- public:
28
- /**
29
- * Convert this Java/Kotlin-based struct to the C++ struct FavoriteConfig by copying all values to C++.
30
- */
31
- [[maybe_unused]]
32
- [[nodiscard]]
33
- FavoriteConfig toCpp() const {
34
- static const auto clazz = javaClassStatic();
35
- static const auto fieldMatch = clazz->getField<JFavoritesMatchMode>("match");
36
- jni::local_ref<JFavoritesMatchMode> match = this->getFieldValue(fieldMatch);
37
- return FavoriteConfig(
38
- match->toCpp()
39
- );
40
- }
41
-
42
- public:
43
- /**
44
- * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java.
45
- */
46
- [[maybe_unused]]
47
- static jni::local_ref<JFavoriteConfig::javaobject> fromCpp(const FavoriteConfig& value) {
48
- using JSignature = JFavoriteConfig(jni::alias_ref<JFavoritesMatchMode>);
49
- static const auto clazz = javaClassStatic();
50
- static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
51
- return create(
52
- clazz,
53
- JFavoritesMatchMode::fromCpp(value.match)
54
- );
55
- }
56
- };
57
-
58
- } // namespace margelo::nitro::audiobrowser