diff --git a/README.md b/README.md index 4f88b40..3182881 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ Request body is a JSON object. Only `message` is required. | `widthPercent` | number | 60 (TV setting) | Card width as a percentage of screen width, 10 to 100. Height follows the content. | | `background` | string (hex) | `#FFFFFF` (TV setting) | Card background. | | `textColor` | string (hex) | `#111111` (TV setting) | Title and message color. | -| `accent` | string (hex) | none (TV setting) | Color of a vertical stripe on the card's left edge. Omit for no stripe. | +| `accent` | string (hex) | none (TV setting) | Color of a vertical stripe on the card's left edge. Omit the field to use the TV's default; send `"none"`, `""` or `null` to force no stripe even when the TV has a default. | | `dim` | number | 0 (TV setting) | Opacity, 0 to 1, of a full-screen black layer behind the card. `0.9` hides what is playing almost completely. | | `sound` | string | `default` (TV setting) | `none`, `default` for the bundled chime, or an `http(s)` URL of an audio file to stream. | | `speak` | boolean | `false` | Read the title and message aloud with the TV's text-to-speech engine after the sound finishes. | diff --git a/client/README.md b/client/README.md index 3b031ba..dec26cd 100644 --- a/client/README.md +++ b/client/README.md @@ -48,6 +48,7 @@ are in the root README under "Hosted client". 3. **Send.** Posts to every selected device in parallel and lists success or the actual error per device. **Clear card by ID** sends `DELETE /notify/{id}` for the ID in the composer, which is how you remove a persistent card. + Turning the accent switch off sends `"accent": "none"`, which overrides any default accent set on the TV. 4. **History tab.** Every send in this browser session, with per-device results. Re-send it as-is, or load it back into the composer. 5. **curl tab.** The composed notification as a working `curl` command against the chosen diff --git a/client/src/lib/payload.ts b/client/src/lib/payload.ts index 7f75605..7305d06 100644 --- a/client/src/lib/payload.ts +++ b/client/src/lib/payload.ts @@ -64,7 +64,7 @@ export function toWire(payload: NotificationPayload): WirePayload { wire.widthPercent = payload.widthPercent wire.background = payload.background wire.textColor = payload.textColor - text('accent') + wire.accent = payload.accent.trim() || 'none' wire.dim = Number(payload.dim.toFixed(2)) wire.sound = payload.sound.trim() || 'none' wire.speak = payload.speak diff --git a/client/src/lib/tvApi.ts b/client/src/lib/tvApi.ts index d57666b..a77bdd4 100644 --- a/client/src/lib/tvApi.ts +++ b/client/src/lib/tvApi.ts @@ -29,9 +29,9 @@ async function readError(response: Response): Promise { function describeNetworkError(error: unknown): string { if (error instanceof DOMException && error.name === 'TimeoutError') return 'Timed out waiting for the TV' if (error instanceof TypeError && servedOverHttps()) { - return 'Blocked: this page is HTTPS and the TV is HTTP. Allow insecure content for this site in your browser.' + return 'Blocked by the browser (this page is HTTPS, the TV is HTTP) or nothing answered on that port. Check the port is 8090, then allow insecure content for this site.' } - if (error instanceof TypeError) return 'Unreachable (network error or blocked by the browser)' + if (error instanceof TypeError) return 'Unreachable: check the IP and that the port is 8090, not 5555' return error instanceof Error ? error.message : String(error) } diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/model/NotificationPayloadParser.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/model/NotificationPayloadParser.kt index 2980853..d9e79d2 100644 --- a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/model/NotificationPayloadParser.kt +++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/model/NotificationPayloadParser.kt @@ -37,7 +37,7 @@ class NotificationPayloadParser( widthPercent = int(json, "widthPercent", 10, 100) ?: defaults.widthPercent, background = color(json, "background") ?: defaults.background, textColor = color(json, "textColor") ?: defaults.textColor, - accent = color(json, "accent") ?: defaults.accent, + accent = accent(json, defaults.accent), dim = fraction(json, "dim") ?: defaults.dim, sound = sound(json) ?: defaults.sound, speak = boolean(json, "speak") ?: false, @@ -92,6 +92,17 @@ class NotificationPayloadParser( return HexColor.parse(value) ?: throw FieldError("field '$key' must be a hex color like #RRGGBB") } + private fun accent(json: JSONObject, default: Int?): Int? { + if (!json.has("accent")) return default + if (json.isNull("accent")) return null + val value = json.get("accent") as? String + ?: throw FieldError("field 'accent' must be a hex color like #RRGGBB, \"none\" or null") + val trimmed = value.trim() + if (trimmed.isEmpty() || trimmed.equals("none", ignoreCase = true)) return null + return HexColor.parse(trimmed) + ?: throw FieldError("field 'accent' must be a hex color like #RRGGBB, \"none\" or null") + } + private fun position(json: JSONObject): Position? { val value = string(json, "position") ?: return null return Position.fromWire(value) diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/StepperRow.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/StepperRow.kt index 0b0fd24..dbd5743 100644 --- a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/StepperRow.kt +++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/StepperRow.kt @@ -28,7 +28,7 @@ fun StepperRow( ) { ListItem( selected = false, - onClick = onIncrease, + onClick = {}, modifier = modifier.onKeyEvent { event -> if (event.type != KeyEventType.KeyDown) return@onKeyEvent false when (event.key) { diff --git a/tv/app/src/test/kotlin/com/zorenkonte/tibeepost/model/NotificationPayloadParserTest.kt b/tv/app/src/test/kotlin/com/zorenkonte/tibeepost/model/NotificationPayloadParserTest.kt index 340e847..29d2658 100644 --- a/tv/app/src/test/kotlin/com/zorenkonte/tibeepost/model/NotificationPayloadParserTest.kt +++ b/tv/app/src/test/kotlin/com/zorenkonte/tibeepost/model/NotificationPayloadParserTest.kt @@ -123,6 +123,22 @@ class NotificationPayloadParserTest { assertEquals(200, n.title!!.length) } + @Test + fun explicitEmptyAccentOverridesTheDefault() { + val withAccent = NotificationDefaults(accent = 0xFFFF1744.toInt()) + assertEquals(0xFFFF1744.toInt(), success("""{"message":"m"}""", withAccent).accent) + assertNull(success("""{"message":"m","accent":""}""", withAccent).accent) + assertNull(success("""{"message":"m","accent":"none"}""", withAccent).accent) + assertNull(success("""{"message":"m","accent":null}""", withAccent).accent) + assertEquals(0xFF00C853.toInt(), success("""{"message":"m","accent":"#00C853"}""", withAccent).accent) + } + + @Test + fun invalidAccentNamesTheField() { + assertTrue(failure("""{"message":"m","accent":"stripe"}""").startsWith("field 'accent' must be")) + assertTrue(failure("""{"message":"m","accent":12}""").startsWith("field 'accent' must be")) + } + @Test fun blankOptionalStringsCountAsAbsent() { val n = success("""{"message":"m","image":"","accent":"","title":""}""")