feat: add recipe theme and resume controls

Fixes #4, #7
This commit is contained in:
2026-08-14 13:51:00 -05:00
parent 803d722836
commit 22daf703bb
7 changed files with 157 additions and 7 deletions
+72 -3
View File
@@ -7,13 +7,18 @@ const NextcloudPublicShare = require("./lib/nextcloud-public-share");
const { parseYield, scaleRecipe } = require("./lib/recipe-scaler");
const { normalizeProgress, updateProgress } = require("./lib/recipe-progress");
function normalizeTheme(theme) {
return theme === "light" ? "light" : "dark";
}
module.exports = NodeHelper.create({
start() {
this.config = { controlPath: "/MMM-NextcloudCookbook/control" };
this.config = { controlPath: "/MMM-NextcloudCookbook/control", theme: "dark" };
this.state = {
active: false, recipeId: null, recipe: null, portions: null,
progress: normalizeProgress(), error: null
recipeName: null, progress: normalizeProgress(), theme: "dark", error: null
};
this.hasPersistedTheme = false;
this.recipeCache = { expires: 0, recipes: [] };
this.stateFile = path.join(process.cwd(), "config", "MMM-NextcloudCookbook-state.json");
this.configureClient();
@@ -26,6 +31,10 @@ module.exports = NodeHelper.create({
this.updateProgressFromMirror(payload);
return;
}
if (notification === "SET_THEME") {
this.updateTheme(payload);
return;
}
if (notification !== "CONFIG") return;
this.config = { ...this.config, ...payload };
try {
@@ -35,6 +44,7 @@ module.exports = NodeHelper.create({
return;
}
this.stateReady.then(async () => {
if (!this.hasPersistedTheme) this.state.theme = normalizeTheme(this.config.theme);
this.sendSocketNotification("STATE", this.publicState());
await this.restoreRecipe();
}).catch((error) => this.setError(error));
@@ -97,6 +107,7 @@ module.exports = NodeHelper.create({
active: true,
recipeId,
recipe,
recipeName: recipe.name || "Untitled recipe",
portions: isResumingRecipe
? this.state.portions
: Math.max(1, Math.round(parseYield(recipe.recipeYield) || 1)),
@@ -110,6 +121,43 @@ module.exports = NodeHelper.create({
return next(error);
}
});
this.expressApp.post("/MMM-NextcloudCookbook/api/resume", async (_request, response, next) => {
try {
if (this.state.active) return response.json(this.publicState());
if (!this.state.recipeId) return response.status(409).json({ error: "No recent recipe to resume" });
const recipe = await this.getClient().getRecipe(this.state.recipeId);
this.state = {
...this.state,
active: true,
recipe,
recipeName: recipe.name || "Untitled recipe",
portions: Number.isInteger(this.state.portions) && this.state.portions > 0
? this.state.portions
: Math.max(1, Math.round(parseYield(recipe.recipeYield) || 1)),
progress: normalizeProgress(this.state.progress),
error: null
};
await this.saveState();
this.sendSocketNotification("STATE", this.publicState());
return response.json(this.publicState());
} catch (error) {
return next(error);
}
});
this.expressApp.post("/MMM-NextcloudCookbook/api/theme", async (request, response, next) => {
try {
const theme = request.body?.theme;
if (!["light", "dark"].includes(theme)) {
return response.status(400).json({ error: "theme must be light or dark" });
}
this.state.theme = theme;
await this.saveState();
this.sendSocketNotification("STATE", this.publicState());
return response.json(this.publicState());
} catch (error) {
return next(error);
}
});
this.expressApp.post("/MMM-NextcloudCookbook/api/portions", async (request, response, next) => {
try {
const delta = request.body?.delta;
@@ -174,13 +222,17 @@ module.exports = NodeHelper.create({
async loadState() {
try {
const persisted = JSON.parse(await fs.readFile(this.stateFile, "utf8"));
this.hasPersistedTheme = typeof persisted.theme === "string";
this.state.theme = normalizeTheme(persisted.theme);
if (!persisted.recipeId) return;
this.state = {
active: Boolean(persisted.active),
recipeId: persisted.recipeId,
recipe: null,
recipeName: typeof persisted.recipeName === "string" ? persisted.recipeName : null,
portions: Number.isInteger(persisted.portions) ? persisted.portions : null,
progress: normalizeProgress(persisted.progress),
theme: this.state.theme,
error: null
};
} catch (error) {
@@ -205,8 +257,10 @@ module.exports = NodeHelper.create({
await fs.writeFile(temporary, `${JSON.stringify({
active: this.state.active,
recipeId: this.state.recipeId,
recipeName: this.state.recipeName,
portions: this.state.portions,
progress: this.state.progress
progress: this.state.progress,
theme: this.state.theme
})}\n`, {
mode: 0o600
});
@@ -227,6 +281,9 @@ module.exports = NodeHelper.create({
: null,
portions: this.state.portions,
progress: this.state.progress,
theme: this.state.theme,
canResume: !this.state.active && Boolean(this.state.recipeId),
lastRecipeName: !this.state.active ? this.state.recipeName : null,
error: this.state.error
};
},
@@ -241,5 +298,17 @@ module.exports = NodeHelper.create({
console.error(`[MMM-NextcloudCookbook] ${error.message}`);
this.sendSocketNotification("STATE", this.publicState());
}
},
async updateTheme(theme) {
try {
if (!["light", "dark"].includes(theme)) throw new Error("theme must be light or dark");
this.state.theme = theme;
await this.saveState();
this.sendSocketNotification("STATE", this.publicState());
} catch (error) {
console.error(`[MMM-NextcloudCookbook] ${error.message}`);
this.sendSocketNotification("STATE", this.publicState());
}
}
});