fix: preserve recipe progress view state

Refs #1, #5
This commit is contained in:
2026-08-14 21:02:21 -05:00
parent 22daf703bb
commit a8fbcf4104
4 changed files with 59 additions and 8 deletions
+35 -3
View File
@@ -28,9 +28,17 @@ Module.register("MMM-NextcloudCookbook", {
socketNotificationReceived(notification, payload) { socketNotificationReceived(notification, payload) {
if (notification !== "STATE") return; if (notification !== "STATE") return;
const previous = this.state;
this.state = payload; this.state = payload;
this.setRecipeMode(Boolean(payload.active)); 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) { 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() { getDom() {
const root = document.createElement("section"); const root = document.createElement("section");
root.className = `ncc-root ${this.state.active ? "ncc-active" : "ncc-idle"} ncc-theme-${this.state.theme || this.config.theme}`; 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"; title.textContent = recipe.name || "Untitled recipe";
header.appendChild(title); header.appendChild(title);
const yieldValue = recipe.originalRecipeYield && recipe.originalRecipeYield !== recipe.recipeYield
? `${recipe.recipeYield} (originally ${recipe.originalRecipeYield})`
: recipe.recipeYield;
const metadata = [ const metadata = [
["Yield", recipe.recipeYield], ["Yield", yieldValue],
["Original yield", recipe.originalRecipeYield !== recipe.recipeYield ? recipe.originalRecipeYield : null],
["Prep", NCCFormatDuration(recipe.prepTime)], ["Prep", NCCFormatDuration(recipe.prepTime)],
["Cook", NCCFormatDuration(recipe.cookTime)], ["Cook", NCCFormatDuration(recipe.cookTime)],
["Total", NCCFormatDuration(recipe.totalTime)] ["Total", NCCFormatDuration(recipe.totalTime)]
@@ -178,6 +206,8 @@ Module.register("MMM-NextcloudCookbook", {
const ingredientList = document.createElement("ul"); const ingredientList = document.createElement("ul");
(recipe.recipeIngredient || []).forEach((ingredient, index) => { (recipe.recipeIngredient || []).forEach((ingredient, index) => {
const item = document.createElement("li"); const item = document.createElement("li");
item.dataset.nccProgressKind = "ingredient";
item.dataset.nccProgressIndex = index;
const status = recipe.recipeIngredientStatus?.[index]; const status = recipe.recipeIngredientStatus?.[index];
if (status === "unquantified") { if (status === "unquantified") {
item.classList.add("ncc-quantity-warning"); item.classList.add("ncc-quantity-warning");
@@ -215,6 +245,8 @@ Module.register("MMM-NextcloudCookbook", {
const instructionList = document.createElement("ol"); const instructionList = document.createElement("ol");
this.flattenInstructions(recipe.recipeInstructions || []).forEach((step, index) => { this.flattenInstructions(recipe.recipeInstructions || []).forEach((step, index) => {
const item = document.createElement("li"); const item = document.createElement("li");
item.dataset.nccProgressKind = "step";
item.dataset.nccProgressIndex = index;
const completed = progress.completedSteps?.includes(index); const completed = progress.completedSteps?.includes(index);
if (completed) item.classList.add("ncc-completed"); if (completed) item.classList.add("ncc-completed");
const label = document.createElement("label"); const label = document.createElement("label");
+5 -1
View File
@@ -20,7 +20,8 @@ link; it should look like `https://cloud.example.test/s/SHARE_TOKEN`.
config: { config: {
shareUrl: "https://cloud.example.test/s/SHARE_TOKEN", shareUrl: "https://cloud.example.test/s/SHARE_TOKEN",
controlUrl: "http://mirror.example.test:8080/MMM-NextcloudCookbook/control", 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 saved locally. After exiting recipe mode, use **Resume last recipe** in the
controller to restore its portions and completion marks. 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 When portions change, the display shows both the selected yield and the
recipe's original yield. Ingredients without a leading quantity are marked in recipe's original yield. Ingredients without a leading quantity are marked in
yellow because they cannot be scaled; unsupported or imprecise numeric scaling yellow because they cannot be scaled; unsupported or imprecise numeric scaling
+1 -2
View File
@@ -106,7 +106,7 @@ function renderActiveRecipe() {
activeRecipeName.textContent = recipe.name || "Untitled recipe"; activeRecipeName.textContent = recipe.name || "Untitled recipe";
const original = recipe.originalRecipeYield; const original = recipe.originalRecipeYield;
activeRecipeYield.textContent = original && original !== recipe.recipeYield activeRecipeYield.textContent = original && original !== recipe.recipeYield
? `Yield: ${recipe.recipeYield} (original: ${original})` ? `Yield: ${recipe.recipeYield} (originally ${original})`
: `Yield: ${recipe.recipeYield || "Not specified"}`; : `Yield: ${recipe.recipeYield || "Not specified"}`;
renderChecklist(activeIngredients, recipe.recipeIngredient || [], progress.completedIngredients, "ingredient", renderChecklist(activeIngredients, recipe.recipeIngredient || [], progress.completedIngredients, "ingredient",
recipe.recipeIngredientStatus || []); recipe.recipeIngredientStatus || []);
@@ -162,7 +162,6 @@ async function updateProgress(kind, index, completed) {
body: JSON.stringify({ kind, index, completed }) body: JSON.stringify({ kind, index, completed })
}); });
applyState(state); applyState(state);
render();
} catch (error) { } catch (error) {
status.textContent = error.message; status.textContent = error.message;
render(); render();
+18 -2
View File
@@ -122,8 +122,15 @@ button {
} }
.checklist li { padding-left: .2rem; } .checklist li { padding-left: .2rem; }
.checklist label { display: flex; gap: .6rem; align-items: flex-start; } .checklist label { display: flex; gap: .6rem; align-items: flex-start; margin: 0; font-weight: normal; }
.checklist input { margin-top: .25rem; min-width: 1.1rem; min-height: 1.1rem; } .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 .completed { color: #777; text-decoration: line-through; }
.checklist .quantity-warning { color: #876500; } .checklist .quantity-warning { color: #876500; }
.checklist .quantity-error { color: #a2342b; } .checklist .quantity-error { color: #a2342b; }
@@ -135,3 +142,12 @@ button {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 2; -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; }
}