diff --git a/www/cookcli-card.js b/www/cookcli-card.js new file mode 100644 index 0000000..0fbde99 --- /dev/null +++ b/www/cookcli-card.js @@ -0,0 +1,283 @@ +/** + * Carte Lovelace pour l'intégration CookCLI. + * + * Consomme les commandes websocket exposées par l'intégration : + * - cookcli/recipes -> liste (path, name, time, servings, tags) + * - cookcli/recipe -> détail (title, ingredients, cookware, sections) + * + * Limitation connue : les images de recettes ne sont pas affichées. Le + * serveur CookCLI n'est joignable depuis le navigateur ni sur son port + * ingress (restreint au Supervisor) ni sur son port direct (non exposé au + * LAN) — il faudrait un endpoint de proxy côté intégration pour les servir. + * Voir le README du projet pour plus de détails si tu veux l'ajouter. + * + * 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 : + * type: custom:cookcli-card + * title: Mes recettes + */ + +class CookCliCard extends HTMLElement { + static getStubConfig() { + return { title: "Recettes" }; + } + + setConfig(config) { + this._config = config || {}; + this._view = "list"; + this._recipes = null; + this._selected = null; + this._loading = false; + this._error = null; + + if (!this.shadowRoot) { + this.attachShadow({ mode: "open" }); + this.shadowRoot.addEventListener("click", (event) => this._onClick(event)); + } + this._render(); + } + + set hass(hass) { + this._hass = hass; + if (this._recipes === null && !this._loading) { + this._fetchRecipes(); + } + } + + getCardSize() { + return 6; + } + + _wsMessage(extra) { + const msg = { ...extra }; + if (this._config.entry_id) { + msg.entry_id = this._config.entry_id; + } + return msg; + } + + async _fetchRecipes() { + this._loading = true; + this._error = null; + this._render(); + try { + const result = await this._hass.connection.sendMessagePromise( + this._wsMessage({ type: "cookcli/recipes" }) + ); + this._recipes = result.recipes || []; + } catch (err) { + this._error = (err && err.message) || "Erreur inconnue"; + } finally { + this._loading = false; + this._render(); + } + } + + async _openRecipe(path) { + this._loading = true; + this._error = null; + this._render(); + try { + const result = await this._hass.connection.sendMessagePromise( + this._wsMessage({ type: "cookcli/recipe", path }) + ); + this._selected = result; + this._view = "detail"; + } catch (err) { + this._error = (err && err.message) || "Erreur inconnue"; + } finally { + this._loading = false; + this._render(); + } + } + + _onClick(event) { + const backEl = event.target.closest("[data-action='back']"); + if (backEl) { + this._view = "list"; + this._selected = null; + this._error = null; + this._render(); + return; + } + const itemEl = event.target.closest("[data-path]"); + if (itemEl) { + this._openRecipe(itemEl.dataset.path); + } + } + + _escape(str) { + const div = document.createElement("div"); + div.textContent = str ?? ""; + return div.innerHTML; + } + + _formatQuantity(quantity) { + if (!quantity) return ""; + const value = quantity.value ?? ""; + const unit = quantity.unit ?? ""; + return `${value} ${unit}`.trim(); + } + + _renderStepItem(item) { + switch (item.type) { + case "text": + return this._escape(item.value); + case "ingredient": { + const qty = this._formatQuantity(item.quantity); + return `${this._escape(item.name)}${ + qty ? ` (${this._escape(qty)})` : "" + }`; + } + case "cookware": + return `${this._escape(item.name)}`; + case "timer": { + const duration = item.duration ?? ""; + const unit = item.unit ?? ""; + return `⏱ ${this._escape(String(duration))}${ + unit ? " " + this._escape(unit) : "" + }`; + } + default: + return ""; + } + } + + _renderList() { + if (this._loading) { + return `