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
|
||||
"todo_entity_id" pour que le frontend puisse l'afficher avec la carte
|
||||
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
|
||||
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
|
||||
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='/')}"
|
||||
|
||||
|
||||
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]:
|
||||
"""Formate chaque ingrédient en une ligne lisible pour la checklist."""
|
||||
summaries = []
|
||||
|
|
@ -85,6 +105,7 @@ async def ws_list_recipes(
|
|||
recipes.append(
|
||||
{
|
||||
"path": r.path,
|
||||
"view_path": _view_path(r.path),
|
||||
"name": r.name,
|
||||
"time": metadata.get("time"),
|
||||
"servings": metadata.get("servings"),
|
||||
|
|
|
|||
|
|
@ -1,26 +1,40 @@
|
|||
/**
|
||||
* 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
|
||||
* ligne navigue vers une vue générée par cookcli-recipe-strategy.js (voir ce
|
||||
* fichier), qui affiche étapes/ingrédients/minuteur via des cartes natives
|
||||
* et des cartes tierces (tabdeck-card, simple-timer-card).
|
||||
* Le détail d'une recette est une VUE DISTINCTE par recette (path = le
|
||||
* "view_path" renvoyé par le backend), générée par une dashboard strategy
|
||||
* (voir cookcli-recipe-strategy.js). C'est nécessaire : une view strategy ne
|
||||
* 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 :
|
||||
* 1. Copie ce fichier dans config/www/cookcli-card.js
|
||||
* 2. Paramètres -> Tableaux de bord -> menu ⋮ -> Ressources -> Ajouter :
|
||||
* 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
|
||||
* 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 {
|
||||
static getStubConfig() {
|
||||
return { title: "Recettes", detail_view_path: "recette" };
|
||||
return { title: "Recettes" };
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
|
|
@ -64,12 +78,13 @@ class CookCliCard extends HTMLElement {
|
|||
}
|
||||
}
|
||||
|
||||
_navigateToRecipe(path) {
|
||||
const detailPath = this._config.detail_view_path || "recette";
|
||||
// Racine du dashboard courant, ex: "/lovelace-cookcli" depuis
|
||||
// "/lovelace-cookcli/recettes" — on y accroche la vue de détail.
|
||||
const dashboardRoot = window.location.pathname.split("/").slice(0, 2).join("/");
|
||||
const url = `${dashboardRoot}/${detailPath}?path=${encodeURIComponent(path)}`;
|
||||
_navigateToView(viewPath) {
|
||||
const dashboardRoot = this._config.dashboard_path
|
||||
? `/${this._config.dashboard_path.replace(/^\/+|\/+$/g, "")}`
|
||||
// Racine du dashboard courant, ex: "/lovelace-cookcli" depuis
|
||||
// "/lovelace-cookcli/recettes" — on y accroche la vue de la recette.
|
||||
: window.location.pathname.split("/").slice(0, 2).join("/");
|
||||
const url = `${dashboardRoot}/${viewPath}`;
|
||||
|
||||
// Navigation interne HA, sans recharger la page (comme un tap_action navigate).
|
||||
window.history.pushState(null, "", url);
|
||||
|
|
@ -77,9 +92,9 @@ class CookCliCard extends HTMLElement {
|
|||
}
|
||||
|
||||
_onClick(event) {
|
||||
const itemEl = event.target.closest("[data-path]");
|
||||
const itemEl = event.target.closest("[data-view-path]");
|
||||
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'">`
|
||||
: `<div class="recipe-thumb recipe-thumb-placeholder"></div>`;
|
||||
return `
|
||||
<div class="recipe-row" data-path="${this._escape(r.path)}">
|
||||
<div class="recipe-row" data-view-path="${this._escape(r.view_path)}">
|
||||
${thumb}
|
||||
<div class="recipe-row-text">
|
||||
<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
|
||||
* son propre état de navigation : elle génère la config d'UNE vue HA
|
||||
* (titre + liste de cartes) à partir de `config` (donné en YAML) et de
|
||||
* `hass`, au moment où la vue est ouverte. Voir :
|
||||
* https://developers.home-assistant.io/docs/frontend/custom-ui/custom-strategy/#views
|
||||
*
|
||||
* Le chemin de la recette à afficher (`path`) est lu soit dans `config`
|
||||
* (fixe, une vue par recette), soit — cas normal ici — dans le paramètre
|
||||
* 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`.
|
||||
* IMPORTANT : la view strategy seule ne suffit pas pour une navigation
|
||||
* fluide. Une view strategy régénère son contenu au MONTAGE de la vue, pas à
|
||||
* chaque changement d'URL — avec une seule vue partagée "recette?path=..."
|
||||
* pour toutes les recettes, passer de l'une à l'autre sans recharger la
|
||||
* 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
|
||||
* recette (un vrai `path` HA différent pour chacune), ce qui force un
|
||||
* remontage — donc une régénération — à chaque navigation.
|
||||
*
|
||||
* Cartes utilisées, toutes tierces ou natives — aucune carte custom pour le
|
||||
* rendu du contenu lui-même :
|
||||
* - markdown (native) : titre, image, ustensiles, texte des étapes
|
||||
* - todo-list (native) : checklist des ingrédients (todo.py côté backend)
|
||||
* - button (native) : démarre le minuteur partagé avec la bonne durée
|
||||
* - custom:simple-timer-card : affichage/contrôle du minuteur partagé
|
||||
* - custom:tabdeck-card : une tab par étape
|
||||
* - markdown (native) : titre, image, ustensiles, texte des étapes
|
||||
* - todo-list (native) : checklist des ingrédients (todo.py backend)
|
||||
* - button (native) : démarre le minuteur avec la bonne durée
|
||||
* - custom:circular-timer-card : affichage/contrôle du minuteur partagé
|
||||
* - custom:tabdeck-card : une tab par étape
|
||||
* - custom:cookcli-card : la liste (voir cookcli-card.js)
|
||||
*
|
||||
* Configuration de la vue (YAML) :
|
||||
* views:
|
||||
* - path: recette
|
||||
* strategy:
|
||||
* type: custom:cookcli-recipe
|
||||
* timer_entity: timer.cookcli # créé manuellement, voir README
|
||||
* entry_id: xxxx # optionnel, si plusieurs serveurs CookCLI
|
||||
* Configuration du tableau de bord (remplace le contenu YAML du dashboard) :
|
||||
* strategy:
|
||||
* type: custom:cookcli
|
||||
* title: Recettes
|
||||
* timer_entity: timer.recette_en_cours # créé manuellement, voir README
|
||||
* 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 {
|
||||
static async generate(config, hass) {
|
||||
config = config || {};
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const path = config.path || params.get("path");
|
||||
const path = config.path;
|
||||
|
||||
if (!path) {
|
||||
return {
|
||||
|
|
@ -202,8 +254,9 @@ class CookCliRecipeViewStrategy extends HTMLElement {
|
|||
* `duration` peut être un nombre (secondes/minutes selon `unit`) ou du
|
||||
* 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
|
||||
* estimation basse ; ajustable ensuite via les boutons +/- de
|
||||
* simple-timer-card ou l'éditeur de durée intégré.
|
||||
* estimation basse ; ajustable ensuite via circular-timer-card (tap =
|
||||
* toggle, double-tap = annuler, appui long = plus d'infos) ou en relançant
|
||||
* timer.start avec une autre durée.
|
||||
*/
|
||||
static _parseDurationSeconds(duration, unit) {
|
||||
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);
|
||||
|
||||
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