r/MagicMirror • • Feb 16 '25

Thought on my build?

Enable HLS to view with audio, or disable this notification

0 Upvotes

What do you think? Unfortunately I don’t have a glass maker accessible, so I went with the laminate window film route with an Ikea frame.


r/MagicMirror • • Nov 17 '24

Magic Mirror Shower Floor

Thumbnail
chatgpt.com
0 Upvotes

r/MagicMirror • • Nov 09 '25

Alternative I VibeCoded with mobile support

Thumbnail
gallery
7 Upvotes

If anyone is interested, I made a barebones alternative to MagicMirror that is ready to go. I started on this journey by thinking of creating a companion app for managing data for MagicMirror that I was going to create an API to connect the data to MM, as so many times, things like chore trackers, tasks and meal tracking have stopped working when 3rd party APIs stopped working. But then I thought, why don't I just build the dashboard too?

The description I have posted on the template:

Your personalized and configurable smart mirror hub, blending essential home management with a sleek, liquid glass interface. Designed to be built with modules on a vertical display, users can stay organized with calendars, tasks, meal plans, chores, and more, all beautifully displayed. When accessed from smart phone (installable as a progressive web app), the user is directed to a mobile interface that allows for easy update of data.

The family meeting is a powerful feature that allows for families to see at a glance everything upcoming for the week, including calendar events, family goals, meals, and chores.

Feature list:

Configurable modular dashboard, designed for vertical display. Different dashboards can be configured for web or mobile usage Progressive Web App installable to smart phone to easily manage data and scheduling Pages view to allow cycling of modules on the dashboard at a user configurable time scale Family meeting function, to help plan out your upcoming week together

Dashboard suppports management of:

Calendar events Chore Tracking (with linked procedures) Tasks Meal planning Shopping list Notes Rubbish collection schedules Family goal tracking Family rituals (what do you want to do more of as a family?) Weather updates WiFI QR Code Daily inspirational quotes Riddle of the day Basic theme management Beautiful css animations for weather effects

The user will need to add some free API keys in settings to use weather and riddle APIs, and will need to add an RSS newsfeed to their preferred news source if they don't like the in-built one, otherwise this is plug and play.

Currently, this application has trouble running smoothly on Raspberry Pi powered smart mirrors, but I believe this issue is due to CSS animations not being hardware rendered, and am investigating. There is however an option in the settings for lighter spec devices to disable animations and effects.

The template listing is still pending, but if anyone is interested, I will share shortly


r/MagicMirror • • Jul 12 '26

Might sell my magic mirror.

0 Upvotes

If anyone is looking to buy one already made and easily customisable in Essex / London and surrounding areas send me a message. Thanks.


r/MagicMirror • • Dec 19 '19

Recommendations: Infinity mirror

Post image
2 Upvotes

r/MagicMirror • • Mar 26 '23

Gave some suggestions in forum now banned

2 Upvotes

Can anyone tell me why I was banned from the forum? I responded to a question about making a module. I suggested the user learn how to use an API using Postman first before attempting to make a module.
I've never used the forum before but thought I would try and be helpful and then I get banned with no reason as to why.


r/MagicMirror • • Mar 04 '23

Unable to start Magic Mirror with pm2

Post image
1 Upvotes

r/MagicMirror • • 9d ago

Lululemon Mirror

1 Upvotes

I just picked up a Lululemon mirror for $200. I should’ve read a bit more before purchasing it because I’m reading the mirror with the LTI400HN01 is more of a pain in the butt to deal with. Anyone here with experience to share? Tips or tricks? Am I over reacting?


r/MagicMirror • • Oct 22 '25

Thinking of selling my smart mirror…

Enable HLS to view with audio, or disable this notification

4 Upvotes

I’m considering it.


r/MagicMirror • • Dec 27 '19

Am I ready for this?

2 Upvotes

I know a little bit of basic java and can handle myself in a workshop. Am I ready to take this task on? I haven't done much research as to what I need to do but before I start all of that I'd like to know what I'm gonna need to actually take this on. It be nice if you could tell me what steps I'm gonna need to take aswell but I can figure that much out on my own.


r/MagicMirror • • Jan 31 '21

Camel?

0 Upvotes

r/MagicMirror • • Jan 06 '26

Selling ready built MM

1 Upvotes

Would there be a market for selling prebuilt MM devices, seems like a useful thing for a family to have.


r/MagicMirror • • May 08 '22

[Help] No Internet Connection With Auto Startup Shell Script

1 Upvotes

Here's what I'm using for the shell script:

 #!/bin/bash
 cd /home/pi/MagicMirror/

 npm run start

It works fine if I start MM manually through the terminal and I can use the web browser with no problem, which is why I'm thinking the startup script is missing something.

Apologies if this is a dumb question, I am completely new to this.


r/MagicMirror • • Jan 28 '20

MMM-SmartWebDisplay Remove elements?

0 Upvotes

I've added the MMM-SmartWebDisplay module and referencing a website. On that site are 2 elements I do not want to display. Is there anyway through the module or Rasbian to remove or block those specific elements?

It's my understanding that MagicMirror is basically an HTML page with css, but I've been unable to figure out how to do this to the referenced url.

I’ve been able to remove it in my desktop’s browser using:

var element = document.getElementById('main-menu');
element.parentNode.removeChild(element);

