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

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 (185) hide show
  1. package/README.md +10 -0
  2. package/android/build.gradle +20 -1
  3. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +8 -1
  4. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +52 -0
  5. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
  6. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +27 -11
  7. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +1 -1
  8. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +11 -0
  9. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +11 -1
  10. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +1 -1
  11. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +21 -22
  12. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +17 -2
  13. package/android/src/main/java/com/audiobrowser/player/Player.kt +3 -0
  14. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  15. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +73 -0
  16. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +16 -6
  17. package/android/src/main/res/values/strings.xml +37 -2
  18. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +50 -5
  19. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +9 -0
  20. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +3 -2
  21. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
  22. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +56 -0
  23. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +4 -4
  24. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +77 -10
  25. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +2 -2
  26. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  27. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +10 -9
  28. package/ios/Browser/BrowserManager.swift +106 -45
  29. package/ios/Browser/InertStyleDiagnostic.swift +279 -0
  30. package/ios/Browser/JsonModels.swift +81 -15
  31. package/ios/Browser/SectionPresentation.swift +30 -0
  32. package/ios/Browser/StyleResolver.swift +11 -0
  33. package/ios/Browser/TabBarEntries.swift +7 -1
  34. package/ios/CarPlay/CarPlayController.swift +139 -35
  35. package/ios/CarPlay/CarPlayListItemFactory.swift +99 -27
  36. package/ios/Extension/Section+Copying.swift +1 -3
  37. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  38. package/ios/Http/HttpClient.swift +8 -2
  39. package/ios/HybridAudioBrowser.swift +56 -12
  40. package/ios/Model/NitroTypeStubs.swift +60 -1
  41. package/ios/Player/MediaIntentCriteria.swift +13 -5
  42. package/ios/Player/MediaLoader.swift +33 -9
  43. package/ios/Player/NowPlayingUpdater.swift +53 -5
  44. package/ios/Player/PlaybackCoordinator.swift +11 -4
  45. package/ios/Player/PlaybackTimer.swift +8 -2
  46. package/ios/Player/QueueManager.swift +36 -10
  47. package/ios/Player/ShuffleOrder.swift +23 -0
  48. package/ios/Player/SleepTimerHandling.swift +13 -0
  49. package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
  50. package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
  51. package/ios/Tests/JsonModelsTests.swift +76 -1
  52. package/ios/Tests/QueueManagerTests.swift +72 -6
  53. package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
  54. package/ios/Tests/ResolvedTrackCopyingTests.swift +2 -2
  55. package/ios/Tests/SectionPresentationTests.swift +29 -0
  56. package/ios/Tests/StyleResolverTests.swift +60 -0
  57. package/ios/Tests/Support/TestTypes.swift +7 -0
  58. package/ios/TrackPlayer.swift +57 -34
  59. package/ios/Util/NetworkMonitor.swift +43 -26
  60. package/ios/Util/SFSymbolRenderer.swift +24 -2
  61. package/ios/Util/SVGProcessor.swift +9 -2
  62. package/lib/commonjs/features/browser-config.js +1 -0
  63. package/lib/commonjs/features/browser-config.js.map +1 -1
  64. package/lib/commonjs/features/playback/progress.js +2 -1
  65. package/lib/commonjs/features/playback/progress.js.map +1 -1
  66. package/lib/commonjs/features/playback/rate.js +8 -1
  67. package/lib/commonjs/features/playback/rate.js.map +1 -1
  68. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  69. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  70. package/lib/module/features/browser-config.js +1 -0
  71. package/lib/module/features/browser-config.js.map +1 -1
  72. package/lib/module/features/playback/progress.js +2 -1
  73. package/lib/module/features/playback/progress.js.map +1 -1
  74. package/lib/module/features/playback/rate.js +8 -1
  75. package/lib/module/features/playback/rate.js.map +1 -1
  76. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  77. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  78. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  79. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  80. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  81. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  82. package/lib/typescript/src/types/browser-native.d.ts +2 -1
  83. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  84. package/lib/typescript/src/types/browser-nodes.d.ts +116 -9
  85. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  86. package/lib/typescript/src/types/browser.d.ts +36 -0
  87. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  88. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
  89. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -0
  90. package/nitrogen/generated/android/c++/JBrowseResult.hpp +6 -0
  91. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  92. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -0
  93. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -0
  94. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -0
  95. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -0
  96. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -0
  97. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -0
  98. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -0
  99. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -0
  100. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
  101. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +6 -0
  102. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -0
  103. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -0
  104. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
  105. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -0
  106. package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
  107. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +13 -0
  108. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  109. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -0
  110. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +20 -1
  111. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +6 -0
  112. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +4 -0
  113. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -0
  114. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +6 -0
  115. package/nitrogen/generated/android/c++/JSection.hpp +7 -5
  116. package/nitrogen/generated/android/c++/JSectionStyle.hpp +30 -3
  117. package/nitrogen/generated/android/c++/JTrack.hpp +4 -0
  118. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -0
  119. package/nitrogen/generated/android/c++/JTrackStyle.hpp +24 -3
  120. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +6 -0
  121. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  122. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +80 -0
  123. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
  124. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  125. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +7 -4
  126. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +2 -5
  127. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +18 -3
  128. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +15 -3
  129. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
  130. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -0
  131. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +9 -0
  132. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +9 -0
  133. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  134. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
  135. package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
  136. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  137. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +48 -1
  138. package/nitrogen/generated/ios/swift/Section.swift +1 -19
  139. package/nitrogen/generated/ios/swift/SectionStyle.swift +70 -1
  140. package/nitrogen/generated/ios/swift/TrackStyle.swift +59 -1
  141. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  142. package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
  143. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  144. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -2
  145. package/nitrogen/generated/shared/c++/Section.hpp +1 -5
  146. package/nitrogen/generated/shared/c++/SectionStyle.hpp +32 -2
  147. package/nitrogen/generated/shared/c++/TrackStyle.hpp +25 -2
  148. package/package.json +7 -2
  149. package/src/features/browser-config.ts +1 -0
  150. package/src/features/playback/progress.ts +2 -1
  151. package/src/features/playback/rate.ts +8 -1
  152. package/src/style-resolution.test.ts +72 -1
  153. package/src/types/browser-native.ts +5 -1
  154. package/src/types/browser-nodes.ts +124 -9
  155. package/src/types/browser.ts +38 -0
  156. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  157. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  158. package/website/guide/android-auto.md +114 -0
  159. package/website/guide/android-certificates.md +409 -0
  160. package/website/guide/artwork.md +255 -0
  161. package/website/guide/audio-output.md +135 -0
  162. package/website/guide/automotive.md +86 -0
  163. package/website/guide/basic-usage.md +151 -0
  164. package/website/guide/battery.md +115 -0
  165. package/website/guide/browser.md +581 -0
  166. package/website/guide/carplay.md +620 -0
  167. package/website/guide/configuration.md +173 -0
  168. package/website/guide/equalizer.md +231 -0
  169. package/website/guide/errors.md +310 -0
  170. package/website/guide/favorites.md +154 -0
  171. package/website/guide/gate.md +143 -0
  172. package/website/guide/getting-started.md +46 -0
  173. package/website/guide/hooks.md +238 -0
  174. package/website/guide/live-streams.md +125 -0
  175. package/website/guide/metadata.md +239 -0
  176. package/website/guide/migrating-from-track-player.md +244 -0
  177. package/website/guide/native-callback-fetch.md +127 -0
  178. package/website/guide/network.md +86 -0
  179. package/website/guide/now-playing.md +175 -0
  180. package/website/guide/playback.md +328 -0
  181. package/website/guide/queue.md +300 -0
  182. package/website/guide/remote-controls.md +256 -0
  183. package/website/guide/search.md +255 -0
  184. package/website/guide/sleep-timer.md +268 -0
  185. package/website/guide/track.md +246 -0
@@ -0,0 +1,277 @@
1
+ package com.audiobrowser.browser
2
+
3
+ import com.audiobrowser.TestFixtures.browseTrack
4
+ import com.audiobrowser.TestFixtures.section
5
+ import com.audiobrowser.TestFixtures.sectionStyle
6
+ import com.audiobrowser.TestFixtures.track
7
+ import com.audiobrowser.TestFixtures.trackStyle
8
+ import com.margelo.nitro.audiobrowser.ArtworkRendering
9
+ import com.margelo.nitro.audiobrowser.CardImage
10
+ import com.margelo.nitro.audiobrowser.GridTile
11
+ import com.margelo.nitro.audiobrowser.ImageShape
12
+ import com.margelo.nitro.audiobrowser.Section
13
+ import com.margelo.nitro.audiobrowser.SectionStyle
14
+ import com.margelo.nitro.audiobrowser.StyleDisplay
15
+ import org.junit.Assert.assertEquals
16
+ import org.junit.Assert.assertTrue
17
+ import org.junit.Test
18
+
19
+ /**
20
+ * The dev diagnostic for declarations that can never render (ADR 0011): the recovery for the
21
+ * compile-time invalid-combination errors the block model deliberately gave up. Structural
22
+ * inertness only — a surface that can't draw a property it understands is intended usage, never a
23
+ * finding. Mirrors `ios/Tests/InertStyleDiagnosticTests.swift`.
24
+ */
25
+ class InertStyleDiagnosticTest {
26
+
27
+ private fun findings(
28
+ sections: List<Section>,
29
+ page: SectionStyle? = null,
30
+ path: String = "/home",
31
+ ) = InertStyleDiagnostic.findings(path, page, sections)
32
+
33
+ // Container properties
34
+
35
+ @Test
36
+ fun `gridWrap on a list section is inert`() {
37
+ val found = findings(listOf(section(style = sectionStyle(gridWrap = false), title = "Recent")))
38
+ assertEquals(1, found.size)
39
+ assertTrue(found[0], found[0].contains("section 'Recent'"))
40
+ assertTrue(found[0], found[0].contains("style.gridWrap"))
41
+ assertTrue(found[0], found[0].contains("display 'list'"))
42
+ assertTrue(found[0], found[0].contains("/home"))
43
+ }
44
+
45
+ @Test
46
+ fun `gridWrap in a grid is live`() {
47
+ val style = sectionStyle(gridWrap = false, display = StyleDisplay.GRID)
48
+ assertTrue(findings(listOf(section(style = style))).isEmpty())
49
+ }
50
+
51
+ @Test
52
+ fun `a section inherits the page's grid display`() {
53
+ // `display` resolves by scope override, so the page's grid makes the section's `gridWrap` live
54
+ // without the section restating it.
55
+ val found =
56
+ findings(
57
+ listOf(section(style = sectionStyle(gridWrap = false))),
58
+ page = sectionStyle(display = StyleDisplay.GRID),
59
+ )
60
+ assertTrue(found.isEmpty())
61
+ }
62
+
63
+ @Test
64
+ fun `gridTile on a list section is inert`() {
65
+ val found = findings(listOf(section(style = sectionStyle(gridTile = GridTile.CARD))))
66
+ assertEquals(1, found.size)
67
+ assertTrue(found[0], found[0].contains("style.gridTile"))
68
+ assertTrue(found[0], found[0].contains("section #1"))
69
+ }
70
+
71
+ // Item properties
72
+
73
+ @Test
74
+ fun `card properties need the card treatment`() {
75
+ val plainGrid =
76
+ sectionStyle(
77
+ display = StyleDisplay.GRID,
78
+ cardTint = "#1e3a8a",
79
+ cardImage = CardImage.BACKGROUND,
80
+ )
81
+ val found = findings(listOf(section(style = plainGrid, title = "Featured")))
82
+ assertEquals(2, found.size)
83
+ assertTrue(found.any { it.contains("style.cardTint") })
84
+ assertTrue(found.any { it.contains("style.cardImage") })
85
+ assertTrue(found.all { it.contains("display 'grid', gridTile 'plain'") })
86
+ }
87
+
88
+ @Test
89
+ fun `card properties in a card grid are live`() {
90
+ val cards =
91
+ sectionStyle(
92
+ display = StyleDisplay.GRID,
93
+ gridTile = GridTile.CARD,
94
+ cardTint = "#1e3a8a",
95
+ cardImage = CardImage.BACKGROUND,
96
+ )
97
+ assertTrue(findings(listOf(section(style = cards))).isEmpty())
98
+ }
99
+
100
+ @Test
101
+ fun `imageShape needs a shaped tile`() {
102
+ val list = sectionStyle(imageShape = ImageShape.CIRCULAR)
103
+ val cards =
104
+ sectionStyle(
105
+ display = StyleDisplay.GRID,
106
+ gridTile = GridTile.CARD,
107
+ imageShape = ImageShape.CIRCULAR,
108
+ )
109
+ val condensed =
110
+ sectionStyle(
111
+ display = StyleDisplay.GRID,
112
+ gridTile = GridTile.CONDENSED,
113
+ imageShape = ImageShape.CIRCULAR,
114
+ )
115
+ assertEquals(1, findings(listOf(section(style = list))).size)
116
+ assertEquals(1, findings(listOf(section(style = cards))).size)
117
+ // Plain and condensed tiles both take a shape.
118
+ assertTrue(findings(listOf(section(style = condensed))).isEmpty())
119
+ }
120
+
121
+ @Test
122
+ fun `accessorySymbol is inert only on cards`() {
123
+ val rows = sectionStyle(accessorySymbol = "lock.fill")
124
+ val cards =
125
+ sectionStyle(
126
+ display = StyleDisplay.GRID,
127
+ gridTile = GridTile.CARD,
128
+ accessorySymbol = "lock.fill",
129
+ )
130
+ // List rows draw accessories; card elements have no slot for one.
131
+ assertTrue(findings(listOf(section(style = rows))).isEmpty())
132
+ val found = findings(listOf(section(style = cards)))
133
+ assertEquals(1, found.size)
134
+ assertTrue(found[0], found[0].contains("no accessory slot"))
135
+ }
136
+
137
+ @Test
138
+ fun `artworkRendering is never inert`() {
139
+ val stencilled =
140
+ sectionStyle(
141
+ display = StyleDisplay.GRID,
142
+ gridTile = GridTile.CARD,
143
+ artworkRendering = ArtworkRendering.STENCIL,
144
+ )
145
+ assertTrue(findings(listOf(section(style = stencilled))).isEmpty())
146
+ }
147
+
148
+ // Track-level declarations
149
+
150
+ @Test
151
+ fun `track declarations are reported per section`() {
152
+ val results =
153
+ section(
154
+ style = sectionStyle(display = StyleDisplay.GRID),
155
+ title = "Results",
156
+ children =
157
+ arrayOf(
158
+ track(style = trackStyle(cardTint = "#111")),
159
+ track(src = "https://s/b.mp3", style = trackStyle(cardTint = "#222")),
160
+ track(src = "https://s/c.mp3"),
161
+ ),
162
+ )
163
+ val found = findings(listOf(results))
164
+ assertEquals(1, found.size)
165
+ assertTrue(found[0], found[0].contains("2 tracks in section 'Results'"))
166
+ assertTrue(found[0], found[0].contains("style.cardTint"))
167
+ }
168
+
169
+ @Test
170
+ fun `a track's own shape is live in a plain grid`() {
171
+ val grid =
172
+ section(
173
+ style = sectionStyle(display = StyleDisplay.GRID),
174
+ children = arrayOf(track(style = trackStyle(imageShape = ImageShape.CIRCULAR))),
175
+ )
176
+ assertTrue(findings(listOf(grid)).isEmpty())
177
+ }
178
+
179
+ // The page block
180
+
181
+ @Test
182
+ fun `a page declaration live in one section is not reported`() {
183
+ // Card tint reaches both sections; one renders cards. Live is live.
184
+ val found =
185
+ findings(
186
+ listOf(
187
+ section(style = sectionStyle(display = StyleDisplay.GRID, gridTile = GridTile.CARD)),
188
+ section(style = sectionStyle(display = StyleDisplay.GRID)),
189
+ ),
190
+ page = sectionStyle(cardTint = "#1e3a8a"),
191
+ )
192
+ assertTrue(found.isEmpty())
193
+ }
194
+
195
+ @Test
196
+ fun `a page declaration no section renders is reported once`() {
197
+ val found =
198
+ findings(
199
+ listOf(
200
+ section(style = sectionStyle(display = StyleDisplay.GRID), title = "A"),
201
+ section(title = "B"),
202
+ ),
203
+ page = sectionStyle(cardTint = "#1e3a8a"),
204
+ )
205
+ assertEquals(1, found.size)
206
+ assertTrue(found[0], found[0].contains("the page block"))
207
+ assertTrue(found[0], found[0].contains("no section that inherits it"))
208
+ }
209
+
210
+ @Test
211
+ fun `a section override shadows the page value`() {
212
+ // Every section declares its own tint, so the page's is never resolved — shadowed, not inert;
213
+ // the diagnostic reports what can't render, not what isn't reached.
214
+ val found =
215
+ findings(
216
+ listOf(
217
+ section(
218
+ style =
219
+ sectionStyle(display = StyleDisplay.GRID, gridTile = GridTile.CARD, cardTint = "#000")
220
+ )
221
+ ),
222
+ page = sectionStyle(cardTint = "#1e3a8a"),
223
+ )
224
+ assertTrue(found.isEmpty())
225
+ }
226
+
227
+ @Test
228
+ fun `a page-wide grid makes section declarations live`() {
229
+ val found =
230
+ findings(
231
+ listOf(section(style = sectionStyle(cardTint = "#111"))),
232
+ page = sectionStyle(display = StyleDisplay.GRID, gridTile = GridTile.CARD),
233
+ )
234
+ assertTrue(found.isEmpty())
235
+ }
236
+
237
+ // The positional `display`
238
+
239
+ @Test
240
+ fun `display on a playable track is inert`() {
241
+ val tracks =
242
+ section(
243
+ title = "Tracks",
244
+ children =
245
+ arrayOf(
246
+ track(style = trackStyle(display = StyleDisplay.GRID)),
247
+ browseTrack(style = trackStyle(display = StyleDisplay.GRID)),
248
+ ),
249
+ )
250
+ val found = findings(listOf(tracks))
251
+ assertEquals(1, found.size)
252
+ assertTrue(found[0], found[0].contains("1 track in section 'Tracks'"))
253
+ assertTrue(found[0], found[0].contains("style.display"))
254
+ assertTrue(found[0], found[0].contains("opens none"))
255
+ }
256
+
257
+ @Test
258
+ fun `display on a section or page is never inert`() {
259
+ val found =
260
+ findings(
261
+ listOf(
262
+ section(
263
+ style = sectionStyle(display = StyleDisplay.LIST),
264
+ children = arrayOf(browseTrack()),
265
+ )
266
+ ),
267
+ page = sectionStyle(display = StyleDisplay.GRID),
268
+ )
269
+ assertTrue(found.isEmpty())
270
+ }
271
+
272
+ @Test
273
+ fun `an unstyled page is silent`() {
274
+ assertTrue(findings(listOf(section(children = arrayOf(track(), browseTrack())))).isEmpty())
275
+ assertTrue(findings(emptyList()).isEmpty())
276
+ }
277
+ }
@@ -1,6 +1,9 @@
1
1
  package com.audiobrowser.browser
