Several improvements
This commit is contained in:
+24
-16
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user