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
@@ -34,6 +34,24 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
34
34
  /// silent.
35
35
  static let instanceChangedEmitter = Emitter<Void>()
36
36
 
37
+ /// Installed once, on first instance creation. React Native posts this
38
+ /// notification (from `RCTReloadCommand`) when a JS reload — dev or
39
+ /// OTA-update restart — is triggered, *before* the old runtime is destroyed:
40
+ /// the earliest signal that this instance's JS callbacks are about to go
41
+ /// stale. `init` of the replacement instance drops them too, but only after
42
+ /// the new runtime is up; this observer closes the window in between.
43
+ /// Observed by name so the library needs no React-Core dependency (and
44
+ /// stays inert under SPM tests, where nothing posts it).
45
+ @MainActor private static let reloadObserver: NSObjectProtocol = NotificationCenter.default.addObserver(
46
+ forName: Notification.Name("RCTTriggerReloadCommandNotification"),
47
+ object: nil,
48
+ queue: .main,
49
+ ) { _ in
50
+ MainActor.assumeIsolated {
51
+ HybridAudioBrowser.shared?.dropStaleJSCallbacks()
52
+ }
53
+ }
54
+
37
55
  // MARK: - Private Properties
38
56
 
39
57
  /// Main-confined by convention, not annotation: `deinit` reads it as a
@@ -70,6 +88,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
70
88
  playerOptions.carPlayNowPlayingButtons
71
89
  }
72
90
 
91
+ @MainActor var carPlayPlayingIndicatorLocation: CarPlayPlayingIndicatorLocation {
92
+ playerOptions.carPlayPlayingIndicatorLocation
93
+ }
94
+
73
95
  /// Written from the JS thread, the cooperative pool and MainActor, with a
74
96
  /// `didSet` that calls back into Nitro. MainActor-isolated so the compiler
75
97
  /// requires an `onMainActor` hop at every access.
