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.
- package/README.md +10 -0
- package/android/build.gradle +20 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +5 -1
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +8 -0
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +27 -9
- 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/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 +8 -1
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +49 -3
- 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 +8 -8
- package/ios/Browser/BrowserManager.swift +106 -45
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +81 -10
- 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/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +54 -11
- package/ios/Model/NitroTypeStubs.swift +60 -0
- 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/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/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/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/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-nodes.d.ts +104 -2
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- 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__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 +12 -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 +6 -0
- 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 +6 -0
- 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/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/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.hpp +54 -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/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- 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++/SectionStyle.hpp +32 -2
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +25 -2
- package/package.json +7 -2
- 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-nodes.ts +112 -2
- 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
|
|
@@ -757,7 +758,10 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
757
758
|
return Promise.async(mainScope) {
|
|
758
759
|
Timber.d("Searching for: $query")
|
|
759
760
|
val searchResults = browserManager.search(query)
|
|
760
|
-
|
|
761
|
+
// Flattened, not `children`: a resolved page carries its rows in
|
|
762
|
+
// `sections` (ADR 0010) and search builds exactly one untitled section,
|
|
763
|
+
// so `children` is always null here and this returned nothing at all.
|
|
764
|
+
searchResults.flattenedChildren?.toTypedArray() ?: emptyArray()
|
|
761
765
|
}
|
|
762
766
|
}
|
|
763
767
|
|
|
@@ -562,6 +562,10 @@ class BrowserManager {
|
|
|
562
562
|
/** Clears all cached content. */
|
|
563
563
|
fun clearContentCache() {
|
|
564
564
|
contentCache.evictAll()
|
|
565
|
+
// The search slot is content too: a locale switch or a re-auth changes titles, src, artwork
|
|
566
|
+
// and availability, and nothing else ever evicts it, so a repeated query returned
|
|
567
|
+
// pre-invalidation results forever. (Matches the Swift twin's clearContentCache.)
|
|
568
|
+
lastSearch = null
|
|
565
569
|
// Bump the layer generation so request/browse resolver thunks are re-resolved on the next
|
|
566
570
|
// request (invalidateAllContent → clearContentCache is the documented re-resolve trigger).
|
|
567
571
|
layerGeneration += 1
|
|
@@ -594,6 +598,10 @@ class BrowserManager {
|
|
|
594
598
|
// section — ADR 0010), then generate contextual paths and transform artwork
|
|
595
599
|
// URLs. The output never carries `children`.
|
|
596
600
|
val sections = resolvedTrack.normalizedSections ?: return resolvedTrack
|
|
601
|
+
// Dev diagnostic (ADR 0011) — before the blocks are folded, while each level is
|
|
602
|
+
// still attributable. Every page passes here whatever its source, and only on a
|
|
603
|
+
// cache miss. No-op outside debug builds.
|
|
604
|
+
InertStyleDiagnostic.warn(path, resolvedTrack.style, sections)
|
|
597
605
|
// The contextual index is the flat page position — children concatenated in
|
|
598
606
|
// section order (ADR 0009/0010).
|
|
599
607
|
var flatIndex = 0
|
|
@@ -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
|
|
@@ -106,19 +111,32 @@ private fun JsonObject.booleanField(key: String): Boolean? =
|
|
|
106
111
|
*/
|
|
107
112
|
private fun JsonElement?.toTrackStyle(): TrackStyle? =
|
|
108
113
|
(this as? JsonObject)?.let { obj ->
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
TrackStyle(
|
|
115
|
+
display = obj.stringField("display").toStyleDisplay(),
|
|
116
|
+
artworkRendering = obj.stringField("artworkRendering").toArtworkRendering(),
|
|
117
|
+
imageShape = obj.stringField("imageShape").toImageShape(),
|
|
118
|
+
// 'none' is a legitimate accessory value (the inheritance escape);
|
|
119
|
+
// only emptiness collapses.
|
|
120
|
+
accessorySymbol = obj.stringField("accessorySymbol")?.takeIf { it.isNotEmpty() },
|
|
121
|
+
cardTint = obj.stringField("cardTint")?.takeIf { it.isNotEmpty() },
|
|
122
|
+
cardImage = obj.stringField("cardImage").toCardImage(),
|
|
123
|
+
)
|
|
124
|
+
.takeIf { it != EMPTY_TRACK_STYLE }
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
private fun JsonElement?.toSectionStyle(): SectionStyle? =
|
|
116
128
|
(this as? JsonObject)?.let { obj ->
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
SectionStyle(
|
|
130
|
+
display = obj.stringField("display").toStyleDisplay(),
|
|
131
|
+
artworkRendering = obj.stringField("artworkRendering").toArtworkRendering(),
|
|
132
|
+
imageShape = obj.stringField("imageShape").toImageShape(),
|
|
133
|
+
accessorySymbol = obj.stringField("accessorySymbol")?.takeIf { it.isNotEmpty() },
|
|
134
|
+
cardTint = obj.stringField("cardTint")?.takeIf { it.isNotEmpty() },
|
|
135
|
+
cardImage = obj.stringField("cardImage").toCardImage(),
|
|
136
|
+
gridWrap = obj.booleanField("gridWrap"),
|
|
137
|
+
gridTile = obj.stringField("gridTile").toGridTile(),
|
|
138
|
+
)
|
|
139
|
+
.takeIf { it != EMPTY_SECTION_STYLE }
|
|
122
140
|
}
|
|
123
141
|
|
|
124
142
|
private fun String?.toCarPlaySiriListButtonPosition(): CarPlaySiriListButtonPosition? {
|
|
@@ -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,
|
|
@@ -4,8 +4,11 @@ import android.content.Context
|
|
|
4
4
|
import android.content.SharedPreferences
|
|
5
5
|
import androidx.core.content.edit
|
|
6
6
|
import androidx.media3.common.C
|
|
7
|
+
import com.audiobrowser.util.EMPTY_TRACK_STYLE
|
|
7
8
|
import com.audiobrowser.util.artworkOf
|
|
8
9
|
import com.audiobrowser.util.toArtworkRendering
|
|
10
|
+
import com.audiobrowser.util.toCardImage
|
|
11
|
+
import com.audiobrowser.util.toImageShape
|
|
9
12
|
import com.audiobrowser.util.toStyleDisplay
|
|
10
13
|
import com.audiobrowser.util.toWireString
|
|
11
14
|
import com.audiobrowser.util.url
|
|
@@ -164,7 +167,11 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
164
167
|
// Apply player settings from persisted state
|
|
165
168
|
player.repeatMode = state.repeatMode
|
|
166
169
|
player.shuffleMode = state.shuffleEnabled
|
|
167
|
-
|
|
170
|
+
// Guarded because this input is untrusted: ExoPlayer's setPlaybackSpeed rejects a
|
|
171
|
+
// non-positive speed with IllegalArgumentException, and a snapshot holding one (a
|
|
172
|
+
// corrupted prefs file — the normal write path can't produce it) would then throw on
|
|
173
|
+
// every resume, permanently, since the bad value stays persisted.
|
|
174
|
+
if (state.playbackSpeed > 0f) player.playbackSpeed = state.playbackSpeed
|
|
168
175
|
|
|
169
176
|
Timber.d(
|
|
170
177
|
"Restored player settings: repeatMode=${state.repeatMode}, shuffle=${state.shuffleEnabled}, speed=${state.playbackSpeed}"
|
|
@@ -216,6 +223,10 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
216
223
|
JSONObject().apply {
|
|
217
224
|
put("display", style.display?.toWireString())
|
|
218
225
|
put("artworkRendering", style.artworkRendering?.toWireString())
|
|
226
|
+
put("imageShape", style.imageShape?.toWireString())
|
|
227
|
+
put("accessorySymbol", style.accessorySymbol)
|
|
228
|
+
put("cardTint", style.cardTint)
|
|
229
|
+
put("cardImage", style.cardImage?.toWireString())
|
|
219
230
|
}
|
|
220
231
|
},
|
|
221
232
|
)
|
|
@@ -254,11 +265,15 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
254
265
|
TrackStyle(
|
|
255
266
|
display = style.optString("display").toStyleDisplay(),
|
|
256
267
|
artworkRendering = style.optString("artworkRendering").toArtworkRendering(),
|
|
268
|
+
imageShape = style.optString("imageShape").toImageShape(),
|
|
269
|
+
accessorySymbol = style.optString("accessorySymbol").takeIf { it.isNotEmpty() },
|
|
270
|
+
cardTint = style.optString("cardTint").takeIf { it.isNotEmpty() },
|
|
271
|
+
cardImage = style.optString("cardImage").toCardImage(),
|
|
257
272
|
)
|
|
258
273
|
}
|
|
259
274
|
// An empty block collapses to "no declaration" — one shape on
|
|
260
275
|
// every platform, matching the browse decoders.
|
|
261
|
-
?.takeIf { it
|
|
276
|
+
?.takeIf { it != EMPTY_TRACK_STYLE },
|
|
262
277
|
disabled =
|
|
263
278
|
if (obj.has("disabled") && !obj.isNull("disabled")) obj.getBoolean("disabled")
|
|
264
279
|
else null,
|
|
@@ -668,6 +668,9 @@ class Player(internal val context: Context) {
|
|
|
668
668
|
) {
|
|
669
669
|
val mediaItems = TrackFactory.toMedia3(tracks).toMutableList()
|
|
670
670
|
exoPlayer.setMediaItems(mediaItems, startIndex, startPositionMs)
|
|
671
|
+
if (mediaItems.isNotEmpty()) {
|
|
672
|
+
exoPlayer.setShuffleOrder(shuffleOrderLedBy(startIndex, mediaItems.size))
|
|
673
|
+
}
|
|
671
674
|
queueSourcePath = sourcePath
|
|
672
675
|
exoPlayer.prepare()
|
|
673
676
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.audiobrowser.player
|
|
2
|
+
|
|
3
|
+
import androidx.media3.exoplayer.source.ShuffleOrder
|
|
4
|
+
import kotlin.random.Random
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A shuffle order over [count] items with [startIndex] pinned to shuffle position 0.
|
|
8
|
+
*
|
|
9
|
+
* Media3 does not do this for us. `ExoPlayerImpl.setMediaSources` hands the start index to
|
|
10
|
+
* `ShuffleOrder.cloneAndSet(insertionCount, startIndex)`, but that is an interface default of
|
|
11
|
+
* `cloneAndClear().cloneAndInsert(0, insertionCount)` — the start index is discarded — and
|
|
12
|
+
* `DefaultShuffleOrder` does not override it (verified against the pinned media3 build). So with
|
|
13
|
+
* shuffle on, the track playback starts from lands at a random position in the order, and a 1-in-N
|
|
14
|
+
* share of queues put it last: `nextMediaItemIndex` is then `INDEX_UNSET` from the very first
|
|
15
|
+
* track, so auto-advance ends the queue after one track and fires `onQueueEnded`. The
|
|
16
|
+
* voice-assistant cold resume expands a queue exactly this way, so it sits squarely on that path.
|
|
17
|
+
*
|
|
18
|
+
* Applied unconditionally by `Player.setQueue`, like the Swift twin's `adoptInitialCurrent`:
|
|
19
|
+
* enabling shuffle later does not regenerate the order, so it has to be sane whether or not shuffle
|
|
20
|
+
* is on right now.
|
|
21
|
+
*/
|
|
22
|
+
internal fun shuffleOrderLedBy(startIndex: Int, count: Int, random: Random = Random): ShuffleOrder {
|
|
23
|
+
val first = startIndex.coerceIn(0, (count - 1).coerceAtLeast(0))
|
|
24
|
+
val indices = IntArray(count)
|
|
25
|
+
if (count > 0) {
|
|
26
|
+
indices[0] = first
|
|
27
|
+
((0 until count) - first).shuffled().forEachIndexed { i, index -> indices[i + 1] = index }
|
|
28
|
+
}
|
|
29
|
+
return ShuffleOrder.DefaultShuffleOrder(indices, random.nextLong())
|
|
30
|
+
}
|
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
package com.audiobrowser.util
|
|
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
|
|
7
|
+
import com.margelo.nitro.audiobrowser.SectionStyle
|
|
4
8
|
import com.margelo.nitro.audiobrowser.StyleDisplay
|
|
9
|
+
import com.margelo.nitro.audiobrowser.TrackStyle
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The all-nil blocks a decode collapses to "no declaration". Every field is listed exactly once
|
|
13
|
+
* here — the decoders and the persistence restore compare against these instead of re-enumerating
|
|
14
|
+
* the fields in each guard (a constructor gaining a field breaks this file, not silently a guard).
|
|
15
|
+
*/
|
|
16
|
+
internal val EMPTY_TRACK_STYLE =
|
|
17
|
+
TrackStyle(
|
|
18
|
+
display = null,
|
|
19
|
+
artworkRendering = null,
|
|
20
|
+
imageShape = null,
|
|
21
|
+
accessorySymbol = null,
|
|
22
|
+
cardTint = null,
|
|
23
|
+
cardImage = null,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
internal val EMPTY_SECTION_STYLE =
|
|
27
|
+
SectionStyle(
|
|
28
|
+
display = null,
|
|
29
|
+
artworkRendering = null,
|
|
30
|
+
imageShape = null,
|
|
31
|
+
accessorySymbol = null,
|
|
32
|
+
cardTint = null,
|
|
33
|
+
cardImage = null,
|
|
34
|
+
gridWrap = null,
|
|
35
|
+
gridTile = null,
|
|
36
|
+
)
|
|
5
37
|
|
|
6
38
|
/**
|
|
7
39
|
* The style block's wire strings (ADR 0011) ↔ Nitro enums, shared by the browse decoder and the
|
|
@@ -33,3 +65,44 @@ internal fun String?.toArtworkRendering(): ArtworkRendering? =
|
|
|
33
65
|
"stencil" -> ArtworkRendering.STENCIL
|
|
34
66
|
else -> null
|
|
35
67
|
}
|
|
68
|
+
|
|
69
|
+
internal fun ImageShape.toWireString(): String =
|
|
70
|
+
when (this) {
|
|
71
|
+
ImageShape.CIRCULAR -> "circular"
|
|
72
|
+
ImageShape.ROUNDED_RECTANGLE -> "rounded-rectangle"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
internal fun String?.toImageShape(): ImageShape? =
|
|
76
|
+
when (this?.takeIf { it.isNotEmpty() }?.lowercase()) {
|
|
77
|
+
"circular" -> ImageShape.CIRCULAR
|
|
78
|
+
"rounded-rectangle" -> ImageShape.ROUNDED_RECTANGLE
|
|
79
|
+
else -> null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
internal fun GridTile.toWireString(): String =
|
|
83
|
+
when (this) {
|
|
84
|
+
GridTile.PLAIN -> "plain"
|
|
85
|
+
GridTile.CARD -> "card"
|
|
86
|
+
GridTile.CONDENSED -> "condensed"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
internal fun String?.toGridTile(): GridTile? =
|
|
90
|
+
when (this?.takeIf { it.isNotEmpty() }?.lowercase()) {
|
|
91
|
+
"plain" -> GridTile.PLAIN
|
|
92
|
+
"card" -> GridTile.CARD
|
|
93
|
+
"condensed" -> GridTile.CONDENSED
|
|
94
|
+
else -> null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
internal fun CardImage.toWireString(): String =
|
|
98
|
+
when (this) {
|
|
99
|
+
CardImage.NORMAL -> "normal"
|
|
100
|
+
CardImage.BACKGROUND -> "background"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
internal fun String?.toCardImage(): CardImage? =
|
|
104
|
+
when (this?.takeIf { it.isNotEmpty() }?.lowercase()) {
|
|
105
|
+
"normal" -> CardImage.NORMAL
|
|
106
|
+
"background" -> CardImage.BACKGROUND
|
|
107
|
+
else -> null
|
|
108
|
+
}
|