react-native-audio-browser 0.2.0-next.2 → 0.2.0-next.20

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 (660) hide show
  1. package/README.md +35 -21
  2. package/android/build.gradle +35 -12
  3. package/android/gradle.properties +2 -2
  4. package/android/src/main/AndroidManifest.xml +3 -1
  5. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +389 -164
  6. package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
  7. package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
  8. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
  9. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
  10. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +24 -5
  11. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
  12. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
  13. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
  14. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
  15. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
  16. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  17. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  18. package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
  19. package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
  20. package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
  21. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
  22. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
  23. package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
  24. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
  25. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
  26. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
  27. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
  28. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
  29. package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
  30. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  31. package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
  32. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
  33. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  34. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
  35. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
  36. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  37. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
  38. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
  39. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
  40. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +56 -40
  41. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  42. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
  43. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
  44. package/android/src/main/res/values/strings.xml +25 -5
  45. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
  46. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
  47. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  48. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
  49. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
  50. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  51. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
  52. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
  53. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
  54. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
  55. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
  56. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  57. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  58. package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
  59. package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
  60. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
  61. package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
  62. package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
  63. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  64. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
  65. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  66. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
  67. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  68. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
  69. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
  70. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  71. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
  72. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  73. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
  74. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  75. package/ios/Browser/BrowserConfig.swift +102 -5
  76. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  77. package/ios/Browser/BrowserManager.swift +230 -205
  78. package/ios/Browser/BrowserPathHelper.swift +44 -44
  79. package/ios/Browser/InertStyleDiagnostic.swift +279 -0
  80. package/ios/Browser/JsonModels.swift +264 -106
  81. package/ios/Browser/SearchDrillIn.swift +7 -7
  82. package/ios/Browser/SectionPresentation.swift +64 -0
  83. package/ios/Browser/SectionScope.swift +58 -24
  84. package/ios/Browser/StyleResolver.swift +58 -0
  85. package/ios/Browser/TabBarEntries.swift +9 -3
  86. package/ios/Browser/TrackIdTemplate.swift +37 -0
  87. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  88. package/ios/CarPlay/CarPlayController.swift +227 -85
  89. package/ios/CarPlay/CarPlayImageLoader.swift +42 -20
  90. package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
  91. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  92. package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
  93. package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
  94. package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
  95. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  96. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  97. package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
  98. package/ios/Extension/Section+Copying.swift +29 -0
  99. package/ios/Extension/String+IcyCharset.swift +21 -0
  100. package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
  101. package/ios/Extension/Track+Copying.swift +8 -12
  102. package/ios/Extension/Track+Identity.swift +18 -0
  103. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  104. package/ios/Http/HttpClient.swift +8 -2
  105. package/ios/HybridAudioBrowser.swift +253 -100
  106. package/ios/Model/NitroTypeStubs.swift +125 -23
  107. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  108. package/ios/Model/RemoteCommand.swift +18 -0
  109. package/ios/Model/TrackPlayerError.swift +2 -2
  110. package/ios/Option/SessionCategories.swift +3 -2
  111. package/ios/Player/MediaIntentCriteria.swift +17 -9
  112. package/ios/Player/MediaLoader.swift +33 -9
  113. package/ios/Player/NowPlayingUpdater.swift +54 -6
  114. package/ios/Player/PlaybackCoordinator.swift +13 -6
  115. package/ios/Player/PlaybackErrorHandler.swift +4 -4
  116. package/ios/Player/PlaybackTimer.swift +8 -2
  117. package/ios/Player/QueueManager.swift +46 -13
  118. package/ios/Player/RetryManager.swift +80 -18
  119. package/ios/Player/SeekCompletionHandler.swift +3 -1
  120. package/ios/Player/ShuffleOrder.swift +33 -0
  121. package/ios/Player/SleepTimerHandling.swift +13 -0
  122. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
  123. package/ios/RemoteCommand/RemoteCommandController.swift +62 -24
  124. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  125. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  126. package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
  127. package/ios/Tests/JsonModelsTests.swift +150 -1
  128. package/ios/Tests/LRUCacheTests.swift +111 -0
  129. package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
  130. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  131. package/ios/Tests/PlaybackCoordinatorTests.swift +1 -1
  132. package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
  133. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  134. package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
  135. package/ios/Tests/QueueManagerTests.swift +122 -10
  136. package/ios/Tests/RemoteCommandControllerTests.swift +172 -0
  137. package/ios/Tests/RemoteCommandTests.swift +67 -0
  138. package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
  139. package/ios/Tests/RetryManagerTests.swift +89 -0
  140. package/ios/Tests/SearchDrillInTests.swift +16 -16
  141. package/ios/Tests/SectionPresentationTests.swift +90 -0
  142. package/ios/Tests/SectionScopeTests.swift +87 -55
  143. package/ios/Tests/StyleResolverTests.swift +127 -0
  144. package/ios/Tests/Support/TestTypes.swift +8 -1
  145. package/ios/Tests/TabBarEntriesTests.swift +19 -19
  146. package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
  147. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  148. package/ios/Tests/TrackIdentityTests.swift +20 -0
  149. package/ios/Tests/TrackSelectorTests.swift +168 -33
  150. package/ios/TrackPlayer.swift +63 -35
  151. package/ios/TrackPlayerCallbacks.swift +5 -38
  152. package/ios/TrackSelector.swift +45 -30
  153. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  154. package/ios/Util/LRUCache.swift +4 -1
  155. package/ios/Util/NetworkMonitor.swift +43 -26
  156. package/ios/Util/SFSymbolRenderer.swift +24 -2
  157. package/ios/Util/SVGProcessor.swift +9 -2
  158. package/lib/commonjs/AudioBrowser.js +8 -8
  159. package/lib/commonjs/AudioBrowser.js.map +1 -1
  160. package/lib/commonjs/CLAUDE.md +52 -0
  161. package/lib/commonjs/features/browser-config.js +12 -8
  162. package/lib/commonjs/features/browser-config.js.map +1 -1
  163. package/lib/commonjs/features/browser.js +11 -3
  164. package/lib/commonjs/features/browser.js.map +1 -1
  165. package/lib/commonjs/features/carConnection.js +1 -1
  166. package/lib/commonjs/features/equalizer.js +1 -1
  167. package/lib/commonjs/features/errors.js +3 -3
  168. package/lib/commonjs/features/favorites.js +14 -11
  169. package/lib/commonjs/features/favorites.js.map +1 -1
  170. package/lib/commonjs/features/gate.js +16 -1
  171. package/lib/commonjs/features/gate.js.map +1 -1
  172. package/lib/commonjs/features/metadata.js +20 -3
  173. package/lib/commonjs/features/metadata.js.map +1 -1
  174. package/lib/commonjs/features/network.js +1 -1
  175. package/lib/commonjs/features/nowPlaying.js +1 -1
  176. package/lib/commonjs/features/output.js +1 -1
  177. package/lib/commonjs/features/playback/controls.js +4 -2
  178. package/lib/commonjs/features/playback/controls.js.map +1 -1
  179. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  180. package/lib/commonjs/features/playback/playing.js +1 -1
  181. package/lib/commonjs/features/playback/progress.js +3 -2
  182. package/lib/commonjs/features/playback/progress.js.map +1 -1
  183. package/lib/commonjs/features/playback/rate.js +8 -1
  184. package/lib/commonjs/features/playback/rate.js.map +1 -1
  185. package/lib/commonjs/features/playback/state.js +3 -4
  186. package/lib/commonjs/features/playback/state.js.map +1 -1
  187. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  188. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  189. package/lib/commonjs/features/playback/volume.js +1 -1
  190. package/lib/commonjs/features/player/options.js +48 -34
  191. package/lib/commonjs/features/player/options.js.map +1 -1
  192. package/lib/commonjs/features/player/setup.js +18 -9
  193. package/lib/commonjs/features/player/setup.js.map +1 -1
  194. package/lib/commonjs/features/player/validateOptions.js +23 -0
  195. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  196. package/lib/commonjs/features/queue/activeTrack.js +15 -4
  197. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  198. package/lib/commonjs/features/queue/queue.js +13 -6
  199. package/lib/commonjs/features/queue/queue.js.map +1 -1
  200. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  201. package/lib/commonjs/features/queue/shuffle.js +1 -1
  202. package/lib/commonjs/features/remoteControls.js +13 -92
  203. package/lib/commonjs/features/remoteControls.js.map +1 -1
  204. package/lib/commonjs/features/sleepTimer.js +13 -1
  205. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  206. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  207. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  208. package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
  209. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  210. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  211. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  212. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  213. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  214. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  215. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  216. package/lib/commonjs/web/NativeAudioBrowser.js +200 -75
  217. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  218. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
  219. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
  220. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  221. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  222. package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
  223. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  224. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  225. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  226. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
  227. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
  228. package/lib/commonjs/web/browser/BrowserManager.js +175 -100
  229. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  230. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  231. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  232. package/lib/commonjs/web/browser/SearchManager.js +18 -23
  233. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  234. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  235. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  236. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  237. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  238. package/lib/commonjs/web/browser/sections.js +89 -0
  239. package/lib/commonjs/web/browser/sections.js.map +1 -0
  240. package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
  241. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  242. package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
  243. package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
  244. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  245. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  246. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  247. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  248. package/lib/module/AudioBrowser.js +2 -1
  249. package/lib/module/AudioBrowser.js.map +1 -1
  250. package/lib/module/CLAUDE.md +52 -0
  251. package/lib/module/features/browser-config.js +12 -8
  252. package/lib/module/features/browser-config.js.map +1 -1
  253. package/lib/module/features/browser.js +11 -3
  254. package/lib/module/features/browser.js.map +1 -1
  255. package/lib/module/features/carConnection.js +1 -1
  256. package/lib/module/features/equalizer.js +1 -1
  257. package/lib/module/features/errors.js +3 -3
  258. package/lib/module/features/favorites.js +14 -11
  259. package/lib/module/features/favorites.js.map +1 -1
  260. package/lib/module/features/gate.js +16 -1
  261. package/lib/module/features/gate.js.map +1 -1
  262. package/lib/module/features/metadata.js +20 -3
  263. package/lib/module/features/metadata.js.map +1 -1
  264. package/lib/module/features/network.js +1 -1
  265. package/lib/module/features/nowPlaying.js +1 -1
  266. package/lib/module/features/output.js +1 -1
  267. package/lib/module/features/playback/controls.js +4 -2
  268. package/lib/module/features/playback/controls.js.map +1 -1
  269. package/lib/module/features/playback/playWhenReady.js +1 -1
  270. package/lib/module/features/playback/playing.js +1 -1
  271. package/lib/module/features/playback/progress.js +3 -2
  272. package/lib/module/features/playback/progress.js.map +1 -1
  273. package/lib/module/features/playback/rate.js +8 -1
  274. package/lib/module/features/playback/rate.js.map +1 -1
  275. package/lib/module/features/playback/state.js +3 -4
  276. package/lib/module/features/playback/state.js.map +1 -1
  277. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  278. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  279. package/lib/module/features/playback/volume.js +1 -1
  280. package/lib/module/features/player/options.js +47 -32
  281. package/lib/module/features/player/options.js.map +1 -1
  282. package/lib/module/features/player/setup.js +17 -8
  283. package/lib/module/features/player/setup.js.map +1 -1
  284. package/lib/module/features/player/validateOptions.js +19 -0
  285. package/lib/module/features/player/validateOptions.js.map +1 -0
  286. package/lib/module/features/queue/activeTrack.js +15 -4
  287. package/lib/module/features/queue/activeTrack.js.map +1 -1
  288. package/lib/module/features/queue/queue.js +13 -6
  289. package/lib/module/features/queue/queue.js.map +1 -1
  290. package/lib/module/features/queue/repeatMode.js +1 -1
  291. package/lib/module/features/queue/shuffle.js +1 -1
  292. package/lib/module/features/remoteControls.js +13 -92
  293. package/lib/module/features/remoteControls.js.map +1 -1
  294. package/lib/module/features/sleepTimer.js +13 -1
  295. package/lib/module/features/sleepTimer.js.map +1 -1
  296. package/lib/module/test-utils/reactNativeStub.js +58 -0
  297. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  298. package/lib/module/utils/LazyNativeEmitter.js +28 -14
  299. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  300. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  301. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  302. package/lib/module/utils/getTrackIdentity.js +20 -0
  303. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  304. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  305. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  306. package/lib/module/web/NativeAudioBrowser.js +200 -75
  307. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  308. package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
  309. package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
  310. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  311. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  312. package/lib/module/web/TrackPlayer/Player.js +254 -39
  313. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  314. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  315. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  316. package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
  317. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
  318. package/lib/module/web/browser/BrowserManager.js +175 -100
  319. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  320. package/lib/module/web/browser/FavoriteManager.js +31 -21
  321. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  322. package/lib/module/web/browser/SearchManager.js +18 -23
  323. package/lib/module/web/browser/SearchManager.js.map +1 -1
  324. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  325. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  326. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  327. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  328. package/lib/module/web/browser/sections.js +83 -0
  329. package/lib/module/web/browser/sections.js.map +1 -0
  330. package/lib/module/web/http/RequestConfigBuilder.js +110 -27
  331. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  332. package/lib/module/web/http/followMediaRedirect.js +215 -0
  333. package/lib/module/web/http/followMediaRedirect.js.map +1 -0
  334. package/lib/module/web/player/NowPlayingManager.js +4 -3
  335. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  336. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  337. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  338. package/lib/typescript/package.json +1 -0
  339. package/lib/typescript/src/AudioBrowser.d.ts +1 -1
  340. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  341. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  342. package/lib/typescript/src/features/browser.d.ts +10 -2
  343. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  344. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  345. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  346. package/lib/typescript/src/features/errors.d.ts +3 -3
  347. package/lib/typescript/src/features/favorites.d.ts +14 -11
  348. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  349. package/lib/typescript/src/features/gate.d.ts +18 -2
  350. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  351. package/lib/typescript/src/features/metadata.d.ts +20 -3
  352. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  353. package/lib/typescript/src/features/network.d.ts +1 -1
  354. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  355. package/lib/typescript/src/features/output.d.ts +1 -1
  356. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  357. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  358. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  359. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  360. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  361. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  362. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  363. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  364. package/lib/typescript/src/features/playback/state.d.ts +3 -4
  365. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  366. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  367. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  368. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  369. package/lib/typescript/src/features/player/options.d.ts +108 -56
  370. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  371. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  372. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  373. package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
  374. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  375. package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
  376. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  377. package/lib/typescript/src/features/queue/queue.d.ts +7 -4
  378. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  379. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  380. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  381. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  382. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  383. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  384. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  385. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  386. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  387. package/lib/typescript/src/types/browser-native.d.ts +3 -2
  388. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  389. package/lib/typescript/src/types/browser-nodes.d.ts +380 -126
  390. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  391. package/lib/typescript/src/types/browser.d.ts +82 -57
  392. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  393. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
  394. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  395. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  396. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  397. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  398. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  399. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  400. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  401. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
  402. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
  403. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
  404. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  405. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
  406. package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
  407. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  408. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  409. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
  410. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
  411. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
  412. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
  413. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
  414. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
  415. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
  416. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
  417. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
  418. 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
  419. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
  420. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +7 -10
  421. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
  422. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
  423. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
  424. package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
  425. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +45 -25
  426. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
  427. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  428. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  429. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
  430. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +36 -11
  431. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
  432. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +15 -12
  433. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +7 -10
  434. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
  435. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  436. package/nitrogen/generated/android/c++/JOptions.hpp +7 -10
  437. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
  438. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  439. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  440. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  441. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +57 -49
  442. package/nitrogen/generated/android/c++/JSection.hpp +116 -0
  443. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  444. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  445. package/nitrogen/generated/android/c++/JTrack.hpp +25 -46
  446. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
  447. package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
  448. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  449. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  450. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
  451. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
  452. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  453. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  454. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  455. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +80 -0
  456. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
  457. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
  458. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  459. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  460. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +8 -5
  461. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
  462. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  463. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  464. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  465. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
  466. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -15
  467. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +15 -6
  468. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
  469. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  470. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -12
  471. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
  472. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
  473. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
  474. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +244 -133
  475. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +31 -16
  476. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +33 -18
  477. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  478. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  479. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  480. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  481. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
  482. package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
  483. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  484. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  485. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  486. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  487. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
  488. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  489. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  490. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  491. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  492. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  493. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
  494. package/nitrogen/generated/ios/swift/Section.swift +82 -0
  495. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  496. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  497. package/nitrogen/generated/ios/swift/Track.swift +28 -63
  498. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  499. package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
  500. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  501. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  502. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  503. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  504. package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
  505. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  506. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  507. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  508. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  509. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
  510. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  511. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  512. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  513. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  514. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  515. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
  516. package/nitrogen/generated/shared/c++/Section.hpp +102 -0
  517. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  518. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  519. package/nitrogen/generated/shared/c++/Track.hpp +18 -30
  520. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  521. package/package.json +24 -11
  522. package/src/AudioBrowser.ts +2 -1
  523. package/src/CLAUDE.md +52 -0
  524. package/src/features/battery.test.ts +79 -0
  525. package/src/features/browser-config.test.ts +21 -6
  526. package/src/features/browser-config.ts +12 -8
  527. package/src/features/browser.test.ts +1 -1
  528. package/src/features/browser.ts +11 -3
  529. package/src/features/carConnection.ts +1 -1
  530. package/src/features/equalizer.ts +1 -1
  531. package/src/features/errors.ts +3 -3
  532. package/src/features/favorites.ts +14 -11
  533. package/src/features/gate.ts +16 -1
  534. package/src/features/hook-wiring.test.ts +302 -0
  535. package/src/features/metadata.ts +20 -3
  536. package/src/features/network.ts +1 -1
  537. package/src/features/nowPlaying.ts +1 -1
  538. package/src/features/output.ts +1 -1
  539. package/src/features/playback/controls.ts +4 -2
  540. package/src/features/playback/playWhenReady.ts +1 -1
  541. package/src/features/playback/playing.ts +1 -1
  542. package/src/features/playback/progress.test.ts +169 -0
  543. package/src/features/playback/progress.ts +3 -2
  544. package/src/features/playback/rate.ts +8 -1
  545. package/src/features/playback/state.ts +3 -4
  546. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  547. package/src/features/playback/trackPlaybackTime.ts +1 -6
  548. package/src/features/playback/volume.ts +1 -1
  549. package/src/features/player/options.ts +126 -76
  550. package/src/features/player/setup.test.ts +24 -7
  551. package/src/features/player/setup.ts +22 -10
  552. package/src/features/player/validateOptions.ts +21 -0
  553. package/src/features/queue/activeTrack.ts +15 -4
  554. package/src/features/queue/queue.ts +13 -6
  555. package/src/features/queue/repeatMode.ts +1 -1
  556. package/src/features/queue/shuffle.ts +1 -1
  557. package/src/features/remoteControls.ts +13 -92
  558. package/src/features/sleepTimer.test.ts +172 -0
  559. package/src/features/sleepTimer.ts +9 -1
  560. package/src/nitro-stub-drift.test.ts +277 -0
  561. package/src/public-surface.test.ts +147 -0
  562. package/src/specs/audio-browser.nitro.ts +5 -4
  563. package/src/style-resolution.test.ts +210 -0
  564. package/src/test-utils/reactNativeStub.ts +60 -0
  565. package/src/types/browser-native.ts +7 -3
  566. package/src/types/browser-nodes.ts +396 -134
  567. package/src/types/browser.ts +85 -59
  568. package/src/utils/LazyNativeEmitter.ts +28 -15
  569. package/src/utils/NativeUpdatedValue.ts +29 -13
  570. package/src/utils/getTrackIdentity.ts +20 -0
  571. package/src/utils/useDebug.test.ts +182 -0
  572. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  573. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  574. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
  575. package/src/web/NativeAudioBrowser.ts +239 -82
  576. package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
  577. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
  578. package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
  579. package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
  580. package/src/web/TrackPlayer/Player.load.test.ts +137 -1
  581. package/src/web/TrackPlayer/Player.ts +284 -37
  582. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  583. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  584. package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
  585. package/src/web/__tests__/lifecycle.test.ts +345 -0
  586. package/src/web/__tests__/lifecycleHarness.ts +351 -0
  587. package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
  588. package/src/web/__tests__/loadSupersession.test.ts +175 -0
  589. package/src/web/__tests__/retry.test.ts +169 -0
  590. package/src/web/browser/BrowserManager.ts +209 -111
  591. package/src/web/browser/FavoriteManager.test.ts +84 -0
  592. package/src/web/browser/FavoriteManager.ts +28 -23
  593. package/src/web/browser/SearchManager.test.ts +75 -0
  594. package/src/web/browser/SearchManager.ts +25 -28
  595. package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
  596. package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
  597. package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
  598. package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
  599. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  600. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  601. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  602. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  603. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  604. package/src/web/browser/sections.test.ts +91 -0
  605. package/src/web/browser/sections.ts +72 -0
  606. package/src/web/http/RequestConfigBuilder.test.ts +241 -1
  607. package/src/web/http/RequestConfigBuilder.ts +140 -33
  608. package/src/web/http/followMediaRedirect.test.ts +289 -0
  609. package/src/web/http/followMediaRedirect.ts +253 -0
  610. package/src/web/player/NowPlayingManager.ts +4 -3
  611. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  612. package/src/web/util/BrowserPathHelper.ts +61 -13
  613. package/website/guide/android-auto.md +114 -0
  614. package/website/guide/android-certificates.md +409 -0
  615. package/website/guide/artwork.md +255 -0
  616. package/website/guide/audio-output.md +135 -0
  617. package/website/guide/automotive.md +86 -0
  618. package/website/guide/basic-usage.md +151 -0
  619. package/website/guide/battery.md +115 -0
  620. package/website/guide/browser.md +581 -0
  621. package/website/guide/carplay.md +620 -0
  622. package/website/guide/configuration.md +173 -0
  623. package/website/guide/equalizer.md +231 -0
  624. package/website/guide/errors.md +310 -0
  625. package/website/guide/favorites.md +154 -0
  626. package/website/guide/gate.md +143 -0
  627. package/website/guide/getting-started.md +46 -0
  628. package/website/guide/hooks.md +238 -0
  629. package/website/guide/live-streams.md +125 -0
  630. package/website/guide/metadata.md +239 -0
  631. package/website/guide/migrating-from-track-player.md +244 -0
  632. package/website/guide/native-callback-fetch.md +127 -0
  633. package/website/guide/network.md +86 -0
  634. package/website/guide/now-playing.md +175 -0
  635. package/website/guide/playback.md +328 -0
  636. package/website/guide/queue.md +300 -0
  637. package/website/guide/remote-controls.md +256 -0
  638. package/website/guide/search.md +255 -0
  639. package/website/guide/sleep-timer.md +268 -0
  640. package/website/guide/track.md +247 -0
  641. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  642. package/ios/Model/MediaURL.swift +0 -67
  643. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  644. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  645. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  646. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  647. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  648. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  649. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  650. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  651. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  652. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  653. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  654. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  655. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  656. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  657. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  658. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  659. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
  660. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
@@ -36,35 +36,46 @@ final class CarPlayImageLoader {
36
36
  return sfSymbolImageForSize(symbolName, canvasSize: canvasSize, backgroundColor: bg, symbolColor: fg)
37
37
  }
38
38
 
39
- /// Cached empty placeholder image to reserve space while artwork loads
40
- lazy var placeholderImage: UIImage? = {
41
- let size = CPListItem.maximumImageSize
42
- let scale = carTraitCollection.displayScale
43
- UIGraphicsBeginImageContextWithOptions(size, false, scale)
39
+ /// Cached empty placeholders, keyed by point size — list items and image-row
40
+ /// cells reserve different footprints.
41
+ private var placeholders: [String: UIImage] = [:]
42
+
43
+ /// Empty placeholder image reserving a `size`-point slot while artwork loads.
44
+ func placeholderImage(size: CGSize) -> UIImage? {
45
+ let key = "\(size.width)×\(size.height)"
46
+ if let cached = placeholders[key] { return cached }
47
+ UIGraphicsBeginImageContextWithOptions(size, false, carTraitCollection.displayScale)
44
48
  defer { UIGraphicsEndImageContext() }
45
- return UIGraphicsGetImageFromCurrentImageContext()
46
- }()
49
+ guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
50
+ placeholders[key] = image
51
+ return image
52
+ }
47
53
 
