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 `
Chargement…
`; + } + if (this._error) { + return `
${this._escape(this._error)}
`; + } + if (!this._recipes || this._recipes.length === 0) { + return `
Aucune recette trouvée.
`; + } + + const rows = this._recipes + .map((r) => { + const meta = []; + if (r.time) meta.push(`⏱ ${this._escape(r.time)}`); + if (r.servings) meta.push(`🍽 ${this._escape(String(r.servings))}`); + return ` +
+
${this._escape(r.name)}
+
${meta.join("")}
+
`; + }) + .join(""); + + return `
${rows}
`; + } + + _renderDetail() { + if (this._loading) { + return `
Chargement…
`; + } + if (this._error) { + return ` + +
${this._escape(this._error)}
`; + } + if (!this._selected) return ""; + + const r = this._selected; + + const ingredients = (r.ingredients || []) + .map((i) => { + const qty = this._formatQuantity(i.quantity); + return `
  • ${qty ? `${this._escape(qty)} ` : ""}${this._escape( + i.name + )}
  • `; + }) + .join(""); + + const cookware = (r.cookware || []) + .map((c) => `
  • ${this._escape(c.name)}
  • `) + .join(""); + + const sections = (r.sections || []) + .map((section) => { + const steps = (section.steps || []) + .map( + (step) => + `
  • ${(step.items || []) + .map((item) => this._renderStepItem(item)) + .join("")}
  • ` + ) + .join(""); + return ` + ${section.name ? `

    ${this._escape(section.name)}

    ` : ""} +
      ${steps}
    `; + }) + .join(""); + + return ` + +

    ${this._escape(r.title || "")}

    + ${cookware ? `

    Ustensiles

    ` : ""} +

    Ingrédients

    + +

    Préparation

    + ${sections} + `; + } + + _render() { + if (!this.shadowRoot) return; + const title = this._config.title || "Recettes"; + this.shadowRoot.innerHTML = ` + + +
    + ${this._view === "list" ? this._renderList() : this._renderDetail()} +
    +
    + `; + } + + _styles() { + return ` + .card-content { padding: 0 16px 16px; } + .state-msg { padding: 16px 0; color: var(--secondary-text-color); } + .state-msg.error { color: var(--error-color, #db4437); } + .recipe-list { display: flex; flex-direction: column; } + .recipe-row { + display: flex; justify-content: space-between; align-items: center; + padding: 12px 4px; border-bottom: 1px solid var(--divider-color); + cursor: pointer; + } + .recipe-row:last-child { border-bottom: none; } + .recipe-row:hover { background: var(--secondary-background-color); } + .recipe-name { font-weight: 500; color: var(--primary-text-color); } + .recipe-meta { + display: flex; gap: 12px; font-size: 0.85em; + color: var(--secondary-text-color); white-space: nowrap; + } + .back-btn { + background: none; border: none; color: var(--primary-color); + font-size: 0.95em; cursor: pointer; padding: 12px 0 4px; display: block; + } + h2 { margin: 4px 0 8px; color: var(--primary-text-color); } + h4 { margin: 16px 0 4px; color: var(--primary-text-color); } + ul, ol { margin: 4px 0; padding-left: 20px; color: var(--primary-text-color); } + ul.ingredients li, ul.cookware-list li { margin: 2px 0; } + ol.steps li { margin: 8px 0; line-height: 1.5; } + .ingredient { color: var(--primary-color); font-weight: 500; } + .cookware { font-style: italic; color: var(--secondary-text-color); } + .timer { color: var(--warning-color, #ff9800); font-weight: 500; } + `; + } +} + +customElements.define("cookcli-card", CookCliCard); + +window.customCards = window.customCards || []; +window.customCards.push({ + type: "cookcli-card", + name: "CookCLI Recipes", + description: "Liste navigable de tes recettes CookCLI, avec ingrédients et étapes.", +});