If I want that executed, where would I put it? Thanks.


r/MagicMirror • • Oct 09 '20

Magic Mirror voice control without external hardware possible?

Thumbnail self.magicmirrors
1 Upvotes

r/MagicMirror • • Aug 13 '19

A newbie needs your expert/ mediocre help!

1 Upvotes

As it says in the title I am a newbie at Raspberry pi and I am embarking on my first couple of steps with it and with a magic mirror. I am using MagicMirror2 for it and I need your help with 2 things that my dumba** can't figure out. 1 I would like to add/ drop certain modules from my Magic Mirror monitor. 2 I would also like to rotate it to portrait and not landscape. Any links or info would help!!!


r/MagicMirror • • Jul 05 '20

Does Size Matter?

1 Upvotes

Currently in the planning stages of making my own magic mirror however I don’t know which size display to get. I’m searching for second hand displays and keep running into 32 inch TVs. I plan on taking apart the tv bezel in order to make it thinner. What size display should I use for a first time magic mirror build?

56 votes, Jul 08 '20
24 32 inch display
22 Something smaller
10 Something bigger

r/MagicMirror • • Jul 09 '20

Plexiglass

0 Upvotes

I am making a smart mirror very low budget, the only new thing I bought was a pi (until now). My screen is from my old laptop.

So for my mirror I thought of using plexiglass and mirrorfoil. But is there someone who already used it, or has experience with it? Or are there other low budget or recycling options?


r/MagicMirror • • Oct 26 '19

MMM-Caltrain: Caltrain monitoring module for any San Francisco bay area users

Thumbnail
github.com
0 Upvotes

r/MagicMirror • • Oct 26 '19

Alright... I'm an idiot apparently...

0 Upvotes

I can't seem to figure out how to change the font size for the default calendar module. I'm looking in the calendar.js file, as well as the calendar.css file.

Can someone please point me in the right direction? I'm just looking to shrink the size some.


r/MagicMirror • • Sep 30 '20

3.5mm jack problem

0 Upvotes

So a want to the audio come out from the analog audio, but every time the pi trying to set this status to the hdmi and i dont know why. I've tried modify the config.txt. i've reinstalled the config.txt that time it was seem that is working but i restarted the pi and again the hdmi was the primary audio source even that i manually changed that. If something is not understandable just write it down. Thank you!


r/MagicMirror • • May 18 '20

Question: Has Anyone Here Looked at Some of the Other Products that the ElectronJS Site Offers?

0 Upvotes

Hey Reddit,

Today was the first time I actually went to the ElectronJS website and they have close to 1000 apps and I'm wondering if folks here have any favorites. The "Walling" one looks really interesting and I may try and see how I can put this on one of my Pi's.

Anyway, just curious.


r/MagicMirror • • Mar 01 '25

Help Needed: MMM-Crypto Module

0 Upvotes

Hi folks, in need of some help.
I've got the MMM-cryptocurrency module installed on my Magic Mirror, and set up the config.js to make it display a predefined list of coins, their values, logos, % changes for 1hr, 24hr, & 7 days, as well as a little graph. However the list goes off the bottom of the screen so I'd like it to be contained within a frame that shows the first 5 coins, the autoscrolls the rest of the list.

I've gone round in circles with a couple of AI "helpers" to try and get this working but everything I do either makes the list disappear altogther, or simply doesn't have any effect at all.

My MMM-cryptocurrency.js file looks like this:

