Show the album cover on a media tile, in the same strip as the live camera pictures (0.2.92)
A single or double-width media player tile takes Display -> Album cover on a Guition: the cover of what plays in the icon's place, like Home Assistant's own tile. It rides in the page's strip of 0.2.91: the request and the answer are the same, a media player is simply among the tiles. The app fetches a player's picture only when its address changes; a page with a camera loads at the camera's pace with the cover reused, a page of media tiles alone loads once, and a new track (the picture's mark in the state, part of the page's wish) asks again at once. A player without a picture keeps its icon and is not asked for; the double-width tile keeps its controls; the tile over the whole page keeps the card's big cover. Firmware 0.2.78. Host harness 31 checks (docs/TEST_RESULTS_0292.md). CYD firmware 1,633,760 B, 89.0 %. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -130,7 +130,7 @@ async function onKey(e: KeyboardEvent) {
|
||||
<span class="lead"><span class="big">{{ clockText(clock24, now) }}</span><span class="nm">{{ name }}</span></span>
|
||||
</template>
|
||||
<template v-else-if="full">
|
||||
<span class="ic mdi" :class="{ lit: isOn, thumb: display === 'live' }">{{ glyph(tileIconCp(tile)) }}</span>
|
||||
<span class="ic mdi" :class="{ lit: isOn, thumb: display === 'live' || display === 'cover' }">{{ glyph(tileIconCp(tile)) }}</span>
|
||||
<span class="lead">
|
||||
<span class="nm">{{ name }}</span>
|
||||
<span v-if="goesTo" class="goto">{{ pageLink }}</span>
|
||||
@@ -150,7 +150,7 @@ async function onKey(e: KeyboardEvent) {
|
||||
</template>
|
||||
<template v-else-if="wide">
|
||||
<span class="lead">
|
||||
<span class="ic mdi" :class="{ lit: isOn, thumb: display === 'live' }">{{ glyph(tileIconCp(tile)) }}</span>
|
||||
<span class="ic mdi" :class="{ lit: isOn, thumb: display === 'live' || display === 'cover' }">{{ glyph(tileIconCp(tile)) }}</span>
|
||||
<span class="tx">
|
||||
<span class="nm">{{ name }}</span>
|
||||
<span v-if="goesTo" class="goto">{{ pageLink }}</span>
|
||||
@@ -178,7 +178,7 @@ async function onKey(e: KeyboardEvent) {
|
||||
<!-- As the screen draws it: the icon on the left, the name and the value beside it. A watch
|
||||
card puts the name on top and the big value under it; the small slider runs underneath. -->
|
||||
<span class="head" :class="{ top: display === 'watch' }">
|
||||
<span class="ic mdi" :class="{ lit: isOn, thumb: display === 'live' }">{{ glyph(tileIconCp(tile)) }}</span>
|
||||
<span class="ic mdi" :class="{ lit: isOn, thumb: display === 'live' || display === 'cover' }">{{ glyph(tileIconCp(tile)) }}</span>
|
||||
<span class="tx">
|
||||
<span class="nm">{{ name }}</span>
|
||||
<span v-if="goesTo" class="goto">{{ pageLink }}</span>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { computed, toRaw } from "vue";
|
||||
import { t } from "../i18n";
|
||||
import { domainInfo, entriesOf, MAX_PAGES, pageCount, pageOf, pageTarget, SLIDER_DOMAINS, TOGGLE_BEFORE } from "../model/layout";
|
||||
import { glyph } from "../model/topbar";
|
||||
import { automaticIcon, closeInspector, entityName, fullPage, markDirty, moveTileToPage, removeTile, retargetPageTile, setTileOption, state, supports, tileIconCp } from "../store";
|
||||
import { automaticIcon, closeInspector, entityName, fullPage, isGuition, markDirty, moveTileToPage, removeTile, retargetPageTile, setTileOption, state, supports, tileIconCp } from "../store";
|
||||
import type { Tile } from "../types";
|
||||
import ActionPicker from "./ActionPicker.vue";
|
||||
import IconPicker from "./IconPicker.vue";
|
||||
@@ -51,6 +51,8 @@ const displays = computed(() => {
|
||||
if (domain.value === "sun") keys.push("sunpath");
|
||||
// A live picture on a camera tile (app 0.2.91): the picker only offers cameras on a Guition, the board that draws images.
|
||||
if (["camera", "image"].includes(domain.value)) keys.push("live");
|
||||
// The album cover on a media tile (app 0.2.92), on a Guition too; the tile over the whole page has the card's big cover.
|
||||
if (domain.value === "media_player" && (isGuition.value || display.value === "cover") && size.value !== "full") keys.push("cover");
|
||||
return keys.map((key) => [key, t(`editor.tile.display.${key}`)] as [string, string]);
|
||||
});
|
||||
const displayHint = computed(() => {
|
||||
@@ -58,6 +60,7 @@ const displayHint = computed(() => {
|
||||
if (c && display.value === "graph" && !c.displays.includes("graph")) return t("editor.tile.display.no_graph");
|
||||
if (c && display.value === "forecast" && !c.displays.includes("forecast")) return t("editor.tile.display.no_forecast");
|
||||
if (display.value === "live") return t(supports(0, 2, 77) ? "editor.tile.display.live_hint" : "editor.tile.display.live_needs_firmware");
|
||||
if (display.value === "cover") return t(supports(0, 2, 78) ? "editor.tile.display.cover_hint" : "editor.tile.display.cover_needs_firmware");
|
||||
return "";
|
||||
});
|
||||
const refresh = computed(() => current("refresh", 15) as number);
|
||||
@@ -128,7 +131,7 @@ function inspect() {
|
||||
<div v-else class="f">
|
||||
<span class="f-label">{{ t("editor.tile.display.label") }}</span>
|
||||
<Segmented :choices="displays" :value="display" @pick="(v) => setTileOption(tile, 'display', v)" />
|
||||
<small v-if="displayHint" :class="{ warn: display !== 'live' || !supports(0, 2, 77) }">{{ displayHint }}</small>
|
||||
<small v-if="displayHint" :class="{ warn: !(display === 'live' && supports(0, 2, 77)) && !(display === 'cover' && supports(0, 2, 78)) }">{{ displayHint }}</small>
|
||||
</div>
|
||||
<div v-if="display === 'live'" class="f">
|
||||
<span class="f-label">{{ t("editor.tile.refresh.label") }}</span>
|
||||
|
||||
@@ -149,7 +149,7 @@ export const newTile = (id: string): Tile => ({ entity: id, name: "", slot: -1,
|
||||
// without a choice the domain's first control set applies to a wide card, none to a full one.
|
||||
export function effectiveControls(tile: Tile, inventory: Inventory): string | null {
|
||||
const domain = tile.entity.split(".")[0], catalogue = inventory.controls?.[domain], o = tile.options || {};
|
||||
if (!catalogue || !["wide", "full"].includes(o.size as string) || (o.display || "standard") !== "standard" || o.inline === "slider") return null;
|
||||
if (!catalogue || !["wide", "full"].includes(o.size as string) || !["standard", "cover"].includes((o.display || "standard") as string) || o.inline === "slider") return null;
|
||||
const choice = o.controls ?? (o.size === "full" ? "none" : catalogue.default);
|
||||
return choice === "none" ? null : choice;
|
||||
}
|
||||
@@ -177,7 +177,7 @@ export function tileLimit(firmware: string | undefined | null) {
|
||||
return versionAtLeast(firmware, "0.2.62") ? MAX_TILES : versionAtLeast(firmware, "0.2.7") ? 20 : 10;
|
||||
}
|
||||
// What a tile shows and how big it is, in a few words (editor.displays, editor.sizes); a key it doesn't know stays as it is.
|
||||
export const DISPLAYS = ["standard", "watch", "forecast", "graph", "digital", "analog", "sunpath"];
|
||||
export const DISPLAYS = ["standard", "watch", "forecast", "graph", "digital", "analog", "sunpath", "live", "cover"];
|
||||
export const displayName = (display: string) => (DISPLAYS.includes(display) ? t(`editor.displays.${display}`) : display);
|
||||
export const sizeName = (size: string | undefined) => t(`editor.sizes.${SIZES.includes(size as Size) ? size : "single"}`);
|
||||
export const TOGGLE_BEFORE = ["light", "switch", "input_boolean", "fan", "media_player", "climate"];
|
||||
|
||||
@@ -213,6 +213,31 @@ describe("TileInspector: a live picture on a camera tile (app 0.2.91)", () => {
|
||||
const card = mount(TileCard, { props: { tile, slot: 0 } });
|
||||
expect(card.find(".ic").classes()).toContain("thumb");
|
||||
});
|
||||
it("offers the album cover for a media player on a Guition, not on a full-page tile, and keeps its controls", async () => {
|
||||
Object.assign(state.inventory.screens[0], { firmware: "0.2.78" });
|
||||
state.inventory.entities.push({ id: "media_player.sonos", name: "Sonos", state: "playing", area: "Hall" } as any);
|
||||
(state.inventory as any).controls.media_player = { default: "volume", choices: [{ key: "volume", label: "Volume" }, { key: "none", label: "None" }] };
|
||||
const tile: Tile = { entity: "media_player.sonos", name: "", slot: 0, options: { size: "wide" } };
|
||||
state.layout!.tiles.push(tile);
|
||||
const drawer = mount(TileInspector, { props: { tile } });
|
||||
expect(choices(drawer, "Display")).toEqual(["Name and status", "Large value", "Album cover"]);
|
||||
await row(drawer, "Display").findAll(".seg button")[2].trigger("click");
|
||||
expect(tile.options).toEqual({ size: "wide", display: "cover" });
|
||||
expect(row(drawer, "Display").find("small").text()).toMatch(/icon's place/);
|
||||
expect(row(drawer, "Refresh")).toBeUndefined();
|
||||
expect(row(drawer, "Direct control on the tile")).toBeDefined();
|
||||
const card = mount(TileCard, { props: { tile, slot: 0 } });
|
||||
expect(card.find(".ic").classes()).toContain("thumb");
|
||||
expect(card.find(".range").exists()).toBe(true);
|
||||
// A CYD gets no such choice; a tile over the whole page keeps the card's big cover.
|
||||
Object.assign(state.inventory.screens[0], { board: "cyd" });
|
||||
tile.options = { size: "single" };
|
||||
await drawer.vm.$nextTick();
|
||||
expect(choices(mount(TileInspector, { props: { tile } }), "Display")).toEqual(["Name and status", "Large value"]);
|
||||
Object.assign(state.inventory.screens[0], { board: "guition" });
|
||||
tile.options = { size: "full" };
|
||||
expect(choices(mount(TileInspector, { props: { tile } }), "Display")).toEqual(["Name and status", "Large value"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TileInspector: pages (app 0.2.78)", () => {
|
||||
|
||||
Reference in New Issue
Block a user