Switch to dashboard strategy from view strategy
This commit is contained in:
parent
ef218a94af
commit
95cb2f48ad
|
|
@ -16,10 +16,16 @@ cookcli/recipe repeuple aussi l'entité todo d'ingrédients (todo.py) avec les
|
||||||
ingrédients de la recette ouverte, et renvoie son entity_id dans
|
ingrédients de la recette ouverte, et renvoie son entity_id dans
|
||||||
"todo_entity_id" pour que le frontend puisse l'afficher avec la carte
|
"todo_entity_id" pour que le frontend puisse l'afficher avec la carte
|
||||||
native `todo-list`.
|
native `todo-list`.
|
||||||
|
|
||||||
|
cookcli/recipes inclut "view_path" par recette : le slug utilisé comme path
|
||||||
|
de vue HA, calculé une seule fois ici (voir _view_path) pour que
|
||||||
|
cookcli-card.js et la dashboard strategy (cookcli-recipe-strategy.js)
|
||||||
|
naviguent vers le même identifiant sans dupliquer la logique de slug.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
@ -48,6 +54,20 @@ def _image_url(entry_id: str, image_ref: str | None) -> str | None:
|
||||||
return f"/api/cookcli/image/{entry_id}/{quote(image_ref, safe='/')}"
|
return f"/api/cookcli/image/{entry_id}/{quote(image_ref, safe='/')}"
|
||||||
|
|
||||||
|
|
||||||
|
def _view_path(recipe_path: str) -> str:
|
||||||
|
"""Slug utilisé comme path de vue HA pour cette recette.
|
||||||
|
|
||||||
|
Calculé une fois ici et renvoyé au frontend (dans "view_path"), pour que
|
||||||
|
cookcli-card.js et la dashboard strategy utilisent exactement le même
|
||||||
|
identifiant sans dupliquer cette logique en JS des deux côtés.
|
||||||
|
Ne gère pas les collisions entre deux recettes qui se slugifieraient
|
||||||
|
pareil (rare en pratique, mais à garder en tête).
|
||||||
|
"""
|
||||||
|
base = re.sub(r"\.cook$", "", recipe_path, flags=re.IGNORECASE)
|
||||||
|
slug = re.sub(r"[^a-zA-Z0-9]+", "-", base).strip("-").lower()
|
||||||
|
return slug or "recette"
|
||||||
|
|
||||||
|
|
||||||
def _ingredient_summaries(ingredients: list[dict]) -> list[str]:
|
def _ingredient_summaries(ingredients: list[dict]) -> list[str]:
|
||||||
"""Formate chaque ingrédient en une ligne lisible pour la checklist."""
|
"""Formate chaque ingrédient en une ligne lisible pour la checklist."""
|
||||||
summaries = []
|
summaries = []
|
||||||
|
|
@ -85,6 +105,7 @@ async def ws_list_recipes(
|
||||||
recipes.append(
|
recipes.append(
|
||||||
{
|
{
|
||||||
"path": r.path,
|
"path": r.path,
|
||||||
|
"view_path": _view_path(r.path),
|
||||||
"name": r.name,
|
"name": r.name,
|
||||||
"time": metadata.get("time"),
|
"time": metadata.get("time"),
|
||||||
"servings": metadata.get("servings"),
|
"servings": metadata.get("servings"),
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,40 @@
|
||||||
/**
|
/**
|
||||||
* Carte Lovelace pour l'intégration CookCLI — liste navigable des recettes.
|
* Carte Lovelace pour l'intégration CookCLI — liste navigable des recettes.
|
||||||
*
|
*
|
||||||
* Le détail d'une recette n'est plus géré par cette carte : un clic sur une
|
* Le détail d'une recette est une VUE DISTINCTE par recette (path = le
|
||||||
* ligne navigue vers une vue générée par cookcli-recipe-strategy.js (voir ce
|
* "view_path" renvoyé par le backend), générée par une dashboard strategy
|
||||||
* fichier), qui affiche étapes/ingrédients/minuteur via des cartes natives
|
* (voir cookcli-recipe-strategy.js). C'est nécessaire : une view strategy ne
|
||||||
* et des cartes tierces (tabdeck-card, simple-timer-card).
|
* régénère son contenu qu'au montage de la vue, pas à chaque changement de
|
||||||
|
* l'URL — avec une seule vue partagée + un paramètre `?path=`, il fallait
|
||||||
|
* recharger la page pour voir une autre recette. Une vraie vue par recette
|
||||||
|
* force un remontage à chaque navigation, donc ça marche en SPA normal.
|
||||||
*
|
*
|
||||||
* Consomme cookcli/recipes (path, name, time, servings, tags, image_url).
|
* Consomme cookcli/recipes (path, view_path, name, time, servings, tags,
|
||||||
|
* image_url).
|
||||||
*
|
*
|
||||||
* Installation :
|
* Installation :
|
||||||
* 1. Copie ce fichier dans config/www/cookcli-card.js
|
* 1. Copie ce fichier dans config/www/cookcli-card.js
|
||||||
* 2. Paramètres -> Tableaux de bord -> menu ⋮ -> Ressources -> Ajouter :
|
* 2. Paramètres -> Tableaux de bord -> menu ⋮ -> Ressources -> Ajouter :
|
||||||
* URL "/local/cookcli-card.js", type "Module JavaScript"
|
* URL "/local/cookcli-card.js", type "Module JavaScript"
|
||||||
* 3. Ajoute une carte manuelle sur un tableau de bord :
|
* 3. Cette carte est normalement insérée automatiquement par la dashboard
|
||||||
|
* strategy cookcli-recipe-strategy.js — voir son README pour la config
|
||||||
|
* complète du tableau de bord. Utilisable aussi seule en carte manuelle,
|
||||||
|
* y compris sur un AUTRE tableau de bord que celui des recettes :
|
||||||
* type: custom:cookcli-card
|
* type: custom:cookcli-card
|
||||||
* title: Mes recettes
|
* title: Mes recettes
|
||||||
* detail_view_path: recette # doit correspondre au "path" de la vue de détail
|
* dashboard_path: test-cuisine # optionnel — voir plus bas
|
||||||
|
*
|
||||||
|
* Par défaut, un clic navigue vers une vue du tableau de bord COURANT (celui
|
||||||
|
* où la carte est affichée). Si tu places cette carte sur un autre tableau
|
||||||
|
* de bord que celui qui contient les vues générées par la dashboard
|
||||||
|
* strategy (ex: carte sur /lovelace, vues sur /test-cuisine), précise
|
||||||
|
* `dashboard_path: test-cuisine` pour que la navigation cible le bon
|
||||||
|
* tableau de bord plutôt que celui où la carte est physiquement posée.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class CookCliCard extends HTMLElement {
|
class CookCliCard extends HTMLElement {
|
||||||
static getStubConfig() {
|
static getStubConfig() {
|
||||||
return { title: "Recettes", detail_view_path: "recette" };
|
return { title: "Recettes" };
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfig(config) {
|
setConfig(config) {
|
||||||
|
|
@ -64,12 +78,13 @@ class CookCliCard extends HTMLElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_navigateToRecipe(path) {
|
_navigateToView(viewPath) {
|
||||||
const detailPath = this._config.detail_view_path || "recette";
|
const dashboardRoot = this._config.dashboard_path
|
||||||
|
? `/${this._config.dashboard_path.replace(/^\/+|\/+$/g, "")}`
|
||||||
// Racine du dashboard courant, ex: "/lovelace-cookcli" depuis
|
// Racine du dashboard courant, ex: "/lovelace-cookcli" depuis
|
||||||
// "/lovelace-cookcli/recettes" — on y accroche la vue de détail.
|
// "/lovelace-cookcli/recettes" — on y accroche la vue de la recette.
|
||||||
const dashboardRoot = window.location.pathname.split("/").slice(0, 2).join("/");
|
: window.location.pathname.split("/").slice(0, 2).join("/");
|
||||||
const url = `${dashboardRoot}/${detailPath}?path=${encodeURIComponent(path)}`;
|
const url = `${dashboardRoot}/${viewPath}`;
|
||||||
|
|
||||||
// Navigation interne HA, sans recharger la page (comme un tap_action navigate).
|
// Navigation interne HA, sans recharger la page (comme un tap_action navigate).
|
||||||
window.history.pushState(null, "", url);
|
window.history.pushState(null, "", url);
|
||||||
|
|
@ -77,9 +92,9 @@ class CookCliCard extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onClick(event) {
|
_onClick(event) {
|
||||||
const itemEl = event.target.closest("[data-path]");
|
const itemEl = event.target.closest("[data-view-path]");
|
||||||
if (itemEl) {
|
if (itemEl) {
|
||||||
this._navigateToRecipe(itemEl.dataset.path);
|
this._navigateToView(itemEl.dataset.viewPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +124,7 @@ class CookCliCard extends HTMLElement {
|
||||||
? `<img class="recipe-thumb" src="${this._escape(r.image_url)}" alt="" loading="lazy" onerror="this.style.display='none'">`
|
? `<img class="recipe-thumb" src="${this._escape(r.image_url)}" alt="" loading="lazy" onerror="this.style.display='none'">`
|
||||||
: `<div class="recipe-thumb recipe-thumb-placeholder"></div>`;
|
: `<div class="recipe-thumb recipe-thumb-placeholder"></div>`;
|
||||||
return `
|
return `
|
||||||
<div class="recipe-row" data-path="${this._escape(r.path)}">
|
<div class="recipe-row" data-view-path="${this._escape(r.view_path)}">
|
||||||
${thumb}
|
${thumb}
|
||||||
<div class="recipe-row-text">
|
<div class="recipe-row-text">
|
||||||
<div class="recipe-name">${this._escape(r.name)}</div>
|
<div class="recipe-name">${this._escape(r.name)}</div>
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,92 @@
|
||||||
/**
|
/**
|
||||||
* View strategy CookCLI : génère la vue de détail d'une recette.
|
* Strategies CookCLI pour Home Assistant : une dashboard strategy + une view
|
||||||
|
* strategy compagne.
|
||||||
*
|
*
|
||||||
* Contrairement à une carte custom classique, une view strategy ne gère pas
|
* IMPORTANT : la view strategy seule ne suffit pas pour une navigation
|
||||||
* son propre état de navigation : elle génère la config d'UNE vue HA
|
* fluide. Une view strategy régénère son contenu au MONTAGE de la vue, pas à
|
||||||
* (titre + liste de cartes) à partir de `config` (donné en YAML) et de
|
* chaque changement d'URL — avec une seule vue partagée "recette?path=..."
|
||||||
* `hass`, au moment où la vue est ouverte. Voir :
|
* pour toutes les recettes, passer de l'une à l'autre sans recharger la
|
||||||
* https://developers.home-assistant.io/docs/frontend/custom-ui/custom-strategy/#views
|
* page laissait afficher l'ancienne recette (vue déjà montée, jamais
|
||||||
*
|
* remontée). D'où la dashboard strategy : elle génère une VUE DISTINCTE par
|
||||||
* Le chemin de la recette à afficher (`path`) est lu soit dans `config`
|
* recette (un vrai `path` HA différent pour chacune), ce qui force un
|
||||||
* (fixe, une vue par recette), soit — cas normal ici — dans le paramètre
|
* remontage — donc une régénération — à chaque navigation.
|
||||||
* d'URL `?path=...` que cookcli-card.js pose avant de naviguer. Une view
|
|
||||||
* strategy tourne dans le navigateur, donc lire window.location est légitime
|
|
||||||
* même si HA ne le passe pas explicitement dans `config`.
|
|
||||||
*
|
*
|
||||||
* Cartes utilisées, toutes tierces ou natives — aucune carte custom pour le
|
* Cartes utilisées, toutes tierces ou natives — aucune carte custom pour le
|
||||||
* rendu du contenu lui-même :
|
* rendu du contenu lui-même :
|
||||||
* - markdown (native) : titre, image, ustensiles, texte des étapes
|
* - markdown (native) : titre, image, ustensiles, texte des étapes
|
||||||
* - todo-list (native) : checklist des ingrédients (todo.py côté backend)
|
* - todo-list (native) : checklist des ingrédients (todo.py backend)
|
||||||
* - button (native) : démarre le minuteur partagé avec la bonne durée
|
* - button (native) : démarre le minuteur avec la bonne durée
|
||||||
* - custom:simple-timer-card : affichage/contrôle du minuteur partagé
|
* - custom:circular-timer-card : affichage/contrôle du minuteur partagé
|
||||||
* - custom:tabdeck-card : une tab par étape
|
* - custom:tabdeck-card : une tab par étape
|
||||||
|
* - custom:cookcli-card : la liste (voir cookcli-card.js)
|
||||||
*
|
*
|
||||||
* Configuration de la vue (YAML) :
|
* Configuration du tableau de bord (remplace le contenu YAML du dashboard) :
|
||||||
* views:
|
|
||||||
* - path: recette
|
|
||||||
* strategy:
|
* strategy:
|
||||||
* type: custom:cookcli-recipe
|
* type: custom:cookcli
|
||||||
* timer_entity: timer.cookcli # créé manuellement, voir README
|
* title: Recettes
|
||||||
|
* timer_entity: timer.recette_en_cours # créé manuellement, voir README
|
||||||
* entry_id: xxxx # optionnel, si plusieurs serveurs CookCLI
|
* entry_id: xxxx # optionnel, si plusieurs serveurs CookCLI
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
class CookCliDashboardStrategy extends HTMLElement {
|
||||||
|
static getCreateSuggestions() {
|
||||||
|
return { title: "Recettes", icon: "mdi:chef-hat" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static async generate(config, hass) {
|
||||||
|
config = config || {};
|
||||||
|
|
||||||
|
const wsMsg = { type: "cookcli/recipes" };
|
||||||
|
if (config.entry_id) wsMsg.entry_id = config.entry_id;
|
||||||
|
|
||||||
|
let recipes = [];
|
||||||
|
try {
|
||||||
|
const result = await hass.connection.sendMessagePromise(wsMsg);
|
||||||
|
recipes = result.recipes || [];
|
||||||
|
} catch (err) {
|
||||||
|
// Liste vide plutôt que planter tout le dashboard ; la carte affichera
|
||||||
|
// son propre message d'erreur au prochain essai de fetch côté carte.
|
||||||
|
}
|
||||||
|
|
||||||
|
const listView = {
|
||||||
|
title: config.title || "Recettes",
|
||||||
|
path: "recettes",
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
type: "custom:cookcli-card",
|
||||||
|
title: config.title || "Mes recettes",
|
||||||
|
entry_id: config.entry_id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const recipeViews = recipes.map((recipe) => ({
|
||||||
|
path: recipe.view_path,
|
||||||
|
subview: true,
|
||||||
|
strategy: {
|
||||||
|
type: "custom:cookcli-recipe",
|
||||||
|
path: recipe.path,
|
||||||
|
timer_entity: config.timer_entity,
|
||||||
|
entry_id: config.entry_id,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: config.title || "Recettes",
|
||||||
|
views: [listView, ...recipeViews],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View strategy : génère le contenu de la vue d'UNE recette. `config.path`
|
||||||
|
* est fourni par la dashboard strategy ci-dessus ; utilisable seule (une vue
|
||||||
|
* par recette écrite à la main) en le fixant directement dans le YAML.
|
||||||
|
*/
|
||||||
class CookCliRecipeViewStrategy extends HTMLElement {
|
class CookCliRecipeViewStrategy extends HTMLElement {
|
||||||
static async generate(config, hass) {
|
static async generate(config, hass) {
|
||||||
config = config || {};
|
config = config || {};
|
||||||
const params = new URLSearchParams(window.location.search);
|
const path = config.path;
|
||||||
const path = config.path || params.get("path");
|
|
||||||
|
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -202,8 +254,9 @@ class CookCliRecipeViewStrategy extends HTMLElement {
|
||||||
* `duration` peut être un nombre (secondes/minutes selon `unit`) ou du
|
* `duration` peut être un nombre (secondes/minutes selon `unit`) ou du
|
||||||
* texte libre façon "2-3 minutes" (Cooklang autorise les plages en texte
|
* texte libre façon "2-3 minutes" (Cooklang autorise les plages en texte
|
||||||
* libre) — dans ce cas on prend le premier nombre trouvé, ce qui donne une
|
* libre) — dans ce cas on prend le premier nombre trouvé, ce qui donne une
|
||||||
* estimation basse ; ajustable ensuite via les boutons +/- de
|
* estimation basse ; ajustable ensuite via circular-timer-card (tap =
|
||||||
* simple-timer-card ou l'éditeur de durée intégré.
|
* toggle, double-tap = annuler, appui long = plus d'infos) ou en relançant
|
||||||
|
* timer.start avec une autre durée.
|
||||||
*/
|
*/
|
||||||
static _parseDurationSeconds(duration, unit) {
|
static _parseDurationSeconds(duration, unit) {
|
||||||
let value = null;
|
let value = null;
|
||||||
|
|
@ -239,4 +292,14 @@ class CookCliRecipeViewStrategy extends HTMLElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("ll-strategy-dashboard-cookcli", CookCliDashboardStrategy);
|
||||||
customElements.define("ll-strategy-view-cookcli-recipe", CookCliRecipeViewStrategy);
|
customElements.define("ll-strategy-view-cookcli-recipe", CookCliRecipeViewStrategy);
|
||||||
|
|
||||||
|
window.customStrategies = window.customStrategies || [];
|
||||||
|
window.customStrategies.push({
|
||||||
|
type: "cookcli",
|
||||||
|
strategyType: "dashboard",
|
||||||
|
name: "CookCLI",
|
||||||
|
description: "Dashboard de recettes CookCLI : liste + une vue par recette.",
|
||||||
|
documentationURL: "https://git.thethomaas.net/TheThomaas/ha-cookcli",
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue