move default modules from /modules/default to /defaultmodules (#4019)

Since the project's inception, I've missed a clear separation between
default and third-party modules.

This increases complexity within the project (exclude `modules`, but not
`modules/default`), but the mixed use is particularly problematic in
Docker setups.

Therefore, with this pull request, I'm moving the default modules to a
different directory.

~~I've chosen `default/modules`, but I'm not bothered about it;
`defaultmodules` or something similar would work just as well.~~

Changed to `defaultmodules`.

Let me know if there's a majority in favor of this change.
This commit is contained in:
Karsten Hassel
2026-01-27 08:37:52 +01:00
committed by GitHub
parent 5e0cd28980
commit d44db6ea10
113 changed files with 59 additions and 40 deletions

View File

@@ -0,0 +1,6 @@
# Module: Clock
The `clock` module is one of the default modules of the MagicMirror².
This module displays the current date and time. The information will be updated realtime.
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/clock.html).

View File

@@ -0,0 +1,318 @@
/* global SunCalc, formatTime */
Module.register("clock", {
// Module config defaults.
defaults: {
displayType: "digital", // options: digital, analog, both
timeFormat: config.timeFormat,
timezone: null,
displaySeconds: true,
showPeriod: true,
showPeriodUpper: false,
clockBold: false,
showDate: true,
showTime: true,
showWeek: false, // options: true, false, 'short'
dateFormat: "dddd, LL",
sendNotifications: false,
/* specific to the analog clock */
analogSize: "200px",
analogFace: "simple", // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive)
analogPlacement: "bottom", // options: 'top', 'bottom', 'left', 'right'
analogShowDate: "top", // OBSOLETE, can be replaced with analogPlacement and showTime, options: false, 'top', or 'bottom'
secondsColor: "#888888", // DEPRECATED, use CSS instead. Class "clock-second-digital" for digital clock, "clock-second" for analog clock.
showSunTimes: false, // options: true, false, 'disableNextEvent'
showMoonTimes: false, // options: false, 'times' (rise/set), 'percent' (lit percent), 'phase' (current phase), or 'both' (percent & phase)
lat: 47.630539,
lon: -122.344147
},
// Define required scripts.
getScripts () {
return ["moment.js", "moment-timezone.js", "suncalc.js"];
},
// Define styles.
getStyles () {
return ["clock_styles.css", "font-awesome.css"];
},
// Define start sequence.
start () {
Log.info(`Starting module: ${this.name}`);
// Schedule update interval.
this.second = moment().second();
this.minute = moment().minute();
// Calculate how many ms should pass until next update depending on if seconds is displayed or not
const delayCalculator = (reducedSeconds) => {
const EXTRA_DELAY = 50; // Deliberate imperceptible delay to prevent off-by-one timekeeping errors
if (this.config.displaySeconds) {
return 1000 - moment().milliseconds() + EXTRA_DELAY;
} else {
return (60 - reducedSeconds) * 1000 - moment().milliseconds() + EXTRA_DELAY;
}
};
// A recursive timeout function instead of interval to avoid drifting
const notificationTimer = () => {
this.updateDom();
if (this.config.sendNotifications) {
// If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
if (this.config.displaySeconds) {
this.second = moment().second();
if (this.second !== 0) {
this.sendNotification("CLOCK_SECOND", this.second);
setTimeout(notificationTimer, delayCalculator(0));
return;
}
}
// If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
this.minute = moment().minute();
this.sendNotification("CLOCK_MINUTE", this.minute);
}
setTimeout(notificationTimer, delayCalculator(0));
};
// Set the initial timeout with the amount of seconds elapsed as
// reducedSeconds, so it will trigger when the minute changes
setTimeout(notificationTimer, delayCalculator(this.second));
// Set locale.
moment.locale(config.language);
},
// Override dom generator.
getDom () {
const wrapper = document.createElement("div");
wrapper.classList.add("clock-grid");
/************************************
* Create wrappers for analog and digital clock
*/
const analogWrapper = document.createElement("div");
analogWrapper.className = "clock-circle";
const digitalWrapper = document.createElement("div");
digitalWrapper.className = "digital";
/************************************
* Create wrappers for DIGITAL clock
*/
const dateWrapper = document.createElement("div");
const timeWrapper = document.createElement("div");
const hoursWrapper = document.createElement("span");
const minutesWrapper = document.createElement("span");
const secondsWrapper = document.createElement("sup");
const periodWrapper = document.createElement("span");
const sunWrapper = document.createElement("div");
const moonWrapper = document.createElement("div");
const weekWrapper = document.createElement("div");
// Style Wrappers
dateWrapper.className = "date normal medium";
timeWrapper.className = "time bright large light";
hoursWrapper.className = "clock-hour-digital";
minutesWrapper.className = "clock-minute-digital";
secondsWrapper.className = "clock-second-digital dimmed";
sunWrapper.className = "sun dimmed small";
moonWrapper.className = "moon dimmed small";
weekWrapper.className = "week dimmed medium";
// Set content of wrappers.
const now = moment();
if (this.config.timezone) {
now.tz(this.config.timezone);
}
if (this.config.showDate) {
dateWrapper.innerHTML = now.format(this.config.dateFormat);
digitalWrapper.appendChild(dateWrapper);
}
if (this.config.displayType !== "analog" && this.config.showTime) {
let hourSymbol = "HH";
if (this.config.timeFormat !== 24) {
hourSymbol = "h";
}
hoursWrapper.innerHTML = now.format(hourSymbol);
minutesWrapper.innerHTML = now.format("mm");
timeWrapper.appendChild(hoursWrapper);
if (this.config.clockBold) {
minutesWrapper.classList.add("bold");
} else {
timeWrapper.innerHTML += ":";
}
timeWrapper.appendChild(minutesWrapper);
secondsWrapper.innerHTML = now.format("ss");
if (this.config.showPeriodUpper) {
periodWrapper.innerHTML = now.format("A");
} else {
periodWrapper.innerHTML = now.format("a");
}
if (this.config.displaySeconds) {
timeWrapper.appendChild(secondsWrapper);
}
if (this.config.showPeriod && this.config.timeFormat !== 24) {
timeWrapper.appendChild(periodWrapper);
}
digitalWrapper.appendChild(timeWrapper);
}
/****************************************************************
* Create wrappers for Sun Times, only if specified in config
*/
if (this.config.showSunTimes) {
const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon);
const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset);
let sunWrapperInnerHTML = "";
if (this.config.showSunTimes !== "disableNextEvent") {
let nextEvent;
if (now.isBefore(sunTimes.sunrise)) {
nextEvent = sunTimes.sunrise;
} else if (now.isBefore(sunTimes.sunset)) {
nextEvent = sunTimes.sunset;
} else {
const tomorrowSunTimes = SunCalc.getTimes(now.clone().add(1, "day"), this.config.lat, this.config.lon);
nextEvent = tomorrowSunTimes.sunrise;
}
const untilNextEvent = moment.duration(moment(nextEvent).diff(now));
const untilNextEventString = `${untilNextEvent.hours()}h ${untilNextEvent.minutes()}m`;
sunWrapperInnerHTML = `<span class="${isVisible ? "bright" : ""}"><i class="fas fa-sun" aria-hidden="true"></i> ${untilNextEventString}</span>`;
}
sunWrapperInnerHTML += `<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunrise)}</span>`
+ `<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunset)}</span>`;
sunWrapper.innerHTML = sunWrapperInnerHTML;
digitalWrapper.appendChild(sunWrapper);
}
/****************************************************************
* Create wrappers for Moon Times, only if specified in config
*/
if (this.config.showMoonTimes) {
const moonIllumination = SunCalc.getMoonIllumination(now.toDate());
const moonTimes = SunCalc.getMoonTimes(now, this.config.lat, this.config.lon);
const moonRise = moonTimes.rise;
let moonSet;
if (moment(moonTimes.set).isAfter(moonTimes.rise)) {
moonSet = moonTimes.set;
} else {
const nextMoonTimes = SunCalc.getMoonTimes(now.clone().add(1, "day"), this.config.lat, this.config.lon);
moonSet = nextMoonTimes.set;
}
const isVisible = now.isBetween(moonRise, moonSet) || moonTimes.alwaysUp === true;
const showFraction = ["both", "percent"].includes(this.config.showMoonTimes);
const showUnicode = ["both", "phase"].includes(this.config.showMoonTimes);
const illuminatedFractionString = `${Math.round(moonIllumination.fraction * 100)}%`;
const image = showUnicode ? [..."🌑🌒🌓🌔🌕🌖🌗🌘"][Math.floor(moonIllumination.phase * 8)] : "<i class=\"fas fa-moon\" aria-hidden=\"true\"></i>";
moonWrapper.innerHTML
= `<span class="${isVisible ? "bright" : ""}">${image} ${showFraction ? illuminatedFractionString : ""}</span>`
+ `<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${moonRise ? formatTime(this.config, moonRise) : "..."}</span>`
+ `<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${moonSet ? formatTime(this.config, moonSet) : "..."}</span>`;
digitalWrapper.appendChild(moonWrapper);
}
if (this.config.showWeek) {
if (this.config.showWeek === "short") {
weekWrapper.innerHTML = this.translate("WEEK_SHORT", { weekNumber: now.week() });
} else {
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
}
digitalWrapper.appendChild(weekWrapper);
}
/****************************************************************
* Create wrappers for ANALOG clock, only if specified in config
*/
if (this.config.displayType !== "digital") {
// If it isn't 'digital', then an 'analog' clock was also requested
// Calculate the degree offset for each hand of the clock
if (this.config.timezone) {
now.tz(this.config.timezone);
}
const second = now.seconds() * 6,
minute = now.minute() * 6 + second / 60,
hour = ((now.hours() % 12) / 12) * 360 + 90 + minute / 12;
// Create wrappers
analogWrapper.style.width = this.config.analogSize;
analogWrapper.style.height = this.config.analogSize;
if (this.config.analogFace !== "" && this.config.analogFace !== "simple" && this.config.analogFace !== "none") {
analogWrapper.style.background = `url(${this.data.path}faces/${this.config.analogFace}.svg)`;
analogWrapper.style.backgroundSize = "100%";
// The following line solves issue: https://github.com/MagicMirrorOrg/MagicMirror/issues/611
// analogWrapper.style.border = "1px solid black";
analogWrapper.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
} else if (this.config.analogFace !== "none") {
analogWrapper.style.border = "2px solid white";
}
const clockFace = document.createElement("div");
clockFace.className = "clock-face";
const clockHour = document.createElement("div");
clockHour.id = "clock-hour";
clockHour.style.transform = `rotate(${hour}deg)`;
clockHour.className = "clock-hour";
const clockMinute = document.createElement("div");
clockMinute.id = "clock-minute";
clockMinute.style.transform = `rotate(${minute}deg)`;
clockMinute.className = "clock-minute";
// Combine analog wrappers
clockFace.appendChild(clockHour);
clockFace.appendChild(clockMinute);
if (this.config.displaySeconds) {
const clockSecond = document.createElement("div");
clockSecond.id = "clock-second";
clockSecond.style.transform = `rotate(${second}deg)`;
clockSecond.className = "clock-second";
clockSecond.style.backgroundColor = this.config.secondsColor; /* DEPRECATED, to be removed in a future version , use CSS instead */
clockFace.appendChild(clockSecond);
}
analogWrapper.appendChild(clockFace);
}
/*******************************************
* Update placement, respect old analogShowDate even if it's not needed anymore
*/
if (this.config.displayType === "analog") {
// Display only an analog clock
if (this.config.showDate) {
// Add date to the analog clock
dateWrapper.innerHTML = now.format(this.config.dateFormat);
wrapper.appendChild(dateWrapper);
}
if (this.config.analogShowDate === "bottom") {
wrapper.classList.add("clock-grid-bottom");
} else if (this.config.analogShowDate === "top") {
wrapper.classList.add("clock-grid-top");
}
wrapper.appendChild(analogWrapper);
} else if (this.config.displayType === "digital") {
wrapper.appendChild(digitalWrapper);
} else if (this.config.displayType === "both") {
wrapper.classList.add(`clock-grid-${this.config.analogPlacement}`);
wrapper.appendChild(analogWrapper);
wrapper.appendChild(digitalWrapper);
}
// Return the wrapper to the dom.
return wrapper;
}
});