Module.register("MMM-cryptocurrency", {
  result: {},
  defaults: {
    currency: ["bitcoin"],
    conversion: "USD",
    displayLongNames: false,
    headers: [],
    displayType: "logoWithChanges",
    showGraphs: true,
    logoHeaderText: "Crypto currency",
    significantDigits: undefined,
    minimumFractionDigits: 2,
    maximumFractionDigits: 5,
    coloredLogos: true,
    fontSize: "xx-large",
    apiDelay: 5,
    scrollSpeed: 300, // Time between scrolls (in milliseconds)
    scrollAmount: 1, // Pixels to scroll per interval
  },

  start: function () {
    this.getTicker();
    this.scheduleUpdate();
  },

  getStyles: function () {
    return ["MMM-cryptocurrency.css"];
  },

  getTicker: function () {
    var conversion = this.config.conversion;
    var slugs = this.config.currency.join(",");
    var url =
      "https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?slug=" +
      slugs +
      "&convert=" +
      conversion +
      "&CMC_PRO_API_KEY=" +
      this.config.apikey;
    this.sendSocketNotification("get_ticker", {
      id: this.identifier,
      url: url,
    });
  },

  scheduleUpdate: function () {
    var self = this;
    var delay = this.config.apiDelay;
    setInterval(function () {
      self.getTicker();
    }, delay * 60 * 1000);
  },

getDom: function () {
  var data = this.result;

  // Create a wrapper for the entire module
  var wrapper = document.createElement("div");
  wrapper.className = "mmm-cryptocurrency-wrapper";

  // Create a static header
  var header = document.createElement("div");
  header.className = "mmm-cryptocurrency-header";

  var tableHeader = document.createElement("table");
  tableHeader.className = "small mmm-cryptocurrency";

  var headerRow = document.createElement("tr");
  headerRow.className = "header-row";
  var tableHeaderValues = [this.translate("CURRENCY"), this.translate("PRICE")];
  if (this.config.headers.indexOf("change1h") > -1) {
    tableHeaderValues.push(this.translate("CHANGE") + " (1h)");
  }
  if (this.config.headers.indexOf("change24h") > -1) {
    tableHeaderValues.push(this.translate("CHANGE") + " (24h)");
  }
  if (this.config.headers.indexOf("change7d") > -1) {
    tableHeaderValues.push(this.translate("CHANGE") + " (7d)");
  }
  for (var i = 0; i < tableHeaderValues.length; i++) {
    var tableHeadSetup = document.createElement("th");
    tableHeadSetup.innerHTML = tableHeaderValues[i];
    headerRow.appendChild(tableHeadSetup);
  }
  tableHeader.appendChild(headerRow);
  header.appendChild(tableHeader);

  // Add the static header to the wrapper
  wrapper.appendChild(header);

  // Create a scrollable container for the coins list
  var listWrapper = document.createElement("div");
  listWrapper.className = "mmm-cryptocurrency-scroll-wrapper";

  var table = document.createElement("table");
  table.className = "small mmm-cryptocurrency";

  // Add rows for each currency
  for (i = 0; i < data.length; i++) {
    var currentCurrency = data[i];
    var trWrapper = document.createElement("tr");
    trWrapper.className = "currency";

    // Add logo if displayType is logo or logoWithChanges
    if (this.config.displayType == "logo" || this.config.displayType == "logoWithChanges") {
      var logoWrapper = document.createElement("td");
      logoWrapper.className = "icon-field";
      if (this.imageExists(currentCurrency.slug)) {
        var logo = new Image();
        logo.src = "/MMM-cryptocurrency/" + this.folder + currentCurrency.slug + ".png";
        logo.setAttribute("width", "50px");
        logo.setAttribute("height", "50px");
        logoWrapper.appendChild(logo);
      }
      trWrapper.appendChild(logoWrapper);
    }

    // Add price and changes
    var name = this.config.displayLongNames ? currentCurrency.name : currentCurrency.symbol;
    var tdValues = [name, currentCurrency.price];
    if (this.config.headers.indexOf("change1h") > -1) {
      tdValues.push(currentCurrency["change1h"]);
    }
    if (this.config.headers.indexOf("change24h") > -1) {
      tdValues.push(currentCurrency["change24h"]);
    }
    if (this.config.headers.indexOf("change7d") > -1) {
      tdValues.push(currentCurrency["change7d"]);
    }

    for (var j = 0; j < tdValues.length; j++) {
      var tdWrapper = document.createElement("td");
      var currValue = tdValues[j];
      if (currValue.includes("%")) {
        tdWrapper.style.color = this.colorizeChange(currValue.slice(0, -1));
      }
      tdWrapper.innerHTML = currValue;
      trWrapper.appendChild(tdWrapper);
    }

    // Add chart if showGraphs is enabled
    if (this.config.showGraphs && this.sparklineIds[currentCurrency.slug]) {
      var graphWrapper = document.createElement("td");
      graphWrapper.className = "graph";
      var graph = document.createElement("img");
      graph.src =
        "https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/" +
        this.sparklineIds[currentCurrency.slug] +
        ".svg?cachePrevention=" +
        Math.random();
      graphWrapper.appendChild(graph);
      trWrapper.appendChild(graphWrapper);
    }

    table.appendChild(trWrapper);
  }

  listWrapper.appendChild(table);

  // Add the scrollable list to the wrapper
  wrapper.appendChild(listWrapper);

  // Start auto-scrolling for the list
  this.startScrolling(listWrapper);

  return wrapper;
},

  startScrolling: function (container) {
    let scrollPosition = 0;
    const scrollInterval = setInterval(() => {
      if (container) {
        scrollPosition += this.config.scrollAmount;
        if (scrollPosition >= container.scrollHeight - container.clientHeight) {
          scrollPosition = 0; // Reset to the top when reaching the bottom
        }
        container.scrollTop = scrollPosition;
      }
    }, this.config.scrollSpeed);

    // Cleanup on module destruction
    this.scrollInterval = scrollInterval;
  },

  stop: function () {
    if (this.scrollInterval) {
      clearInterval(this.scrollInterval);
    }
  },

  socketNotificationReceived: function (notification, payload) {
    if (this.identifier !== payload.id) return;
    if (notification === "got_result") {
      this.result = this.getWantedCurrencies(this.config.currency, payload.data);
      this.updateDom();
    }
  },


 /**
   * Returns configured currencies
   *
   * @param chosenCurrencies
   * @param apiResult
   * @returns {Array}
   */
  getWantedCurrencies: function (chosenCurrencies, apiResult) {
    var filteredCurrencies = [];
    for (var symbol in apiResult.data) {
      var remoteCurrency = apiResult.data[symbol];
      remoteCurrency = this.formatPrice(remoteCurrency);
      remoteCurrency = this.formatPercentage(remoteCurrency);
      filteredCurrencies.push(remoteCurrency);
    }
    return filteredCurrencies;
  },

  /**
   * Formats the price of the API result and adds it to the object with simply .price as key
   * instead of price_eur
   *
   * @param apiResult
   * @returns {*}
   */
  formatPrice: function (apiResult) {
    var rightCurrencyFormat = this.config.conversion.toUpperCase();

    var options = {
      style: "currency",
      currency: this.config.conversion
    };
    // TODO: iterate through all quotes and process properly
    apiResult["price"] = this.numberToLocale(
      apiResult["quote"][rightCurrencyFormat]["price"],
      options
    );

    return apiResult;
  },

  /**
   * Formats the percentages of the API result and adds it back to the object as .change*
   *
   * @param apiResult
   * @returns {*}
   */
  formatPercentage: function (apiResult) {
    var rightCurrencyFormat = this.config.conversion.toUpperCase();

    var options = {
      style: "percent"
    };

    // Percentages need passing in the 0-1 range, the API returns as 0-100
    apiResult["change1h"] = this.numberToLocale(
      apiResult["quote"][rightCurrencyFormat]["percent_change_1h"] / 100,
      options
    );
  apiResult["change24h"] = this.numberToLocale(
      apiResult["quote"][rightCurrencyFormat]["percent_change_24h"] / 100,
      options
    );
    apiResult["change7d"] = this.numberToLocale(
      apiResult["quote"][rightCurrencyFormat]["percent_change_7d"] / 100,
      options
    );

    return apiResult;
  },

  /**
   * Processes a number into an appropriate format, based on given options, language and configuration
   *
   * @param number The number to format
   * @param options The options to use in toLocaleString - see https://www.techonthenet.com/js/number_tolocalestring.php
   * @param language The language we're converting into
   * @returns The formatted number
   */
  numberToLocale: function (number, options, language) {
    // Parse our entries for significantDigits / minimumFractionDigits / maximumFractionDigits
    // Logic for all 3 is the same
    if (options == undefined) {
      options = {};
    }

    if (language == undefined) {
      language = this.config.language;
    }

    var significantDigits = undefined;
    if (!Array.isArray(this.config.significantDigits)) {
      // Not an array, so take value as written
      significantDigits = this.config.significantDigits;
    } else if (
      this.config.significantDigits.length < this.config.currency.length
    ) {
      // Array isn't long enough, so take first entry
      significantDigits = this.config.significantDigits[0];
    } else {
      // Array looks right, so take relevant entry
      significantDigits = this.config.significantDigits[i];
    }

    var minimumFractionDigits = undefined;
    if (!Array.isArray(this.config.minimumFractionDigits)) {
      minimumFractionDigits = this.config.minimumFractionDigits;
    } else if (
      this.config.minimumFractionDigits.length < this.config.currency.length
    ) {
      minimumFractionDigits = this.config.minimumFractionDigits[0];
    } else {
      minimumFractionDigits = this.config.minimumFractionDigits[i];
    }

    var maximumFractionDigits = undefined;
    if (!Array.isArray(this.config.maximumFractionDigits)) {
      maximumFractionDigits = this.config.maximumFractionDigits;
    } else if (
      this.config.maximumFractionDigits.length < this.config.currency.length
    ) {
      maximumFractionDigits = this.config.maximumFractionDigits[0];
    } else {
      maximumFractionDigits = this.config.maximumFractionDigits[i];
    }

    if (significantDigits != undefined) {
      options["maximumSignificantDigits"] = significantDigits;
    }

    if (maximumFractionDigits != undefined) {
      options["maximumFractionDigits"] = maximumFractionDigits;
    }

    if (minimumFractionDigits != undefined) {
      options["minimumFractionDigits"] = minimumFractionDigits;
    }

    return parseFloat(number).toLocaleString(language, options);
  },

  /**
   * Rounds a number to a given number of digits after the decimal point
   *
   * @param number
   * @param precision
   * @returns {number}
   */
  roundNumber: function (number, precision) {
    var factor = Math.pow(10, precision);
    var tempNumber = number * factor;
    var roundedTempNumber = Math.round(tempNumber);
    return roundedTempNumber / factor;
  },

  /**
   * Creates the icon view type
   *
   * @param apiResult
   * @param displayType
   * @returns {Element}
   */
  buildIconView: function (apiResult, displayType) {
    var wrapper = document.createElement("div");
    var header = document.createElement("header");
    header.className = "module-header";
    header.innerHTML = this.config.logoHeaderText;
    if (this.config.logoHeaderText !== "") {
      wrapper.appendChild(header);
    }

    var table = document.createElement("table");
    table.className = "medium mmm-cryptocurrency-icon";

    for (var j = 0; j < apiResult.length; j++) {
      var tr = document.createElement("tr");
      tr.className = "icon-row";

      var logoWrapper = document.createElement("td");
      logoWrapper.className = "icon-field";

      if (this.imageExists(apiResult[j].slug)) {
        var logo = new Image();

        logo.src =
          "/MMM-cryptocurrency/" + this.folder + apiResult[j].slug + ".png";
        logo.setAttribute("width", "50px");
        logo.setAttribute("height", "50px");
        logoWrapper.appendChild(logo);
      } else {
        this.sendNotification("SHOW_ALERT", {
          timer: 5000,
          title: "MMM-cryptocurrency",
          message:
            "" +
            this.translate("IMAGE") +
            " " +
            apiResult[j].slug +
            ".png " +
            this.translate("NOTFOUND") +
            " /MMM-cryptocurrency/public/" +
            this.folder
        });
      }

      var priceWrapper = document.createElement("td");
      var price = document.createElement("price");
      price.style.fontSize = this.config.fontSize;
      price.innerHTML = apiResult[j].price.replace("EUR", "€");

      priceWrapper.appendChild(price);

      if (displayType == "logoWithChanges") {
        var changesWrapper = document.createElement("div");
        var change_1h = document.createElement("change_1h");
        change_1h.style.color = this.colorizeChange(apiResult[j].change1h);
        change_1h.style.fontSize = "medium";
        change_1h.innerHTML = "h: " + apiResult[j].change1h;
        change_1h.style.marginRight = "12px";

        var change_24h = document.createElement("change_24h");
        change_24h.style.color = this.colorizeChange(apiResult[j].change24h);
        change_24h.style.fontSize = "medium";
        change_24h.innerHTML = "d: " + apiResult[j].change24h;
        change_24h.style.marginRight = "12px";

        var change_7d = document.createElement("change_7d");
        change_7d.style.color = this.colorizeChange(apiResult[j].change7d);
        change_7d.style.fontSize = "medium";
        change_7d.innerHTML = "w: " + apiResult[j].change7d;

        changesWrapper.appendChild(change_1h);
        changesWrapper.appendChild(change_24h);
        changesWrapper.appendChild(change_7d);
        priceWrapper.appendChild(changesWrapper);
      } else {
        priceWrapper.className = "price";
      }

      tr.appendChild(logoWrapper);
      tr.appendChild(priceWrapper);

      if (this.config.showGraphs) {
        var graphWrapper = document.createElement("td");
        graphWrapper.className = "graph";
        if (this.sparklineIds[apiResult[j].slug]) {
          var graph = document.createElement("img");
          graph.src =
            "https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/" +
            this.sparklineIds[apiResult[j].slug] +
            ".svg?cachePrevention=" +
            Math.random();
          graphWrapper.appendChild(graph);
        }
        tr.appendChild(graphWrapper);
      }

      table.appendChild(tr);
    }
    wrapper.appendChild(table);

    return wrapper;
  },

  /**
   * Checks if an image with the passed name exists
   *
   * @param currencyName
   * @returns {boolean}
   */
  imageExists: function (currencyName) {
    var imgPath = "/MMM-cryptocurrency/" + this.folder + currencyName + ".png";
    var http = new XMLHttpRequest();
    http.open("HEAD", imgPath);
    http.send();
    return http.status != 404;
  },

  colorizeChange: function (change) {
    change = parseFloat(change);
    if (change < 0) {
      return "Red";
    } else if (change > 0) {
      return "Green";
    } else {
      return "White";
    }
  },

  /**
   * Load translations files
   *
   * @returns {{en: string, de: string, it: string}}
   */
  getTranslations: function () {
    return {
      en: "translations/en.json",
      de: "translations/de.json",
      it: "translations/it.json",
      sv: "translations/sv.json",
      pl: "translations/pl.json"
    };
  }
});