48
54
  // MARK: - Artwork Loading
49
55
 
50
56
  /// Loads artwork for a track with size context, using the artwork transform if configured.
51
57
  /// - Parameters:
52
58
  /// - track: The track to load artwork for
59
+ /// - style: The track's resolved style block (inherited values folded in).
60
+ /// nil = the track's own declaration — callers without a section context
61
+ /// (tabs, now-playing) have no inheritance to resolve.
53
62
  /// - size: The target size in points (will be multiplied by CarPlay display scale)
54
63
  /// - completion: Called with the loaded image, or nil on failure
55
- func loadArtwork(for track: Track, size: CGSize, completion: @escaping @Sendable (UIImage?) -> Void) {
64
+ func loadArtwork(for track: Track, style: TrackStyle? = nil, size: CGSize, completion: @escaping @Sendable (UIImage?) -> Void) {
56
65
  // A track that ships one image per appearance is loaded as two ordinary
57
66
  // single-URL loads and combined below. Recursing rather than branching
58
67
  // inside the loader means each variant still goes through the whole
59
68
  // pipeline — route config, headers, SVG rasterising, the resolve transform
60
69
  // — instead of a second, thinner fetch path that would drift from it.
61
70
  if let variants = track.artwork?.variants {
62
- loadVariants(for: track, variants: variants, size: size, completion: completion)
71
+ loadVariants(for: track, style: style, variants: variants, size: size, completion: completion)
63
72
  return
64
73
  }
65
74
 
66
- // Build URL resolver closure that wraps BrowserManager
67
- let urlResolver: ((Double, Double) async -> ArtworkResolvedImage?)? = browserManager.map { bm in
75
+ // The annotation goes on the closure's return type, not the binding:
76
+ // @Sendable isn't inferred through `map`, and omitting it fails with
77
+ // "type of expression is ambiguous" — which doesn't mention @Sendable.
78
+ let urlResolver = browserManager.map { bm -> @Sendable (Double, Double) async -> ArtworkResolvedImage? in
68
79
  { [weak bm] pixelWidth, pixelHeight in
69
80
  guard let bm else { return nil }
70
81
  let imageContext = ImageContext(width: pixelWidth, height: pixelHeight)
@@ -77,7 +88,7 @@ final class CarPlayImageLoader {
77
88
  let action = await CarPlayArtworkResolver.resolve(
78
89
  artwork: track.artwork?.url,
79
90
  artworkSourceUri: track.artworkSource?.uri,
80
- artworkCarPlayTinted: track.artworkCarPlayTinted,
91
+ stencil: (style ?? track.style)?.artworkRendering == .stencil,
81
92
  targetWidth: Double(size.width),
82
93
  targetHeight: Double(size.height),
83
94
  displayScale: Double(self.carTraitCollection.displayScale),
@@ -88,7 +99,7 @@ final class CarPlayImageLoader {
88
99
  case let .sfSymbol(artwork, width, height):
89
100
  completion(self.sfSymbolImage(forArtwork: artwork, canvasSize: CGSize(width: width, height: height)))
90
101
  case let .fetch(uri, headers, shouldTint, isSvg):
91
- let image = await self.fetchImage(uri: uri, headers: headers, isSvg: isSvg)
102
+ let image = await self.fetchImage(uri: uri, headers: headers, isSvg: isSvg, size: size)
92
103
  if let image, shouldTint {
93
104
  completion(self.createAdaptiveImage(image, carTraitCollection: self.carTraitCollection))
94
105
  } else {
@@ -115,13 +126,18 @@ final class CarPlayImageLoader {
115
126
  /// beats an empty slot, and this is usually a transient fetch failure.
116
127
  private func loadVariants(
117
128
  for track: Track,
129
+ style: TrackStyle?,
118
130
  variants: ArtworkVariants,
119
131
  size: CGSize,
120
132
  completion: @escaping @Sendable (UIImage?) -> Void,
121
133
  ) {
122
134
  Task {
123
- async let light = self.loadArtworkAsync(for: track.copying(artwork: .some(.first(variants.light))), size: size)
124
- async let dark = self.loadArtworkAsync(for: track.copying(artwork: .some(.first(variants.dark))), size: size)
135
+ async let light = self.loadArtworkAsync(
136
+ for: track.copying(artwork: .some(.first(variants.light))), style: style, size: size,
137
+ )
138
+ async let dark = self.loadArtworkAsync(
139
+ for: track.copying(artwork: .some(.first(variants.dark))), style: style, size: size,
140
+ )
125
141
  let (lightImage, darkImage) = await (light, dark)
126
142
 
127
143
  guard let lightImage, let darkImage else {
@@ -137,19 +153,25 @@ final class CarPlayImageLoader {
137
153
  }
138
154
 
139
155
  /// `loadArtwork` as an awaitable, so the two variants can load concurrently.
140
- private func loadArtworkAsync(for track: Track, size: CGSize) async -> UIImage? {
156
+ private func loadArtworkAsync(for track: Track, style: TrackStyle?, size: CGSize) async -> UIImage? {
141
157
  await withCheckedContinuation { continuation in
142
- self.loadArtwork(for: track, size: size) { image in
158
+ self.loadArtwork(for: track, style: style, size: size) { image in
143
159
  continuation.resume(returning: image)
144
160
  }
145
161
  }
146
162
  }
147
163
 
148
164
  /// Fetches an image from a URL with optional headers and SVG processing.
149
- private func fetchImage(uri: String, headers: [String: String]?, isSvg: Bool) async -> UIImage? {
165
+ /// Rasters are downsampled to `size` points at the car's display scale —
166
+ /// CPListItem.h: "size them to the display scale of the car screen". SVGs
167
+ /// already rasterize at car scale via SVGProcessor.
168
+ private func fetchImage(uri: String, headers: [String: String]?, isSvg: Bool, size: CGSize) async -> UIImage? {
150
169
  let source = ImageSource(uri: uri, method: nil, headers: headers, body: nil)
151
- let svgScale: CGFloat? = isSvg ? carTraitCollection.displayScale : nil
152
- return await ArtworkImageFetcher.fetchImage(from: source, svgScale: svgScale)
170
+ let scale = carTraitCollection.displayScale
171
+ if isSvg {
172
+ return await ArtworkImageFetcher.fetchImage(from: source, svgScale: scale)
173
+ }
174
+ return await ArtworkImageFetcher.fetchImage(from: source, downsampleTo: (size, scale))
153
175
  }
154
176
 
155
177
  /// Creates an SF Symbol image rendered at the given canvas size.
@@ -7,8 +7,9 @@ import os.log
7
7
 
8
8
  /// Typed metadata stored on CPListItems, replacing stringly-typed userInfo dictionaries.
9
9
  struct CarPlayItemInfo {
10
- let id: String?
11
- let src: String?
10
+ /// The track's identity (`id` when non-blank, else `src`) — the comparison
11
+ /// key for the now-playing indicator repaint sweep.
12
+ let identity: String?
12
13
  }
13
14
 
14
15
  extension CPListItem {
@@ -29,11 +30,14 @@ final class CarPlayListItemFactory {
29
30
  private let logger = Logger(subsystem: "com.audiobrowser", category: "CarPlayListItemFactory")
30
31
 
31
32
  var imageLoader: CarPlayImageLoader?
32
- private let isActiveTrack: (_ id: String?, _ src: String?) -> Bool
33
+ /// Pushed by the controller (at creation and when options change), like
34
+ /// `imageLoader` — the factory never reads options itself.
35
+ var playingIndicatorLocation: CPListItemPlayingIndicatorLocation = .leading
36
+ private let isActiveTrack: (_ identity: String?) -> Bool
33
37
  private let onItemSelected: (Track, @escaping () -> Void) -> Void
34
38
 
35
39
  init(
36
- isActiveTrack: @escaping (_ id: String?, _ src: String?) -> Bool,
40
+ isActiveTrack: @escaping (_ identity: String?) -> Bool,
37
41
  onItemSelected: @escaping (Track, @escaping () -> Void) -> Void,
38
42
  ) {
39
43
  self.isActiveTrack = isActiveTrack
@@ -42,55 +46,69 @@ final class CarPlayListItemFactory {
42
46
 
43
47
  // MARK: - Sections
44
48
 
49
+ /// Whether this OS has CarPlay's wrapping tile container (the iOS 26
50
+ /// element API) — the capability `SectionPresentation` degrades against.
51
+ private static var supportsWrappingGrid: Bool {
52
+ if #available(iOS 26.0, *) { return true }
53
+ return false
54
+ }
55
+
56
+ /// Maps the page's sections 1:1 to CPListSections (ADR 0010), respecting
57
+ /// CarPlay's section and total-item budgets. Style blocks declare the
58
+ /// requested presentation (resolved `section ?? page` here — ADR 0011);
59
+ /// this renders CarPlay's nearest supported form (`SectionPresentation`):
60
+ /// - list (default): a titled/headerless section of list rows.
61
+ /// - single-line grid (`gridWrap: false`): a headerless section holding
62
+ /// one single-line image-row item whose text is the section title — the
63
+ /// tiles that fit render, the rest truncate (the platform doesn't
64
+ /// report the fit).
65
+ /// - wrapping grid: a wrapping, titled tile grid on iOS 26+; a plain list
66
+ /// before that.
45
67
  func createSections(from resolvedTrack: ResolvedTrack) -> [CPListSection] {
46
- guard let children = resolvedTrack.children else {
68
+ guard let sections = resolvedTrack.normalizedSections else {
47
69
  return []
48
70
  }
49
71
 
50
72
  let maxSections = CPListTemplate.maximumSectionCount
51
73
  let maxTotalItems = CPListTemplate.maximumItemCount
52
74
 
53
- // Contiguous runs sharing a groupTitle form a section (the same semantics
54
- // Android Auto's GROUP_TITLE hint has), so sections — titled or not —
55
- // render in exact source (server) order. Ungrouped runs become headerless
56
- // sections in place rather than being hoisted to the top.
57
- var runs: [(title: String?, tracks: [Track])] = []
58
- for track in children {
59
- if let last = runs.indices.last, runs[last].title == track.groupTitle {
60
- runs[last].tracks.append(track)
61
- } else {
62
- runs.append((track.groupTitle, [track]))
63
- }
64
- }
65
-
66
- // Create sections (respecting both section and total item limits)
67
- var sections: [CPListSection] = []
75
+ var listSections: [CPListSection] = []
68
76
  var totalItemCount = 0
69
77
 
70
- for run in runs {
71
- guard sections.count < maxSections else { break }
78
+ for section in sections {
79
+ guard listSections.count < maxSections else { break }
72
80
  guard totalItemCount < maxTotalItems else { break }
73
-
74
- let availableSlots = maxTotalItems - totalItemCount
75
- let items: [CPListTemplateItem] = run.tracks.prefix(availableSlots).map { track in
76
- // Empty image rows fall through to a plain list item: zero-image
77
- // CPListImageRowItem init is undocumented territory, and a header-only
78
- // row is a dead end regardless (a "popular" row with no entries yet).
79
- if let imageRow = track.imageRow, !imageRow.isEmpty {
80
- return createImageRowItem(for: track)
81
+ // An empty section is a dead end regardless of style (a "popular"
82
+ // section with no entries yet) — skip it rather than render a header
83
+ // with nothing under it.
84
+ guard !section.children.isEmpty else { continue }
85
+
86
+ let style = StyleResolver.sectionStyle(section: section.style, page: resolvedTrack.style)
87
+ let presentation = SectionPresentation(for: style, supportsWrappingGrid: Self.supportsWrappingGrid)
88
+ switch presentation {
89
+ case .list:
90
+ let availableSlots = maxTotalItems - totalItemCount
91
+ let items: [CPListTemplateItem] = section.children.prefix(availableSlots).map {
92
+ createListItem(for: $0, style: StyleResolver.trackStyle(track: $0.style, section: style))
81
93
  }
82
- return createListItem(for: track)
83
- }
84
- guard !items.isEmpty else { continue }
85
- if let title = run.title {
86
- sections.append(CPListSection(items: items, header: title, sectionIndexTitle: nil))
87
- } else {
88
- sections.append(CPListSection(items: items))
94
+ if let title = section.title {
95
+ listSections.append(CPListSection(items: items, header: title, sectionIndexTitle: nil))
96
+ } else {
97
+ listSections.append(CPListSection(items: items))
98
+ }
99
+ totalItemCount += items.count
100
+ case .singleLineRow, .wrappingGrid:
101
+ // Tile presentations render as one image-row item inside a
102
+ // headerless CPListSection, with the section title as the item's
103
+ // text — a headed section would render the title twice.
104
+ guard let item = createImageRowItem(for: section, style: style, presentation: presentation)
105
+ else { continue }
106
+ listSections.append(CPListSection(items: [item]))
107
+ totalItemCount += 1
89
108
  }
90
- totalItemCount += items.count
91
109
  }
92
110
 
93
- return sections
111
+ return listSections
94
112
  }
95
113
 
96
114
  // MARK: - List Item
@@ -98,9 +116,14 @@ final class CarPlayListItemFactory {
98
116
  /// Creates a CPListItem for a track with common setup (typed userInfo, artwork, isPlaying).
99
117
  /// - Parameters:
100
118
  /// - track: The track to create the item for
119
+ /// - style: The track's resolved style block (`track ?? section ?? page`,
120
+ /// via `StyleResolver`). nil = the track's own declaration — the one
121
+ /// genuinely section-less caller (the Up Next queue list) has no
122
+ /// inheritance to resolve.
101
123
  /// - handler: Optional custom handler. If nil, uses default browse/play handling.
102
124
  func createListItem(
103
125
  for track: Track,
126
+ style: TrackStyle? = nil,
104
127
  handler: ((CPSelectableListItem, @escaping () -> Void) -> Void)? = nil,
105
128
  ) -> CPListItem {
106
129
  let item = CPListItem(
@@ -109,46 +132,70 @@ final class CarPlayListItemFactory {
109
132
  )
110
133
 
111
134
  // Store typed info for updatePlayingIndicators()
112
- item.setCarPlayItemInfo(CarPlayItemInfo(id: track.id, src: track.src))
135
+ item.setCarPlayItemInfo(CarPlayItemInfo(identity: track.identity))
113
136
 
114
137
  // Set accessory type based on whether track is browsable or playable
115
138
  if let src = track.src {
116
139
  // Playable track - check if it's currently playing
117
140
  item.accessoryType = .none
118
- // Leading (the system default, set explicitly to make the choice
119
- // deliberate): the indicator draws in the artwork slot. Note the
141
+ // Consumer-configurable via `ios.carPlayPlayingIndicatorLocation`
142
+ // (default leading: the indicator draws in the artwork slot). Note the
120
143
  // indicator's rendering is owned by the phone's CarPlay service, and on
121
144
  // some iOS/CarPlay-Simulator combinations it isn't drawn at all — for
122
145
  // ANY third-party app. Verify on a real head unit before assuming a
123
146
  // logic bug here.
124
- item.playingIndicatorLocation = .leading
125
- item.isPlaying = isActiveTrack(track.id, src)
147
+ item.playingIndicatorLocation = playingIndicatorLocation
148
+ item.isPlaying = isActiveTrack(track.identity)
126
149
  if item.isPlaying {
127
150
  logger.debug("Setting isPlaying=true for: \(track.title) (src: \(src))")
128
151
  }
129
- } else if track.url != nil {
152
+ } else if track.path != nil {
130
153
  // Browsable only - show disclosure indicator
131
154
  item.accessoryType = .disclosureIndicator
132
155
  }
133
156
 
157
+ // A resolved accessorySymbol replaces the derived accessory above
158
+ // (accessoryImage outranks accessoryType in the SDK). 'none' — the
159
+ // inheritance escape — keeps the derived behavior instead.
160
+ if let symbol = SectionPresentation.effectiveAccessorySymbol(style ?? track.style),
161
+ let accessory = UIImage(systemName: symbol)
162
+ {
163
+ // `accessoryImage` is readonly on CPListItem; post-init updates go
164
+ // through the setter (which also clears `accessoryType`, per the SDK).
165
+ item.setAccessoryImage(accessory)
166
+ }
167
+
168
+ // An unavailable track grays out and goes inert — list rows are the
169
+ // surface that CAN draw the fact, so it shows rather than hides
170
+ // (Track.disabled's rendering ladder).
171
+ if track.disabled == true {
172
+ item.isEnabled = false
173
+ }
174
+
134
175
  // Load artwork with size context for proper CDN optimization
135
176
  if track.artwork != nil || track.artworkSource != nil {
136
177
  // Set empty placeholder to reserve space while loading
137
- item.setImage(imageLoader?.placeholderImage)
138
- imageLoader?.loadArtwork(for: track, size: CPListItem.maximumImageSize) { [weak item] image in
178
+ item.setImage(imageLoader?.placeholderImage(size: CPListItem.maximumImageSize))
179
+ imageLoader?.loadArtwork(for: track, style: style, size: CPListItem.maximumImageSize) { [weak item] image in
139
180
  Task { @MainActor in
140
181
  item?.setImage(image)
141
182
  }
142
183
  }
143
184
  }
144
185
 
145
- // Set selection handler
146
- if let handler {
147
- item.handler = handler
148
- } else {
149
- item.handler = { [onItemSelected] _, completion in
150
- onItemSelected(track, completion)
186
+ // Set selection handler. The disabled guard wraps BOTH handler paths —
187
+ // custom handlers (Up Next rows skipping the queue, say) included: belt
188
+ // over the isEnabled braces, so an unavailable track stays inert even if
189
+ // a surface delivers the tap.
190
+ let selection = handler ?? { [onItemSelected] _, completion in
191
+ onItemSelected(track, completion)
192
+ }
193
+ item.handler = { item, completion in
194
+ guard track.disabled != true else {
195
+ completion()
196
+ return
151
197
  }
198
+ selection(item, completion)
152
199
  }
153
200
 
154
201
  return item
@@ -156,58 +203,202 @@ final class CarPlayListItemFactory {
156
203
 
157
204
  // MARK: - Image Row Item
158
205
 
159
- /// Creates a CPListImageRowItem for a track that has an imageRow.
160
- /// Renders as a horizontal row of tappable artwork thumbnails with a header title.
161
- func createImageRowItem(for track: Track) -> CPListImageRowItem {
162
- guard let imageRowItems = track.imageRow else {
163
- fatalError("createImageRowItem called without imageRow")
206
+ /// Target size for images inside an image row. The iOS 26 SDK deprecates
207
+ /// `CPListImageRowItem.maximumImageSize` in favor of the element-based API,
208
+ /// so read whichever the running OS considers canonical.
209
+ private static var rowImageSize: CGSize {
210
+ if #available(iOS 26.0, *) {
211
+ return CPListImageRowItemElement.maximumImageSize
212
+ }
213
+ return CPListImageRowItem.maximumImageSize
214
+ }
215
+
216
+ /// Target artwork size for one tile. Cards read their own class maximums
217
+ /// ('background' selects the larger full-height target); every other
218
+ /// family uses the shared element size.
219
+ private static func tileImageSize(gridTile: GridTile?, cardImage: CardImage?) -> CGSize {
220
+ if #available(iOS 26.0, *), gridTile == .card {
221
+ return cardImage == .background
222
+ ? CPListImageRowItemCardElement.maximumFullHeightImageSize
223
+ : CPListImageRowItemCardElement.maximumImageSize
164
224
  }
225
+ return rowImageSize
226
+ }
165
227
 
166
- // CPListImageRowItem requires images at init — start with placeholders
167
- let maxImages = CPMaximumNumberOfGridImages
168
- let visibleItems = Array(imageRowItems.prefix(maxImages))
169
- let placeholders = visibleItems.map { _ in imageLoader?.placeholderImage ?? UIImage() }
170
- let titles = visibleItems.map(\.title)
228
+ /// Creates the image-row item rendering a tile-presented section: a single
229
+ /// truncating line (`.singleLineRow`) or a wrapping multi-line grid
230
+ /// (`.wrappingGrid` — constructed on iOS 26+ only, by `SectionPresentation`).
231
+ ///
232
+ /// Returns nil when nothing is drawable — every child hidden by the
233
+ /// disabled ladder (pre-26 only; the element APIs gray instead).
234
+ private func createImageRowItem(
235
+ for section: Section, style: SectionStyle, presentation: SectionPresentation,
236
+ ) -> CPListImageRowItem? {
237
+ let singleLine = presentation == .singleLineRow
238
+ // Disabled ladder: iOS 26 elements can draw unavailability (grayed +
239
+ // inert, below); the legacy image row can't, so there a disabled track
240
+ // hides — never a normal-looking dead tile.
241
+ let visibleChildren: [Track] = if #available(iOS 26.0, *) {
242
+ section.children
243
+ } else {
244
+ section.children.filter { $0.disabled != true }
245
+ }
246
+ guard !visibleChildren.isEmpty else { return nil }
247
+ // A single line shows at most CPMaximumNumberOfGridImages; the wrapping
248
+ // grid takes every child (queue scope is declared, never rendered — the
249
+ // cap here only limits what's drawn, and only where the platform does).
250
+ let tracks = singleLine
251
+ ? Array(visibleChildren.prefix(CPMaximumNumberOfGridImages))
252
+ : visibleChildren
253
+ let placeholder = { [imageLoader] in
254
+ imageLoader?.placeholderImage(size: Self.rowImageSize) ?? UIImage()
255
+ }
171
256
 
172
- // Use imageTitles variant on iOS 17.4+ to show titles below each thumbnail
173
- let item = if #available(iOS 17.4, *) {
174
- CPListImageRowItem(text: track.title, images: placeholders, imageTitles: titles)
257
+ let item: CPListImageRowItem
258
+ // Applies an async-loaded artwork image to a tile on the element-based
259
+ // API (the elements are captured per availability branch — their type
260
+ // doesn't exist before iOS 26). nil = the legacy gridImages path.
261
+ // @MainActor makes the closure Sendable for the artwork completion hop.
262
+ var applyImage: (@MainActor @Sendable (Int, UIImage) -> Void)?
263
+ // Resolved once per track, shared by element construction and the
264
+ // artwork loads below.
265
+ let resolvedStyles = tracks.map { StyleResolver.trackStyle(track: $0.style, section: style) }
266
+ if #available(iOS 26.0, *) {
267
+ // Builds a family's elements with the shared per-element tail: the
268
+ // disabled fact (isEnabled lives on the element base class) applied
269
+ // once, not per family.
270
+ func makeElements<Element: CPListImageRowItemElement>(
271
+ _ make: (Track, TrackStyle) -> Element,
272
+ ) -> [Element] {
273
+ zip(tracks, resolvedStyles).map { track, resolved in
274
+ let element = make(track, resolved)
275
+ if track.disabled == true {
276
+ element.isEnabled = false
277
+ }
278
+ return element
279
+ }
280
+ }
281
+ // The image setter every element family shares (`image` is on the base
282
+ // class); bounds-checked against the elements the row was built with.
283
+ func imageApplier(_ elements: [some CPListImageRowItemElement]) -> @MainActor @Sendable (Int, UIImage) -> Void {
284
+ { index, image in
285
+ guard index < elements.count else { return }
286
+ elements[index].image = image
287
+ }
288
+ }
289
+ // The family is style-driven (`SectionPresentation.tileFamily`); both
290
+ // non-plain families take either wrap mode. Pre-26 the treatment drops
291
+ // and the layout survives (the legacy branch below).
292
+ switch SectionPresentation.tileFamily(for: style, singleLine: singleLine) {
293
+ case .cardElements:
294
+ // Cards have no shape and no accessory slot; their knobs are the
295
+ // tint and the image mode ('background' fills the card full-height
296
+ // and turns the tint into the color behind the labels).
297
+ let elements = makeElements { track, resolved in
298
+ CPListImageRowItemCardElement(
299
+ image: placeholder(),
300
+ showsImageFullHeight: resolved.cardImage == .background,
301
+ title: track.title,
302
+ subtitle: track.subtitle,
303
+ tintColor: resolved.cardTint.flatMap { UIColor(declaredHex: $0) },
304
+ )
305
+ }
306
+ item = CPListImageRowItem(
307
+ text: section.title, cardElements: elements, allowsMultipleLines: !singleLine,
308
+ )
309
+ applyImage = imageApplier(elements)
310
+ case .condensedElements:
311
+ let elements = makeElements { track, resolved in
312
+ CPListImageRowItemCondensedElement(
313
+ image: placeholder(),
314
+ imageShape: resolved.imageShape == .circular ? .circular : .roundedRectangle,
315
+ title: track.title,
316
+ subtitle: track.subtitle,
317
+ accessorySymbolName: SectionPresentation.effectiveAccessorySymbol(resolved),
318
+ )
319
+ }
320
+ item = CPListImageRowItem(
321
+ text: section.title, condensedElements: elements, allowsMultipleLines: !singleLine,
322
+ )
323
+ applyImage = imageApplier(elements)
324
+ case .rowElements:
325
+ // The only plain tile with a subtitle slot — the imageGridElements
326
+ // family has no equivalent, by SDK design.
327
+ let elements = makeElements { track, _ in
328
+ CPListImageRowItemRowElement(
329
+ image: placeholder(), title: track.title, subtitle: track.subtitle,
330
+ )
331
+ }
332
+ item = CPListImageRowItem(text: section.title, elements: elements, allowsMultipleLines: false)
333
+ applyImage = imageApplier(elements)
334
+ case .imageGridElements:
335
+ // imageGridElements, never the title-less gridElements: an
336
+ // artwork-less track must keep its name on screen (ADR 0010).
337
+ let elements = makeElements { track, resolved in
338
+ CPListImageRowItemImageGridElement(
339
+ image: placeholder(),
340
+ imageShape: resolved.imageShape == .circular ? .circular : .roundedRectangle,
341
+ title: track.title,
342
+ accessorySymbolName: SectionPresentation.effectiveAccessorySymbol(resolved),
343
+ )
344
+ }
345
+ item = CPListImageRowItem(text: section.title, imageGridElements: elements, allowsMultipleLines: true)
346
+ applyImage = imageApplier(elements)
347
+ }
175
348
  } else {
176
- CPListImageRowItem(text: track.title, images: placeholders)
349
+ let placeholders = tracks.map { _ in placeholder() }
350
+ // Use imageTitles variant on iOS 17.4+ to show titles below each thumbnail
351
+ if #available(iOS 17.4, *) {
352
+ item = CPListImageRowItem(text: section.title ?? "", images: placeholders, imageTitles: tracks.map(\.title))
353
+ } else {
354
+ item = CPListImageRowItem(text: section.title ?? "", images: placeholders)
355
+ }
177
356
  }
178
357
 
179
- // Handler for row header tap → navigate to track.url if present. A row
180
- // without a url (pure preview) has nothing to open, so its header tap is
181
- // a no-op rather than a selection that can't resolve.
358
+ // Header tap → navigate to section.path ("view all"). A path-less
359
+ // section is a pure preview — its header tap is a no-op rather than a
360
+ // selection that can't resolve.
182
361
  item.handler = { [onItemSelected] _, completion in
183
- guard track.url != nil else {
362
+ guard let path = section.path else {
184
363
  completion()
185
364
  return
186
365
  }
187
- onItemSelected(track, completion)
366
+ onItemSelected(Self.navigationTrack(path: path, title: section.title), completion)
188
367
  }
189
368
 
190
- // Handler for individual image taps
369
+ // Per-tile taps select the child track directly. A disabled tile is
370
+ // grayed and inert (26+ elements) — the guard keeps it inert even if
371
+ // the surface delivers the tap anyway.
191
372
  item.listImageRowHandler = { [onItemSelected] _, index, completion in
192
- guard index < visibleItems.count else {
373
+ guard index < tracks.count, tracks[index].disabled != true else {
193
374
  completion()
194
375
  return
195
376
  }
196
- let tappedItem = visibleItems[index]
197
- onItemSelected(tappedItem.toTrack(), completion)
377
+ onItemSelected(tracks[index], completion)
198
378
  }
199
379
 
200
- // Load artwork for each visible image row item asynchronously
201
- for (index, imageRowItem) in visibleItems.enumerated() {
202
- guard imageRowItem.artwork != nil || imageRowItem.artworkSource != nil else { continue }
380
+ // Load artwork for each rendered tile asynchronously
381
+ for (index, track) in tracks.enumerated() {
382
+ guard track.artwork != nil || track.artworkSource != nil else { continue }
203
383
 
204
- imageLoader?.loadArtwork(for: imageRowItem.toTrack(), size: CPListImageRowItem.maximumImageSize) { [weak item] image in
384
+ let resolved = resolvedStyles[index]
385
+ imageLoader?.loadArtwork(
386
+ for: track,
387
+ style: resolved,
388
+ size: Self.tileImageSize(gridTile: style.gridTile, cardImage: resolved.cardImage),
389
+ ) { [weak item, applyImage] image in
205
390
  Task { @MainActor in
206
391
  guard let item, let image else { return }
207
- var images = item.gridImages
208
- if index < images.count {
209
- images[index] = image
210
- item.update(images)
392
+ if let applyImage {
393
+ // The element-based API exposes a settable `image`; assigning it
394
+ // updates the element the row observes.
395
+ applyImage(index, image)
396
+ } else {
397
+ var images = item.gridImages
398
+ if index < images.count {
399
+ images[index] = image
400
+ item.update(images)
401
+ }
211
402
  }
212
403
  }
213
404
  }
@@ -215,4 +406,18 @@ final class CarPlayListItemFactory {
215
406
 
216
407
  return item
217
408
  }
409
+
410
+ /// A synthetic browsable Track for section-header navigation — the
411
+ /// section's "view all" surface has a path and a title, but no Track.
412
+ /// Style-less by design: CarPlay resolves presentation from the page it
413
+ /// navigates to. (Android's counterpart projects the section's block —
414
+ /// the promise only exists to feed Android Auto's parent-level hint.)
415
+ private static func navigationTrack(path: String, title: String?) -> Track {
416
+ Track(
417
+ id: nil, path: path, src: nil, artwork: nil, artworkSource: nil,
418
+ request: nil, title: title ?? "", subtitle: nil,
419
+ artist: nil, albumPath: nil, album: nil, description: nil, genre: nil,
420
+ duration: nil, style: nil, disabled: nil, favorited: nil, live: nil, icyCharset: nil,
421
+ )
422
+ }
218
423
  }