View File

@@ -0,0 +1,118 @@
.clock-grid {
display: inline-flex;
gap: 15px;
}
.clock-grid-left {
flex-direction: row;
}
.clock-grid-right {
flex-direction: row-reverse;
}
.clock-grid-top {
flex-direction: column;
}
.clock-grid-bottom {
flex-direction: column-reverse;
}
.clock-circle {
place-self: center;
position: relative;
border-radius: 50%;
background-size: 100%;
}
.clock-face {
width: 100%;
height: 100%;
}
.clock-face::after {
position: absolute;
top: 50%;
left: 50%;
width: 6px;
height: 6px;
margin: -3px 0 0 -3px;
background: var(--color-text-bright);
border-radius: 3px;
content: "";
display: block;
}
.clock-hour {
width: 0;
height: 0;
position: absolute;
top: 50%;
left: 50%;
margin: -2px 0 -2px -25%; /* numbers must match negative length & thickness */
padding: 2px 0 2px 25%; /* indicator length & thickness */
background: var(--color-text-bright);
transform-origin: 100% 50%;
border-radius: 3px 0 0 3px;
}
.clock-minute {
width: 0;
height: 0;
position: absolute;
top: 50%;
left: 50%;
margin: -35% -2px 0; /* numbers must match negative length & thickness */
padding: 35% 2px 0; /* indicator length & thickness */
background: var(--color-text-bright);
transform-origin: 50% 100%;
border-radius: 3px 0 0 3px;
}
.clock-second {
width: 0;
height: 0;
position: absolute;
top: 50%;
left: 50%;
margin: -38% -1px 0 0; /* numbers must match negative length & thickness */
padding: 38% 1px 0 0; /* indicator length & thickness */
/* background: #888888 !important; */
/* use this instead of secondsColor */
/* have to use !important, because the code explicitly sets the color currently */
transform-origin: 50% 100%;
}
.module.clock .digital {
display: flex;
flex-direction: column;
gap: 3px;
}
.module.clock .sun,
.module.clock .moon {
display: flex;
white-space: nowrap;
gap: 10px;
}
.module.clock .sun > *,
.module.clock .moon > * {
flex: 1;
}
.module.clock .clock-hour-digital {
color: var(--color-text-bright);
}
.module.clock .clock-minute-digital {
color: var(--color-text-bright);
}
.module.clock .clock-second-digital {
color: var(--color-text-dimmed);
}

View File

@@ -0,0 +1 @@
<svg id="Hour_Markers_-_Singlets" data-name="Hour Markers - Singlets" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1,.cls-2{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;}.cls-2{stroke-width:0.5px;}</style></defs><title>face-001</title><line class="cls-1" x1="125" y1="1.25" x2="125" y2="16.23"/><line class="cls-1" x1="186.87" y1="17.83" x2="179.39" y2="30.8"/><line class="cls-1" x1="232.17" y1="63.12" x2="219.2" y2="70.61"/><line class="cls-1" x1="248.75" y1="125" x2="233.77" y2="125"/><line class="cls-1" x1="232.17" y1="186.87" x2="219.2" y2="179.39"/><line class="cls-1" x1="186.88" y1="232.17" x2="179.39" y2="219.2"/><line class="cls-1" x1="125" y1="248.75" x2="125" y2="233.77"/><line class="cls-1" x1="63.13" y1="232.17" x2="70.61" y2="219.2"/><line class="cls-1" x1="17.83" y1="186.88" x2="30.8" y2="179.39"/><line class="cls-1" x1="1.25" y1="125" x2="16.23" y2="125"/><line class="cls-1" x1="17.83" y1="63.13" x2="30.8" y2="70.61"/><line class="cls-1" x1="63.12" y1="17.83" x2="70.61" y2="30.8"/><line class="cls-2" x1="138.01" y1="1.25" x2="136.96" y2="11.23"/><line class="cls-2" x1="150.87" y1="3.29" x2="148.78" y2="13.11"/><line class="cls-2" x1="163.45" y1="6.66" x2="160.35" y2="16.21"/><line class="cls-2" x1="175.61" y1="11.33" x2="171.53" y2="20.5"/><line class="cls-2" x1="198.14" y1="24.33" x2="192.24" y2="32.45"/><line class="cls-2" x1="208.26" y1="32.53" x2="201.54" y2="39.99"/><line class="cls-2" x1="217.47" y1="41.74" x2="210.01" y2="48.46"/><line class="cls-2" x1="225.67" y1="51.86" x2="217.55" y2="57.76"/><line class="cls-2" x1="238.67" y1="74.39" x2="229.5" y2="78.47"/><line class="cls-2" x1="243.34" y1="86.55" x2="233.79" y2="89.65"/><line class="cls-2" x1="246.71" y1="99.13" x2="236.89" y2="101.22"/><line class="cls-2" x1="248.75" y1="111.99" x2="238.77" y2="113.04"/><line class="cls-2" x1="248.75" y1="138.01" x2="238.77" y2="136.96"/><line class="cls-2" x1="246.71" y1="150.87" x2="236.89" y2="148.78"/><line class="cls-2" x1="243.34" y1="163.45" x2="233.79" y2="160.35"/><line class="cls-2" x1="238.67" y1="175.61" x2="229.5" y2="171.53"/><line class="cls-2" x1="225.67" y1="198.14" x2="217.55" y2="192.24"/><line class="cls-2" x1="217.47" y1="208.26" x2="210.01" y2="201.54"/><line class="cls-2" x1="208.26" y1="217.47" x2="201.54" y2="210.01"/><line class="cls-2" x1="198.14" y1="225.67" x2="192.24" y2="217.55"/><line class="cls-2" x1="175.61" y1="238.67" x2="171.53" y2="229.5"/><line class="cls-2" x1="163.45" y1="243.34" x2="160.35" y2="233.79"/><line class="cls-2" x1="150.87" y1="246.71" x2="148.78" y2="236.89"/><line class="cls-2" x1="138.01" y1="248.75" x2="136.96" y2="238.77"/><line class="cls-2" x1="111.99" y1="248.75" x2="113.04" y2="238.77"/><line class="cls-2" x1="99.13" y1="246.71" x2="101.22" y2="236.89"/><line class="cls-2" x1="86.55" y1="243.34" x2="89.65" y2="233.79"/><line class="cls-2" x1="74.39" y1="238.67" x2="78.47" y2="229.5"/><line class="cls-2" x1="51.86" y1="225.67" x2="57.76" y2="217.55"/><line class="cls-2" x1="41.74" y1="217.47" x2="48.46" y2="210.01"/><line class="cls-2" x1="32.53" y1="208.26" x2="39.99" y2="201.54"/><line class="cls-2" x1="24.33" y1="198.14" x2="32.45" y2="192.24"/><line class="cls-2" x1="11.33" y1="175.61" x2="20.5" y2="171.53"/><line class="cls-2" x1="6.66" y1="163.45" x2="16.21" y2="160.35"/><line class="cls-2" x1="3.29" y1="150.87" x2="13.11" y2="148.78"/><line class="cls-2" x1="1.25" y1="138.01" x2="11.23" y2="136.96"/><line class="cls-2" x1="1.25" y1="111.99" x2="11.23" y2="113.04"/><line class="cls-2" x1="3.29" y1="99.13" x2="13.11" y2="101.22"/><line class="cls-2" x1="6.66" y1="86.55" x2="16.21" y2="89.65"/><line class="cls-2" x1="11.33" y1="74.39" x2="20.5" y2="78.47"/><line class="cls-2" x1="24.33" y1="51.86" x2="32.45" y2="57.76"/><line class="cls-2" x1="32.53" y1="41.74" x2="39.99" y2="48.46"/><line class="cls-2" x1="41.74" y1="32.53" x2="48.46" y2="39.99"/><line class="cls-2" x1="51.86" y1="24.33" x2="57.76" y2="32.45"/><line class="cls-2" x1="74.39" y1="11.33" x2="78.47" y2="20.5"/><line class="cls-2" x1="86.55" y1="6.66" x2="89.65" y2="16.21"/><line class="cls-2" x1="99.13" y1="3.29" x2="101.22" y2="13.11"/><line class="cls-2" x1="111.99" y1="1.25" x2="113.04" y2="11.23"/></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1 @@
<svg id="Hour_Markers_-_Doubles" data-name="Hour Markers - Doubles" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2.98px;}</style></defs><title>face-002</title><line class="cls-1" x1="122.01" y1="1.75" x2="122.01" y2="16.67"/><line class="cls-1" x1="186.62" y1="18.26" x2="179.17" y2="31.18"/><line class="cls-1" x1="231.74" y1="63.37" x2="218.82" y2="70.83"/><line class="cls-1" x1="248.25" y1="127.99" x2="233.33" y2="127.99"/><line class="cls-1" x1="231.74" y1="186.62" x2="218.82" y2="179.17"/><line class="cls-1" x1="186.63" y1="231.74" x2="179.17" y2="218.82"/><line class="cls-1" x1="127.99" y1="248.25" x2="127.99" y2="233.33"/><line class="cls-1" x1="63.38" y1="231.74" x2="70.83" y2="218.82"/><line class="cls-1" x1="18.26" y1="186.63" x2="31.18" y2="179.17"/><line class="cls-1" x1="1.75" y1="122.01" x2="16.67" y2="122.01"/><line class="cls-1" x1="18.26" y1="63.38" x2="31.18" y2="70.83"/><line class="cls-1" x1="63.37" y1="18.26" x2="70.83" y2="31.18"/><line class="cls-1" x1="127.99" y1="1.75" x2="127.99" y2="16.67"/><line class="cls-1" x1="248.25" y1="122.01" x2="233.33" y2="122.01"/><line class="cls-1" x1="122.01" y1="248.25" x2="122.01" y2="233.33"/><line class="cls-1" x1="1.75" y1="127.99" x2="16.67" y2="127.99"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB