From a713af2feef167a613fcfc5f9d89f8fe9a7948e2 Mon Sep 17 00:00:00 2001 From: Aaron Axvig Date: Mon, 3 Aug 2026 16:04:17 -0500 Subject: [PATCH] Several improvements --- MMM-NextcloudCookbook.css | 40 +++++++++++++++++++++--------------- MMM-NextcloudCookbook.js | 19 +++++++++++------ README.md | 30 ++++++++++++++++++++------- lib/format-duration.js | 24 ++++++++++++++++++++++ node_helper.js | 3 ++- package.json | 2 +- test/format-duration.test.js | 15 ++++++++++++++ 7 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 lib/format-duration.js create mode 100644 test/format-duration.test.js diff --git a/MMM-NextcloudCookbook.css b/MMM-NextcloudCookbook.css index 0c86d3c..db92d24 100644 --- a/MMM-NextcloudCookbook.css +++ b/MMM-NextcloudCookbook.css @@ -15,7 +15,7 @@ inset: 0; z-index: 20; overflow: hidden; - padding: 50px 58px; + padding: 42px 48px; background: #11110f; } @@ -23,26 +23,34 @@ position: absolute; right: 18px; top: 18px; + display: flex; + flex-direction: column; + gap: 8px; + align-items: center; padding: 6px; border-radius: 6px; + color: #27231b; background: #fff; - line-height: 0; + font-size: 20px; + font-weight: 700; + line-height: 1.15; + text-decoration: none; } .MMM-NextcloudCookbook .ncc-qr img { - width: 82px; - height: 82px; + width: 164px; + height: 164px; } .MMM-NextcloudCookbook .ncc-active .ncc-qr img { - width: 68px; - height: 68px; + width: 144px; + height: 144px; } .MMM-NextcloudCookbook .ncc-recipe h1 { - margin: 0 100px 24px 0; + margin: 0 220px 26px 0; color: #fff6d8; - font-size: 68px; + font-size: 82px; line-height: 1.08; } @@ -50,8 +58,8 @@ display: flex; gap: 10px 22px; align-items: baseline; - margin: 0 0 32px; - font-size: 28px; + margin: 0 0 36px; + font-size: 36px; } .MMM-NextcloudCookbook .ncc-meta dt { @@ -65,26 +73,26 @@ .MMM-NextcloudCookbook .ncc-columns { display: grid; - grid-template-columns: minmax(280px, 0.8fr) minmax(430px, 1.4fr); - gap: 52px; + grid-template-columns: minmax(260px, 0.8fr) minmax(400px, 1.4fr); + gap: 42px; } .MMM-NextcloudCookbook .ncc-columns h2 { margin: 0 0 18px; color: #d6b96b; - font-size: 42px; + font-size: 56px; } .MMM-NextcloudCookbook .ncc-columns ul, .MMM-NextcloudCookbook .ncc-columns ol { margin: 0; padding-left: 1.25em; - font-size: 34px; - line-height: 1.34; + font-size: 50px; + line-height: 1.36; } .MMM-NextcloudCookbook .ncc-columns li { - margin-bottom: 14px; + margin-bottom: 18px; } .MMM-NextcloudCookbook .ncc-error { diff --git a/MMM-NextcloudCookbook.js b/MMM-NextcloudCookbook.js index 01476d9..cf00a2d 100644 --- a/MMM-NextcloudCookbook.js +++ b/MMM-NextcloudCookbook.js @@ -1,8 +1,9 @@ -/* global Module, MM */ +/* global Module, MM, NCCFormatDuration */ Module.register("MMM-NextcloudCookbook", { defaults: { controlPath: "/MMM-NextcloudCookbook/control", + controlUrl: "", animationSpeed: 400, shareUrl: "", sharePassword: "" @@ -18,6 +19,10 @@ Module.register("MMM-NextcloudCookbook", { return ["MMM-NextcloudCookbook.css"]; }, + getScripts() { + return ["lib/format-duration.js"]; + }, + socketNotificationReceived(notification, payload) { if (notification !== "STATE") return; this.state = payload; @@ -47,12 +52,14 @@ Module.register("MMM-NextcloudCookbook", { const qr = document.createElement("a"); qr.className = "ncc-qr"; - qr.href = this.config.controlPath; + qr.href = this.config.controlUrl || this.config.controlPath; qr.setAttribute("aria-label", "Open recipe controller"); + const qrLabel = document.createElement("span"); + qrLabel.textContent = "Recipe mode control:"; const qrImage = document.createElement("img"); qrImage.src = "/MMM-NextcloudCookbook/qr.svg"; qrImage.alt = "Recipe controller QR code"; - qr.appendChild(qrImage); + qr.append(qrLabel, qrImage); root.appendChild(qr); if (!this.state.active) return root; @@ -88,9 +95,9 @@ Module.register("MMM-NextcloudCookbook", { const metadata = [ ["Yield", recipe.recipeYield], - ["Prep", recipe.prepTime], - ["Cook", recipe.cookTime], - ["Total", recipe.totalTime] + ["Prep", NCCFormatDuration(recipe.prepTime)], + ["Cook", NCCFormatDuration(recipe.cookTime)], + ["Total", NCCFormatDuration(recipe.totalTime)] ].filter(([, value]) => value); if (metadata.length) { const list = document.createElement("dl"); diff --git a/README.md b/README.md index 1a1cba4..6464ded 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,25 @@ link; it should look like `https://cloud.example.test/s/SHARE_TOKEN`. position: "fullscreen_above", config: { shareUrl: "https://cloud.example.test/s/SHARE_TOKEN", - controlPath: "/MMM-NextcloudCookbook/control" + controlUrl: "http://mirror.example.test:8080/MMM-NextcloudCookbook/control" } } ``` +`controlUrl` is the complete address that should open when someone scans the QR +code. Use the MagicMirror host name or IP address and port that your phone can +reach. The module generates the QR image itself; it does not use an external QR +code service. + +`controlUrl` is optional. When omitted, the module attempts to construct it +from the address used to load MagicMirror. Set it explicitly when MagicMirror +runs as an app or service using an address such as `localhost`, or when phones +reach it through a different host name, port, or reverse proxy. + +When `controlUrl` is omitted, the module appends its default control path, +`/MMM-NextcloudCookbook/control`, to the detected MagicMirror address. Most +installations do not need to configure `controlPath` separately. + For a password-protected public share, add `sharePassword`: ```js @@ -30,16 +44,18 @@ config: { } ``` -MagicMirror sends module configuration to its browser clients. A public-share -URL is normally appropriate there because the token is already a scoped share -credential, but do not put a Nextcloud account password or app password in this +Do not treat values in MagicMirror's `config.js` as private: someone who can +load the MagicMirror display may be able to inspect them. A read-only public +share URL is normally appropriate because its token is already limited to that +share. Do not put a Nextcloud account password or app password in this configuration. -For deployments that keep even the share token out of browser-visible -configuration, set `SECRET_NEXTCLOUD_COOKBOOK_SHARE_URL` and, if needed, +To keep even the share token out of the configuration delivered with the +MagicMirror display, set `SECRET_NEXTCLOUD_COOKBOOK_SHARE_URL` and, if needed, `SECRET_NEXTCLOUD_COOKBOOK_SHARE_PASSWORD` in the MagicMirror process environment and omit the corresponding config values. Environment values take -precedence over module configuration and are read only by `node_helper.js`. +precedence over module configuration and are read only by `node_helper.js`. Read +more here: https://docs.magicmirror.builders/configuration/secrets.html Open the controller at: diff --git a/lib/format-duration.js b/lib/format-duration.js new file mode 100644 index 0000000..5511619 --- /dev/null +++ b/lib/format-duration.js @@ -0,0 +1,24 @@ +(function exposeDurationFormatter(root, factory) { + const formatter = factory(); + if (typeof module === "object" && module.exports) { + module.exports = formatter; + } else { + root.NCCFormatDuration = formatter; + } +}(globalThis, () => function formatDuration(value) { + if (typeof value !== "string") return value; + const match = value.match(/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/); + if (!match || !match.slice(1).some((part) => part !== undefined)) return value; + + const units = [ + [match[1], "day"], + [match[2], "hr"], + [match[3], "min"], + [match[4], "sec"] + ].filter(([amount]) => Number(amount) > 0); + + if (!units.length) return "0 min"; + return units.map(([amount, unit]) => ( + `${Number(amount)} ${unit}${unit === "day" && Number(amount) !== 1 ? "s" : ""}` + )).join(" "); +})); diff --git a/node_helper.js b/node_helper.js index 2573482..5b991e0 100644 --- a/node_helper.js +++ b/node_helper.js @@ -57,7 +57,8 @@ module.exports = NodeHelper.create({ ); this.expressApp.get("/MMM-NextcloudCookbook/qr.svg", async (request, response, next) => { try { - const url = `${request.protocol}://${request.get("host")}${this.config.controlPath}`; + const url = this.config.controlUrl + || `${request.protocol}://${request.get("host")}${this.config.controlPath}`; response.type("image/svg+xml").send(await QRCode.toString(url, { type: "svg", margin: 1 })); } catch (error) { next(error); diff --git a/package.json b/package.json index 3dc1159..40475b1 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "Display recipes from a Nextcloud Cookbook share on MagicMirror", "main": "node_helper.js", "scripts": { - "check": "node --check MMM-NextcloudCookbook.js && node --check node_helper.js && node --check lib/nextcloud-public-share.js && node --check lib/recipe-scaler.js && node --check control/app.js", + "check": "node --check MMM-NextcloudCookbook.js && node --check node_helper.js && node --check lib/format-duration.js && node --check lib/nextcloud-public-share.js && node --check lib/recipe-scaler.js && node --check control/app.js", "test": "npm run check && node --test" }, "engines": { diff --git a/test/format-duration.test.js b/test/format-duration.test.js new file mode 100644 index 0000000..df7b773 --- /dev/null +++ b/test/format-duration.test.js @@ -0,0 +1,15 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const formatDuration = require("../lib/format-duration"); + +test("formats Cookbook ISO 8601 durations for people", () => { + assert.equal(formatDuration("PT0H10M0S"), "10 min"); + assert.equal(formatDuration("PT1H30M0S"), "1 hr 30 min"); + assert.equal(formatDuration("P2DT1H"), "2 days 1 hr"); + assert.equal(formatDuration("PT0S"), "0 min"); +}); + +test("preserves duration values it does not recognize", () => { + assert.equal(formatDuration("about an hour"), "about an hour"); + assert.equal(formatDuration(null), null); +});