2
2
 
3
3
  import com.margelo.nitro.audiobrowser.ArtworkRendering
4
+ import com.margelo.nitro.audiobrowser.CardImage
5
+ import com.margelo.nitro.audiobrowser.GridTile
6
+ import com.margelo.nitro.audiobrowser.ImageShape
4
7
  import com.margelo.nitro.audiobrowser.StyleDisplay
5
8
  import kotlinx.serialization.json.Json
6
9
  import org.junit.Assert.assertEquals
@@ -88,4 +91,57 @@ class JsonStyleTest {
88
91
  val track = json.decodeFromString<JsonTrack>("""{"title":"X","src":"s","disabled":true}""")
89
92
  assertEquals(true, track.toNitro().disabled)
90
93
  }
94
+
95
+ @Test
96
+ fun `decodes imageShape and accessorySymbol`() {
97
+ val track =
98
+ json.decodeFromString<JsonTrack>(
99
+ """{"title":"X","style":{"imageShape":"circular","accessorySymbol":"lock.fill"}}"""
100
+ )
101
+ val style = track.toNitro().style
102
+ assertEquals(ImageShape.CIRCULAR, style?.imageShape)
103
+ assertEquals("lock.fill", style?.accessorySymbol)
104
+ }
105
+
106
+ @Test
107
+ fun `accessorySymbol 'none' is a value - not emptiness`() {
108
+ // The inheritance-escape sentinel must survive the wire — collapsing it
109
+ // to "no declaration" would let the section value flow back in.
110
+ val track =
111
+ json.decodeFromString<JsonTrack>("""{"title":"X","style":{"accessorySymbol":"none"}}""")
112
+ assertEquals("none", track.toNitro().style?.accessorySymbol)
113
+ }
114
+
115
+ @Test
116
+ fun `unknown imageShape decodes as no declaration`() {
117
+ val track =
118
+ json.decodeFromString<JsonTrack>("""{"title":"X","style":{"imageShape":"hexagonal"}}""")
119
+ assertNull(track.toNitro().style)
120
+ }
121
+
122
+ @Test
123
+ fun `decodes the card family`() {
124
+ val resolved =
125
+ json.decodeFromString<JsonResolvedTrack>(
126
+ """{"path":"/home","title":"Home","sections":[
127
+ {"style":{"display":"grid","gridTile":"card","cardTint":"#1e3a8a","cardImage":"background"},
128
+ "children":[{"title":"C","src":"s"}]}
129
+ ]}"""
130
+ )
131
+ val style = resolved.toNitro().sections?.first()?.style
132
+ assertEquals(GridTile.CARD, style?.gridTile)
133
+ assertEquals("#1e3a8a", style?.cardTint)
134
+ assertEquals(CardImage.BACKGROUND, style?.cardImage)
135
+ }
136
+
137
+ @Test
138
+ fun `unknown gridTile decodes as no declaration`() {
139
+ val resolved =
140
+ json.decodeFromString<JsonResolvedTrack>(
141
+ """{"path":"/home","title":"Home","sections":[
142
+ {"style":{"gridTile":"hexagon"},"children":[{"title":"C","src":"s"}]}
143
+ ]}"""
144
+ )
145
+ assertNull(resolved.toNitro().sections?.first()?.style)
146
+ }
91
147
  }
@@ -2,8 +2,8 @@ package com.audiobrowser.browser
2
2
 
3
3
  import com.audiobrowser.TestFixtures.resolvedTrack
4
4
  import com.audiobrowser.TestFixtures.section
5
+ import com.audiobrowser.TestFixtures.sectionStyle
5
6
  import com.audiobrowser.TestFixtures.track
6
- import com.margelo.nitro.audiobrowser.SectionStyle
7
7
  import com.margelo.nitro.audiobrowser.StyleDisplay
8
8
  import org.junit.Assert.assertEquals
9
9
  import org.junit.Assert.assertNull
@@ -138,7 +138,7 @@ class SectionScopeTest {
138
138
  val page =
139
139
  resolvedTrack(
140
140
  style =
141
- SectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = false),
141
+ sectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = false),
142
142
  sections = arrayOf(section(children = arrayOf(track(src = "a")))),
143
143
  )
144
144
 
@@ -153,12 +153,12 @@ class SectionScopeTest {
153
153
  fun `a section's own block overrides the page's in the fold`() {
154
154
  val page =
155
155
  resolvedTrack(
156
- style = SectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = null),
156
+ style = sectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = null),
157
157
  sections =
158
158
  arrayOf(
159
159
  section(
160
160
  style =
161
- SectionStyle(display = StyleDisplay.LIST, artworkRendering = null, gridWrap = null),
161
+ sectionStyle(display = StyleDisplay.LIST, artworkRendering = null, gridWrap = null),
162
162
  children = arrayOf(track(src = "a")),
163
163
  )
164
164
  ),
@@ -1,9 +1,12 @@
1
1
  package com.audiobrowser.browser
2
2
 
3
+ import com.audiobrowser.TestFixtures.sectionStyle
4
+ import com.audiobrowser.TestFixtures.trackStyle
3
5
  import com.margelo.nitro.audiobrowser.ArtworkRendering
4
- import com.margelo.nitro.audiobrowser.SectionStyle
6
+ import com.margelo.nitro.audiobrowser.CardImage
7
+ import com.margelo.nitro.audiobrowser.GridTile
8
+ import com.margelo.nitro.audiobrowser.ImageShape
5
9
  import com.margelo.nitro.audiobrowser.StyleDisplay
6
- import com.margelo.nitro.audiobrowser.TrackStyle
7
10
  import org.junit.Assert.assertEquals
8
11
  import org.junit.Assert.assertNull
9
12
  import org.junit.Test
@@ -20,9 +23,9 @@ class StyleResolverTest {
20
23
  val resolved =
21
24
  StyleResolver.sectionStyle(
22
25
  section =
23
- SectionStyle(display = StyleDisplay.LIST, artworkRendering = null, gridWrap = null),
26
+ sectionStyle(display = StyleDisplay.LIST, artworkRendering = null, gridWrap = null),
24
27
  page =
25
- SectionStyle(
28
+ sectionStyle(
26
29
  display = StyleDisplay.GRID,
27
30
  artworkRendering = ArtworkRendering.STENCIL,
28
31
  gridWrap = false,
@@ -39,7 +42,7 @@ class StyleResolverTest {
39
42
  val resolved =
40
43
  StyleResolver.sectionStyle(
41
44
  section = null,
42
- page = SectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = true),
45
+ page = sectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = true),
43
46
  )
44
47
  assertEquals(StyleDisplay.GRID, resolved.display)
45
48
  assertEquals(true, resolved.gridWrap)
@@ -49,9 +52,9 @@ class StyleResolverTest {
49
52
  fun `track overrides the section per property`() {
50
53
  val resolved =
51
54
  StyleResolver.trackStyle(
52
- track = TrackStyle(display = null, artworkRendering = ArtworkRendering.ORIGINAL),
55
+ track = trackStyle(display = null, artworkRendering = ArtworkRendering.ORIGINAL),
53
56
  section =
54
- SectionStyle(
57
+ sectionStyle(
55
58
  display = StyleDisplay.GRID,
56
59
  artworkRendering = ArtworkRendering.STENCIL,
57
60
  gridWrap = null,
@@ -66,7 +69,7 @@ class StyleResolverTest {
66
69
  StyleResolver.trackStyle(
67
70
  track = null,
68
71
  section =
69
- SectionStyle(display = null, artworkRendering = ArtworkRendering.STENCIL, gridWrap = null),
72
+ sectionStyle(display = null, artworkRendering = ArtworkRendering.STENCIL, gridWrap = null),
70
73
  )
71
74
  assertEquals(ArtworkRendering.STENCIL, resolved.artworkRendering)
72
75
  }
@@ -78,10 +81,74 @@ class StyleResolverTest {
78
81
  // item's resolved display.
79
82
  val resolved =
80
83
  StyleResolver.trackStyle(
81
- track = TrackStyle(display = StyleDisplay.GRID, artworkRendering = null),
84
+ track = trackStyle(display = StyleDisplay.GRID, artworkRendering = null),
82
85
  section =
83
- SectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = null),
86
+ sectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = null),
84
87
  )
85
88
  assertNull(resolved.display)
86
89
  }
90
+
91
+ @Test
92
+ fun `imageShape inherits and overrides per item`() {
93
+ // Section-wide circular (an artists shelf); one album overrides.
94
+ val inherited =
95
+ StyleResolver.trackStyle(
96
+ track = null,
97
+ section = sectionStyle(imageShape = ImageShape.CIRCULAR),
98
+ )
99
+ assertEquals(ImageShape.CIRCULAR, inherited.imageShape)
100
+ val overridden =
101
+ StyleResolver.trackStyle(
102
+ track = trackStyle(imageShape = ImageShape.ROUNDED_RECTANGLE),
103
+ section = sectionStyle(imageShape = ImageShape.CIRCULAR),
104
+ )
105
+ assertEquals(ImageShape.ROUNDED_RECTANGLE, overridden.imageShape)
106
+ }
107
+
108
+ @Test
109
+ fun `accessorySymbol inherits and 'none' resolves as a value`() {
110
+ val inherited =
111
+ StyleResolver.trackStyle(track = null, section = sectionStyle(accessorySymbol = "lock.fill"))
112
+ assertEquals("lock.fill", inherited.accessorySymbol)
113
+ // 'none' is the inheritance escape — it must survive resolution intact
114
+ // (the renderer, not the resolver, maps it to "no accessory").
115
+ val escaped =
116
+ StyleResolver.trackStyle(
117
+ track = trackStyle(accessorySymbol = "none"),
118
+ section = sectionStyle(accessorySymbol = "lock.fill"),
119
+ )
120
+ assertEquals("none", escaped.accessorySymbol)
121
+ }
122
+
123
+ @Test
124
+ fun `gridTile resolves by scope override`() {
125
+ // A page-wide card treatment; one section opts back to plain tiles.
126
+ val inherited =
127
+ StyleResolver.sectionStyle(section = null, page = sectionStyle(gridTile = GridTile.CARD))
128
+ assertEquals(GridTile.CARD, inherited.gridTile)
129
+ val overridden =
130
+ StyleResolver.sectionStyle(
131
+ section = sectionStyle(gridTile = GridTile.PLAIN),
132
+ page = sectionStyle(gridTile = GridTile.CARD),
133
+ )
134
+ assertEquals(GridTile.PLAIN, overridden.gridTile)
135
+ }
136
+
137
+ @Test
138
+ fun `card properties inherit and override per item`() {
139
+ val inherited =
140
+ StyleResolver.trackStyle(
141
+ track = null,
142
+ section = sectionStyle(cardTint = "#1e3a8a", cardImage = CardImage.BACKGROUND),
143
+ )
144
+ assertEquals("#1e3a8a", inherited.cardTint)
145
+ assertEquals(CardImage.BACKGROUND, inherited.cardImage)
146
+ val overridden =
147
+ StyleResolver.trackStyle(
148
+ track = trackStyle(cardImage = CardImage.NORMAL),
149
+ section = sectionStyle(cardTint = "#1e3a8a", cardImage = CardImage.BACKGROUND),
150
+ )
151
+ assertEquals(CardImage.NORMAL, overridden.cardImage)
152
+ assertEquals("#1e3a8a", overridden.cardTint)
153
+ }
87
154
  }
@@ -1,9 +1,9 @@
1
1
  package com.audiobrowser.player
2
2
 
3
3
  import com.audiobrowser.TestFixtures
4
+ import com.audiobrowser.TestFixtures.trackStyle
4
5
  import com.margelo.nitro.audiobrowser.ArtworkRendering
5
6
  import com.margelo.nitro.audiobrowser.StyleDisplay
6
- import com.margelo.nitro.audiobrowser.TrackStyle
7
7
  import org.junit.Assert.assertEquals
8
8
  import org.junit.Assert.assertNull
9
9
  import org.junit.Test
@@ -25,7 +25,7 @@ class PlaybackStateStoreTrackJsonTest {
25
25
  TestFixtures.track(title = "Night Mix", src = "https://s/a.mp3", disabled = true)
26
26
  .copy(
27
27
  style =
28
- TrackStyle(display = StyleDisplay.GRID, artworkRendering = ArtworkRendering.STENCIL)
28
+ trackStyle(display = StyleDisplay.GRID, artworkRendering = ArtworkRendering.STENCIL)
29
29
  )
30
30
 
31
31
  val restored = PlaybackStateStore.trackFromJson(PlaybackStateStore.trackToJson(track))
@@ -0,0 +1,79 @@
1
+ package com.audiobrowser.player
2
+
3
+ import androidx.media3.common.C
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Assert.assertNotEquals
6
+ import org.junit.Assert.assertTrue
7
+ import org.junit.Test
8
+
9
+ /**
10
+ * Media3's `ShuffleOrder.cloneAndSet(insertionCount, startIndex)` discards the start index (the
11
+ * interface default clears and re-inserts from 0, and `DefaultShuffleOrder` does not override it),
12
+ * so `setMediaItems(items, startIndex, …)` leaves the starting track at a random shuffle position —
13
+ * last, for a 1-in-N share of queues, which reads as end-of-queue from the very first track.
14
+ */
15
+ class ShuffleOrderLedByTest {
16
+ @Test
17
+ fun `the start index leads the order`() {
18
+ for (start in 0 until 6) {
19
+ val order = shuffleOrderLedBy(startIndex = start, count = 6)
20
+ assertEquals("start index must be at shuffle position 0", start, order.firstIndex)
21
+ assertNotEquals(
22
+ "a queue of 6 must have somewhere to advance to from its start track",
23
+ C.INDEX_UNSET.toLong(),
24
+ order.getNextIndex(start).toLong(),
25
+ )
26
+ }
27
+ }
28
+
29
+ @Test
30
+ fun `every index appears exactly once`() {
31
+ val order = shuffleOrderLedBy(startIndex = 3, count = 8)
32
+ assertEquals(8, order.length)
33
+ val seen = mutableListOf<Int>()
34
+ var index = order.firstIndex
35
+ while (index != C.INDEX_UNSET) {
36
+ seen.add(index)
37
+ index = order.getNextIndex(index)
38
+ }
39
+ assertEquals((0 until 8).toList(), seen.sorted())
40
+ }
41
+
42
+ @Test
43
+ fun `a single-track queue leads with its only track and ends there`() {
44
+ val order = shuffleOrderLedBy(startIndex = 0, count = 1)
45
+ assertEquals(0, order.firstIndex)
46
+ assertEquals(C.INDEX_UNSET, order.getNextIndex(0))
47
+ }
48
+
49
+ @Test
50
+ fun `an out-of-range start index is clamped rather than throwing`() {
51
+ assertEquals(4, shuffleOrderLedBy(startIndex = 99, count = 5).firstIndex)
52
+ assertEquals(0, shuffleOrderLedBy(startIndex = -3, count = 5).firstIndex)
53
+ }
54
+
55
+ @Test
56
+ fun `an empty queue produces an empty order`() {
57
+ assertEquals(0, shuffleOrderLedBy(startIndex = 0, count = 0).length)
58
+ }
59
+
60
+ @Test
61
+ fun `the tail is actually shuffled`() {
62
+ // Not a strict guarantee of any single draw — but across many draws an
63
+ // unshuffled tail would be impossible to miss.
64
+ val tails =
65
+ (0 until 50)
66
+ .map { shuffleOrderLedBy(startIndex = 0, count = 8) }
67
+ .map { order ->
68
+ buildList {
69
+ var index = order.getNextIndex(order.firstIndex)
70
+ while (index != C.INDEX_UNSET) {
71
+ add(index)
72
+ index = order.getNextIndex(index)
73
+ }
74
+ }
75
+ }
76
+ .toSet()
77
+ assertTrue("50 draws produced one identical tail — the order is not shuffled", tails.size > 1)
78
+ }
79
+ }
@@ -2,10 +2,10 @@ package com.audiobrowser.util
2
2
 
3
3
  import androidx.media.utils.MediaConstants
4
4
  import com.audiobrowser.TestFixtures
5
+ import com.audiobrowser.TestFixtures.sectionStyle
6
+ import com.audiobrowser.TestFixtures.trackStyle
5
7
  import com.audiobrowser.browser.styleResolvedSections
6
- import com.margelo.nitro.audiobrowser.SectionStyle
7
8
  import com.margelo.nitro.audiobrowser.StyleDisplay
8
- import com.margelo.nitro.audiobrowser.TrackStyle
9
9
  import org.junit.Assert.assertEquals
10
10
  import org.junit.Assert.assertFalse
11
11
  import org.junit.Test
@@ -23,7 +23,7 @@ class MediaExtrasBuilderTest {
23
23
  private val track = TestFixtures.track()
24
24
 
25
25
  private fun sectionStyle(display: StyleDisplay?) =
26
- SectionStyle(display = display, artworkRendering = null, gridWrap = null)
26
+ sectionStyle(display = display, artworkRendering = null, gridWrap = null)
27
27
 
28
28
  @Test
29
29
  fun `owning section title stamps the group-title hint`() {
@@ -65,7 +65,7 @@ class MediaExtrasBuilderTest {
65
65
  @Test
66
66
  fun `a track's own display never sets its item hint - display is positional`() {
67
67
  val gridPromiseTrack =
68
- track.copy(style = TrackStyle(display = StyleDisplay.GRID, artworkRendering = null))
68
+ track.copy(style = trackStyle(display = StyleDisplay.GRID, artworkRendering = null))
69
69
  val section = TestFixtures.section(arrayOf(gridPromiseTrack), style = null)
70
70
  val extras = MediaExtrasBuilder.build(gridPromiseTrack, section)
71
71
  assertFalse(extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM))
@@ -75,7 +75,7 @@ class MediaExtrasBuilderTest {
75
75
  fun `a browsable track's declared display emits the parent-level promise`() {
76
76
  val handle =
77
77
  TestFixtures.browseTrack()
78
- .copy(style = TrackStyle(display = StyleDisplay.GRID, artworkRendering = null))
78
+ .copy(style = trackStyle(display = StyleDisplay.GRID, artworkRendering = null))
79
79
  val extras = MediaExtrasBuilder.build(handle)
80
80
  val expected = MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
81
81
  assertEquals(
@@ -99,7 +99,7 @@ class MediaExtrasBuilderTest {
99
99
  @Test
100
100
  fun `a playable track's display is inert - playables open no page`() {
101
101
  val playable =
102
- track.copy(style = TrackStyle(display = StyleDisplay.GRID, artworkRendering = null))
102
+ track.copy(style = trackStyle(display = StyleDisplay.GRID, artworkRendering = null))
103
103
  val extras = MediaExtrasBuilder.build(playable)
104
104
  assertFalse(extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE))
105
105
  assertFalse(extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE))
@@ -128,10 +128,11 @@ class MediaExtrasBuilderTest {
128
128
  arrayOf(track),
129
129
  title = "Popular",
130
130
  path = "/popular",
131
- style = SectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = false),
131
+ style = sectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = false),
132
132
  )
133
- val navigation = TrackFactory.navigationTrack(section)
133
+ val navigation = TrackFactory.navigationTrack(section, "View more")
134
134
  assertEquals(StyleDisplay.GRID, navigation.style?.display)
135
+ assertEquals("View more", navigation.title)
135
136
 
136
137
  val extras = MediaExtrasBuilder.build(navigation)
137
138
  assertEquals(
@@ -147,7 +148,7 @@ class MediaExtrasBuilderTest {
147
148
  // narrower unit test stays green while the page tier goes dark.
148
149
  val page =
149
150
  TestFixtures.resolvedTrack(
150
- style = SectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = null),
151
+ style = sectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = null),
151
152
  sections = arrayOf(TestFixtures.section(arrayOf(track))),
152
153
  )
153
154