diff --git a/MMM-NextcloudCookbook.js b/MMM-NextcloudCookbook.js index a2b53ea..7382754 100644 --- a/MMM-NextcloudCookbook.js +++ b/MMM-NextcloudCookbook.js @@ -28,9 +28,17 @@ Module.register("MMM-NextcloudCookbook", { socketNotificationReceived(notification, payload) { if (notification !== "STATE") return; + const previous = this.state; this.state = payload; this.setRecipeMode(Boolean(payload.active)); - this.updateDom(this.config.animationSpeed); + const requiresDomUpdate = previous.active !== payload.active + || previous.recipeId !== payload.recipeId + || previous.portions !== payload.portions + || previous.theme !== payload.theme + || previous.error !== payload.error + || previous.recipe?.recipeYield !== payload.recipe?.recipeYield; + if (requiresDomUpdate) this.updateDom(this.config.animationSpeed); + else this.updateProgressDom(); }, notificationReceived(notification) { @@ -86,6 +94,24 @@ Module.register("MMM-NextcloudCookbook", { } }, + updateProgressDom() { + const root = document.getElementById(this.identifier)?.querySelector(".ncc-active"); + if (!root) return; + root.querySelectorAll("[data-ncc-progress-kind]").forEach((item) => { + const kind = item.dataset.nccProgressKind; + const index = Number(item.dataset.nccProgressIndex); + const completed = kind === "ingredient" + ? this.state.progress?.completedIngredients?.includes(index) + : this.state.progress?.completedSteps?.includes(index); + item.classList.toggle("ncc-completed", completed); + const checkbox = item.querySelector("input[type=checkbox]"); + if (checkbox) { + checkbox.checked = completed; + checkbox.disabled = false; + } + }); + }, + getDom() { const root = document.createElement("section"); root.className = `ncc-root ${this.state.active ? "ncc-active" : "ncc-idle"} ncc-theme-${this.state.theme || this.config.theme}`; @@ -143,9 +169,11 @@ Module.register("MMM-NextcloudCookbook", { title.textContent = recipe.name || "Untitled recipe"; header.appendChild(title); + const yieldValue = recipe.originalRecipeYield && recipe.originalRecipeYield !== recipe.recipeYield + ? `${recipe.recipeYield} (originally ${recipe.originalRecipeYield})` + : recipe.recipeYield; const metadata = [ - ["Yield", recipe.recipeYield], - ["Original yield", recipe.originalRecipeYield !== recipe.recipeYield ? recipe.originalRecipeYield : null], + ["Yield", yieldValue], ["Prep", NCCFormatDuration(recipe.prepTime)], ["Cook", NCCFormatDuration(recipe.cookTime)], ["Total", NCCFormatDuration(recipe.totalTime)] @@ -178,6 +206,8 @@ Module.register("MMM-NextcloudCookbook", { const ingredientList = document.createElement("ul"); (recipe.recipeIngredient || []).forEach((ingredient, index) => { const item = document.createElement("li"); + item.dataset.nccProgressKind = "ingredient"; + item.dataset.nccProgressIndex = index; const status = recipe.recipeIngredientStatus?.[index]; if (status === "unquantified") { item.classList.add("ncc-quantity-warning"); @@ -215,6 +245,8 @@ Module.register("MMM-NextcloudCookbook", { const instructionList = document.createElement("ol"); this.flattenInstructions(recipe.recipeInstructions || []).forEach((step, index) => { const item = document.createElement("li"); + item.dataset.nccProgressKind = "step"; + item.dataset.nccProgressIndex = index; const completed = progress.completedSteps?.includes(index); if (completed) item.classList.add("ncc-completed"); const label = document.createElement("label"); diff --git a/README.md b/README.md index f70002f..19c22f7 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ link; it should look like `https://cloud.example.test/s/SHARE_TOKEN`. config: { shareUrl: "https://cloud.example.test/s/SHARE_TOKEN", controlUrl: "http://mirror.example.test:8080/MMM-NextcloudCookbook/control", - layout: "stacked" + layout: "stacked", + theme: "light" } } ``` @@ -38,6 +39,9 @@ the controller and recipe overlay also offer a toggle, and the chosen theme is saved locally. After exiting recipe mode, use **Resume last recipe** in the controller to restore its portions and completion marks. +Ingredients and instructions can be marked complete from either the phone +controller or a touchscreen on the mirror; both views remain synchronized. + When portions change, the display shows both the selected yield and the recipe's original yield. Ingredients without a leading quantity are marked in yellow because they cannot be scaled; unsupported or imprecise numeric scaling diff --git a/control/app.js b/control/app.js index d493e4c..62b21d3 100644 --- a/control/app.js +++ b/control/app.js @@ -106,7 +106,7 @@ function renderActiveRecipe() { activeRecipeName.textContent = recipe.name || "Untitled recipe"; const original = recipe.originalRecipeYield; activeRecipeYield.textContent = original && original !== recipe.recipeYield - ? `Yield: ${recipe.recipeYield} (original: ${original})` + ? `Yield: ${recipe.recipeYield} (originally ${original})` : `Yield: ${recipe.recipeYield || "Not specified"}`; renderChecklist(activeIngredients, recipe.recipeIngredient || [], progress.completedIngredients, "ingredient", recipe.recipeIngredientStatus || []); @@ -162,7 +162,6 @@ async function updateProgress(kind, index, completed) { body: JSON.stringify({ kind, index, completed }) }); applyState(state); - render(); } catch (error) { status.textContent = error.message; render(); diff --git a/control/styles.css b/control/styles.css index f54987c..6acabd4 100644 --- a/control/styles.css +++ b/control/styles.css @@ -122,8 +122,15 @@ button { } .checklist li { padding-left: .2rem; } -.checklist label { display: flex; gap: .6rem; align-items: flex-start; } -.checklist input { margin-top: .25rem; min-width: 1.1rem; min-height: 1.1rem; } +.checklist label { display: flex; gap: .6rem; align-items: flex-start; margin: 0; font-weight: normal; } +.checklist input { + width: 1.2rem; + min-width: 1.2rem; + height: 1.2rem; + min-height: 1.2rem; + margin: .18rem 0 0; + padding: 0; +} .checklist .completed { color: #777; text-decoration: line-through; } .checklist .quantity-warning { color: #876500; } .checklist .quantity-error { color: #a2342b; } @@ -135,3 +142,12 @@ button { -webkit-box-orient: vertical; -webkit-line-clamp: 2; } + +@media (max-width: 560px) { + main { padding: 18px 14px 44px; } + header { align-items: flex-start; flex-direction: column; } + .header-actions { width: 100%; justify-content: stretch; } + .header-actions button { flex: 1 1 auto; } + .active-recipe { margin: 1rem 0; padding: .85rem; } + .checklist { gap: .55rem; padding-left: 1.2rem; } +}