diff --git a/MMM-NextcloudCookbook.css b/MMM-NextcloudCookbook.css index 0671174..ccd7d7b 100644 --- a/MMM-NextcloudCookbook.css +++ b/MMM-NextcloudCookbook.css @@ -15,6 +15,15 @@ text-align: left; } +.MMM-NextcloudCookbook .ncc-active.ncc-theme-light { + color: #27231b; + background: #f8f6ef; +} + +.MMM-NextcloudCookbook .ncc-theme-light .ncc-recipe h1 { color: #302817; } +.MMM-NextcloudCookbook .ncc-theme-light .ncc-meta dt, +.MMM-NextcloudCookbook .ncc-theme-light .ncc-columns h2 { color: #765900; } + .MMM-NextcloudCookbook .ncc-qr { display: flex; flex-direction: column; @@ -37,6 +46,28 @@ top: 18px; } +.MMM-NextcloudCookbook .ncc-theme-toggle { + padding: 10px 14px; + border: 0; + border-radius: 6px; + color: #27231b; + background: #fff; + font: inherit; + font-weight: 700; + cursor: pointer; +} + +.MMM-NextcloudCookbook .ncc-active .ncc-theme-toggle { + position: absolute; + top: 190px; + right: 18px; +} + +.MMM-NextcloudCookbook .ncc-theme-light .ncc-theme-toggle { + color: #f8f6ef; + background: #302817; +} + .MMM-NextcloudCookbook .ncc-qr img { width: 164px; height: 164px; diff --git a/MMM-NextcloudCookbook.js b/MMM-NextcloudCookbook.js index c5ef371..a2b53ea 100644 --- a/MMM-NextcloudCookbook.js +++ b/MMM-NextcloudCookbook.js @@ -6,6 +6,7 @@ Module.register("MMM-NextcloudCookbook", { controlUrl: "", animationSpeed: 400, layout: "stacked", + theme: "dark", shareUrl: "", sharePassword: "" }, @@ -87,9 +88,7 @@ Module.register("MMM-NextcloudCookbook", { getDom() { const root = document.createElement("section"); - root.className = this.state.active - ? "ncc-root ncc-active" - : "ncc-root ncc-idle"; + root.className = `ncc-root ${this.state.active ? "ncc-active" : "ncc-idle"} ncc-theme-${this.state.theme || this.config.theme}`; const qr = document.createElement("a"); qr.className = "ncc-qr"; @@ -105,6 +104,14 @@ Module.register("MMM-NextcloudCookbook", { qr.append(qrLabel, qrImage); root.appendChild(qr); + const themeToggle = document.createElement("button"); + themeToggle.type = "button"; + themeToggle.className = "ncc-theme-toggle"; + const nextTheme = this.state.theme === "light" ? "dark" : "light"; + themeToggle.textContent = `Use ${nextTheme} mode`; + themeToggle.addEventListener("click", () => this.sendSocketNotification("SET_THEME", nextTheme)); + root.appendChild(themeToggle); + if (!this.state.active) return root; if (this.state.error) { diff --git a/README.md b/README.md index 389b577..f70002f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,11 @@ the best use of the available screen while keeping all recipe content visible. The recipe title, timing metadata, yield, and QR controller remain at fixed sizes. +`theme` defaults to `"dark"`. Set it to `"light"` for a light starting theme; +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. + 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 ed059c1..d493e4c 100644 --- a/control/app.js +++ b/control/app.js @@ -3,6 +3,8 @@ const status = document.querySelector("#status"); const list = document.querySelector("#recipes"); const search = document.querySelector("#search"); const exit = document.querySelector("#exit"); +const resume = document.querySelector("#resume"); +const themeToggle = document.querySelector("#theme-toggle"); const portionControls = document.querySelector("#portions"); const portionCount = document.querySelector("#portion-count"); const fewerPortions = document.querySelector("#fewer-portions"); @@ -19,6 +21,9 @@ let displayedRecipe = null; let portions = null; let progress = { completedIngredients: [], completedSteps: [] }; let recipeModeActive = false; +let canResume = false; +let lastRecipeName = null; +let theme = "dark"; async function request(path, options) { const response = await fetch(`${apiBase}${path}`, { @@ -56,6 +61,9 @@ function render() { portionControls.hidden = !recipeModeActive; portionCount.textContent = portions ?? ""; fewerPortions.disabled = portions <= 1; + resume.hidden = !canResume; + resume.textContent = lastRecipeName ? `Resume ${lastRecipeName}` : "Resume last recipe"; + themeToggle.textContent = theme === "light" ? "Use dark mode" : "Use light mode"; renderActiveRecipe(); } @@ -112,6 +120,9 @@ function applyState(state) { displayedRecipe = recipeModeActive ? state.recipe : null; portions = state.portions; progress = state.progress || { completedIngredients: [], completedSteps: [] }; + canResume = Boolean(state.canResume); + lastRecipeName = state.lastRecipeName || null; + theme = state.theme === "light" ? "light" : "dark"; } async function activate(recipe) { @@ -161,6 +172,27 @@ async function updateProgress(kind, index, completed) { fewerPortions.addEventListener("click", () => adjustPortions(-1)); morePortions.addEventListener("click", () => adjustPortions(1)); +resume.addEventListener("click", async () => { + try { + applyState(await request("/resume", { method: "POST", body: "{}" })); + status.textContent = `${activeRecipeLabel} is back on the mirror.`; + render(); + } catch (error) { + status.textContent = error.message; + } +}); + +themeToggle.addEventListener("click", async () => { + try { + applyState(await request("/theme", { + method: "POST", body: JSON.stringify({ theme: theme === "light" ? "dark" : "light" }) + })); + render(); + } catch (error) { + status.textContent = error.message; + } +}); + exit.addEventListener("click", async () => { status.textContent = "Leaving recipe mode…"; try { diff --git a/control/index.html b/control/index.html index ae3cd59..dd6e52a 100644 --- a/control/index.html +++ b/control/index.html @@ -13,7 +13,11 @@

Kitchen Mirror

Recipes

- +
+ + + +

Loading recipes…