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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) 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 +5 -1
  4. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +8 -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 -9
  7. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +11 -0
  8. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +11 -1
  9. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +1 -1
  10. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +17 -2
  11. package/android/src/main/java/com/audiobrowser/player/Player.kt +3 -0
  12. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  13. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +73 -0
  14. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -1
  15. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +49 -3
  16. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +3 -2
  17. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
  18. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +56 -0
  19. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +4 -4
  20. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +77 -10
  21. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +2 -2
  22. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  23. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +8 -8
  24. package/ios/Browser/BrowserManager.swift +106 -45
  25. package/ios/Browser/InertStyleDiagnostic.swift +279 -0
  26. package/ios/Browser/JsonModels.swift +81 -10
  27. package/ios/Browser/SectionPresentation.swift +30 -0
  28. package/ios/Browser/StyleResolver.swift +11 -0
  29. package/ios/Browser/TabBarEntries.swift +7 -1
  30. package/ios/CarPlay/CarPlayController.swift +139 -35
  31. package/ios/CarPlay/CarPlayListItemFactory.swift +99 -27
  32. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  33. package/ios/Http/HttpClient.swift +8 -2
  34. package/ios/HybridAudioBrowser.swift +54 -11
  35. package/ios/Model/NitroTypeStubs.swift +60 -0
  36. package/ios/Player/MediaIntentCriteria.swift +13 -5
  37. package/ios/Player/MediaLoader.swift +33 -9
  38. package/ios/Player/NowPlayingUpdater.swift +53 -5
  39. package/ios/Player/PlaybackCoordinator.swift +11 -4
  40. package/ios/Player/PlaybackTimer.swift +8 -2
  41. package/ios/Player/QueueManager.swift +36 -10
  42. package/ios/Player/ShuffleOrder.swift +23 -0
  43. package/ios/Player/SleepTimerHandling.swift +13 -0
  44. package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
  45. package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
  46. package/ios/Tests/JsonModelsTests.swift +76 -1
  47. package/ios/Tests/QueueManagerTests.swift +72 -6
  48. package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
  49. package/ios/Tests/SectionPresentationTests.swift +29 -0
  50. package/ios/Tests/StyleResolverTests.swift +60 -0
  51. package/ios/Tests/Support/TestTypes.swift +7 -0
  52. package/ios/TrackPlayer.swift +57 -34
  53. package/ios/Util/NetworkMonitor.swift +43 -26
  54. package/ios/Util/SFSymbolRenderer.swift +24 -2
  55. package/ios/Util/SVGProcessor.swift +9 -2
  56. package/lib/commonjs/features/playback/progress.js +2 -1
  57. package/lib/commonjs/features/playback/progress.js.map +1 -1
  58. package/lib/commonjs/features/playback/rate.js +8 -1
  59. package/lib/commonjs/features/playback/rate.js.map +1 -1
  60. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  61. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  62. package/lib/module/features/playback/progress.js +2 -1
  63. package/lib/module/features/playback/progress.js.map +1 -1
  64. package/lib/module/features/playback/rate.js +8 -1
  65. package/lib/module/features/playback/rate.js.map +1 -1
  66. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  67. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  68. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  69. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  70. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  71. package/lib/typescript/src/types/browser-nodes.d.ts +104 -2
  72. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  73. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -0
  74. package/nitrogen/generated/android/c++/JBrowseResult.hpp +6 -0
  75. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  76. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -0
  77. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -0
  78. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -0
  79. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -0
  80. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -0
  81. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -0
  82. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -0
  83. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -0
  84. 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
  85. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -0
  86. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -0
  87. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
  88. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -0
  89. package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
  90. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +12 -0
  91. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  92. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -0
  93. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +6 -0
  94. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +6 -0
  95. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +4 -0
  96. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -0
  97. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +6 -0
  98. package/nitrogen/generated/android/c++/JSection.hpp +6 -0
  99. package/nitrogen/generated/android/c++/JSectionStyle.hpp +30 -3
  100. package/nitrogen/generated/android/c++/JTrack.hpp +4 -0
  101. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -0
  102. package/nitrogen/generated/android/c++/JTrackStyle.hpp +24 -3
  103. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +6 -0
  104. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  105. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
  106. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  107. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +18 -3
  108. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +15 -3
  109. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +54 -0
  110. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +9 -0
  111. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +9 -0
  112. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  113. package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
  114. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  115. package/nitrogen/generated/ios/swift/SectionStyle.swift +70 -1
  116. package/nitrogen/generated/ios/swift/TrackStyle.swift +59 -1
  117. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  118. package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
  119. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  120. package/nitrogen/generated/shared/c++/SectionStyle.hpp +32 -2
  121. package/nitrogen/generated/shared/c++/TrackStyle.hpp +25 -2
  122. package/package.json +7 -2
  123. package/src/features/playback/progress.ts +2 -1
  124. package/src/features/playback/rate.ts +8 -1
  125. package/src/style-resolution.test.ts +72 -1
  126. package/src/types/browser-nodes.ts +112 -2
  127. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  128. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  129. package/website/guide/android-auto.md +114 -0
  130. package/website/guide/android-certificates.md +409 -0
  131. package/website/guide/artwork.md +255 -0
  132. package/website/guide/audio-output.md +135 -0
  133. package/website/guide/automotive.md +86 -0
  134. package/website/guide/basic-usage.md +151 -0
  135. package/website/guide/battery.md +115 -0
  136. package/website/guide/browser.md +581 -0
  137. package/website/guide/carplay.md +620 -0
  138. package/website/guide/configuration.md +173 -0
  139. package/website/guide/equalizer.md +231 -0
  140. package/website/guide/errors.md +310 -0
  141. package/website/guide/favorites.md +154 -0
  142. package/website/guide/gate.md +143 -0
  143. package/website/guide/getting-started.md +46 -0
  144. package/website/guide/hooks.md +238 -0
  145. package/website/guide/live-streams.md +125 -0
  146. package/website/guide/metadata.md +239 -0
  147. package/website/guide/migrating-from-track-player.md +244 -0
  148. package/website/guide/native-callback-fetch.md +127 -0
  149. package/website/guide/network.md +86 -0
  150. package/website/guide/now-playing.md +175 -0
  151. package/website/guide/playback.md +328 -0
  152. package/website/guide/queue.md +300 -0
  153. package/website/guide/remote-controls.md +256 -0
  154. package/website/guide/search.md +255 -0
  155. package/website/guide/sleep-timer.md +268 -0
  156. package/website/guide/track.md +246 -0
@@ -49,7 +49,14 @@ object TrackFactory {
49
49
  duration = null,
50
50
  style =
51
51
  section.style?.let {
52
- TrackStyle(display = it.display, artworkRendering = it.artworkRendering)
52
+ TrackStyle(
53
+ display = it.display,
54
+ artworkRendering = it.artworkRendering,
55
+ imageShape = it.imageShape,
56
+ accessorySymbol = it.accessorySymbol,
57
+ cardTint = it.cardTint,
58
+ cardImage = it.cardImage,
59
+ )
53
60
  },
54
61
  disabled = null,
55
62
  favorited = null,
@@ -1,15 +1,21 @@
1
1
  package com.audiobrowser
2
2
 
3
3
  import com.audiobrowser.util.artworkOf
4
+ import com.margelo.nitro.audiobrowser.ArtworkRendering
4
5
  import com.margelo.nitro.audiobrowser.ArtworkRequestConfig
6
+ import com.margelo.nitro.audiobrowser.CardImage
7
+ import com.margelo.nitro.audiobrowser.GridTile
5
8
  import com.margelo.nitro.audiobrowser.ImageQueryParams
9
+ import com.margelo.nitro.audiobrowser.ImageShape
6
10
  import com.margelo.nitro.audiobrowser.ImageSource
7
11
  import com.margelo.nitro.audiobrowser.MediaRequestConfig
8
12
  import com.margelo.nitro.audiobrowser.NativeRouteEntry
9
13
  import com.margelo.nitro.audiobrowser.ResolvedTrack
10
14
  import com.margelo.nitro.audiobrowser.Section
11
15
  import com.margelo.nitro.audiobrowser.SectionStyle
16
+ import com.margelo.nitro.audiobrowser.StyleDisplay
12
17
  import com.margelo.nitro.audiobrowser.Track
18
+ import com.margelo.nitro.audiobrowser.TrackStyle
13
19
  import com.margelo.nitro.audiobrowser.TransformableRequestConfig
14
20
 
15
21
  /**
@@ -28,6 +34,7 @@ object TestFixtures {
28
34
  album: String? = null,
29
35
  favorited: Boolean? = null,
30
36
  disabled: Boolean? = null,
37
+ style: TrackStyle? = null,
31
38
  ) =
32
39
  Track(
33
40
  id = id,
@@ -44,7 +51,7 @@ object TestFixtures {
44
51
  description = null,
45
52
  genre = null,
46
53
  duration = null,
47
- style = null,
54
+ style = style,
48
55
  disabled = disabled,
49
56
  favorited = favorited,
50
57
  live = null,
@@ -52,7 +59,7 @@ object TestFixtures {
52
59
 
53
60
  /** A page [Section] — plain list by default. */
54
61
  fun section(
55
- children: Array<Track>,
62
+ children: Array<Track> = emptyArray(),
56
63
  title: String? = null,
57
64
  subtitle: String? = null,
58
65
  style: SectionStyle? = null,
@@ -109,6 +116,7 @@ object TestFixtures {
109
116
  path: String = "https://api.example.com/channel/abc",
110
117
  artwork: String? = null,
111
118
  artworkSource: ImageSource? = null,
119
+ style: TrackStyle? = null,
112
120
  ) =
113
121
  Track(
114
122
  id = null,
@@ -125,7 +133,7 @@ object TestFixtures {
125
133
  description = null,
126
134
  genre = null,
127
135
  duration = null,
128
- style = null,
136
+ style = style,
129
137
  disabled = null,
130
138
  favorited = null,
131
139
  live = null,
@@ -201,4 +209,42 @@ object TestFixtures {
201
209
  contentType = null,
202
210
  userAgent = null,
203
211
  )
212
+
213
+ fun trackStyle(
214
+ display: StyleDisplay? = null,
215
+ artworkRendering: ArtworkRendering? = null,
216
+ imageShape: ImageShape? = null,
217
+ accessorySymbol: String? = null,
218
+ cardTint: String? = null,
219
+ cardImage: CardImage? = null,
220
+ ) =
221
+ TrackStyle(
222
+ display = display,
223
+ artworkRendering = artworkRendering,
224
+ imageShape = imageShape,
225
+ accessorySymbol = accessorySymbol,
226
+ cardTint = cardTint,
227
+ cardImage = cardImage,
228
+ )
229
+
230
+ fun sectionStyle(
231
+ display: StyleDisplay? = null,
232
+ artworkRendering: ArtworkRendering? = null,
233
+ imageShape: ImageShape? = null,
234
+ accessorySymbol: String? = null,
235
+ cardTint: String? = null,
236
+ cardImage: CardImage? = null,
237
+ gridWrap: Boolean? = null,
238
+ gridTile: GridTile? = null,
239
+ ) =
240
+ SectionStyle(
241
+ gridWrap = gridWrap,
242
+ gridTile = gridTile,
243
+ display = display,
244
+ artworkRendering = artworkRendering,
245
+ imageShape = imageShape,
246
+ accessorySymbol = accessorySymbol,
247
+ cardTint = cardTint,
248
+ cardImage = cardImage,
249
+ )
204
250
  }
@@ -4,6 +4,7 @@ import androidx.media3.common.C
4
4
  import androidx.media3.common.MediaItem
5
5
  import com.audiobrowser.TestFixtures.resolvedTrack
6
6
  import com.audiobrowser.TestFixtures.section
7
+ import com.audiobrowser.TestFixtures.sectionStyle
7
8
  import com.audiobrowser.TestFixtures.staticRoute
8
9
  import com.audiobrowser.TestFixtures.track
9
10
  import com.audiobrowser.util.BrowserPathHelper
@@ -183,7 +184,7 @@ class BrowserManagerTest {
183
184
  section(
184
185
  title = "Row",
185
186
  style =
186
- com.margelo.nitro.audiobrowser.SectionStyle(
187
+ sectionStyle(
187
188
  display = com.margelo.nitro.audiobrowser.StyleDisplay.GRID,
188
189
  artworkRendering = null,
189
190
  gridWrap = false,
@@ -252,7 +253,7 @@ class BrowserManagerTest {
252
253
  section(
253
254
  title = "Most Played",
254
255
  style =
255
- com.margelo.nitro.audiobrowser.SectionStyle(
256
+ sectionStyle(
256
257
  display = com.margelo.nitro.audiobrowser.StyleDisplay.GRID,
257
258
  artworkRendering = null,
258
259
  gridWrap = false,
@@ -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))