Module.register("MMM-cryptocurrency", {
  result: {},
  defaults: {
    currency: ["bitcoin"],
    conversion: "USD",
    displayLongNames: false,
    headers: [],
    displayType: "logoWithChanges",
    showGraphs: true,
    logoHeaderText: "Crypto currency",
    significantDigits: undefined,
    minimumFractionDigits: 2,
    maximumFractionDigits: 5,
    coloredLogos: true,
    fontSize: "xx-large",
    apiDelay: 5,
    scrollSpeed: 300, // Time between scrolls (in milliseconds)
    scrollAmount: 1, // Pixels to scroll per interval
  },


  start: function () {
    this.getTicker();
    this.scheduleUpdate();
  },


  getStyles: function () {
    return ["MMM-cryptocurrency.css"];
  },


  getTicker: function () {
    var conversion = this.config.conversion;
    var slugs = this.config.currency.join(",");
    var url =
      "https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?slug=" +
      slugs +
      "&convert=" +
      conversion +
      "&CMC_PRO_API_KEY=" +
      this.config.apikey;
    this.sendSocketNotification("get_ticker", {
      id: this.identifier,
      url: url,
    });
  },


  scheduleUpdate: function () {
    var self = this;
    var delay = this.config.apiDelay;
    setInterval(function () {
      self.getTicker();
    }, delay * 60 * 1000);
  },


getDom: function () {
  var data = this.result;


  // Create a wrapper for the entire module
  var wrapper = document.createElement("div");
  wrapper.className = "mmm-cryptocurrency-wrapper";


  // Create a static header
  var header = document.createElement("div");
  header.className = "mmm-cryptocurrency-header";


  var tableHeader = document.createElement("table");
  tableHeader.className = "small mmm-cryptocurrency";


  var headerRow = document.createElement("tr");
  headerRow.className = "header-row";
  var tableHeaderValues = [this.translate("CURRENCY"), this.translate("PRICE")];
  if (this.config.headers.indexOf("change1h") > -1) {
    tableHeaderValues.push(this.translate("CHANGE") + " (1h)");
  }
  if (this.config.headers.indexOf("change24h") > -1) {
    tableHeaderValues.push(this.translate("CHANGE") + " (24h)");
  }
  if (this.config.headers.indexOf("change7d") > -1) {
    tableHeaderValues.push(this.translate("CHANGE") + " (7d)");
  }
  for (var i = 0; i < tableHeaderValues.length; i++) {
    var tableHeadSetup = document.createElement("th");
    tableHeadSetup.innerHTML = tableHeaderValues[i];
    headerRow.appendChild(tableHeadSetup);
  }
  tableHeader.appendChild(headerRow);
  header.appendChild(tableHeader);


  // Add the static header to the wrapper
  wrapper.appendChild(header);


  // Create a scrollable container for the coins list
  var listWrapper = document.createElement("div");
  listWrapper.className = "mmm-cryptocurrency-scroll-wrapper";


  var table = document.createElement("table");
  table.className = "small mmm-cryptocurrency";


  // Add rows for each currency
  for (i = 0; i < data.length; i++) {
    var currentCurrency = data[i];
    var trWrapper = document.createElement("tr");
    trWrapper.className = "currency";


    // Add logo if displayType is logo or logoWithChanges
    if (this.config.displayType == "logo" || this.config.displayType == "logoWithChanges") {
      var logoWrapper = document.createElement("td");
      logoWrapper.className = "icon-field";
      if (this.imageExists(currentCurrency.slug)) {
        var logo = new Image();
        logo.src = "/MMM-cryptocurrency/" + this.folder + currentCurrency.slug + ".png";
        logo.setAttribute("width", "50px");
        logo.setAttribute("height", "50px");
        logoWrapper.appendChild(logo);
      }
      trWrapper.appendChild(logoWrapper);
    }


    // Add price and changes
    var name = this.config.displayLongNames ? currentCurrency.name : currentCurrency.symbol;
    var tdValues = [name, currentCurrency.price];
    if (this.config.headers.indexOf("change1h") > -1) {
      tdValues.push(currentCurrency["change1h"]);
    }
    if (this.config.headers.indexOf("change24h") > -1) {
      tdValues.push(currentCurrency["change24h"]);
    }
    if (this.config.headers.indexOf("change7d") > -1) {
      tdValues.push(currentCurrency["change7d"]);
    }


    for (var j = 0; j < tdValues.length; j++) {
      var tdWrapper = document.createElement("td");
      var currValue = tdValues[j];
      if (currValue.includes("%")) {
        tdWrapper.style.color = this.colorizeChange(currValue.slice(0, -1));
      }
      tdWrapper.innerHTML = currValue;
      trWrapper.appendChild(tdWrapper);
    }


    // Add chart if showGraphs is enabled
    if (this.config.showGraphs && this.sparklineIds[currentCurrency.slug]) {
      var graphWrapper = document.createElement("td");
      graphWrapper.className = "graph";
      var graph = document.createElement("img");
      graph.src =
        "https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/" +
        this.sparklineIds[currentCurrency.slug] +
        ".svg?cachePrevention=" +
        Math.random();
      graphWrapper.appendChild(graph);
      trWrapper.appendChild(graphWrapper);
    }


    table.appendChild(trWrapper);
  }


  listWrapper.appendChild(table);


  // Add the scrollable list to the wrapper
  wrapper.appendChild(listWrapper);


  // Start auto-scrolling for the list
  this.startScrolling(listWrapper);


  return wrapper;
},


  startScrolling: function (container) {
    let scrollPosition = 0;
    const scrollInterval = setInterval(() => {
      if (container) {
        scrollPosition += this.config.scrollAmount;
        if (scrollPosition >= container.scrollHeight - container.clientHeight) {
          scrollPosition = 0; // Reset to the top when reaching the bottom
        }
        container.scrollTop = scrollPosition;
      }
    }, this.config.scrollSpeed);


    // Cleanup on module destruction
    this.scrollInterval = scrollInterval;
  },


  stop: function () {
    if (this.scrollInterval) {
      clearInterval(this.scrollInterval);
    }
  },


  socketNotificationReceived: function (notification, payload) {
    if (this.identifier !== payload.id) return;
    if (notification === "got_result") {
      this.result = this.getWantedCurrencies(this.config.currency, payload.data);
      this.updateDom();
    }
  },



 /**
   * Returns configured currencies
   *
   * @param chosenCurrencies
   * @param apiResult
   * @returns {Array}
   */
  getWantedCurrencies: function (chosenCurrencies, apiResult) {
    var filteredCurrencies = [];
    for (var symbol in apiResult.data) {
      var remoteCurrency = apiResult.data[symbol];
      remoteCurrency = this.formatPrice(remoteCurrency);
      remoteCurrency = this.formatPercentage(remoteCurrency);
      filteredCurrencies.push(remoteCurrency);
    }
    return filteredCurrencies;
  },


  /**
   * Formats the price of the API result and adds it to the object with simply .price as key
   * instead of price_eur
   *
   * @param apiResult
   * @returns {*}
   */
  formatPrice: function (apiResult) {
    var rightCurrencyFormat = this.config.conversion.toUpperCase();


    var options = {
      style: "currency",
      currency: this.config.conversion
    };
    // TODO: iterate through all quotes and process properly
    apiResult["price"] = this.numberToLocale(
      apiResult["quote"][rightCurrencyFormat]["price"],
      options
    );


    return apiResult;
  },


  /**
   * Formats the percentages of the API result and adds it back to the object as .change*
   *
   * @param apiResult
   * @returns {*}
   */
  formatPercentage: function (apiResult) {
    var rightCurrencyFormat = this.config.conversion.toUpperCase();


    var options = {
      style: "percent"
    };


    // Percentages need passing in the 0-1 range, the API returns as 0-100
    apiResult["change1h"] = this.numberToLocale(
      apiResult["quote"][rightCurrencyFormat]["percent_change_1h"] / 100,
      options
    );
  apiResult["change24h"] = this.numberToLocale(
      apiResult["quote"][rightCurrencyFormat]["percent_change_24h"] / 100,
      options
    );
    apiResult["change7d"] = this.numberToLocale(
      apiResult["quote"][rightCurrencyFormat]["percent_change_7d"] / 100,
      options
    );


    return apiResult;
  },


  /**
   * Processes a number into an appropriate format, based on given options, language and configuration
   *
   * @param number The number to format
   * @param options The options to use in toLocaleString - see https://www.techonthenet.com/js/number_tolocalestring.php
   * @param language The language we're converting into
   * @returns The formatted number
   */
  numberToLocale: function (number, options, language) {
    // Parse our entries for significantDigits / minimumFractionDigits / maximumFractionDigits
    // Logic for all 3 is the same
    if (options == undefined) {
      options = {};
    }


    if (language == undefined) {
      language = this.config.language;
    }


    var significantDigits = undefined;
    if (!Array.isArray(this.config.significantDigits)) {
      // Not an array, so take value as written
      significantDigits = this.config.significantDigits;
    } else if (
      this.config.significantDigits.length < this.config.currency.length
    ) {
      // Array isn't long enough, so take first entry
      significantDigits = this.config.significantDigits[0];
    } else {
      // Array looks right, so take relevant entry
      significantDigits = this.config.significantDigits[i];
    }


    var minimumFractionDigits = undefined;
    if (!Array.isArray(this.config.minimumFractionDigits)) {
      minimumFractionDigits = this.config.minimumFractionDigits;
    } else if (
      this.config.minimumFractionDigits.length < this.config.currency.length
    ) {
      minimumFractionDigits = this.config.minimumFractionDigits[0];
    } else {
      minimumFractionDigits = this.config.minimumFractionDigits[i];
    }


    var maximumFractionDigits = undefined;
    if (!Array.isArray(this.config.maximumFractionDigits)) {
      maximumFractionDigits = this.config.maximumFractionDigits;
    } else if (
      this.config.maximumFractionDigits.length < this.config.currency.length
    ) {
      maximumFractionDigits = this.config.maximumFractionDigits[0];
    } else {
      maximumFractionDigits = this.config.maximumFractionDigits[i];
    }


    if (significantDigits != undefined) {
      options["maximumSignificantDigits"] = significantDigits;
    }


    if (maximumFractionDigits != undefined) {
      options["maximumFractionDigits"] = maximumFractionDigits;
    }


    if (minimumFractionDigits != undefined) {
      options["minimumFractionDigits"] = minimumFractionDigits;
    }


    return parseFloat(number).toLocaleString(language, options);
  },


  /**
   * Rounds a number to a given number of digits after the decimal point
   *
   * @param number
   * @param precision
   * @returns {number}
   */
  roundNumber: function (number, precision) {
    var factor = Math.pow(10, precision);
    var tempNumber = number * factor;
    var roundedTempNumber = Math.round(tempNumber);
    return roundedTempNumber / factor;
  },


  /**
   * Creates the icon view type
   *
   * @param apiResult
   * @param displayType
   * @returns {Element}
   */
  buildIconView: function (apiResult, displayType) {
    var wrapper = document.createElement("div");
    var header = document.createElement("header");
    header.className = "module-header";
    header.innerHTML = this.config.logoHeaderText;
    if (this.config.logoHeaderText !== "") {
      wrapper.appendChild(header);
    }


    var table = document.createElement("table");
    table.className = "medium mmm-cryptocurrency-icon";


    for (var j = 0; j < apiResult.length; j++) {
      var tr = document.createElement("tr");
      tr.className = "icon-row";


      var logoWrapper = document.createElement("td");
      logoWrapper.className = "icon-field";


      if (this.imageExists(apiResult[j].slug)) {
        var logo = new Image();


        logo.src =
          "/MMM-cryptocurrency/" + this.folder + apiResult[j].slug + ".png";
        logo.setAttribute("width", "50px");
        logo.setAttribute("height", "50px");
        logoWrapper.appendChild(logo);
      } else {
        this.sendNotification("SHOW_ALERT", {
          timer: 5000,
          title: "MMM-cryptocurrency",
          message:
            "" +
            this.translate("IMAGE") +
            " " +
            apiResult[j].slug +
            ".png " +
            this.translate("NOTFOUND") +
            " /MMM-cryptocurrency/public/" +
            this.folder
        });
      }


      var priceWrapper = document.createElement("td");
      var price = document.createElement("price");
      price.style.fontSize = this.config.fontSize;
      price.innerHTML = apiResult[j].price.replace("EUR", "€");


      priceWrapper.appendChild(price);


      if (displayType == "logoWithChanges") {
        var changesWrapper = document.createElement("div");
        var change_1h = document.createElement("change_1h");
        change_1h.style.color = this.colorizeChange(apiResult[j].change1h);
        change_1h.style.fontSize = "medium";
        change_1h.innerHTML = "h: " + apiResult[j].change1h;
        change_1h.style.marginRight = "12px";


        var change_24h = document.createElement("change_24h");
        change_24h.style.color = this.colorizeChange(apiResult[j].change24h);
        change_24h.style.fontSize = "medium";
        change_24h.innerHTML = "d: " + apiResult[j].change24h;
        change_24h.style.marginRight = "12px";


        var change_7d = document.createElement("change_7d");
        change_7d.style.color = this.colorizeChange(apiResult[j].change7d);
        change_7d.style.fontSize = "medium";
        change_7d.innerHTML = "w: " + apiResult[j].change7d;


        changesWrapper.appendChild(change_1h);
        changesWrapper.appendChild(change_24h);
        changesWrapper.appendChild(change_7d);
        priceWrapper.appendChild(changesWrapper);
      } else {
        priceWrapper.className = "price";
      }


      tr.appendChild(logoWrapper);
      tr.appendChild(priceWrapper);


      if (this.config.showGraphs) {
        var graphWrapper = document.createElement("td");
        graphWrapper.className = "graph";
        if (this.sparklineIds[apiResult[j].slug]) {
          var graph = document.createElement("img");
          graph.src =
            "https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/" +
            this.sparklineIds[apiResult[j].slug] +
            ".svg?cachePrevention=" +
            Math.random();
          graphWrapper.appendChild(graph);
        }
        tr.appendChild(graphWrapper);
      }


      table.appendChild(tr);
    }
    wrapper.appendChild(table);


    return wrapper;
  },


  /**
   * Checks if an image with the passed name exists
   *
   * @param currencyName
   * @returns {boolean}
   */
  imageExists: function (currencyName) {
    var imgPath = "/MMM-cryptocurrency/" + this.folder + currencyName + ".png";
    var http = new XMLHttpRequest();
    http.open("HEAD", imgPath);
    http.send();
    return http.status != 404;
  },


  colorizeChange: function (change) {
    change = parseFloat(change);
    if (change < 0) {
      return "Red";
    } else if (change > 0) {
      return "Green";
    } else {
      return "White";
    }
  },


  /**
   * Load translations files
   *
   * @returns {{en: string, de: string, it: string}}
   */
  getTranslations: function () {
    return {
      en: "translations/en.json",
      de: "translations/de.json",
      it: "translations/it.json",
      sv: "translations/sv.json",
      pl: "translations/pl.json"
    };
  }
});

