From eea0814948d76c3e15dbd7754b04b2d839e8e286 Mon Sep 17 00:00:00 2001 From: sam detweiler Date: Tue, 24 Mar 2026 21:51:07 -0700 Subject: [PATCH 1/3] add support for multiple instances --- MMM-RandomPhoto.css | 22 ++++++++-------- MMM-RandomPhoto.js | 49 ++++++++++++++++++++---------------- README.md | 12 +++++++++ node_helper.js | 61 ++++++++++++++++++++++++--------------------- 4 files changed, 84 insertions(+), 60 deletions(-) diff --git a/MMM-RandomPhoto.css b/MMM-RandomPhoto.css index b53ec0e..be66c28 100644 --- a/MMM-RandomPhoto.css +++ b/MMM-RandomPhoto.css @@ -1,8 +1,8 @@ :root { - --randomphoto-blur-value: 0px; + --_randomPhoto-blur-value: 0px; } -#randomPhoto img { +#_randomPhoto img { opacity: 0; position: absolute;; top: 0; @@ -12,39 +12,39 @@ object-fit: cover; } -#randomPhoto img.grayscale { +#_randomPhoto img.grayscale { filter: grayscale(100%); } -#randomPhoto img.blur { +#_randomPhoto img.blur { filter: blur(var(--randomphoto-blur-value)); } -#randomPhotoIcon { +#_randomPhotoIcon { position: absolute; } -#randomPhotoIcon.rpitop { +#_randomPhotoIcon.rpitop { top: 5px; } -#randomPhotoIcon.rpibottom { +#_randomPhotoIcon.rpibottom { bottom: 5px; } -#randomPhotoIcon.rpiright { +#_randomPhotoIcon.rpiright { right: 10px; } -#randomPhotoIcon.rpileft { +#_randomPhotoIcon.rpileft { left: 10px; } -#randomPhotoIcon i { +#_randomPhotoIcon i { opacity: 1; } -#randomPhotoIcon i.rpihidden { +#_randomPhotoIcon i.rpihidden { opacity: 0; } diff --git a/MMM-RandomPhoto.js b/MMM-RandomPhoto.js index a403906..0e53c0b 100644 --- a/MMM-RandomPhoto.js +++ b/MMM-RandomPhoto.js @@ -1,5 +1,5 @@ /* global Module */ - + /* MagicMirror² * Module: MMM-RandomPhoto * @@ -13,7 +13,8 @@ Module.register("MMM-RandomPhoto",{ opacity: 0.3, animationSpeed: 500, updateInterval: 60, - imageRepository: "picsum", // Select the image repository source. One of "picsum" (default / fallback), "localdirectory" or "nextcloud" (currently broken because of CORS bug in nextcloud) + imageRepository: "picsum", // Select the image repository source. One of "picsum" (default / fallback), "localdirectory" or "nextcloud" (currently broken because of CORS bug in nextcloud) + config : '-', repositoryConfig: { // if imageRepository = "picsum" -> "path", "username" and "password" are ignored and can be left empty // if imageRepository = "nextcloud" @@ -39,7 +40,10 @@ Module.register("MMM-RandomPhoto",{ showStatusIcon: true, statusIconMode: "show", // one of: "show" (default / fallback) or "fade" statusIconPosition: "top_right", // one of: "top_right" (default / fallback), "top_left", "bottom_right" or "bottom_left" - }, + }, + imgID1: "", + imgID2: "", + statusIconID:"", start: function() { this.updateTimer = null; @@ -48,8 +52,10 @@ Module.register("MMM-RandomPhoto",{ this.running = false; this.nextcloud = false; - this.localdirectory = false; - + this.localdirectory = false; + this.imgID1 = this.config.id + "randomPhoto-placeholder1" + this.imgID2 = this.config.id + "randomPhoto-placeholder2" + this.statusIconID = this.config.id +"randomPhotoStatusIcon" this.config.imageRepository = this.config.imageRepository.toLowerCase(); if (this.config.imageRepository === "nextcloud") { this.nextcloud = true; @@ -73,7 +79,7 @@ Module.register("MMM-RandomPhoto",{ fetchImageList: function() { if (typeof this.config.repositoryConfig.path !== "undefined" && this.config.repositoryConfig.path !== null) { - this.sendSocketNotification('FETCH_IMAGE_LIST'); + this.sendSocketNotification('FETCH_IMAGE_LIST',{ id: this.config.id }); } else { Log.error("[" + this.name + "] Trying to use 'nextcloud' or 'localdirectory' but did not specify any 'config.repositoryConfig.path'."); } @@ -107,7 +113,7 @@ Module.register("MMM-RandomPhoto",{ if (self.localdirectory || self.nextcloud) { if (self.imageList && self.imageList.length > 0) { - url = "/" + this.name + "/images/" + this.returnImageFromList(mode); + url = "/" + this.name + "/images/"+this.config.id+'/' + this.returnImageFromList(mode); jQuery.ajax({ method: "GET", @@ -155,16 +161,16 @@ Module.register("MMM-RandomPhoto",{ var self = this; var img = $('').attr('src', url); img.on('load', function() { - $('#randomPhoto-placeholder1').attr('src', url).animate({ + $('#'+self.imgID1).attr('src', url).animate({ opacity: self.config.opacity }, self.config.animationSpeed, function() { - $(this).attr('id', 'randomPhoto-placeholder2'); + $(this).attr('id', self.imgID2); }); - $('#randomPhoto-placeholder2').animate({ + $('#'+self.imgID2).animate({ opacity: 0 }, self.config.animationSpeed, function() { - $(this).attr('id', 'randomPhoto-placeholder1'); + $(this).attr('id', self.imgID1); }); }); }, @@ -200,7 +206,7 @@ Module.register("MMM-RandomPhoto",{ loadIcon: function(navigate="none") { var self = this; - const statusIcon = document.getElementById("randomPhotoStatusIcon"); + const statusIcon = document.getElementById(self.statusIconID); let currentIndex = -1; let iconloadInProgress = false; @@ -294,12 +300,12 @@ Module.register("MMM-RandomPhoto",{ getDom: function() { var wrapper = document.createElement("div"); - wrapper.id = "randomPhoto"; + wrapper.id = this.config.id+"randomPhoto"; var img1 = document.createElement("img"); - img1.id = "randomPhoto-placeholder1"; + img1.id = this.imgID1; var img2 = document.createElement("img"); - img2.id = "randomPhoto-placeholder2"; + img2.id = this.imgID2; // Only apply grayscale / blur css classes if we are NOT using picsum, as picsum is doing it via URL parameters if (this.nextcloud || this.localdirectory) { @@ -310,8 +316,8 @@ Module.register("MMM-RandomPhoto",{ if (this.config.blur) { img1.classList.add("blur"); img2.classList.add("blur"); - img1.style.setProperty("--randomphoto-blur-value", this.config.blurAmount + "px"); - img2.style.setProperty("--randomphoto-blur-value", this.config.blurAmount + "px"); + img1.style.setProperty("--"+this.config.id+"randomphoto-blur-value", this.config.blurAmount + "px"); + img2.style.setProperty("--"+this.config.id+"randomphoto-blur-value", this.config.blurAmount + "px"); } } @@ -324,12 +330,12 @@ Module.register("MMM-RandomPhoto",{ this.config.statusIconPosition = 'top_right'; } var statusIconObject = document.createElement("span"); - statusIconObject.id = "randomPhotoIcon"; + statusIconObject.id = this.config.id+"blurandomPhotoIcon"; statusIconObject.classList.add("dimmed"); this.config.statusIconPosition.split("_").forEach(function(extractedName) { statusIconObject.classList.add("rpi" + extractedName); }); - statusIconObject.innerHTML = ''; + statusIconObject.innerHTML = ''; wrapper.appendChild(statusIconObject); } return wrapper; @@ -359,6 +365,7 @@ Module.register("MMM-RandomPhoto",{ } } } + if (notification === "RANDOMPHOTO_NEXT") { // Don't call the pause or resume functions here, so we can actually work with both states ("paused" and "active"), so independent of what "this.running" is set to clearTimeout(this.updateTimer); @@ -395,8 +402,8 @@ Module.register("MMM-RandomPhoto",{ socketNotificationReceived: function(notification, payload) { //Log.log("["+ this.name + "] received a '" + notification + "' with payload: " + payload); //console.dir(payload); - if (notification === "IMAGE_LIST") { - this.imageList = payload; + if (notification === "IMAGE_LIST" && payload.id === this.config.id) { + this.imageList = payload.data; // After we now received the image list, go ahead and display them (only when not starting as hidden) if(!this.config.startHidden) { this.resumeImageLoading(true); diff --git a/README.md b/README.md index 3a13c11..17a4f8f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ The entry in `config.js` can include the following options: | Option | Description |-----------------------|------------ | `imageRepository` | *Optional* - The image source.

**Type:** `string`
**Allowed:** `picsum`, `nextcloud`, `localdirectory`
**Default:** `picsum` +| `id` | *Optional* - the unique id of this instance, defaults to '_'
see the section on ****multiple instances*** below on how to use this property | `repositoryConfig` | *Optional* - The configuration block for the selected image repository. See below.

**Type:** `Object` | `random` | *Optional* - Should the images be shown at random? Has **NO** effect when `imageRepository` is set to `picsum`, as it is forced there.

**Type:** `boolean`
**Default:** `true` | `width` | *Optional* - The width of the image in px. Only used when `imageRepository` is set to `picsum`

**Type:** `int`
**Default:** `1920` @@ -134,7 +135,18 @@ Thinking about implementing the following things: - possibility to show the EXIF comment from each image on screen (target selectable) - ... +## Using multiple instances +This module uses content ids to do the transitions between images. These were hard coded before, preventing multiple instances from working. +now you can use an ID in the config for each instance and the content ids will include that value. +not you need to create updated css values that include the id for each instance + +to do that follow these steps +1. copy all the MMM-RandomPhoto.css lines to custom.css +2. mass change the default id value '_' to whatever value you set in the id for an instance +3. repeat steps 1 and 2 for each additional instance + +for example: if the config id value is 'foo', then the info in the css file needs to change from #_random to #foorandom ## Dependencies - [jQuery](https://www.npmjs.com/package/jquery) (installed via `npm install`) diff --git a/node_helper.js b/node_helper.js index bc9c32d..d5ede04 100644 --- a/node_helper.js +++ b/node_helper.js @@ -6,16 +6,16 @@ const Log = require("logger"); const NodeHelper = require("node_helper"); module.exports = NodeHelper.create({ - + config: {}, + imageList: {}, start: function() { var self = this; this.nextcloud = false; this.localdirectory = false; - this.imageList = []; - this.expressApp.get("/" + this.name + "/images/:randomImageName", async function(request, response) { - var imageBase64Encoded = await self.fetchEncodedImage(decodeURIComponent(request.params.randomImageName)); + this.expressApp.get("/" + this.name + "/images/:id/:randomImageName", async function(request, response) { + var imageBase64Encoded = await self.fetchEncodedImage(decodeURIComponent(request.params.randomImageName, request.params.id)); response.send(imageBase64Encoded); }); }, @@ -24,24 +24,28 @@ module.exports = NodeHelper.create({ socketNotificationReceived: function(notification, payload) { //console.log("["+ this.name + "] received a '" + notification + "' with payload: " + payload); if (notification === "SET_CONFIG") { - this.config = payload; - if (this.config.imageRepository === "nextcloud") { + this.imageList[payload.id]=[] + this.config[payload.id] = payload; + if (this.config[payload.id].imageRepository === "nextcloud") { + this.nextcloud = true; - } else if (this.config.imageRepository === "localdirectory") { + } else if (this.config[payload.id].imageRepository === "localdirectory") { this.localdirectory = true; } } if (notification === "FETCH_IMAGE_LIST") { - if (this.config.imageRepository === "nextcloud") { - this.fetchNextcloudImageList(); + if (this.imageList[payload.id] === undefined) + this.imageList[payload.id] + if (this.config[payload.id].imageRepository === "nextcloud") { + this.fetchNextcloudImageList(this.config[payload.id]); } - if (this.config.imageRepository === "localdirectory") { - this.fetchLocalImageList(); + if (this.config[payload.id].imageRepository === "localdirectory") { + this.fetchLocalImageList(this.config[payload.id]); } } }, - fetchLocalImageDirectory: function(path) { + fetchLocalImageDirectory: function(path,config) { var self = this; // Validate path @@ -50,7 +54,7 @@ module.exports = NodeHelper.create({ return false; } - const excludePattern = self.config.repositoryConfig.exclude?.map(pattern => new RegExp(pattern)); + const excludePattern = config.repositoryConfig.exclude?.map(pattern => new RegExp(pattern)); var fileList = fs.readdirSync(path, { withFileTypes: true }); if (fileList.length > 0) { @@ -59,38 +63,38 @@ module.exports = NodeHelper.create({ if (fileList[f].isFile()) { //TODO: add mime type check here - self.imageList.push(encodeURIComponent(path + "/" + fileList[f].name)); + self.imageList[config.id].push(encodeURIComponent(path + "/" + fileList[f].name)); } - if ((self.config.repositoryConfig.recursive === true) && fileList[f].isDirectory()) { - self.fetchLocalImageDirectory(path + "/" + fileList[f].name); + if ((config.repositoryConfig.recursive === true) && fileList[f].isDirectory()) { + self.fetchLocalImageDirectory(path + "/" + fileList[f].name,config); } } return; } }, - fetchLocalImageList: function() { + fetchLocalImageList: function(config) { var self = this; - var path = self.config.repositoryConfig.path; + var path = config.repositoryConfig.path; - self.imageList = []; - self.fetchLocalImageDirectory(path); + self.imageList[config.id] = []; + self.fetchLocalImageDirectory(path,config); - self.sendSocketNotification("IMAGE_LIST", self.imageList); + self.sendSocketNotification("IMAGE_LIST", { data: self.imageList[config.id], id: config.id }); return false; }, - fetchNextcloudImageList: function() { + fetchNextcloudImageList: function(config) { var self = this; var imageList = []; - var path = self.config.repositoryConfig.path; + var path = config.repositoryConfig.path; const urlParts = new URL(path); const requestOptions = { method: "PROPFIND", headers: { - "Authorization": "Basic " + new Buffer.from(this.config.repositoryConfig.username + ":" + this.config.repositoryConfig.password).toString("base64") + "Authorization": "Basic " + new Buffer.from(config.repositoryConfig.username + ":" + config.repositoryConfig.password).toString("base64") } }; https.get(path, requestOptions, function(response) { @@ -107,7 +111,7 @@ module.exports = NodeHelper.create({ imageList[index] = encodeURIComponent(item.replace("href>" + urlParts.pathname, "")); //console.log("[" + self.name + "] Found entry: " + imageList[index]); }); - self.sendSocketNotification("IMAGE_LIST", imageList); + self.sendSocketNotification("IMAGE_LIST", { data: imageList, id: config.id }); return; } else { console.log("[" + this.name + "] WARNING: did not get any images from nextcloud url"); @@ -122,8 +126,9 @@ module.exports = NodeHelper.create({ }, - fetchEncodedImage: async function(passedImageName) { + fetchEncodedImage: async function(passedImageName,id) { var self = this; + config=this.config[id] return new Promise(function(resolve, reject) { var fullImagePath = passedImageName; @@ -138,10 +143,10 @@ module.exports = NodeHelper.create({ const requestOptions = { method: "GET", headers: { - "Authorization": "Basic " + new Buffer.from(self.config.repositoryConfig.username + ":" + self.config.repositoryConfig.password).toString("base64") + "Authorization": "Basic " + new Buffer.from(self.config.repositoryConfig.username + ":" + config.repositoryConfig.password).toString("base64") } }; - https.get(self.config.repositoryConfig.path + fullImagePath, requestOptions, (response) => { + https.get(config.repositoryConfig.path + fullImagePath, requestOptions, (response) => { response.setEncoding('base64'); var fileEncoded = "data:" + response.headers["content-type"] + ";base64,"; response.on("data", (data) => { fileEncoded += data; }); From c5b2cfd7221cb841d892356326fd5f2b70ed333e Mon Sep 17 00:00:00 2001 From: sam detweiler Date: Wed, 25 Mar 2026 07:45:35 -0700 Subject: [PATCH 2/3] fix typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 17a4f8f..bb3cec0 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Thinking about implementing the following things: This module uses content ids to do the transitions between images. These were hard coded before, preventing multiple instances from working. now you can use an ID in the config for each instance and the content ids will include that value. -not you need to create updated css values that include the id for each instance +now you need to create updated css values that include the id for each instance to do that follow these steps 1. copy all the MMM-RandomPhoto.css lines to custom.css From bf30ffcff06c893a35d5990ed37e7451bf5c0efa Mon Sep 17 00:00:00 2001 From: sam detweiler Date: Thu, 26 Mar 2026 06:46:52 -0700 Subject: [PATCH 3/3] update readme for one change --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bb3cec0..41c8096 100644 --- a/README.md +++ b/README.md @@ -144,9 +144,16 @@ now you need to create updated css values that include the id for each instance to do that follow these steps 1. copy all the MMM-RandomPhoto.css lines to custom.css 2. mass change the default id value '_' to whatever value you set in the id for an instance -3. repeat steps 1 and 2 for each additional instance +3. replace the one _ in the :root section at the top +``` +:root { + --_randomPhoto-blur-value: 0px; +} +``` +4. repeat steps 1 and 2 for each additional instance -for example: if the config id value is 'foo', then the info in the css file needs to change from #_random to #foorandom +for example: if the config id value is 'foo', then the info in the css file needs to change from #_random to #foorandom
and + --_random... to --foorandom... ## Dependencies - [jQuery](https://www.npmjs.com/package/jquery) (installed via `npm install`)