Several improvements
This commit is contained in:
+24
-16
@@ -15,7 +15,7 @@
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 50px 58px;
|
padding: 42px 48px;
|
||||||
background: #11110f;
|
background: #11110f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,26 +23,34 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 18px;
|
right: 18px;
|
||||||
top: 18px;
|
top: 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
color: #27231b;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
line-height: 0;
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.15;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MMM-NextcloudCookbook .ncc-qr img {
|
.MMM-NextcloudCookbook .ncc-qr img {
|
||||||
width: 82px;
|
width: 164px;
|
||||||
height: 82px;
|
height: 164px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MMM-NextcloudCookbook .ncc-active .ncc-qr img {
|
.MMM-NextcloudCookbook .ncc-active .ncc-qr img {
|
||||||
width: 68px;
|
width: 144px;
|
||||||
height: 68px;
|
height: 144px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MMM-NextcloudCookbook .ncc-recipe h1 {
|
.MMM-NextcloudCookbook .ncc-recipe h1 {
|
||||||
margin: 0 100px 24px 0;
|
margin: 0 220px 26px 0;
|
||||||
color: #fff6d8;
|
color: #fff6d8;
|
||||||
font-size: 68px;
|
font-size: 82px;
|
||||||
line-height: 1.08;
|
line-height: 1.08;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +58,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px 22px;
|
gap: 10px 22px;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
margin: 0 0 32px;
|
margin: 0 0 36px;
|
||||||
font-size: 28px;
|
font-size: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MMM-NextcloudCookbook .ncc-meta dt {
|
.MMM-NextcloudCookbook .ncc-meta dt {
|
||||||
@@ -65,26 +73,26 @@
|
|||||||
|
|
||||||
.MMM-NextcloudCookbook .ncc-columns {
|
.MMM-NextcloudCookbook .ncc-columns {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(280px, 0.8fr) minmax(430px, 1.4fr);
|
grid-template-columns: minmax(260px, 0.8fr) minmax(400px, 1.4fr);
|
||||||
gap: 52px;
|
gap: 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MMM-NextcloudCookbook .ncc-columns h2 {
|
.MMM-NextcloudCookbook .ncc-columns h2 {
|
||||||
margin: 0 0 18px;
|
margin: 0 0 18px;
|
||||||
color: #d6b96b;
|
color: #d6b96b;
|
||||||
font-size: 42px;
|
font-size: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MMM-NextcloudCookbook .ncc-columns ul,
|
.MMM-NextcloudCookbook .ncc-columns ul,
|
||||||
.MMM-NextcloudCookbook .ncc-columns ol {
|
.MMM-NextcloudCookbook .ncc-columns ol {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 1.25em;
|
padding-left: 1.25em;
|
||||||
font-size: 34px;
|
font-size: 50px;
|
||||||
line-height: 1.34;
|
line-height: 1.36;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MMM-NextcloudCookbook .ncc-columns li {
|
.MMM-NextcloudCookbook .ncc-columns li {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MMM-NextcloudCookbook .ncc-error {
|
.MMM-NextcloudCookbook .ncc-error {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/* global Module, MM */
|
/* global Module, MM, NCCFormatDuration */
|
||||||
|
|
||||||
Module.register("MMM-NextcloudCookbook", {
|
Module.register("MMM-NextcloudCookbook", {
|
||||||
defaults: {
|
defaults: {
|
||||||
controlPath: "/MMM-NextcloudCookbook/control",
|
controlPath: "/MMM-NextcloudCookbook/control",
|
||||||
|
controlUrl: "",
|
||||||
animationSpeed: 400,
|
animationSpeed: 400,
|
||||||
shareUrl: "",
|
shareUrl: "",
|
||||||
sharePassword: ""
|
sharePassword: ""
|
||||||
@@ -18,6 +19,10 @@ Module.register("MMM-NextcloudCookbook", {
|
|||||||
return ["MMM-NextcloudCookbook.css"];
|
return ["MMM-NextcloudCookbook.css"];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getScripts() {
|
||||||
|
return ["lib/format-duration.js"];
|
||||||
|
},
|
||||||
|
|
||||||
socketNotificationReceived(notification, payload) {
|
socketNotificationReceived(notification, payload) {
|
||||||
if (notification !== "STATE") return;
|
if (notification !== "STATE") return;
|
||||||
this.state = payload;
|
this.state = payload;
|
||||||
@@ -47,12 +52,14 @@ Module.register("MMM-NextcloudCookbook", {
|
|||||||
|
|
||||||
const qr = document.createElement("a");
|
const qr = document.createElement("a");
|
||||||
qr.className = "ncc-qr";
|
qr.className = "ncc-qr";
|
||||||
qr.href = this.config.controlPath;
|
qr.href = this.config.controlUrl || this.config.controlPath;
|
||||||
qr.setAttribute("aria-label", "Open recipe controller");
|
qr.setAttribute("aria-label", "Open recipe controller");
|
||||||
|
const qrLabel = document.createElement("span");
|
||||||
|
qrLabel.textContent = "Recipe mode control:";
|
||||||
const qrImage = document.createElement("img");
|
const qrImage = document.createElement("img");
|
||||||
qrImage.src = "/MMM-NextcloudCookbook/qr.svg";
|
qrImage.src = "/MMM-NextcloudCookbook/qr.svg";
|
||||||
qrImage.alt = "Recipe controller QR code";
|
qrImage.alt = "Recipe controller QR code";
|
||||||
qr.appendChild(qrImage);
|
qr.append(qrLabel, qrImage);
|
||||||
root.appendChild(qr);
|
root.appendChild(qr);
|
||||||
|
|
||||||
if (!this.state.active) return root;
|
if (!this.state.active) return root;
|
||||||
@@ -88,9 +95,9 @@ Module.register("MMM-NextcloudCookbook", {
|
|||||||
|
|
||||||
const metadata = [
|
const metadata = [
|
||||||
["Yield", recipe.recipeYield],
|
["Yield", recipe.recipeYield],
|
||||||
["Prep", recipe.prepTime],
|
["Prep", NCCFormatDuration(recipe.prepTime)],
|
||||||
["Cook", recipe.cookTime],
|
["Cook", NCCFormatDuration(recipe.cookTime)],
|
||||||
["Total", recipe.totalTime]
|
["Total", NCCFormatDuration(recipe.totalTime)]
|
||||||
].filter(([, value]) => value);
|
].filter(([, value]) => value);
|
||||||
if (metadata.length) {
|
if (metadata.length) {
|
||||||
const list = document.createElement("dl");
|
const list = document.createElement("dl");
|
||||||
|
|||||||
@@ -16,11 +16,25 @@ link; it should look like `https://cloud.example.test/s/SHARE_TOKEN`.
|
|||||||
position: "fullscreen_above",
|
position: "fullscreen_above",
|
||||||
config: {
|
config: {
|
||||||
shareUrl: "https://cloud.example.test/s/SHARE_TOKEN",
|
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`:
|
For a password-protected public share, add `sharePassword`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -30,16 +44,18 @@ config: {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
MagicMirror sends module configuration to its browser clients. A public-share
|
Do not treat values in MagicMirror's `config.js` as private: someone who can
|
||||||
URL is normally appropriate there because the token is already a scoped share
|
load the MagicMirror display may be able to inspect them. A read-only public
|
||||||
credential, but do not put a Nextcloud account password or app password in this
|
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.
|
configuration.
|
||||||
|
|
||||||
For deployments that keep even the share token out of browser-visible
|
To keep even the share token out of the configuration delivered with the
|
||||||
configuration, set `SECRET_NEXTCLOUD_COOKBOOK_SHARE_URL` and, if needed,
|
MagicMirror display, set `SECRET_NEXTCLOUD_COOKBOOK_SHARE_URL` and, if needed,
|
||||||
`SECRET_NEXTCLOUD_COOKBOOK_SHARE_PASSWORD` in the MagicMirror process
|
`SECRET_NEXTCLOUD_COOKBOOK_SHARE_PASSWORD` in the MagicMirror process
|
||||||
environment and omit the corresponding config values. Environment values take
|
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:
|
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) => {
|
this.expressApp.get("/MMM-NextcloudCookbook/qr.svg", async (request, response, next) => {
|
||||||
try {
|
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 }));
|
response.type("image/svg+xml").send(await QRCode.toString(url, { type: "svg", margin: 1 }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"description": "Display recipes from a Nextcloud Cookbook share on MagicMirror",
|
"description": "Display recipes from a Nextcloud Cookbook share on MagicMirror",
|
||||||
"main": "node_helper.js",
|
"main": "node_helper.js",
|
||||||
"scripts": {
|
"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"
|
"test": "npm run check && node --test"
|
||||||
},
|
},
|
||||||
"engines": {
|
"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