Several improvements

This commit is contained in:
2026-08-03 16:04:17 -05:00
parent f94b08a4e6
commit a713af2fee
7 changed files with 102 additions and 31 deletions
+24 -16
View File
@@ -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 {
+13 -6
View File
@@ -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");
+23 -7
View File
@@ -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:
+24
View File
@@ -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(" ");
}));
+2 -1
View File
@@ -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);
+1 -1
View File
@@ -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": {
+15
View File
@@ -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);
});