react-native-audio-browser 0.2.0-next.21 → 0.2.0-next.22
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 +1 -1
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +28 -7
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +2 -0
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +32 -8
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +21 -0
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +6 -0
- package/ios/Browser/InertStyleDiagnostic.swift +25 -8
- package/ios/Browser/SectionPresentation.swift +18 -5
- package/ios/CarPlay/CarPlayListItemFactory.swift +74 -54
- package/ios/Model/NitroTypeStubs.swift +3 -0
- package/ios/Tests/InertStyleDiagnosticTests.swift +17 -8
- package/ios/Tests/JsonModelsTests.swift +14 -0
- package/ios/Tests/SectionPresentationTests.swift +21 -13
- package/ios/Tests/StyleResolverTests.swift +5 -0
- package/lib/typescript/src/types/browser-nodes.d.ts +28 -21
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JGridTile.hpp +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +3 -2
- package/nitrogen/generated/ios/swift/GridTile.swift +4 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +6 -2
- package/package.json +1 -1
- package/src/types/browser-nodes.ts +28 -21
- package/website/guide/artwork.md +1 -1
- package/website/guide/browser.md +11 -5
- package/website/guide/track.md +1 -1
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ Full guides and the complete [API Reference](https://audiobrowser.dev/api/) live
|
|
|
172
172
|
|
|
173
173
|
**Troubleshooting** — [Networking in native callbacks](https://audiobrowser.dev/guide/native-callback-fetch) · [Android SSL / trust anchors](https://audiobrowser.dev/guide/android-certificates)
|
|
174
174
|
|
|
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.
|
|
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. Its **Style Lab** tab renders every CarPlay `style` declaration, each shelf titled with the block it draws ([source](https://github.com/radio-garden/react-native-audio-browser/blob/main/apps/example-native/src/utils/style-lab.ts)).
|
|
176
176
|
|
|
177
177
|
### For coding agents
|
|
178
178
|
|
|
@@ -66,8 +66,8 @@ object InertStyleDiagnostic {
|
|
|
66
66
|
when (this) {
|
|
67
67
|
GRID_WRAP,
|
|
68
68
|
GRID_TILE -> !context.isGrid
|
|
69
|
-
IMAGE_SHAPE -> !context.
|
|
70
|
-
ACCESSORY_SYMBOL -> context.
|
|
69
|
+
IMAGE_SHAPE -> !context.isShaped
|
|
70
|
+
ACCESSORY_SYMBOL -> context.isGrid && !context.isShaped
|
|
71
71
|
CARD_TINT,
|
|
72
72
|
CARD_IMAGE -> !context.isCard
|
|
73
73
|
}
|
|
@@ -78,8 +78,9 @@ object InertStyleDiagnostic {
|
|
|
78
78
|
when (this) {
|
|
79
79
|
GRID_WRAP,
|
|
80
80
|
GRID_TILE -> "`$key` renders only in a grid"
|
|
81
|
-
IMAGE_SHAPE -> "only
|
|
82
|
-
ACCESSORY_SYMBOL ->
|
|
81
|
+
IMAGE_SHAPE -> "only image and condensed tiles have a shape"
|
|
82
|
+
ACCESSORY_SYMBOL ->
|
|
83
|
+
"among grid tiles only image and condensed ones have an accessory slot"
|
|
83
84
|
CARD_TINT,
|
|
84
85
|
CARD_IMAGE -> "the card properties render only under `gridTile: 'card'`"
|
|
85
86
|
}
|
|
@@ -89,8 +90,9 @@ object InertStyleDiagnostic {
|
|
|
89
90
|
when (this) {
|
|
90
91
|
GRID_WRAP,
|
|
91
92
|
GRID_TILE -> "Declare `display: 'grid'` on the section or the page, or drop `$key`."
|
|
92
|
-
IMAGE_SHAPE -> "
|
|
93
|
-
ACCESSORY_SYMBOL ->
|
|
93
|
+
IMAGE_SHAPE -> "Use `gridTile: 'image'` or `'condensed'` in a grid, or drop `imageShape`."
|
|
94
|
+
ACCESSORY_SYMBOL ->
|
|
95
|
+
"Use `gridTile: 'image'` or `'condensed'`, or a list, or drop `accessorySymbol`."
|
|
94
96
|
CARD_TINT,
|
|
95
97
|
CARD_IMAGE -> "Declare `display: 'grid'` with `gridTile: 'card'`, or drop `$key`."
|
|
96
98
|
}
|
|
@@ -146,9 +148,28 @@ object InertStyleDiagnostic {
|
|
|
146
148
|
|
|
147
149
|
/**
|
|
148
150
|
* Whether the card treatment is what this container renders — the `card*` family's condition.
|
|
151
|
+
* Exhaustive, like [isShaped]: a new tile kind has to be ruled on here.
|
|
149
152
|
*/
|
|
150
153
|
val isCard: Boolean
|
|
151
|
-
get() =
|
|
154
|
+
get() =
|
|
155
|
+
isGrid &&
|
|
156
|
+
when (tile) {
|
|
157
|
+
GridTile.CARD -> true
|
|
158
|
+
GridTile.PLAIN,
|
|
159
|
+
GridTile.IMAGE,
|
|
160
|
+
GridTile.CONDENSED -> false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Whether this container renders a tile family with a shape and an accessory slot. */
|
|
164
|
+
val isShaped: Boolean
|
|
165
|
+
get() =
|
|
166
|
+
isGrid &&
|
|
167
|
+
when (tile) {
|
|
168
|
+
GridTile.IMAGE,
|
|
169
|
+
GridTile.CONDENSED -> true
|
|
170
|
+
GridTile.PLAIN,
|
|
171
|
+
GridTile.CARD -> false
|
|
172
|
+
}
|
|
152
173
|
|
|
153
174
|
/**
|
|
154
175
|
* The effective style, for the message — in the wire spelling the author types ([toWireString]
|
|
@@ -82,6 +82,7 @@ internal fun String?.toImageShape(): ImageShape? =
|
|
|
82
82
|
internal fun GridTile.toWireString(): String =
|
|
83
83
|
when (this) {
|
|
84
84
|
GridTile.PLAIN -> "plain"
|
|
85
|
+
GridTile.IMAGE -> "image"
|
|
85
86
|
GridTile.CARD -> "card"
|
|
86
87
|
GridTile.CONDENSED -> "condensed"
|
|
87
88
|
}
|
|
@@ -89,6 +90,7 @@ internal fun GridTile.toWireString(): String =
|
|
|
89
90
|
internal fun String?.toGridTile(): GridTile? =
|
|
90
91
|
when (this?.takeIf { it.isNotEmpty() }?.lowercase()) {
|
|
91
92
|
"plain" -> GridTile.PLAIN
|
|
93
|
+
"image" -> GridTile.IMAGE
|
|
92
94
|
"card" -> GridTile.CARD
|
|
93
95
|
"condensed" -> GridTile.CONDENSED
|
|
94
96
|
else -> null
|
|
@@ -100,6 +100,7 @@ class InertStyleDiagnosticTest {
|
|
|
100
100
|
@Test
|
|
101
101
|
fun `imageShape needs a shaped tile`() {
|
|
102
102
|
val list = sectionStyle(imageShape = ImageShape.CIRCULAR)
|
|
103
|
+
val plain = sectionStyle(display = StyleDisplay.GRID, imageShape = ImageShape.CIRCULAR)
|
|
103
104
|
val cards =
|
|
104
105
|
sectionStyle(
|
|
105
106
|
display = StyleDisplay.GRID,
|
|
@@ -112,26 +113,49 @@ class InertStyleDiagnosticTest {
|
|
|
112
113
|
gridTile = GridTile.CONDENSED,
|
|
113
114
|
imageShape = ImageShape.CIRCULAR,
|
|
114
115
|
)
|
|
116
|
+
val image =
|
|
117
|
+
sectionStyle(
|
|
118
|
+
display = StyleDisplay.GRID,
|
|
119
|
+
gridTile = GridTile.IMAGE,
|
|
120
|
+
imageShape = ImageShape.CIRCULAR,
|
|
121
|
+
)
|
|
115
122
|
assertEquals(1, findings(listOf(section(style = list))).size)
|
|
123
|
+
assertEquals(1, findings(listOf(section(style = plain))).size)
|
|
116
124
|
assertEquals(1, findings(listOf(section(style = cards))).size)
|
|
117
|
-
// Plain and condensed tiles both take a shape.
|
|
118
125
|
assertTrue(findings(listOf(section(style = condensed))).isEmpty())
|
|
126
|
+
assertTrue(findings(listOf(section(style = image))).isEmpty())
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
@Test
|
|
122
|
-
fun `accessorySymbol
|
|
130
|
+
fun `accessorySymbol needs an accessory slot`() {
|
|
123
131
|
val rows = sectionStyle(accessorySymbol = "lock.fill")
|
|
132
|
+
val condensed =
|
|
133
|
+
sectionStyle(
|
|
134
|
+
display = StyleDisplay.GRID,
|
|
135
|
+
gridTile = GridTile.CONDENSED,
|
|
136
|
+
accessorySymbol = "lock.fill",
|
|
137
|
+
)
|
|
138
|
+
val image =
|
|
139
|
+
sectionStyle(
|
|
140
|
+
display = StyleDisplay.GRID,
|
|
141
|
+
gridTile = GridTile.IMAGE,
|
|
142
|
+
accessorySymbol = "lock.fill",
|
|
143
|
+
)
|
|
144
|
+
val plain = sectionStyle(display = StyleDisplay.GRID, accessorySymbol = "lock.fill")
|
|
124
145
|
val cards =
|
|
125
146
|
sectionStyle(
|
|
126
147
|
display = StyleDisplay.GRID,
|
|
127
148
|
gridTile = GridTile.CARD,
|
|
128
149
|
accessorySymbol = "lock.fill",
|
|
129
150
|
)
|
|
130
|
-
// List rows draw accessories; card elements have no slot for one.
|
|
131
151
|
assertTrue(findings(listOf(section(style = rows))).isEmpty())
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
assertTrue(findings(listOf(section(style = condensed))).isEmpty())
|
|
153
|
+
assertTrue(findings(listOf(section(style = image))).isEmpty())
|
|
154
|
+
for (style in listOf(plain, cards)) {
|
|
155
|
+
val found = findings(listOf(section(style = style)))
|
|
156
|
+
assertEquals(1, found.size)
|
|
157
|
+
assertTrue(found[0], found[0].contains("accessory slot"))
|
|
158
|
+
}
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
@Test
|
|
@@ -167,10 +191,10 @@ class InertStyleDiagnosticTest {
|
|
|
167
191
|
}
|
|
168
192
|
|
|
169
193
|
@Test
|
|
170
|
-
fun `a track's own shape is live in a
|
|
194
|
+
fun `a track's own shape is live in a shaped grid`() {
|
|
171
195
|
val grid =
|
|
172
196
|
section(
|
|
173
|
-
style = sectionStyle(display = StyleDisplay.GRID),
|
|
197
|
+
style = sectionStyle(display = StyleDisplay.GRID, gridTile = GridTile.IMAGE),
|
|
174
198
|
children = arrayOf(track(style = trackStyle(imageShape = ImageShape.CIRCULAR))),
|
|
175
199
|
)
|
|
176
200
|
assertTrue(findings(listOf(grid)).isEmpty())
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.audiobrowser.browser
|
|
2
2
|
|
|
3
|
+
import com.audiobrowser.util.toGridTile
|
|
4
|
+
import com.audiobrowser.util.toWireString
|
|
3
5
|
import com.margelo.nitro.audiobrowser.ArtworkRendering
|
|
4
6
|
import com.margelo.nitro.audiobrowser.CardImage
|
|
5
7
|
import com.margelo.nitro.audiobrowser.GridTile
|
|
@@ -134,6 +136,25 @@ class JsonStyleTest {
|
|
|
134
136
|
assertEquals(CardImage.BACKGROUND, style?.cardImage)
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
@Test
|
|
140
|
+
fun `decodes the image family`() {
|
|
141
|
+
val resolved =
|
|
142
|
+
json.decodeFromString<JsonResolvedTrack>(
|
|
143
|
+
"""{"path":"/home","title":"Home","sections":[
|
|
144
|
+
{"style":{"display":"grid","gridTile":"image","imageShape":"circular"},
|
|
145
|
+
"children":[{"title":"C","src":"s"}]}
|
|
146
|
+
]}"""
|
|
147
|
+
)
|
|
148
|
+
val style = resolved.toNitro().sections?.first()?.style
|
|
149
|
+
assertEquals(GridTile.IMAGE, style?.gridTile)
|
|
150
|
+
assertEquals(ImageShape.CIRCULAR, style?.imageShape)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Test
|
|
154
|
+
fun `every gridTile survives the wire`() {
|
|
155
|
+
GridTile.entries.forEach { assertEquals(it, it.toWireString().toGridTile()) }
|
|
156
|
+
}
|
|
157
|
+
|
|
137
158
|
@Test
|
|
138
159
|
fun `unknown gridTile decodes as no declaration`() {
|
|
139
160
|
val resolved =
|
|
@@ -132,6 +132,12 @@ class StyleResolverTest {
|
|
|
132
132
|
page = sectionStyle(gridTile = GridTile.CARD),
|
|
133
133
|
)
|
|
134
134
|
assertEquals(GridTile.PLAIN, overridden.gridTile)
|
|
135
|
+
val image =
|
|
136
|
+
StyleResolver.sectionStyle(
|
|
137
|
+
section = sectionStyle(gridTile = GridTile.IMAGE),
|
|
138
|
+
page = sectionStyle(gridTile = GridTile.CARD),
|
|
139
|
+
)
|
|
140
|
+
assertEquals(GridTile.IMAGE, image.gridTile)
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
@Test
|
|
@@ -69,8 +69,8 @@ enum InertStyleDiagnostic {
|
|
|
69
69
|
func isInert(in context: Context) -> Bool {
|
|
70
70
|
switch self {
|
|
71
71
|
case .gridWrap, .gridTile: !context.isGrid
|
|
72
|
-
case .imageShape: !context.
|
|
73
|
-
case .accessorySymbol: context.
|
|
72
|
+
case .imageShape: !context.isShaped
|
|
73
|
+
case .accessorySymbol: context.isGrid && !context.isShaped
|
|
74
74
|
case .cardTint, .cardImage: !context.isCard
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -79,8 +79,8 @@ enum InertStyleDiagnostic {
|
|
|
79
79
|
var rule: String {
|
|
80
80
|
switch self {
|
|
81
81
|
case .gridWrap, .gridTile: "`\(rawValue)` renders only in a grid"
|
|
82
|
-
case .imageShape: "only
|
|
83
|
-
case .accessorySymbol: "
|
|
82
|
+
case .imageShape: "only image and condensed tiles have a shape"
|
|
83
|
+
case .accessorySymbol: "among grid tiles only image and condensed ones have an accessory slot"
|
|
84
84
|
case .cardTint, .cardImage: "the card properties render only under `gridTile: 'card'`"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -90,9 +90,9 @@ enum InertStyleDiagnostic {
|
|
|
90
90
|
case .gridWrap, .gridTile:
|
|
91
91
|
"Declare `display: 'grid'` on the section or the page, or drop `\(rawValue)`."
|
|
92
92
|
case .imageShape:
|
|
93
|
-
"
|
|
93
|
+
"Use `gridTile: 'image'` or `'condensed'` in a grid, or drop `imageShape`."
|
|
94
94
|
case .accessorySymbol:
|
|
95
|
-
"Use `gridTile: '
|
|
95
|
+
"Use `gridTile: 'image'` or `'condensed'`, or a list, or drop `accessorySymbol`."
|
|
96
96
|
case .cardTint, .cardImage:
|
|
97
97
|
"Declare `display: 'grid'` with `gridTile: 'card'`, or drop `\(rawValue)`."
|
|
98
98
|
}
|
|
@@ -129,8 +129,25 @@ enum InertStyleDiagnostic {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/// Whether the card treatment is what this container renders — the
|
|
132
|
-
/// condition the `card*` family is named after.
|
|
133
|
-
|
|
132
|
+
/// condition the `card*` family is named after. Exhaustive, like
|
|
133
|
+
/// `isShaped`: a new tile kind has to be ruled on here.
|
|
134
|
+
var isCard: Bool {
|
|
135
|
+
guard isGrid else { return false }
|
|
136
|
+
return switch tile {
|
|
137
|
+
case .card: true
|
|
138
|
+
case .plain, .image, .condensed: false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Whether this container renders a tile family with a shape and an
|
|
143
|
+
/// accessory slot.
|
|
144
|
+
var isShaped: Bool {
|
|
145
|
+
guard isGrid else { return false }
|
|
146
|
+
return switch tile {
|
|
147
|
+
case .image, .condensed: true
|
|
148
|
+
case .plain, .card: false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
134
151
|
|
|
135
152
|
/// The effective style, for the message — in the wire spelling the author
|
|
136
153
|
/// types (`stringValue` is the generated enum's own mapping). `gridTile`
|
|
@@ -41,11 +41,23 @@ enum SectionPresentation: Equatable {
|
|
|
41
41
|
return symbol
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/// The title a tile shelf draws as the list section's header; nil when
|
|
45
|
+
/// the image row's own text carries it — here, not in the CarPlay factory,
|
|
46
|
+
/// so the rule is testable off-device. CarPlay draws a chevron beside an
|
|
47
|
+
/// image row's text whether or not the row has a handler, so only a shelf
|
|
48
|
+
/// with a "view all" `path` keeps its title there.
|
|
49
|
+
///
|
|
50
|
+
/// - Parameter supportsElements: whether the platform has the iOS 26
|
|
51
|
+
/// element API; the legacy image row's text is not optional.
|
|
52
|
+
static func tileSectionHeader(_ section: Section, supportsElements: Bool) -> String? {
|
|
53
|
+
supportsElements && section.path == nil ? section.title : nil
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
/// The element family a tile presentation renders on iOS 26+ — here, not
|
|
45
57
|
/// in the CarPlay factory, so the style→family mapping is testable
|
|
46
|
-
/// off-device. `gridTile` picks the family (plain by default)
|
|
47
|
-
///
|
|
48
|
-
///
|
|
58
|
+
/// off-device. `gridTile` picks the family (plain by default), in either
|
|
59
|
+
/// wrap mode: a plain tile is a row element (title + subtitle), an image
|
|
60
|
+
/// tile an image-grid element (title, shape, accessory). Pre-26 has no element API at all: the treatment drops
|
|
49
61
|
/// with the legacy image row (decorations drop before layout).
|
|
50
62
|
enum TileElementFamily: Equatable {
|
|
51
63
|
case rowElements
|
|
@@ -54,11 +66,12 @@ enum SectionPresentation: Equatable {
|
|
|
54
66
|
case condensedElements
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
static func tileFamily(for style: SectionStyle
|
|
69
|
+
static func tileFamily(for style: SectionStyle?) -> TileElementFamily {
|
|
58
70
|
switch style?.gridTile {
|
|
71
|
+
case .image: .imageGridElements
|
|
59
72
|
case .card: .cardElements
|
|
60
73
|
case .condensed: .condensedElements
|
|
61
|
-
case .plain, nil:
|
|
74
|
+
case .plain, nil: .rowElements
|
|
62
75
|
}
|
|
63
76
|
}
|
|
64
77
|
}
|
|
@@ -46,22 +46,29 @@ final class CarPlayListItemFactory {
|
|
|
46
46
|
|
|
47
47
|
// MARK: - Sections
|
|
48
48
|
|
|
49
|
-
/// Whether this OS has CarPlay's
|
|
50
|
-
///
|
|
51
|
-
|
|
49
|
+
/// Whether this OS has CarPlay's iOS 26 element API — the wrapping tile
|
|
50
|
+
/// container and the text-less image row `SectionPresentation` degrades
|
|
51
|
+
/// against.
|
|
52
|
+
private static var supportsElements: Bool {
|
|
52
53
|
if #available(iOS 26.0, *) { return true }
|
|
53
54
|
return false
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
private static func listSection(_ items: [CPListTemplateItem], header: String?) -> CPListSection {
|
|
58
|
+
if let header {
|
|
59
|
+
return CPListSection(items: items, header: header, sectionIndexTitle: nil)
|
|
60
|
+
}
|
|
61
|
+
return CPListSection(items: items)
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
/// Maps the page's sections 1:1 to CPListSections (ADR 0010), respecting
|
|
57
65
|
/// CarPlay's section and total-item budgets. Style blocks declare the
|
|
58
66
|
/// requested presentation (resolved `section ?? page` here — ADR 0011);
|
|
59
67
|
/// this renders CarPlay's nearest supported form (`SectionPresentation`):
|
|
60
68
|
/// - list (default): a titled/headerless section of list rows.
|
|
61
|
-
/// - single-line grid (`gridWrap: false`): a
|
|
62
|
-
///
|
|
63
|
-
///
|
|
64
|
-
/// report the fit).
|
|
69
|
+
/// - single-line grid (`gridWrap: false`): a section holding one
|
|
70
|
+
/// single-line image-row item — the tiles that fit render, the rest
|
|
71
|
+
/// truncate (the platform doesn't report the fit).
|
|
65
72
|
/// - wrapping grid: a wrapping, titled tile grid on iOS 26+; a plain list
|
|
66
73
|
/// before that.
|
|
67
74
|
func createSections(from resolvedTrack: ResolvedTrack) -> [CPListSection] {
|
|
@@ -84,26 +91,24 @@ final class CarPlayListItemFactory {
|
|
|
84
91
|
guard !section.children.isEmpty else { continue }
|
|
85
92
|
|
|
86
93
|
let style = StyleResolver.sectionStyle(section: section.style, page: resolvedTrack.style)
|
|
87
|
-
let presentation = SectionPresentation(for: style, supportsWrappingGrid: Self.
|
|
94
|
+
let presentation = SectionPresentation(for: style, supportsWrappingGrid: Self.supportsElements)
|
|
88
95
|
switch presentation {
|
|
89
96
|
case .list:
|
|
90
97
|
let availableSlots = maxTotalItems - totalItemCount
|
|
91
98
|
let items: [CPListTemplateItem] = section.children.prefix(availableSlots).map {
|
|
92
99
|
createListItem(for: $0, style: StyleResolver.trackStyle(track: $0.style, section: style))
|
|
93
100
|
}
|
|
94
|
-
|
|
95
|
-
listSections.append(CPListSection(items: items, header: title, sectionIndexTitle: nil))
|
|
96
|
-
} else {
|
|
97
|
-
listSections.append(CPListSection(items: items))
|
|
98
|
-
}
|
|
101
|
+
listSections.append(Self.listSection(items, header: section.title))
|
|
99
102
|
totalItemCount += items.count
|
|
100
103
|
case .singleLineRow, .wrappingGrid:
|
|
101
|
-
// Tile presentations render as one image-row item
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// Tile presentations render as one image-row item in its own
|
|
105
|
+
// CPListSection. The title is drawn once: as the section's header
|
|
106
|
+
// (`SectionPresentation.tileSectionHeader`), or as the item's text.
|
|
107
|
+
let header = SectionPresentation.tileSectionHeader(section, supportsElements: Self.supportsElements)
|
|
108
|
+
guard let item = createImageRowItem(
|
|
109
|
+
for: section, text: header == nil ? section.title : nil, style: style, presentation: presentation,
|
|
110
|
+
) else { continue }
|
|
111
|
+
listSections.append(Self.listSection([item], header: header))
|
|
107
112
|
totalItemCount += 1
|
|
108
113
|
}
|
|
109
114
|
}
|
|
@@ -213,16 +218,25 @@ final class CarPlayListItemFactory {
|
|
|
213
218
|
return CPListImageRowItem.maximumImageSize
|
|
214
219
|
}
|
|
215
220
|
|
|
216
|
-
/// Target artwork size for one tile. Cards
|
|
217
|
-
/// ('background' selects the larger
|
|
218
|
-
///
|
|
219
|
-
private static func tileImageSize(
|
|
220
|
-
|
|
221
|
+
/// Target artwork size for one tile. Cards, image tiles and plain row
|
|
222
|
+
/// elements read their own class maximums ('background' selects the larger
|
|
223
|
+
/// full-height card target); condensed tiles use the shared element size.
|
|
224
|
+
private static func tileImageSize(
|
|
225
|
+
family: SectionPresentation.TileElementFamily, cardImage: CardImage?,
|
|
226
|
+
) -> CGSize {
|
|
227
|
+
guard #available(iOS 26.0, *) else { return CPListImageRowItem.maximumImageSize }
|
|
228
|
+
switch family {
|
|
229
|
+
case .cardElements:
|
|
221
230
|
return cardImage == .background
|
|
222
231
|
? CPListImageRowItemCardElement.maximumFullHeightImageSize
|
|
223
232
|
: CPListImageRowItemCardElement.maximumImageSize
|
|
233
|
+
case .rowElements:
|
|
234
|
+
return CPListImageRowItemRowElement.maximumImageSize
|
|
235
|
+
case .imageGridElements:
|
|
236
|
+
return CPListImageRowItemImageGridElement.maximumImageSize
|
|
237
|
+
case .condensedElements:
|
|
238
|
+
return CPListImageRowItemElement.maximumImageSize
|
|
224
239
|
}
|
|
225
|
-
return rowImageSize
|
|
226
240
|
}
|
|
227
241
|
|
|
228
242
|
/// Creates the image-row item rendering a tile-presented section: a single
|
|
@@ -231,10 +245,14 @@ final class CarPlayListItemFactory {
|
|
|
231
245
|
///
|
|
232
246
|
/// Returns nil when nothing is drawable — every child hidden by the
|
|
233
247
|
/// disabled ladder (pre-26 only; the element APIs gray instead).
|
|
248
|
+
///
|
|
249
|
+
/// - Parameter text: the row's own title; nil when the list section's
|
|
250
|
+
/// header carries it.
|
|
234
251
|
private func createImageRowItem(
|
|
235
|
-
for section: Section, style: SectionStyle, presentation: SectionPresentation,
|
|
252
|
+
for section: Section, text rowText: String?, style: SectionStyle, presentation: SectionPresentation,
|
|
236
253
|
) -> CPListImageRowItem? {
|
|
237
254
|
let singleLine = presentation == .singleLineRow
|
|
255
|
+
let family = SectionPresentation.tileFamily(for: style)
|
|
238
256
|
// Disabled ladder: iOS 26 elements can draw unavailability (grayed +
|
|
239
257
|
// inert, below); the legacy image row can't, so there a disabled track
|
|
240
258
|
// hides — never a normal-looking dead tile.
|
|
@@ -286,14 +304,14 @@ final class CarPlayListItemFactory {
|
|
|
286
304
|
elements[index].image = image
|
|
287
305
|
}
|
|
288
306
|
}
|
|
289
|
-
// The family is style-driven (`SectionPresentation.tileFamily`);
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
switch
|
|
307
|
+
// The family is style-driven (`SectionPresentation.tileFamily`); every
|
|
308
|
+
// family takes either wrap mode. Pre-26 the treatment drops and the
|
|
309
|
+
// layout survives (the legacy branch below).
|
|
310
|
+
switch family {
|
|
293
311
|
case .cardElements:
|
|
294
312
|
// Cards have no shape and no accessory slot; their knobs are the
|
|
295
313
|
// tint and the image mode ('background' fills the card full-height
|
|
296
|
-
// and
|
|
314
|
+
// and fades the tint in behind the labels).
|
|
297
315
|
let elements = makeElements { track, resolved in
|
|
298
316
|
CPListImageRowItemCardElement(
|
|
299
317
|
image: placeholder(),
|
|
@@ -304,7 +322,7 @@ final class CarPlayListItemFactory {
|
|
|
304
322
|
)
|
|
305
323
|
}
|
|
306
324
|
item = CPListImageRowItem(
|
|
307
|
-
text:
|
|
325
|
+
text: rowText, cardElements: elements, allowsMultipleLines: !singleLine,
|
|
308
326
|
)
|
|
309
327
|
applyImage = imageApplier(elements)
|
|
310
328
|
case .condensedElements:
|
|
@@ -318,19 +336,9 @@ final class CarPlayListItemFactory {
|
|
|
318
336
|
)
|
|
319
337
|
}
|
|
320
338
|
item = CPListImageRowItem(
|
|
321
|
-
text:
|
|
339
|
+
text: rowText, condensedElements: elements, allowsMultipleLines: !singleLine,
|
|
322
340
|
)
|
|
323
341
|
applyImage = imageApplier(elements)
|
|
324
|
-
case .rowElements:
|
|
325
|
-
// The only plain tile with a subtitle slot — the imageGridElements
|
|
326
|
-
// family has no equivalent, by SDK design.
|
|
327
|
-
let elements = makeElements { track, _ in
|
|
328
|
-
CPListImageRowItemRowElement(
|
|
329
|
-
image: placeholder(), title: track.title, subtitle: track.subtitle,
|
|
330
|
-
)
|
|
331
|
-
}
|
|
332
|
-
item = CPListImageRowItem(text: section.title, elements: elements, allowsMultipleLines: false)
|
|
333
|
-
applyImage = imageApplier(elements)
|
|
334
342
|
case .imageGridElements:
|
|
335
343
|
// imageGridElements, never the title-less gridElements: an
|
|
336
344
|
// artwork-less track must keep its name on screen (ADR 0010).
|
|
@@ -342,28 +350,40 @@ final class CarPlayListItemFactory {
|
|
|
342
350
|
accessorySymbolName: SectionPresentation.effectiveAccessorySymbol(resolved),
|
|
343
351
|
)
|
|
344
352
|
}
|
|
345
|
-
item = CPListImageRowItem(
|
|
353
|
+
item = CPListImageRowItem(
|
|
354
|
+
text: rowText, imageGridElements: elements, allowsMultipleLines: !singleLine,
|
|
355
|
+
)
|
|
356
|
+
applyImage = imageApplier(elements)
|
|
357
|
+
case .rowElements:
|
|
358
|
+
// Titled, so an artwork-less track keeps its name on screen
|
|
359
|
+
// (ADR 0010); no shape and no accessory slot.
|
|
360
|
+
let elements = makeElements { track, _ in
|
|
361
|
+
CPListImageRowItemRowElement(
|
|
362
|
+
image: placeholder(), title: track.title, subtitle: track.subtitle,
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
item = CPListImageRowItem(
|
|
366
|
+
text: rowText, elements: elements, allowsMultipleLines: !singleLine,
|
|
367
|
+
)
|
|
346
368
|
applyImage = imageApplier(elements)
|
|
347
369
|
}
|
|
348
370
|
} else {
|
|
349
371
|
let placeholders = tracks.map { _ in placeholder() }
|
|
350
372
|
// Use imageTitles variant on iOS 17.4+ to show titles below each thumbnail
|
|
351
373
|
if #available(iOS 17.4, *) {
|
|
352
|
-
item = CPListImageRowItem(text:
|
|
374
|
+
item = CPListImageRowItem(text: rowText ?? "", images: placeholders, imageTitles: tracks.map(\.title))
|
|
353
375
|
} else {
|
|
354
|
-
item = CPListImageRowItem(text:
|
|
376
|
+
item = CPListImageRowItem(text: rowText ?? "", images: placeholders)
|
|
355
377
|
}
|
|
356
378
|
}
|
|
357
379
|
|
|
358
380
|
// Header tap → navigate to section.path ("view all"). A path-less
|
|
359
|
-
// section is a pure preview
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
completion
|
|
364
|
-
return
|
|
381
|
+
// section is a pure preview and gets no handler.
|
|
382
|
+
if let path = section.path {
|
|
383
|
+
let target = Self.navigationTrack(path: path, title: section.title)
|
|
384
|
+
item.handler = { [onItemSelected] _, completion in
|
|
385
|
+
onItemSelected(target, completion)
|
|
365
386
|
}
|
|
366
|
-
onItemSelected(Self.navigationTrack(path: path, title: section.title), completion)
|
|
367
387
|
}
|
|
368
388
|
|
|
369
389
|
// Per-tile taps select the child track directly. A disabled tile is
|
|
@@ -385,7 +405,7 @@ final class CarPlayListItemFactory {
|
|
|
385
405
|
imageLoader?.loadArtwork(
|
|
386
406
|
for: track,
|
|
387
407
|
style: resolved,
|
|
388
|
-
size: Self.tileImageSize(
|
|
408
|
+
size: Self.tileImageSize(family: family, cardImage: resolved.cardImage),
|
|
389
409
|
) { [weak item, applyImage] image in
|
|
390
410
|
Task { @MainActor in
|
|
391
411
|
guard let item, let image else { return }
|
|
@@ -128,12 +128,14 @@
|
|
|
128
128
|
|
|
129
129
|
enum GridTile: Equatable {
|
|
130
130
|
case plain
|
|
131
|
+
case image
|
|
131
132
|
case card
|
|
132
133
|
case condensed
|
|
133
134
|
|
|
134
135
|
init?(fromString string: String) {
|
|
135
136
|
switch string {
|
|
136
137
|
case "plain": self = .plain
|
|
138
|
+
case "image": self = .image
|
|
137
139
|
case "card": self = .card
|
|
138
140
|
case "condensed": self = .condensed
|
|
139
141
|
default: return nil
|
|
@@ -145,6 +147,7 @@
|
|
|
145
147
|
var stringValue: String {
|
|
146
148
|
switch self {
|
|
147
149
|
case .plain: "plain"
|
|
150
|
+
case .image: "image"
|
|
148
151
|
case .card: "card"
|
|
149
152
|
case .condensed: "condensed"
|
|
150
153
|
}
|
|
@@ -74,22 +74,31 @@ struct InertStyleDiagnosticTests {
|
|
|
74
74
|
|
|
75
75
|
@Test func imageShapeNeedsAShapedTile() {
|
|
76
76
|
let list = SectionStyle(imageShape: .circular)
|
|
77
|
+
let plain = SectionStyle(display: .grid, imageShape: .circular)
|
|
77
78
|
let cards = SectionStyle(gridTile: .card, display: .grid, imageShape: .circular)
|
|
78
79
|
let condensed = SectionStyle(gridTile: .condensed, display: .grid, imageShape: .circular)
|
|
80
|
+
let image = SectionStyle(gridTile: .image, display: .grid, imageShape: .circular)
|
|
79
81
|
#expect(findings([Section(style: list, children: [])]).count == 1)
|
|
82
|
+
#expect(findings([Section(style: plain, children: [])]).count == 1)
|
|
80
83
|
#expect(findings([Section(style: cards, children: [])]).count == 1)
|
|
81
|
-
// Plain and condensed tiles both take a shape.
|
|
82
84
|
#expect(findings([Section(style: condensed, children: [])]).isEmpty)
|
|
85
|
+
#expect(findings([Section(style: image, children: [])]).isEmpty)
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
@Test func
|
|
88
|
+
@Test func accessorySymbolNeedsAnAccessorySlot() {
|
|
86
89
|
let rows = SectionStyle(accessorySymbol: "lock.fill")
|
|
90
|
+
let condensed = SectionStyle(gridTile: .condensed, display: .grid, accessorySymbol: "lock.fill")
|
|
91
|
+
let image = SectionStyle(gridTile: .image, display: .grid, accessorySymbol: "lock.fill")
|
|
92
|
+
let plain = SectionStyle(display: .grid, accessorySymbol: "lock.fill")
|
|
87
93
|
let cards = SectionStyle(gridTile: .card, display: .grid, accessorySymbol: "lock.fill")
|
|
88
|
-
// List rows draw accessories; card elements have no slot for one.
|
|
89
94
|
#expect(findings([Section(style: rows, children: [])]).isEmpty)
|
|
90
|
-
|
|
91
|
-
#expect(
|
|
92
|
-
|
|
95
|
+
#expect(findings([Section(style: condensed, children: [])]).isEmpty)
|
|
96
|
+
#expect(findings([Section(style: image, children: [])]).isEmpty)
|
|
97
|
+
for style in [plain, cards] {
|
|
98
|
+
let found = findings([Section(style: style, children: [])])
|
|
99
|
+
#expect(found.count == 1)
|
|
100
|
+
#expect(found[0].contains("accessory slot"))
|
|
101
|
+
}
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
@Test func artworkRenderingIsNeverInert() {
|
|
@@ -114,9 +123,9 @@ struct InertStyleDiagnosticTests {
|
|
|
114
123
|
#expect(found[0].contains("style.cardTint"))
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
@Test func
|
|
126
|
+
@Test func aTracksOwnShapeIsLiveInAShapedGrid() {
|
|
118
127
|
let section = Section(
|
|
119
|
-
style: SectionStyle(display: .grid),
|
|
128
|
+
style: SectionStyle(gridTile: .image, display: .grid),
|
|
120
129
|
children: [playable(style: TrackStyle(imageShape: .circular))],
|
|
121
130
|
)
|
|
122
131
|
#expect(findings([section]).isEmpty)
|
|
@@ -162,6 +162,20 @@ struct JsonStyleTests {
|
|
|
162
162
|
#expect(style?.cardImage == .background)
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
@Test func decodesTheImageFamily() throws {
|
|
166
|
+
let json = Data(
|
|
167
|
+
"""
|
|
168
|
+
{"path":"/home","title":"Home","sections":[
|
|
169
|
+
{"style":{"display":"grid","gridTile":"image","imageShape":"circular"},
|
|
170
|
+
"children":[{"title":"C","src":"s"}]}
|
|
171
|
+
]}
|
|
172
|
+
""".utf8)
|
|
173
|
+
let resolved = try JSONDecoder().decode(JsonResolvedTrack.self, from: json).toNitro()
|
|
174
|
+
let style = resolved.sections?.first?.style
|
|
175
|
+
#expect(style?.gridTile == .image)
|
|
176
|
+
#expect(style?.imageShape == .circular)
|
|
177
|
+
}
|
|
178
|
+
|
|
165
179
|
@Test func unknownGridTileDecodesAsNoDeclaration() throws {
|
|
166
180
|
let json = Data(
|
|
167
181
|
"""
|
|
@@ -59,21 +59,29 @@ struct SectionPresentationTests {
|
|
|
59
59
|
== .list)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
@Test func
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
62
|
+
@Test func tileFamilyFollowsGridTile() {
|
|
63
|
+
// The family follows the declaration alone; the wrap mode only decides
|
|
64
|
+
// whether its elements take more than one line.
|
|
65
|
+
#expect(SectionPresentation.tileFamily(for: SectionStyle(gridTile: .card)) == .cardElements)
|
|
66
66
|
#expect(
|
|
67
|
-
SectionPresentation.tileFamily(for: SectionStyle(gridTile: .
|
|
68
|
-
== .cardElements)
|
|
67
|
+
SectionPresentation.tileFamily(for: SectionStyle(gridTile: .image)) == .imageGridElements)
|
|
69
68
|
#expect(
|
|
70
|
-
SectionPresentation.tileFamily(for: SectionStyle(gridTile: .condensed)
|
|
71
|
-
|
|
72
|
-
#expect(SectionPresentation.tileFamily(for:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
SectionPresentation.tileFamily(for: SectionStyle(gridTile: .condensed)) == .condensedElements)
|
|
70
|
+
#expect(SectionPresentation.tileFamily(for: nil) == .rowElements)
|
|
71
|
+
#expect(SectionPresentation.tileFamily(for: SectionStyle(gridTile: .plain)) == .rowElements)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Test func aPathlessShelfTitlesTheSectionHeader() {
|
|
75
|
+
let viewAll = Section(title: "Popular", path: "/popular", children: [])
|
|
76
|
+
let preview = Section(title: "Popular", children: [])
|
|
77
|
+
let untitled = Section(children: [])
|
|
78
|
+
// The image row's own text carries a chevron, so it is kept for the
|
|
79
|
+
// shelf that has somewhere to go.
|
|
80
|
+
#expect(SectionPresentation.tileSectionHeader(viewAll, supportsElements: true) == nil)
|
|
81
|
+
#expect(SectionPresentation.tileSectionHeader(preview, supportsElements: true) == "Popular")
|
|
82
|
+
#expect(SectionPresentation.tileSectionHeader(untitled, supportsElements: true) == nil)
|
|
83
|
+
// The legacy row cannot drop its text.
|
|
84
|
+
#expect(SectionPresentation.tileSectionHeader(preview, supportsElements: false) == nil)
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
@Test func effectiveAccessorySymbol_noneAndAbsenceDrawNothing() {
|
|
@@ -48,6 +48,11 @@ struct SectionStyleResolutionTests {
|
|
|
48
48
|
page: SectionStyle(gridTile: .card),
|
|
49
49
|
)
|
|
50
50
|
#expect(overridden.gridTile == .plain)
|
|
51
|
+
let image = StyleResolver.sectionStyle(
|
|
52
|
+
section: SectionStyle(gridTile: .image),
|
|
53
|
+
page: SectionStyle(gridTile: .card),
|
|
54
|
+
)
|
|
55
|
+
#expect(image.gridTile == .image)
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
58
|
|
|
@@ -23,7 +23,7 @@ export type StyleDisplay = 'list' | 'grid';
|
|
|
23
23
|
*/
|
|
24
24
|
export type ArtworkRendering = 'original' | 'stencil';
|
|
25
25
|
/**
|
|
26
|
-
* The shape
|
|
26
|
+
* The shape an image or condensed grid tile's artwork is cropped to.
|
|
27
27
|
*
|
|
28
28
|
* - `'rounded-rectangle'` (default) — the standard tile crop.
|
|
29
29
|
* - `'circular'` — the standard presentation for *people* (artists, hosts,
|
|
@@ -38,26 +38,31 @@ export type ImageShape = 'circular' | 'rounded-rectangle';
|
|
|
38
38
|
/**
|
|
39
39
|
* The kind of tile a grid renders.
|
|
40
40
|
*
|
|
41
|
-
* - `'plain'` (default) — artwork tiles with a title
|
|
41
|
+
* - `'plain'` (default) — large artwork tiles with a title and subtitle
|
|
42
|
+
* below. No shape and no accessory.
|
|
43
|
+
* - `'image'` — artwork tiles with a title and no subtitle; takes
|
|
44
|
+
* {@link TrackStyle.imageShape} and {@link TrackStyle.accessorySymbol}.
|
|
45
|
+
* The grid of round tiles for people is `'image'` with
|
|
46
|
+
* `imageShape: 'circular'`.
|
|
42
47
|
* - `'card'` — a larger card with title/subtitle and a
|
|
43
48
|
* {@link TrackStyle.cardTint} color; its image mode is
|
|
44
|
-
* {@link TrackStyle.cardImage}. Cards have no shape.
|
|
45
|
-
* - `'condensed'` — a denser cell carrying title + subtitle
|
|
46
|
-
* {@link TrackStyle.imageShape} and
|
|
47
|
-
*
|
|
49
|
+
* {@link TrackStyle.cardImage}. Cards have no shape and no accessory.
|
|
50
|
+
* - `'condensed'` — a denser cell carrying title + subtitle beside a small
|
|
51
|
+
* image; takes {@link TrackStyle.imageShape} and
|
|
52
|
+
* {@link TrackStyle.accessorySymbol}.
|
|
48
53
|
*
|
|
49
54
|
* Element families are mutually exclusive within a container, so this is
|
|
50
55
|
* a container property — hence the enum, and the `grid` prefix.
|
|
51
56
|
*/
|
|
52
|
-
export type GridTile = 'plain' | 'card' | 'condensed';
|
|
57
|
+
export type GridTile = 'plain' | 'image' | 'card' | 'condensed';
|
|
53
58
|
/**
|
|
54
59
|
* How a card tile uses its image.
|
|
55
60
|
*
|
|
56
|
-
* - `'normal'` (default) — image above the labels;
|
|
57
|
-
* {@link TrackStyle.cardTint} colors
|
|
61
|
+
* - `'normal'` (default) — a square image inset above the labels;
|
|
62
|
+
* {@link TrackStyle.cardTint} colors the whole card.
|
|
58
63
|
* - `'background'` — the image fills the card full-height with the labels
|
|
59
|
-
* overlaid; `cardTint`
|
|
60
|
-
*
|
|
64
|
+
* overlaid; `cardTint` fades in behind them. Selects the larger artwork
|
|
65
|
+
* target size.
|
|
61
66
|
*
|
|
62
67
|
* An enum rather than a boolean: it names the card's image mode and
|
|
63
68
|
* leaves room for future modes.
|
|
@@ -120,12 +125,13 @@ export interface TrackStyle {
|
|
|
120
125
|
*/
|
|
121
126
|
artworkRendering?: ArtworkRendering;
|
|
122
127
|
/**
|
|
123
|
-
* Inherited (`track ?? section ?? page`): the shape
|
|
124
|
-
* artwork is cropped to — `'circular'` for people,
|
|
125
|
-
* `'rounded-rectangle'` for everything else. Shape follows
|
|
126
|
-
* wherever it travels: a circular artist stays circular in
|
|
127
|
-
* grid renders it next. Inert where the presentation has
|
|
128
|
-
* (list rows, pre-26 CarPlay, Android
|
|
128
|
+
* Inherited (`track ?? section ?? page`): the shape an image or
|
|
129
|
+
* condensed grid tile's artwork is cropped to — `'circular'` for people,
|
|
130
|
+
* the default `'rounded-rectangle'` for everything else. Shape follows
|
|
131
|
+
* the item wherever it travels: a circular artist stays circular in
|
|
132
|
+
* whatever shaped grid renders it next. Inert where the presentation has
|
|
133
|
+
* no shape (list rows, plain and card tiles, pre-26 CarPlay, Android
|
|
134
|
+
* Auto).
|
|
129
135
|
*
|
|
130
136
|
* @default 'rounded-rectangle'
|
|
131
137
|
*
|
|
@@ -135,8 +141,9 @@ export interface TrackStyle {
|
|
|
135
141
|
/**
|
|
136
142
|
* Inherited (`track ?? section ?? page`): an SF Symbol name drawn as
|
|
137
143
|
* the item's accessory wherever the current presentation supports one —
|
|
138
|
-
* the trailing edge of a list row
|
|
139
|
-
* tile (placement is the platform's choice per
|
|
144
|
+
* the trailing edge of a list row or an iOS 26 condensed tile, leading
|
|
145
|
+
* the title on an image tile (placement is the platform's choice per
|
|
146
|
+
* surface); plain and card tiles have no slot for one. A resolved
|
|
140
147
|
* symbol replaces the derived accessory (the disclosure chevron a
|
|
141
148
|
* browsable row gets); `'none'` clears an inherited value, restoring
|
|
142
149
|
* the derived behavior — inheritance has no other escape, and no SF
|
|
@@ -148,8 +155,8 @@ export interface TrackStyle {
|
|
|
148
155
|
accessorySymbol?: string;
|
|
149
156
|
/**
|
|
150
157
|
* Inherited (`track ?? section ?? page`): the card treatment's tint
|
|
151
|
-
* color (hex, e.g. `'#1e3a8a'`) —
|
|
152
|
-
*
|
|
158
|
+
* color (hex, e.g. `'#1e3a8a'`) — the card's own color, or the color
|
|
159
|
+
* that fades in behind the labels when {@link TrackStyle.cardImage} is
|
|
153
160
|
* `'background'`. Card-prefixed: inert unless the container renders
|
|
154
161
|
* `gridTile: 'card'`.
|
|
155
162
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-nodes.d.ts","sourceRoot":"","sources":["../../../../src/types/browser-nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE1E;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,CAAA;AAE1C;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,SAAS,CAAA;AAErD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,mBAAmB,CAAA;AAEzD
|
|
1
|
+
{"version":3,"file":"browser-nodes.d.ts","sourceRoot":"","sources":["../../../../src/types/browser-nodes.ts"],"names":[],"mappings":"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"}
|
|
@@ -45,6 +45,9 @@ namespace margelo::nitro::audiobrowser {
|
|
|
45
45
|
case GridTile::PLAIN:
|
|
46
46
|
static const auto fieldPLAIN = clazz->getStaticField<JGridTile>("PLAIN");
|
|
47
47
|
return clazz->getStaticFieldValue(fieldPLAIN);
|
|
48
|
+
case GridTile::IMAGE:
|
|
49
|
+
static const auto fieldIMAGE = clazz->getStaticField<JGridTile>("IMAGE");
|
|
50
|
+
return clazz->getStaticFieldValue(fieldIMAGE);
|
|
48
51
|
case GridTile::CARD:
|
|
49
52
|
static const auto fieldCARD = clazz->getStaticField<JGridTile>("CARD");
|
|
50
53
|
return clazz->getStaticFieldValue(fieldCARD);
|
|
@@ -19,6 +19,8 @@ public extension GridTile {
|
|
|
19
19
|
switch string {
|
|
20
20
|
case "plain":
|
|
21
21
|
self = .plain
|
|
22
|
+
case "image":
|
|
23
|
+
self = .image
|
|
22
24
|
case "card":
|
|
23
25
|
self = .card
|
|
24
26
|
case "condensed":
|
|
@@ -35,6 +37,8 @@ public extension GridTile {
|
|
|
35
37
|
switch self {
|
|
36
38
|
case .plain:
|
|
37
39
|
return "plain"
|
|
40
|
+
case .image:
|
|
41
|
+
return "image"
|
|
38
42
|
case .card:
|
|
39
43
|
return "card"
|
|
40
44
|
case .condensed:
|
|
@@ -30,8 +30,9 @@ namespace margelo::nitro::audiobrowser {
|
|
|
30
30
|
*/
|
|
31
31
|
enum class GridTile {
|
|
32
32
|
PLAIN SWIFT_NAME(plain) = 0,
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
IMAGE SWIFT_NAME(image) = 1,
|
|
34
|
+
CARD SWIFT_NAME(card) = 2,
|
|
35
|
+
CONDENSED SWIFT_NAME(condensed) = 3,
|
|
35
36
|
} CLOSED_ENUM;
|
|
36
37
|
|
|
37
38
|
} // namespace margelo::nitro::audiobrowser
|
|
@@ -45,6 +46,7 @@ namespace margelo::nitro {
|
|
|
45
46
|
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, arg);
|
|
46
47
|
switch (hashString(unionValue.c_str(), unionValue.size())) {
|
|
47
48
|
case hashString("plain"): return margelo::nitro::audiobrowser::GridTile::PLAIN;
|
|
49
|
+
case hashString("image"): return margelo::nitro::audiobrowser::GridTile::IMAGE;
|
|
48
50
|
case hashString("card"): return margelo::nitro::audiobrowser::GridTile::CARD;
|
|
49
51
|
case hashString("condensed"): return margelo::nitro::audiobrowser::GridTile::CONDENSED;
|
|
50
52
|
default: [[unlikely]]
|
|
@@ -54,6 +56,7 @@ namespace margelo::nitro {
|
|
|
54
56
|
static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::audiobrowser::GridTile arg) {
|
|
55
57
|
switch (arg) {
|
|
56
58
|
case margelo::nitro::audiobrowser::GridTile::PLAIN: return JSIConverter<std::string>::toJSI(runtime, "plain");
|
|
59
|
+
case margelo::nitro::audiobrowser::GridTile::IMAGE: return JSIConverter<std::string>::toJSI(runtime, "image");
|
|
57
60
|
case margelo::nitro::audiobrowser::GridTile::CARD: return JSIConverter<std::string>::toJSI(runtime, "card");
|
|
58
61
|
case margelo::nitro::audiobrowser::GridTile::CONDENSED: return JSIConverter<std::string>::toJSI(runtime, "condensed");
|
|
59
62
|
default: [[unlikely]]
|
|
@@ -68,6 +71,7 @@ namespace margelo::nitro {
|
|
|
68
71
|
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, value);
|
|
69
72
|
switch (hashString(unionValue.c_str(), unionValue.size())) {
|
|
70
73
|
case hashString("plain"):
|
|
74
|
+
case hashString("image"):
|
|
71
75
|
case hashString("card"):
|
|
72
76
|
case hashString("condensed"):
|
|
73
77
|
return true;
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ export type StyleDisplay = 'list' | 'grid'
|
|
|
26
26
|
export type ArtworkRendering = 'original' | 'stencil'
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* The shape
|
|
29
|
+
* The shape an image or condensed grid tile's artwork is cropped to.
|
|
30
30
|
*
|
|
31
31
|
* - `'rounded-rectangle'` (default) — the standard tile crop.
|
|
32
32
|
* - `'circular'` — the standard presentation for *people* (artists, hosts,
|
|
@@ -42,27 +42,32 @@ export type ImageShape = 'circular' | 'rounded-rectangle'
|
|
|
42
42
|
/**
|
|
43
43
|
* The kind of tile a grid renders.
|
|
44
44
|
*
|
|
45
|
-
* - `'plain'` (default) — artwork tiles with a title
|
|
45
|
+
* - `'plain'` (default) — large artwork tiles with a title and subtitle
|
|
46
|
+
* below. No shape and no accessory.
|
|
47
|
+
* - `'image'` — artwork tiles with a title and no subtitle; takes
|
|
48
|
+
* {@link TrackStyle.imageShape} and {@link TrackStyle.accessorySymbol}.
|
|
49
|
+
* The grid of round tiles for people is `'image'` with
|
|
50
|
+
* `imageShape: 'circular'`.
|
|
46
51
|
* - `'card'` — a larger card with title/subtitle and a
|
|
47
52
|
* {@link TrackStyle.cardTint} color; its image mode is
|
|
48
|
-
* {@link TrackStyle.cardImage}. Cards have no shape.
|
|
49
|
-
* - `'condensed'` — a denser cell carrying title + subtitle
|
|
50
|
-
* {@link TrackStyle.imageShape} and
|
|
51
|
-
*
|
|
53
|
+
* {@link TrackStyle.cardImage}. Cards have no shape and no accessory.
|
|
54
|
+
* - `'condensed'` — a denser cell carrying title + subtitle beside a small
|
|
55
|
+
* image; takes {@link TrackStyle.imageShape} and
|
|
56
|
+
* {@link TrackStyle.accessorySymbol}.
|
|
52
57
|
*
|
|
53
58
|
* Element families are mutually exclusive within a container, so this is
|
|
54
59
|
* a container property — hence the enum, and the `grid` prefix.
|
|
55
60
|
*/
|
|
56
|
-
export type GridTile = 'plain' | 'card' | 'condensed'
|
|
61
|
+
export type GridTile = 'plain' | 'image' | 'card' | 'condensed'
|
|
57
62
|
|
|
58
63
|
/**
|
|
59
64
|
* How a card tile uses its image.
|
|
60
65
|
*
|
|
61
|
-
* - `'normal'` (default) — image above the labels;
|
|
62
|
-
* {@link TrackStyle.cardTint} colors
|
|
66
|
+
* - `'normal'` (default) — a square image inset above the labels;
|
|
67
|
+
* {@link TrackStyle.cardTint} colors the whole card.
|
|
63
68
|
* - `'background'` — the image fills the card full-height with the labels
|
|
64
|
-
* overlaid; `cardTint`
|
|
65
|
-
*
|
|
69
|
+
* overlaid; `cardTint` fades in behind them. Selects the larger artwork
|
|
70
|
+
* target size.
|
|
66
71
|
*
|
|
67
72
|
* An enum rather than a boolean: it names the card's image mode and
|
|
68
73
|
* leaves room for future modes.
|
|
@@ -128,12 +133,13 @@ export interface TrackStyle {
|
|
|
128
133
|
artworkRendering?: ArtworkRendering
|
|
129
134
|
|
|
130
135
|
/**
|
|
131
|
-
* Inherited (`track ?? section ?? page`): the shape
|
|
132
|
-
* artwork is cropped to — `'circular'` for people,
|
|
133
|
-
* `'rounded-rectangle'` for everything else. Shape follows
|
|
134
|
-
* wherever it travels: a circular artist stays circular in
|
|
135
|
-
* grid renders it next. Inert where the presentation has
|
|
136
|
-
* (list rows, pre-26 CarPlay, Android
|
|
136
|
+
* Inherited (`track ?? section ?? page`): the shape an image or
|
|
137
|
+
* condensed grid tile's artwork is cropped to — `'circular'` for people,
|
|
138
|
+
* the default `'rounded-rectangle'` for everything else. Shape follows
|
|
139
|
+
* the item wherever it travels: a circular artist stays circular in
|
|
140
|
+
* whatever shaped grid renders it next. Inert where the presentation has
|
|
141
|
+
* no shape (list rows, plain and card tiles, pre-26 CarPlay, Android
|
|
142
|
+
* Auto).
|
|
137
143
|
*
|
|
138
144
|
* @default 'rounded-rectangle'
|
|
139
145
|
*
|
|
@@ -144,8 +150,9 @@ export interface TrackStyle {
|
|
|
144
150
|
/**
|
|
145
151
|
* Inherited (`track ?? section ?? page`): an SF Symbol name drawn as
|
|
146
152
|
* the item's accessory wherever the current presentation supports one —
|
|
147
|
-
* the trailing edge of a list row
|
|
148
|
-
* tile (placement is the platform's choice per
|
|
153
|
+
* the trailing edge of a list row or an iOS 26 condensed tile, leading
|
|
154
|
+
* the title on an image tile (placement is the platform's choice per
|
|
155
|
+
* surface); plain and card tiles have no slot for one. A resolved
|
|
149
156
|
* symbol replaces the derived accessory (the disclosure chevron a
|
|
150
157
|
* browsable row gets); `'none'` clears an inherited value, restoring
|
|
151
158
|
* the derived behavior — inheritance has no other escape, and no SF
|
|
@@ -158,8 +165,8 @@ export interface TrackStyle {
|
|
|
158
165
|
|
|
159
166
|
/**
|
|
160
167
|
* Inherited (`track ?? section ?? page`): the card treatment's tint
|
|
161
|
-
* color (hex, e.g. `'#1e3a8a'`) —
|
|
162
|
-
*
|
|
168
|
+
* color (hex, e.g. `'#1e3a8a'`) — the card's own color, or the color
|
|
169
|
+
* that fades in behind the labels when {@link TrackStyle.cardImage} is
|
|
163
170
|
* `'background'`. Card-prefixed: inert unless the container renders
|
|
164
171
|
* `gridTile: 'card'`.
|
|
165
172
|
*
|
package/website/guide/artwork.md
CHANGED
|
@@ -238,7 +238,7 @@ A page's [`Section`](/api/types/browser-nodes/#section) can render its tracks as
|
|
|
238
238
|
}
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
-
Each tile is an ordinary Track, so its `artwork` runs through the same pipeline as any other. Tile styles presume artwork: a track without any renders as a placeholder tile plus its title — the artwork `resolve` hook is the place to supply fallback art. On CarPlay a single-line grid (`gridWrap: false`) shows the tiles that fit (
|
|
241
|
+
Each tile is an ordinary Track, so its `artwork` runs through the same pipeline as any other. Tile styles presume artwork: a track without any renders as a placeholder tile plus its title — the artwork `resolve` hook is the place to supply fallback art. On CarPlay a single-line grid (`gridWrap: false`) shows the tiles that fit on one line (seven on a wide screen, or two condensed cells; the rest are truncated) and a wrapping grid takes as many lines as it needs on iOS 26+ (rendering as a list before that); Android Auto's grid always wraps, so it renders both identically. See [Browser → Presentation](/guide/browser#presentation) for the full per-surface rundown.
|
|
242
242
|
|
|
243
243
|
## API summary
|
|
244
244
|
|
package/website/guide/browser.md
CHANGED
|
@@ -521,9 +521,9 @@ Presentation lives in `style` blocks, separate from content, plus a few Track fi
|
|
|
521
521
|
| `style` (same block) | a page | declaration for the whole page — a section overrides it for its own children | all |
|
|
522
522
|
| `style: { display: 'grid' }` | a browsable track | the layout of the page it opens — Android Auto reads it from the parent item | Android Auto / AAOS |
|
|
523
523
|
| `style: { artworkRendering: 'stencil' }` | a track / section / page | tint monochrome artwork to the surface appearance | CarPlay |
|
|
524
|
-
| `style: { imageShape: 'circular' }` | a track / section / page | crop
|
|
525
|
-
| `style: { accessorySymbol: 'lock.fill' }` | a track / section / page | SF Symbol accessory on rows and
|
|
526
|
-
| `style: { gridTile: 'card' }` | a section / page | the grid's tile kind: `'plain'` (default), `'card'`, or `'condensed'`
|
|
524
|
+
| `style: { imageShape: 'circular' }` | a track / section / page | crop `'image'` and `'condensed'` tile artwork circular (people) | CarPlay (iOS 26+) |
|
|
525
|
+
| `style: { accessorySymbol: 'lock.fill' }` | a track / section / page | SF Symbol accessory on rows and on `'image'` / `'condensed'` tiles | CarPlay |
|
|
526
|
+
| `style: { gridTile: 'card' }` | a section / page | the grid's tile kind: `'plain'` (default), `'image'`, `'card'`, or `'condensed'` | CarPlay (iOS 26+) |
|
|
527
527
|
| `style: { cardTint: '#1e3a8a' }` (+ `cardImage`) | a track / section / page | card color and image mode (`'background'` fills the card full-height) | CarPlay (iOS 26+) |
|
|
528
528
|
| `disabled: true` | a track | unavailable: never plays; grayed where drawable, hidden elsewhere | all |
|
|
529
529
|
| `artwork: 'sf:heart.fill'` | any item | SF Symbol icon (supports `?bg=…&fg=…`) | iOS |
|
|
@@ -532,11 +532,11 @@ Presentation lives in `style` blocks, separate from content, plus a few Track fi
|
|
|
532
532
|
| `albumPath` + `resolveAlbumPath` | track + config | make the now-playing album line tappable | CarPlay |
|
|
533
533
|
|
|
534
534
|
::: info Caveats from the table
|
|
535
|
-
**`albumPath` requires `album`** — CarPlay renders the tappable line from the album metadata, so without an `album` there is no line to tap. **\*Layouts render each platform's nearest supported form** — a single-line grid (`gridWrap: false`) on CarPlay shows the tiles that fit (
|
|
535
|
+
**`albumPath` requires `album`** — CarPlay renders the tappable line from the album metadata, so without an `album` there is no line to tap. **\*Layouts render each platform's nearest supported form** — a single-line grid (`gridWrap: false`) on CarPlay shows the tiles that fit on one line (seven on a wide screen, or two condensed cells) and truncates the rest; a wrapping grid takes as many lines as it needs on iOS 26+ and renders as a list before that. Android Auto's grid always wraps, so it shows more, never less. No car surface scrolls horizontally; an app UI typically renders `gridWrap: false` as a horizontal scroller. Truncation is visual only: tapping a tile queues the whole section (see [Playback behavior](#playback-behavior)). Item properties like `artworkRendering`, `imageShape`, and `accessorySymbol` are inherited — `track ?? section ?? page`, per property — so a section-wide value can be overridden per item; `accessorySymbol: 'none'` is the per-item escape from an inherited symbol, and where the accessory draws is the platform's choice per surface (trailing on list rows and condensed tiles, leading the title on image tiles). On an iOS 26 grid, which tile draws what is `gridTile`'s choice: `'plain'` has a subtitle but no shape and no accessory, `'image'` has a shape and an accessory but no subtitle, `'condensed'` has all three beside a small image, and `'card'` has a subtitle, a tint and an image mode. `display` is never inherited: it always describes the children of whatever it's set on. On a page or section it lays out that page or section; on a browsable track it declares the layout of the page that track opens — Android Auto needs it there, or that page renders as a list. `gridWrap` is inert unless `display` is `'grid'`. One AAOS caveat: AOSP-based Automotive units honor only the parent item's `display`, so a page mixing grid and list sections renders uniformly there — see [Android Auto → Browse display](/guide/android-auto#browse-display).
|
|
536
536
|
:::
|
|
537
537
|
|
|
538
538
|
::: tip Inert declarations are logged in debug builds
|
|
539
|
-
A combination that can never render — `gridWrap` or `gridTile` outside a grid, `cardTint` with no card treatment in scope, `imageShape`
|
|
539
|
+
A combination that can never render — `gridWrap` or `gridTile` outside a grid, `cardTint` with no card treatment in scope, `imageShape` anywhere but an image or condensed grid, `accessorySymbol` on a plain or card grid, `display` on a track that renders playable — is inert, not an error, so nothing breaks. In a **debug build**, each one is logged once per page resolution (`Timber` on Android, `os.log` on iOS) naming the section, the effective style, and the fix:
|
|
540
540
|
|
|
541
541
|
```
|
|
542
542
|
Inert style declaration on page '/home': `style.gridWrap` on section 'Featured'
|
|
@@ -557,6 +557,8 @@ A property a _surface_ can't draw is never reported — that's the aspirational
|
|
|
557
557
|
title: 'Home',
|
|
558
558
|
sections: [
|
|
559
559
|
{ title: 'Popular', style: { display: 'grid', gridWrap: false }, path: '/popular', children: [...] },
|
|
560
|
+
// Round tiles for people: the image tile is the one with a shape.
|
|
561
|
+
{ title: 'Artists', style: { display: 'grid', gridTile: 'image', imageShape: 'circular' }, children: [...] },
|
|
560
562
|
{ title: 'All stations', children: [...] }
|
|
561
563
|
]
|
|
562
564
|
}
|
|
@@ -567,6 +569,10 @@ A property a _surface_ can't draw is never reported — that's the aspirational
|
|
|
567
569
|
}
|
|
568
570
|
```
|
|
569
571
|
|
|
572
|
+
::: tip See every style rendered
|
|
573
|
+
The example app's **Style Lab** tab draws each `style` declaration on CarPlay: one page per `gridTile` value, plus pages for section headers and for inheritance from page to section to track. Every shelf is titled with the block it renders, so what you see on screen names what to copy. The pages are plain route data in [`style-lab.ts`](https://github.com/radio-garden/react-native-audio-browser/blob/main/apps/example-native/src/utils/style-lab.ts), with a comment on each example saying what it shows.
|
|
574
|
+
:::
|
|
575
|
+
|
|
570
576
|
Two config-level platform options round it out: `carPlayLoadingTitle` (localized "Loading…" on older CarPlay) and `androidControllerOfflineError`. The album-line tap needs both `albumPath` on the track and a `resolveAlbumPath` callback in the config that maps it to a browse path — see [CarPlay](/guide/carplay) and [Android Auto](/guide/android-auto) for the platform setup these build on.
|
|
571
577
|
|
|
572
578
|
## Where to go next
|
package/website/guide/track.md
CHANGED
|
@@ -142,7 +142,7 @@ Optional fields that change how a Track renders on CarPlay / Android Auto. Each
|
|
|
142
142
|
| ------------------------------------------ | ---------------------------------------------------------------------- | -------------------------- |
|
|
143
143
|
| `style: { display: 'list' \| 'grid' }` | on a browsable track: the layout of the page it opens | Android Auto / AAOS |
|
|
144
144
|
| `style: { artworkRendering: 'stencil' }` | tint monochrome artwork to the surface appearance | CarPlay |
|
|
145
|
-
| `style: { imageShape: 'circular' }` | crop
|
|
145
|
+
| `style: { imageShape: 'circular' }` | crop `'image'` and `'condensed'` tile artwork circular (people) | CarPlay (iOS 26+) |
|
|
146
146
|
| `style: { accessorySymbol: '…' }` | SF Symbol accessory; `'none'` restores the derived accessory | CarPlay |
|
|
147
147
|
| `style: { cardTint: '…', cardImage: '…' }` | card color and image mode, when the section renders `gridTile: 'card'` | CarPlay (iOS 26+) |
|
|
148
148
|
| `disabled` | unavailable: never plays; grayed where drawable, hidden elsewhere | all |
|