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.
- package/README.md +10 -0
- package/android/build.gradle +20 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +8 -1
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +52 -0
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +27 -11
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +1 -1
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +11 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +11 -1
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +21 -22
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +17 -2
- package/android/src/main/java/com/audiobrowser/player/Player.kt +3 -0
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +73 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +16 -6
- package/android/src/main/res/values/strings.xml +37 -2
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +50 -5
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +9 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +3 -2
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +56 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +4 -4
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +77 -10
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +2 -2
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +10 -9
- package/ios/Browser/BrowserManager.swift +106 -45
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +81 -15
- package/ios/Browser/SectionPresentation.swift +30 -0
- package/ios/Browser/StyleResolver.swift +11 -0
- package/ios/Browser/TabBarEntries.swift +7 -1
- package/ios/CarPlay/CarPlayController.swift +139 -35
- package/ios/CarPlay/CarPlayListItemFactory.swift +99 -27
- package/ios/Extension/Section+Copying.swift +1 -3
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +56 -12
- package/ios/Model/NitroTypeStubs.swift +60 -1
- package/ios/Player/MediaIntentCriteria.swift +13 -5
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +53 -5
- package/ios/Player/PlaybackCoordinator.swift +11 -4
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +36 -10
- package/ios/Player/ShuffleOrder.swift +23 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
- package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
- package/ios/Tests/JsonModelsTests.swift +76 -1
- package/ios/Tests/QueueManagerTests.swift +72 -6
- package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +2 -2
- package/ios/Tests/SectionPresentationTests.swift +29 -0
- package/ios/Tests/StyleResolverTests.swift +60 -0
- package/ios/Tests/Support/TestTypes.swift +7 -0
- package/ios/TrackPlayer.swift +57 -34
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/SFSymbolRenderer.swift +24 -2
- package/ios/Util/SVGProcessor.swift +9 -2
- package/lib/commonjs/features/browser-config.js +1 -0
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/playback/progress.js +2 -1
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/rate.js +8 -1
- package/lib/commonjs/features/playback/rate.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/module/features/browser-config.js +1 -0
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/playback/progress.js +2 -1
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/rate.js +8 -1
- package/lib/module/features/playback/rate.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/rate.d.ts +8 -1
- package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-native.d.ts +2 -1
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +116 -9
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +36 -0
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -0
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +6 -0
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
- 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
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -0
- package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +13 -0
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -0
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +20 -1
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +6 -0
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +4 -0
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +6 -0
- package/nitrogen/generated/android/c++/JSection.hpp +7 -5
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +30 -3
- package/nitrogen/generated/android/c++/JTrack.hpp +4 -0
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +24 -3
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +6 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +7 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +2 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +18 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +15 -3
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +9 -0
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +9 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +48 -1
- package/nitrogen/generated/ios/swift/Section.swift +1 -19
- package/nitrogen/generated/ios/swift/SectionStyle.swift +70 -1
- package/nitrogen/generated/ios/swift/TrackStyle.swift +59 -1
- package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -2
- package/nitrogen/generated/shared/c++/Section.hpp +1 -5
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +32 -2
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +25 -2
- package/package.json +7 -2
- package/src/features/browser-config.ts +1 -0
- package/src/features/playback/progress.ts +2 -1
- package/src/features/playback/rate.ts +8 -1
- package/src/style-resolution.test.ts +72 -1
- package/src/types/browser-native.ts +5 -1
- package/src/types/browser-nodes.ts +124 -9
- package/src/types/browser.ts +38 -0
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- package/website/guide/android-auto.md +114 -0
- package/website/guide/android-certificates.md +409 -0
- package/website/guide/artwork.md +255 -0
- package/website/guide/audio-output.md +135 -0
- package/website/guide/automotive.md +86 -0
- package/website/guide/basic-usage.md +151 -0
- package/website/guide/battery.md +115 -0
- package/website/guide/browser.md +581 -0
- package/website/guide/carplay.md +620 -0
- package/website/guide/configuration.md +173 -0
- package/website/guide/equalizer.md +231 -0
- package/website/guide/errors.md +310 -0
- package/website/guide/favorites.md +154 -0
- package/website/guide/gate.md +143 -0
- package/website/guide/getting-started.md +46 -0
- package/website/guide/hooks.md +238 -0
- package/website/guide/live-streams.md +125 -0
- package/website/guide/metadata.md +239 -0
- package/website/guide/migrating-from-track-player.md +244 -0
- package/website/guide/native-callback-fetch.md +127 -0
- package/website/guide/network.md +86 -0
- package/website/guide/now-playing.md +175 -0
- package/website/guide/playback.md +328 -0
- package/website/guide/queue.md +300 -0
- package/website/guide/remote-controls.md +256 -0
- package/website/guide/search.md +255 -0
- package/website/guide/sleep-timer.md +268 -0
- package/website/guide/track.md +246 -0
package/README.md
CHANGED
|
@@ -174,6 +174,16 @@ Full guides and the complete [API Reference](https://audiobrowser.dev/api/) live
|
|
|
174
174
|
|
|
175
175
|
The example app in [`apps/example-native`](https://github.com/radio-garden/react-native-audio-browser/tree/main/apps/example-native) browses archive.org's audio collection — in-app, on CarPlay, and on Android Auto — with search, favorites, and the gate.
|
|
176
176
|
|
|
177
|
+
### For coding agents
|
|
178
|
+
|
|
179
|
+
Every page is also served as raw Markdown — append `.md` to any URL, e.g.
|
|
180
|
+
[`audiobrowser.dev/guide/queue.md`](https://audiobrowser.dev/guide/queue.md).
|
|
181
|
+
|
|
182
|
+
- [`/llms.txt`](https://audiobrowser.dev/llms.txt) — an index of every page with a one-line summary, to pick what to read.
|
|
183
|
+
- [`/llms-full.txt`](https://audiobrowser.dev/llms-full.txt) — every guide in one file.
|
|
184
|
+
|
|
185
|
+
The same guide Markdown ships in the package, so it can be read straight off disk without a network fetch: `node_modules/react-native-audio-browser/website/guide/`.
|
|
186
|
+
|
|
177
187
|
## Support
|
|
178
188
|
|
|
179
189
|
Questions or bugs? [Open an issue](https://github.com/radio-garden/react-native-audio-browser/issues). Release notes live in the [changelog](CHANGELOG.md).
|
package/android/build.gradle
CHANGED
|
@@ -158,6 +158,13 @@ repositories {
|
|
|
158
158
|
google()
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
// Mockito's inline mock maker needs a Byte Buddy agent in the running JVM. Left to itself it
|
|
162
|
+
// installs one through the JVM attach mechanism, which binds a unix socket under $TMPDIR —
|
|
163
|
+
// denied inside sandboxed shells, and disabled by default on JDK 24+. Resolving the agent as its
|
|
164
|
+
// own configuration lets the test JVM pre-load it with -javaagent, so no attach is needed.
|
|
165
|
+
configurations {
|
|
166
|
+
mockitoAgent
|
|
167
|
+
}
|
|
161
168
|
|
|
162
169
|
dependencies {
|
|
163
170
|
// For < 0.71, this will be from the local maven repo
|
|
@@ -210,14 +217,26 @@ dependencies {
|
|
|
210
217
|
implementation("io.coil-kt.coil3:coil-network-okhttp:3.3.0")
|
|
211
218
|
|
|
212
219
|
// Testing dependencies
|
|
220
|
+
def mockitoVersion = "5.8.0"
|
|
213
221
|
testImplementation 'junit:junit:4.13.2'
|
|
214
|
-
testImplementation
|
|
222
|
+
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
|
223
|
+
// Resolved transitively so the agent jar always matches the Byte Buddy that Mockito uses.
|
|
224
|
+
mockitoAgent "org.mockito:mockito-core:${mockitoVersion}"
|
|
215
225
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
|
216
226
|
testImplementation 'org.robolectric:robolectric:4.11.1'
|
|
217
227
|
|
|
218
228
|
api 'com.jakewharton.timber:timber:5.0.1'
|
|
219
229
|
}
|
|
220
230
|
|
|
231
|
+
tasks.withType(Test).configureEach {
|
|
232
|
+
def agentJar = configurations.mockitoAgent.find { it.name.startsWith("byte-buddy-agent") }
|
|
233
|
+
if (agentJar == null) {
|
|
234
|
+
throw new GradleException("No byte-buddy-agent jar in the mockitoAgent configuration; " +
|
|
235
|
+
"Mockito would fall back to runtime self-attach. Check the mockito-core coordinates.")
|
|
236
|
+
}
|
|
237
|
+
jvmArgs "-javaagent:${agentJar}"
|
|
238
|
+
}
|
|
239
|
+
|
|
221
240
|
if (isNewArchitectureEnabled()) {
|
|
222
241
|
react {
|
|
223
242
|
jsRootDir = file("../src/")
|
|
@@ -20,6 +20,7 @@ import com.audiobrowser.browser.CallbackException
|
|
|
20
20
|
import com.audiobrowser.browser.ContentNotFoundException
|
|
21
21
|
import com.audiobrowser.browser.HttpStatusException
|
|
22
22
|
import com.audiobrowser.browser.NetworkException
|
|
23
|
+
import com.audiobrowser.browser.flattenedChildren
|
|
23
24
|
import com.audiobrowser.browser.handleTrackLoad
|
|
24
25
|
import com.audiobrowser.browser.resolveMediaUrl
|
|
25
26
|
import com.audiobrowser.extension.NumberExt.Companion.toSeconds
|
|
@@ -202,6 +203,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
202
203
|
singleTrack = null,
|
|
203
204
|
handleTrackLoad = null,
|
|
204
205
|
androidControllerOfflineError = null,
|
|
206
|
+
viewMoreTitle = null,
|
|
205
207
|
carPlayLoadingTitle = null,
|
|
206
208
|
resolveAlbumPath = null,
|
|
207
209
|
formatNavigationError = null,
|
|
@@ -368,6 +370,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
368
370
|
routes = _configuration.routes,
|
|
369
371
|
singleTrack = _configuration.singleTrack ?: false,
|
|
370
372
|
androidControllerOfflineError = _configuration.androidControllerOfflineError ?: true,
|
|
373
|
+
viewMoreTitle = _configuration.viewMoreTitle,
|
|
371
374
|
)
|
|
372
375
|
}
|
|
373
376
|
|
|
@@ -757,7 +760,10 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
757
760
|
return Promise.async(mainScope) {
|
|
758
761
|
Timber.d("Searching for: $query")
|
|
759
762
|
val searchResults = browserManager.search(query)
|
|
760
|
-
|
|
763
|
+
// Flattened, not `children`: a resolved page carries its rows in
|
|
764
|
+
// `sections` (ADR 0010) and search builds exactly one untitled section,
|
|
765
|
+
// so `children` is always null here and this returned nothing at all.
|
|
766
|
+
searchResults.flattenedChildren?.toTypedArray() ?: emptyArray()
|
|
761
767
|
}
|
|
762
768
|
}
|
|
763
769
|
|
|
@@ -1567,6 +1573,7 @@ private fun NativeBrowserConfiguration.strippingJSCallbacks() =
|
|
|
1567
1573
|
requestResolver = null,
|
|
1568
1574
|
browseResolver = null,
|
|
1569
1575
|
handleTrackLoad = null,
|
|
1576
|
+
viewMoreTitle = null,
|
|
1570
1577
|
resolveAlbumPath = null,
|
|
1571
1578
|
formatNavigationError = null,
|
|
1572
1579
|
request = request?.strippingJSCallbacks(),
|
|
@@ -24,6 +24,7 @@ import com.margelo.nitro.audiobrowser.Section
|
|
|
24
24
|
import com.margelo.nitro.audiobrowser.StyleDisplay
|
|
25
25
|
import com.margelo.nitro.audiobrowser.Track
|
|
26
26
|
import com.margelo.nitro.audiobrowser.TransformableRequestConfig
|
|
27
|
+
import com.margelo.nitro.core.Promise
|
|
27
28
|
import kotlinx.coroutines.Dispatchers
|
|
28
29
|
import kotlinx.coroutines.async
|
|
29
30
|
import kotlinx.coroutines.awaitAll
|
|
@@ -133,6 +134,12 @@ class BrowserManager {
|
|
|
133
134
|
private var resolvedRequestLayer: TransformableRequestConfig? = null
|
|
134
135
|
private var resolvedBrowseLayer: TransformableRequestConfig? = null
|
|
135
136
|
|
|
137
|
+
// The "view more" row title, cached on the same generation as the layers above — see
|
|
138
|
+
// viewMoreTitle(). Kept separate from them because it is resolved lazily (only a browse that
|
|
139
|
+
// actually appends a row asks for it), not eagerly alongside every request.
|
|
140
|
+
private var resolvedViewMoreTitleGeneration = -1
|
|
141
|
+
private var resolvedViewMoreTitle = DEFAULT_VIEW_MORE_TITLE
|
|
142
|
+
|
|
136
143
|
/** Test-only accessors for the resolver-layer cache state (see ensureLayersResolved). */
|
|
137
144
|
internal val layerGenerationForTest: Int
|
|
138
145
|
get() = layerGeneration
|
|
@@ -562,6 +569,10 @@ class BrowserManager {
|
|
|
562
569
|
/** Clears all cached content. */
|
|
563
570
|
fun clearContentCache() {
|
|
564
571
|
contentCache.evictAll()
|
|
572
|
+
// The search slot is content too: a locale switch or a re-auth changes titles, src, artwork
|
|
573
|
+
// and availability, and nothing else ever evicts it, so a repeated query returned
|
|
574
|
+
// pre-invalidation results forever. (Matches the Swift twin's clearContentCache.)
|
|
575
|
+
lastSearch = null
|
|
565
576
|
// Bump the layer generation so request/browse resolver thunks are re-resolved on the next
|
|
566
577
|
// request (invalidateAllContent → clearContentCache is the documented re-resolve trigger).
|
|
567
578
|
layerGeneration += 1
|
|
@@ -594,6 +605,10 @@ class BrowserManager {
|
|
|
594
605
|
// section — ADR 0010), then generate contextual paths and transform artwork
|
|
595
606
|
// URLs. The output never carries `children`.
|
|
596
607
|
val sections = resolvedTrack.normalizedSections ?: return resolvedTrack
|
|
608
|
+
// Dev diagnostic (ADR 0011) — before the blocks are folded, while each level is
|
|
609
|
+
// still attributable. Every page passes here whatever its source, and only on a
|
|
610
|
+
// cache miss. No-op outside debug builds.
|
|
611
|
+
InertStyleDiagnostic.warn(path, resolvedTrack.style, sections)
|
|
597
612
|
// The contextual index is the flat page position — children concatenated in
|
|
598
613
|
// section order (ADR 0009/0010).
|
|
599
614
|
var flatIndex = 0
|
|
@@ -1121,6 +1136,14 @@ class BrowserManager {
|
|
|
1121
1136
|
|
|
1122
1137
|
/** Internal path used for search */
|
|
1123
1138
|
internal const val SEARCH_ROUTE_PATH = "__search__"
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Title of the appended "view more" row when the consumer sets no
|
|
1142
|
+
* [BrowserConfig.viewMoreTitle]. English in code, like [defaultFormattedError]'s error copy — a
|
|
1143
|
+
* default that renders beats a section whose `path` is unreachable, and it still resolves when
|
|
1144
|
+
* the JS runtime is gone.
|
|
1145
|
+
*/
|
|
1146
|
+
internal const val DEFAULT_VIEW_MORE_TITLE = "View more"
|
|
1124
1147
|
}
|
|
1125
1148
|
|
|
1126
1149
|
/**
|
|
@@ -1296,6 +1319,33 @@ class BrowserManager {
|
|
|
1296
1319
|
return resolvedRequestLayer
|
|
1297
1320
|
}
|
|
1298
1321
|
|
|
1322
|
+
/**
|
|
1323
|
+
* The title for the "view more" row appended to a pathed section on a surface whose header cannot
|
|
1324
|
+
* be tapped (Android Auto).
|
|
1325
|
+
*
|
|
1326
|
+
* Resolved from [BrowserConfig.viewMoreTitle] once per content generation — the same generation
|
|
1327
|
+
* the resolver layers use, so `invalidateAllContent()` re-reads it and an app that switched
|
|
1328
|
+
* language mid-drive gets the new copy without reconfiguring. Falls back to
|
|
1329
|
+
* [DEFAULT_VIEW_MORE_TITLE] when unset, blank, or throwing: a row with the default copy still
|
|
1330
|
+
* reaches the section's page, an untitled one does not.
|
|
1331
|
+
*/
|
|
1332
|
+
internal suspend fun viewMoreTitle(): String {
|
|
1333
|
+
val generation = layerGeneration
|
|
1334
|
+
if (resolvedViewMoreTitleGeneration == generation) return resolvedViewMoreTitle
|
|
1335
|
+
val title =
|
|
1336
|
+
try {
|
|
1337
|
+
config.viewMoreTitle?.invoke()?.await()?.takeIf { it.isNotBlank() }
|
|
1338
|
+
} catch (e: Exception) {
|
|
1339
|
+
Timber.e(e, "viewMoreTitle callback failed; falling back to the default")
|
|
1340
|
+
null
|
|
1341
|
+
} ?: DEFAULT_VIEW_MORE_TITLE
|
|
1342
|
+
// A newer generation started while awaiting — use this result, but don't cache it as current.
|
|
1343
|
+
if (generation != layerGeneration) return title
|
|
1344
|
+
resolvedViewMoreTitle = title
|
|
1345
|
+
resolvedViewMoreTitleGeneration = generation
|
|
1346
|
+
return title
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1299
1349
|
/**
|
|
1300
1350
|
* Builds the HTTP request for an API-backed path by layering request (shared) → kind
|
|
1301
1351
|
* (browse/search) → route configs. Each layer's transform receives the previous layer's output; a
|
|
@@ -1494,6 +1544,8 @@ data class BrowserConfig(
|
|
|
1494
1544
|
// Behavior
|
|
1495
1545
|
val singleTrack: Boolean = false,
|
|
1496
1546
|
val androidControllerOfflineError: Boolean = true,
|
|
1547
|
+
// Titles the "view more" row appended to a pathed section. See BrowserManager.viewMoreTitle.
|
|
1548
|
+
val viewMoreTitle: (() -> Promise<String>)? = null,
|
|
1497
1549
|
) {
|
|
1498
1550
|
/** Returns true if search functionality is configured (either callback or config). */
|
|
1499
1551
|
val hasSearch: Boolean
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
package com.audiobrowser.browser
|
|
2
|
+
|
|
3
|
+
import com.audiobrowser.BuildConfig
|
|
4
|
+
import com.audiobrowser.util.toWireString
|
|
5
|
+
import com.margelo.nitro.audiobrowser.GridTile
|
|
6
|
+
import com.margelo.nitro.audiobrowser.Section
|
|
7
|
+
import com.margelo.nitro.audiobrowser.SectionStyle
|
|
8
|
+
import com.margelo.nitro.audiobrowser.StyleDisplay
|
|
9
|
+
import com.margelo.nitro.audiobrowser.TrackStyle
|
|
10
|
+
import timber.log.Timber
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Dev diagnostic (ADR 0011): the declarations on a resolved page that can never render.
|
|
14
|
+
*
|
|
15
|
+
* The block model deliberately gave up compile-time invalid-combination errors — combinations are
|
|
16
|
+
* not invalid, they are *inert*, the way `grid-template` on a non-grid element is inert in CSS.
|
|
17
|
+
* This is the recovery: at page resolution every declaration its own block makes unreadable is
|
|
18
|
+
* reported once, with the effective context and the fix.
|
|
19
|
+
*
|
|
20
|
+
* The line it draws, and never crosses: only **structural** inertness — a declaration the resolved
|
|
21
|
+
* block itself renders unreadable, on every surface (`gridWrap` outside a grid, `cardTint` with no
|
|
22
|
+
* card treatment in scope). *Surface* inertness is never reported, because declarations are
|
|
23
|
+
* aspirational: `imageShape` on Android Auto, or a card treatment on a pre-26 CarPlay, is intended
|
|
24
|
+
* usage — the renderer drops what it can't draw.
|
|
25
|
+
*
|
|
26
|
+
* Runs where every page passes regardless of its source (static config, route callback, or fetched
|
|
27
|
+
* browse JSON), and only on a cache miss, so a mistake is reported once per resolution rather than
|
|
28
|
+
* once per serve. Mirrors `ios/Browser/InertStyleDiagnostic.swift`.
|
|
29
|
+
*
|
|
30
|
+
* Debug builds only: [warn] is the whole seam, so a release build never even walks the page (the
|
|
31
|
+
* trade is that a page served only to a release build — an internal track, production browse JSON —
|
|
32
|
+
* reports nothing; reproduce it against a debug build).
|
|
33
|
+
*/
|
|
34
|
+
object InertStyleDiagnostic {
|
|
35
|
+
/**
|
|
36
|
+
* Reports every inert declaration on a resolved page. The one gate — the [BuildConfig] check is
|
|
37
|
+
* here rather than at the call sites so callers read unconditionally and the rule set stays free
|
|
38
|
+
* of build-configuration knowledge.
|
|
39
|
+
*/
|
|
40
|
+
fun warn(path: String, pageStyle: SectionStyle?, sections: List<Section>) {
|
|
41
|
+
if (!BuildConfig.DEBUG) return
|
|
42
|
+
findings(path, pageStyle, sections).forEach { Timber.w(it) }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The declarations that carry a structural inertness condition.
|
|
47
|
+
*
|
|
48
|
+
* Two keys of the block are deliberately absent — `display` and `artworkRendering`, dispositioned
|
|
49
|
+
* below instead.
|
|
50
|
+
*/
|
|
51
|
+
private enum class Property(val key: String, val isItemProperty: Boolean) {
|
|
52
|
+
// Container properties resolve by scope override (`section ?? page`) and never reach an item;
|
|
53
|
+
// item properties inherit (`track ?? section ?? page`), so they may also be declared per track.
|
|
54
|
+
GRID_WRAP("gridWrap", isItemProperty = false),
|
|
55
|
+
GRID_TILE("gridTile", isItemProperty = false),
|
|
56
|
+
IMAGE_SHAPE("imageShape", isItemProperty = true),
|
|
57
|
+
ACCESSORY_SYMBOL("accessorySymbol", isItemProperty = true),
|
|
58
|
+
CARD_TINT("cardTint", isItemProperty = true),
|
|
59
|
+
CARD_IMAGE("cardImage", isItemProperty = true);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether a value of this property, resolved into [context], has any rendering to affect. Each
|
|
63
|
+
* branch is the spec's own "inert unless" clause.
|
|
64
|
+
*/
|
|
65
|
+
fun isInert(context: Context): Boolean =
|
|
66
|
+
when (this) {
|
|
67
|
+
GRID_WRAP,
|
|
68
|
+
GRID_TILE -> !context.isGrid
|
|
69
|
+
IMAGE_SHAPE -> !context.isGrid || context.isCard
|
|
70
|
+
ACCESSORY_SYMBOL -> context.isCard
|
|
71
|
+
CARD_TINT,
|
|
72
|
+
CARD_IMAGE -> !context.isCard
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The rule the declaration falls foul of, stated positively. */
|
|
76
|
+
val rule: String
|
|
77
|
+
get() =
|
|
78
|
+
when (this) {
|
|
79
|
+
GRID_WRAP,
|
|
80
|
+
GRID_TILE -> "`$key` renders only in a grid"
|
|
81
|
+
IMAGE_SHAPE -> "only plain and condensed tiles have a shape"
|
|
82
|
+
ACCESSORY_SYMBOL -> "card tiles have no accessory slot"
|
|
83
|
+
CARD_TINT,
|
|
84
|
+
CARD_IMAGE -> "the card properties render only under `gridTile: 'card'`"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
val hint: String
|
|
88
|
+
get() =
|
|
89
|
+
when (this) {
|
|
90
|
+
GRID_WRAP,
|
|
91
|
+
GRID_TILE -> "Declare `display: 'grid'` on the section or the page, or drop `$key`."
|
|
92
|
+
IMAGE_SHAPE -> "Render the items as plain or condensed grid tiles, or drop `imageShape`."
|
|
93
|
+
ACCESSORY_SYMBOL -> "Use `gridTile: 'plain'` or `'condensed'`, or drop `accessorySymbol`."
|
|
94
|
+
CARD_TINT,
|
|
95
|
+
CARD_IMAGE -> "Declare `display: 'grid'` with `gridTile: 'card'`, or drop `$key`."
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fun declaredIn(style: SectionStyle?): Boolean =
|
|
99
|
+
when (this) {
|
|
100
|
+
GRID_WRAP -> style?.gridWrap != null
|
|
101
|
+
GRID_TILE -> style?.gridTile != null
|
|
102
|
+
IMAGE_SHAPE -> style?.imageShape != null
|
|
103
|
+
ACCESSORY_SYMBOL -> style?.accessorySymbol != null
|
|
104
|
+
CARD_TINT -> style?.cardTint != null
|
|
105
|
+
CARD_IMAGE -> style?.cardImage != null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Nitro flattens the spec's `extends`, so the two blocks share no type — hence two lookups over
|
|
109
|
+
// the same key set rather than one interface.
|
|
110
|
+
fun declaredIn(style: TrackStyle?): Boolean =
|
|
111
|
+
when (this) {
|
|
112
|
+
// Container properties have no Track slot to be declared in.
|
|
113
|
+
GRID_WRAP,
|
|
114
|
+
GRID_TILE -> false
|
|
115
|
+
IMAGE_SHAPE -> style?.imageShape != null
|
|
116
|
+
ACCESSORY_SYMBOL -> style?.accessorySymbol != null
|
|
117
|
+
CARD_TINT -> style?.cardTint != null
|
|
118
|
+
CARD_IMAGE -> style?.cardImage != null
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The rest of the block's keys, dispositioned rather than ruled on: `artworkRendering` has no
|
|
124
|
+
* structural inertness condition (artwork renders in every layout), and `display` is positional —
|
|
125
|
+
* inert only in the one position that opens no page (see [playableDisplayFinding]). Declared here
|
|
126
|
+
* so the spec-level completeness test can hold this file to the whole vocabulary: every key of
|
|
127
|
+
* the block is a rule, always renderable, or positional, and a newly added property is a decision
|
|
128
|
+
* rather than an omission.
|
|
129
|
+
*/
|
|
130
|
+
val ALWAYS_RENDERABLE = listOf("artworkRendering")
|
|
131
|
+
|
|
132
|
+
val POSITIONAL = listOf("display")
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A section's effective container context: the two container values every inertness condition is
|
|
136
|
+
* stated against.
|
|
137
|
+
*
|
|
138
|
+
* Folded through [StyleResolver] rather than re-deriving the scope override: the diagnostic
|
|
139
|
+
* predicts what a renderer will do with these declarations, so it has to resolve them the way the
|
|
140
|
+
* renderers do.
|
|
141
|
+
*/
|
|
142
|
+
private class Context(section: SectionStyle?, page: SectionStyle?) {
|
|
143
|
+
private val folded = StyleResolver.sectionStyle(section, page)
|
|
144
|
+
val isGrid = folded.display == StyleDisplay.GRID
|
|
145
|
+
val tile = folded.gridTile ?: GridTile.PLAIN
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Whether the card treatment is what this container renders — the `card*` family's condition.
|
|
149
|
+
*/
|
|
150
|
+
val isCard: Boolean
|
|
151
|
+
get() = isGrid && tile == GridTile.CARD
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The effective style, for the message — in the wire spelling the author types ([toWireString]
|
|
155
|
+
* is the decoder's own mapping). `gridTile` is quoted only where a grid gives it meaning:
|
|
156
|
+
* naming it on a list would be advice to fix the wrong property.
|
|
157
|
+
*/
|
|
158
|
+
val described: String
|
|
159
|
+
get() = if (!isGrid) "display 'list'" else "display 'grid', gridTile '${tile.toWireString()}'"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Every inert declaration on a resolved page, as ready-to-log messages — pure, so the whole rule
|
|
164
|
+
* set is testable off-device; the caller logs.
|
|
165
|
+
*
|
|
166
|
+
* @param path the resolved path, named in every message.
|
|
167
|
+
* @param pageStyle the page's own block (`ResolvedTrack.style`).
|
|
168
|
+
* @param sections the page's normalized sections, blocks *unfolded* — the diagnostic attributes
|
|
169
|
+
* each finding to the level that declared it, so it must see the levels before [StyleResolver]
|
|
170
|
+
* merges them.
|
|
171
|
+
*/
|
|
172
|
+
fun findings(path: String, pageStyle: SectionStyle?, sections: List<Section>): List<String> {
|
|
173
|
+
val findings = mutableListOf<String>()
|
|
174
|
+
// A page-level declaration is judged across the whole page: it is live if it renders in any
|
|
175
|
+
// section it reaches, however many others ignore it.
|
|
176
|
+
val pageReached = mutableSetOf<Property>()
|
|
177
|
+
val pageRendered = mutableSetOf<Property>()
|
|
178
|
+
|
|
179
|
+
sections.forEachIndexed { index, section ->
|
|
180
|
+
val context = Context(section.style, pageStyle)
|
|
181
|
+
val sectionLabel = label(section, index)
|
|
182
|
+
|
|
183
|
+
for (property in Property.entries) {
|
|
184
|
+
val inert = property.isInert(context)
|
|
185
|
+
|
|
186
|
+
if (property.declaredIn(section.style)) {
|
|
187
|
+
if (inert) {
|
|
188
|
+
findings +=
|
|
189
|
+
message(property, path, sectionLabel, "its effective style is ${context.described}")
|
|
190
|
+
}
|
|
191
|
+
} else if (property.declaredIn(pageStyle)) {
|
|
192
|
+
// The section takes the page's value (nothing closer overrode it).
|
|
193
|
+
pageReached += property
|
|
194
|
+
if (!inert) pageRendered += property
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!property.isItemProperty || !inert) continue
|
|
198
|
+
val tracks = section.children.count { property.declaredIn(it.style) }
|
|
199
|
+
if (tracks > 0) {
|
|
200
|
+
findings +=
|
|
201
|
+
message(
|
|
202
|
+
property,
|
|
203
|
+
path,
|
|
204
|
+
"${trackCount(tracks)} in $sectionLabel",
|
|
205
|
+
"the section's effective style is ${context.described}",
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
playableDisplayFinding(section, path, sectionLabel)?.let { findings += it }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (property in Property.entries) {
|
|
214
|
+
if (property in pageReached && property !in pageRendered) {
|
|
215
|
+
findings +=
|
|
216
|
+
message(property, path, "the page block", "no section that inherits it renders one")
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return findings
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* `display` is positional, so it is never inert *in* a container — but on a track that renders
|
|
225
|
+
* playable (`src` wins the rendering) it promises the layout of a page that never opens.
|
|
226
|
+
*/
|
|
227
|
+
private fun playableDisplayFinding(section: Section, path: String, label: String): String? {
|
|
228
|
+
val tracks = section.children.count { it.src != null && it.style?.display != null }
|
|
229
|
+
if (tracks == 0) return null
|
|
230
|
+
return message(
|
|
231
|
+
path = path,
|
|
232
|
+
owner = "${trackCount(tracks)} in $label",
|
|
233
|
+
key = "display",
|
|
234
|
+
reason =
|
|
235
|
+
"`display` is the layout promise for the page a track opens, and a track rendered playable " +
|
|
236
|
+
"opens none",
|
|
237
|
+
hint = "Drop `display`, or give the track a `path` if it should be browsable.",
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private fun message(property: Property, path: String, owner: String, scope: String): String =
|
|
242
|
+
message(
|
|
243
|
+
path = path,
|
|
244
|
+
owner = owner,
|
|
245
|
+
key = property.key,
|
|
246
|
+
reason = "${property.rule}, but $scope",
|
|
247
|
+
hint = property.hint,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The one message skeleton — every finding names the page, the declaration, who declared it, why
|
|
252
|
+
* it can't render, and what to do about it.
|
|
253
|
+
*/
|
|
254
|
+
private fun message(
|
|
255
|
+
path: String,
|
|
256
|
+
owner: String,
|
|
257
|
+
key: String,
|
|
258
|
+
reason: String,
|
|
259
|
+
hint: String,
|
|
260
|
+
): String =
|
|
261
|
+
"Inert style declaration on page '$path': `style.$key` on $owner never renders — $reason. $hint"
|
|
262
|
+
|
|
263
|
+
private fun trackCount(count: Int): String = "$count ${if (count == 1) "track" else "tracks"}"
|
|
264
|
+
|
|
265
|
+
private fun label(section: Section, index: Int): String =
|
|
266
|
+
section.title?.takeIf { it.isNotEmpty() }?.let { "section '$it'" } ?: "section #${index + 1}"
|
|
267
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
package com.audiobrowser.browser
|
|
2
2
|
|
|
3
|
+
import com.audiobrowser.util.EMPTY_SECTION_STYLE
|
|
4
|
+
import com.audiobrowser.util.EMPTY_TRACK_STYLE
|
|
3
5
|
import com.audiobrowser.util.toArtworkRendering
|
|
6
|
+
import com.audiobrowser.util.toCardImage
|
|
7
|
+
import com.audiobrowser.util.toGridTile
|
|
8
|
+
import com.audiobrowser.util.toImageShape
|
|
4
9
|
import com.audiobrowser.util.toStyleDisplay
|
|
5
10
|
import com.margelo.nitro.audiobrowser.CarPlaySiriListButtonPosition
|
|
6
11
|
import com.margelo.nitro.audiobrowser.ResolvedTrack
|
|
@@ -35,7 +40,6 @@ data class JsonTrackRequest(
|
|
|
35
40
|
@Serializable
|
|
36
41
|
data class JsonSection(
|
|
37
42
|
val title: String? = null,
|
|
38
|
-
val subtitle: String? = null,
|
|
39
43
|
val style: JsonElement? = null,
|
|
40
44
|
val path: String? = null,
|
|
41
45
|
val children: List<JsonTrack>,
|
|
@@ -106,19 +110,32 @@ private fun JsonObject.booleanField(key: String): Boolean? =
|
|
|
106
110
|
*/
|
|
107
111
|
private fun JsonElement?.toTrackStyle(): TrackStyle? =
|
|
108
112
|
(this as? JsonObject)?.let { obj ->
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
TrackStyle(
|
|
114
|
+
display = obj.stringField("display").toStyleDisplay(),
|
|
115
|
+
artworkRendering = obj.stringField("artworkRendering").toArtworkRendering(),
|
|
116
|
+
imageShape = obj.stringField("imageShape").toImageShape(),
|
|
117
|
+
// 'none' is a legitimate accessory value (the inheritance escape);
|
|
118
|
+
// only emptiness collapses.
|
|
119
|
+
accessorySymbol = obj.stringField("accessorySymbol")?.takeIf { it.isNotEmpty() },
|
|
120
|
+
cardTint = obj.stringField("cardTint")?.takeIf { it.isNotEmpty() },
|
|
121
|
+
cardImage = obj.stringField("cardImage").toCardImage(),
|
|
122
|
+
)
|
|
123
|
+
.takeIf { it != EMPTY_TRACK_STYLE }
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
private fun JsonElement?.toSectionStyle(): SectionStyle? =
|
|
116
127
|
(this as? JsonObject)?.let { obj ->
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
SectionStyle(
|
|
129
|
+
display = obj.stringField("display").toStyleDisplay(),
|
|
130
|
+
artworkRendering = obj.stringField("artworkRendering").toArtworkRendering(),
|
|
131
|
+
imageShape = obj.stringField("imageShape").toImageShape(),
|
|
132
|
+
accessorySymbol = obj.stringField("accessorySymbol")?.takeIf { it.isNotEmpty() },
|
|
133
|
+
cardTint = obj.stringField("cardTint")?.takeIf { it.isNotEmpty() },
|
|
134
|
+
cardImage = obj.stringField("cardImage").toCardImage(),
|
|
135
|
+
gridWrap = obj.booleanField("gridWrap"),
|
|
136
|
+
gridTile = obj.stringField("gridTile").toGridTile(),
|
|
137
|
+
)
|
|
138
|
+
.takeIf { it != EMPTY_SECTION_STYLE }
|
|
122
139
|
}
|
|
123
140
|
|
|
124
141
|
private fun String?.toCarPlaySiriListButtonPosition(): CarPlaySiriListButtonPosition? {
|
|
@@ -132,7 +149,6 @@ private fun String?.toCarPlaySiriListButtonPosition(): CarPlaySiriListButtonPosi
|
|
|
132
149
|
fun JsonSection.toNitro(): Section {
|
|
133
150
|
return Section(
|
|
134
151
|
title = title,
|
|
135
|
-
subtitle = subtitle,
|
|
136
152
|
style = style.toSectionStyle(),
|
|
137
153
|
path = path,
|
|
138
154
|
children = children.map { it.toNitro() }.toTypedArray(),
|
|
@@ -59,7 +59,7 @@ object SectionScope {
|
|
|
59
59
|
* search results — ADR 0010).
|
|
60
60
|
*/
|
|
61
61
|
fun untitledSection(children: Array<Track>): Section =
|
|
62
|
-
Section(title = null,
|
|
62
|
+
Section(title = null, style = null, path = null, children = children)
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* The canonical sectioned shape: `sections` wins when present; plain `children` is authoring sugar
|
|
@@ -29,7 +29,12 @@ object StyleResolver {
|
|
|
29
29
|
SectionStyle(
|
|
30
30
|
display = section?.display ?: page?.display,
|
|
31
31
|
artworkRendering = section?.artworkRendering ?: page?.artworkRendering,
|
|
32
|
+
imageShape = section?.imageShape ?: page?.imageShape,
|
|
33
|
+
accessorySymbol = section?.accessorySymbol ?: page?.accessorySymbol,
|
|
34
|
+
cardTint = section?.cardTint ?: page?.cardTint,
|
|
35
|
+
cardImage = section?.cardImage ?: page?.cardImage,
|
|
32
36
|
gridWrap = section?.gridWrap ?: page?.gridWrap,
|
|
37
|
+
gridTile = section?.gridTile ?: page?.gridTile,
|
|
33
38
|
)
|
|
34
39
|
|
|
35
40
|
/**
|
|
@@ -45,5 +50,11 @@ object StyleResolver {
|
|
|
45
50
|
// renderer can mistake the handle's page promise for this item's own layout.
|
|
46
51
|
display = null,
|
|
47
52
|
artworkRendering = track?.artworkRendering ?: section?.artworkRendering,
|
|
53
|
+
imageShape = track?.imageShape ?: section?.imageShape,
|
|
54
|
+
// 'none' resolves like any value — a renderer treats it as "no accessory, derived
|
|
55
|
+
// behavior" (the inheritance escape). No Android surface draws these properties today.
|
|
56
|
+
accessorySymbol = track?.accessorySymbol ?: section?.accessorySymbol,
|
|
57
|
+
cardTint = track?.cardTint ?: section?.cardTint,
|
|
58
|
+
cardImage = track?.cardImage ?: section?.cardImage,
|
|
48
59
|
)
|
|
49
60
|
}
|
|
@@ -30,7 +30,17 @@ fun ResolvedTrack.toTrack(): Track =
|
|
|
30
30
|
// properties (gridWrap) have no meaning on a plain Track, so they drop —
|
|
31
31
|
// Nitro flattens the spec's `extends`, making this narrowing a hand-written
|
|
32
32
|
// field list rather than an upcast (ADR 0011).
|
|
33
|
-
style =
|
|
33
|
+
style =
|
|
34
|
+
style?.let {
|
|
35
|
+
TrackStyle(
|
|
36
|
+
display = it.display,
|
|
37
|
+
artworkRendering = it.artworkRendering,
|
|
38
|
+
imageShape = it.imageShape,
|
|
39
|
+
accessorySymbol = it.accessorySymbol,
|
|
40
|
+
cardTint = it.cardTint,
|
|
41
|
+
cardImage = it.cardImage,
|
|
42
|
+
)
|
|
43
|
+
},
|
|
34
44
|
disabled = disabled,
|
|
35
45
|
favorited = favorited,
|
|
36
46
|
live = live,
|
|
@@ -20,7 +20,7 @@ data class PlayerUpdateOptions(
|
|
|
20
20
|
var progressUpdateEventInterval: Double? = null,
|
|
21
21
|
|
|
22
22
|
// Player capabilities - most enabled by default, only false values disable
|
|
23
|
-
// Exceptions:
|
|
23
|
+
// Exceptions: jumpForward, jumpBackward default to false
|
|
24
24
|
var capabilities: PlayerCapabilities =
|
|
25
25
|
PlayerCapabilities(
|
|
26
26
|
play = null,
|