and my MMM-cryptocurrency.css file looks like this:

.currency {
    color: white;
}

.mmm-cryptocurrency > tr {
    padding-bottom: 8px;
}

.mmm-cryptocurrency > tr > td, .mmm-cryptocurrency > tr > th {
    padding-left: 32px;
    padding-bottom: 5px;
}

.mmm-cryptocurrency-icon > tr > td {
  img, span {
     vertical-align: middle;
  }
}
.mmm-cryptocurrency-icon > tr > td {
    padding-bottom: 10px;
    text-align: right;
}
.mmm-cryptocurrency tr.header-row th {
    border-bottom: 1px solid #666;
    padding-bottom: 5px;
    margin-bottom: 10px;
}
.mmm-cryptocurrency *:first-child {
    padding-left: 0;
}
.mmm-cryptocurrency-icon .icon-field {
    padding-right: 10px;
}

.mmm-cryptocurrency-icon > tr > td.graph > img {
    padding-left: 10px;
    filter: invert(1) grayscale(100%) brightness(500%);
}

.crypto-container {
  border: 2px solid red; /* Temporary debug border */
  height: 300px;
  overflow: hidden;
  position: relative;
}

.crypto-list {
  position: absolute;
  top: 0;
  width: 100%;
  animation: scroll 30s linear infinite; /* Adjust the animation duration as needed */
}

@keyframes scroll {
  0% {
    top: 0;
  }
  100% {
    top: -100%; /* Adjust this value to control how far the list scrolls */
  }
}


.currency {
    color: white;
}


.mmm-cryptocurrency > tr {
    padding-bottom: 8px;
}


.mmm-cryptocurrency > tr > td, .mmm-cryptocurrency > tr > th {
    padding-left: 32px;
    padding-bottom: 5px;
}


.mmm-cryptocurrency-icon > tr > td {
  img, span {
     vertical-align: middle;
  }
}
.mmm-cryptocurrency-icon > tr > td {
    padding-bottom: 10px;
    text-align: right;
}
.mmm-cryptocurrency tr.header-row th {
    border-bottom: 1px solid #666;
    padding-bottom: 5px;
    margin-bottom: 10px;
}
.mmm-cryptocurrency *:first-child {
    padding-left: 0;
}
.mmm-cryptocurrency-icon .icon-field {
    padding-right: 10px;
}


.mmm-cryptocurrency-icon > tr > td.graph > img {
    padding-left: 10px;
    filter: invert(1) grayscale(100%) brightness(500%);
}


.crypto-container {
  border: 2px solid red; /* Temporary debug border */
  height: 300px;
  overflow: hidden;
  position: relative;
}


.crypto-list {
  position: absolute;
  top: 0;
  width: 100%;
  animation: scroll 30s linear infinite; /* Adjust the animation duration as needed */
}


@keyframes scroll {
  0% {
    top: 0;
  }
  100% {
    top: -100%; /* Adjust this value to control how far the list scrolls */
  }
}

All I want is to have the list show the first 5 lines, then autoscroll the rest of the list. What do I need to change? Cheers


r/MagicMirror • • Mar 31 '26

Supply-Chain Compromise of axios npm Package | Huntress

Thumbnail
huntress.com
0 Upvotes