@@ -164,7 +186,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
164
186
  public var configuration: NativeBrowserConfiguration = .init(
165
187
  path: nil, request: nil, requestResolver: nil, browse: nil, browseResolver: nil, media: nil, artwork: nil, nowPlayingArtwork: nil, routes: nil,
166
188
  singleTrack: nil, handleTrackLoad: nil,
167
- androidControllerOfflineError: nil, carPlayLoadingTitle: nil, resolveAlbumUrl: nil, formatNavigationError: nil,
189
+ androidControllerOfflineError: nil, carPlayLoadingTitle: nil, resolveAlbumPath: nil, formatNavigationError: nil,
168
190
  ) {
169
191
  didSet {
170
192
  // Copied here, on the setter's thread: the Task below outlives this call,
@@ -181,7 +203,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
181
203
  Task {
182
204
  let tabs = try? await browserManager.queryTabs()
183
205
  // Navigate to configured path, first tab, or "/"
184
- let initialPath = configuredPath ?? tabs?.first?.url ?? "/"
206
+ let initialPath = configuredPath ?? tabs?.first?.path ?? "/"
185
207
  // Clear error before navigation (matches Kotlin clearNavigationError())
186
208
  clearNavigationError()
187
209
  do {
@@ -213,8 +235,14 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
213
235
  /// state, e.g. CarPlay re-seeding `isGated` at scene connect). Serving content
214
236
  /// in that window would be a fail-open leak — same direction as the
215
237
  /// resolver-error path in `gateDecision`. Matches Android's default.
216
- public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = { _ in
217
- Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
238
+ public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = HybridAudioBrowser.defaultResolveGate
239
+
240
+ // Computed, not stored: a nonisolated static holding a non-Sendable closure
241
+ // is rejected under Swift 6 concurrency checking.
242
+ static var defaultResolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> {
243
+ { _ in
244
+ Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
245
+ }
218
246
  }
219
247
 
220
248
  /// Fired when a request is gated (served the gate). Set by JS.
@@ -306,7 +334,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
306
334
  // playerAndConfiguredBrowser OnceValue, so we must explicitly stop
307
335
  // its player to prevent two audio streams running simultaneously.
308
336
  onMainActor {
337
+ // Statics are lazy — touch the observer so it's installed from the first
338
+ // instance onward.
339
+ _ = HybridAudioBrowser.reloadObserver
309
340
  HybridAudioBrowser.shared?.player?.destroy()
341
+ HybridAudioBrowser.shared?.dropStaleJSCallbacks()
310
342
  playerAndConfiguredBrowser.reset()
311
343
  HybridAudioBrowser.shared = self
312
344
  // Tell connected external controllers (CarPlay) to re-subscribe against
@@ -319,7 +351,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
319
351
  setupBrowserCallbacks()
320
352
  }
321
353
 
354
+ /// Drops every JS-provided callback that native code can invoke on its own
355
+ /// initiative (browse resolvers, `handleTrackLoad`, `resolveAlbumPath`,
356
+ /// `formatNavigationError`, `resolveGate`, the now-playing formatter).
357
+ ///
358
+ /// Called on the *previous* instance when a new one takes over: its JS runtime
359
+ /// is gone, and invoking a callback from a dead runtime aborts the process —
360
+ /// Nitro's `AsyncJSCallback` throws on a destroyed Dispatcher, and the throw
361
+ /// crosses the generated bridge wrapper's `noexcept` boundary. Native code
362
+ /// (e.g. a CarPlay controller still bound to this instance mid-reload) keeps
363
+ /// working off the data-only config with built-in fallbacks; `resolveGate`
364
+ /// reverts to the fail-closed default it documents for exactly this window.
365
+ /// One-shot: on a normal reload both triggers hit the same dying instance
366
+ /// (the reload notification, then the replacement's `init`), and the second
367
+ /// strip would re-fire the config-setter cascade — another `onConfigChanged`
368
+ /// → CarPlay now-playing button rebuild — for nothing.
369
+ @MainActor private var didDropJSCallbacks = false
370
+
371
+ @MainActor
372
+ private func dropStaleJSCallbacks() {
373
+ guard !didDropJSCallbacks else { return }
374
+ didDropJSCallbacks = true
375
+ // Guarded: assigning `config` marks the browser configured (the setter's
376
+ // didSet), and a never-configured browser must keep cold-start waiters
377
+ // (CarPlay, Siri) blocked on the readiness gate rather than resolve them
378
+ // against an empty config.
379
+ if browserManager.isConfigured {
380
+ browserManager.config = browserManager.config.strippingJSCallbacks()
381
+ }
382
+ resolveGate = Self.defaultResolveGate
383
+ nowPlayingMetadataFormatter = nil
384
+ }
385
+
322
386
  deinit {
387
+ // Block-based notification observers are retained by the center until an
388
+ // explicit removeObserver — observer death isn't detected — so without this
389
+ // every JS reload strands another token + closure. They capture `self`
390
+ // weakly, so a stranded one no-ops rather than misbehaving; what accumulates
391
+ // is the tokens and a wasted main-queue dispatch per interruption / route
392
+ // change / media-services reset. (`volumeObservation` needs no counterpart:
393
+ // NSKeyValueObservation invalidates itself when this instance releases it.)
394
+ for observer in [interruptionObserver, routeChangeObserver, mediaServicesResetObserver]
395
+ .compactMap(\.self)
396
+ {
397
+ NotificationCenter.default.removeObserver(observer)
398
+ }
399
+
323
400
  // Safety net: ensure the AVPlayer is stopped if this instance is deallocated.
324
401
  let player = self.player
325
402
  if Thread.isMainThread {
@@ -447,8 +524,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
447
524
  executePlayback(intent)
448
525
  case .intercepted:
449
526
  break
450
- case let .browse(url):
451
- navigateToBrowsableUrl(url)
527
+ case let .browse(path):
528
+ navigateToBrowsablePath(path)
452
529
  case .none:
453
530
  break
454
531
  }
@@ -472,13 +549,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
472
549
  }
473
550
  }
474
551
 
475
- private func navigateToBrowsableUrl(_ url: String) {
552
+ private func navigateToBrowsablePath(_ path: String) {
476
553
  clearNavigationError()
477
554
  Task {
478
555
  do {
479
- try await browserManager.navigate(url)
556
+ try await browserManager.navigate(path)
480
557
  } catch {
481
- handleNavigationError(error, path: url)
558
+ handleNavigationError(error, path: path)
482
559
  }
483
560
  }
484
561
  }
@@ -487,7 +564,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
487
564
  Promise.async { [weak self] in
488
565
  guard let self else { return [] }
489
566
  let resolved = try await browserManager.search(query)
490
- return resolved.children ?? []
567
+ return resolved.flattenedChildren ?? []
491
568
  }
492
569
  }
493
570
 
@@ -510,7 +587,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
510
587
  static let invalidateAllSentinel = "__rnab_invalidate_all__"
511
588
 
512
589
  public func notifyContentChanged(path: String) throws {
513
- onMainActor { browserManager.invalidateContentCache(path) }
590
+ // Throws on a contextual path (matching Android's `require`) — the caller
591
+ // must pass the container path, or the invalidation silently hits nothing.
592
+ try onMainActor { try browserManager.invalidateContentCache(path) }
514
593
 
515
594
  // Notify external controllers (CarPlay) that content changed
516
595
  externalContentChangedEmitter.emit(path)
@@ -690,6 +769,16 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
690
769
  guard let self else { return }
691
770
  self.applyAudioSessionCategory = applyCategory
692
771
 
772
+ // A re-setup REPLACES the player, so the outgoing one has to be torn down
773
+ // and un-published first. `TrackPlayer` has no deinit: dropping the
774
+ // reference alone would leave its AVPlayer and MPNowPlayingSession alive.
775
+ // And `playerAndConfiguredBrowser` holds the old player strongly — its
776
+ // `resolve` is a one-shot, so without the reset the gate would keep
777
+ // handing the dead player to the CarPlay controller and Siri intents,
778
+ // which both await it. No-op on first setup.
779
+ player?.destroy()
780
+ playerAndConfiguredBrowser.reset()
781
+
693
782
  // Create player with self as callbacks delegate
694
783
  player = TrackPlayer(callbacks: self)
695
784
 
@@ -739,11 +828,16 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
739
828
  return await browserManager.resolveMediaUrl(src, track: track)
740
829
  }
741
830
 
742
- // Configure artwork URL resolver for Now Playing (with size context)
831
+ // Configure artwork URL resolver for Now Playing (with size context). The
832
+ // `nowPlayingArtwork` config runs for EVERY playing track, id or not — an id-less track
833
+ // leaves any `{id}` token unfilled (resolveArtworkUrl warns and the request visibly
834
+ // 404s) rather than skipping the config.
743
835
  player?.nowPlayingUpdater.artworkUrlResolver = { [weak self] track, imageContext in
744
836
  guard let self else { return nil }
745
- let perRoute = (track.id?.isEmpty == false) ? browserManager.config.nowPlayingArtwork : nil
746
- return await browserManager.resolveArtworkUrl(track: track, perRouteConfig: perRoute, imageContext: imageContext)
837
+ return await browserManager.resolveArtworkUrl(
838
+ track: track, perRouteConfig: browserManager.config.nowPlayingArtwork,
839
+ imageContext: imageContext,
840
+ )
747
841
  }
748
842
 
749
843
  // Emit the JS `onNowPlayingChanged` event when the rendered text lines change. The updater
@@ -784,6 +878,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
784
878
  if let intent = options.playWhenReady { pendingPlayWhenReady = intent }
785
879
  applyPendingPlayerState()
786
880
 
881
+ // Push the resolved event cadence unconditionally. `updateOptions` applies
882
+ // the interval only when it *differs* from the stored one, but the store
883
+ // outlives the player — so on a re-setup with unchanged options the new
884
+ // player would come up with no progress timer at all.
885
+ player?.setProgressUpdateInterval(playerOptions.progressUpdateEventInterval)
886
+ player?.setPlaybackIntervalEnabled(playbackIntervalEnabled)
887
+
787
888
  // Notify listeners that player is ready (e.g., CarPlay)
788
889
  playerAndConfiguredBrowser.check()
789
890
  }
@@ -813,9 +914,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
813
914
  // Update stored options
814
915
  playerOptions.update(from: options)
815
916
 
816
- // Propagate the favorite match mode to the browser so it can hydrate
917
+ // Propagate the favorite flag to the browser so it can hydrate
817
918
  // row hearts (the `favorite` capability is the single favoriting switch).
818
- browserManager.setFavoriteMatch(playerOptions.capabilities.favoriteMatch)
919
+ browserManager.setFavoriteEnabled(playerOptions.capabilities.favoriteEnabled)
819
920
 
820
921
  // Apply remote commands to player
821
922
  applyRemoteCommands()
@@ -848,8 +949,16 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
848
949
  onMainActor { playerOptions.toOptions() }
849
950
  }
850
951
 
952
+ /// Stored so a re-setup can re-apply it: unlike the other runtime options this
953
+ /// one has no home in `playerOptions`, and the player it was pushed to does not
954
+ /// survive `setupPlayer`.
955
+ @MainActor private var playbackIntervalEnabled = false
956
+
851
957
  public func setPlaybackIntervalEnabled(enabled: Bool) {
852
- onMainActor { player?.setPlaybackIntervalEnabled(enabled) }
958
+ onMainActor {
959
+ playbackIntervalEnabled = enabled
960
+ player?.setPlaybackIntervalEnabled(enabled)
961
+ }
853
962
  }
854
963
 
855
964
  // MARK: - Playback Control
@@ -1063,24 +1172,25 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1063
1172
  }
1064
1173
 
1065
1174
  public func setActiveTrackFavorited(favorited: Bool) throws {
1066
- onMainActor {
1067
- guard let track = player?.currentTrack, let src = track.src else { return }
1175
+ try onMainActor {
1176
+ guard let track = player?.currentTrack, let identity = track.identity else { return }
1068
1177
  guard let index = player?.currentIndex, index >= 0 else { return }
1069
- // Optimistically reflect in the authoritative match set so the now-playing
1070
- // heart (hydrated from it in getActiveTrack) flips immediately; native
1071
- // reconciles to its canonical ids on the next setFavorites.
1072
- browserManager.setFavorited(src: src, favorited: favorited)
1178
+ // Optimistically reflect in the authoritative favorite set so the
1179
+ // now-playing heart (hydrated from it in getActiveTrack) flips
1180
+ // immediately; the consumer reconciles to its canonical identities on
1181
+ // the next setFavorites.
1182
+ browserManager.setFavorited(identity: identity, favorited: favorited)
1073
1183
  let updatedTrack = track.copying(favorited: favorited)
1184
+ // Hydration never overrides a caller-set `favorited`, so the queued
1185
+ // track must itself carry the latest explicit value (as Android's tag
1186
+ // swap and web's replaceTrack do).
1187
+ try player?.replace(index, updatedTrack)
1188
+ // The ONLY emit: a favorite toggle is an in-place mutation of the active
1189
+ // track, not a transition — onPlaybackActiveTrackChanged stays
1190
+ // transition-only, and the useActiveTrack / useQueue hooks subscribe to
1191
+ // onFavoriteChanged themselves. Native surfaces (CarPlay heart) observe
1192
+ // favoriteChangedEmitter / onFavoritesChanged directly.
1074
1193
  favoriteChangedEmitter.emit(FavoriteChangedEvent(track: updatedTrack, favorited: favorited))
1075
- // Fire active track changed so the now-playing heart + useActiveTrack() refresh.
1076
- let position = player?.currentTime ?? 0
1077
- activeTrackChangedEmitter.emit(PlaybackActiveTrackChangedEvent(
1078
- lastIndex: Double(index),
1079
- lastTrack: track,
1080
- lastPosition: position,
1081
- index: Double(index),
1082
- track: updatedTrack,
1083
- ))
1084
1194
  }
1085
1195
  }
1086
1196
 
@@ -1220,7 +1330,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1220
1330
  duration: track.duration,
1221
1331
  artwork: nowPlayingArtwork(for: track),
1222
1332
  description: track.description,
1223
- mediaId: track.src ?? track.url,
1333
+ mediaId: track.src ?? track.path,
1224
1334
  genre: track.genre,
1225
1335
  )
1226
1336
  }
@@ -1233,7 +1343,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1233
1343
  // Classify a stall by connectivity so the formatter can show "No internet connection" vs
1234
1344
  // "Reconnecting…" without a separate read. nil while not stalled.
1235
1345
  let stalled: StallReason? =
1236
- player.isStalled ? (networkMonitor.getOnline() ? .buffering : .offline) : nil
1346
+ player.isStalled ? (networkMonitor.isOnline ? .buffering : .offline) : nil
1237
1347
  player.nowPlayingUpdater.render(
1238
1348
  track: track,
1239
1349
  timedMetadata: latestTimedMetadata,
@@ -1249,7 +1359,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1249
1359
  // MARK: - Network
1250
1360
 
1251
1361
  public func getOnline() throws -> Bool {
1252
- networkMonitor.getOnline()
1362
+ // No `onMainActor` hop: `isOnline` is lock-protected, so this stays a
1363
+ // cheap read on the JS thread instead of blocking on the main queue.
1364
+ networkMonitor.isOnline
1253
1365
  }
1254
1366
 
1255
1367
  // MARK: - System Volume
@@ -1429,11 +1541,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1429
1541
  public func openOutputPicker() throws {
1430
1542
  DispatchQueue.main.async { [weak self] in
1431
1543
  guard let self else { return }
1432
- guard let windowScene = UIApplication.shared.connectedScenes
1433
- .compactMap({ $0 as? UIWindowScene })
1434
- .first(where: { $0.activationState == .foregroundActive }),
1435
- let window = windowScene.windows.first(where: { $0.isKeyWindow }),
1436
- var topController = window.rootViewController
1544
+ guard let windowScene = UIApplication.shared.activeWindowScene,
1545
+ let window = windowScene.windows.first(where: { $0.isKeyWindow }),
1546
+ var topController = window.rootViewController
1437
1547
  else {
1438
1548
  self.logger.error("openOutputPicker: no active window scene / root view controller")
1439
1549
  return
@@ -1809,14 +1919,25 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1809
1919
  browser.showNowPlayingRequestedEmitter.emit(())
1810
1920
  completion(true)
1811
1921
  } else if let state = browser.playbackStateStore.load() {
1812
- browser.logger.info("resume: cold — restoring persisted track")
1813
1922
  let track = state.track.toNitro()
1923
+ // The persisted track can carry `disabled` — it became unavailable
1924
+ // after it was saved. A disabled track never plays, resumption
1925
+ // included (Track.disabled).
1926
+ if track.disabled == true {
1927
+ browser.logger.info("resume: persisted track is disabled — refusing")
1928
+ completion(false)
1929
+ return
1930
+ }
1931
+ browser.logger.info("resume: cold — restoring persisted track")
1932
+ // Repeat / shuffle / speed come back before the queue is built — see
1933
+ // `restorePlaybackSettings(from:)` for why that ordering matters.
1934
+ player.restorePlaybackSettings(from: state)
1814
1935
  let startMs = (track.live == true) ? nil : state.positionMs
1815
1936
  // Match Android resume: re-expand the full queue from the track's contextual
1816
- // URL (parent container → siblings + selected index). Fall back to the single
1817
- // track when the url isn't contextual or expansion fails.
1818
- if let url = state.track.url,
1819
- let expanded = try? await browser.browserManager.expandQueueFromContextualUrl(url)
1937
+ // path (parent container → siblings + selected index). Fall back to the single
1938
+ // track when the path isn't contextual or expansion fails.
1939
+ if let path = state.track.path,
1940
+ let expanded = try? await browser.browserManager.expandQueueFromContextualPath(path)
1820
1941
  {
1821
1942
  player.setQueue(expanded.tracks, initialIndex: expanded.selectedIndex, startPositionMs: startMs, playWhenReady: true)
1822
1943
  } else {
@@ -30,18 +30,18 @@
30
30
 
31
31
  struct Track: Equatable {
32
32
  var id: String?
33
- var url: String?
33
+ var path: String?
34
34
  var src: String?
35
+ var artwork: Variant_String_ArtworkVariants?
36
+ var artworkSource: ImageSource?
35
37
  var request: TrackRequest?
36
38
  var title: String = ""
37
39
  var artist: String?
38
- var albumUrl: String?
40
+ var albumPath: String?
39
41
  var album: String?
42
+ var style: TrackStyle?
43
+ var disabled: Bool?
40
44
  var live: Bool?
41
- var artwork: Variant_String_ArtworkVariants?
42
- var artworkSource: ImageSource?
43
- var groupTitle: String?
44
- var imageRow: [ImageRowItem]?
45
45
  }
46
46
 
47
47
  struct TrackLoadEvent {
@@ -86,7 +86,7 @@
86
86
  var body: String?
87
87
  }
88
88
 
89
- enum TrackStyle {
89
+ enum StyleDisplay: Equatable {
90
90
  case list
91
91
  case grid
92
92
 
@@ -99,17 +99,106 @@
99
99
  }
100
100
  }
101
101
 
102
- struct ImageRowItem: Equatable {
103
- var id: String?
104
- var url: String?
105
- var src: String?
106
- var artwork: String?
107
- var artworkSource: ImageSource?
108
- var title: String
102
+ enum ArtworkRendering: Equatable {
103
+ case original
104
+ case stencil
105
+
106
+ init?(fromString string: String) {
107
+ switch string {
108
+ case "original": self = .original
109
+ case "stencil": self = .stencil
110
+ default: return nil
111
+ }
112
+ }
113
+ }
114
+
115
+ enum ImageShape: Equatable {
116
+ case circular
117
+ case roundedRectangle
118
+
119
+ init?(fromString string: String) {
120
+ switch string {
121
+ case "circular": self = .circular
122
+ case "rounded-rectangle": self = .roundedRectangle
123
+ default: return nil
124
+ }
125
+ }
126
+ }
127
+
128
+ enum GridTile: Equatable {
129
+ case plain
130
+ case card
131
+ case condensed
132
+
133
+ init?(fromString string: String) {
134
+ switch string {
135
+ case "plain": self = .plain
136
+ case "card": self = .card
137
+ case "condensed": self = .condensed
138
+ default: return nil
139
+ }
140
+ }
141
+
142
+ // Mirrors the generated extension (nitrogen emits `stringValue` beside
143
+ // `init?(fromString:)` for every string-backed enum).
144
+ var stringValue: String {
145
+ switch self {
146
+ case .plain: "plain"
147
+ case .card: "card"
148
+ case .condensed: "condensed"
149
+ }
150
+ }
151
+ }
152
+
153
+ enum CardImage: Equatable {
154
+ case normal
155
+ case background
156
+
157
+ init?(fromString string: String) {
158
+ switch string {
159
+ case "normal": self = .normal
160
+ case "background": self = .background
161
+ default: return nil
162
+ }
163
+ }
164
+ }
165
+
166
+ struct TrackStyle: Equatable {
167
+ var display: StyleDisplay?
168
+ var artworkRendering: ArtworkRendering?
169
+ var imageShape: ImageShape?
170
+ var accessorySymbol: String?
171
+ var cardTint: String?
172
+ var cardImage: CardImage?
173
+ }
174
+
175
+ // Member order matches the generated init (own properties before inherited
176
+ // ones — Nitro flattens `extends` own-props-first): the memberwise init this
177
+ // produces must accept the same argument order as the real type, or
178
+ // `swift test` passes while the app build breaks on argument order.
179
+ struct SectionStyle: Equatable {
180
+ var gridWrap: Bool?
181
+ var gridTile: GridTile?
182
+ var display: StyleDisplay?
183
+ var artworkRendering: ArtworkRendering?
184
+ var imageShape: ImageShape?
185
+ var accessorySymbol: String?
186
+ var cardTint: String?
187
+ var cardImage: CardImage?
188
+ }
189
+
190
+ struct Section: Equatable {
191
+ var title: String?
192
+ var subtitle: String?
193
+ var style: SectionStyle?
194
+ var path: String?
195
+ var children: [Track]
109
196
  }
110
197
 
111
198
  struct ResolvedTrack: Equatable {
112
- var url: String
199
+ var path: String
200
+ var style: SectionStyle?
201
+ var sections: [Section]?
113
202
  var children: [Track]?
114
203
  var carPlaySiriListButton: CarPlaySiriListButtonPosition?
115
204
  var id: String?
@@ -117,21 +206,17 @@
117
206
  var artwork: Variant_String_ArtworkVariants?
118
207
  var artworkSource: ImageSource?
119
208
  var request: TrackRequest?
120
- var artworkCarPlayTinted: Bool?
121
209
  var title: String
122
210
  var subtitle: String?
123
211
  var artist: String?
124
- var albumUrl: String?
212
+ var albumPath: String?
125
213
  var album: String?
126
214
  var description: String?
127
215
  var genre: String?
128
216
  var duration: Double?
129
- var style: TrackStyle?
130
- var childrenStyle: TrackStyle?
217
+ var disabled: Bool?
131
218
  var favorited: Bool?
132
- var groupTitle: String?
133
219
  var live: Bool?
134
- var imageRow: [ImageRowItem]?
135
220
  }
136
221
 
137
222
  enum PlaybackState: Equatable {
@@ -32,6 +32,9 @@ class PlayerUpdateOptions {
32
32
  /// Custom buttons for the CarPlay Now Playing screen
33
33
  var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] = []
34
34
 
35
+ /// Where the now-playing indicator draws on CarPlay list rows
36
+ var carPlayPlayingIndicatorLocation: CarPlayPlayingIndicatorLocation = .leading
37
+
35
38
  // MARK: - Initialization
36
39
 
37
40
  init() {}
@@ -74,6 +77,9 @@ class PlayerUpdateOptions {
74
77
  if let buttons = ios.carPlayNowPlayingButtons {
75
78
  carPlayNowPlayingButtons = buttons
76
79
  }
80
+ if let location = ios.carPlayPlayingIndicatorLocation {
81
+ carPlayPlayingIndicatorLocation = location
82
+ }
77
83
  }
78
84
  }
79
85
 
@@ -92,6 +98,7 @@ class PlayerUpdateOptions {
92
98
  playbackRates: playbackRates,
93
99
  carPlayUpNextButton: carPlayUpNextButton,
94
100
  carPlayNowPlayingButtons: carPlayNowPlayingButtons,
101
+ carPlayPlayingIndicatorLocation: carPlayPlayingIndicatorLocation,
95
102
  ),
96
103
  )
97
104
  }
@@ -126,14 +126,22 @@ public struct MediaIntentCriteria: Sendable {
126
126
 
127
127
  /// Whether `query` is effectively the host app's own name. Case- and
128
128
  /// diacritic-insensitive.
129
+ ///
130
+ /// Whole-word subset match in either direction. Siri may deliver only part of
131
+ /// the name ("Name" for «App Name»), so a partial match must count — but a
132
+ /// substring match swallowed any query appearing inside the app name, making
133
+ /// one-word stations unreachable by voice.
129
134
  private static func queryMatchesAppName(_ query: String, appName: String?) -> Bool {
130
135
  guard let appName else { return false }
131
- let normalize: (String) -> String = {
132
- $0.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: nil)
133
- .trimmingCharacters(in: .whitespacesAndNewlines)
136
+ let tokens: (String) -> Set<String> = {
137
+ Set(
138
+ $0.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: nil)
139
+ .split(whereSeparator: { !$0.isLetter && !$0.isNumber })
140
+ .map(String.init),
141
+ )
134
142
  }
135
- let q = normalize(query), a = normalize(appName)
143
+ let q = tokens(query), a = tokens(appName)
136
144
  guard !q.isEmpty, !a.isEmpty else { return false }
137
- return a.contains(q) || q.contains(a)
145
+ return q.isSubset(of: a) || a.isSubset(of: q)
138
146
  }
139
147
  }