Compare commits

...

221 Commits

Author SHA1 Message Date
Karsten Hassel
62b0f7f26e Release 2.32.0 (#3826)
## [2.32.0] - 2025-07-01

Thanks to: @bughaver, @bugsounet, @khassel, @KristjanESPERANTO,
@plebcity, @rejas, @sdetweil.

> ⚠️ This release needs nodejs version `v22.14.0 or higher`

### Added

- [config] Allow to change module order for final renderer (or
dynamically with CSS): Feature `order` in config (#3762)
- [clock] Added option 'disableNextEvent' to hide next sun event (#3769)
- [clock] Implement short syntax for clock week (#3775)

### Changed

- [refactor] Simplify module loading process (#3766)
- Use `node --run` instead of `npm run` (#3764) and adapt `start:dev`
script (#3773)
- [workflow] Run linter and spellcheck with LTS node version (#3767)
- [workflow] Split "Run test" step into two steps for more clarity
(#3767)
- [linter] Review linter setup (#3783)
  - Fix command to lint markdown in `CONTRIBUTING.md`
  - Re-activate JSDoc linting and fix linting issues
  - Refactor ESLint config to use `defineConfig` and `globalIgnores`
  - Replace `eslint-plugin-import` with `eslint-plugin-import-x`
- Switch Stylelint config to flat format and simplify Stylelint scripts
- [workflow] Replace Node.js version v23 with v24 (#3770)
- [refactor] Replace deprecated constants `fs.F_OK` and `fs.R_OK`
(#3789)
- [refactor] Replace `ansis` with built-in function `util.styleText`
(#3793)
- [core] Integrate stuff from `vendor` and `fonts` folders into main
`package.json`, simplifies install and maintaining dependencies (#3795,
#3805)
- [l10n] Complete translations (with the help of translation tools)
(#3794)
- [refactor] Refactored `calendarfetcherutils` in Calendar module to
handle timezones better (#3806)
  - Removed as many of the date conversions as possible
- Use `moment-timezone` when calculating recurring events, this will fix
problems from the past with offsets and DST not being handled properly
- Added some tests to test the behavior of the refactored methods to
make sure the correct event dates are returned
- [linter] Enable ESLint rule `no-console` and replace `console` with
`Log` in some files (#3810)
- [tests] Review and refactor translation tests (#3792)

### Fixed

- [fix] Handle spellcheck issues (#3783)
- [calendar] fix fullday event rrule until with timezone offset (#3781)
- [feat] Add rule `no-undef` in config file validation to fix #3785
(#3786)
- [fonts] Fix `roboto.css` to avoid error message `Unknown descriptor
'var(' in @font-face rule.` in firefox console (#3787)
- [tests] Fix and refactor e2e test `Same keys` in
`translations_spec.js` (#3809)
- [tests] Fix e2e tests newsfeed and calendar to exit without open
handles (#3817)

### Updated

- [core] Update dependencies including electron to v36 (#3774, #3788,
#3811, #3804, #3815, #3823)
- [core] Update package type to `commonjs`
- [logger] Review factory code part: use `switch/case` instead of
`if/else if` (#3812)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Michael Teeuw <michael@xonaymedia.nl>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ross Younger <crazyscot@gmail.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: jkriegshauser <joshuakr@nvidia.com>
Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com>
Co-authored-by: sam detweiler <sdetweil@gmail.com>
Co-authored-by: vppencilsharpener <tim.pray@gmail.com>
Co-authored-by: veeck <michael.veeck@nebenan.de>
Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
Co-authored-by: Brian O'Connor <btoconnor@users.noreply.github.com>
Co-authored-by: WallysWellies <59727507+WallysWellies@users.noreply.github.com>
Co-authored-by: Jason Stieber <jrstieber@gmail.com>
Co-authored-by: jargordon <50050429+jargordon@users.noreply.github.com>
Co-authored-by: Daniel <32464403+dkallen78@users.noreply.github.com>
Co-authored-by: Ryan Williams <65094007+ryan-d-williams@users.noreply.github.com>
Co-authored-by: Panagiotis Skias <panagiotis.skias@gmail.com>
Co-authored-by: Marc Landis <dirk.rettschlag@gmail.com>
Co-authored-by: HeikoGr <20295490+HeikoGr@users.noreply.github.com>
Co-authored-by: Pedro Lamas <pedrolamas@gmail.com>
Co-authored-by: veeck <gitkraken@veeck.de>
Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com>
Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com>
Co-authored-by: DevIncomin <56730075+Developer-Incoming@users.noreply.github.com>
Co-authored-by: Nathan <n8nyoung@gmail.com>
Co-authored-by: mixasgr <mixasgr@users.noreply.github.com>
Co-authored-by: Savvas Adamtziloglou <savvas-gr@greeklug.gr>
Co-authored-by: Konstantinos <geraki@gmail.com>
Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com>
Co-authored-by: BugHaver <43462320+bughaver@users.noreply.github.com>
Co-authored-by: BugHaver <43462320+lsaadeh@users.noreply.github.com>
Co-authored-by: Koen Konst <koenspero@gmail.com>
Co-authored-by: Koen Konst <c.h.konst@avisi.nl>
2025-07-01 00:10:47 +02:00
Veeck
8e0b8468d3 Add Code of Conduct (#3763)
The project is lacking a Code of Conduct

---------

Co-authored-by: veeck <gitkraken@veeck.de>
2025-05-16 08:03:43 +02:00
Veeck
39a614e0de Release 2.31.0 (#3758)
Thanks to: @Developer-Incoming, @eltociear, @geraki, @khassel,
@KristjanESPERANTO, @MagMar94, @mixasgr, @n8many, @OWL4C, @rejas,
@savvadam, @sdetweil.

> ⚠️ This release needs nodejs version `v22.14.0 or higher`

### Added

- Add CSS support to the digital clock hour/minute/second through the
use of the classes `clock-hour-digital`, `clock-minute-digital`, and
`clock-second-digital`.
- Add Arabic (#3719) and Esperanto translation.
- Mark option `secondsColor` as deprecated in clock module.
- Add Greek translation to Alerts module.
- [newsfeed] Add specific ignoreOlderThan value (override) per feed
(#3360)
- [weather] Added option Humidity to hourly View
- [weather] Added option to hide hourly entries that are Zero, hiding
the entire column if empty.
- [updatenotification] Added option to iterate over modules directory
instead using modules defined in `config.js` (#3739)

### Changed

- [core] starting clientonly now checks for needed env var
`WAYLAND_DISPLAY` or `DISPLAY` and starts electron with needed
parameters (if both are set wayland is used) (#3677)
- [core] Optimize systeminformation calls and output (#3689)
- [core] Add issue templates for feature requests and bug reports
(#3695)
- [core] Adapt `start:x11:dev` script
- [weather/yr] The Yr weather provider now enforces a minimum
`updateInterval` of 600 000 ms (10 minutes) to comply with the terms of
service. If a lower value is set, it will be automatically increased to
this minimum.
- [weather/weatherflow] Fixed icons and added hourly support as well as
UV, precipitation, and location name support.
- [workflow] Run `sudo apt-get update` before installing packages to
avoid install errors
- [workflow] Exclude issues with label `ready (coming with next
release)` from stale job

### Removed

### Updated

- [core] Update requirements and dependencies including electron to v35
and formatting (#3593, #3693, #3717)
- [core] Update prettier, ESLint and simplify config
- Update Greek translation

### Fixed

- [calendar] Fix clipping events being broadcast (#3678)
- [tests] Fix Electron tests by running them under new github image
ubuntu-24.04, replace xserver with labwc, running under xserver and
labwc depending on env variable WAYLAND_DISPLAY is set (#3676)
- [calendar] Fix arrayed symbols, #3267, again, add testcase, add
testcase for #3678
- [weather] Fix wrong weatherCondition name in openmeteo provider which
lead to n/a icon (#3691)
- [core] Fix wrong port in log message when starting server only (#3696)
- [calendar] Fix NewYork event processed on system in Central timezone
shows wrong time #3701
- [weather/yr] The Yr weather provider is now able to recover from bad
API responses instead of freezing (#3296)
- [compliments] Fix evening events being shown during the day (#3727)
- [weather] Fixed minor spacing issues when using UV Index in Hourly
- [workflow] Fix command to run spellcheck

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Michael Teeuw <michael@xonaymedia.nl>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
Co-authored-by: Ross Younger <crazyscot@gmail.com>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: jkriegshauser <joshuakr@nvidia.com>
Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com>
Co-authored-by: sam detweiler <sdetweil@gmail.com>
Co-authored-by: vppencilsharpener <tim.pray@gmail.com>
Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
Co-authored-by: Brian O'Connor <btoconnor@users.noreply.github.com>
Co-authored-by: WallysWellies <59727507+WallysWellies@users.noreply.github.com>
Co-authored-by: Jason Stieber <jrstieber@gmail.com>
Co-authored-by: jargordon <50050429+jargordon@users.noreply.github.com>
Co-authored-by: Daniel <32464403+dkallen78@users.noreply.github.com>
Co-authored-by: Ryan Williams <65094007+ryan-d-williams@users.noreply.github.com>
Co-authored-by: Panagiotis Skias <panagiotis.skias@gmail.com>
Co-authored-by: Marc Landis <dirk.rettschlag@gmail.com>
Co-authored-by: HeikoGr <20295490+HeikoGr@users.noreply.github.com>
Co-authored-by: Pedro Lamas <pedrolamas@gmail.com>
Co-authored-by: veeck <gitkraken@veeck.de>
Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com>
Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com>
Co-authored-by: DevIncomin <56730075+Developer-Incoming@users.noreply.github.com>
Co-authored-by: Nathan <n8nyoung@gmail.com>
Co-authored-by: mixasgr <mixasgr@users.noreply.github.com>
Co-authored-by: Savvas Adamtziloglou <savvas-gr@greeklug.gr>
Co-authored-by: Konstantinos <geraki@gmail.com>
Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com>
2025-04-01 20:11:02 +02:00
Karsten Hassel
9c9a5359dd Use different issue templates (#3695)
This PR will introduce different issue templates for bug reports,
feature requests and so on on GitHub. There is still room for
fine-tuning, but it's reached a state to show it to you and get
feedback.

I think that this can lead to better bug reports.

You can see the result in my repo:
https://github.com/KristjanESPERANTO/MagicMirror/issues/new/choose

Feel free to create new issues for testing.

What do you think? Do we want to pursue this further?
# Conflicts:
#	CHANGELOG.md
2025-01-17 19:19:17 +01:00
sam detweiler
c24de64d77 Release 2.30.0 (#3673)
## [2.30.0] - 2025-01-01

Thanks to: @xsorifc28, @HeikoGr, @bugsounet, @khassel,
@KristjanESPERANTO, @rejas, @sdetweil.

> ⚠️ This release needs nodejs version `v20` or `v22 or higher`, minimum
version is `v20.18.1`

### Added

- [core] Add wayland and windows start options to `package.json` (#3594)
- [docs] Add step for npm publishing in release process (#3595)
- [core] Add GitHub workflow to run spellcheck a few days before each
release (#3623)
- [core] Add test flag to `index.html` to pass to module js for test
mode detection (needed by #3630)
- [core] Add export on animation names (#3644)
- [compliments] Add support for refreshing remote compliments file, and
test cases (#3630)
- [linter] Re-add `eslint-plugin-import`now that it supports ESLint v9
(#3586)
- [linter] Re-activate `eslint-plugin-package-json` to lint
`package.json` (#3643)
- [linter] Add linting for markdown files (#3646)
- [linter] Add some handy ESLint rules.
- [calendar] Add ability to display end date for full date events, where
end is not same day (showEnd=true) (#3650)
- [core] Add text to the config.js.sample file about the locale variable
(#3654, #3655)
- [core] Add fetch timeout for all node_helpers (thru undici, forces
node 20.18.1 minimum) to help on slower systems. (#3660) (3661)

### Changed

- [core] Run code style checks in workflow only once (#3648)
- [core] Fix animations export #3644 only on server side (#3649)
- [core] Use project URL in fallback config (#3656)
- [core] Fix Access Denied crash writing js/positions.js (on synology
nas) #3651. new message, MM starts, but no modules showing (#3652)
- [linter] Switch to 'npx' for lint-staged in pre-commit hook (#3658)

### Removed

- [tests] Remove `node-pty` and `drivelist` from rebuilded test (#3575)
- [deps] Remove `@eslint/js` dependency. Already installed with `eslint`
in deep (#3636)

### Updated

- [repo] Reactivate `stale.yaml` as GitHub action to mark issues as
stale after 60 days and close them 7 days later (if no activity) (#3577,
#3580, #3581)
- [core] Update electron dependency to v32 (test electron rebuild) and
all other dependencies too (#3657)
- [tests] All test configs have been updated to allow full external
access, allowing for easier debugging (especially when running as a
container)
- [core] Run and test with node 23 (#3588)
- [workflow] delete exception `allow-ghsas: GHSA-8hc4-vh64-cxmj` in
`dep-review.yaml` (#3659)

### Fixed

- [updatenotification] Fix pm2 using detection when pm2 script is inside
or outside MagicMirror root folder (#3576) (#3605) (#3626) (#3628)
- [core] Fix loading node_helper of modules: avoid black screen, display
errors and continue loading with next module (#3578)
- [weather] Change default value for weatherEndpoint of provider
openweathermap to "/onecall" (#3574)
- [tests] Fix electron tests with mock dates, the mock on server side
was missing (#3597)
- [tests] Fix testcases with hard coded Date.now (#3597)
- [core] Fix missing `basePath` where `location.host` is used (#3613)
- [compliments] croner library changed filenames used in latest version
(#3624)
- [linter] Fix ESLint ignore pattern which caused that default modules
not to be linted (#3632)
- [core] Fix module path in case of sub/sub folder is used and use
path.resolve for resolve `moduleFolder` and `defaultModuleFolder` in
app.js (#3653)
- [calendar] Update to resolve issues #3098 #3144 #3351 #3422 #3443
#3467 #3537 related to timezone changes
- [calendar] Fix #3267 (styles array), also fixes event with both exdate
AND recurrence(and testcase)
- [calendar] Fix showEndsOnlyWithDuration not working, #3598, applies
ONLY to full day events
- [calendar] Fix showEnd for Full Day events (#3602)
- [tests] Suppress "module is not defined" in e2e tests (#3647)
- [calendar] Fix #3267 (styles array, really this time!)
- [core] Fix #3662 js/positions.js created incorrectly

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Michael Teeuw <michael@xonaymedia.nl>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
Co-authored-by: Ross Younger <crazyscot@gmail.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: jkriegshauser <joshuakr@nvidia.com>
Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com>
Co-authored-by: vppencilsharpener <tim.pray@gmail.com>
Co-authored-by: veeck <michael.veeck@nebenan.de>
Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
Co-authored-by: Brian O'Connor <btoconnor@users.noreply.github.com>
Co-authored-by: WallysWellies <59727507+WallysWellies@users.noreply.github.com>
Co-authored-by: Jason Stieber <jrstieber@gmail.com>
Co-authored-by: jargordon <50050429+jargordon@users.noreply.github.com>
Co-authored-by: Daniel <32464403+dkallen78@users.noreply.github.com>
Co-authored-by: Ryan Williams <65094007+ryan-d-williams@users.noreply.github.com>
Co-authored-by: Panagiotis Skias <panagiotis.skias@gmail.com>
Co-authored-by: Marc Landis <dirk.rettschlag@gmail.com>
Co-authored-by: HeikoGr <20295490+HeikoGr@users.noreply.github.com>
Co-authored-by: Pedro Lamas <pedrolamas@gmail.com>
Co-authored-by: veeck <gitkraken@veeck.de>
2025-01-01 08:27:27 -06:00
Karsten Hassel
94c3c699e8 Release 2.29.0 (#3568)
## [2.29.0] - 2024-10-01

Thanks to: @bugsounet, @dkallen78, @jargordon, @khassel,
@KristjanESPERANTO, @MarcLandis, @rejas, @ryan-d-williams, @sdetweil,
@skpanagiotis.

> ⚠️ This release needs nodejs version `v20` or `v22`, minimum version
is `v20.9.0`

### Added

- [compliments] Added support for cron type date/time format entries mm
hh DD MM dow (minutes/hours/days/months and day of week) see
https://crontab.cronhub.io for construction (#3481)
- [core] Check config at every start of MagicMirror² (#3450)
- [core] Add spelling check (cspell): `npm run test:spelling` and handle
spelling issues (#3544)
- [core] removed `config.paths.vendor` (could not work because `vendor`
is hardcoded in `index.html`), renamed `config.paths.modules` to
`config.foreignModulesDir`, added variable `MM_CUSTOMCSS_FILE` which -
if set - overrides `config.customCss`, added variable `MM_MODULES_DIR`
which - if set - overrides `config.foreignModulesDir`, added test for
`MM_MODULES_DIR` (#3530)
- [core] elements are now removed from `index.html` when loading script
or stylesheet files fails
- [core] Added `MODULE_DOM_UPDATED` notification each time the DOM is
re-rendered via `updateDom` (#3534)
- [tests] added minimal needed node version to tests (currently v20.9.0)
to avoid releases with wrong node version info
- [tests] Added `node-libgpiod` library to electron-rebuild tests
(#3563)

### Removed

- [core] removed installer only files (#3492)
- [core] removed raspberry object from systeminformation (#3505)
- [linter] removed `eslint-plugin-import`, because it doesn't support
ESLint v9. We will reenter it later when it does.
- [tests] removed `onoff` library from electron-rebuild tests (#3563)

### Updated

- [weather] Updated `apiVersion` default from 2.5 to 3.0 (#3424)
- [core] Updated dependencies including stylistic-eslint
- [core] nail down `node-ical` version to `0.18.0` with exception
`allow-ghsas: GHSA-8hc4-vh64-cxmj` in `dep-review.yaml` (which should
removed after next `node-ical` update)
- [core] Updated SocketIO catch all to new API
- [core] Allow custom modules positions by scanning index.html for the
defined regions, instead of hard coded (PR #3518 fixes issue #3504)
- [core] Detail optimizations in `config_check.js`
- [core] Updated minimal needed node version in `package.json`
(currently v20.9.0) (#3559) and except for v21 (no security updates)
(#3561)
- [linter] Switch to ESLint v9 and flat config and replace
`eslint-plugin-unicorn` by `@eslint/js`
- [core] fix discovering module positions twice after #3450

### Fixed

- Fixed `checks` badge in README.md
- [weather] Fixed issue with the UK Met Office provider following a
change in their API paths and header info.
- [core] add check for node_helper loading for multiple instances of
same module (#3502)
- [weather] Fixed issue for respecting unit config on broadcasted
notifications
- [tests] Fixes calendar test by moving it from e2e to electron with
fixed date (#3532)
- [calendar] fixed sliceMultiDayEvents getting wrong count and
displaying incorrect entries, Europe/Berlin (#3542)
- [tests] ignore `js/positions.js` when linting (this file is created at
runtime)
- [calendar] fixed sliceMultiDayEvents showing previous day without
config enabled

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Michael Teeuw <michael@xonaymedia.nl>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ross Younger <crazyscot@gmail.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: jkriegshauser <joshuakr@nvidia.com>
Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com>
Co-authored-by: sam detweiler <sdetweil@gmail.com>
Co-authored-by: vppencilsharpener <tim.pray@gmail.com>
Co-authored-by: veeck <michael.veeck@nebenan.de>
Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
Co-authored-by: Brian O'Connor <btoconnor@users.noreply.github.com>
Co-authored-by: WallysWellies <59727507+WallysWellies@users.noreply.github.com>
Co-authored-by: Jason Stieber <jrstieber@gmail.com>
Co-authored-by: jargordon <50050429+jargordon@users.noreply.github.com>
Co-authored-by: Daniel <32464403+dkallen78@users.noreply.github.com>
Co-authored-by: Ryan Williams <65094007+ryan-d-williams@users.noreply.github.com>
Co-authored-by: Panagiotis Skias <panagiotis.skias@gmail.com>
Co-authored-by: Marc Landis <dirk.rettschlag@gmail.com>
2024-10-01 00:02:17 +02:00
Karsten Hassel
53fc814ff8 Release 2.28.0 (#3490)
## [2.28.0] - 2024-07-01

Thanks to: @btoconnor, @bugsounet, @JasonStieber, @khassel,
@kleinmantara and @WallysWellies.

> ⚠️ This release needs nodejs version >= v20

### Added

- [calendar] Added config option "showEndsOnlyWithDuration" for default
calendar
- [compliments] Added `specialDayUnique` config option, defaults to
`false` (#3465)
- [weather] Provider weathergov: Use `precipitationLast3Hours` if
`precipitationLastHour` is `null` (#3124)

### Removed

- [tests] delete node v18 support (#3462)

### Updated

- [core] Update dependencies including electron to v31
- [core] use node >= v20 (#3462)
- [core] Update `config.js.sample` to use openmeteo as weather provider
which needs no api key
- [tests] Use latest@version of node for `automated-tests.yaml` (#3483)
- [updatenotification] Avoid using pm2 when running in docker container

### Fixed

- [core] Fixed crash possibility if `module: <name>` is not defined and
on `postion: <positon>` mistake (#3445)
- [weather] Fixed precipitationProbability in forecast for provider
openmeteo (#3446)
- [weather] Fixed type=daily for provider openmeteo having no data when
running after 23:00 (#3449)
- [weather] Fixed type=daily for provider openmeteo showing nightly
icons in forecast when current time is "nightly" (#3458)
- [weather] Fixed forecast and hourly weather for provider openmeteo to
use real temperatures, not apparent temperatures (#3466)
- [tests] Fixed e2e tests running in docker container which needs
`address: "0.0.0.0"` (#3479)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Michael Teeuw <michael@xonaymedia.nl>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ross Younger <crazyscot@gmail.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: jkriegshauser <joshuakr@nvidia.com>
Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com>
Co-authored-by: sam detweiler <sdetweil@gmail.com>
Co-authored-by: vppencilsharpener <tim.pray@gmail.com>
Co-authored-by: veeck <michael.veeck@nebenan.de>
Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
Co-authored-by: Brian O'Connor <btoconnor@users.noreply.github.com>
Co-authored-by: WallysWellies <59727507+WallysWellies@users.noreply.github.com>
Co-authored-by: Jason Stieber <jrstieber@gmail.com>
2024-06-30 23:44:21 +02:00
Veeck
5ea8a3469a Release 2.27.0 (#3410)
## [2.27.0] - 2024-04-01

Thanks to: @bugsounet, @crazyscot, @illimarkangur, @jkriegshauser,
@khassel, @KristjanESPERANTO, @Paranoid93, @rejas, @sdetweil and
@vppencilsharpener.

This release marks the first release without Michael Teeuw (@michmich).
A very special thanks to him for creating MagicMirror and leading the
project for so many years.

For more info, please read the following post: [A New Chapter for
MagicMirror: The Community Takes the
Lead](https://forum.magicmirror.builders/topic/18329/a-new-chapter-for-magicmirror-the-community-takes-the-lead).

### Added

- Output of system information to the console for troubleshooting (#3328
and #3337), ignore errors under aarch64 (#3349)
- [chore] Add `eslint-plugin-package-json` to lint the `package.json`
files (#3368)
- [weather] `showHumidity` config is now a string describing where to
show this element. Supported values: "wind", "temp", "feelslike",
"below", "none". (#3330)
- electron-rebuild test suite for electron and 3rd party modules
compatibility (#3392)
- Create MM² icon and attach it to electron process (#3407)

### Updated

- Update updatenotification (update_helper.js): Recode with pm2 library
(#3332)
- Removing lodash dependency by replacing merge by spread operator
(#3339)
- Use node prefix for build-in modules (#3340)
- Rework logging colors (#3350)
- Update pm2 to v5.3.1 with no allow-ghsas (#3364)
- [chore] Update husky and let lint-staged fix ESLint issues
- [chore] Update dependencies including electron to v29 (#3357) and
node-ical
- Update translations for estonian (#3371)
- Update electron to v29 and update other dependencies
- [calendar] fullDay events over several days now show the left days
from the first day on and 'today' on the last day
- Update layout of current weather indoor values

### Fixed

- Correct apibase of weathergov weatherprovider to match documentation
(#2926)
- Worked around several issues in the RRULE library that were causing
deleted calender events to still show, some
initial and recurring events to not show, and some event times to be off
an hour. (#3291)
- Skip changelog requirement when running tests for dependency updates
(#3320)
- Display precipitation probability when it is 0% instead of blank/empty
(#3345)
- [newsfeed] Suppress unsightly animation cases when there are 0 or 1
active news items (#3336)
- [newsfeed] Always compute the feed item URL using the same helper
function (#3336)
- Ignore all custom css files (#3359)
- [newsfeed] Fix newsfeed stall issue introduced by #3336 (#3361)
- Changed `log.debug` to `log.log` in `app.js` where logLevel is not set
because config is not loaded at this time (#3353)
- [calendar] deny fetch interval < 60000 and set 60000 in this case
(prevent fetch loop failed) (#3382)
- added message in case where config.js is missing the module.export
line PR #3383
- Fixed an issue where recurring events could extend past their
recurrence end date (#3393)
- Don't display any `npm WARN <....>` on install (#3399)
- Fixed move suncalc dependency to production from dev, as it is used by
clock module
- [compliments] Fix mirror not responding anymore when no compliments
are to be shown (#3385)

### Deleted

- Unneeded file headers (#3358)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Michael Teeuw <michael@xonaymedia.nl>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
Co-authored-by: Ross Younger <crazyscot@gmail.com>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: jkriegshauser <joshuakr@nvidia.com>
Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com>
Co-authored-by: sam detweiler <sdetweil@gmail.com>
Co-authored-by: vppencilsharpener <tim.pray@gmail.com>
Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
2024-04-01 22:03:20 +02:00
Michael Teeuw
118e21238c Merge branch 'develop' of https://github.com/MichMich/MagicMirror 2024-01-01 15:40:44 +01:00
Michael Teeuw
8c0e7db494 Release 2.26.0 (#3319)
## [2.26.0] - 01-01-2024

Thanks to: @bnitkin, @bugsounet, @dependabot, @jkriegshauser,
@kaennchenstruggle, @KristjanESPERANTO and @Ybbet.

Special thanks to @khassel, @rejas and @sdetweil for taking over most
(if not all) of the work on this release as project collaborators. This
version would not be there without their effort. Thank you guys! You are
awesome!

This release also marks the latest release by Michael Teeuw. For more
info, please read the following post: [A New Chapter for MagicMirror:
The Community Takes the
Lead](https://forum.magicmirror.builders/topic/18329/a-new-chapter-for-magicmirror-the-community-takes-the-lead).

### Added

- Added update notification updater (for 3rd party modules)
- Added node 21 to the test matrix
- Added transform object to calendar:customEvents
- Added ESLint rules for jest (including jest/expect-expect and
jest/no-done-callback)

### Removed

- Removed Codecov workflow (not working anymore, other workflow
required) (#3107)
- Removed titleReplace from calendar, replaced + extended by
customEvents (backward compatibility included) (#3249)
- Removed failing unit test (#3254)
- Removed some unused variables

### Updated

- Update electron to v27 and update other dependencies as well as github
actions
- Update newsfeed: Use `html-to-text` instead of regex for transform
description
- Review ESLint config (#3269)
- Updated dependencies
- Clock module: optionally display current moon phase in addition to
rise/set times
- electron is now per default started without gpu, if needed it must be
enabled with new env var `ELECTRON_ENABLE_GPU=1` on startup (#3226)
- Replace prettier by stylistic in ESLint config to lint JavaScript (and
disable some rules for `config/config.js*` files)
- Update node-ical to v0.17.1 and fix tests

### Fixed

- Avoid fade out/in on updateDom when many calendars are used
- Fix the option eventClass on customEvents.
- Fix yr API version in locationforecast and sunrise call (#3227)
- Fix cloneObject() function to respect RegExp (#3237)
- Fix newsfeed module for feeds using "a10:updated" tag (#3238)
- Fix issue template (#3167)
- Fix #3256 filter out bad results from rrule.between
- Fix calendar events sometimes not respecting deleted events (#3250)
- Fix electron loadurl locally on Windows when address "0.0.0.0" (#2550)
- Fix updatanotification (update_helper.js): catch error if reponse is
not an JSON format (check PM2)
- Fix missing typeof in calendar module
- Fix style issues after prettier update
- Fix calendar test (#3291) by moving "Exdate check" from e2e to
electron to run on a Thursday
- Fix calendar config params `fetchInterval` and `excludedEvents` were
never used from single calendar config (#3297)
- Fix MM_PORT variable not used in electron and allow full path for
MM_CONFIG_FILE variable (#3302)

---------

Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
Co-authored-by: Malte Hallström <46646495+SkySails@users.noreply.github.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: veeck <michael@veeck.de>
Co-authored-by: dWoolridge <dwoolridge@charter.net>
Co-authored-by: Johan <jojjepersson@yahoo.se>
Co-authored-by: Dario Mratovich <dario_mratovich@hotmail.com>
Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com>
Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com>
Co-authored-by: Naveen <172697+naveensrinivasan@users.noreply.github.com>
Co-authored-by: buxxi <buxxi@omfilm.net>
Co-authored-by: Thomas Hirschberger <47733292+Tom-Hirschberger@users.noreply.github.com>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com>
Co-authored-by: Dave Child <dave@addedbytes.com>
Co-authored-by: grenagit <46225780+grenagit@users.noreply.github.com>
Co-authored-by: Grena <grena@grenabox.fr>
Co-authored-by: Magnus Marthinsen <magmar@online.no>
Co-authored-by: Patrick <psieg@users.noreply.github.com>
Co-authored-by: Piotr Rajnisz <56397164+rajniszp@users.noreply.github.com>
Co-authored-by: Suthep Yonphimai <tomzt@users.noreply.github.com>
Co-authored-by: CarJem Generations (Carter Wallace) <cwallacecs@gmail.com>
Co-authored-by: Nicholas Fogal <nfogal.misc@gmail.com>
Co-authored-by: JakeBinney <126349119+JakeBinney@users.noreply.github.com>
Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com>
Co-authored-by: Oscar Björkman <17575446+oscarb@users.noreply.github.com>
Co-authored-by: Ismar Slomic <ismar@slomic.no>
Co-authored-by: Jørgen Veum-Wahlberg <jorgen.wahlberg@amedia.no>
Co-authored-by: Eddie Hung <6740044+eddiehung@users.noreply.github.com>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: bugsounet <bugsounet@bugsounet.fr>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Knapoc <Knapoc@users.noreply.github.com>
Co-authored-by: sam detweiler <sdetweil@gmail.com>
Co-authored-by: veeck <michael.veeck@nebenan.de>
Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
Co-authored-by: NolanKingdon <27908974+NolanKingdon@users.noreply.github.com>
Co-authored-by: J. Kenzal Hunter <kenzal.hunter@gmail.com>
Co-authored-by: Teddy <teddy.payet@gmail.com>
Co-authored-by: TeddyStarinvest <teddy.payet@starinvest.com>
Co-authored-by: martingron <61826403+martingron@users.noreply.github.com>
Co-authored-by: dgoth <132394363+dgoth@users.noreply.github.com>
Co-authored-by: kaennchenstruggle <54073894+kaennchenstruggle@users.noreply.github.com>
Co-authored-by: jkriegshauser <jkriegshauser@gmail.com>
Co-authored-by: Ben Nitkin <ben@nitkin.net>
2024-01-01 15:38:08 +01:00
veeck
d20d9a7ef8 Merge branch 'master' into develop 2024-01-01 15:33:31 +01:00
Michael Teeuw
b8e0e2a791 Release 2.26.0 2024-01-01 15:01:38 +01:00
Karsten Hassel
a927eb20d9 fix variable problems (#3304)
related to #3302

- fix MM_PORT variable not used in electron
- fix to allow full path for MM_CONFIG_FILE variable
2023-12-28 21:58:31 +01:00
Karsten Hassel
aad3eefc62 update node-ical to v0.17.1 (and other deps) (#3309)
- update `node-ical` to `v0.17.1` (and other deps)
- remove `luxon` (not needed anymore with new `node-ical` version)
- fix `navigator is not defined` errors in e2e tests when running with
Node `v20`
2023-12-28 19:54:49 +01:00
Karsten Hassel
ee1960ced0 change rule exceptions in .eslintrc (#3305)
for all files beginning with `config/config.js` so e.g. `config.js` and
`config.js.template` are included.

Otherwise the test will always fail locally if someone has renamed
`config.js.sample` to `config.js`.
2023-12-25 23:35:30 +01:00
Kristjan ESPERANTO
0b70274a1a Replace prettier by stylistic to lint JavaScript (#3303)
In the latest versions of ESLint, more and more formatting rules were
removed or declared deprecated. These rules have been integrated into
the new Stylistic package (https://eslint.style/guide/why) and expanded.

Stylistic acts as a better formatter  for JavaScript as Prettier.

With this PR there are many changes that make the code more uniform, but
it may be difficult to review due to the large amount. Even if I have no
worries about the changes, perhaps this would be something for the
release after next.

Let me know what you think.
2023-12-25 08:17:11 +01:00
Kristjan ESPERANTO
4e7b68a69d Remove some unused variables (#3301)
I have noticed unused variables that seem superfluous.
2023-12-23 07:27:02 +01:00
Veeck
786ea86e0e Cleanup calendar module (#3300)
- Update default calendar config to use customEvents
- Update url that is displayed when old authentication is used
2023-12-22 19:34:06 +01:00
Karsten Hassel
d397568062 fix calendar config (#3299)
params `fetchInterval` and `excludedEvents` were never used from single
calendar config.

Fixes #3297
2023-12-21 21:44:17 +01:00
Karsten Hassel
a7af76b619 fix calendar test exdate check (#3293)
fixes #3291
2023-12-17 07:19:15 +01:00
Karsten Hassel
319a921f75 start electron with --disable-gpu flag (#3290)
can be overriden with env var ELECTRON_ENABLE_GPU=1.

see #3226 

Tests will fail as long as
https://github.com/MichMich/MagicMirror/pull/3289 is not merged.
2023-12-13 22:49:35 +01:00
Karsten Hassel
d5406f4900 update deps and fix style issue in js/class.js (#3289)
see title, should be merged because style tests are failing, see
https://github.com/MichMich/MagicMirror/pull/3284#issuecomment-1854513673
2023-12-13 22:48:23 +01:00
Ben Nitkin
55cd03576f Show moon phase in clock module (#3284)
This change replaces the font-awesome moon icon and percent-lit with an
icon showing the current lunar phase.

It uses emoji, which may not be installed on all machines. The fallback
text version is backwards (the dark part of the moon is text-color,
which is normally black but white in MagicMirror).

---------

Co-authored-by: veeck <michael.veeck@nebenan.de>
2023-12-13 09:43:07 +01:00
Kristjan ESPERANTO
9d97724401 Fix missing typeof in calendar module (#3286)
I think comparing here with "undefined" (as a string) makes only sense
with typeof.
2023-12-07 08:11:56 +01:00
Bugsounet - Cédric
74854387cd Fix updatenotification (#3281)
Sometime, `pm2 jlist` don't send an json reponse
Catch error if happen

---------

Co-authored-by: bugsounet <bugsounet@bugsounet.fr>
2023-12-04 07:34:14 +01:00
Veeck
e77f10b86d Update dependencies (#3280)
Took the ones from dependabot and updated the rest...

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 19:14:37 +01:00
Karsten Hassel
6ffdc7b55b enable eslint jest/expect-expect and jest/no-done-callback (#3272)
follow up to https://github.com/MichMich/MagicMirror/pull/3270
2023-11-22 23:37:17 +01:00
Kristjan ESPERANTO
7098f1e41f Enable and apply ESLint Jest rules (#3270)
Jest was in the plugin array of the ESLint configuration, but no rules
were enabled. So ESLint hasn't checked any Jest rules yet.

So I activated the recommended Jest rules and added a few more. Then I
fixed the issues (mostly automatically). I have deactivated the rules
"jest/expect-expect" and "jest/no-done-callback" for the time being, as
they would have entailed major changes. I didn't want to make the PR too
big.

I'm not a Jest expert, but the changes so far look good to me. What do
you think of that @khassel? 🙂
2023-11-20 20:11:41 +01:00
Kristjan ESPERANTO
679a413788 Review eslint config (#3269)
- Remove "prettier" from plugin array, because it's already enabled by
"plugin:prettier/recommended"
- Remove "jsdoc" from plugin array, because it's already enabled by
"plugin:jsdoc/recommended"
- Enable recommended import rules
- Add two additional import rules

Note: To avoid overloading this PR I'll tackle the jest part with
another PR after this one has been dealt with.
2023-11-20 08:03:29 +01:00
sam detweiler
247115d2e4 fix electron start loadurl on windows when address="0.0.0.0" (#3268)
> - Does the pull request solve a **related** issue?
Fixes #2550 

> - What does the pull request accomplish? Use a list if needed.

changes the loadUrl to use localhost, as electron and MM are on this
same system
the mm 'server' is still listening on all adapters, including localhost
2023-11-15 19:44:08 +01:00
Bugsounet - Cédric
70ddd80632 Use html-to-text instead of regex for transform description (#3264)
I try to use only `html-to-text` library

it's will solve issue #3235 

@rejas, @sdetweil, @khassel: Can you do tests with your own feeds?

Thanks for feedbacks
2023-11-11 09:13:16 +01:00
Bugsounet - Cédric
203e8647d4 3rd party modules updater for updatenotification (#3150)
Added my (modified) updater main core into updatenotification default
module

Missing: callback display in MM² (i will code it after)

new part of configuration added:

```
		updates: [
			// array of module update commands
			{
				// with embed npm script
				"MMM-Test": "npm run update"
			},
			{
				// with "complex" process
				"MMM-OtherSample": "rm -rf package-lock.json && git reset --hard && git pull && npm install"
			},
			{
				// with git pull && npm install
				"MMM-OtherSample2": "git pull && npm install"
			},
			{
				// with a simple git pull
				"MMM-OtherSample3": "git pull"
			}
		],
		updateTimeout: 2 * 60 * 1000, // max update duration
		updateAutorestart: false // autoRestart MM when update done ?
```

@khassel: i need your help
I don't use docker, maybe you can help me for this:
How can i check if MM² is running inside a docker ? (from MM² main core)
Actually, I check if we use pm2 or not.
I have to check if docker is used or not too
last time you tell me: "you can't use updater with docker", so I want to
check and deny any update if docker used

---------

Co-authored-by: bugsounet <bugsounet@bugsounet.fr>
2023-11-10 12:43:34 +01:00
Karsten Hassel
3fe5ad4b3d remove failing unit test (#3265)
see
https://github.com/MichMich/MagicMirror/issues/3254#issuecomment-1800120812
2023-11-09 22:57:15 +01:00
sam detweiler
b300191609 fix crash on rrule.between returned bad dates #3256 (#3257)
Fixes: #3256 

BUT.. the testcase is inconclusive.. as the code FAILS without the fix,
BUT somehow RETURNS 0 entries..
in real life run the node helper fails, and all calendar processing
stops.
2023-11-07 19:48:00 +01:00
dependabot[bot]
296df06c21 Bump eslint-plugin-jest from 27.4.3 to 27.6.0 (#3260)
Bumps
[eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest)
from 27.4.3 to 27.6.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/jest-community/eslint-plugin-jest/releases">eslint-plugin-jest's
releases</a>.</em></p>
<blockquote>
<h2>v27.6.0</h2>
<h1><a
href="https://github.com/jest-community/eslint-plugin-jest/compare/v27.5.0...v27.6.0">27.6.0</a>
(2023-10-26)</h1>
<h3>Features</h3>
<ul>
<li>include plugin <code>meta</code> information for ESLint v9 (<a
href="https://redirect.github.com/jest-community/eslint-plugin-jest/issues/1454">#1454</a>)
(<a
href="4d57146763">4d57146</a>)</li>
</ul>
<h2>v27.5.0</h2>
<h1><a
href="https://github.com/jest-community/eslint-plugin-jest/compare/v27.4.3...v27.5.0">27.5.0</a>
(2023-10-26)</h1>
<h3>Features</h3>
<ul>
<li><strong>valid-title:</strong> allow ignoring tests with non-string
titles (<a
href="https://redirect.github.com/jest-community/eslint-plugin-jest/issues/1460">#1460</a>)
(<a
href="ea89da9b4e">ea89da9</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md">eslint-plugin-jest's
changelog</a>.</em></p>
<blockquote>
<h1><a
href="https://github.com/jest-community/eslint-plugin-jest/compare/v27.5.0...v27.6.0">27.6.0</a>
(2023-10-26)</h1>
<h3>Features</h3>
<ul>
<li>include plugin <code>meta</code> information for ESLint v9 (<a
href="https://redirect.github.com/jest-community/eslint-plugin-jest/issues/1454">#1454</a>)
(<a
href="4d57146763">4d57146</a>)</li>
</ul>
<h1><a
href="https://github.com/jest-community/eslint-plugin-jest/compare/v27.4.3...v27.5.0">27.5.0</a>
(2023-10-26)</h1>
<h3>Features</h3>
<ul>
<li><strong>valid-title:</strong> allow ignoring tests with non-string
titles (<a
href="https://redirect.github.com/jest-community/eslint-plugin-jest/issues/1460">#1460</a>)
(<a
href="ea89da9b4e">ea89da9</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="6dfbf15d02"><code>6dfbf15</code></a>
chore(release): 27.6.0 [skip ci]</li>
<li><a
href="4d57146763"><code>4d57146</code></a>
feat: include plugin <code>meta</code> information for ESLint v9 (<a
href="https://redirect.github.com/jest-community/eslint-plugin-jest/issues/1454">#1454</a>)</li>
<li><a
href="55ad33675d"><code>55ad336</code></a>
chore: update <code>moduleResolution</code> and <code>module</code> to
<code>node16</code> (<a
href="https://redirect.github.com/jest-community/eslint-plugin-jest/issues/1455">#1455</a>)</li>
<li><a
href="9cc95920ea"><code>9cc9592</code></a>
chore: replace <code>eslint-plugin-node</code> with
<code>eslint-plugin-n</code> (<a
href="https://redirect.github.com/jest-community/eslint-plugin-jest/issues/1462">#1462</a>)</li>
<li><a
href="1d5bdd1391"><code>1d5bdd1</code></a>
chore(release): 27.5.0 [skip ci]</li>
<li><a
href="ea89da9b4e"><code>ea89da9</code></a>
feat(valid-title): allow ignoring tests with non-string titles (<a
href="https://redirect.github.com/jest-community/eslint-plugin-jest/issues/1460">#1460</a>)</li>
<li><a
href="f2af5194bd"><code>f2af519</code></a>
chore: run CI on Node 21 (<a
href="https://redirect.github.com/jest-community/eslint-plugin-jest/issues/1461">#1461</a>)</li>
<li><a
href="d8b10b47e7"><code>d8b10b4</code></a>
chore: update permissions granted on CI</li>
<li><a
href="4295882c21"><code>4295882</code></a>
chore(deps): lock file maintenance</li>
<li>See full diff in <a
href="https://github.com/jest-community/eslint-plugin-jest/compare/v27.4.3...v27.6.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=eslint-plugin-jest&package-manager=npm_and_yarn&previous-version=27.4.3&new-version=27.6.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-01 19:48:50 +01:00
jkriegshauser
fe882bf92a Fix issue #3250: Respect deleted (excluded) calendar events (#3251)
Hello and thank you for wanting to contribute to the MagicMirror²
project

**Please make sure that you have followed these 4 rules before
submitting your Pull Request:**

> 1. Base your pull requests against the `develop` branch.
> 2. Include these infos in the description:
>
> - Does the pull request solve a **related** issue?
> - If so, can you reference the issue like this `Fixes
#<issue_number>`?
> - What does the pull request accomplish? Use a list if needed.
> - If it includes major visual changes please add screenshots.
>
> 3. Please run `npm run lint:prettier` before submitting so that
>    style issues are fixed.
> 4. Don't forget to add an entry about your changes to
>    the CHANGELOG.md file.

**Note**: Sometimes the development moves very fast. It is highly
recommended that you update your branch of `develop` before creating a
pull request to send us your changes. This makes everyone's lives
easier (including yours) and helps us out on the development team.

Thanks again and have a nice day!
2023-11-01 07:42:47 +01:00
kaennchenstruggle
2a6e2aacdc Calendar translate (#3249)
Hello and thank you for wanting to contribute to the MagicMirror²
project

**Please make sure that you have followed these 4 rules before
submitting your Pull Request:**

> 1. Base your pull requests against the `develop` branch.

DONE ;D

> 2. Include these infos in the description:
> - Does the pull request solve a **related** issue?

NO
> - What does the pull request accomplish? Use a list if needed.

For calendar entries containing a year (e.g. DOB) in the title, the age
can be calculated.

Example before:

![grafik](https://github.com/MichMich/MagicMirror/assets/54073894/67ca65f4-24c3-46a8-bee8-0519e4bba3f5)
after:

![grafik](https://github.com/MichMich/MagicMirror/assets/54073894/0b4af07d-d3d9-4644-a4a6-e8c402598208)

Achieved by adding a new keyword `transform` to customEvents
```
customEvents: [
                        {keyword: 'Geburtstag', symbol: 'birthday-cake', color: 'Gold', transform: { search: '^([^\']*) \'(\\d{4})$' , replace: '$1 ($2.)', yearmatchgroup: 2}},
                        {keyword: 'in Hamburg', transform: { search: ' in Hamburg$' , replace: ''}} 
		],
```

and therewith obsoleting `titleReplace`; a backward compatibility part
is already included.

If `yearmatchgroup` is unset, behaviour is as in previous code (some
additions to which RegExes are accepted, though)

If `yearmatchgroup` is set, it is considered the RegEx match group id,
which will be used for calculating the age.



> - If it includes major visual changes please add screenshots.

NO

> 3. Please run `npm run lint:prettier` before submitting so that style
issues are fixed.

DONE

> 4. Don't forget to add an entry about your changes to the CHANGELOG.md
file.

DONE

> Thanks again and have a nice day!

You too and if any questions, feel free to let me know.

---------

Co-authored-by: veeck <michael.veeck@nebenan.de>
2023-11-01 00:07:56 +01:00
Karsten Hassel
3a01acd389 fix for failing unit test, use UTC as timezone (#3254) (#3259)
fixes #3254
2023-11-01 00:02:53 +01:00
Karsten Hassel
a8d06ae74e hotfix for failing unit test (#3258)
I know this isn't a real solution, but it's the quickest way to get it
working again.

A real solution must be found later.
2023-10-31 19:37:34 +01:00
Veeck
04f0df269a Fix yr weather provider api version (#3248)
Fixes #3227 once more
2023-10-24 00:46:25 +02:00
Veeck
f80889d953 Update github test action (#3247)
... add node 21 to the tests and also update dependencies
2023-10-23 21:37:52 +02:00
Karsten Hassel
6815dfa02b fix ISSUE_TEMPLATE (#3243)
fix for #3167
2023-10-21 21:17:24 +02:00
Bugsounet - Cédric
bbc27f5ae2 Avoid fade out/in on updateDom when many calendars are used (#3220)
related to #3185 

* I have limited updated dom to one update
-> `updateDom()` is activated by a timer which resets when a new event
arrives (`CALENDAR_EVENTS`)

* I have set no speed to self update. I think it's not necessary
-> update it directly

If somebody can test and tell me result
In all case, I will patch my prod mirror for testing
2023-10-21 19:41:17 +02:00
Karsten Hassel
f46b226940 fix newsfeed module for feeds using "a10:updated" tag (#3242)
solves  #3238
2023-10-20 06:41:31 +02:00
Karsten Hassel
764ca3ac5c update electron to v27 (#3241)
and 
- update other deps
- update package-lock.json version to v3 in /vendor and /fonts
2023-10-19 23:44:31 +02:00
Karsten Hassel
0e2da630d5 fix cloneObject() function to respect RegExp (#3240)
fixes #3237
2023-10-19 22:31:02 +02:00
Veeck
a0b444d6c4 Fix API version in yr weather provider call (#3223)
Fixes #3227 

and also
- removes unused code
- de-duplicates code fragments
- fixes typos
- inlines code
- adds more weather util tests

@martingron and @Justheretoreportanissue would you be so kind to check
if I didnt mess anything up?
2023-10-15 13:25:44 +02:00
Karsten Hassel
5d2ddbd3dd removed Codecov workflow (not working anymore, other workflow required) (#3230)
see #3107
2023-10-13 07:41:35 +02:00
Teddy
b067711ede Event class bugfix (#3218)
Hello,

Bugfix. :-) The "break" line did not allow the "eventClass" option to
execute.
2023-10-03 12:24:50 +02:00
Michael Teeuw
66b29ec26e Prepare v2.26.0-develop 2023-10-01 20:17:06 +02:00
Michael Teeuw
343e7de7bd Release v2.25.0 (#3214)
## [2.25.0] - 2023-10-01

Thanks to: @bugsounet, @dgoth, @dependabot, @kenzal, @Knapoc,
@KristjanESPERANTO, @martingron, @NolanKingdon, @Paranoid93,
@TeddyStarinvest and @Ybbet.

Special thanks to @khassel, @rejas and @sdetweil for taking over most
(if not all) of the work on this release as project collaborators. This
version would not be there without their effort. Thank you guys! You are
awesome!

> ⚠️ This release needs nodejs version >= `v18`, older releases have
reached end of life and will not work!

### Added

- Added UV Index support to OpenWeatherMap
- Added 'hideDuplicates' flag to the calendar module
- Added `allowOverrideNotification` to weather module to enable sending
current weather objects with the `CURRENT_WEATHER_OVERRIDE` notification
to supplement/replace the current weather displayed
- Added optional AnimateCSS animate for `hide()`, `show()`,
`updateDom()`
- Added AnimateIn and animateOut in module config definition
- Apply AnimateIn rules on the first start
- Added automatic client page reload when server was restarted by
setting `reloadAfterServerRestart: true` in `config.js`, per default
`false` (#3105)
- Added eventClass option for customEvents on the default calendar
- Added AnimateCSS integration in tests suite (#3206)
- Added npm dependabot [Reserved to developer] (#3210)
- Added improved logging for calendar (#3110)

### Removed

- **Breaking Change**: Removed `digest` authentication method from
calendar module (which was already broken since release `2.15.0`)

### Updated

- Update roboto fonts to version v5
- Update issue template
- Update dev/dependencies incl. electron to v26
- Replace pretty-quick by lint-staged
(<https://github.com/azz/pretty-quick/issues/164>)
- Update engine node >=18. v16 reached it's end of life. (#3170)
- Update typescript definition for modules
- Cleaned up nunjuck templates
- Replace `node-fetch` with internal fetch (#2649) and remove
`digest-fetch`
- Update the French translation according to the English file.
- Update dependabot incl. vendor/fonts (monthly check)
- Renew `package-lock.json` for release

### Fixed

- Fix engine check on npm install (#3135)
- Fix undefined formatTime method in clock module (#3143)
- Fix clientonly startup fails after async added (#3151)
- Fix electron width/heigth when using xrandr under bullseye
- Fix time issue with certain recurring events in calendar module
- Fix ipWhiteList test (#3179)
- Fix newsfeed: Convert HTML entities, codes and tag in description
(#3191)
- Respect width/height (no fullscreen) if set in electronOptions
(together with `fullscreen: false`) in `config.js` (#3174)
- Fix: AnimateCSS merge hide() and show() animated css class when we do
multiple call
- Fix `Uncaught SyntaxError: Identifier 'getCorsUrl' has already been
declared (at utils.js:1:1)` when using `clock` and `weather` module
(#3204)
- Fix overriding `config.js` when running tests (#3201)
- Fix issue in weathergov provider with probability of precipitation not
showing up on hourly or daily forecast

---------

Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
Co-authored-by: Malte Hallström <46646495+SkySails@users.noreply.github.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: veeck <michael@veeck.de>
Co-authored-by: dWoolridge <dwoolridge@charter.net>
Co-authored-by: Johan <jojjepersson@yahoo.se>
Co-authored-by: Dario Mratovich <dario_mratovich@hotmail.com>
Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com>
Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com>
Co-authored-by: Naveen <172697+naveensrinivasan@users.noreply.github.com>
Co-authored-by: buxxi <buxxi@omfilm.net>
Co-authored-by: Thomas Hirschberger <47733292+Tom-Hirschberger@users.noreply.github.com>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com>
Co-authored-by: Dave Child <dave@addedbytes.com>
Co-authored-by: grenagit <46225780+grenagit@users.noreply.github.com>
Co-authored-by: Grena <grena@grenabox.fr>
Co-authored-by: Magnus Marthinsen <magmar@online.no>
Co-authored-by: Patrick <psieg@users.noreply.github.com>
Co-authored-by: Piotr Rajnisz <56397164+rajniszp@users.noreply.github.com>
Co-authored-by: Suthep Yonphimai <tomzt@users.noreply.github.com>
Co-authored-by: CarJem Generations (Carter Wallace) <cwallacecs@gmail.com>
Co-authored-by: Nicholas Fogal <nfogal.misc@gmail.com>
Co-authored-by: JakeBinney <126349119+JakeBinney@users.noreply.github.com>
Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com>
Co-authored-by: Oscar Björkman <17575446+oscarb@users.noreply.github.com>
Co-authored-by: Ismar Slomic <ismar@slomic.no>
Co-authored-by: Jørgen Veum-Wahlberg <jorgen.wahlberg@amedia.no>
Co-authored-by: Eddie Hung <6740044+eddiehung@users.noreply.github.com>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: bugsounet <bugsounet@bugsounet.fr>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Knapoc <Knapoc@users.noreply.github.com>
Co-authored-by: sam detweiler <sdetweil@gmail.com>
Co-authored-by: veeck <michael.veeck@nebenan.de>
Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
Co-authored-by: NolanKingdon <27908974+NolanKingdon@users.noreply.github.com>
Co-authored-by: J. Kenzal Hunter <kenzal.hunter@gmail.com>
Co-authored-by: Teddy <teddy.payet@gmail.com>
Co-authored-by: TeddyStarinvest <teddy.payet@starinvest.com>
Co-authored-by: martingron <61826403+martingron@users.noreply.github.com>
Co-authored-by: dgoth <132394363+dgoth@users.noreply.github.com>
2023-10-01 20:13:41 +02:00
Michael Teeuw
6ea94e4512 Release v2.25.0 2023-10-01 20:01:14 +02:00
Bugsounet - Cédric
290b350856 Update last Dependencies before release and add dependabot to vendor/fonts directory (#3213)
Last check before release:

* update to electron: v2.26.4
* update eslint-plugins-jest: v27.4.2
* renew `package-lock.json`
* add dependabot to `vendor` and `fonts` directory (monthly check)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-30 15:18:56 +02:00
Bugsounet - Cédric
9566d6c9a0 Add npm dependabot (#3210)
Like mentioned
[there](https://github.com/MichMich/MagicMirror/pull/3207#issuecomment-1736181753)

I open an PR with npm dependabot (every monthly)

It might be interesting to have an overview every month

---------

Co-authored-by: Veeck <github@veeck.de>
2023-09-27 23:43:13 +02:00
Karsten Hassel
6b204cda25 calendar: add url to broadcast logging (#3211)
minimal solution for #3110
2023-09-27 23:37:10 +02:00
Bugsounet - Cédric
e530c783f8 Update Electron based on a severity vulnerability (develop) (#3207)
I just see `electron` package used in develop branch have `1 high
severity vulnerability`

Detail is [there](https://github.com/advisories/GHSA-j7hp-h8jx-5ppr)

We can patch it with electron v26.2.2 (last version at this day) and
will correct it

(ChangeLog is not needed in this case)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: veeck <michael.veeck@nebenan.de>
2023-09-26 23:12:09 +02:00
dgoth
a3c2e7b816 Fixed probability of precipitation in weathergov.js (#3195)
Fixes https://github.com/MichMich/MagicMirror/issues/3182

Fixed issue with probability of precipitation not showing up on hourly
or daily forecast

---------

Co-authored-by: veeck <michael.veeck@nebenan.de>
2023-09-25 22:42:27 +02:00
Bugsounet - Cédric
ad665a7a33 AnimateCSS integration in tests suite (#3206)
Hi,

Just because, i never try to code a test
I purpose to supervise this work

After, perhaps it is not necessary to integrate it in develop branch. 
It's up to you to decide
2023-09-25 22:27:52 +02:00
Karsten Hassel
95ec3096e0 avoid overriding config.js when running tests (#3205)
solves #3201
2023-09-22 14:45:46 +02:00
Teddy
a67a0b677c Add eventClass for customEvents in calendar (#3193)
Hello,
This pull request allows you to add a class to the tr of the event
sought in customEvents. You must enter the class with the "eventClass"
option.

---------

Co-authored-by: TeddyStarinvest <teddy.payet@starinvest.com>
2023-09-20 22:04:41 +02:00
martingron
8b1c279c07 Update yr provider to new api (#3197)
Some changes after yr api was deprecated and replaced with a new one.

Fixes #3189
2023-09-20 11:37:01 +02:00
Bugsounet - Cédric
4eccce3f77 Fix: AnimateCSS merge hide() and show() animated css class when we do multiple call (#3200)
PR: #3113  

I see this bugs:

AnimateCSS merge hide() and show() animated css class when we do
multiple call
--> result it will stay en hide state

I think event listener (is animateCSS file) is not a proper solution

I correct it with like traditional code with timer

Fix too: AnimateIn on first start
2023-09-19 20:14:35 +02:00
Bugsounet - Cédric
af0fe37f70 Fix: Uncaught SyntaxError: Identifier 'getCorsUrl' has already been declared (#3204)
Issue #3202 

move shared modules/default/utils.js file to index.html
2023-09-19 07:14:11 +02:00
Teddy
e5adbea49c Update french translation (#3194)
Updated the French translation according to the English file.

---------

Co-authored-by: TeddyStarinvest <teddy.payet@starinvest.com>
2023-09-14 15:32:24 +02:00
Karsten Hassel
7127979c6f electron: add missing fullscreen option (#3192)
follow up for https://github.com/MichMich/MagicMirror/pull/3187

@bugsounet can you please confirm that this now works for you?
2023-09-14 08:01:50 +02:00
Karsten Hassel
fa7c7fc8cf respect width/height (no fullscreen) if set in electronOptions... (#3187)
... in `config.js`.

Solves #3174 

With getting width/heigt from
`electron.screen.getPrimaryDisplay().workAreaSize` introduced with
https://github.com/MichMich/MagicMirror/pull/3161 the solution was to
remove the `setFullscreen` line.

So per default the fullscreen resolution is used but when someone now
uses `electronOptions.width`/`electronOptions.height` in `config.js`
these parameters are used and so "no fullscreen" is possible.
2023-09-13 22:59:36 +02:00
Bugsounet - Cédric
91fd931a58 Convert HTML entities, codes and tag (#3191)
related to PR [#3092](https://github.com/MichMich/MagicMirror/pull/3092)

maybe best way is using `html-to-text` library

sample: `&quotHello World&quot` will be transformed to `"Hello World"`
2023-09-13 22:47:07 +02:00
Karsten Hassel
7a1591b2d6 added automatic client page reload (#3188)
solution for #3105 

~~not sure if updatenotification is the right place, so opinions?~~

now impleneted in `main.js`
2023-09-13 22:46:17 +02:00
Karsten Hassel
f2957f90df use internal fetch as replacement for node-fetch (#3184)
related to #2649

I was able to move to internal fetch and all tests seems fine so far.

But we have one problem with the calendar module. In the docs we have
several authentication methods and one of them is `digest`. For this we
used `digest-fetch` which needs `node-fetch` (this is not so clear from
code but I was not able to get it working).

So we have 3 options:
- remove `digest` as authentication method for calendar module (this is
what this PR does at the moment)
- find an alternative npm package or implement the digest stuff
ourselves
- use `digest-fetch` and `node-fetch` for calendar module (so they would
remain as dependencies in `package.json`)

Opinions? @KristjanESPERANTO @rejas @sdetweil @MichMich
2023-09-09 21:12:31 +02:00
Bugsounet - Cédric
ffdf321e23 Mistake on Changelog (#3186)
Move AnimateCSS changeLog from  v2.24 to  v2.25
2023-09-09 10:38:19 +02:00
J. Kenzal Hunter
79e99e18ea Cross UTC time fix (#3175)
Update calendarfetcherutils.js to force recurrence date time to be the
same as event datetime

I found an issue with one of my calendars displaying the wrong time for
certain recurring events. Each event was set up by someone in a
different timezone (Central European) than my own (Eastern US). I traced
the issue back to the `Rrule.between()` method generating odd time
portions under certain circumstances.

The fix I found was to set the UTC time portion of the recurrence
datetime to be the same as the UTC time portion of the event start date.

This resolved the issues with the maladjusted event times, and had no
effect on other event times. While there may be edge cases that are
affected, I have been unable to locate any.

---------

Co-authored-by: Veeck <github@veeck.de>
2023-09-08 07:44:49 +02:00
Bugsounet - Cédric
a92b3d3f71 Add AnimateCSS (#3113)
Hi,

This is my testing code for AnimateCSS for `show()`, `hide()`,
`updateDom()`

Naturally, we have to do better !

I voluntarily modify `newsfeed` and `compliments` in order to test

Note: I will correct checks later... it's a test...

---------

Co-authored-by: bugsounet <bugsounet@bugsounet.fr>
Co-authored-by: veeck <michael.veeck@nebenan.de>
2023-09-08 07:43:39 +02:00
NolanKingdon
5cbdd28db3 Added Override Notification Option to Weather Module (#3178)
Fixes #3126 

Added the option `allowOverrideNotification` to `weather.js`. This
allows the module to receive the `CURRENT_WEATHER_OVERRIDE`
notification. The expected payload for this notification is a
full/partial `weatherObject` that is used to supplement/replace the
existing `weatherObject` returned by whichever weather provider is in
use.

No visual changes.

First time contributing - let me know if I've missed something
🙂

---------

Co-authored-by: veeck <michael.veeck@nebenan.de>
2023-09-08 07:41:20 +02:00
Kristjan ESPERANTO
9d49196e69 Fix ipWhiteList test (#3181)
Port change seems to fix a timing issue.
2023-09-06 23:53:02 +02:00
Veeck
ef20fe2d11 Update dependencies and actions (#3179)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-05 22:15:45 +02:00
Veeck
c0a5f35a00 Cleanup nunjuck templates (#3109)
Ran a linter over it (djlint) which fixed intendation and a few errors

---------

Co-authored-by: veeck <michael@veeck.de>
2023-09-02 22:18:57 +02:00
Paranoid93
2ad463b6c7 fix calendar not showing events with the same name and start date but different calendar url (#3166)
I fixed the calendar module, which did not show calendar entries from
different calendars that share the same name and start date.

My use case: We have each office days documented in each an own
calendar. If both "Office" Calendar entries start at the same date just
one was shown

Google Calendar (each Office event in an own calendar)

![260753381-c8d5aedf-3c11-4d91-83e8-8549eb261e58](https://github.com/MichMich/MagicMirror/assets/6515818/0cb0ecbd-65bb-4ec4-b5e6-b011cb1c9c6b)

Before

![260751994-b308d549-fcb9-406e-9419-cdd2fed96dc6](https://github.com/MichMich/MagicMirror/assets/6515818/ed4d4645-0852-4e19-99ed-fec3f25d547a)

After

![260753208-3278e32b-9ca5-483a-bc6f-745cbf3964fc](https://github.com/MichMich/MagicMirror/assets/6515818/41b70843-af9f-47fc-baed-91c3c63e9acb)
2023-08-26 13:53:41 +02:00
Veeck
200db181d5 Update typescript definition (#3173) 2023-08-22 20:52:10 +01:00
Bugsounet - Cédric
7ba96aeb98 Add Npmrc (#3135)
Fix engines check on npm install

it will alow to check engine requirement of `package.json`

actually:
```js
	"engines": {
		"node": ">=16"
	}
```

if requirements are not suitable, it's break installer

```sh
bugsounet@Kubuntu:~/MagicMirror-dev$ npm install
npm ERR! code EBADENGINE
npm ERR! engine Unsupported engine
npm ERR! engine Not compatible with your version of node/npm: magicmirror@2.24.0-develop
npm ERR! notsup Not compatible with your version of node/npm: magicmirror@2.24.0-develop
npm ERR! notsup Required: {"node":">=16"}
npm ERR! notsup Actual:   {"npm":"x.y.z","node":"v14.xx.yy"}
```

actually, it's just a warn:
```sh
bugsounet@KUbuntu:~/MagicMirror-dev$ npm install
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'magicmirror@2.24.0-develop',
npm WARN EBADENGINE   required: { node: '>=16' },
npm WARN EBADENGINE   current: { node: 'v14.xx.yy', npm: 'x.y.z' }
npm WARN EBADENGINE }
...
```
and don't break installation, this can causes errors in the main core of
MM²

---------

Co-authored-by: veeck <michael.veeck@nebenan.de>
2023-08-20 13:14:21 +02:00
Kristjan ESPERANTO
7c64d8fce6 Minimum node version v18 (#3170)
When the next MagicMirror version is released, node v16 will have
reached its end of life.
2023-08-20 12:55:17 +02:00
Kristjan ESPERANTO
7dcea98e99 Handle pretty-quick issue (#3169)
- Replace pretty-quick by lint-staged (to fix:
<https://github.com/azz/pretty-quick/issues/164>)
- Update prettier dependencies
- Handle prettier issues
2023-08-16 14:21:02 +02:00
Karsten Hassel
59e9d765e2 update to electron v26 (#3168)
- tested on rpi4
- electron tests are fine
2023-08-16 00:05:32 +02:00
Karsten Hassel
156db32c76 fix electron width/heigth when using xrandr under bullseye (#3161)
This PR uses `electron.screen.getPrimaryDisplay().workAreaSize` under
electron to get the screen size.

If this fails the current defaults (800x600) are used.

This solves some problems with xrandr under bullseye where the sreen
comes up with 800x600 instead of fullscreen, e.g. described
[here](https://khassel.gitlab.io/magicmirror/pi-modules/).

Tested this on my pi setup.

By the way, the Issue #1919 is back on my side (not related to this PR)
...
2023-08-12 22:57:42 +02:00
Karsten Hassel
58cdfa3cb1 update dependencies (except prettier v3) (#3160) 2023-08-12 22:55:09 +02:00
Veeck
49c72d8dfc Update dependencies (#3155)
Takes https://github.com/MichMich/MagicMirror/pull/3153 (why does that
always base against master despite the yaml configuring it against
develop?) and updates othe rminor dependencies (not prettier v3 that
will take a little time)

---------

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-16 23:16:53 +02:00
sam detweiler
1bd146f52e fix clientOnly not starting up after async startup added in 2.23 (#3154)
Fixes #3151 

app.whenReady() was removed when async changes made, needed for
clientonly in electron.js
2023-07-16 23:04:58 +02:00
Karsten Hassel
948910d304 add section for foreign modules in issue template (#3149) 2023-07-05 20:22:19 +02:00
Karsten Hassel
0b97639341 update roboto fonts to v5 (#3121)
there are no "all" files in the v5 versions of roboto-fonts anymore so I
used the content of their css files in `roboto.css`.

~~PR is set to draft because @rejas wants to wait until next release is
done.~~
2023-07-03 00:01:12 +02:00
Veeck
f802c85a38 Fix undefined error for showSunTime / showMoonTime in clock module (#3146)
Fixes #3143 and adds tests for showSunTime / showMoonTime
2023-07-02 22:10:58 +02:00
Knapoc
62eb23ba6a Add openweathermap UV index support (#3145)
This PR adds UV index support to the openweathermap provider for the One
Call api.
2023-07-02 21:47:25 +02:00
Michael Teeuw
4b0e0aa48f Prepare 2.25.0-develop branch. 2023-07-01 21:24:52 +02:00
Michael Teeuw
e9f1bd9d7a Merge branch 'master' into develop 2023-07-01 21:18:12 +02:00
Michael Teeuw
e87f50e64a Release 2.24.0 (#3141)
Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
Co-authored-by: Malte Hallström <46646495+SkySails@users.noreply.github.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: veeck <michael@veeck.de>
Co-authored-by: dWoolridge <dwoolridge@charter.net>
Co-authored-by: Johan <jojjepersson@yahoo.se>
Co-authored-by: Dario Mratovich <dario_mratovich@hotmail.com>
Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com>
Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com>
Co-authored-by: Naveen <172697+naveensrinivasan@users.noreply.github.com>
Co-authored-by: buxxi <buxxi@omfilm.net>
Co-authored-by: Thomas Hirschberger <47733292+Tom-Hirschberger@users.noreply.github.com>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com>
Co-authored-by: Dave Child <dave@addedbytes.com>
Co-authored-by: grenagit <46225780+grenagit@users.noreply.github.com>
Co-authored-by: Grena <grena@grenabox.fr>
Co-authored-by: Magnus Marthinsen <magmar@online.no>
Co-authored-by: Patrick <psieg@users.noreply.github.com>
Co-authored-by: Piotr Rajnisz <56397164+rajniszp@users.noreply.github.com>
Co-authored-by: Suthep Yonphimai <tomzt@users.noreply.github.com>
Co-authored-by: CarJem Generations (Carter Wallace) <cwallacecs@gmail.com>
Co-authored-by: Nicholas Fogal <nfogal.misc@gmail.com>
Co-authored-by: JakeBinney <126349119+JakeBinney@users.noreply.github.com>
Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com>
Co-authored-by: Oscar Björkman <17575446+oscarb@users.noreply.github.com>
Co-authored-by: Ismar Slomic <ismar@slomic.no>
Co-authored-by: Jørgen Veum-Wahlberg <jorgen.wahlberg@amedia.no>
Co-authored-by: Eddie Hung <6740044+eddiehung@users.noreply.github.com>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: bugsounet <bugsounet@bugsounet.fr>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-01 21:17:31 +02:00
Michael Teeuw
46bca1bc6d Merge branch 'master' into develop 2023-07-01 20:59:34 +02:00
Michael Teeuw
2b6720e6e5 Prepare 2.24.0 release. 2023-07-01 20:34:43 +02:00
Veeck
ea818bf899 Fix fetchInterval for sample calendar (#3139)
fixes #3138 just in case #3137 doesnt get worked on until the next
release

- inc fetchInterval for sample calender
- add fetchInterval config per calendar
2023-06-30 20:13:07 +02:00
Karsten Hassel
0e00e64493 update dependencies (#3140)
last dependency update before next release
2023-06-29 22:59:43 +02:00
Karsten Hassel
3c35d346ee fix updatenotification where no branch is checked out ... (#3136)
... but e.g. a version tag

fixes #3130 

This happens e.g. in my docker image where I use the version tag to get
the mm sources.

With this PR the error message is avoided and there will be never an
updatenotification when using a tag. This is o.k. because a tag should
never be moved.
2023-06-27 11:18:16 +02:00
Veeck
675e4d4f67 Update dependencies and fix dependabot issues (#3134)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-25 20:12:55 +02:00
Bugsounet - Cédric
c1850f2577 Remote force check update (#3127)
updatenotification: 

allow force scanning with `SCAN_UPDATES` notification from other modules
2023-06-18 14:33:03 +02:00
Bugsounet - Cédric
e985e99036 Updates notification (#3119)
Hi,

Like some default modules, I propose to send an `UPDATES` notification
in an array with the git information of these modules

This allows developers to create their own auto-update system (which
I've been using in my case since 3 years, with automatic things)

Of course, for security reasons `MagicMirror` is excluded

---------

Co-authored-by: bugsounet <bugsounet@bugsounet.fr>
2023-06-08 22:41:48 +02:00
Karsten Hassel
b7371538bc update deps and remove stylelint fix (#3120)
~~there are no "all" files in the v5 versions of roboto-fonts anymore so
I used the content of their css files in `roboto.css`~~

coming with separate PR
2023-06-08 22:40:23 +02:00
Karsten Hassel
a56b92990d update deps incl. electron to v25, fix stylelint segmentation dump (#3118)
- update electron to v25
- added `overrides:` section to `package.json` to fix
https://github.com/stylelint/stylelint/issues/6898 (must be removed when
fixed upstream, fixing the version to `v15.6.2` did not fix the problem)
- update other deps
2023-06-06 10:16:24 +02:00
Veeck
c7405b76b3 Split install and run commands in github actions (#3112)
... makes looking at the checks a little easier in case they fail

---------

Co-authored-by: veeck <michael@veeck.de>
2023-05-27 22:22:26 +02:00
OWL4C
eceec8285d Adding experimental UV Index support for weather module (#3108)
This pr adds (config.js toggleable) showUV_Index to the hourly and
current weather modules, with right now only openmeteo configured to
supply the data. Other providers could have support too by adding
`uv_index` to current and hourly.

For example the current weather looks like
![image](https://github.com/MichMich/MagicMirror/assets/124401812/00fdf5db-c0d5-4797-9a31-1d72dd970d26)
positioned after sunset in the top row.



The following "hacks" are included and could be fixed to make it
cleaner, but the functionality is wanted and it works without problem.
- To hide entries where the UV Index is 0 i added an if statement to the
`hourly.njk` which is not how precipitation is handled.
The following are minor things that might not need fixing:
- The forecast option does not have UV support. This might not be
relevant since UV changes throughout the day but i tried to implement a
"max_UV" to openmeteo.js, but am not confident enough in JS to
accomplish that.
- The UV Icon is wi-hot and manually added to the `.njk`'s. This could
be made changeable by a config but does not seem relevant since wi-hot
is not used by anything else as far as i can tell.

---------

Co-authored-by: veeck <michael@veeck.de>
2023-05-22 10:57:48 +02:00
Eddie Hung
0573d6e772 Fix envcanada today pop (#3106)
Fix for today's probability-of-precipitation weather module's envcanada
provider.

Without which, requesting `showPrecipitationProbability` causes
"undefined" to be shown.

Consistent with method used elsewhere in the file:

abe5c08a52/modules/default/weather/providers/envcanada.js (L395-L399)
2023-05-21 10:51:13 +02:00
Jørgen Veum-Wahlberg
babd22b04f Bug fix: adding date to the analog clock if showDate is true (#3101)
Adding date to the clock module when displayType is "analog" and
"showDate" is true. The setting in analogShowDate is respected.

Fixes #3100

---------

Co-authored-by: Michael Teeuw <michael@xonaymedia.nl>
Co-authored-by: Veeck <github@veeck.de>
2023-05-19 14:52:59 +02:00
Veeck
432d900ecd Add tests for some weather utils (#3103)
Co-authored-by: veeck <michael@veeck.de>
2023-05-15 21:04:50 +02:00
Ismar Slomic
83315f1fed fix: don't filter out ongoing full day events in Calendar module (#3095) (#3096)
Fixes #3095.

Param `hideOngoing` is by default set to `false`, but the event
filtering handles `full day` & `non-full day` events inconsistently. For
`non-full day` _ongoing_ and _upcoming_ events are returned, while for
`full day` only _upcoming_ events where returned.
2023-05-15 20:16:33 +02:00
Oscar Björkman
e09d60d1d1 Use single config backup file when config.template is used (#3104)
Copy `config.js` to a single `config.js-old` file whenever a
`config.template` is present, instead of using `Date()` as a suffix.
Creating files with a timestamp suffix means that whenever Magic Mirror
is restarted or recreated a new file is written into the config
directory.

Benefits:
* Single backup file will avoid excessive writing of files
* Saves space and usage on SD cards.
* Makes the folder cleaner and easier to overview, compared to ending up
with something like this as time goes on:

![image](https://github.com/MichMich/MagicMirror/assets/17575446/9b66a99c-e760-471e-884c-9daa19689702)
2023-05-15 20:11:23 +02:00
OWL4C
d832d795df Add openmeteo precipitation probability (#3099)
Due to the size of the commit there is no need for a long explanation:
Openmeteo supports precipitation probability in api, but the weather
provider .js had no support for it. After adding 4 lines it works as
expected. This should have no consequences on other files but improves
usability for (imo) the best weather provider.

---------

Co-authored-by: veeck <michael@veeck.de>
2023-05-13 09:29:00 +02:00
Karsten Hassel
a41aa48dd1 add .gitattributes and fix prettier/js warnings (#3094)
see title, as discussed in
https://github.com/MichMich/MagicMirror/pull/3093
2023-04-22 09:29:51 +02:00
Karsten Hassel
b80485b52f use node v20 in github workflow (replacing v19) (#3093)
additional:

- update deps
- suppress more unwanted log errors in e2e tests
- add .gitattributes
- fix prettier/js warnings
2023-04-22 09:29:23 +02:00
Veeck
7e58b38ddf Add no-param-reassign from eslint (#3089)
While waiting for the easterbunny I cleaned up some bad coding practice
:-)

Very open for comments especially regarding the places I commented
myself...

---------

Co-authored-by: veeck <michael@veeck.de>
2023-04-16 17:38:39 +02:00
Karsten Hassel
979f4ec664 fix electron not running under windows (after async changes) (#3091)
fixes #3083 

tested under windows 11 and linux.
2023-04-12 08:25:07 +02:00
Veeck
4e3369062e Fix envcanada hourly forecast time (#3090)
Fixes #3080

---------

Co-authored-by: veeck <michael@veeck.de>
2023-04-09 19:23:44 +02:00
Veeck
77f9c86774 Refactor calendar methods into util class (#3088)
Refactored some methods in calendar module:
- move methods into own file 
- dont call shorten method from titelTransform because why? just call
them after each other.
- added tests for util methods
- cleaned up other tests

---------

Co-authored-by: veeck <michael@veeck.de>
2023-04-09 12:49:50 +02:00
JakeBinney
dee3cd3da7 Fixed clock module sunrise/sunset timezone bug (#3070)
Updated sunrise/sunset times to display user-requested timezone rather
than system timezone.

Note: rebase of  #3069 against develop rather than master

Co-authored-by: Veeck <github@veeck.de>
2023-04-09 09:19:51 +02:00
Karsten Hassel
09f117c3d9 set Timezone Europe/Berlin in unit tests (#3087)
needed for new formatTime unit tests.

Avoiding ugly TZ setting in github workflows, see
https://github.com/MichMich/MagicMirror/pull/3073
2023-04-08 08:55:43 +02:00
Veeck
32192d1698 Refactor formatTime into util class (#3073)
While looking at https://github.com/MichMich/MagicMirror/pull/3070 I
noticed that the weather and clock module do some formatTime stuff, so
why not use a common function for that?

---------

Co-authored-by: veeck <michael@veeck.de>
2023-04-07 23:11:54 +02:00
Karsten Hassel
2c7beeaaaf added test for serveronly (#3086)
- add test for serveronly, fixes #3076 
- use template literals in port_variable.js.template
2023-04-07 19:42:46 +02:00
Kristjan ESPERANTO
0d3ad9812c Drop node v14 support (#3085)
node v14 will have reached end-of-life by the next release.

From April 18th we can use node v20 instead of v19 for testing.
2023-04-07 19:24:27 +02:00
Karsten Hassel
b7eb21e48f removed unneeded (and unwanted) '.' (#3084)
after the year in calendar repeatingCountTitle (#2896, second attempt
...)
2023-04-07 14:54:19 +02:00
Karsten Hassel
9703226c73 Update electron to v24, remove th.json from root dir (#3079)
- current electron v22 has end of life 07 Jul 2023 so it has to be
upgraded before next release
- removed th.json from root dir (came back with latest release)
- updated other dependencies
2023-04-05 07:43:42 +02:00
Michael Teeuw
cc11b77f24 Prepare v2.24.0-develop 2023-04-04 20:56:49 +02:00
Michael Teeuw
c5a8b85f4e Merge branch 'master' into develop 2023-04-04 20:34:31 +02:00
Michael Teeuw
fa40a3e8e8 Prepare release 2.23.0 2023-04-04 20:13:45 +02:00
Veeck
6223584392 Add sendNotifications option to clock module (#3059)
Fixes #3056 

One question here would be if the default for this new option should be
true or false.

True: keeps the current behaviour, nobody needs to change his config if
they rely on this option

False: keeps the clock notifications quiet, doesnt waste time/resources,
keeps the noise low

Maybe the original author @cybex-dev can weigh in on this, and why he
added this notification.

---------

Co-authored-by: veeck <michael@veeck.de>
2023-04-01 21:40:32 +02:00
Veeck
b5a22bc09b Add sendNotifications option to clock module (#3059)
Fixes #3056 

One question here would be if the default for this new option should be
true or false.

True: keeps the current behaviour, nobody needs to change his config if
they rely on this option

False: keeps the clock notifications quiet, doesnt waste time/resources,
keeps the noise low

Maybe the original author @cybex-dev can weigh in on this, and why he
added this notification.

---------

Co-authored-by: veeck <michael@veeck.de>
2023-04-01 21:40:05 +02:00
Kristjan ESPERANTO
4ef030af5f Fix import order in serveronly (#3072)
To fix issue caused by #3071

In order to still fulfill the new linter rule `import/order`, I replaced
the alias with the path.

I also see two other approaches, but I opted for the simplest one here
for now.

The other approaches:
1. Also create an alias for `app`.
2. Use new `imports` (like in PR #2934), but let `_moduleAliases`
untouched to stay compatible to 3rd party modules.

**Edit**: Oh, I thought of another option:
- Add `require("module-alias/register");`
2023-03-25 10:59:57 +01:00
Kristjan ESPERANTO
5f38c53260 Revise require imports (#3071)
- order (external first)
- remove superfluous file extensions
- new line after imports
- deconstruct (only one time (in `check_config.js`))
- fix path (only one time (in `global-setup.js`))
2023-03-22 23:53:10 +01:00
Karsten Hassel
d5395ee3f8 update dependencies and main.css to match new stylelint rules (#3067)
On `develop` my css test failed because I was already on newest
dependencies.

- update deps
- update `main.css` because of stylelint issue

Co-authored-by: Veeck <github@veeck.de>
2023-03-20 22:30:04 +01:00
Kristjan ESPERANTO
ab0876f07a Update Eslint config, add new rule and handle issue (#3068) 2023-03-20 22:18:58 +01:00
Kristjan ESPERANTO
d276a7ddb9 Use template literals instead of string concatenation (#3066)
We have used it inconsistently till now. Template literals are more
modern and easier to maintain in my opinion.

Because that's a large amount of changes, here's a way to reproduce it:
I added the rule `"prefer-template": "error"` to the `.eslintrc.json`
and did an autofix. Since this caused a new problem in line 409 of
`newsfeed.js`, I reversed it in that line and also removed the rule from
the eslint config file.

The rule is described here:
https://eslint.org/docs/latest/rules/prefer-template

Note: I've played around with some other linter rules as well, and some
seem to point to some specific, non-cosmetic, issues. But before I dive
even deeper and then introduce even bigger and hardly understandable
changes at once, I thought I'd start with this simple cosmetic rule.
2023-03-19 14:32:23 +01:00
Nicholas Fogal
8f8945d418 Issue#3064 html alert title message (#3065)
Fixes [#3064](https://github.com/MichMich/MagicMirror/issues/3064)

- Fixes default alert module nunjucks templates to render HTML by
default unless 'titleType' and 'messageType' are set to 'text' in the
payload data

e.g. 

Display Text:
`this.sendNotification('SHOW_ALERT', {type: "notification", title:
"<u>YoLink LeakSensor</u>", titleType: "text", message: "<b>" +
deviceName + "</b> reported an alarm that needs attention.",
messageType: "text"});`

Display HTML:
`this.sendNotification('SHOW_ALERT', {type: "notification", title:
"<u>YoLink LeakSensor</u>", message: "<b>" + deviceName + "</b> reported
an alarm that needs attention."});`
2023-03-19 13:23:37 +01:00
Karsten Hassel
6d779235cf fix e2e tests (failed after async changes) (#3063)
by running calendar and newsfeed tests last.

Additional change: allow unit tests to run parallel

This is no fix of the real issue of calendar and newsfeed tests but I
moved them to the end of the tests so other tests are not failing
anymore.

There are coming follow up PR's for the real fixes (when I find the
culprits).

With these change we can stay with the async changes done by @rejas and
https://github.com/MichMich/MagicMirror/pull/3060 is obsolete.
2023-03-12 10:22:23 +01:00
buxxi
beea754514 Fix empty news feed stopping the feed from ever updating again (#3062)
If a news feed returns an empty file it will cause the feed to only show
the previous entries forever since no new fetch is scheduled.
2023-03-11 16:42:13 +01:00
Karsten Hassel
c6db22524a set all calendar colums to vertical align top (#3055)
The title column was vertical centered before.

Fixes #3053
2023-03-06 22:59:37 +01:00
Andrés Vanegas Jiménez
23ee155ded Fixed wind speed units for Open-Meteo Weather Provider (#3054)
Resolved technical debt for
[2964](https://github.com/MichMich/MagicMirror/pull/2964):

- Set wind speed unit to m/s
- Rename parameter `past_days` to `pastDays` to be consistent with all
configs
2023-03-01 09:38:38 +01:00
Veeck
1b2785cc56 Cleanup more callback things (#3051)
Looks quite stable on my computers, so maybe we give it a try?

---------

Co-authored-by: veeck <michael@veeck.de>
2023-02-26 18:36:47 +01:00
Veeck
b5b61246e6 Convert load callbacks to async/await (#3050)
Another one of these PRs....
2023-02-22 20:03:05 +01:00
Veeck
498b440174 Convert module-start to async (#3049)
Similar to the node_helper async start PR...

---------

Co-authored-by: veeck <michael@veeck.de>
2023-02-22 18:58:29 +01:00
Veeck
fe0b915a5d Convert app-start/-stop callbacks to async/await (#3035)
supersedes https://github.com/MichMich/MagicMirror/pull/3027 and just
touches the start/stop calls.

---------

Co-authored-by: veeck <michael@veeck.de>
2023-02-22 18:58:00 +01:00
Veeck
2b792cdbb8 Convert translator callbacks to async/await (#3048)
Co-authored-by: veeck <michael@veeck.de>
2023-02-21 22:58:18 +01:00
Thomas Hirschberger
a23769156e Show events of a configurable amount of past days (#3046)
Hi,

want to include a birthday calendar to my mirror which shows upcoming
birthdays and as a reminder birthdays of the last two days.
I used
[MMM-CalendarExt2](https://github.com/MMM-CalendarExt2/MMM-CalendarExt2)
for this job in the past but the module is not supported any more and
very complicated to configure.
I managed to style the default calendar module to my needs but what i am
missing is to display already past events within a configurable time
range.

I included the translations of "YESTERDAY" and "DAYBEFOREYESTERDAY" to
all translation files and modified the code to accept a new option
`pastDaysCount` which controls of how many days past events should be
displayed.

---------

Co-authored-by: Veeck <github@veeck.de>
2023-02-21 22:39:21 +01:00
Veeck
6d86ffade4 Fix rounding in precipitation percentage (#3045)
Percentage should be always rounded so that we dont get something like
"47.0000000001 %"

Some small typo and naming fixes also while I am here

---------

Co-authored-by: veeck <michael@veeck.de>
2023-02-20 20:04:40 +01:00
Veeck
390e5d6490 Update thai language (#3044)
actually add it to the tanslation list
and also add the alert translations from
https://github.com/MichMich/MagicMirror/pull/3029 since the OP didnt
seem to work on it (yet)

---------

Co-authored-by: veeck <michael@veeck.de>
Co-authored-by: Suthep Yonphimai <tomzt@users.noreply.github.com>
2023-02-20 20:03:42 +01:00
Kristjan ESPERANTO
b08a4737af Update stylelint (#3042)
1. Update `stylelint` dependencies
- As of stylelint v15, we do not need `stylelint-config-prettier`
anymore:
https://github.com/prettier/stylelint-config-prettier/releases/tag/v9.0.5
2. Switch to `stylelint-config-standard`:
`stylelint-prettier/recommended` has not been updated for a long time
and still needs the old `stylelint-config-prettier`
3. Handle new `stylelint` issues
2023-02-19 21:36:50 +01:00
Kristjan ESPERANTO
bf28e63709 Move th.json to translations folder (#3043) 2023-02-19 10:51:32 +01:00
Veeck
fb22a76796 Fix precipitation styles (#3041)
They weren't applied to wrong classnames, this PR fixes that and also
expands the weather util tests

---------

Co-authored-by: veeck <michael@veeck.de>
2023-02-18 18:24:11 +01:00
Veeck
81244d961e Update config sample and collab doc (#3039)
removing some old example and add info about the mastermerge label

---------

Co-authored-by: veeck <michael@veeck.de>
2023-02-17 19:49:36 +01:00
Veeck
65aa1b0ddc Update coverage and jest settings (#3038)
Co-authored-by: veeck <michael@veeck.de>
2023-02-16 22:29:43 +01:00
Veeck
88c7e42368 Enforce PRs to be based against develop branch (#3031)
"Inspired" by my mistake in
https://github.com/MichMich/MagicMirror/pull/3028 this PR will add a
worfklow check for the branch a PR is based against.

Open question is if this prevents @MichMich from preparing a release?

---------

Co-authored-by: veeck <michael@veeck.de>
2023-02-16 18:18:18 +01:00
CarJem Generations (Carter Wallace)
e24dfa6b1a Calendar Module QOL Features and Adjustments (#3033)
This commit adds several QOL features and adjustments to the calendar
module including:
- **New Options**
- ``coloredText``: ``(default: false)`` Determines if you want your
entry text to be colored based on the calendar's color
- ``coloredBorder``: ``(default: false)`` Determines if you want entry
borders to be colored based on the calendar's color
- ``coloredSymbol``: ``(default: false)`` Determines if you want entry
symbols to be colored based on the calendar's color
- ``coloredBackground``: ``(default: false)`` Determines if you want
entry backgrounds to be colored based on the calendar's color
> These new colored options allows for more out-of-box styling options
for the calendar module. With this the ``coloredSymbolOnly`` option has
been removed due to redundancy
- ``limitDaysNeverSkip``: ``(default: false)`` show every event for
every day regardless of if the day only has a single full day event
- ``flipDateHeaderTitle``: ``(default: false)`` determines if the title
for the date header in the ``dateheaders`` time format should align to
the left ``[eg: false]`` or right ``[eg: true]``
- **Layout Changes** 
- ``dateheader`` is now a class avaliable for date headers in the
``dateheaders`` time format.
- Event entries have been better *container-ized* for better styling
(using the ``event-container`` class)
- ``repeatingCountTitle`` now has a seperator between the ``yearDiff``
and ``repeatingCountTitle``
    - ``endDate`` in ``dateheaders`` now capitalizes it's first letter
2023-02-15 21:53:24 +01:00
Karsten Hassel
a65ee86501 Introduce envsubst for config.js, update deps (#3032)
This is the implemenation of envsubst discussed in #1756 

Documentation update will follow after merge.
2023-02-12 22:34:57 +01:00
Suthep Yonphimai
4b478a5a5e Create Thai Language file (#3028)
Create Thai Language file. 
Translation from English to Thai.

Co-authored-by: veeck <michael@veeck.de>
2023-02-07 09:25:18 +01:00
Veeck
1dc0a0d5b5 Update some tests (#3024)
Co-authored-by: veeck <michael@veeck.de>
2023-02-07 00:03:08 +01:00
Magnus
bf279d9a57 Tidy up precipitation (#3023)
Fixes #2953

This is an attempt to fix the issue with precipitation amount and
percentage mixup. I have created a separate
`precipitationPercentage`-variable where the probability of rain can be
stored.

The config options now has the old `showPrecipitationAmount` in addition
to a new setting: `showPrecipitationProbability` (shows the likelihood
of rain).

<details>
  <summary>Examples</summary>
  
  ### Yr

I tested the Yr weather provider for a Norwegian city Bergen that has a
lot of rain. I have removed properties that are irrelevant for this demo
from the config-samples below.

Config:
  ```js
{
	module: "weather",
	config: {
			weatherProvider: "yr",
			type: "current",
			showPrecipitationAmount: true,
			showPrecipitationProbability: true
	}
},
{
	module: "weather",
	config: {
			weatherProvider: "yr",
			type: "hourly",
			showPrecipitationAmount: true,
			showPrecipitationProbability: true
	}
},
{
	module: "weather",
	config: {
			weatherProvider: "yr",
			type: "daily",
			showPrecipitationAmount: true,
			showPrecipitationProbability: true
	}
}
  ```

Result:<br/>
<img width="444" alt="screenshot"
src="https://user-images.githubusercontent.com/34011212/216775423-4e37345c-f915-47e5-8551-7c544ebd24b1.png">

</details>

---------

Co-authored-by: Magnus Marthinsen <magmar@online.no>
Co-authored-by: Veeck <github@veeck.de>
2023-02-04 19:02:55 +01:00
Karsten Hassel
42d42ef452 Prevent electron flashing white screen on startup (#3001)
see #1919

thanks @dfanica for providing the solution in [this
comment](https://github.com/MichMich/MagicMirror/issues/1919#issuecomment-1369898385)

tested this on a pi4 with bullseye 32-bit and 64-bit
2023-01-26 22:16:50 +01:00
Veeck
ed90f0546f Fix async node_helper stopping electron start (#3021)
Async node_helper dont have to finish immediately in loadModules. So the
start callback in the app.js with the config isnt called for some time.
But the electron ready event can already be fired in the meantime.

This lead to the electron app starting but without a config (which is
provded by the node_helper callback) therefor crashing.

This PR fixes #2487 by moving the callback call out of the loadModules
block, therefor the config is provided in time.

If any new async node_helper doesnt like this, we will see it :-)

Co-authored-by: veeck <michael@veeck.de>
2023-01-26 19:44:54 +01:00
Veeck
a8dc563a31 Update weather tests (#3008)
Added a few tests for sunset/sunrise and feelsLike 
Lets see if they run through first...

Co-authored-by: veeck <michael@veeck.de>
2023-01-26 19:43:34 +01:00
Karsten Hassel
58b9ddcd9f updatenotification: only notify mm-repo for master if tag present (#3004)
see discussion here:
https://github.com/MichMich/MagicMirror/pull/2991#issuecomment-1376372720

I still see a need for updating `master` in special cases (e.g. correct
errors in README.md which would otherwise be present up to 3 month until
next release) so with this PR **only for MagicMirror repo** and **only
for `master` branch** updatenotifications are only triggered if at least
one of the new commits has a tag.

May @MichMich must decide if this is wanted.

Co-authored-by: Veeck <github@veeck.de>
2023-01-26 13:21:30 +01:00
Piotr Rajnisz
7198ae5eae Add (in Alert module) templateName parameter (#3009)
This simple change allows to use your own templates (under "templates"
directory). The parameter `templateName` is optional (ignored on falsy
value - undefined, null, empty string, etc.) and independent of `type`.

Co-authored-by: Veeck <github@veeck.de>
2023-01-26 13:00:49 +01:00
Patrick
f6dcfb5ca3 Refresh the Calendar DOM every minute (#3016)
This keeps relative dates accurate when the calendar's fetch frequency
is much larger than a minute.
As fetching incurs network traffic and load on servers and most
calendars don't update that often, simply refreshing locally is enough.

When using relative for today's events, dates will show as "in X
minutes" or "ends in X minutes" for events within an hour and this goes
out of date quickly. It's weird to see that the time is, say, 16:30 and
an event that you know ends at 16:45 is shown to "ends in 23 minutes"
because that's when the last fetch happened.

Please forgive me if there's style issue, I don't have npm set up on my
machine to run the formatter.
2023-01-26 12:55:34 +01:00
Karsten Hassel
157e74ce7c added error message if <modulename>.js file is missing … (#3015)
… in module folder to get a hint in the logs

fixes #2403
2023-01-26 12:45:17 +01:00
Magnus
67e4dbaacd Point wind arrow in the direction the wind is flowing (#3022)
Fixes #3019

The previous implementation had the arrow pointing in to the wind. When
the wind blows from the north (0 degrees), the arrow should point
straight down. In other words, no rotation of the arrow-down symbol.
When the wind blows from the south (180 degrees), the arrow should point
straight up (I.e. the arrow down symbol rotated 180 degrees).

Co-authored-by: Magnus Marthinsen <magmar@online.no>
2023-01-22 11:41:19 +01:00
Magnus
2e2962d492 Fix yr weather direction (#3020)
Fixes so the Yr weather direction is not inverted. This error was
[reported in the
community](https://forum.magicmirror.builders/topic/17561/wind-direction-180-degrees-twisted-at-yr).

I misunderstood when developing the module because of the wind direction
arrow was pointing opposite to the arrow displayed on Yr.no. I renamed
the variable to help other developers in the future.

Co-authored-by: Magnus Marthinsen <magmar@online.no>
Co-authored-by: veeck <michael@veeck.de>
2023-01-21 22:40:08 +01:00
grenagit
cd4ba428da [Alert Module] Fix HTML message (#3018)
Solve #2828 by adding nunjucks safe filter to the message

Co-authored-by: Grena <grena@grenabox.fr>
Co-authored-by: Veeck <github@veeck.de>
2023-01-21 12:46:03 +01:00
grenagit
ee8695637b Fix typo into french translation (#3017)
French translation: Removes unnecessary spaces
2023-01-21 12:40:20 +01:00
Dave Child
4244c05764 Fix calendar.js missing default symbol prefix (#3007)
Symbols provided in customEvents don't get the "fas fa-fw fa-" prefix,
which according to the docs they should. This fixes that.

Hello and thank you for wanting to contribute to the MagicMirror²
project

**Please make sure that you have followed these 4 rules before
submitting your Pull Request:**

> 1. Base your pull requests against the `develop` branch.
>
> 2. Include these infos in the description:
>
> - Does the pull request solve a **related** issue?
> - If so, can you reference the issue like this `Fixes
#<issue_number>`?
> - What does the pull request accomplish? Use a list if needed.
> - If it includes major visual changes please add screenshots.
>
> 3. Please run `npm run lint:prettier` before submitting so that
>    style issues are fixed.
>
> 4. Don't forget to add an entry about your changes to
>    the CHANGELOG.md file.

**Note**: Sometimes the development moves very fast. It is highly
recommended that you update your branch of `develop` before creating a
pull request to send us your changes. This makes everyone's lives
easier (including yours) and helps us out on the development team.

Thanks again and have a nice day!

Co-authored-by: veeck <michael@veeck.de>
2023-01-16 22:33:05 +01:00
Veeck
c714399b4d Fix weathergov provider hourly weather forecast (#3011)
Hourly forecast wasnt converted properly during the last release cycle,
one fix and two cleanups were necessary.

Fixes #3010

Co-authored-by: veeck <michael@veeck.de>
2023-01-16 21:52:11 +01:00
Karsten Hassel
8d9f132666 add Pirate Weather as new weather provider (#3006)
solves #3005 

- same api as former dark sky provider
- website: https://pirateweather.net/
2023-01-15 11:12:55 +01:00
Karsten Hassel
d2327d3d6f removed unneeded (and unwanted) '.' in calendar (#3003)
- removed unneeded (and unwanted) '.' after the year in calendar
repeatingCountTitle (fixes #2896)
- update Collaboration.md
2023-01-14 13:47:32 +01:00
Karsten Hassel
29e3ec06cb added possibility to ignore MagicMirror repo in updatenotification (#3002)
was [requested in the
forum](https://forum.magicmirror.builders/topic/17519/updatenotification).

- added possibility to exclude MagicMirror Repo and renamed it from
`default` to `MagicMirror`
- improved getting `behind` in case a hard `git fetch` was already done
- removed test "excludes repo if refs don't match regex" because of
above improvement this case is obsolete
- improved `git fetch --dry-run` with `-n` option to exclude tags (noise
reduction)
2023-01-12 09:14:20 +01:00
Veeck
877f8ad380 Refactor mock-data for weather-tests generation (#3000)
Refactored the mock data generation for the tests so we can use plain
JSON files for the data and read it in a more general way.

Comments welcome!

Co-authored-by: veeck <michael@veeck.de>
2023-01-11 21:47:20 +01:00
Veeck
6e80e5a295 Add option to show hourly forecast in increments (#2998)
Adds new config option to show weather forecast for every X hour
(default value is 1 which reflects the current behaviour)
Also adds tests for hourly forecast
Fixes #2996

Co-authored-by: veeck <michael@veeck.de>
2023-01-10 18:55:07 +01:00
Karsten Hassel
7bc91a742f Fix badge status (#2991) 2023-01-07 20:40:36 +01:00
Veeck
fc303146a5 Remove darksky weather provider (#2993)
since the web api gets shut down end of march, see
https://darksky.net/dev

Co-authored-by: veeck <michael@veeck.de>
2023-01-07 20:14:03 +01:00
Veeck
2908c15ea6 Cleanup md files (#2994)
Update the issue template and the contributing guidelines

Co-authored-by: veeck <michael@veeck.de>
2023-01-07 20:13:25 +01:00
Veeck
a975b44fbb Fix wrong day labels in envcanada forecast (#2992)
Fixes #2987

lastDate isnt a unix date, but just a normal moment-date

Co-authored-by: veeck <michael@veeck.de>
2023-01-07 20:12:23 +01:00
Veeck
4fc38bd5bb Update dependabot action (#2985)
Supersedes https://github.com/MichMich/MagicMirror/pull/2984 
and tell dependabot to base its PRs against develop
2023-01-02 20:11:26 +01:00
Michael Teeuw
c99f660d98 Prepare 2.23.0 develop branch. 2023-01-01 18:19:30 +01:00
Michael Teeuw
cd739b6912 Merge branch 'master' into develop 2023-01-01 18:02:15 +01:00
Michael Teeuw
0ebedd0fb8 Prepare Release 2.22.0 2023-01-01 17:57:13 +01:00
Andrés Vanegas Jiménez
e9be668d1b Added a WeatherProvider for Open-Meteo (#2964)
## Added Weather Provider for Open-Meteo.

I've found a completely free weather REST API (event with option of
self-hosting) after having problems with API keys from all MagicMirror
weather providers currently implemented (the remote services, not the
providers themselves).

This API doesn't return information about reverse geocode from latitude
and longitude options like others. I solved that issue using another
free API.

### APIs used
- [Open-Meteo Weather Forecast API](https://open-meteo.com/en/docs)
- [BigDataCloud’s Free Client-Side Reverse Geocoding
API](https://www.bigdatacloud.com/docs/api/free-reverse-geocode-to-city-api)

### Considerations
- This provider is config reliable so, be free to use the same config
you can found in the official MagicMirror Weather module documentation.
- This module config skips the `apiKey` parameter. It's not used at all.
Only `latitude` and `longitude` are required.

#### Config examples:
```
modules: [
  {
    module: "weather",
    position: "top_right",
    header: "Weather Forecast",
    config: {
      updateInterval: <number here>, 
      weatherProvider: "openmeteo",
      type: "current",
      lat: <number here>,
      lon: <number here>,
      showHumidity: true,
      showWindDirectionAsArrow: true,
      showWindDirection: true,
      degreeLabel: true,
    }
  },
  {
    module: "weather",
    position: "top_right",
    header: "Weather Forecast",
    config: {
      updateInterval: <number here>,
      weatherProvider: "openmeteo",
      type: "daily",
      lat: <number here>,
      lon: <number here>
      colored: true,
      maxNumberOfDays: <number here>,
      showPrecipitationAmount: true,
      appendLocationNameToHeader: true
    }
  },
  {
    module: "weather",
    position: "top_right",
    header: "Weather Forecast",
    config: {
      updateInterval: <number here>,
      weatherProvider: "openmeteo",
      type: "hourly",
      lat: <number here>,
      lon: <number here>,
      maxEntries: <number here>,
      showPrecipitationAmount: true,
      degreeLabel: true,
      appendLocationNameToHeader: true
    }
  },
]
```

Co-authored-by: Michael Teeuw <michael@xonaymedia.nl>
2022-12-26 11:56:01 +01:00
Karsten Hassel
76d9042e60 update playwright to v1.29.1 and other dependencies (#2980)
see title, fixes https://github.com/MichMich/MagicMirror/issues/2969
2022-12-22 16:40:53 +01:00
Karsten Hassel
2fec314ff5 improve electron tests (avoid errors in github workflows) (#2977)
Fix electron tests failing sometimes in github workflow.
2022-12-12 21:43:22 +01:00
Kristjan ESPERANTO
3124b0a9c5 Replace &hellip; with (#2973)
I think it is clearer if we don't use the HTML entity.

Co-authored-by: Karsten Hassel <hassel@gmx.de>
2022-12-10 21:50:56 +01:00
Veeck
a2624442cc Cleanup compliments module (#2965)
Lots of small fixes and cleanups:
- only render something when there is a compliment
- cleanup naming
- use es6 notations
- use fetch instead of XMLHttpRequest in compliments

Co-authored-by: veeck <michael@veeck.de>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
2022-12-10 21:23:00 +01:00
Veeck
eee289aee8 Only add clock type wrappers to DOM when they are used (#2971)
Fixes a layout gap when digital clock is displayd on the left

Reported via discord:
https://discord.com/channels/545884423703494657/545884914982322177/1044376412997562418

Co-authored-by: veeck <michael@veeck.de>
2022-12-10 21:17:29 +01:00
Karsten Hassel
abbae90a8f update dependencies: electron to v22, fix playwright to v1.27.1 (#2976)
Changes:
- as discussed in #2903: update to electron v22 (we can revert it before
next release if we find any problems)
- update other dependencies
- set playwright to version v1.27.1 until #2969 is solved
2022-12-08 21:30:05 +01:00
Magnus
bd0b3c00ad New weather provider: Yr.no (#2948)
# Added Yr.no as a weather provider

Yr.no is a free Norwegian weather service. The configuration is quite
simple:
```js
{
    weatherProvider: "yr",
    lat: 59.9171,
    lon: 10.7276,
    altitude: 30
}
```
The latitude and longitude cannot have more than 4 decimals, but that
should be plenty. To quote yr: "There is no need to ask for weather
forecasts with nanometer precision!". The altitude should be meters
above sea level and defaults to 0. If `type` is set to `current` the
symbol can display the next 1, 6 or 12 hours by setting
`currentForecastHours` (default is 1).

It states in [Getting
started-guide](https://developer.yr.no/doc/GettingStarted/) that users
of the API should cache the results and use the `Expires`-header to know
when to ask for new data. By using the `If-Modified-Since`-header we can
avoid downloading the same data over and over again. I chose not to
override the `User-Agent`-header set in
[`server.js`](https://github.com/MichMich/MagicMirror/blob/a328ce5/js/server.js#L97)
even though it does not comply with [the terms of
service](https://developer.yr.no/doc/TermsOfService/). It currently
works with the default header, and by searching the web for MagicMirror
the GitHub-repo should be easy to find without an explicit link.

I also had to make some minor changes to `server.js` and
`weatherprovider.js` to be able to send and return HTTP headers. To
handle the HTTP 304 response without body I chose to return `undefined`
so we easily can use the response as a condition: `if (response) ...`.

The documentation for the API is available here:
- [API Reference overview](https://api.met.no/weatherapi/)
-
[Locationforecast](https://api.met.no/weatherapi/locationforecast/2.0/)
- Used to get the weather forecast
- [Sunrise](https://api.met.no/weatherapi/sunrise/2.0/documentation) -
used to find sunrise and sunset times

Co-authored-by: Veeck <github@veeck.de>
2022-11-08 06:18:47 +01:00
Thomas Hirschberger
b9b7d2c95d Add option to remove "x-frame-options" and "content-security-policy" response headers (#2963)
Many users like me do have the problem that they want to embed other
sites to their mirror by "iframe".
As some developers set the "x-frame-options" and
"content-security-policy" for security reasons these sites can not be
embedded.
Electron provides the "webview" element additionally to "iframe" which
allows to embed these sites although. The main difference is that a new
process is started which handles the "webview" element.
BUT: As the "webview" process needs to be started and is isolated
"webview" is slower and the elements can not be accessed from the
embedding website.

As an alternative i implemented a small callback function in electron.js
which removes the response headers that forbid the embedding.

The removing can be controlled with the new config options:
* ignoreXOriginHeader
* ignoreContentSecurityPolicy
2022-11-07 07:42:27 +01:00
veeck
0b01e9dbe0 Fix jsdoc error 2022-11-06 17:51:15 +01:00
Karsten Hassel
4fecffc3df Add Collaboration.md (#2954)
As already discussed here the first shot of the collaboration rules.

We can discuss this in the comments until ready to merge.

Co-authored-by: Veeck <github@veeck.de>
2022-10-31 14:27:31 +01:00
Magnus
4d47c0837f Support HTTP headers with CORS-method (#2957)
Adds support for sending and receiving HTTP-headers when using the
CORS-method.

This change is required for the Yr weather-provider introduced in
https://github.com/MichMich/MagicMirror/pull/2948.

To make it easier to add unit tests I moved the server-functions into a
separate file.
2022-10-30 18:14:02 +01:00
Veeck
3879949f58 Switch back to third party fetch lib for all node versions (#2961)
As discussed in https://github.com/MichMich/MagicMirror/pull/2952

Co-authored-by: veeck <michael@veeck.de>
2022-10-29 23:10:25 +02:00
buxxi
f25abfd2f8 Make the e2e tests wait for the app to start and close before running next test (#2952)
When trying to debug why the tests broke for
https://github.com/MichMich/MagicMirror/pull/2946 I found that the tests
does not wait for the app to start and close. So if the startup isn't
blocking that would fail.

So I added a callback for `close()` too and converted them to promises
for the `startApplication()` and `stopApplication()` and updated all the
e2e tests to await both. Will try to refactor all these callbacks to
promises in a later PR.
2022-10-29 22:34:17 +02:00
Veeck
7058fc5fd8 Update dependencies (#2960)
Co-authored-by: veeck <michael@veeck.de>
2022-10-28 21:51:18 +02:00
Naveen
00bc6eb28c Add dependency review action (#2862)
> Dependency Review GitHub Action in your repository to enforce
dependency
> reviews on your pull requests.
> The action scans for vulnerable versions of dependencies introduced by
package version
> changes in pull requests,
> and warns you about the associated security vulnerabilities.
> This gives you better visibility of what's changing in a pull request,
> and helps prevent vulnerabilities being added to your repository.


https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com>

Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
2022-10-28 21:16:42 +02:00
Veeck
f79d3f007d Cleanup jest config (#2959)
Some small cleanups with regards to jest
- call jest directly (nyc is integrated in jest these days)
- move jest config into seperate file so we dont clutter up the
package.json
- remove empty test file for newsletter-unit-tests
- update dependencies that touch jest
- try out v8 as coverageProvider

Co-authored-by: veeck <michael@veeck.de>
2022-10-28 20:57:08 +02:00
Veeck
c191ff0032 Refactor common weather methods into utils class (#2958)
Co-authored-by: veeck <michael@veeck.de>
2022-10-28 19:56:55 +02:00
Veeck
dde88601a6 Make sure smhi provider api only gets a maximimum of 6 digits coordinates (#2956)
Fixes #2955

Co-authored-by: veeck <michael@veeck.de>
2022-10-24 20:27:18 +02:00
Veeck
2d3940a4ff Use metric units internally in all weatherproviders (#2849)
So finally I think this refactorin is ready to be reviewed :-)

DONE:
- [x] Removed all conversion functions for wind and temperature from
specific weatherproviders
- [x] Use internally only metric units: celsius for temperature, meters
per seconds for wind
- [x] Convert temp and wind into the configured units when displaying
data on the UI
- [x] look how beaufort calculation uses metrics, added knots as new
windunit
- [x] add more e2e tests 

Checked providers:
- [x] Darksky
- [x] EnvCanada
- [x] OpenWeatherMap
- [x] SMHI provider 
- [x] UK Met Office
- [x] UK Met Office DataHub
- [x] WeatherBit
- [x] WeatherFlow
- [x] WeatherGov

TODO in different tickets:
- check weatherproviders for usage of weatherEndpoint (as seen in
https://github.com/MichMich/MagicMirror-Documentation/issues/131) -> see
#2926
- cleanup precipations -> #2953

Co-authored-by: veeck <michael@veeck.de>
2022-10-24 19:41:34 +02:00
Veeck
64ed5a54cb Add error handling to node_helper startup sequence (#2945)
Fixes https://github.com/MichMich/MagicMirror/issues/2944

Also splits the Server js into a constrcutor and an open call to remove
one callback parameter :-)

Co-authored-by: veeck <michael@veeck.de>
2022-10-19 21:40:43 +02:00
Karsten Hassel
7bd944391e fix cors problems with newsfeed articles (#2940)
solves #2840 as far as possible. There could still be errors on the
embedded iframe when the owner of the site has set `X-Frame-Options` or
`Access-Control-Allow-Origin` headers (as already mentioned in the
docs).
2022-10-17 21:38:08 +02:00
Karsten Hassel
ad4dbd786a added new electron tests supporting date mocking (#2947)
first PR for #2942 

- added new electron tests for calendar which test new css classes from
https://github.com/MichMich/MagicMirror/pull/2939
- moved some compliments tests from `e2e` to `electron` because of date
mocking
- removed mock stuff from compliments module
2022-10-17 07:20:23 +02:00
Veeck
fc59ed20e3 Convert moment(..., "X") to moment.unix(...) (#2950)
because I thought it was more readable and I found a little bug when
calculatin suntimes on the way....

Co-authored-by: veeck <michael@veeck.de>
2022-10-16 23:37:50 +02:00
Magnus
835c893205 Support for configuring FontAwesome class in calendar-module: (#2949)
Some icons in FontAwesome, like the Facebook-logo, requires a different
class than `fas fa-fw fa-`. Added support for specifying the
`className`:
```js
{
    symbol: "facebook-square",
    symbolClassName: "fab fa-",
    url: "https://www.facebook.com/events/ical/upcoming/?uid=<some_uid>"
}
```
2022-10-16 21:48:57 +02:00
Veeck
7bbf8c19db Wait till all node_helper are started before finishing startup (#2928)
In response to #2487 this implements a Promise.all for the node_helper
start calls

Co-authored-by: veeck <michael@veeck.de>
2022-10-13 21:38:04 +02:00
Dario Mratovich
1eb2965b2b Ensure updatenotification module isn't shown when local is *ahead* of remote (#2943)
This PR resolves a small bug in the updatenotification module if a local
git repo is ahead of the remote (for example I have made local commits
for my personal needs).

Currently, if `git status -sb` reports a status like: `##
master...origin/master [ahead 2]` then updatenotification treats this as
though it's "behind".

This PR uses a single Regex to match `git status -sb` output and uses
capture groups to extract info to populate the `gitInfo` object to avoid
needing to do string manipulation to extract this information.

Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com>
2022-10-12 20:23:42 +02:00
Johan
85a9f14178 Added css class names "today" and "tomorrow" for calendar (#2939)
Added class names "today" and "tomorrow" on the calendar module tr
elements (i.e. calendar items).
This way you can for example color your events today and/or tomorrow to
more easily see what's happening in the near future.

Implemented by adding an event.tomorrow variable (similar to
event.today) that can be used for other things in the future. Also
replaced a few hardcoded values (hours, seconds etc.) with constants to
make the code more consistent.

Edit: tested with normal events, split day events and events with
locations.
2022-10-11 21:05:11 +02:00
Veeck
a328ce537f Cleanup test directory (#2937)
Moves files around and renames some so that the structure is cleaner and
more consistent
2022-10-07 12:16:37 -05:00
dWoolridge
21ae79b386 weathergov.js: Removed weatherEndpoint definition (#2936)
Removed weatherEnpoint definition in defaults. It is not used in the
weathergov.js provider.
2022-10-07 19:03:52 +02:00
Veeck
d5e855dd6d Use fetch instead of XMLHttpRequest in weatherprovider (#2935)
small update to the fetchData method to use the fetch helper instead of
the old XCMLHttpRequest.
Also fixes some typos :-)

Co-authored-by: veeck <michael@veeck.de>
2022-10-06 19:44:16 +02:00
dWoolridge
a86e27a12c Added fetchWeatherHourly functionality to Weather.gov provider (#2933)
Added fetchWeatherHourly functionality to:
 modules/default/weather/providers/weathergov.js
2022-10-06 09:56:32 +02:00
Veeck
f434be3d44 Update da.json (#2930)
as proposed in
05f0d1855c (commitcomment-85730050)

Co-authored-by: veeck <michael@veeck.de>
2022-10-06 09:41:52 +02:00
Veeck
ce4906d13b Add test in compliments module for remotFile option (#2932)
nothing fancy here, just a simple test after @khassel's changes to the
test setup :-)

Co-authored-by: veeck <michael@veeck.de>
2022-10-04 14:26:31 +02:00
Malte Hallström
8212d30c4c fix(weather/smhi) Correctly reference apparent temp method (#2931)
This PR addresses [this
comment](48756e8774 (commitcomment-85772193)),
which points out an issue with #2902.

Looks like the apparent temp calculation method was incorrectly
referenced 😅
2022-10-04 11:07:40 +02:00
Karsten Hassel
f04d578704 improve tests (#2923)
use es6 syntax in all tests, split weather tests, remove callbacks
2022-10-04 10:15:24 +02:00
Michael Teeuw
7694d6fa86 Prepare 2.22.0-develop 2022-10-01 20:14:26 +02:00
351 changed files with 21238 additions and 15426 deletions

View File

@@ -1 +0,0 @@
modules/default/calendar/vendor/*

View File

@@ -1,33 +0,0 @@
{
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
"plugins": ["prettier", "import", "jsdoc", "jest"],
"env": {
"browser": true,
"es2022": true,
"jest/globals": true,
"node": true
},
"globals": {
"config": true,
"Log": true,
"MM": true,
"Module": true,
"moment": true
},
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2022,
"ecmaFeatures": {
"globalReturn": true
}
},
"rules": {
"eqeqeq": "error",
"import/order": "error",
"no-prototype-builtins": "off",
"no-throw-literal": "error",
"no-unused-vars": "off",
"no-useless-return": "error",
"prefer-template": "error"
}
}

56
.gitattributes vendored Normal file
View File

@@ -0,0 +1,56 @@
# .gitattributes snippet to force users to use same line endings for project.
#
# Handle line endings automatically for files detected as text
# and leave all files detected as binary untouched.
* text=auto
#
# The above will handle all files NOT found below
# https://help.github.com/articles/dealing-with-line-endings/
# https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes
# These files are text and should be normalized (Convert crlf => lf)
*.php text
*.css text
*.scss text
*.js text
*.json text
*.htm text
*.html text
*.xml text
*.txt text
*.ini text
*.inc text
*.pl text
*.rb text
*.py text
*.scm text
*.sql text
.htaccess text
*.sh text
Dockerfile* text
*.yml text
*.yaml text
*.md text
*.markdown text
# These files are binary and should be left untouched
# (binary is a macro for -text -diff)
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.mov binary
*.mp4 binary
*.mp3 binary
*.flv binary
*.fla binary
*.swf binary
*.gz binary
*.zip binary
*.7z binary
*.ttf binary
*.pyc binary

137
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@@ -0,0 +1,137 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official email address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement:
Contact [Rejas](https://forum.magicmirror.builders/user/rejas),
[Karsten](https://forum.magicmirror.builders/user/karsten13),
[Sam](https://forum.magicmirror.builders/user/sdetweil) or
[Kristjan](https://forum.magicmirror.builders/user/kristjanesperanto)
via private message in the forum.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -6,55 +6,33 @@ We hold our code to standard, and these standards are documented below.
## Linters
We use prettier for automatic linting of all our files: `npm run lint:prettier`.
We use [prettier](https://prettier.io/) for automatic formatting a lot all our files. The configuration is in our `prettier.config.mjs` file.
To run prettier, use `node --run lint:prettier`.
### JavaScript: Run ESLint
We use [ESLint](https://eslint.org) on our JavaScript files.
We use [ESLint](https://eslint.org) to lint our JavaScript files. The configuration is in our `eslint.config.mjs` file.
Our ESLint configuration is in our `.eslintrc.json` and `.eslintignore` files.
To run ESLint, use `npm run lint:js`.
To run ESLint, use `node --run lint:js`.
### CSS: Run StyleLint
We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our `.stylelintrc` file.
We use [StyleLint](https://stylelint.io) to lint our CSS. The configuration is in our `stylelint.config.mjs` file.
To run StyleLint, use `npm run lint:css`.
To run StyleLint, use `node --run lint:css`.
### Markdown: Run markdownlint
We use [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) to lint our markdown files. The configuration is in our `.markdownlint.json` file.
To run markdownlint, use `node --run lint:markdown`.
## Testing
We use [Jest](https://jestjs.io) for JavaScript testing.
To run all tests, use `npm run test`.
To run all tests, use `node --run test`.
The specific test commands are defined in `package.json`.
So you can also run the specific tests with other commands, e.g. `npm run test:unit` or `npx jest tests/e2e/env_spec.js`.
## Submitting Issues
Please only submit reproducible issues.
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
When submitting a new issue, please supply the following information:
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
**Node Version**: Make sure it's version 14 or later (recommended is 16).
**MagicMirror² Version**: Please let us know which version of MagicMirror² you are running. It can be found in the `package.json` file.
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.
**Steps to Reproduce**: List the step by step process to reproduce the issue.
**Expected Results**: Describe what you expected to see.
**Actual Results**: Describe what you actually saw.
**Configuration**: What does the used config.js file look like? Don't forget to remove any sensitive information!
**Additional Notes**: Provide any other relevant notes not previously mentioned. This is optional.
So you can also run the specific tests with other commands, e.g. `node --run test:unit` or `npx jest tests/e2e/env_spec.js`.

154
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,154 @@
name: 🐛 Report a problem
description: Report an issue with MagicMirror² 🚨
title: "[Bug] {{ brief description }}"
labels:
- bug
body:
- type: markdown
attributes:
value: |
Thanks for reporting a bug! Please fill in the following template to help us reproduce the issue.
Please only submit reproducible issues. If you're not sure if it's a real bug or if it's just you, please open a topic on the forum.
- type: textarea
id: environment
attributes:
label: Environment
description: |
Please tell us about how your MagicMirror² is set up.
Optimal would be the systeminformation from the logs, which looks like this:
```bash
[2025-01-14 20:05:03.529] [INFO] System information:
### SYSTEM: manufacturer: Raspberry Pi Foundation; model: Raspberry Pi 4 Model B Rev 1.5; virtual: false
### OS: platform: linux; distro: Debian GNU/Linux; release: 12; arch: arm64; kernel: 6.1.21-v8+
### VERSIONS: electron: 31.2.1; used node: 20.15.0; installed node: 22.4.1; npm: 10.8.1; pm2:
### OTHER: timeZone: Europe/Berlin; ELECTRON_ENABLE_GPU: undefined
```
If you can't provide this information, please provide the following:
- MagicMirror² version: Can be found in the `package.json` file. Please use the latest version before reporting a bug.
- Node version: Run `node -v` to find out. Make sure it's version 20 or later (recommended is 22).
- npm version: Run `npm -v` to find out.
- Platform: Are you using a Raspberry Pi (2/3/4/5), Windows, Mac, Linux, Docker, or something else?
value: |
MagicMirror² version:
Node version:
npm version:
Platform:
validations:
required: true
- type: dropdown
id: start-option
attributes:
label: Which start option are you using?
description: |
Please keep in mind that some problems are specific to certain start options.
options:
- "node --run start"
- "node --run start:wayland"
- "node --run start:windows"
- "node --run start:x11"
- "node --run server"
- "node clientonly --address ... --port ..."
validations:
required: true
- type: dropdown
id: pm2
attributes:
label: Are you using PM2?
options:
- "No"
- "Yes"
- "I don't know"
validations:
required: true
- type: dropdown
id: module
attributes:
label: Module
description: |
If the issue is related to a specific module, please provide the name of the module.
Note: Please don't report issues with 3rd party modules here. Report them on the module's repository.
options:
- "alert"
- "calendar"
- "clock"
- "compliments"
- "helloworld"
- "newsfeed"
- "updatenotification"
- "weather"
- type: checkboxes
id: module-disabled
attributes:
label: Have you tried disabling other modules?
options:
- label: "Yes"
- label: "No"
- type: checkboxes
id: search
attributes:
label: Have you searched if someone else has already reported the issue on the forum or in the issues?
options:
- label: "Yes"
required: true
- type: textarea
id: description
attributes:
label: What did you do?
description: |
Please include a *minimal* reproduction case. List the step by step process to reproduce the issue.
You can use Markdown in this field.
value: |
<details>
<summary>Configuration</summary>
```
<!-- Paste your configuration here. Don't forget to remove any sensitive information! -->
```
</details>
```js
<!-- Paste relevant code here -->
```
Steps to reproduce the issue:
validations:
required: true
- type: textarea
id: expectation
attributes:
label: What did you expect to happen?
description: |
You can use Markdown in this field.
validations:
required: true
- type: textarea
id: lint-output
attributes:
label: What actually happened?
description: |
Please copy-paste relevant log output or error messages.
You can use Markdown in this field.
validations:
required: true
- type: textarea
id: comments
attributes:
label: Additional comments
description: |
Is there anything else that's important for the team to know?
Fill out all fields and provide as much information as possible.
Adding screenshots might help us understand your problem better.
- type: checkboxes
attributes:
label: Participation
options:
- label: "I am willing to submit a pull request for this change."
required: false
- type: markdown
attributes:
value: Please **do not** open a pull request until this issue has been accepted by the team.

View File

@@ -0,0 +1,41 @@
name: 🔀 Request a change
description: Request a change that is not a bug fix, a feature request or a support request.
title: "[Change Request] {{ brief description }}"
labels:
- enhancement
- core
body:
- type: markdown
attributes:
value: Thanks for requesting a change! Please fill in the following template to help us understand your request.
- type: textarea
attributes:
label: What problem do you want to solve with this change?
description: |
Please explain your use case in as much detail as possible.
placeholder: |
Currently...
validations:
required: true
- type: textarea
attributes:
label: What do you think is the correct solution?
description: |
Please explain how you'd like to change MagicMirror² to address the problem.
placeholder: |
I'd like MagicMirror² to...
validations:
required: true
- type: checkboxes
attributes:
label: Participation
options:
- label: I am willing to submit a pull request for this change.
required: false
- type: markdown
attributes:
value: Please **do not** open a pull request until this issue has been accepted by the team.
- type: textarea
attributes:
label: Additional comments
description: Is there anything else that's important for the team to know?

14
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
blank_issues_enabled: false
contact_links:
- name: 📚 Documentation
url: https://github.com/MagicMirrorOrg/MagicMirror-Documentation/issues
about: This issue tracker is not for documentation issues. Please file documentation issues on the docs repo.
- name: 🤔 Support Question
url: https://forum.magicmirror.builders/
about: Problems installing or configuring your MagicMirror? Please post your question on the MagicMirror² Forum.
- name: 💬 Exchange of ideas
url: https://discord.gg/AmGBBwPph5
about: This issue tracker is not for general discussion. Please use the Discord channel.
- name: 📦 Issues with a 3rd-party module
url: https://kristjanesperanto.github.io/MagicMirror-3rd-Party-Modules/
about: This issue tracker is not for 3rd-party module issues. Please file 3rd-party module issues on the module's repo.

View File

@@ -1,48 +0,0 @@
Hello and thank you for opening an issue.
**Please make sure that you have read the following lines before submitting your Issue:**
## I'm not sure if this is a bug
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
## I'm having troubles installing or configuring MagicMirror
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
A common problem is that your config file could be invalid. Please run in your MagicMirror² directory: `npm run config:check` and see if it reports an error.
## I found a bug in the MagicMirror² installer
If you are facing an issue or found a bug while trying to install MagicMirror² via the installer please report it in the respective GitHub repository:
[https://github.com/sdetweil/MagicMirror_scripts](https://github.com/sdetweil/MagicMirror_scripts)
## I found a bug in the MagicMirror² Docker image
If you are facing an issue or found a bug while running MagicMirror² inside a Docker container please create an issue in the corresponding repository:
[https://gitlab.com/khassel/magicmirror](https://gitlab.com/khassel/magicmirror)
---
## I found a bug in MagicMirror
Please make sure to only submit reproducible issues. You can safely remove everything above the dividing line.
When submitting a new issue, please supply the following information:
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
**Node Version**: Make sure it's version 14 or later (recommended is 16).
**MagicMirror² Version**: Please let us know which version of MagicMirror² you are running. It can be found in the `package.json` file.
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.
**Steps to Reproduce**: List the step by step process to reproduce the issue.
**Expected Results**: Describe what you expected to see.
**Actual Results**: Describe what you actually saw.
**Configuration**: What does the used config.js file look like? Don't forget to remove any sensitive information!
**Additional Notes**: Provide any other relevant notes not previously mentioned. This is optional.

View File

@@ -0,0 +1,67 @@
name: 🚀 Feature Request
description: Suggest a new feature for MagicMirror² 💡
title: "[Feature Request] {{ brief description }}"
body:
- type: checkboxes
id: prerequisites
attributes:
label: Prerequisites
description: Please ensure you have completed all of the following.
options:
- label: I am running the latest version of MagicMirror², and know that this feature is not available now.
required: true
- label: I know my issue is not related to a third-party module.
required: true
- label: I have searched for [existing issues](https://github.com/MagicMirrorOrg/MagicMirror/issues) that already include this feature request, without success.
required: true
- type: textarea
id: description
attributes:
label: Describe the Feature Request
description: A clear and concise description of what the feature does.
validations:
required: true
- type: textarea
id: use-case
attributes:
label: Describe the Use Case
description: A clear and concise use case for what problem this feature would solve.
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: Describe Preferred Solution
description: A clear and concise description of how you want this feature to be added to MagicMirror².
- type: textarea
id: alternatives-considered
attributes:
label: Describe Alternatives
description: A clear and concise description of any alternative solutions or features you have considered.
- type: textarea
id: related-code
attributes:
label: Related Code
description: If you are able to illustrate the feature request with an example, please provide a sample here.
- type: textarea
id: additional-information
attributes:
label: Additional Information
description: List any other information that is relevant to your issue. Related issues, suggestions on how to implement, Stack Overflow links, forum links, etc.
- type: checkboxes
attributes:
label: Participation
options:
- label: I am willing to submit a pull request for this change.
required: false
- type: markdown
attributes:
value: Please **do not** open a pull request until this issue has been accepted by the team.

View File

@@ -1,9 +1,8 @@
Hello and thank you for wanting to contribute to the MagicMirror² project
Hello and thank you for wanting to contribute to the MagicMirror² project!
**Please make sure that you have followed these 4 rules before submitting your Pull Request:**
> 1. Base your pull requests against the `develop` branch.
>
> 2. Include these infos in the description:
>
> - Does the pull request solve a **related** issue?
@@ -11,9 +10,8 @@ Hello and thank you for wanting to contribute to the MagicMirror² project
> - What does the pull request accomplish? Use a list if needed.
> - If it includes major visual changes please add screenshots.
>
> 3. Please run `npm run lint:prettier` before submitting so that
> 3. Please run `node --run lint:prettier` before submitting so that
> style issues are fixed.
>
> 4. Don't forget to add an entry about your changes to
> the CHANGELOG.md file.

10
.github/codecov.yaml vendored
View File

@@ -1,10 +0,0 @@
coverage:
status:
project:
default:
# advanced settings
informational: true
patch:
default:
threshold: 0%
target: 0

View File

@@ -5,3 +5,16 @@ updates:
schedule:
interval: "weekly"
target-branch: "develop"
labels:
- "Skip Changelog"
- "dependencies"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
target-branch: "develop"
labels:
- "Skip Changelog"
- "dependencies"
- "javascript"

19
.github/stale.yaml vendored
View File

@@ -1,19 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- under investigation
- pr welcome
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@@ -13,27 +13,57 @@ permissions:
contents: read
jobs:
test:
code-style-check:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- name: "Use Node.js"
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "npm"
- name: "Install dependencies"
run: |
node --run install-mm:dev
- name: "Run linter tests"
run: |
node --run test:prettier
node --run test:js
node --run test:css
node --run test:markdown
test:
runs-on: ubuntu-24.04
timeout-minutes: 30
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
node-version: [22.14.0, 22.x, 24.x]
steps:
- name: Install electron dependencies and labwc
run: |
sudo apt-get update
sudo apt-get install -y libnss3 libasound2t64 labwc
- name: "Checkout code"
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: "Use Node.js ${{ matrix.node-version }}"
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
check-latest: true
cache: "npm"
- name: "Install dependencies and run tests"
- name: "Install MagicMirror²"
run: |
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
npm run install-mm:dev
node --run install-mm:dev
- name: "Prepare environment for tests"
run: |
# Fix chrome-sandbox permissions:
sudo chown root:root ./node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox
# Start labwc
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 WLR_RENDERER=pixman labwc &
touch css/custom.css
npm run test:prettier
npm run test:js
npm run test:css
npm run test
- name: "Run tests"
run: |
export WAYLAND_DISPLAY=wayland-0
node --run test

View File

@@ -1,33 +0,0 @@
# This workflow runs the automated test and uploads the coverage results to codecov.io
# For more information see: https://github.com/codecov/codecov-action
name: "Run Codecov Tests"
on:
push:
branches: [master, develop]
pull_request:
branches: [master, develop]
permissions:
contents: read
jobs:
run-and-upload-coverage-report:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: "Checkout code"
uses: actions/checkout@v3
- name: "Install dependencies and run coverage"
run: |
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
npm ci
touch css/custom.css
npm run test:coverage
- name: "Upload coverage results to codecov"
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
fail_ci_if_error: true

View File

@@ -13,6 +13,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: "Dependency Review"
uses: actions/dependency-review-action@v3
uses: actions/dependency-review-action@v4

32
.github/workflows/electron-rebuild.yaml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: "Electron Rebuild Testing"
on: [pull_request]
jobs:
rebuild:
name: Run electron-rebuild
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.14.0, 22.x, 24.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: "Use Node.js ${{ matrix.node-version }}"
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
check-latest: true
- name: Install MagicMirror
run: node --run install-mm
- name: Install @electron/rebuild
run: npm install @electron/rebuild
- name: Install node-libgpiod deps
run: |
sudo apt-get update
sudo apt-get install gpiod libgpiod2 libgpiod-dev
- name: Install test library (node-libgpiod) to be rebuilded
run: npm install node-libgpiod
- name: Run electron-rebuild
run: npx electron-rebuild
continue-on-error: false

View File

@@ -5,7 +5,7 @@
name: "Enforce Pull-Request Rules"
on:
pull_request:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
jobs:
@@ -19,10 +19,10 @@ jobs:
changeLogPath: "CHANGELOG.md"
skipLabels: "Skip Changelog"
- name: "Enforce develop branch"
if: ${{ github.base_ref == 'master' && !contains(github.event.pull_request.labels.*.name, 'mastermerge') }}
if: ${{ github.event.pull_request.base.ref == 'master' && !contains(github.event.pull_request.labels.*.name, 'mastermerge') }}
run: |
echo "This PR is based against the master branch and not a release or hotfix."
echo "Please don't do this. Switch the branch to 'develop'."
exit 1
env:
BASE_BRANCH: ${{ github.base_ref }}
BASE_BRANCH: ${{ github.event.pull_request.base.ref }}

31
.github/workflows/spellcheck.yaml vendored Normal file
View File

@@ -0,0 +1,31 @@
# This workflow will run a spellcheck on the codebase.
# It runs a few days before each release. At 00:00 on day-of-month 27 in March, June, September, and December.
name: Run Spellcheck
on:
schedule:
- cron: "0 0 27 3,6,9,12 *"
permissions:
contents: read
jobs:
spellcheck:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: develop
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
check-latest: true
cache: "npm"
- name: Install dependencies
run: |
node --run install-mm:dev
- name: Run Spellcheck
run: node --run test:spelling

22
.github/workflows/stale.yaml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: "Close stale issues and PRs"
on:
workflow_dispatch: # needed for manually running this workflow
schedule:
- cron: "30 1 * * 6" # every Saturday at 1:30
permissions:
issues: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-message: "This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions."
days-before-issue-stale: 60
days-before-issue-close: 7
operations-per-run: 100
stale-issue-label: "wontfix"
exempt-issue-labels: "pinned,security,under investigation,pr welcome,ready (coming with next release)"

17
.gitignore vendored
View File

@@ -10,8 +10,6 @@ coverage
.lock-wscript
build/Release
/node_modules/**/*
fonts/node_modules/**/*
vendor/node_modules/**/*
!/tests/node_modules/**/*
jspm_modules
.npm
@@ -63,8 +61,12 @@ Temporary Items
!/modules/default/**
!/modules/README.md**
# Ignore changes to the custom css files.
/css/custom.css
# Ignore changes to the custom css files but keep the sample and main.
/css/*
!/css/custom.css.sample
!/css/main.css
!/css/roboto.css
!/css/font-awesome.css
# Ignore users config file but keep the sample.
/config/*
@@ -79,3 +81,10 @@ Temporary Items
*.orig
*.rej
*.bak
# Ignore positions file (#3518)
js/positions.js
# Ignore lock files other than package-lock.json
pnpm-lock.yaml
yarn.lock

View File

@@ -1,7 +1,5 @@
#!/bin/sh
[ -f "$(dirname "$0")/_/husky.sh" ] && . "$(dirname "$0")/_/husky.sh"
if command -v npm &> /dev/null; then
npm run lint:staged
if command -v npx &> /dev/null; then
npx lint-staged
fi

6
.markdownlint.json Normal file
View File

@@ -0,0 +1,6 @@
{
"line_length": false,
"no-duplicate-heading": false,
"no-inline-html": false,
"no-trailing-punctuation": false
}

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
engine-strict=true
audit=false
loglevel="error"

View File

@@ -1,3 +1,8 @@
*.js
*.mjs
.husky/pre-commit
.prettierignore
/config
/coverage
package-lock.json
**.ics

View File

@@ -1,3 +0,0 @@
{
"trailingComma": "none"
}

View File

@@ -1,7 +0,0 @@
{
"extends": ["stylelint-config-standard"],
"plugins": ["stylelint-prettier"],
"rules": {
"prettier/prettier": true
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
# Collaboration
This document describes how collaborators of this repository should work together.
## Pull Requests
@@ -5,7 +7,7 @@ This document describes how collaborators of this repository should work togethe
- never merge your own PR's
- never merge without someone having approved (approving and merging from same person is allowed)
- wait for all approvals requested (or the author decides something different in the comments)
- never merge to `master`, except for releases (because of update notification)
- merge to `master` only for releases or other urgent issues (update notification is only triggered by tags)
- merges to master should be tagged with the "mastermerge" label so that the test runs through
## Issues
@@ -15,4 +17,51 @@ This document describes how collaborators of this repository should work togethe
## Releases
- are done by @MichMich only
Are done by
- [ ] @rejas
- [ ] @sdetweil
- [ ] @khassel
### Pre-Deployment steps
- [ ] update dependencies (a few days before)
### Deployment steps
- [ ] pull latest `develop` branch
- [ ] create `prep-release` branch from `develop`
- [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0`
- [ ] test `prep-release` branch
- [ ] update `CHANGELOG.md`
- [ ] add all contributor names: `...`
- [ ] add min. node version: > ⚠️ This release needs nodejs version `v22.14.0` or higher
- [ ] check release link at the bottom of the file
- [ ] commit and push all changes
- [ ] create pull request from `prep-release` to `develop` branch with title `Prepare Release 2.xx.0`
- [ ] after successful test run via github actions: merge pull request to `develop`
- [ ] after successful test run via github actions: create pull request from `develop` to `master` branch
- [ ] add label `mastermerge`
- [ ] title of the PR is `Release 2.xx.0`
- [ ] description of the PR is the section of the `CHANGELOG.md`
- [ ] after PR tests run without issues, merge PR
- [ ] create new release with
- [ ] corresponding version tag `v2.xx.0`
- [ ] a release name: `...`
- [ ] description of the release is the section of the `CHANGELOG.md`
### Draft new development release
- [ ] checkout `develop` branch
- [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0-develop`
- [ ] draft new section in `CHANGELOG.md`
- [ ] create new release link at the bottom of the file
- [ ] commit and push `develop` branch
- [ ] if new release will be in January, update the year in LICENSE.md
### After release
- [ ] publish release notes with link to github release on forum in new locked topic
- [ ] close all issues with label `ready (coming with next release)`
- [ ] release new documentation by merging `develop` on `master` in documentation repository
- [ ] publish new version on [npm](https://www.npmjs.com/package/magicmirror)

View File

@@ -1,6 +1,6 @@
# The MIT License (MIT)
Copyright © 2016-2022 Michael Teeuw
Copyright © 2016-2025 Michael Teeuw
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation

View File

@@ -1,20 +1,17 @@
![MagicMirror²: The open source modular smart mirror platform. ](.github/header.png)
# ![MagicMirror²: The open source modular smart mirror platform.](.github/header.png)
<p style="text-align: center">
<a href="https://choosealicense.com/licenses/mit">
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
</a>
<img src="https://img.shields.io/github/actions/workflow/status/michmich/magicmirror/automated-tests.yaml" alt="GitHub Actions">
<img src="https://img.shields.io/github/checks-status/michmich/magicmirror/master" alt="Build Status">
<a href="https://codecov.io/gh/MichMich/MagicMirror">
<img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/>
</a>
<a href="https://github.com/MichMich/MagicMirror">
<img src="https://img.shields.io/github/stars/michmich/magicmirror?style=social">
</a>
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
</a>
<img src="https://img.shields.io/github/actions/workflow/status/magicmirrororg/magicmirror/automated-tests.yaml" alt="GitHub Actions">
<img src="https://img.shields.io/github/check-runs/magicmirrororg/magicmirror/master" alt="Build Status">
<a href="https://github.com/MagicMirrorOrg/MagicMirror">
<img src="https://img.shields.io/github/stars/magicmirrororg/magicmirror?style=social" alt="GitHub Stars">
</a>
</p>
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MagicMirrorOrg/MagicMirror/graphs/contributors).
MagicMirror² focuses on a modular plugin system and uses [Electron](https://www.electronjs.org/) as an application wrapper. So no more web server or browser installs necessary!
@@ -27,7 +24,7 @@ For the full documentation including **[installation instructions](https://docs.
- Website: [https://magicmirror.builders](https://magicmirror.builders)
- Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders)
- Forum: [https://forum.magicmirror.builders](https://forum.magicmirror.builders)
- Technical discussions: https://forum.magicmirror.builders/category/11/core-system
- Technical discussions: <https://forum.magicmirror.builders/category/11/core-system>
- Discord: [https://discord.gg/J5BAtvx](https://discord.gg/J5BAtvx)
- Blog: [https://michaelteeuw.nl/tagged/magicmirror](https://michaelteeuw.nl/tagged/magicmirror)
- Donations: [https://magicmirror.builders/#donate](https://magicmirror.builders/#donate)
@@ -44,7 +41,7 @@ For the full contribution guidelines, check out: [https://docs.magicmirror.build
## Enjoying MagicMirror? Consider a donation!
MagicMirror² is opensource and free. That doesn't mean we don't need any money.
MagicMirror² is Open Source and free. That doesn't mean we don't need any money.
Please consider a donation to help us cover the ongoing costs like webservers and email services.
If we receive enough donations we might even be able to free up some working hours and spend some extra time improving the MagicMirror² core.
@@ -52,5 +49,5 @@ If we receive enough donations we might even be able to free up some working hou
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
<p style="text-align: center">
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
</p>

View File

@@ -7,16 +7,16 @@
/**
* Helper function to get server address/hostname from either the commandline or env
*/
function getServerAddress() {
function getServerAddress () {
/**
* Get command line parameters
* Assumes that a cmdline parameter is defined with `--key [value]`
*
* @param {string} key key to look for at the command line
* @param {string} defaultValue value if no key is given at the command line
* @returns {string} the value of the parameter
*/
function getCommandLineParameter(key, defaultValue = undefined) {
function getCommandLineParameter (key, defaultValue = undefined) {
const index = process.argv.indexOf(`--${key}`);
const value = index > -1 ? process.argv[index + 1] : undefined;
return value !== undefined ? String(value) : defaultValue;
@@ -28,20 +28,19 @@
});
// determine if "--use-tls"-flag was provided
config["tls"] = process.argv.indexOf("--use-tls") > 0;
config.tls = process.argv.indexOf("--use-tls") > 0;
}
/**
* Gets the config from the specified server url
*
* @param {string} url location where the server is running.
* @returns {Promise} the config
*/
function getServerConfig(url) {
function getServerConfig (url) {
// Return new pending promise
return new Promise((resolve, reject) => {
// Select http or https module, depending on requested url
const lib = url.startsWith("https") ? require("https") : require("http");
const lib = url.startsWith("https") ? require("node:https") : require("node:http");
const request = lib.get(url, (response) => {
let configData = "";
@@ -63,11 +62,10 @@
/**
* Print a message to the console in case of errors
*
* @param {string} message error message to print
* @param {number} code error code for the exit call
*/
function fail(message, code = 1) {
function fail (message, code = 1) {
if (message !== undefined && typeof message === "string") {
console.log(message);
} else {
@@ -85,8 +83,20 @@
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) {
getServerConfig(`${prefix}${config.address}:${config.port}/config/`)
.then(function (configReturn) {
// check environment for DISPLAY or WAYLAND_DISPLAY
const elecParams = ["js/electron.js"];
if (process.env.WAYLAND_DISPLAY) {
console.log(`Client: Using WAYLAND_DISPLAY=${process.env.WAYLAND_DISPLAY}`);
elecParams.push("--enable-features=UseOzonePlatform");
elecParams.push("--ozone-platform=wayland");
} else if (process.env.DISPLAY) {
console.log(`Client: Using DISPLAY=${process.env.DISPLAY}`);
} else {
fail("Error: Requires environment variable WAYLAND_DISPLAY or DISPLAY, none is provided.");
}
// Pass along the server config via an environment variable
const env = Object.create(process.env);
env.clientonly = true; // set to pass to electron.js
const options = { env: env };
configReturn.address = config.address;
configReturn.port = config.port;
@@ -95,7 +105,7 @@
// Spawn electron application
const electron = require("electron");
const child = require("child_process").spawn(electron, ["js/electron.js"], options);
const child = require("node:child_process").spawn(electron, elecParams, options);
// Pipe all child process output to current stdout
child.stdout.on("data", function (buf) {
@@ -123,4 +133,4 @@
} else {
fail();
}
})();
}());

View File

@@ -1,7 +1,4 @@
/* MagicMirror² Config Sample
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
/* Config Sample
*
* For more information on how you can configure this file
* see https://docs.magicmirror.builders/configuration/introduction.html
@@ -18,20 +15,24 @@ let config = {
// - "0.0.0.0", "::" to listen on any interface
// Default, when address config is left out or empty, is "localhost"
port: 8080,
basePath: "/", // The URL path where MagicMirror² is hosted. If you are using a Reverse proxy
// you must set the sub path here. basePath must end with a /
basePath: "/", // The URL path where MagicMirror² is hosted. If you are using a Reverse proxy
// you must set the sub path here. basePath must end with a /
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses
// or add a specific IPv4 of 192.168.1.5 :
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"],
// or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format :
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"],
// or add a specific IPv4 of 192.168.1.5 :
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"],
// or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format :
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"],
useHttps: false, // Support HTTPS or not, default "false" will use HTTP
httpsPrivateKey: "", // HTTPS private key path, only require when useHttps is true
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
useHttps: false, // Support HTTPS or not, default "false" will use HTTP
httpsPrivateKey: "", // HTTPS private key path, only require when useHttps is true
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
language: "en",
locale: "en-US",
locale: "en-US", // this variable is provided as a consistent location
// it is currently only used by 3rd party modules. no MagicMirror code uses this value
// as we have no usage, we have no constraints on what this field holds
// see https://en.wikipedia.org/wiki/Locale_(computer_software) for the possibilities
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
timeFormat: 24,
units: "metric",
@@ -55,8 +56,9 @@ let config = {
config: {
calendars: [
{
fetchInterval: 7 * 24 * 60 * 60 * 1000,
symbol: "calendar-check",
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics"
url: "https://ics.calendarlabs.com/76/mm3137/US_Holidays.ics"
}
]
}
@@ -69,11 +71,10 @@ let config = {
module: "weather",
position: "top_right",
config: {
weatherProvider: "openweathermap",
weatherProvider: "openmeteo",
type: "current",
location: "New York",
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
apiKey: "YOUR_OPENWEATHER_API_KEY"
lat: 40.776676,
lon: -73.971321
}
},
{
@@ -81,11 +82,10 @@ let config = {
position: "top_right",
header: "Weather Forecast",
config: {
weatherProvider: "openweathermap",
weatherProvider: "openmeteo",
type: "forecast",
location: "New York",
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
apiKey: "YOUR_OPENWEATHER_API_KEY"
lat: 40.776676,
lon: -73.971321
}
},
{
@@ -108,4 +108,4 @@ let config = {
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = config;}
if (typeof module !== "undefined") { module.exports = config; }

253
cspell.config.json Normal file
View File

@@ -0,0 +1,253 @@
{
"version": "0.2",
"language": "en",
"words": [
"aarch",
"Alvinger",
"Ampio",
"andrezibaia",
"angeldeejay",
"apiontek",
"armv",
"ashishtank",
"autoplay",
"beada",
"Binney",
"bluemanos",
"bnitkin",
"bokmål",
"Brasileiro",
"Brento",
"browserwindow",
"bryanzzhu",
"btoconnor",
"bugsounet",
"buxxi",
"byday",
"calendarfetcherutils",
"calendarutils",
"chamakura",
"cjbrunner",
"clientonly",
"clockfaces",
"cmdline",
"codac",
"Crazylegstoo",
"crazyscot",
"Creepin",
"currentweather",
"CUSTOMCSS",
"customregions",
"cxmj",
"Cymraeg",
"dariom",
"darksky",
"dateheader",
"dateheaders",
"davide",
"DAYAFTERTOMORROW",
"DAYBEFOREYESTERDAY",
"defaultmodules",
"dgoth",
"dkallen",
"drivelist",
"DTEND",
"DTSTAMP",
"DTSTART",
"Duffman",
"earlman",
"easyas",
"eddiehung",
"Edgardos",
"Ekristoffe",
"elec",
"eltociear",
"envcanada",
"envsub",
"envsubst",
"eouia",
"exdate",
"expectedheaders",
"ezeholz",
"Faizan",
"feedme",
"feelslike",
"Fenner",
"fewieden",
"fixuppm",
"flopp",
"fontawesome",
"fontface",
"forecastweather",
"fortawesome",
"frameguard",
"Frysk",
"fulldate",
"fullday",
"fullscreen",
"geraki",
"Gevoelstemperatuur",
"GHSA",
"ghsas",
"grenagit",
"Heiko",
"Hirschberger",
"hourlyweather",
"Hwind",
"ical",
"illimarkangur",
"Ingan",
"ipfilter",
"ismarslomic",
"jakemulley",
"jakobsarwary",
"jalibu",
"jargordon",
"jetson",
"jkriegshauser",
"jsdocs",
"jsonlint",
"jupadin",
"kaennchenstruggle",
"Kalenderwoche",
"kenzal",
"Keyport",
"khassel",
"Kingdon",
"kioskmode",
"klaernie",
"kleinmantara",
"Kmph",
"Knapoc",
"Koepke",
"kolbyjack",
"krekos",
"Kristjan",
"krukle",
"labwc",
"Landis",
"larryare",
"letsencrypt",
"libgpiod",
"Lightspeed",
"locationforecast",
"lockstring",
"lstrip",
"Luciella",
"luxon",
"lxsession",
"magicmirror",
"martingron",
"marvai",
"mastermerge",
"matchtype",
"maxentries",
"Meteo",
"michaelteeuw",
"michmich",
"Midori",
"mirontoli",
"MISSINGLANG",
"mixasgr",
"MMPM",
"modernizr",
"modulename",
"multiday",
"Mystara",
"Ñandú",
"nathannaveen",
"naveensrinivasan",
"ndom",
"Nerfzooka",
"NEWSFEED",
"newsitems",
"nfogal",
"njwilliams",
"nonrepeating",
"Norsk",
"nunjuck",
"odroid",
"oemel",
"onecall",
"onevent",
"openmeteo",
"openweathermap",
"oraclesean",
"oscarb",
"philnagel",
"Português",
"PRECIP",
"Problema",
"psieg",
"radokristof",
"rajniszp",
"rebuilded",
"Reis",
"rejas",
"Resig",
"roboto",
"rohitdharavath",
"Rosso",
"rrule",
"savvadam",
"sdetweil",
"sendheaders",
"serveronly",
"sexualized",
"skpanagiotis",
"SMHI",
"Snille",
"socketclient",
"socketio",
"spectron",
"Starinvest",
"sthuber",
"Stieber",
"stylelintrc",
"subclassing",
"sunaction",
"suncalc",
"suntimes",
"symboltest",
"systeminformation",
"tada",
"taglist",
"Teeuw",
"TESTMODE",
"thomasrockhu",
"tomzt",
"ukmetoffice",
"ukmetofficedatahub",
"unitless",
"unparseable",
"updatenotification",
"Vaice",
"veeck",
"VEVENT",
"vgtu",
"Voelt",
"vppencilsharpener",
"Wallys",
"Weatherbit",
"WEATHERDATA",
"Weatherflow",
"weatherforecast",
"weathergov",
"weathericons",
"weatherobject",
"weatherutils",
"windspeed",
"Woolridge",
"worktree",
"xlarge",
"xrandr",
"xsmall",
"xsorifc",
"xwindows",
"xxxe",
"Ybbet",
"yearmatchgroup"
],
"ignorePaths": ["node_modules/**", "modules/**", "translations/**", "tests/mocks/**", "tests/e2e/modules/clock_es_spec.js", "css/roboto.css"],
"dictionaries": ["node"]
}

View File

@@ -1,10 +1,8 @@
/* MagicMirror² Custom CSS Sample
/* Custom CSS Sample
*
* Change color and fonts here.
*
* Beware that properties cannot be unitless, so for example write '--gap-body: 0px;' instead of just '--gap-body: 0;'
*
* MIT Licensed.
*/
/* Uncomment and adjust accordingly if you want to import another font from the google-fonts-api: */
@@ -18,7 +16,7 @@
--font-primary: "Roboto Condensed";
--font-secondary: "Roboto";
--font-size: 20px;
--font-size-small: 0.75rem;
@@ -26,6 +24,6 @@
--gap-body-right: 60px;
--gap-body-bottom: 60px;
--gap-body-left: 60px;
--gap-modules: 30px;
}

View File

@@ -239,3 +239,16 @@ sup {
border-spacing: 0;
border-collapse: separate;
}
/**
* Container Definitions.
*/
.region .container {
display: flex;
flex-direction: column;
}
.region .container.hidden {
display: none;
}

671
css/roboto.css Normal file
View File

@@ -0,0 +1,671 @@
/* roboto-cyrillic-ext-100-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 100;
src:
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-100-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-100-normal.woff") format("woff");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* roboto-cyrillic-100-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 100;
src:
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-100-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-100-normal.woff") format("woff");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* roboto-greek-ext-100-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 100;
src:
url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-100-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-100-normal.woff") format("woff");
unicode-range: U+1F00-1FFF;
}
/* roboto-greek-100-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 100;
src:
url("../node_modules/@fontsource/roboto/files/roboto-greek-100-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-greek-100-normal.woff") format("woff");
unicode-range: U+0370-03FF;
}
/* roboto-vietnamese-100-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 100;
src:
url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-100-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-100-normal.woff") format("woff");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* roboto-latin-ext-100-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 100;
src:
url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-100-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-100-normal.woff") format("woff");
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* roboto-latin-100-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 100;
src:
url("../node_modules/@fontsource/roboto/files/roboto-latin-100-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-latin-100-normal.woff") format("woff");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* roboto-cyrillic-ext-300-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-300-normal.woff") format("woff");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* roboto-cyrillic-300-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-300-normal.woff") format("woff");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* roboto-greek-ext-300-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-300-normal.woff") format("woff");
unicode-range: U+1F00-1FFF;
}
/* roboto-greek-300-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto/files/roboto-greek-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-greek-300-normal.woff") format("woff");
unicode-range: U+0370-03FF;
}
/* roboto-vietnamese-300-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-300-normal.woff") format("woff");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* roboto-latin-ext-300-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-300-normal.woff") format("woff");
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* roboto-latin-300-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto/files/roboto-latin-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-latin-300-normal.woff") format("woff");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* roboto-cyrillic-ext-400-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-400-normal.woff") format("woff");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* roboto-cyrillic-400-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-400-normal.woff") format("woff");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* roboto-greek-ext-400-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-400-normal.woff") format("woff");
unicode-range: U+1F00-1FFF;
}
/* roboto-greek-400-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto/files/roboto-greek-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-greek-400-normal.woff") format("woff");
unicode-range: U+0370-03FF;
}
/* roboto-vietnamese-400-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-400-normal.woff") format("woff");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* roboto-latin-ext-400-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-400-normal.woff") format("woff");
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* roboto-latin-400-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto/files/roboto-latin-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-latin-400-normal.woff") format("woff");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* roboto-cyrillic-ext-500-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 500;
src:
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-500-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-500-normal.woff") format("woff");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* roboto-cyrillic-500-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 500;
src:
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-500-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-500-normal.woff") format("woff");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* roboto-greek-ext-500-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 500;
src:
url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-500-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-500-normal.woff") format("woff");
unicode-range: U+1F00-1FFF;
}
/* roboto-greek-500-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 500;
src:
url("../node_modules/@fontsource/roboto/files/roboto-greek-500-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-greek-500-normal.woff") format("woff");
unicode-range: U+0370-03FF;
}
/* roboto-vietnamese-500-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 500;
src:
url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-500-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-500-normal.woff") format("woff");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* roboto-latin-ext-500-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 500;
src:
url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-500-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-500-normal.woff") format("woff");
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* roboto-latin-500-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 500;
src:
url("../node_modules/@fontsource/roboto/files/roboto-latin-500-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-latin-500-normal.woff") format("woff");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* roboto-cyrillic-ext-700-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-700-normal.woff") format("woff");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* roboto-cyrillic-700-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-700-normal.woff") format("woff");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* roboto-greek-ext-700-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-700-normal.woff") format("woff");
unicode-range: U+1F00-1FFF;
}
/* roboto-greek-700-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto/files/roboto-greek-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-greek-700-normal.woff") format("woff");
unicode-range: U+0370-03FF;
}
/* roboto-vietnamese-700-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-700-normal.woff") format("woff");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* roboto-latin-ext-700-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-700-normal.woff") format("woff");
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* roboto-latin-700-normal */
@font-face {
font-family: Roboto;
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto/files/roboto-latin-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto/files/roboto-latin-700-normal.woff") format("woff");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* roboto-condensed-cyrillic-ext-300-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-300-normal.woff") format("woff");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* roboto-condensed-cyrillic-300-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-300-normal.woff") format("woff");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* roboto-condensed-greek-ext-300-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-300-normal.woff") format("woff");
unicode-range: U+1F00-1FFF;
}
/* roboto-condensed-greek-300-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-300-normal.woff") format("woff");
unicode-range: U+0370-03FF;
}
/* roboto-condensed-vietnamese-300-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-300-normal.woff") format("woff");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* roboto-condensed-latin-ext-300-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-300-normal.woff") format("woff");
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* roboto-condensed-latin-300-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 300;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-300-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-300-normal.woff") format("woff");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* roboto-condensed-cyrillic-ext-400-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-400-normal.woff") format("woff");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* roboto-condensed-cyrillic-400-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-400-normal.woff") format("woff");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* roboto-condensed-greek-ext-400-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-400-normal.woff") format("woff");
unicode-range: U+1F00-1FFF;
}
/* roboto-condensed-greek-400-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-400-normal.woff") format("woff");
unicode-range: U+0370-03FF;
}
/* roboto-condensed-vietnamese-400-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-400-normal.woff") format("woff");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* roboto-condensed-latin-ext-400-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-400-normal.woff") format("woff");
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* roboto-condensed-latin-400-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-400-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-400-normal.woff") format("woff");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* roboto-condensed-cyrillic-ext-700-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-700-normal.woff") format("woff");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* roboto-condensed-cyrillic-700-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-700-normal.woff") format("woff");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* roboto-condensed-greek-ext-700-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-700-normal.woff") format("woff");
unicode-range: U+1F00-1FFF;
}
/* roboto-condensed-greek-700-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-700-normal.woff") format("woff");
unicode-range: U+0370-03FF;
}
/* roboto-condensed-vietnamese-700-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-700-normal.woff") format("woff");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* roboto-condensed-latin-ext-700-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-700-normal.woff") format("woff");
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* roboto-condensed-latin-700-normal */
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: swap;
font-weight: 700;
src:
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-700-normal.woff2") format("woff2"),
url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-700-normal.woff") format("woff");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

132
eslint.config.mjs Normal file
View File

@@ -0,0 +1,132 @@
import {defineConfig, globalIgnores} from "eslint/config";
import globals from "globals";
import {flatConfigs as importX} from "eslint-plugin-import-x";
import jest from "eslint-plugin-jest";
import js from "@eslint/js";
import jsdoc from "eslint-plugin-jsdoc";
import packageJson from "eslint-plugin-package-json";
import stylistic from "@stylistic/eslint-plugin";
export default defineConfig([
globalIgnores(["config/**", "modules/**/*", "!modules/default/**", "js/positions.js"]),
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: "latest",
globals: {
...globals.browser,
...globals.node,
Log: "readonly",
MM: "readonly",
Module: "readonly",
config: "readonly",
moment: "readonly"
}
},
plugins: {js, jsdoc, stylistic},
extends: [importX.recommended, jest.configs["flat/recommended"], "js/recommended", jsdoc.configs["flat/recommended"], "stylistic/all"],
rules: {
"@stylistic/array-element-newline": ["error", "consistent"],
"@stylistic/arrow-parens": ["error", "always"],
"@stylistic/brace-style": "off",
"@stylistic/comma-dangle": ["error", "never"],
"@stylistic/dot-location": ["error", "property"],
"@stylistic/function-call-argument-newline": ["error", "consistent"],
"@stylistic/function-paren-newline": ["error", "consistent"],
"@stylistic/implicit-arrow-linebreak": ["error", "beside"],
"@stylistic/indent": ["error", "tab"],
"@stylistic/max-statements-per-line": ["error", {max: 2}],
"@stylistic/multiline-comment-style": "off",
"@stylistic/multiline-ternary": ["error", "always-multiline"],
"@stylistic/newline-per-chained-call": ["error", {ignoreChainWithDepth: 4}],
"@stylistic/no-extra-parens": "off",
"@stylistic/no-tabs": "off",
"@stylistic/object-curly-spacing": ["error", "always"],
"@stylistic/object-property-newline": ["error", {allowAllPropertiesOnSameLine: true}],
"@stylistic/operator-linebreak": ["error", "before"],
"@stylistic/padded-blocks": "off",
"@stylistic/quote-props": ["error", "as-needed"],
"@stylistic/quotes": ["error", "double"],
"@stylistic/semi": ["error", "always"],
"@stylistic/space-before-function-paren": ["error", "always"],
"@stylistic/spaced-comment": "off",
"dot-notation": "error",
eqeqeq: "error",
"id-length": "off",
"import-x/extensions": "error",
"import-x/newline-after-import": "error",
"import-x/order": "error",
"init-declarations": "off",
"jest/consistent-test-it": "warn",
"jest/no-done-callback": "warn",
"jest/prefer-expect-resolves": "warn",
"jest/prefer-mock-promise-shorthand": "warn",
"jest/prefer-to-be": "warn",
"jest/prefer-to-have-length": "warn",
"max-lines-per-function": ["warn", 400],
"max-statements": "off",
"no-global-assign": "off",
"no-inline-comments": "off",
"no-magic-numbers": "off",
"no-param-reassign": "error",
"no-plusplus": "off",
"no-prototype-builtins": "off",
"no-ternary": "off",
"no-throw-literal": "error",
"no-undefined": "off",
"no-unneeded-ternary": "error",
"no-unused-vars": "off",
"no-useless-return": "error",
"no-warning-comments": "off",
"object-shorthand": ["error", "methods"],
"one-var": "off",
"prefer-template": "error",
"sort-keys": "off"
}
},
{
files: ["**/*.js"],
ignores: [
"clientonly/index.js",
"modules/default/calendar/debug.js",
"js/logger.js",
"tests/**/*.js"
],
rules: {"no-console": "error"}
},
{
files: ["**/package.json"],
plugins: {packageJson},
extends: ["packageJson/recommended"]
},
{
files: ["**/*.mjs"],
languageOptions: {
ecmaVersion: "latest",
globals: {
...globals.node
},
sourceType: "module"
},
plugins: {js, stylistic},
extends: [importX.recommended, "js/all", "stylistic/all"],
rules: {
"@stylistic/array-element-newline": "off",
"@stylistic/indent": ["error", "tab"],
"@stylistic/object-property-newline": ["error", {allowAllPropertiesOnSameLine: true}],
"@stylistic/padded-blocks": ["error", "never"],
"@stylistic/quote-props": ["error", "as-needed"],
"import-x/no-unresolved": ["error", {ignore: ["eslint/config"]}],
"max-lines-per-function": ["error", 100],
"no-magic-numbers": "off",
"one-var": ["error", "never"],
"sort-keys": "off"
}
},
{
files: ["tests/configs/modules/weather/*.js"],
rules: {
"@stylistic/quotes": "off"
}
}
]);

View File

@@ -1,37 +0,0 @@
{
"name": "magicmirror-fonts",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "magicmirror-fonts",
"license": "MIT",
"dependencies": {
"@fontsource/roboto": "^4.5.8",
"@fontsource/roboto-condensed": "^4.5.9"
}
},
"node_modules/@fontsource/roboto": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz",
"integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA=="
},
"node_modules/@fontsource/roboto-condensed": {
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-4.5.9.tgz",
"integrity": "sha512-ql4sQq+h8puBVildZ5ssjYf8DWDONYDe3PD3Bu/p1ZW9GnRETRNPPcCTs/q62HIl3QimwwkiKWynn6wZhQaetg=="
}
},
"dependencies": {
"@fontsource/roboto": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz",
"integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA=="
},
"@fontsource/roboto-condensed": {
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-4.5.9.tgz",
"integrity": "sha512-ql4sQq+h8puBVildZ5ssjYf8DWDONYDe3PD3Bu/p1ZW9GnRETRNPPcCTs/q62HIl3QimwwkiKWynn6wZhQaetg=="
}
}
}

View File

@@ -1,16 +0,0 @@
{
"name": "magicmirror-fonts",
"description": "Package for fonts use by MagicMirror² Core.",
"repository": {
"type": "git",
"url": "git+https://github.com/MichMich/MagicMirror.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/MichMich/MagicMirror/issues"
},
"dependencies": {
"@fontsource/roboto": "^4.5.8",
"@fontsource/roboto-condensed": "^4.5.9"
}
}

View File

@@ -1,55 +0,0 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-weight: 100;
src: local("Roboto Thin"), local("Roboto-Thin"), url("node_modules/@fontsource/roboto/files/roboto-all-100-normal.woff") format("woff");
}
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-weight: 300;
src: local("Roboto Condensed Light"), local("RobotoCondensed-Light"), url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-all-300-normal.woff") format("woff");
}
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-weight: 400;
src: local("Roboto Condensed"), local("RobotoCondensed-Regular"), url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-all-400-normal.woff") format("woff");
}
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-weight: 700;
src: local("Roboto Condensed Bold"), local("RobotoCondensed-Bold"), url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-all-700-normal.woff") format("woff");
}
@font-face {
font-family: Roboto;
font-style: normal;
font-weight: 400;
src: local("Roboto"), local("Roboto-Regular"), url("node_modules/@fontsource/roboto/files/roboto-all-400-normal.woff") format("woff");
}
@font-face {
font-family: Roboto;
font-style: normal;
font-weight: 500;
src: local("Roboto Medium"), local("Roboto-Medium"), url("node_modules/@fontsource/roboto/files/roboto-all-500-normal.woff") format("woff");
}
@font-face {
font-family: Roboto;
font-style: normal;
font-weight: 700;
src: local("Roboto Bold"), local("Roboto-Bold"), url("node_modules/@fontsource/roboto/files/roboto-all-700-normal.woff") format("woff");
}
@font-face {
font-family: Roboto;
font-style: normal;
font-weight: 300;
src: local("Roboto Light"), local("Roboto-Light"), url("node_modules/@fontsource/roboto/files/roboto-all-300-normal.woff") format("woff");
}

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<title>MagicMirror²</title>
@@ -12,11 +12,13 @@
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<link rel="stylesheet" type="text/css" href="css/main.css" />
<link rel="stylesheet" type="text/css" href="fonts/roboto.css" />
<link rel="stylesheet" type="text/css" href="css/roboto.css" />
<link rel="stylesheet" type="text/css" href="node_modules/animate.css/animate.min.css" />
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
<script type="text/javascript">
window.mmVersion = "#VERSION#";
window.mmTestMode = "#TESTMODE#";
</script>
</head>
<body>
@@ -40,11 +42,12 @@
</div>
<div class="region fullscreen above"><div class="container"></div></div>
<script type="text/javascript" src="socket.io/socket.io.js"></script>
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
<script type="text/javascript" src="node_modules/nunjucks/browser/nunjucks.min.js"></script>
<script type="text/javascript" src="js/defaults.js"></script>
<script type="text/javascript" src="#CONFIG_FILE#"></script>
<script type="text/javascript" src="vendor/vendor.js"></script>
<script type="text/javascript" src="js/vendor.js"></script>
<script type="text/javascript" src="modules/default/defaultmodules.js"></script>
<script type="text/javascript" src="modules/default/utils.js"></script>
<script type="text/javascript" src="js/logger.js"></script>
<script type="text/javascript" src="translations/translations.js"></script>
<script type="text/javascript" src="js/translator.js"></script>
@@ -52,6 +55,8 @@
<script type="text/javascript" src="js/module.js"></script>
<script type="text/javascript" src="js/loader.js"></script>
<script type="text/javascript" src="js/socketclient.js"></script>
<script type="text/javascript" src="js/animateCSS.js"></script>
<script type="text/javascript" src="js/positions.js"></script>
<script type="text/javascript" src="js/main.js"></script>
</body>
</html>

View File

@@ -1,4 +0,0 @@
#!/bin/bash
# This file is still here to keep PM2 working on older installations.
cd ~/MagicMirror
DISPLAY=:0 npm start

View File

@@ -6,23 +6,23 @@ module.exports = async () => {
projects: [
{
displayName: "unit",
globalSetup: "<rootDir>/tests/unit/helpers/global-setup.js",
moduleNameMapper: {
logger: "<rootDir>/js/logger.js"
},
testMatch: ["**/tests/unit/**/*.[jt]s?(x)"],
testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks"]
testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks", "<rootDir>/tests/unit/helpers"]
},
{
displayName: "electron",
testMatch: ["**/tests/electron/**/*.[jt]s?(x)"],
testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers/"]
testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers"]
},
{
displayName: "e2e",
setupFilesAfterEnv: ["<rootDir>/tests/e2e/helpers/mock-console.js"],
testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"],
modulePaths: ["<rootDir>/js/"],
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers/", "<rootDir>/tests/e2e/mocks"]
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers", "<rootDir>/tests/e2e/mocks"]
}
],
collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/default/**/*.js", "./serveronly/**/*.js"],

158
js/animateCSS.js Normal file
View File

@@ -0,0 +1,158 @@
/* enumeration of animations in Array **/
const AnimateCSSIn = [
// Attention seekers
"bounce",
"flash",
"pulse",
"rubberBand",
"shakeX",
"shakeY",
"headShake",
"swing",
"tada",
"wobble",
"jello",
"heartBeat",
// Back entrances
"backInDown",
"backInLeft",
"backInRight",
"backInUp",
// Bouncing entrances
"bounceIn",
"bounceInDown",
"bounceInLeft",
"bounceInRight",
"bounceInUp",
// Fading entrances
"fadeIn",
"fadeInDown",
"fadeInDownBig",
"fadeInLeft",
"fadeInLeftBig",
"fadeInRight",
"fadeInRightBig",
"fadeInUp",
"fadeInUpBig",
"fadeInTopLeft",
"fadeInTopRight",
"fadeInBottomLeft",
"fadeInBottomRight",
// Flippers
"flip",
"flipInX",
"flipInY",
// Lightspeed
"lightSpeedInRight",
"lightSpeedInLeft",
// Rotating entrances
"rotateIn",
"rotateInDownLeft",
"rotateInDownRight",
"rotateInUpLeft",
"rotateInUpRight",
// Specials
"jackInTheBox",
"rollIn",
// Zooming entrances
"zoomIn",
"zoomInDown",
"zoomInLeft",
"zoomInRight",
"zoomInUp",
// Sliding entrances
"slideInDown",
"slideInLeft",
"slideInRight",
"slideInUp"
];
const AnimateCSSOut = [
// Back exits
"backOutDown",
"backOutLeft",
"backOutRight",
"backOutUp",
// Bouncing exits
"bounceOut",
"bounceOutDown",
"bounceOutLeft",
"bounceOutRight",
"bounceOutUp",
// Fading exits
"fadeOut",
"fadeOutDown",
"fadeOutDownBig",
"fadeOutLeft",
"fadeOutLeftBig",
"fadeOutRight",
"fadeOutRightBig",
"fadeOutUp",
"fadeOutUpBig",
"fadeOutTopLeft",
"fadeOutTopRight",
"fadeOutBottomRight",
"fadeOutBottomLeft",
// Flippers
"flipOutX",
"flipOutY",
// Lightspeed
"lightSpeedOutRight",
"lightSpeedOutLeft",
// Rotating exits
"rotateOut",
"rotateOutDownLeft",
"rotateOutDownRight",
"rotateOutUpLeft",
"rotateOutUpRight",
// Specials
"hinge",
"rollOut",
// Zooming exits
"zoomOut",
"zoomOutDown",
"zoomOutLeft",
"zoomOutRight",
"zoomOutUp",
// Sliding exits
"slideOutDown",
"slideOutLeft",
"slideOutRight",
"slideOutUp"
];
/**
* Create an animation with Animate CSS
* @param {string} [element] div element to animate.
* @param {string} [animation] animation name.
* @param {number} [animationTime] animation duration.
*/
function addAnimateCSS (element, animation, animationTime) {
const animationName = `animate__${animation}`;
const node = document.getElementById(element);
if (!node) {
// don't execute animate: we don't find div
Log.warn("addAnimateCSS: node not found for", element);
return;
}
node.style.setProperty("--animate-duration", `${animationTime}s`);
node.classList.add("animate__animated", animationName);
}
/**
* Remove an animation with Animate CSS
* @param {string} [element] div element to animate.
* @param {string} [animation] animation name.
*/
function removeAnimateCSS (element, animation) {
const animationName = `animate__${animation}`;
const node = document.getElementById(element);
if (!node) {
// don't execute animate: we don't find div
Log.warn("removeAnimateCSS: node not found for", element);
return;
}
node.classList.remove("animate__animated", animationName);
node.style.removeProperty("--animate-duration");
}
if (typeof window === "undefined") module.exports = { AnimateCSSIn, AnimateCSSOut };

175
js/app.js
View File

@@ -1,34 +1,38 @@
/* MagicMirror²
* The Core App (Server)
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
// Alias modules mentioned in package.js under _moduleAliases.
require("module-alias/register");
const fs = require("fs");
const path = require("path");
const fs = require("node:fs");
const path = require("node:path");
const envsub = require("envsub");
const Log = require("logger");
const Server = require(`${__dirname}/server`);
const Utils = require(`${__dirname}/utils`);
const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`);
const { getEnvVarsAsObj } = require(`${__dirname}/server_functions`);
// used to control fetch timeout for node_helpers
const { setGlobalDispatcher, Agent } = require("undici");
// common timeout value, provide environment override in case
const fetch_timeout = process.env.mmFetchTimeout !== undefined ? process.env.mmFetchTimeout : 30000;
// Get version number.
global.version = require(`${__dirname}/../package.json`).version;
global.mmTestMode = process.env.mmTestMode === "true";
Log.log(`Starting MagicMirror: v${global.version}`);
// Log system information.
Utils.logSystemInformation();
// global absolute root path
global.root_path = path.resolve(`${__dirname}/../`);
if (process.env.MM_CONFIG_FILE) {
global.configuration_file = process.env.MM_CONFIG_FILE;
global.configuration_file = process.env.MM_CONFIG_FILE.replace(`${global.root_path}/`, "");
}
// FIXME: Hotfix Pull Request
// https://github.com/MichMich/MagicMirror/pull/673
// https://github.com/MagicMirrorOrg/MagicMirror/pull/673
if (process.env.MM_PORT) {
global.mmPort = process.env.MM_PORT;
}
@@ -36,30 +40,35 @@ if (process.env.MM_PORT) {
// The next part is here to prevent a major exception when there
// is no internet connection. This could probable be solved better.
process.on("uncaughtException", function (err) {
Log.error("Whoops! There was an uncaught exception...");
Log.error(err);
Log.error("MagicMirror² will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
// ignore strange exceptions under aarch64 coming from systeminformation:
if (!err.stack.includes("node_modules/systeminformation")) {
Log.error("Whoops! There was an uncaught exception...");
Log.error(err);
Log.error("MagicMirror² will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MagicMirrorOrg/MagicMirror/issues");
}
});
/**
* The core app.
*
* @class
*/
function App() {
function App () {
let nodeHelpers = [];
let httpServer;
/**
* Loads the config file. Combines it with the defaults and returns the config
*
* @async
* @returns {Promise<object>} the loaded config or the defaults if something goes wrong
*/
async function loadConfig() {
async function loadConfig () {
Log.log("Loading config ...");
const defaults = require(`${__dirname}/defaults`);
if (process.env.JEST_WORKER_ID !== undefined) {
// if we are running with jest
defaults.address = "0.0.0.0";
}
// For this check proposed to TestSuite
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
@@ -68,17 +77,17 @@ function App() {
// check if templateFile exists
try {
fs.accessSync(templateFile, fs.F_OK);
fs.accessSync(templateFile, fs.constants.F_OK);
} catch (err) {
templateFile = null;
Log.debug("config template file not exists, no envsubst");
Log.log("config template file not exists, no envsubst");
}
if (templateFile) {
// save current config.js
try {
if (fs.existsSync(configFilename)) {
fs.copyFileSync(configFilename, `${configFilename}_${Date.now()}`);
fs.copyFileSync(configFilename, `${configFilename}-old`);
}
} catch (err) {
Log.warn(`Could not copy ${configFilename}: ${err.message}`);
@@ -92,7 +101,7 @@ function App() {
envFiles.push(configEnvFile);
}
} catch (err) {
Log.debug(`${configEnvFile} does not exist. ${err.message}`);
Log.log(`${configEnvFile} does not exist. ${err.message}`);
}
let options = {
@@ -114,18 +123,23 @@ function App() {
}
}
require(`${global.root_path}/js/check_config.js`);
try {
fs.accessSync(configFilename, fs.F_OK);
fs.accessSync(configFilename, fs.constants.F_OK);
const c = require(configFilename);
if (Object.keys(c).length === 0) {
Log.error("WARNING! Config file appears empty, maybe missing module.exports last line?");
}
checkDeprecatedOptions(c);
return Object.assign(defaults, c);
} catch (e) {
if (e.code === "ENOENT") {
Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
Log.error("WARNING! Could not find config file. Please create one. Starting with default configuration.");
} else if (e instanceof ReferenceError || e instanceof SyntaxError) {
Log.error(Utils.colors.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`));
Log.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`);
} else {
Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
Log.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`);
}
}
@@ -135,37 +149,56 @@ function App() {
/**
* Checks the config for deprecated options and throws a warning in the logs
* if it encounters one option from the deprecated.js list
*
* @param {object} userConfig The user config
*/
function checkDeprecatedOptions(userConfig) {
function checkDeprecatedOptions (userConfig) {
const deprecated = require(`${global.root_path}/js/deprecated`);
const deprecatedOptions = deprecated.configs;
// check for deprecated core options
const deprecatedOptions = deprecated.configs;
const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option));
if (usedDeprecated.length > 0) {
Log.warn(Utils.colors.warn(`WARNING! Your config is using deprecated options: ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`));
Log.warn(`WARNING! Your config is using deprecated option(s): ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`);
}
// check for deprecated module options
for (const element of userConfig.modules) {
if (deprecated[element.module] !== undefined && element.config !== undefined) {
const deprecatedModuleOptions = deprecated[element.module];
const usedDeprecatedModuleOptions = deprecatedModuleOptions.filter((option) => element.config.hasOwnProperty(option));
if (usedDeprecatedModuleOptions.length > 0) {
Log.warn(`WARNING! Your config for module ${element.module} is using deprecated option(s): ${usedDeprecatedModuleOptions.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`);
}
}
}
}
/**
* Loads a specific module.
*
* @param {string} module The name of the module (including subpath).
*/
function loadModule(module) {
function loadModule (module) {
const elements = module.split("/");
const moduleName = elements[elements.length - 1];
let moduleFolder = `${__dirname}/../modules/${module}`;
const env = getEnvVarsAsObj();
let moduleFolder = path.resolve(`${__dirname}/../${env.modulesDir}`, module);
if (defaultModules.includes(moduleName)) {
moduleFolder = `${__dirname}/../modules/default/${module}`;
const defaultModuleFolder = path.resolve(`${__dirname}/../modules/default/`, module);
if (process.env.JEST_WORKER_ID === undefined) {
moduleFolder = defaultModuleFolder;
} else {
// running in Jest, allow defaultModules placed under moduleDir for testing
if (env.modulesDir === "modules" || env.modulesDir === "tests/mocks") {
moduleFolder = defaultModuleFolder;
}
}
}
const moduleFile = `${moduleFolder}/${module}.js`;
const moduleFile = `${moduleFolder}/${moduleName}.js`;
try {
fs.accessSync(moduleFile, fs.R_OK);
fs.accessSync(moduleFile, fs.constants.R_OK);
} catch (e) {
Log.warn(`No ${moduleFile} found for module: ${moduleName}.`);
}
@@ -174,14 +207,21 @@ function App() {
let loadHelper = true;
try {
fs.accessSync(helperPath, fs.R_OK);
fs.accessSync(helperPath, fs.constants.R_OK);
} catch (e) {
loadHelper = false;
Log.log(`No helper found for module: ${moduleName}.`);
}
// if the helper was found
if (loadHelper) {
const Module = require(helperPath);
let Module;
try {
Module = require(helperPath);
} catch (e) {
Log.error(`Error when loading ${moduleName}:`, e.message);
return;
}
let m = new Module();
if (m.requiresVersion) {
@@ -204,42 +244,27 @@ function App() {
/**
* Loads all modules.
*
* @param {string[]} modules All modules to be loaded
* @param {Module[]} modules All modules to be loaded
* @returns {Promise} A promise that is resolved when all modules been loaded
*/
async function loadModules(modules) {
return new Promise((resolve) => {
Log.log("Loading module helpers ...");
async function loadModules (modules) {
Log.log("Loading module helpers ...");
/**
*
*/
function loadNextModule() {
if (modules.length > 0) {
const nextModule = modules[0];
loadModule(nextModule);
modules = modules.slice(1);
loadNextModule();
} else {
// All modules are loaded
Log.log("All module helpers loaded.");
resolve();
}
}
for (let module of modules) {
await loadModule(module);
}
loadNextModule();
});
Log.log("All module helpers loaded.");
}
/**
* Compare two semantic version numbers and return the difference.
*
* @param {string} a Version number a.
* @param {string} b Version number b.
* @returns {number} A positive number if a is larger than b, a negative
* number if a is smaller and 0 if they are the same
*/
function cmpVersions(a, b) {
function cmpVersions (a, b) {
let i, diff;
const regExStrip0 = /(\.0+)+$/;
const segmentsA = a.replace(regExStrip0, "").split(".");
@@ -259,7 +284,6 @@ function App() {
* Start the core app.
*
* It loads the config, then it loads all modules.
*
* @async
* @returns {Promise<object>} the config used
*/
@@ -268,12 +292,28 @@ function App() {
Log.setLogLevel(config.logLevel);
// get the used module positions
Utils.getModulePositions();
let modules = [];
for (const module of config.modules) {
if (!modules.includes(module.module) && !module.disabled) {
modules.push(module.module);
if (module.disabled) continue;
if (module.module) {
if (Utils.moduleHasValidPosition(module.position) || typeof (module.position) === "undefined") {
// Only add this module to be loaded if it is not a duplicate (repeated instance of the same module)
if (!modules.includes(module.module)) {
modules.push(module.module);
}
} else {
Log.warn("Invalid module position found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`);
}
} else {
Log.warn("No module name found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`);
}
}
setGlobalDispatcher(new Agent({ connect: { timeout: fetch_timeout } }));
await loadModules(modules);
httpServer = new Server(config);
@@ -312,7 +352,6 @@ function App() {
* exists.
*
* Added to fix #1056
*
* @returns {Promise} A promise that is resolved when all node_helpers and
* the http server has been closed
*/
@@ -325,7 +364,7 @@ function App() {
}
} catch (error) {
Log.error(`Error when stopping node_helper for module ${nodeHelper.name}:`);
console.error(error);
Log.error(error);
}
}

View File

@@ -1,27 +1,23 @@
/* MagicMirror²
*
* Check the configuration file for errors
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
const path = require("path");
const fs = require("fs");
const path = require("node:path");
const fs = require("node:fs");
const { styleText } = require("node:util");
const Ajv = require("ajv");
const globals = require("globals");
const { Linter } = require("eslint");
const linter = new Linter();
const rootPath = path.resolve(`${__dirname}/../`);
const Log = require(`${rootPath}/js/logger.js`);
const Utils = require(`${rootPath}/js/utils.js`);
const linter = new Linter({ configType: "flat" });
const ajv = new Ajv();
/**
* Returns a string with path of configuration file.
* Check if set by environment variable MM_CONFIG_FILE
*
* @returns {string} path and filename of the config file
*/
function getConfigFile() {
function getConfigFile () {
// FIXME: This function should be in core. Do you want refactor me ;) ?, be good!
return path.resolve(process.env.MM_CONFIG_FILE || `${rootPath}/config/config.js`);
}
@@ -29,45 +25,112 @@ function getConfigFile() {
/**
* Checks the config file using eslint.
*/
function checkConfigFile() {
function checkConfigFile () {
const configFileName = getConfigFile();
// Check if file is present
if (fs.existsSync(configFileName) === false) {
Log.error(Utils.colors.error("File not found: "), configFileName);
throw new Error("No config file present!");
throw new Error(`File not found: ${configFileName}\nNo config file present!`);
}
// Check permission
try {
fs.accessSync(configFileName, fs.F_OK);
} catch (e) {
Log.error(Utils.colors.error(e));
throw new Error("No permission to access config file!");
fs.accessSync(configFileName, fs.constants.F_OK);
} catch (error) {
throw new Error(`${error}\nNo permission to access config file!`);
}
// Validate syntax of the configuration file.
Log.info(Utils.colors.info("Checking file... "), configFileName);
Log.info(`Checking config file ${configFileName} ...`);
// I'm not sure if all ever is utf-8
const configFile = fs.readFileSync(configFileName, "utf-8");
// Explicitly tell linter that he might encounter es6 syntax ("let config = {...}")
const errors = linter.verify(configFile, {
env: {
es6: true
}
});
const errors = linter.verify(
configFile,
{
languageOptions: {
ecmaVersion: "latest",
globals: {
...globals.node
}
},
rules: { "no-undef": "error" }
},
configFileName
);
if (errors.length === 0) {
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
Log.info(styleText("green", "Your configuration file doesn't contain syntax errors :)"));
validateModulePositions(configFileName);
} else {
Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
let errorMessage = "Your configuration file contains syntax errors :(";
for (const error of errors) {
Log.error(`Line ${error.line} column ${error.column}: ${error.message}`);
errorMessage += `\nLine ${error.line} column ${error.column}: ${error.message}`;
}
throw new Error(errorMessage);
}
}
checkConfigFile();
/**
*
* @param {string} configFileName - The path and filename of the configuration file to validate.
*/
function validateModulePositions (configFileName) {
Log.info("Checking modules structure configuration ...");
const positionList = Utils.getModulePositions();
// Make Ajv schema configuration of modules config
// Only scan "module" and "position"
const schema = {
type: "object",
properties: {
modules: {
type: "array",
items: {
type: "object",
properties: {
module: {
type: "string"
},
position: {
type: "string",
enum: positionList
}
},
required: ["module"]
}
}
}
};
// Scan all modules
const validate = ajv.compile(schema);
const data = require(configFileName);
const valid = validate(data);
if (valid) {
Log.info(styleText("green", "Your modules structure configuration doesn't contain errors :)"));
} else {
const module = validate.errors[0].instancePath.split("/")[2];
const position = validate.errors[0].instancePath.split("/")[3];
let errorMessage = "This module configuration contains errors:";
errorMessage += `\n${JSON.stringify(data.modules[module], null, 2)}`;
if (position) {
errorMessage += `\n${position}: ${validate.errors[0].message}`;
errorMessage += `\n${JSON.stringify(validate.errors[0].params.allowedValues, null, 2).slice(1, -1)}`;
} else {
errorMessage += validate.errors[0].message;
}
Log.error(errorMessage);
}
}
try {
checkConfigFile();
} catch (error) {
Log.error(error.message);
process.exit(1);
}

View File

@@ -1,6 +1,7 @@
/* global Class, xyz */
/* Simple JavaScript Inheritance
/*
* Simple JavaScript Inheritance
* By John Resig https://johnresig.com/
*
* Inspired by base2 and Prototype
@@ -9,7 +10,7 @@
*/
(function () {
let initializing = false;
const fnTest = /xyz/.test(function () {
const fnTest = (/xyz/).test(function () {
xyz;
})
? /\b_super\b/
@@ -22,8 +23,10 @@
Class.extend = function (prop) {
let _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
/*
* Instantiate a base class (but only create the instance,
* don't run the init constructor)
*/
initializing = true;
const prototype = new this();
initializing = false;
@@ -36,31 +39,35 @@
// Copy the properties over onto the new prototype
for (const name in prop) {
// Check if we're overwriting an existing function
prototype[name] =
typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name])
prototype[name]
= typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name])
? (function (name, fn) {
return function () {
const tmp = this._super;
return function () {
const tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
/*
* Add a new ._super() method that is the same method
* but on the super-class
*/
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
const ret = fn.apply(this, arguments);
this._super = tmp;
/*
* The method only need to be bound temporarily, so we
* remove it when we're done executing
*/
const ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name])
return ret;
};
}(name, prop[name]))
: prop[name];
}
/**
* The dummy class constructor
*/
function Class() {
function Class () {
// All construction is actually done in the init method
if (!initializing && this.init) {
this.init.apply(this, arguments);
@@ -78,19 +85,22 @@
return Class;
};
})();
}());
/**
* Define the clone method for later use. Helper Method.
*
* @param {object} obj Object to be cloned
* @returns {object} the cloned object
*/
function cloneObject(obj) {
function cloneObject (obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (obj.constructor.name === "RegExp") {
return new RegExp(obj);
}
const temp = obj.constructor(); // give temp the original obj's constructor
for (const key in obj) {
temp[key] = cloneObject(obj[key]);

View File

@@ -1,11 +1,5 @@
/* global mmPort */
/* MagicMirror²
* Config Defaults
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const address = "localhost";
let port = 8080;
if (typeof mmPort !== "undefined") {
@@ -25,10 +19,16 @@ const defaults = {
units: "metric",
zoom: 1,
customCss: "css/custom.css",
foreignModulesDir: "modules",
// httpHeaders used by helmet, see https://helmetjs.github.io/. You can add other/more object values by overriding this in config.js,
// e.g. you need to add `frameguard: false` for embedding MagicMirror in another website, see https://github.com/MichMich/MagicMirror/issues/2847
// e.g. you need to add `frameguard: false` for embedding MagicMirror in another website, see https://github.com/MagicMirrorOrg/MagicMirror/issues/2847
httpHeaders: { contentSecurityPolicy: false, crossOriginOpenerPolicy: false, crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false },
// properties for checking if server is alive and has same startup-timestamp, the check is per default enabled
// (interval 30 seconds). If startup-timestamp has changed the client reloads the magicmirror webpage.
checkServerInterval: 30 * 1000,
reloadAfterServerRestart: false,
modules: [
{
module: "updatenotification",
@@ -62,7 +62,7 @@ const defaults = {
position: "middle_center",
classes: "xsmall",
config: {
text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror² directory<br>" + "<pre>npm run config:check</pre>"
text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror² directory<br>" + "<pre>node --run config:check</pre>"
}
},
{
@@ -70,15 +70,10 @@ const defaults = {
position: "bottom_bar",
classes: "xsmall dimmed",
config: {
text: "www.michaelteeuw.nl"
text: "https://magicmirror.builders/"
}
}
],
paths: {
modules: "modules",
vendor: "vendor"
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/

View File

@@ -1,11 +1,4 @@
/* MagicMirror² Deprecated Config Options List
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*
* Olex S. original idea this deprecated option
*/
module.exports = {
configs: ["kioskmode"]
configs: ["kioskmode"],
clock: ["secondsColor"]
};

View File

@@ -8,28 +8,47 @@ const Log = require("./logger");
let config = process.env.config ? JSON.parse(process.env.config) : {};
// Module to control application life.
const app = electron.app;
// If ELECTRON_DISABLE_GPU is set electron is started with --disable-gpu flag.
// See https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering for more info.
if (process.env.ELECTRON_DISABLE_GPU !== undefined) {
/*
* Per default electron is started with --disable-gpu flag, if you want the gpu enabled,
* you must set the env var ELECTRON_ENABLE_GPU=1 on startup.
* See https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering for more info.
*/
if (process.env.ELECTRON_ENABLE_GPU !== "1") {
app.disableHardwareAcceleration();
}
// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow;
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
/*
* Keep a global reference of the window object, if you don't, the window will
* be closed automatically when the JavaScript object is garbage collected.
*/
let mainWindow;
/**
*
*/
function createWindow() {
function createWindow () {
/*
* see https://www.electronjs.org/docs/latest/api/screen
* Create a window that fills the screen's available work area.
*/
let electronSize = (800, 600);
try {
electronSize = electron.screen.getPrimaryDisplay().workAreaSize;
} catch {
Log.warn("Could not get display size, using defaults ...");
}
let electronSwitchesDefaults = ["autoplay-policy", "no-user-gesture-required"];
app.commandLine.appendSwitch(...new Set(electronSwitchesDefaults, config.electronSwitches));
let electronOptionsDefaults = {
width: 800,
height: 600,
width: electronSize.width,
height: electronSize.height,
icon: "mm2.png",
x: 0,
y: 0,
darkTheme: true,
@@ -41,8 +60,10 @@ function createWindow() {
backgroundColor: "#000000"
};
// DEPRECATED: "kioskmode" backwards compatibility, to be removed
// settings these options directly instead provides cleaner interface
/*
* DEPRECATED: "kioskmode" backwards compatibility, to be removed
* settings these options directly instead provides cleaner interface
*/
if (config.kioskmode) {
electronOptionsDefaults.kiosk = true;
} else {
@@ -50,27 +71,48 @@ function createWindow() {
electronOptionsDefaults.frame = false;
electronOptionsDefaults.transparent = true;
electronOptionsDefaults.hasShadow = false;
electronOptionsDefaults.fullscreen = true;
}
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
if (process.env.JEST_WORKER_ID !== undefined && process.env.MOCK_DATE !== undefined) {
// if we are running with jest and we want to mock the current date
const fakeNow = new Date(process.env.MOCK_DATE).valueOf();
Date = class extends Date {
constructor (...args) {
if (args.length === 0) {
super(fakeNow);
} else {
super(...args);
}
}
};
const __DateNowOffset = fakeNow - Date.now();
const __DateNow = Date.now;
Date.now = () => __DateNow() + __DateNowOffset;
}
// Create the browser window.
mainWindow = new BrowserWindow(electronOptions);
// and load the index.html of the app.
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
/*
* and load the index.html of the app.
* If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
*/
let prefix;
if ((config["tls"] !== null && config["tls"]) || config.useHttps) {
if ((config.tls !== null && config.tls) || config.useHttps) {
prefix = "https://";
} else {
prefix = "http://";
}
let address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address;
mainWindow.loadURL(`${prefix}${address}:${config.port}`);
let address = (config.address === void 0) | (config.address === "") | (config.address === "0.0.0.0") ? (config.address = "localhost") : config.address;
const port = process.env.MM_PORT || config.port;
mainWindow.loadURL(`${prefix}${address}:${port}`);
// Open the DevTools if run with "npm start dev"
// Open the DevTools if run with "node --run start:dev"
if (process.argv.includes("dev")) {
if (process.env.JEST_WORKER_ID !== undefined) {
// if we are running with jest
@@ -109,30 +151,22 @@ function createWindow() {
//remove response headers that prevent sites of being embedded into iframes if configured
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
let curHeaders = details.responseHeaders;
if (config["ignoreXOriginHeader"] || false) {
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !/x-frame-options/i.test(header[0])));
if (config.ignoreXOriginHeader || false) {
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !(/x-frame-options/i).test(header[0])));
}
if (config["ignoreContentSecurityPolicy"] || false) {
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !/content-security-policy/i.test(header[0])));
if (config.ignoreContentSecurityPolicy || false) {
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !(/content-security-policy/i).test(header[0])));
}
callback({ responseHeaders: curHeaders });
});
mainWindow.once("ready-to-show", () => {
mainWindow.setFullScreen(true);
mainWindow.show();
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on("ready", function () {
Log.log("Launching application.");
createWindow();
});
// Quit when all windows are closed.
app.on("window-all-closed", function () {
if (process.env.JEST_WORKER_ID !== undefined) {
@@ -144,14 +178,18 @@ app.on("window-all-closed", function () {
});
app.on("activate", function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
/*
* On OS X it's common to re-create a window in the app when the
* dock icon is clicked and there are no other windows open.
*/
if (mainWindow === null) {
createWindow();
}
});
/* This method will be called when SIGINT is received and will call
/*
* This method will be called when SIGINT is received and will call
* each node_helper's stop function if it exists. Added to fix #1056
*
* Note: this is only used if running Electron. Otherwise
@@ -175,8 +213,23 @@ app.on("certificate-error", (event, webContents, url, error, certificate, callba
callback(true);
});
// Start the core application if server is run on localhost
// This starts all node helpers and starts the webserver.
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
core.start().then((c) => (config = c));
if (process.env.clientonly) {
app.whenReady().then(() => {
Log.log("Launching client viewer application.");
createWindow();
});
}
/*
* Start the core application if server is run on localhost
* This starts all node helpers and starts the webserver.
*/
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
core.start().then((c) => {
config = c;
app.whenReady().then(() => {
Log.log("Launching application.");
createWindow();
});
});
}

View File

@@ -1,28 +0,0 @@
/**
* Helper class to provide either third party fetch library or (if node >= 18)
* return internal node fetch implementation.
*
* Attention: After some discussion we always return the third party
* implementation until the node implementation is stable and more tested
*
* @see https://github.com/MichMich/MagicMirror/pull/2952
* @see https://github.com/MichMich/MagicMirror/issues/2649
* @param {string} url to be fetched
* @param {object} options object e.g. for headers
* @class
*/
async function fetch(url, options = {}) {
// const nodeVersion = process.version.match(/^v(\d+)\.*/)[1];
// if (nodeVersion >= 18) {
// // node version >= 18
// return global.fetch(url, options);
// } else {
// // node version < 18
// const nodefetch = require("node-fetch");
// return nodefetch(url, options);
// }
const nodefetch = require("node-fetch");
return nodefetch(url, options);
}
module.exports = fetch;

View File

@@ -1,12 +1,7 @@
/* global defaultModules, vendor */
/* MagicMirror²
* Module and File loaders.
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const Loader = (function () {
/* Create helper variables */
const loadedModuleFiles = [];
@@ -15,6 +10,15 @@ const Loader = (function () {
/* Private Methods */
/**
* Retrieve object of env variables.
* @returns {object} with key: values as assembled in js/server_functions.js
*/
const getEnvVars = async function () {
const res = await fetch(`${location.protocol}//${location.host}${config.basePath}env`);
return JSON.parse(await res.text());
};
/**
* Loops through all modules and requests start for every module.
*/
@@ -52,31 +56,39 @@ const Loader = (function () {
/**
* Retrieve list of all modules.
*
* @returns {object[]} module data as configured in config
*/
const getAllModules = function () {
return config.modules;
const AllModules = config.modules.filter((module) => (module.module !== undefined) && (MM.getAvailableModulePositions.indexOf(module.position) > -1 || typeof (module.position) === "undefined"));
return AllModules;
};
/**
* Generate array with module information including module paths.
*
* @returns {object[]} Module information.
*/
const getModuleData = function () {
const getModuleData = async function () {
const modules = getAllModules();
const moduleFiles = [];
const envVars = await getEnvVars();
modules.forEach(function (moduleData, index) {
const module = moduleData.module;
const elements = module.split("/");
const moduleName = elements[elements.length - 1];
let moduleFolder = `${config.paths.modules}/${module}`;
let moduleFolder = `${envVars.modulesDir}/${module}`;
if (defaultModules.indexOf(moduleName) !== -1) {
moduleFolder = `${config.paths.modules}/default/${module}`;
const defaultModuleFolder = `modules/default/${module}`;
if (window.name !== "jsdom") {
moduleFolder = defaultModuleFolder;
} else {
// running in Jest, allow defaultModules placed under moduleDir for testing
if (envVars.modulesDir === "modules") {
moduleFolder = defaultModuleFolder;
}
}
}
if (moduleData.disabled === true) {
@@ -90,11 +102,14 @@ const Loader = (function () {
path: `${moduleFolder}/`,
file: `${moduleName}.js`,
position: moduleData.position,
animateIn: moduleData.animateIn,
animateOut: moduleData.animateOut,
hiddenOnStartup: moduleData.hiddenOnStartup,
header: moduleData.header,
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,
config: moduleData.config,
classes: typeof moduleData.classes !== "undefined" ? `${moduleData.classes} ${module}` : module
classes: typeof moduleData.classes !== "undefined" ? `${moduleData.classes} ${module}` : module,
order: (typeof moduleData.order === "number" && Number.isInteger(moduleData.order)) ? moduleData.order : 0
});
});
@@ -103,7 +118,6 @@ const Loader = (function () {
/**
* Load modules via ajax request and create module objects.
*
* @param {object} module Information about the module we want to load.
* @returns {Promise<void>} resolved when module is loaded
*/
@@ -131,7 +145,6 @@ const Loader = (function () {
/**
* Bootstrap modules by setting the module data and loading the scripts & styles.
*
* @param {object} module Information about the module we want to load.
* @param {Module} mObj Modules instance.
*/
@@ -153,7 +166,6 @@ const Loader = (function () {
/**
* Load a script or stylesheet by adding it to the dom.
*
* @param {string} fileName Path of the file we want to load.
* @returns {Promise} resolved when the file is loaded
*/
@@ -173,6 +185,7 @@ const Loader = (function () {
};
script.onerror = function () {
Log.error("Error on loading script:", fileName);
script.remove();
resolve();
};
document.getElementsByTagName("body")[0].appendChild(script);
@@ -190,6 +203,7 @@ const Loader = (function () {
};
stylesheet.onerror = function () {
Log.error("Error on loading stylesheet:", fileName);
stylesheet.remove();
resolve();
};
document.getElementsByTagName("head")[0].appendChild(stylesheet);
@@ -199,42 +213,37 @@ const Loader = (function () {
/* Public Methods */
return {
/**
* Load all modules as defined in the config.
*/
loadModules: async function () {
let moduleData = getModuleData();
async loadModules () {
const moduleData = await getModuleData();
const envVars = await getEnvVars();
const customCss = envVars.customCss;
/**
* @returns {Promise<void>} when all modules are loaded
*/
const loadNextModule = async function () {
if (moduleData.length > 0) {
const nextModule = moduleData[0];
await loadModule(nextModule);
moduleData = moduleData.slice(1);
await loadNextModule();
} else {
// All modules loaded. Load custom.css
// This is done after all the modules so we can
// overwrite all the defined styles.
await loadFile(config.customCss);
// custom.css loaded. Start all modules.
await startModules();
}
};
await loadNextModule();
// Load all modules
for (const module of moduleData) {
await loadModule(module);
}
// Load custom.css
// Since this happens after loading the modules,
// it overwrites the default styles.
await loadFile(customCss);
// Start all modules.
await startModules();
},
/**
* Load a file (script or stylesheet).
* Prevent double loading and search for files in the vendor folder.
*
* @param {string} fileName Path of the file we want to load.
* @param {Module} module The module that calls the loadFile function.
* @returns {Promise} resolved when the file is loaded
*/
loadFileForModule: async function (fileName, module) {
async loadFileForModule (fileName, module) {
if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) {
Log.log(`File already loaded: ${fileName}`);
return;
@@ -251,7 +260,7 @@ const Loader = (function () {
// This file is available in the vendor folder.
// Load it from this vendor folder.
loadedFiles.push(fileName.toLowerCase());
return loadFile(`${config.paths.vendor}/${vendor[fileName]}`);
return loadFile(`${vendor[fileName]}`);
}
// File not loaded yet.
@@ -260,4 +269,4 @@ const Loader = (function () {
return loadFile(module.file(fileName));
}
};
})();
}());

View File

@@ -1,19 +1,49 @@
/* MagicMirror²
* Log
*
* This logger is very simple, but needs to be extended.
* This system can eventually be used to push the log messages to an external target.
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
// This logger is very simple, but needs to be extended.
(function (root, factory) {
if (typeof exports === "object") {
if (process.env.JEST_WORKER_ID === undefined) {
const { styleText } = require("node:util");
// add timestamps in front of log messages
require("console-stamp")(console, {
pattern: "yyyy-mm-dd HH:MM:ss.l",
include: ["debug", "log", "info", "warn", "error"]
format: ":date(yyyy-mm-dd HH:MM:ss.l) :label(7) :msg",
tokens: {
label: (arg) => {
const { method, defaultTokens } = arg;
let label = defaultTokens.label(arg);
switch (method) {
case "error":
label = styleText("red", label);
break;
case "warn":
label = styleText("yellow", label);
break;
case "debug":
label = styleText("bgBlue", label);
break;
case "info":
label = styleText("blue", label);
break;
}
return label;
},
msg: (arg) => {
const { method, defaultTokens } = arg;
let msg = defaultTokens.msg(arg);
switch (method) {
case "error":
msg = styleText("red", msg);
break;
case "warn":
msg = styleText("yellow", msg);
break;
case "info":
msg = styleText("blue", msg);
break;
}
return msg;
}
}
});
}
// Node, CommonJS-like
@@ -22,7 +52,7 @@
// Browser globals (root is window)
root.Log = factory(root.config);
}
})(this, function (config) {
}(this, function (config) {
let logLevel;
let enableLog;
if (typeof exports === "object") {
@@ -50,7 +80,7 @@
logLevel.setLogLevel = function (newLevel) {
if (newLevel) {
Object.keys(logLevel).forEach(function (key, index) {
Object.keys(logLevel).forEach(function (key) {
if (!newLevel.includes(key.toLocaleUpperCase())) {
logLevel[key] = function () {};
}
@@ -59,21 +89,21 @@
};
} else {
logLevel = {
debug: function () {},
log: function () {},
info: function () {},
warn: function () {},
error: function () {},
group: function () {},
groupCollapsed: function () {},
groupEnd: function () {},
time: function () {},
timeEnd: function () {},
timeStamp: function () {}
debug () {},
log () {},
info () {},
warn () {},
error () {},
group () {},
groupCollapsed () {},
groupEnd () {},
time () {},
timeEnd () {},
timeStamp () {}
};
logLevel.setLogLevel = function () {};
}
return logLevel;
});
}));

View File

@@ -1,11 +1,5 @@
/* global Loader, defaults, Translator */
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut, modulePositions */
/* MagicMirror²
* Main System
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const MM = (function () {
let modules = [];
@@ -22,6 +16,10 @@ const MM = (function () {
return;
}
let haveAnimateIn = null;
// check if have valid animateIn in module definition (module.data.animateIn)
if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateIn = module.data.animateIn;
const wrapper = selectWrapper(module.data.position);
const dom = document.createElement("div");
@@ -32,6 +30,8 @@ const MM = (function () {
dom.className = `module ${dom.className} ${module.data.classes}`;
}
dom.style.order = (typeof module.data.order === "number" && Number.isInteger(module.data.order)) ? module.data.order : 0;
dom.opacity = 0;
wrapper.appendChild(dom);
@@ -50,7 +50,12 @@ const MM = (function () {
moduleContent.className = "module-content";
dom.appendChild(moduleContent);
const domCreationPromise = updateDom(module, 0);
// create the domCreationPromise with AnimateCSS (with animateIn of module definition)
// or just display it
var domCreationPromise;
if (haveAnimateIn) domCreationPromise = updateDom(module, { options: { speed: 1000, animate: { in: haveAnimateIn } } }, true);
else domCreationPromise = updateDom(module, 0);
domCreationPromises.push(domCreationPromise);
domCreationPromise
.then(function () {
@@ -68,7 +73,6 @@ const MM = (function () {
/**
* Select the wrapper dom object for a specific position.
*
* @param {string} position The name of the position.
* @returns {HTMLElement | void} the wrapper element
*/
@@ -85,7 +89,6 @@ const MM = (function () {
/**
* Send a notification to all modules.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {Module} sender The module that sent the notification.
@@ -102,13 +105,31 @@ const MM = (function () {
/**
* Update the dom for a specific module.
*
* @param {Module} module The module that needs an update.
* @param {number} [speed] The (optional) number of microseconds for the animation.
* @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates)
* @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start of MagicMirror)
* @returns {Promise} Resolved when the dom is fully updated.
*/
const updateDom = function (module, speed) {
const updateDom = function (module, updateOptions, createAnimatedDom = false) {
return new Promise(function (resolve) {
let speed = updateOptions;
let animateOut = null;
let animateIn = null;
if (typeof updateOptions === "object") {
if (typeof updateOptions.options === "object" && updateOptions.options.speed !== undefined) {
speed = updateOptions.options.speed;
Log.debug(`updateDom: ${module.identifier} Has speed in object: ${speed}`);
if (typeof updateOptions.options.animate === "object") {
animateOut = updateOptions.options.animate.out;
animateIn = updateOptions.options.animate.in;
Log.debug(`updateDom: ${module.identifier} Has animate in object: out->${animateOut}, in->${animateIn}`);
}
} else {
Log.debug(`updateDom: ${module.identifier} Has no speed in object`);
speed = 0;
}
}
const newHeader = module.getHeader();
let newContentPromise = module.getDom();
@@ -119,7 +140,7 @@ const MM = (function () {
newContentPromise
.then(function (newContent) {
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom);
updatePromise.then(resolve).catch(Log.error);
})
@@ -129,14 +150,16 @@ const MM = (function () {
/**
* Update the dom with the specified content
*
* @param {Module} module The module that needs an update.
* @param {number} [speed] The (optional) number of microseconds for the animation.
* @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated.
* @param {string} [animateOut] AnimateCss animation name before hidden
* @param {string} [animateIn] AnimateCss animation name on show
* @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start)
* @returns {Promise} Resolved when the module dom has been updated.
*/
const updateDomWithContent = function (module, speed, newHeader, newContent) {
const updateDomWithContent = function (module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom = false) {
return new Promise(function (resolve) {
if (module.hidden || !speed) {
updateModuleContent(module, newHeader, newContent);
@@ -155,19 +178,33 @@ const MM = (function () {
return;
}
hideModule(module, speed / 2, function () {
if (createAnimatedDom && animateIn !== null) {
Log.debug(`${module.identifier} createAnimatedDom (${animateIn})`);
updateModuleContent(module, newHeader, newContent);
if (!module.hidden) {
showModule(module, speed / 2);
showModule(module, speed, null, { animate: animateIn });
}
resolve();
});
return;
}
hideModule(
module,
speed / 2,
function () {
updateModuleContent(module, newHeader, newContent);
if (!module.hidden) {
showModule(module, speed / 2, null, { animate: animateIn });
}
resolve();
},
{ animate: animateOut }
);
});
};
/**
* Check if the content has changed.
*
* @param {Module} module The module to check.
* @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated.
@@ -198,7 +235,6 @@ const MM = (function () {
/**
* Update the content of a module on screen.
*
* @param {Module} module The module to check.
* @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated.
@@ -224,15 +260,12 @@ const MM = (function () {
/**
* Hide the module.
*
* @param {Module} module The module to hide.
* @param {number} speed The speed of the hide animation.
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the hide method.
*/
const hideModule = function (module, speed, callback, options) {
options = options || {};
const hideModule = function (module, speed, callback, options = {}) {
// set lockString if set in options.
if (options.lockString) {
// Log.log("Has lockstring: " + options.lockString);
@@ -243,24 +276,65 @@ const MM = (function () {
const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) {
moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
moduleWrapper.style.opacity = 0;
moduleWrapper.classList.add("hidden");
clearTimeout(module.showHideTimer);
module.showHideTimer = setTimeout(function () {
// To not take up any space, we just make the position absolute.
// since it's fade out anyway, we can see it lay above or
// below other modules. This works way better than adjusting
// the .display property.
moduleWrapper.style.position = "fixed";
// reset all animations if needed
if (module.hasAnimateOut) {
removeAnimateCSS(module.identifier, module.hasAnimateOut);
Log.debug(`${module.identifier} Force remove animateOut (in hide): ${module.hasAnimateOut}`);
module.hasAnimateOut = false;
}
if (module.hasAnimateIn) {
removeAnimateCSS(module.identifier, module.hasAnimateIn);
Log.debug(`${module.identifier} Force remove animateIn (in hide): ${module.hasAnimateIn}`);
module.hasAnimateIn = false;
}
// haveAnimateName for verify if we are using AnimateCSS library
// we check AnimateCSSOut Array for validate it
// and finally return the animate name or `null` (for default MM² animation)
let haveAnimateName = null;
// check if have valid animateOut in module definition (module.data.animateOut)
if (module.data.animateOut && AnimateCSSOut.indexOf(module.data.animateOut) !== -1) haveAnimateName = module.data.animateOut;
// can't be override with options.animate
else if (options.animate && AnimateCSSOut.indexOf(options.animate) !== -1) haveAnimateName = options.animate;
updateWrapperStates();
if (haveAnimateName) {
// with AnimateCSS
Log.debug(`${module.identifier} Has animateOut: ${haveAnimateName}`);
module.hasAnimateOut = haveAnimateName;
addAnimateCSS(module.identifier, haveAnimateName, speed / 1000);
module.showHideTimer = setTimeout(function () {
removeAnimateCSS(module.identifier, haveAnimateName);
Log.debug(`${module.identifier} Remove animateOut: ${module.hasAnimateOut}`);
// AnimateCSS is now done
moduleWrapper.style.opacity = 0;
moduleWrapper.classList.add("hidden");
moduleWrapper.style.position = "fixed";
module.hasAnimateOut = false;
if (typeof callback === "function") {
callback();
}
}, speed);
updateWrapperStates();
if (typeof callback === "function") {
callback();
}
}, speed);
} else {
// default MM² Animate
moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
moduleWrapper.style.opacity = 0;
moduleWrapper.classList.add("hidden");
module.showHideTimer = setTimeout(function () {
// To not take up any space, we just make the position absolute.
// since it's fade out anyway, we can see it lay above or
// below other modules. This works way better than adjusting
// the .display property.
moduleWrapper.style.position = "fixed";
updateWrapperStates();
if (typeof callback === "function") {
callback();
}
}, speed);
}
} else {
// invoke callback even if no content, issue 1308
if (typeof callback === "function") {
@@ -271,15 +345,12 @@ const MM = (function () {
/**
* Show the module.
*
* @param {Module} module The module to show.
* @param {number} speed The speed of the show animation.
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the show method.
*/
const showModule = function (module, speed, callback, options) {
options = options || {};
const showModule = function (module, speed, callback, options = {}) {
// remove lockString if set in options.
if (options.lockString) {
const index = module.lockStrings.indexOf(options.lockString);
@@ -288,7 +359,7 @@ const MM = (function () {
}
}
// Check if there are no more lockstrings set, or the force option is set.
// Check if there are no more lockStrings set, or the force option is set.
// Otherwise cancel show action.
if (module.lockStrings.length !== 0 && options.force !== true) {
Log.log(`Will not show ${module.name}. LockStrings active: ${module.lockStrings.join(",")}`);
@@ -297,10 +368,21 @@ const MM = (function () {
}
return;
}
// reset all animations if needed
if (module.hasAnimateOut) {
removeAnimateCSS(module.identifier, module.hasAnimateOut);
Log.debug(`${module.identifier} Force remove animateOut (in show): ${module.hasAnimateOut}`);
module.hasAnimateOut = false;
}
if (module.hasAnimateIn) {
removeAnimateCSS(module.identifier, module.hasAnimateIn);
Log.debug(`${module.identifier} Force remove animateIn (in show): ${module.hasAnimateIn}`);
module.hasAnimateIn = false;
}
module.hidden = false;
// If forced show, clean current lockstrings.
// If forced show, clean current lockStrings.
if (module.lockStrings.length !== 0 && options.force === true) {
Log.log(`Force show of module: ${module.name}`);
module.lockStrings = [];
@@ -308,7 +390,18 @@ const MM = (function () {
const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) {
moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
clearTimeout(module.showHideTimer);
// haveAnimateName for verify if we are using AnimateCSS library
// we check AnimateCSSIn Array for validate it
// and finally return the animate name or `null` (for default MM² animation)
let haveAnimateName = null;
// check if have valid animateOut in module definition (module.data.animateIn)
if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateName = module.data.animateIn;
// can't be override with options.animate
else if (options.animate && AnimateCSSIn.indexOf(options.animate) !== -1) haveAnimateName = options.animate;
if (!haveAnimateName) moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
// Restore the position. See hideModule() for more info.
moduleWrapper.style.position = "static";
moduleWrapper.classList.remove("hidden");
@@ -319,12 +412,27 @@ const MM = (function () {
const dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
moduleWrapper.style.opacity = 1;
clearTimeout(module.showHideTimer);
module.showHideTimer = setTimeout(function () {
if (typeof callback === "function") {
callback();
}
}, speed);
if (haveAnimateName) {
// with AnimateCSS
Log.debug(`${module.identifier} Has animateIn: ${haveAnimateName}`);
module.hasAnimateIn = haveAnimateName;
addAnimateCSS(module.identifier, haveAnimateName, speed / 1000);
module.showHideTimer = setTimeout(function () {
removeAnimateCSS(module.identifier, haveAnimateName);
Log.debug(`${module.identifier} Remove animateIn: ${haveAnimateName}`);
module.hasAnimateIn = false;
if (typeof callback === "function") {
callback();
}
}, speed);
} else {
// default MM² Animate
module.showHideTimer = setTimeout(function () {
if (typeof callback === "function") {
callback();
}
}, speed);
}
} else {
// invoke callback
if (typeof callback === "function") {
@@ -344,10 +452,9 @@ const MM = (function () {
* an ugly top margin. By using this function, the top bar will be hidden if the
* update notification is not visible.
*/
const updateWrapperStates = function () {
const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
positions.forEach(function (position) {
const updateWrapperStates = function () {
modulePositions.forEach(function (position) {
const wrapper = selectWrapper(position);
const moduleWrappers = wrapper.getElementsByClassName("module");
@@ -358,7 +465,8 @@ const MM = (function () {
}
});
wrapper.style.display = showWrapper ? "block" : "none";
// move container definitions to main CSS
wrapper.className = showWrapper ? "container" : "container hidden";
});
};
@@ -367,7 +475,6 @@ const MM = (function () {
*/
const loadConfig = function () {
// FIXME: Think about how to pass config around without breaking tests
/* eslint-disable */
if (typeof config === "undefined") {
config = defaults;
Log.error("Config file is missing! Please create a config file.");
@@ -375,18 +482,16 @@ const MM = (function () {
}
config = Object.assign({}, defaults, config);
/* eslint-enable */
};
/**
* Adds special selectors on a collection of modules.
*
* @param {Module[]} modules Array of modules.
*/
const setSelectionMethodsForModules = function (modules) {
/**
* Filter modules with the specified classes.
*
* @param {string|string[]} className one or multiple classnames (array or space divided).
* @returns {Module[]} Filtered collection of modules.
*/
@@ -396,7 +501,6 @@ const MM = (function () {
/**
* Filter modules without the specified classes.
*
* @param {string|string[]} className one or multiple classnames (array or space divided).
* @returns {Module[]} Filtered collection of modules.
*/
@@ -406,7 +510,6 @@ const MM = (function () {
/**
* Filters a collection of modules based on classname(s).
*
* @param {string|string[]} className one or multiple classnames (array or space divided).
* @param {boolean} include if the filter should include or exclude the modules with the specific classes.
* @returns {Module[]} Filtered collection of modules.
@@ -435,7 +538,6 @@ const MM = (function () {
/**
* Removes a module instance from the collection.
*
* @param {object} module The module instance to remove from the collection.
* @returns {Module[]} Filtered collection of modules.
*/
@@ -450,7 +552,6 @@ const MM = (function () {
/**
* Walks thru a collection of modules and executes the callback with the module as an argument.
*
* @param {Function} callback The function to execute with the module as an argument.
*/
const enumerate = function (callback) {
@@ -474,12 +575,13 @@ const MM = (function () {
};
return {
/* Public Methods */
/**
* Main init method.
*/
init: async function () {
async init () {
Log.info("Initializing MagicMirror².");
loadConfig();
@@ -491,27 +593,46 @@ const MM = (function () {
/**
* Gets called when all modules are started.
*
* @param {Module[]} moduleObjects All module instances.
*/
modulesStarted: function (moduleObjects) {
modulesStarted (moduleObjects) {
modules = [];
let startUp = "";
moduleObjects.forEach((module) => modules.push(module));
Log.info("All modules started!");
sendNotification("ALL_MODULES_STARTED");
createDomObjects();
if (config.reloadAfterServerRestart) {
setInterval(async () => {
// if server startup time has changed (which means server was restarted)
// the client reloads the mm page
try {
const res = await fetch(`${location.protocol}//${location.host}${config.basePath}startup`);
const curr = await res.text();
if (startUp === "") startUp = curr;
if (startUp !== curr) {
startUp = "";
window.location.reload(true);
Log.warn("Refreshing Website because server was restarted");
}
} catch (err) {
Log.error(`MagicMirror not reachable: ${err}`);
}
}, config.checkServerInterval);
}
},
/**
* Send a notification to all modules.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {Module} sender The module that sent the notification.
*/
sendNotification: function (notification, payload, sender) {
sendNotification (notification, payload, sender) {
if (arguments.length < 3) {
Log.error("sendNotification: Missing arguments.");
return;
@@ -533,11 +654,10 @@ const MM = (function () {
/**
* Update the dom for a specific module.
*
* @param {Module} module The module that needs an update.
* @param {number} [speed] The number of microseconds for the animation.
* @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates)
*/
updateDom: function (module, speed) {
updateDom (module, updateOptions) {
if (!(module instanceof Module)) {
Log.error("updateDom: Sender should be a module.");
return;
@@ -549,46 +669,49 @@ const MM = (function () {
}
// Further implementation is done in the private method.
updateDom(module, speed);
updateDom(module, updateOptions).then(function () {
// Once the update is complete and rendered, send a notification to the module that the DOM has been updated
sendNotification("MODULE_DOM_UPDATED", null, null, module);
});
},
/**
* Returns a collection of all modules currently active.
*
* @returns {Module[]} A collection of all modules currently active.
*/
getModules: function () {
getModules () {
setSelectionMethodsForModules(modules);
return modules;
},
/**
* Hide the module.
*
* @param {Module} module The module to hide.
* @param {number} speed The speed of the hide animation.
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the hide method.
*/
hideModule: function (module, speed, callback, options) {
hideModule (module, speed, callback, options) {
module.hidden = true;
hideModule(module, speed, callback, options);
},
/**
* Show the module.
*
* @param {Module} module The module to show.
* @param {number} speed The speed of the show animation.
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the show method.
*/
showModule: function (module, speed, callback, options) {
showModule (module, speed, callback, options) {
// do not change module.hidden yet, only if we really show it later
showModule(module, speed, callback, options);
}
},
// Return all available module positions.
getAvailableModulePositions: modulePositions
};
})();
}());
// Add polyfill for Object.assign.
if (typeof Object.assign !== "function") {
@@ -611,7 +734,7 @@ if (typeof Object.assign !== "function") {
}
return output;
};
})();
}());
}
MM.init();

View File

@@ -1,16 +1,16 @@
/* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */
/* MagicMirror²
/*
* Module Blueprint.
* @typedef {Object} Module
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const Module = Class.extend({
/*********************************************************
/**
********************************************************
* All methods (and properties) below can be subclassed. *
*********************************************************/
********************************************************
*/
// Set the minimum MagicMirror² module version for this module.
requiresVersion: "2.0.0",
@@ -21,44 +21,46 @@ const Module = Class.extend({
// Timer reference used for showHide animation callbacks.
showHideTimer: null,
// Array to store lockStrings. These strings are used to lock
// visibility when hiding and showing module.
/*
* Array to store lockStrings. These strings are used to lock
* visibility when hiding and showing module.
*/
lockStrings: [],
// Storage of the nunjucks Environment,
// This should not be referenced directly.
// Use the nunjucksEnvironment() to get it.
/*
* Storage of the nunjucks Environment,
* This should not be referenced directly.
* Use the nunjucksEnvironment() to get it.
*/
_nunjucksEnvironment: null,
/**
* Called when the module is instantiated.
*/
init: function () {
init () {
//Log.log(this.defaults);
},
/**
* Called when the module is started.
*/
start: async function () {
async start () {
Log.info(`Starting module: ${this.name}`);
},
/**
* Returns a list of scripts the module requires to be loaded.
*
* @returns {string[]} An array with filenames.
*/
getScripts: function () {
getScripts () {
return [];
},
/**
* Returns a list of stylesheets the module requires to be loaded.
*
* @returns {string[]} An array with filenames.
*/
getStyles: function () {
getStyles () {
return [];
},
@@ -66,10 +68,9 @@ const Module = Class.extend({
* Returns a map of translation files the module requires to be loaded.
*
* return Map<String, String> -
*
* @returns {*} A map with langKeys and filenames.
*/
getTranslations: function () {
getTranslations () {
return false;
},
@@ -77,17 +78,16 @@ const Module = Class.extend({
* Generates the dom which needs to be displayed. This method is called by the MagicMirror² core.
* This method can to be subclassed if the module wants to display info on the mirror.
* Alternatively, the getTemplate method could be subclassed.
*
* @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
*/
getDom: function () {
getDom () {
return new Promise((resolve) => {
const div = document.createElement("div");
const template = this.getTemplate();
const templateData = this.getTemplateData();
// Check to see if we need to render a template string or a file.
if (/^.*((\.html)|(\.njk))$/.test(template)) {
if ((/^.*((\.html)|(\.njk))$/).test(template)) {
// the template is a filename
this.nunjucksEnvironment().render(template, templateData, function (err, res) {
if (err) {
@@ -111,10 +111,9 @@ const Module = Class.extend({
* Generates the header string which needs to be displayed if a user has a header configured for this module.
* This method is called by the MagicMirror² core, but only if the user has configured a default header for the module.
* This method needs to be subclassed if the module wants to display modified headers on the mirror.
*
* @returns {string} The header to display above the header.
*/
getHeader: function () {
getHeader () {
return this.data.header;
},
@@ -123,31 +122,28 @@ const Module = Class.extend({
* This method needs to be subclassed if the module wants to use a template.
* It can either return a template sting, or a template filename.
* If the string ends with '.html' it's considered a file from within the module's folder.
*
* @returns {string} The template string of filename.
*/
getTemplate: function () {
getTemplate () {
return `<div class="normal">${this.name}</div><div class="small dimmed">${this.identifier}</div>`;
},
/**
* Returns the data to be used in the template.
* This method needs to be subclassed if the module wants to use a custom data.
*
* @returns {object} The data for the template
*/
getTemplateData: function () {
getTemplateData () {
return {};
},
/**
* Called by the MagicMirror² core when a notification arrives.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {Module} sender The module that sent the notification.
*/
notificationReceived: function (notification, payload, sender) {
notificationReceived (notification, payload, sender) {
if (sender) {
// Log.log(this.name + " received a module notification: " + notification + " from sender: " + sender.name);
} else {
@@ -158,10 +154,9 @@ const Module = Class.extend({
/**
* Returns the nunjucks environment for the current module.
* The environment is checked in the _nunjucksEnvironment instance variable.
*
* @returns {object} The Nunjucks Environment
*/
nunjucksEnvironment: function () {
nunjucksEnvironment () {
if (this._nunjucksEnvironment !== null) {
return this._nunjucksEnvironment;
}
@@ -180,63 +175,63 @@ const Module = Class.extend({
/**
* Called when a socket notification arrives.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
*/
socketNotificationReceived: function (notification, payload) {
socketNotificationReceived (notification, payload) {
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
},
/**
* Called when the module is hidden.
*/
suspend: function () {
suspend () {
Log.log(`${this.name} is suspended.`);
},
/**
* Called when the module is shown.
*/
resume: function () {
resume () {
Log.log(`${this.name} is resumed.`);
},
/*********************************************
* The methods below don"t need subclassing. *
*********************************************/
/**
********************************************
* The methods below don't need subclassing. *
********************************************
*/
/**
* Set the module data.
*
* @param {object} data The module data
*/
setData: function (data) {
setData (data) {
this.data = data;
this.name = data.name;
this.identifier = data.identifier;
this.hidden = false;
this.hasAnimateIn = false;
this.hasAnimateOut = false;
this.setConfig(data.config, data.configDeepMerge);
},
/**
* Set the module config and combine it with the module defaults.
*
* @param {object} config The combined module config.
* @param {boolean} deep Merge module config in deep.
*/
setConfig: function (config, deep) {
setConfig (config, deep) {
this.config = deep ? configMerge({}, this.defaults, config) : Object.assign({}, this.defaults, config);
},
/**
* Returns a socket object. If it doesn't exist, it's created.
* It also registers the notification callback.
*
* @returns {MMSocket} a socket object
*/
socket: function () {
socket () {
if (typeof this._socket === "undefined") {
this._socket = new MMSocket(this.name);
}
@@ -250,39 +245,35 @@ const Module = Class.extend({
/**
* Retrieve the path to a module file.
*
* @param {string} file Filename
* @returns {string} the file path
*/
file: function (file) {
file (file) {
return `${this.data.path}/${file}`.replace("//", "/");
},
/**
* Load all required stylesheets by requesting the MM object to load the files.
*
* @returns {Promise<void>}
*/
loadStyles: function () {
loadStyles () {
return this.loadDependencies("getStyles");
},
/**
* Load all required scripts by requesting the MM object to load the files.
*
* @returns {Promise<void>}
*/
loadScripts: function () {
loadScripts () {
return this.loadDependencies("getScripts");
},
/**
* Helper method to load all dependencies.
*
* @param {string} funcName Function name to call to get scripts or styles.
* @returns {Promise<void>}
*/
loadDependencies: async function (funcName) {
async loadDependencies (funcName) {
let dependencies = this[funcName]();
const loadNextDependency = async () => {
@@ -301,8 +292,9 @@ const Module = Class.extend({
/**
* Load all translations.
* @returns {Promise<void>}
*/
loadTranslations: async function () {
async loadTranslations () {
const translations = this.getTranslations() || {};
const language = config.language.toLowerCase();
@@ -329,13 +321,12 @@ const Module = Class.extend({
/**
* Request the translation for a given key with optional variables and default value.
*
* @param {string} key The key of the string to translate
* @param {string|object} [defaultValueOrVariables] The default value or variables for translating.
* @param {string} [defaultValue] The default value with variables.
* @returns {string} the translated key
*/
translate: function (key, defaultValueOrVariables, defaultValue) {
translate (key, defaultValueOrVariables, defaultValue) {
if (typeof defaultValueOrVariables === "object") {
return Translator.translate(this, key, defaultValueOrVariables) || defaultValue || "";
}
@@ -344,84 +335,81 @@ const Module = Class.extend({
/**
* Request an (animated) update of the module.
*
* @param {number} [speed] The speed of the animation.
* @param {number|object} [updateOptions] The speed of the animation or object with for updateOptions (speed/animates)
*/
updateDom: function (speed) {
MM.updateDom(this, speed);
updateDom (updateOptions) {
MM.updateDom(this, updateOptions);
},
/**
* Send a notification to all modules.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
*/
sendNotification: function (notification, payload) {
sendNotification (notification, payload) {
MM.sendNotification(notification, payload, this);
},
/**
* Send a socket notification to the node helper.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
*/
sendSocketNotification: function (notification, payload) {
sendSocketNotification (notification, payload) {
this.socket().sendNotification(notification, payload);
},
/**
* Hide this module.
*
* @param {number} speed The speed of the hide animation.
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the hide method.
*/
hide: function (speed, callback, options) {
if (typeof callback === "object") {
options = callback;
callback = function () {};
}
hide (speed, callback, options = {}) {
let usedCallback = callback || function () {};
let usedOptions = options;
callback = callback || function () {};
options = options || {};
if (typeof callback === "object") {
Log.error("Parameter mismatch in module.hide: callback is not an optional parameter!");
usedOptions = callback;
usedCallback = function () {};
}
MM.hideModule(
this,
speed,
() => {
this.suspend();
callback();
usedCallback();
},
options
usedOptions
);
},
/**
* Show this module.
*
* @param {number} speed The speed of the show animation.
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the show method.
*/
show: function (speed, callback, options) {
if (typeof callback === "object") {
options = callback;
callback = function () {};
}
show (speed, callback, options) {
let usedCallback = callback || function () {};
let usedOptions = options;
callback = callback || function () {};
options = options || {};
if (typeof callback === "object") {
Log.error("Parameter mismatch in module.show: callback is not an optional parameter!");
usedOptions = callback;
usedCallback = function () {};
}
MM.showModule(
this,
speed,
() => {
this.resume();
callback();
usedCallback();
},
options
usedOptions
);
}
});
@@ -445,11 +433,10 @@ const Module = Class.extend({
* -------
*
* Todo: idea of Mich determinate what do you want to merge or not
*
* @param {object} result the initial object
* @returns {object} the merged config
*/
function configMerge(result) {
function configMerge (result) {
const stack = Array.prototype.slice.call(arguments, 1);
let item, key;
@@ -507,13 +494,12 @@ window.Module = Module;
/**
* Compare two semantic version numbers and return the difference.
*
* @param {string} a Version number a.
* @param {string} b Version number b.
* @returns {number} A positive number if a is larger than b, a negative
* number if a is smaller and 0 if they are the same
*/
function cmpVersions(a, b) {
function cmpVersions (a, b) {
const regExStrip0 = /(\.0+)+$/;
const segmentsA = a.replace(regExStrip0, "").split(".");
const segmentsB = b.replace(regExStrip0, "").split(".");

View File

@@ -1,23 +1,17 @@
/* MagicMirror²
* Node Helper Superclass
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const express = require("express");
const Log = require("logger");
const Class = require("./class");
const NodeHelper = Class.extend({
init() {
init () {
Log.log("Initializing new module helper ...");
},
loaded() {
loaded () {
Log.log(`Module helper loaded: ${this.name}`);
},
start() {
start () {
Log.log(`Starting module helper: ${this.name}`);
},
@@ -26,86 +20,75 @@ const NodeHelper = Class.extend({
* Close any open connections, stop any sub-processes and
* gracefully exit the module.
*/
stop() {
stop () {
Log.log(`Stopping module helper: ${this.name}`);
},
/**
* This method is called when a socket notification arrives.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
*/
socketNotificationReceived(notification, payload) {
socketNotificationReceived (notification, payload) {
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
},
/**
* Set the module name.
*
* @param {string} name Module name.
*/
setName(name) {
setName (name) {
this.name = name;
},
/**
* Set the module path.
*
* @param {string} path Module path.
*/
setPath(path) {
setPath (path) {
this.path = path;
},
/* sendSocketNotification(notification, payload)
/*
* sendSocketNotification(notification, payload)
* Send a socket notification to the node helper.
*
* argument notification string - The identifier of the notification.
* argument payload mixed - The payload of the notification.
*/
sendSocketNotification(notification, payload) {
sendSocketNotification (notification, payload) {
this.io.of(this.name).emit(notification, payload);
},
/* setExpressApp(app)
/*
* setExpressApp(app)
* Sets the express app object for this module.
* This allows you to host files from the created webserver.
*
* argument app Express app - The Express app object.
*/
setExpressApp(app) {
setExpressApp (app) {
this.expressApp = app;
app.use(`/${this.name}`, express.static(`${this.path}/public`));
},
/* setSocketIO(io)
/*
* setSocketIO(io)
* Sets the socket io object for this module.
* Binds message receiver.
*
* argument io Socket.io - The Socket io object.
*/
setSocketIO(io) {
setSocketIO (io) {
this.io = io;
Log.log(`Connecting socket for: ${this.name}`);
io.of(this.name).on("connection", (socket) => {
// add a catch all event.
const onevent = socket.onevent;
socket.onevent = function (packet) {
const args = packet.data || [];
onevent.call(this, packet); // original call
packet.data = ["*"].concat(args);
onevent.call(this, packet); // additional call to catch-all
};
// register catch all.
socket.on("*", (notification, payload) => {
if (notification !== "*") {
this.socketNotificationReceived(notification, payload);
}
socket.onAny((notification, payload) => {
this.socketNotificationReceived(notification, payload);
});
});
}
@@ -123,7 +106,6 @@ NodeHelper.checkFetchStatus = function (response) {
/**
* Look at the specified error and return an appropriate error type, that
* can be translated to a detailed error message
*
* @param {Error} error the error from fetching something
* @returns {string} the string of the detailed error message in the translations
*/

View File

@@ -1,29 +1,22 @@
/* MagicMirror²
* Server
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const fs = require("fs");
const http = require("http");
const https = require("https");
const path = require("path");
const fs = require("node:fs");
const http = require("node:http");
const https = require("node:https");
const path = require("node:path");
const express = require("express");
const ipfilter = require("express-ipfilter").IpFilter;
const helmet = require("helmet");
const socketio = require("socket.io");
const Log = require("logger");
const Utils = require("./utils");
const { cors, getConfig, getHtml, getVersion } = require("./server_functions");
const { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars } = require("./server_functions");
const vendor = require(`${__dirname}/vendor`);
/**
* Server
*
* @param {object} config The MM config
* @class
*/
function Server(config) {
function Server (config) {
const app = express();
const port = process.env.MM_PORT || config.port;
const serverSockets = new Set();
@@ -31,7 +24,6 @@ function Server(config) {
/**
* Opens the server for incoming connections
*
* @returns {Promise} A promise that is resolved when the server listens to connections
*/
this.open = function () {
@@ -64,7 +56,7 @@ function Server(config) {
server.listen(port, config.address || "localhost");
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
Log.warn("You're using a full whitelist configuration to allow for all IPs");
}
app.use(function (req, res, next) {
@@ -81,9 +73,13 @@ function Server(config) {
app.use(helmet(config.httpHeaders));
app.use("/js", express.static(__dirname));
// TODO add tests directory only when running tests?
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs", "/tests/mocks"];
for (const directory of directories) {
let directories = ["/config", "/css", "/modules", "/node_modules/animate.css", "/node_modules/@fontsource", "/node_modules/@fortawesome", "/translations", "/tests/configs", "/tests/mocks"];
for (const [key, value] of Object.entries(vendor)) {
const dirArr = value.split("/");
if (dirArr[0] === "node_modules") directories.push(`/${dirArr[0]}/${dirArr[1]}`);
}
const uniqDirs = [...new Set(directories)];
for (const directory of uniqDirs) {
app.use(directory, express.static(path.resolve(global.root_path + directory)));
}
@@ -93,6 +89,10 @@ function Server(config) {
app.get("/config", (req, res) => getConfig(req, res));
app.get("/startup", (req, res) => getStartup(req, res));
app.get("/env", (req, res) => getEnvVars(req, res));
app.get("/", (req, res) => getHtml(req, res));
server.on("listening", () => {
@@ -106,7 +106,6 @@ function Server(config) {
/**
* Closes the server and destroys all lingering connections to it.
*
* @returns {Promise} A promise that resolves when server has successfully shut down
*/
this.close = function () {

View File

@@ -1,29 +1,37 @@
const fs = require("fs");
const path = require("path");
const fs = require("node:fs");
const path = require("node:path");
const Log = require("logger");
const fetch = require("./fetch");
const startUp = new Date();
/**
* Gets the config.
*
* @param {Request} req - the request
* @param {Response} res - the result
*/
function getConfig(req, res) {
function getConfig (req, res) {
res.send(config);
}
/**
* Gets the startup time.
* @param {Request} req - the request
* @param {Response} res - the result
*/
function getStartup (req, res) {
res.send(startUp);
}
/**
* A method that forwards HTTP Get-methods to the internet to avoid CORS-errors.
*
* Example input request url: /cors?sendheaders=header1:value1,header2:value2&expectedheaders=header1,header2&url=http://www.test.com/path?param1=value1
*
* Only the url-param of the input request url is required. It must be the last parameter.
*
* @param {Request} req - the request
* @param {Response} res - the result
*/
async function cors(req, res) {
async function cors (req, res) {
try {
const urlRegEx = "url=(.+?)$";
let url;
@@ -37,12 +45,12 @@ async function cors(req, res) {
url = match[1];
const headersToSend = getHeadersToSend(req.url);
const expectedRecievedHeaders = geExpectedRecievedHeaders(req.url);
const expectedReceivedHeaders = geExpectedReceivedHeaders(req.url);
Log.log(`cors url: ${url}`);
const response = await fetch(url, { headers: headersToSend });
for (const header of expectedRecievedHeaders) {
for (const header of expectedReceivedHeaders) {
const headerValue = response.headers.get(header);
if (header) res.set(header, headerValue);
}
@@ -57,11 +65,10 @@ async function cors(req, res) {
/**
* Gets headers and values to attach to the web request.
*
* @param {string} url - The url containing the headers and values to send.
* @returns {object} An object specifying name and value of the headers.
*/
function getHeadersToSend(url) {
function getHeadersToSend (url) {
const headersToSend = { "User-Agent": `Mozilla/5.0 MagicMirror/${global.version}` };
const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url);
if (headersToSendMatch) {
@@ -79,31 +86,30 @@ function getHeadersToSend(url) {
/**
* Gets the headers expected from the response.
*
* @param {string} url - The url containing the expected headers from the response.
* @returns {string[]} headers - The name of the expected headers.
*/
function geExpectedRecievedHeaders(url) {
const expectedRecievedHeaders = ["Content-Type"];
const expectedRecievedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
if (expectedRecievedHeadersMatch) {
const headers = expectedRecievedHeadersMatch[1].split(",");
function geExpectedReceivedHeaders (url) {
const expectedReceivedHeaders = ["Content-Type"];
const expectedReceivedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
if (expectedReceivedHeadersMatch) {
const headers = expectedReceivedHeadersMatch[1].split(",");
for (const header of headers) {
expectedRecievedHeaders.push(header);
expectedReceivedHeaders.push(header);
}
}
return expectedRecievedHeaders;
return expectedReceivedHeaders;
}
/**
* Gets the HTML to display the magic mirror.
*
* @param {Request} req - the request
* @param {Response} res - the result
*/
function getHtml(req, res) {
function getHtml (req, res) {
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
html = html.replace("#VERSION#", global.version);
html = html.replace("#TESTMODE#", global.mmTestMode);
let configFile = "config/config.js";
if (typeof global.configuration_file !== "undefined") {
@@ -116,12 +122,37 @@ function getHtml(req, res) {
/**
* Gets the MagicMirror version.
*
* @param {Request} req - the request
* @param {Response} res - the result
*/
function getVersion(req, res) {
function getVersion (req, res) {
res.send(global.version);
}
module.exports = { cors, getConfig, getHtml, getVersion };
/**
* Gets environment variables needed in the browser.
* @returns {object} environment variables key: values
*/
function getEnvVarsAsObj () {
const obj = { modulesDir: `${config.foreignModulesDir}`, customCss: `${config.customCss}` };
if (process.env.MM_MODULES_DIR) {
obj.modulesDir = process.env.MM_MODULES_DIR.replace(`${global.root_path}/`, "");
}
if (process.env.MM_CUSTOMCSS_FILE) {
obj.customCss = process.env.MM_CUSTOMCSS_FILE.replace(`${global.root_path}/`, "");
}
return obj;
}
/**
* Gets environment variables needed in the browser.
* @param {Request} req - the request
* @param {Response} res - the result
*/
function getEnvVars (req, res) {
const obj = getEnvVarsAsObj();
res.send(obj);
}
module.exports = { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj };

View File

@@ -1,11 +1,5 @@
/* global io */
/* MagicMirror²
* TODO add description
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const MMSocket = function (moduleName) {
if (typeof moduleName !== "string") {
throw new Error("Please set the module name for the MMSocket.");
@@ -44,10 +38,7 @@ const MMSocket = function (moduleName) {
notificationCallback = callback;
};
this.sendNotification = (notification, payload) => {
if (typeof payload === "undefined") {
payload = {};
}
this.sendNotification = (notification, payload = {}) => {
this.socket.emit(notification, payload);
};
};

View File

@@ -1,34 +1,28 @@
/* global translations */
/* MagicMirror²
* Translator (l10n)
*
* By Christopher Fenner https://github.com/CFenner
* MIT Licensed.
*/
const Translator = (function () {
/**
* Load a JSON file via XHR.
*
* @param {string} file Path of the file we want to load.
* @returns {Promise<object>} the translations in the specified file
*/
async function loadJSON(file) {
async function loadJSON (file) {
const xhr = new XMLHttpRequest();
return new Promise(function (resolve, reject) {
return new Promise(function (resolve) {
xhr.overrideMimeType("application/json");
xhr.open("GET", file, true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
// needs error handler try/catch at least
let fileinfo = null;
let fileInfo = null;
try {
fileinfo = JSON.parse(xhr.responseText);
fileInfo = JSON.parse(xhr.responseText);
} catch (exception) {
// nothing here, but don't die
Log.error(` loading json file =${file} failed`);
}
resolve(fileinfo);
resolve(fileInfo);
}
};
xhr.send(null);
@@ -43,33 +37,31 @@ const Translator = (function () {
/**
* Load a translation for a given key for a given module.
*
* @param {Module} module The module to load the translation for.
* @param {string} key The key of the text to translate.
* @param {object} variables The variables to use within the translation template (optional)
* @returns {string} the translated key
*/
translate: function (module, key, variables) {
variables = variables || {}; // Empty object by default
translate (module, key, variables = {}) {
/**
* Combines template and variables like:
* template: "Please wait for {timeToWait} before continuing with {work}."
* variables: {timeToWait: "2 hours", work: "painting"}
* to: "Please wait for 2 hours before continuing with painting."
*
* @param {string} template Text with placeholder
* @param {object} variables Variables for the placeholder
* @returns {string} the template filled with the variables
*/
function createStringFromTemplate(template, variables) {
function createStringFromTemplate (template, variables) {
if (Object.prototype.toString.call(template) !== "[object String]") {
return template;
}
let templateToUse = template;
if (variables.fallback && !template.match(new RegExp("{.+}"))) {
template = variables.fallback;
templateToUse = variables.fallback;
}
return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
return templateToUse.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
return varName in variables ? variables[varName] : `{${varName}}`;
});
}
@@ -99,12 +91,11 @@ const Translator = (function () {
/**
* Load a translation file (json) and remember the data.
*
* @param {Module} module The module to load the translation file for.
* @param {string} file Path of the file we want to load.
* @param {boolean} isFallback Flag to indicate fallback translations.
*/
async load(module, file, isFallback) {
async load (module, file, isFallback) {
Log.log(`${module.name} - Load translation${isFallback ? " fallback" : ""}: ${file}`);
if (this.translationsFallback[module.name]) {
@@ -118,10 +109,9 @@ const Translator = (function () {
/**
* Load the core translations.
*
* @param {string} lang The language identifier of the core language.
*/
loadCoreTranslations: async function (lang) {
async loadCoreTranslations (lang) {
if (lang in translations) {
Log.log(`Loading core translation file: ${translations[lang]}`);
this.coreTranslations = await loadJSON(translations[lang]);
@@ -136,7 +126,7 @@ const Translator = (function () {
* Load the core translations' fallback.
* The first language defined in translations.js will be used.
*/
loadCoreTranslationsFallback: async function () {
async loadCoreTranslationsFallback () {
let first = Object.keys(translations)[0];
if (first) {
Log.log(`Loading core translation fallback file: ${translations[first]}`);
@@ -144,6 +134,6 @@ const Translator = (function () {
}
}
};
})();
}());
window.Translator = Translator;

View File

@@ -1,16 +1,79 @@
/* MagicMirror²
* Utils
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
const colors = require("colors/safe");
const execSync = require("node:child_process").execSync;
const path = require("node:path");
const rootPath = path.resolve(`${__dirname}/../`);
const Log = require(`${rootPath}/js/logger.js`);
const os = require("node:os");
const fs = require("node:fs");
const si = require("systeminformation");
const modulePositions = []; // will get list from index.html
const regionRegEx = /"region ([^"]*)/i;
const indexFileName = "index.html";
const discoveredPositionsJSFilename = "js/positions.js";
module.exports = {
colors: {
warn: colors.yellow,
error: colors.red,
info: colors.blue,
pass: colors.green
async logSystemInformation () {
try {
let installedNodeVersion = execSync("node -v", { encoding: "utf-8" }).replace("v", "").replace(/(?:\r\n|\r|\n)/g, "");
const staticData = await si.get({
system: "manufacturer, model, virtual",
osInfo: "platform, distro, release, arch",
versions: "kernel, node, npm, pm2"
});
let systemDataString = `System information:
### SYSTEM: manufacturer: ${staticData.system.manufacturer}; model: ${staticData.system.model}; virtual: ${staticData.system.virtual}
### OS: platform: ${staticData.osInfo.platform}; distro: ${staticData.osInfo.distro}; release: ${staticData.osInfo.release}; arch: ${staticData.osInfo.arch}; kernel: ${staticData.versions.kernel}
### VERSIONS: electron: ${process.versions.electron}; used node: ${staticData.versions.node}; installed node: ${installedNodeVersion}; npm: ${staticData.versions.npm}; pm2: ${staticData.versions.pm2}
### OTHER: timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`
.replace(/\t/g, "");
Log.info(systemDataString);
// Return is currently only for jest
return systemDataString;
} catch (e) {
Log.error(e);
}
},
// return all available module positions
getAvailableModulePositions () {
return modulePositions;
},
// return if position is on modulePositions Array (true/false)
moduleHasValidPosition (position) {
if (this.getAvailableModulePositions().indexOf(position) === -1) return false;
return true;
},
getModulePositions () {
// if not already discovered
if (modulePositions.length === 0) {
// get the lines of the index.html
const lines = fs.readFileSync(indexFileName).toString().split("\n");
// loop thru the lines
lines.forEach((line) => {
// run the regex on each line
const results = regionRegEx.exec(line);
// if the regex returned something
if (results && results.length > 0) {
// get the position parts and replace space with underscore
const positionName = results[1].replace(" ", "_");
// add it to the list
modulePositions.push(positionName);
}
});
try {
fs.writeFileSync(discoveredPositionsJSFilename, `const modulePositions=${JSON.stringify(modulePositions)}`);
}
catch (error) {
Log.error("unable to write js/positions.js with the discovered module positions\nmake the MagicMirror/js folder writeable by the user starting MagicMirror");
}
}
// return the list to the caller
return modulePositions;
}
};

View File

@@ -1,9 +1,3 @@
/* MagicMirror²
* Vendor File Definition
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const vendor = {
"moment.js": "node_modules/moment/min/moment-with-locales.js",
"moment-timezone.js": "node_modules/moment-timezone/builds/moment-timezone-with-data.js",
@@ -11,7 +5,8 @@ const vendor = {
"weather-icons-wind.css": "node_modules/weathericons/css/weather-icons-wind.css",
"font-awesome.css": "css/font-awesome.css",
"nunjucks.js": "node_modules/nunjucks/browser/nunjucks.min.js",
"suncalc.js": "node_modules/suncalc/suncalc.js"
"suncalc.js": "node_modules/suncalc/suncalc.js",
"croner.js": "node_modules/croner/dist/croner.umd.js"
};
if (typeof module !== "undefined") {

BIN
mm2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -1,16 +1,19 @@
type ModuleProperties = {
defaults?: object;
[key: string]: any;
start?(): void;
getScripts?(): string[];
getStyles?(): string[];
getTranslations?(): object;
getDom?(): HTMLElement;
getHeader?(): string;
getTemplate?(): string;
getTemplateData?(): object;
notificationReceived?(notification: string, payload: any, sender: object): void;
nunjucksEnvironment?(): void;
socketNotificationReceived?(notification: string, payload: any): void;
suspend?(): void;
resume?(): void;
getDom?(): HTMLElement;
getStyles?(): string[];
[key: string]: any;
};
export declare const Module: {

View File

@@ -1,11 +1,5 @@
/* global NotificationFx */
/* MagicMirror²
* Module: alert
*
* By Paul-Vincent Roll https://paulvincentroll.com/
* MIT Licensed.
*/
Module.register("alert", {
alerts: {},
@@ -17,20 +11,21 @@ Module.register("alert", {
welcome_message: false // shown at startup
},
getScripts() {
getScripts () {
return ["notificationFx.js"];
},
getStyles() {
return ["font-awesome.css", this.file(`./styles/notificationFx.css`), this.file(`./styles/${this.config.position}.css`)];
getStyles () {
return ["font-awesome.css", this.file("./styles/notificationFx.css"), this.file(`./styles/${this.config.position}.css`)];
},
getTranslations() {
getTranslations () {
return {
bg: "translations/bg.json",
da: "translations/da.json",
de: "translations/de.json",
en: "translations/en.json",
eo: "translations/eo.json",
es: "translations/es.json",
fr: "translations/fr.json",
hu: "translations/hu.json",
@@ -40,11 +35,11 @@ Module.register("alert", {
};
},
getTemplate(type) {
getTemplate (type) {
return `templates/${type}.njk`;
},
async start() {
async start () {
Log.info(`Starting module: ${this.name}`);
if (this.config.effect === "slide") {
@@ -57,7 +52,7 @@ Module.register("alert", {
}
},
notificationReceived(notification, payload, sender) {
notificationReceived (notification, payload, sender) {
if (notification === "SHOW_ALERT") {
if (payload.type === "notification") {
this.showNotification(payload);
@@ -69,7 +64,7 @@ Module.register("alert", {
}
},
async showNotification(notification) {
async showNotification (notification) {
const message = await this.renderMessage(notification.templateName || "notification", notification);
new NotificationFx({
@@ -80,7 +75,7 @@ Module.register("alert", {
}).show();
},
async showAlert(alert, sender) {
async showAlert (alert, sender) {
// If module already has an open alert close it
if (this.alerts[sender.name]) {
this.hideAlert(sender, false);
@@ -113,7 +108,7 @@ Module.register("alert", {
}
},
hideAlert(sender, close = true) {
hideAlert (sender, close = true) {
// Dismiss alert and remove from this.alerts
if (this.alerts[sender.name]) {
this.alerts[sender.name].dismiss(close);
@@ -125,7 +120,7 @@ Module.register("alert", {
}
},
renderMessage(type, data) {
renderMessage (type, data) {
return new Promise((resolve) => {
this.nunjucksEnvironment().render(this.getTemplate(type), data, function (err, res) {
if (err) {
@@ -137,7 +132,7 @@ Module.register("alert", {
});
},
toggleBlur(add = false) {
toggleBlur (add = false) {
const method = add ? "add" : "remove";
const modules = document.querySelectorAll(".module");
for (const module of modules) {

View File

@@ -9,18 +9,17 @@
*
* Copyright 2014, Codrops
* https://tympanus.net/codrops/
*
* @param {object} window The window object
*/
(function (window) {
/**
* Extend one object with another one
*
* @param {object} a The object to extend
* @param {object} b The object which extends the other, overwrites existing keys
* @returns {object} The merged object
*/
function extend(a, b) {
function extend (a, b) {
for (let key in b) {
if (b.hasOwnProperty(key)) {
a[key] = b[key];
@@ -31,11 +30,10 @@
/**
* NotificationFx constructor
*
* @param {object} options The configuration options
* @class
*/
function NotificationFx(options) {
function NotificationFx (options) {
this.options = extend({}, this.options);
extend(this.options, options);
this._init();
@@ -66,10 +64,10 @@
ttl: 6000,
al_no: "ns-box",
// callbacks
onClose: function () {
onClose () {
return false;
},
onOpen: function () {
onOpen () {
return false;
}
};
@@ -81,7 +79,7 @@
// create HTML structure
this.ntf = document.createElement("div");
this.ntf.className = `${this.options.al_no} ns-${this.options.layout} ns-effect-${this.options.effect} ns-type-${this.options.type}`;
let strinner = '<div class="ns-box-inner">';
let strinner = "<div class=\"ns-box-inner\">";
strinner += this.options.message;
strinner += "</div>";
this.ntf.innerHTML = strinner;
@@ -124,7 +122,6 @@
/**
* Dismiss the notification
*
* @param {boolean} [close] call the onClose callback at the end
*/
NotificationFx.prototype.dismiss = function (close = true) {
@@ -157,4 +154,4 @@
* Add to global namespace
*/
window.NotificationFx = NotificationFx;
})(window);
}(window));

View File

@@ -1,18 +1,20 @@
{% if imageUrl or imageFA %}
{% set imageHeight = imageHeight if imageHeight else "80px" %}
{% if imageUrl %}
<img src="{{ imageUrl }}" height="{{ imageHeight }}" style="margin-bottom: 10px;"/>
{% else %}
<span class="bright fas fa-{{ imageFA }}" style='margin-bottom: 10px; font-size: {{ imageHeight }};'/></span>
{% endif %}
<br/>
{% set imageHeight = imageHeight if imageHeight else "80px" %}
{% if imageUrl %}
<img src="{{ imageUrl }}"
height="{{ imageHeight }}"
style="margin-bottom: 10px" />
{% else %}
<span class="bright fas fa-{{ imageFA }}"
style="margin-bottom: 10px;
font-size: {{ imageHeight }}"></span>
{% endif %}
<br />
{% endif %}
{% if title %}
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
{% endif %}
{% if message %}
{% if title %}
<br/>
{% endif %}
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
{% if title %}<br />{% endif %}
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
{% endif %}

View File

@@ -1,9 +1,7 @@
{% if title %}
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
{% endif %}
{% if message %}
{% if title %}
<br/>
{% endif %}
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
{% if title %}<br />{% endif %}
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
{% endif %}

View File

@@ -0,0 +1,4 @@
{
"sysTitle": "MagicMirror² Ειδοποίηση",
"welcome": "Καλώς ήρθατε, η εκκίνηση ήταν επιτυχής!"
}

View File

@@ -0,0 +1,4 @@
{
"sysTitle": "MagicMirror²-sciigo",
"welcome": "Bonvenon, lanĉo sukcesis!"
}

View File

@@ -1,11 +1,5 @@
/* global cloneObject */
/* global CalendarUtils */
/* MagicMirror²
* Module: Calendar
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
Module.register("calendar", {
// Define module defaults
defaults: {
@@ -21,27 +15,29 @@ Module.register("calendar", {
defaultRepeatingCountTitle: "",
maxTitleLength: 25,
maxLocationTitleLength: 25,
wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
wrapEvents: false, // Wrap events to multiple lines breaking at maxTitleLength
wrapLocationEvents: false,
maxTitleLines: 3,
maxEventTitleLines: 3,
fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
fetchInterval: 60 * 60 * 1000, // Update every hour
animationSpeed: 2000,
fade: true,
fadePoint: 0.25, // Start on 1/4th of the list.
urgency: 7,
timeFormat: "relative",
dateFormat: "MMM Do",
dateEndFormat: "LT",
fullDayEventDateFormat: "MMM Do",
showEnd: false,
showEndsOnlyWithDuration: false,
getRelative: 6,
fadePoint: 0.25, // Start on 1/4th of the list.
hidePrivate: false,
hideOngoing: false,
hideTime: false,
hideDuplicates: true,
showTimeToday: false,
colored: false,
customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
forceUseCurrentTime: false,
tableClass: "small",
calendars: [
{
@@ -49,10 +45,11 @@ Module.register("calendar", {
url: "https://www.calendarlabs.com/templates/ical/US-Holidays.ics"
}
],
titleReplace: {
"De verjaardag van ": "",
"'s birthday": ""
},
customEvents: [
// Array of {keyword: "", symbol: "", color: "", eventClass: ""} where Keyword is a regexp and symbol/color/eventClass are to be applied for matched
{ keyword: ".*", transform: { search: "De verjaardag van ", replace: "" } },
{ keyword: ".*", transform: { search: "'s birthday", replace: "" } }
],
locationTitleReplace: {
"street ": ""
},
@@ -67,48 +64,50 @@ Module.register("calendar", {
coloredSymbol: false,
coloredBackground: false,
limitDaysNeverSkip: false,
flipDateHeaderTitle: false
flipDateHeaderTitle: false,
updateOnFetch: true
},
requiresVersion: "2.1.0",
// Define required scripts.
getStyles: function () {
getStyles () {
return ["calendar.css", "font-awesome.css"];
},
// Define required scripts.
getScripts: function () {
return ["moment.js"];
getScripts () {
return ["calendarutils.js", "moment.js", "moment-timezone.js"];
},
// Define required translations.
getTranslations: function () {
// The translations for the default modules are defined in the core translation files.
// Therefore we can just return false. Otherwise we should have returned a dictionary.
// If you're trying to build your own module including translations, check out the documentation.
getTranslations () {
/*
* The translations for the default modules are defined in the core translation files.
* Therefore we can just return false. Otherwise we should have returned a dictionary.
* If you're trying to build your own module including translations, check out the documentation.
*/
return false;
},
// Override start method.
start: function () {
const ONE_MINUTE = 60 * 1000;
start () {
Log.info(`Starting module: ${this.name}`);
if (this.config.colored) {
Log.warn("Your are using the deprecated config values 'colored'. Please switch to 'coloredSymbol' & 'coloredText'!");
Log.warn("Your are using the deprecated config values 'colored'. Please switch to 'coloredSymbol' & 'coloredText'!");
this.config.coloredText = true;
this.config.coloredSymbol = true;
}
if (this.config.coloredSymbolOnly) {
Log.warn("Your are using the deprecated config values 'coloredSymbolOnly'. Please switch to 'coloredSymbol' & 'coloredText'!");
Log.warn("Your are using the deprecated config values 'coloredSymbolOnly'. Please switch to 'coloredSymbol' & 'coloredText'!");
this.config.coloredText = false;
this.config.coloredSymbol = true;
}
// Set locale.
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
moment.updateLocale(config.language, CalendarUtils.getLocaleSpecification(config.timeFormat));
// clear data holder before start
this.calendarData = {};
@@ -116,6 +115,9 @@ Module.register("calendar", {
// indicate no data available yet
this.loaded = false;
// data holder of calendar url. Avoid fade out/in on updateDom (one for each calendar update)
this.calendarDisplayer = {};
this.config.calendars.forEach((calendar) => {
calendar.url = calendar.url.replace("webcal://", "http://");
@@ -124,48 +126,59 @@ Module.register("calendar", {
maximumNumberOfDays: calendar.maximumNumberOfDays,
pastDaysCount: calendar.pastDaysCount,
broadcastPastEvents: calendar.broadcastPastEvents,
selfSignedCert: calendar.selfSignedCert
selfSignedCert: calendar.selfSignedCert,
excludedEvents: calendar.excludedEvents,
fetchInterval: calendar.fetchInterval
};
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
if (typeof calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
calendarConfig.symbolClass = "";
}
if (calendar.titleClass === "undefined" || calendar.titleClass === null) {
if (typeof calendar.titleClass === "undefined" || calendar.titleClass === null) {
calendarConfig.titleClass = "";
}
if (calendar.timeClass === "undefined" || calendar.timeClass === null) {
if (typeof calendar.timeClass === "undefined" || calendar.timeClass === null) {
calendarConfig.timeClass = "";
}
// we check user and password here for backwards compatibility with old configs
if (calendar.user && calendar.pass) {
Log.warn("Deprecation warning: Please update your calendar authentication configuration.");
Log.warn("https://github.com/MichMich/MagicMirror/tree/v2.1.2/modules/default/calendar#calendar-authentication-options");
Log.warn("https://docs.magicmirror.builders/modules/calendar.html#configuration-options");
calendar.auth = {
user: calendar.user,
pass: calendar.pass
};
}
// tell helper to start a fetcher for this calendar
// fetcher till cycle
/*
* tell helper to start a fetcher for this calendar
* fetcher till cycle
*/
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
});
// Refresh the DOM every minute if needed: When using relative date format for events that start
// or end in less than an hour, the date shows minute granularity and we want to keep that accurate.
setTimeout(() => {
setInterval(() => {
this.updateDom(1);
}, ONE_MINUTE);
}, ONE_MINUTE - (new Date() % ONE_MINUTE));
// for backward compatibility titleReplace
if (typeof this.config.titleReplace !== "undefined") {
Log.warn("Deprecation warning: Please consider upgrading your calendar titleReplace configuration to customEvents.");
for (const [titlesearchstr, titlereplacestr] of Object.entries(this.config.titleReplace)) {
this.config.customEvents.push({ keyword: ".*", transform: { search: titlesearchstr, replace: titlereplacestr } });
}
}
this.selfUpdate();
},
notificationReceived (notification, payload, sender) {
if (notification === "FETCH_CALENDAR") {
if (this.hasCalendarURL(payload.url)) {
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
}
}
},
// Override socket notification handler.
socketNotificationReceived: function (notification, payload) {
if (notification === "FETCH_CALENDAR") {
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
}
socketNotificationReceived (notification, payload) {
if (this.identifier !== payload.id) {
return;
@@ -180,6 +193,18 @@ Module.register("calendar", {
if (this.config.broadcastEvents) {
this.broadcastEvents();
}
if (!this.config.updateOnFetch) {
if (this.calendarDisplayer[payload.url] === undefined) {
// calendar will never displayed, so display it
this.updateDom(this.config.animationSpeed);
// set this calendar as displayed
this.calendarDisplayer[payload.url] = true;
} else {
Log.debug("[Calendar] DOM not updated waiting self update()");
}
return;
}
}
} else if (notification === "CALENDAR_ERROR") {
let error_message = this.translate(payload.error_type);
@@ -191,12 +216,8 @@ Module.register("calendar", {
},
// Override dom generator.
getDom: function () {
getDom () {
const ONE_SECOND = 1000; // 1,000 milliseconds
const ONE_MINUTE = ONE_SECOND * 60;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;
const events = this.createEventList(true);
const wrapper = document.createElement("table");
wrapper.className = this.config.tableClass;
@@ -228,7 +249,9 @@ Module.register("calendar", {
let lastSeenDate = "";
events.forEach((event, index) => {
const dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
const eventStartDateMoment = this.timestampToMoment(event.startDate);
const eventEndDateMoment = this.timestampToMoment(event.endDate);
const dateAsString = eventStartDateMoment.format(this.config.dateFormat);
if (this.config.timeFormat === "dateheaders") {
if (lastSeenDate !== dateAsString) {
const dateRow = document.createElement("tr");
@@ -310,19 +333,24 @@ Module.register("calendar", {
repeatingCountTitle = this.countTitleForUrl(event.url);
if (repeatingCountTitle !== "") {
const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
const thisYear = eventStartDateMoment.year(),
yearDiff = thisYear - event.firstYear;
repeatingCountTitle = `, ${yearDiff}. ${repeatingCountTitle}`;
repeatingCountTitle = `, ${yearDiff} ${repeatingCountTitle}`;
}
}
// Color events if custom color is specified
var transformedTitle = event.title;
// Color events if custom color or eventClass are specified, transform title if required
if (this.config.customEvents.length > 0) {
for (let ev in this.config.customEvents) {
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
if (needle.test(event.title)) {
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
if (needle.test(event.title)) {
if (typeof this.config.customEvents[ev].transform === "object") {
transformedTitle = CalendarUtils.titleTransform(transformedTitle, [this.config.customEvents[ev].transform]);
}
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
// Respect parameter ColoredSymbolOnly also for custom events
if (this.config.coloredText) {
eventWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
@@ -331,13 +359,15 @@ Module.register("calendar", {
if (this.config.displaySymbol && this.config.coloredSymbol) {
symbolWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
}
break;
}
if (typeof this.config.customEvents[ev].eventClass !== "undefined" && this.config.customEvents[ev].eventClass !== "") {
eventWrapper.className += ` ${this.config.customEvents[ev].eventClass}`;
}
}
}
}
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
titleWrapper.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines) + repeatingCountTitle;
const titleClass = this.titleClassForUrl(event.url);
@@ -358,11 +388,15 @@ Module.register("calendar", {
timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`;
timeWrapper.style.paddingLeft = "2px";
timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left";
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
timeWrapper.innerHTML = eventStartDateMoment.format("LT");
// Add endDate to dataheaders if showEnd is enabled
if (this.config.showEnd) {
timeWrapper.innerHTML += ` - ${this.capFirst(moment(event.endDate, "x").format("LT"))}`;
if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) {
// no duration here, don't display end
} else {
timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(eventEndDateMoment.format("LT"))}`;
}
}
eventWrapper.appendChild(timeWrapper);
@@ -374,90 +408,105 @@ Module.register("calendar", {
const timeWrapper = document.createElement("td");
eventWrapper.appendChild(titleWrapper);
const now = new Date();
const now = moment();
if (this.config.timeFormat === "absolute") {
// Use dateFormat
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.dateFormat));
// Add end time if showEnd
if (this.config.showEnd) {
timeWrapper.innerHTML += "-";
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
// and has a duation
if (event.startDate !== event.endDate) {
timeWrapper.innerHTML += "-";
timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.dateEndFormat));
}
}
// For full day events we use the fullDayEventDateFormat
if (event.fullDayEvent) {
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
event.endDate -= ONE_SECOND;
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
} else if (this.config.getRelative > 0 && event.startDate < now) {
eventEndDateMoment.subtract(1, "second");
timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.fullDayEventDateFormat));
// only show end if requested and allowed and the dates are different
if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(eventEndDateMoment, "d")) {
timeWrapper.innerHTML += "-";
timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.fullDayEventDateFormat));
} else if (!eventStartDateMoment.isSame(eventEndDateMoment, "d") && eventStartDateMoment.isBefore(now)) {
timeWrapper.innerHTML = CalendarUtils.capFirst(now.format(this.config.fullDayEventDateFormat));
}
} else if (this.config.getRelative > 0 && eventStartDateMoment.isBefore(now)) {
// Ongoing and getRelative is set
timeWrapper.innerHTML = this.capFirst(
timeWrapper.innerHTML = CalendarUtils.capFirst(
this.translate("RUNNING", {
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
timeUntilEnd: eventEndDateMoment.fromNow(true)
})
);
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * ONE_DAY) {
} else if (this.config.urgency > 0 && eventStartDateMoment.diff(now, "d") < this.config.urgency) {
// Within urgency days
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.fromNow());
}
if (event.fullDayEvent && this.config.nextDaysRelative) {
// Full days events within the next two days
if (event.today) {
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
} else if (event.yesterday) {
timeWrapper.innerHTML = this.capFirst(this.translate("YESTERDAY"));
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
} else if (event.tomorrow) {
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
} else if (event.dayAfterTomorrow) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
}
}
}
} else {
// Show relative times
if (event.startDate >= now || (event.fullDayEvent && event.today)) {
if (eventStartDateMoment.isSameOrAfter(now) || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) {
// Use relative time
if (!this.config.hideTime && !event.fullDayEvent) {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
Log.debug("event not hidden and not fullday");
timeWrapper.innerHTML = `${CalendarUtils.capFirst(eventStartDateMoment.calendar(null, { sameElse: this.config.dateFormat }))}`;
} else {
timeWrapper.innerHTML = this.capFirst(
moment(event.startDate, "x").calendar(null, {
Log.debug("event full day or hidden");
timeWrapper.innerHTML = `${CalendarUtils.capFirst(
eventStartDateMoment.calendar(null, {
sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
nextDay: `[${this.translate("TOMORROW")}]`,
nextWeek: "dddd",
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
})
);
)}`;
}
if (event.fullDayEvent) {
// Full days events within the next two days
if (event.today) {
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
if (event.today || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) {
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
} else if (event.dayBeforeYesterday) {
if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYBEFOREYESTERDAY"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYBEFOREYESTERDAY"));
}
} else if (event.yesterday) {
timeWrapper.innerHTML = this.capFirst(this.translate("YESTERDAY"));
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
} else if (event.tomorrow) {
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
} else if (event.dayAfterTomorrow) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
}
}
} else if (event.startDate - now < this.config.getRelative * ONE_HOUR) {
Log.info("event fullday");
} else if (eventStartDateMoment.diff(now, "h") < this.config.getRelative) {
Log.info("not full day but within getrelative size");
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
timeWrapper.innerHTML = `${CalendarUtils.capFirst(eventStartDateMoment.fromNow())}`;
}
} else {
// Ongoing event
timeWrapper.innerHTML = this.capFirst(
timeWrapper.innerHTML = CalendarUtils.capFirst(
this.translate("RUNNING", {
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
timeUntilEnd: eventEndDateMoment.fromNow(true)
})
);
}
@@ -503,7 +552,9 @@ Module.register("calendar", {
const descCell = document.createElement("td");
descCell.className = "location";
descCell.colSpan = "2";
descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines);
const transformedTitle = CalendarUtils.titleTransform(event.location, this.config.locationTitleReplace);
descCell.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxLocationTitleLength, this.config.wrapLocationEvents, this.config.maxEventTitleLines);
locationRow.appendChild(descCell);
wrapper.appendChild(locationRow);
@@ -519,35 +570,12 @@ Module.register("calendar", {
return wrapper;
},
/**
* This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
* corresponding timeformat to be used in the calendar display. If no number is given (or otherwise invalid input)
* it will a localeSpecification object with the system locale time format.
*
* @param {number} timeFormat Specifies either 12 or 24 hour time format
* @returns {moment.LocaleSpecification} formatted time
*/
getLocaleSpecification: function (timeFormat) {
switch (timeFormat) {
case 12: {
return { longDateFormat: { LT: "h:mm A" } };
}
case 24: {
return { longDateFormat: { LT: "HH:mm" } };
}
default: {
return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
}
}
},
/**
* Checks if this config contains the calendar url.
*
* @param {string} url The calendar url
* @returns {boolean} True if the calendar config contains the url, False otherwise
*/
hasCalendarURL: function (url) {
hasCalendarURL (url) {
for (const calendar of this.config.calendars) {
if (calendar.url === url) {
return true;
@@ -557,92 +585,114 @@ Module.register("calendar", {
return false;
},
/**
* converts the given timestamp to a moment with a timezone
* @param {number} timestamp timestamp from an event
* @returns {moment.Moment} moment with a timezone
*/
timestampToMoment (timestamp) {
return moment(timestamp, "x").tz(moment.tz.guess());
},
/**
* Creates the sorted list of all events.
*
* @param {boolean} limitNumberOfEntries Whether to filter returned events for display.
* @returns {object[]} Array with events.
*/
createEventList: function (limitNumberOfEntries) {
const ONE_SECOND = 1000; // 1,000 milliseconds
const ONE_MINUTE = ONE_SECOND * 60;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;
createEventList (limitNumberOfEntries) {
let now = moment();
let today = now.clone().startOf("day");
let future = now.clone().startOf("day").add(this.config.maximumNumberOfDays, "days");
const now = new Date();
const today = moment().startOf("day");
const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
let events = [];
for (const calendarUrl in this.calendarData) {
const calendar = this.calendarData[calendarUrl];
let remainingEntries = this.maximumEntriesForUrl(calendarUrl);
let maxPastDaysCompare = now - this.maximumPastDaysForUrl(calendarUrl) * ONE_DAY;
let maxPastDaysCompare = now.clone().subtract(this.maximumPastDaysForUrl(calendarUrl), "days");
let by_url_calevents = [];
for (const e in calendar) {
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
const eventStartDateMoment = this.timestampToMoment(event.startDate);
const eventEndDateMoment = this.timestampToMoment(event.endDate);
if (this.config.hidePrivate && event.class === "PRIVATE") {
// do not add the current event, skip it
continue;
}
if (limitNumberOfEntries) {
if (event.endDate < maxPastDaysCompare) {
if (eventEndDateMoment.isBefore(maxPastDaysCompare)) {
continue;
}
if (this.config.hideOngoing && event.startDate < now) {
if (this.config.hideOngoing && eventStartDateMoment.isBefore(now)) {
continue;
}
if (this.listContainsEvent(events, event)) {
if (this.config.hideDuplicates && this.listContainsEvent(events, event)) {
continue;
}
if (--remainingEntries < 0) {
break;
}
}
event.url = calendarUrl;
event.today = event.startDate >= today && event.startDate < today + ONE_DAY;
event.dayBeforeYesterday = event.startDate >= today - ONE_DAY * 2 && event.startDate < today - ONE_DAY;
event.yesterday = event.startDate >= today - ONE_DAY && event.startDate < today;
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
event.dayAfterTomorrow = !event.tomorrow && event.startDate >= today + ONE_DAY * 2 && event.startDate < today + 3 * ONE_DAY;
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
event.url = calendarUrl;
event.today = eventStartDateMoment.isSame(now, "d");
event.dayBeforeYesterday = eventStartDateMoment.isSame(now.clone().subtract(2, "days"), "d");
event.yesterday = eventStartDateMoment.isSame(now.clone().subtract(1, "days"), "d");
event.tomorrow = eventStartDateMoment.isSame(now.clone().add(1, "days"), "d");
event.dayAfterTomorrow = eventStartDateMoment.isSame(now.clone().add(2, "days"), "d");
/*
* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
*/
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / ONE_DAY) + 1;
const maxCount = eventEndDateMoment.diff(eventStartDateMoment, "days");
if (this.config.sliceMultiDayEvents && maxCount > 1) {
const splitEvents = [];
let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
let midnight
= eventStartDateMoment
.clone()
.startOf("day")
.add(1, "day")
.endOf("day");
let count = 1;
while (event.endDate > midnight) {
while (eventEndDateMoment.isAfter(midnight)) {
const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + ONE_DAY;
thisEvent.tomorrow = !thisEvent.today && thisEvent.startDate >= today + ONE_DAY && thisEvent.startDate < today + 2 * ONE_DAY;
thisEvent.endDate = midnight;
thisEvent.today = this.timestampToMoment(thisEvent.startDate).isSame(now, "d");
thisEvent.tomorrow = this.timestampToMoment(thisEvent.startDate).isSame(now.clone().add(1, "days"), "d");
thisEvent.endDate = midnight.clone().subtract(1, "day").format("x");
thisEvent.title += ` (${count}/${maxCount})`;
splitEvents.push(thisEvent);
event.startDate = midnight;
event.startDate = midnight.format("x");
count += 1;
midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
midnight = midnight.clone().add(1, "day").endOf("day"); // next day
}
// Last day
event.title += ` (${count}/${maxCount})`;
event.today += event.startDate >= today && event.startDate < today + ONE_DAY;
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
event.today += this.timestampToMoment(event.startDate).isSame(now, "d");
event.tomorrow = this.timestampToMoment(event.startDate).isSame(now.clone().add(1, "days"), "d");
splitEvents.push(event);
for (let splitEvent of splitEvents) {
if (splitEvent.endDate > now && splitEvent.endDate <= future) {
events.push(splitEvent);
if (this.timestampToMoment(splitEvent.endDate).isAfter(now) && this.timestampToMoment(splitEvent.endDate).isSameOrBefore(future)) {
by_url_calevents.push(splitEvent);
}
}
} else {
events.push(event);
by_url_calevents.push(event);
}
}
if (limitNumberOfEntries) {
// sort entries before clipping
by_url_calevents.sort(function (a, b) {
return a.startDate - b.startDate;
});
Log.debug(`pushing ${by_url_calevents.length} events to total with room for ${remainingEntries}`);
events = events.concat(by_url_calevents.slice(0, remainingEntries));
Log.debug(`events for calendar=${events.length}`);
} else {
events = events.concat(by_url_calevents);
}
}
Log.info(`sorting events count=${events.length}`);
events.sort(function (a, b) {
return a.startDate - b.startDate;
});
@@ -651,17 +701,22 @@ Module.register("calendar", {
return events;
}
// Limit the number of days displayed
// If limitDays is set > 0, limit display to that number of days
/*
* Limit the number of days displayed
* If limitDays is set > 0, limit display to that number of days
*/
if (this.config.limitDays > 0) {
let newEvents = [];
let lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
let lastDate = today.clone().subtract(1, "days");
let days = 0;
for (const ev of events) {
let eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
// if date of event is later than lastdate
// check if we already are showing max unique days
if (eventDate > lastDate) {
let eventDate = this.timestampToMoment(ev.startDate);
/*
* if date of event is later than lastdate
* check if we already are showing max unique days
*/
if (eventDate.isAfter(lastDate)) {
// if the only entry in the first day is a full day event that day is not counted as unique
if (!this.config.limitDaysNeverSkip && newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
days--;
@@ -677,13 +732,13 @@ Module.register("calendar", {
}
events = newEvents;
}
Log.info(`slicing events total maxcount=${this.config.maximumEntries}`);
return events.slice(0, this.config.maximumEntries);
},
listContainsEvent: function (eventList, event) {
listContainsEvent (eventList, event) {
for (const evt of eventList) {
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) {
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate) && parseInt(evt.endDate) === parseInt(event.endDate)) {
return true;
}
}
@@ -692,12 +747,11 @@ Module.register("calendar", {
/**
* Requests node helper to add calendar url.
*
* @param {string} url The calendar url to add
* @param {object} auth The authentication method and credentials
* @param {object} calendarConfig The config of the specific calendar
*/
addCalendar: function (url, auth, calendarConfig) {
addCalendar (url, auth, calendarConfig) {
this.sendSocketNotification("ADD_CALENDAR", {
id: this.identifier,
url: url,
@@ -705,7 +759,7 @@ Module.register("calendar", {
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
pastDaysCount: calendarConfig.pastDaysCount || this.config.pastDaysCount,
fetchInterval: this.config.fetchInterval,
fetchInterval: calendarConfig.fetchInterval || this.config.fetchInterval,
symbolClass: calendarConfig.symbolClass,
titleClass: calendarConfig.titleClass,
timeClass: calendarConfig.timeClass,
@@ -717,11 +771,10 @@ Module.register("calendar", {
/**
* Retrieves the symbols for a specific event.
*
* @param {object} event Event to look for.
* @returns {string[]} The symbols
*/
symbolsForEvent: function (event) {
symbolsForEvent (event) {
let symbols = this.getCalendarPropertyAsArray(event.url, "symbol", this.config.defaultSymbol);
if (event.recurringEvent === true && this.hasCalendarProperty(event.url, "recurringSymbol")) {
@@ -748,7 +801,7 @@ Module.register("calendar", {
return symbols;
},
mergeUnique: function (arr1, arr2) {
mergeUnique (arr1, arr2) {
return arr1.concat(
arr2.filter(function (item) {
return arr1.indexOf(item) === -1;
@@ -758,94 +811,85 @@ Module.register("calendar", {
/**
* Retrieves the symbolClass for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {string} The class to be used for the symbols of the calendar
*/
symbolClassForUrl: function (url) {
symbolClassForUrl (url) {
return this.getCalendarProperty(url, "symbolClass", "");
},
/**
* Retrieves the titleClass for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {string} The class to be used for the title of the calendar
*/
titleClassForUrl: function (url) {
titleClassForUrl (url) {
return this.getCalendarProperty(url, "titleClass", "");
},
/**
* Retrieves the timeClass for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {string} The class to be used for the time of the calendar
*/
timeClassForUrl: function (url) {
timeClassForUrl (url) {
return this.getCalendarProperty(url, "timeClass", "");
},
/**
* Retrieves the calendar name for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {string} The name of the calendar
*/
calendarNameForUrl: function (url) {
calendarNameForUrl (url) {
return this.getCalendarProperty(url, "name", "");
},
/**
* Retrieves the color for a specific calendar url.
*
* @param {string} url The calendar url
* @param {boolean} isBg Determines if we fetch the bgColor or not
* @returns {string} The color
*/
colorForUrl: function (url, isBg) {
colorForUrl (url, isBg) {
return this.getCalendarProperty(url, isBg ? "bgColor" : "color", "#fff");
},
/**
* Retrieves the count title for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {string} The title
*/
countTitleForUrl: function (url) {
countTitleForUrl (url) {
return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
},
/**
* Retrieves the maximum entry count for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {number} The maximum entry count
*/
maximumEntriesForUrl: function (url) {
maximumEntriesForUrl (url) {
return this.getCalendarProperty(url, "maximumEntries", this.config.maximumEntries);
},
/**
* Retrieves the maximum count of past days which events of should be displayed for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {number} The maximum past days count
*/
maximumPastDaysForUrl: function (url) {
maximumPastDaysForUrl (url) {
return this.getCalendarProperty(url, "pastDaysCount", this.config.pastDaysCount);
},
/**
* Helper method to retrieve the property for a specific calendar url.
*
* @param {string} url The calendar url
* @param {string} property The property to look for
* @param {string} defaultValue The value if the property is not found
* @returns {*} The property
*/
getCalendarProperty: function (url, property, defaultValue) {
getCalendarProperty (url, property, defaultValue) {
for (const calendar of this.config.calendars) {
if (calendar.url === url && calendar.hasOwnProperty(property)) {
return calendar[property];
@@ -855,118 +899,30 @@ Module.register("calendar", {
return defaultValue;
},
getCalendarPropertyAsArray: function (url, property, defaultValue) {
getCalendarPropertyAsArray (url, property, defaultValue) {
let p = this.getCalendarProperty(url, property, defaultValue);
if (property === "symbol" || property === "recurringSymbol" || property === "fullDaySymbol") {
const className = this.getCalendarProperty(url, "symbolClassName", this.config.defaultSymbolClassName);
p = className + p;
if (p instanceof Array) {
let t = [];
p.forEach((n) => { t.push(className + n); });
p = t;
}
else p = className + p;
}
if (!(p instanceof Array)) p = [p];
return p;
},
hasCalendarProperty: function (url, property) {
hasCalendarProperty (url, property) {
return !!this.getCalendarProperty(url, property, undefined);
},
/**
* Shortens a string if it's longer than maxLength and add a ellipsis to the end
*
* @param {string} string Text string to shorten
* @param {number} maxLength The max length of the string
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
* @returns {string} The shortened string
*/
shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
if (typeof string !== "string") {
return "";
}
if (wrapEvents === true) {
const words = string.split(" ");
let temp = "";
let currentLine = "";
let line = 0;
for (let i = 0; i < words.length; i++) {
const word = words[i];
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
// max - 1 to account for a space
currentLine += `${word} `;
} else {
line++;
if (line > maxTitleLines - 1) {
if (i < words.length) {
currentLine += "…";
}
break;
}
if (currentLine.length > 0) {
temp += `${currentLine}<br>${word} `;
} else {
temp += `${word}<br>`;
}
currentLine = "";
}
}
return (temp + currentLine).trim();
} else {
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
return `${string.trim().slice(0, maxLength)}`;
} else {
return string.trim();
}
}
},
/**
* Capitalize the first letter of a string
*
* @param {string} string The string to capitalize
* @returns {string} The capitalized string
*/
capFirst: function (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
/**
* Transforms the title of an event for usage.
* Replaces parts of the text as defined in config.titleReplace.
* Shortens title based on config.maxTitleLength and config.wrapEvents
*
* @param {string} title The title to transform.
* @param {object} titleReplace Pairs of strings to be replaced in the title
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
* @param {number} maxTitleLength The max length of the string
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
* @returns {string} The transformed title.
*/
titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
for (let needle in titleReplace) {
const replacement = titleReplace[needle];
const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
if (regParts) {
// the parsed pattern is a regexp.
needle = new RegExp(regParts[1], regParts[2]);
}
title = title.replace(needle, replacement);
}
title = this.shorten(title, maxTitleLength, wrapEvents, maxTitleLines);
return title;
},
/**
* Broadcasts the events to all other modules for reuse.
* The all events available in one array, sorted on startdate.
*/
broadcastEvents: function () {
broadcastEvents () {
const eventList = this.createEventList(false);
for (const event of eventList) {
event.symbol = this.symbolsForEvent(event);
@@ -976,5 +932,30 @@ Module.register("calendar", {
}
this.sendNotification("CALENDAR_EVENTS", eventList);
},
/**
* Refresh the DOM every minute if needed: When using relative date format for events that start
* or end in less than an hour, the date shows minute granularity and we want to keep that accurate.
* --
* When updateOnFetch is not set, it will Avoid fade out/in on updateDom when many calendars are used
* and it's allow to refresh The DOM every minute with animation speed too
* (because updateDom is not set in CALENDAR_EVENTS for this case)
*/
selfUpdate () {
const ONE_MINUTE = 60 * 1000;
setTimeout(
() => {
setInterval(() => {
Log.debug("[Calendar] self update");
if (this.config.updateOnFetch) {
this.updateDom(1);
} else {
this.updateDom(this.config.animationSpeed);
}
}, ONE_MINUTE);
},
ONE_MINUTE - (new Date() % ONE_MINUTE)
);
}
});

View File

@@ -1,17 +1,8 @@
/* MagicMirror²
* Node Helper: Calendar - CalendarFetcher
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const https = require("https");
const digest = require("digest-fetch");
const https = require("node:https");
const ical = require("node-ical");
const fetch = require("fetch");
const Log = require("logger");
const NodeHelper = require("node_helper");
const CalendarUtils = require("./calendarutils");
const CalendarFetcherUtils = require("./calendarfetcherutils");
/**
*
@@ -39,7 +30,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
clearTimeout(reloadTimer);
reloadTimer = null;
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
let fetcher = null;
let httpsAgent = null;
let headers = {
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`
@@ -53,17 +43,12 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
if (auth) {
if (auth.method === "bearer") {
headers.Authorization = `Bearer ${auth.pass}`;
} else if (auth.method === "digest") {
fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, agent: httpsAgent });
} else {
headers.Authorization = `Basic ${Buffer.from(`${auth.user}:${auth.pass}`).toString("base64")}`;
}
}
if (fetcher === null) {
fetcher = fetch(url, { headers: headers, agent: httpsAgent });
}
fetcher
fetch(url, { headers: headers, agent: httpsAgent })
.then(NodeHelper.checkFetchStatus)
.then((response) => response.text())
.then((responseData) => {
@@ -71,8 +56,8 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
try {
data = ical.parseICS(responseData);
Log.debug(`parsed data=${JSON.stringify(data)}`);
events = CalendarUtils.filterEvents(data, {
Log.debug(`parsed data=${JSON.stringify(data, null, 2)}`);
events = CalendarFetcherUtils.filterEvents(data, {
excludedEvents,
includePastEvents,
maximumEntries,
@@ -96,10 +81,13 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
* Schedule the timer for the next update.
*/
const scheduleTimer = function () {
clearTimeout(reloadTimer);
reloadTimer = setTimeout(function () {
fetchCalendar();
}, reloadInterval);
if (process.env.JEST_WORKER_ID === undefined) {
// only set timer when not running in jest
clearTimeout(reloadTimer);
reloadTimer = setTimeout(function () {
fetchCalendar();
}, reloadInterval);
}
};
/* public methods */
@@ -115,13 +103,12 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
* Broadcast the existing events.
*/
this.broadcastEvents = function () {
Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events.`);
Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events from ${url}.`);
eventsReceivedCallback(this);
};
/**
* Sets the on success callback
*
* @param {Function} callback The on success callback.
*/
this.onReceive = function (callback) {
@@ -130,7 +117,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/**
* Sets the on error callback
*
* @param {Function} callback The on error callback.
*/
this.onError = function (callback) {
@@ -139,7 +125,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/**
* Returns the url of this fetcher.
*
* @returns {string} The url of this fetcher.
*/
this.url = function () {
@@ -148,7 +133,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/**
* Returns current available events for this fetcher.
*
* @returns {object[]} The current available events for this fetcher.
*/
this.events = function () {

View File

@@ -0,0 +1,431 @@
/**
* @external Moment
*/
const moment = require("moment-timezone");
const Log = require("../../../js/logger");
const CalendarFetcherUtils = {
/**
* Determine based on the title of an event if it should be excluded from the list of events
* TODO This seems like an overly complicated way to exclude events based on the title.
* @param {object} config the global config
* @param {string} title the title of the event
* @returns {object} excluded: true if the event should be excluded, false otherwise
* until: the date until the event should be excluded.
*/
shouldEventBeExcluded (config, title) {
let filter = {
excluded: false,
until: null
};
for (let f in config.excludedEvents) {
let filter = config.excludedEvents[f],
testTitle = title.toLowerCase(),
until = null,
useRegex = false,
regexFlags = "g";
if (filter instanceof Object) {
if (typeof filter.until !== "undefined") {
until = filter.until;
}
if (typeof filter.regex !== "undefined") {
useRegex = filter.regex;
}
// If additional advanced filtering is added in, this section
// must remain last as we overwrite the filter object with the
// filterBy string
if (filter.caseSensitive) {
filter = filter.filterBy;
testTitle = title;
} else if (useRegex) {
filter = filter.filterBy;
testTitle = title;
regexFlags += "i";
} else {
filter = filter.filterBy.toLowerCase();
}
} else {
filter = filter.toLowerCase();
}
if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
if (until) {
filter.until = until;
} else {
filter.excluded = true;
}
break;
}
}
return filter;
},
/**
* Get local timezone.
* This method makes it easier to test if different timezones cause problems by changing this implementation.
* @returns {string} timezone
*/
getLocalTimezone () {
return moment.tz.guess();
},
/**
* This function returns a list of moments for a recurring event.
* @param {object} event the current event which is a recurring event
* @param {moment.Moment} pastLocalMoment The past date to search for recurring events
* @param {moment.Moment} futureLocalMoment The future date to search for recurring events
* @param {number} durationInMs the duration of the event, this is used to take into account currently running events
* @returns {moment.Moment[]} All moments for the recurring event
*/
getMomentsFromRecurringEvent (event, pastLocalMoment, futureLocalMoment, durationInMs) {
const rule = event.rrule;
// can cause problems with e.g. birthdays before 1900
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
rule.origOptions.dtstart.setYear(1900);
rule.options.dtstart.setYear(1900);
}
// subtract the max of the duration of this event or 1 day to find events in the past that are currently still running and should therefor be displayed.
const oneDayInMs = 24 * 60 * 60000;
let searchFromDate = pastLocalMoment.clone().subtract(Math.max(durationInMs, oneDayInMs), "milliseconds").toDate();
let searchToDate = futureLocalMoment.clone().add(1, "days").toDate();
Log.debug(`Search for recurring events between: ${searchFromDate} and ${searchToDate}`);
// if until is set, and its a full day event, force the time to midnight. rrule gets confused with non-00 offset
// looks like MS Outlook sets the until time incorrectly for fullday events
if ((rule.options.until !== undefined) && CalendarFetcherUtils.isFullDayEvent(event)) {
Log.debug("fixup rrule until");
rule.options.until = moment(rule.options.until).clone().startOf("day").add(1, "day")
.toDate();
}
Log.debug("fix rrule start=", rule.options.dtstart);
Log.debug("event before rrule.between=", JSON.stringify(event, null, 2), "exdates=", event.exdate);
Log.debug(`RRule: ${rule.toString()}`);
rule.options.tzid = null; // RRule gets *very* confused with timezones
let dates = rule.between(searchFromDate, searchToDate, true, () => {
return true;
});
Log.debug(`Title: ${event.summary}, with dates: \n\n${JSON.stringify(dates)}\n`);
// shouldn't need this anymore, as RRULE not passed junk
dates = dates.filter((d) => {
return JSON.stringify(d) !== "null";
});
// Dates are returned in UTC timezone but with localdatetime because tzid is null.
// So we map the date to a moment using the original timezone of the event.
return dates.map((d) => (event.start.tz ? moment.tz(d, "UTC").tz(event.start.tz, true) : moment.tz(d, "UTC").tz(CalendarFetcherUtils.getLocalTimezone(), true)));
},
/**
* Filter the events from ical according to the given config
* @param {object} data the calendar data from ical
* @param {object} config The configuration object
* @returns {string[]} the filtered events
*/
filterEvents (data, config) {
const newEvents = [];
const eventDate = function (event, time) {
const startMoment = event[time].tz ? moment.tz(event[time], event[time].tz) : moment.tz(event[time], CalendarFetcherUtils.getLocalTimezone());
return CalendarFetcherUtils.isFullDayEvent(event) ? startMoment.startOf("day") : startMoment;
};
Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
const now = moment();
const pastLocalMoment = config.includePastEvents ? now.clone().startOf("day").subtract(config.maximumNumberOfDays, "days") : now;
const futureLocalMoment
= now
.clone()
.startOf("day")
.add(config.maximumNumberOfDays, "days")
// Subtract 1 second so that events that start on the middle of the night will not repeat.
.subtract(1, "seconds");
Object.entries(data).forEach(([key, event]) => {
Log.debug("Processing entry...");
const title = CalendarFetcherUtils.getTitleFromEvent(event);
Log.debug(`title: ${title}`);
// Return quickly if event should be excluded.
let { excluded, eventFilterUntil } = this.shouldEventBeExcluded(config, title);
if (excluded) {
return;
}
// FIXME: Ugly fix to solve the facebook birthday issue.
// Otherwise, the recurring events only show the birthday for next year.
let isFacebookBirthday = false;
if (typeof event.uid !== "undefined") {
if (event.uid.indexOf("@facebook.com") !== -1) {
isFacebookBirthday = true;
}
}
if (event.type === "VEVENT") {
Log.debug(`Event:\n${JSON.stringify(event, null, 2)}`);
let eventStartMoment = eventDate(event, "start");
let eventEndMoment;
if (typeof event.end !== "undefined") {
eventEndMoment = eventDate(event, "end");
} else if (typeof event.duration !== "undefined") {
eventEndMoment = eventStartMoment.clone().add(moment.duration(event.duration));
} else {
if (!isFacebookBirthday) {
// make copy of start date, separate storage area
eventEndMoment = eventStartMoment.clone();
} else {
eventEndMoment = eventStartMoment.clone().add(1, "days");
}
}
Log.debug(`start: ${eventStartMoment.toDate()}`);
Log.debug(`end:: ${eventEndMoment.toDate()}`);
// Calculate the duration of the event for use with recurring events.
const durationMs = eventEndMoment.valueOf() - eventStartMoment.valueOf();
Log.debug(`duration: ${durationMs}`);
const location = event.location || false;
const geo = event.geo || false;
const description = event.description || false;
// TODO This should be a seperate function.
if (event.rrule && typeof event.rrule !== "undefined" && !isFacebookBirthday) {
// Recurring event.
let moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(event, pastLocalMoment, futureLocalMoment, durationMs);
// Loop through the set of moment entries to see which recurrences should be added to our event list.
// TODO This should create an event per moment so we can change anything we want.
for (let m in moments) {
let curEvent = event;
let showRecurrence = true;
let recurringEventStartMoment = moments[m].tz(CalendarFetcherUtils.getLocalTimezone()).clone();
let recurringEventEndMoment = recurringEventStartMoment.clone().add(durationMs, "ms");
let dateKey = recurringEventStartMoment.tz("UTC").format("YYYY-MM-DD");
Log.debug("event date dateKey=", dateKey);
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
if (curEvent.recurrences !== undefined) {
Log.debug("have recurrences=", curEvent.recurrences);
if (curEvent.recurrences[dateKey] !== undefined) {
Log.debug("have a recurrence match for dateKey=", dateKey);
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
curEvent = curEvent.recurrences[dateKey];
// Some event start/end dates don't have timezones
if (curEvent.start.tz) {
recurringEventStartMoment = moment(curEvent.start).tz(curEvent.start.tz).tz(CalendarFetcherUtils.getLocalTimezone());
} else {
recurringEventStartMoment = moment(curEvent.start).tz(CalendarFetcherUtils.getLocalTimezone());
}
if (curEvent.end.tz) {
recurringEventEndMoment = moment(curEvent.end).tz(curEvent.end.tz).tz(CalendarFetcherUtils.getLocalTimezone());
} else {
recurringEventEndMoment = moment(curEvent.end).tz(CalendarFetcherUtils.getLocalTimezone());
}
} else {
Log.debug("recurrence key ", dateKey, " doesn't match");
}
}
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
if (curEvent.exdate !== undefined) {
Log.debug("have datekey=", dateKey, " exdates=", curEvent.exdate);
if (curEvent.exdate[dateKey] !== undefined) {
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
}
}
if (recurringEventStartMoment.valueOf() === recurringEventEndMoment.valueOf()) {
recurringEventEndMoment = recurringEventEndMoment.endOf("day");
}
const recurrenceTitle = CalendarFetcherUtils.getTitleFromEvent(curEvent);
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
// it to the event list.
if (recurringEventEndMoment.isBefore(pastLocalMoment) || recurringEventStartMoment.isAfter(futureLocalMoment)) {
showRecurrence = false;
}
if (CalendarFetcherUtils.timeFilterApplies(now, recurringEventEndMoment, eventFilterUntil)) {
showRecurrence = false;
}
if (showRecurrence === true) {
Log.debug(`saving event: ${recurrenceTitle}`);
newEvents.push({
title: recurrenceTitle,
startDate: recurringEventStartMoment.format("x"),
endDate: recurringEventEndMoment.format("x"),
fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event),
recurringEvent: true,
class: event.class,
firstYear: event.start.getFullYear(),
location: location,
geo: geo,
description: description
});
} else {
Log.debug("not saving event ", recurrenceTitle, eventStartMoment);
}
Log.debug(" ");
}
// End recurring event parsing.
} else {
// Single event.
const fullDayEvent = isFacebookBirthday ? true : CalendarFetcherUtils.isFullDayEvent(event);
// Log.debug("full day event")
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
if (fullDayEvent && eventStartMoment.valueOf() === eventEndMoment.valueOf()) {
eventEndMoment = eventEndMoment.endOf("day");
}
if (config.includePastEvents) {
// Past event is too far in the past, so skip.
if (eventEndMoment < pastLocalMoment) {
return;
}
} else {
// It's not a fullday event, and it is in the past, so skip.
if (!fullDayEvent && eventEndMoment < now) {
return;
}
// It's a fullday event, and it is before today, So skip.
if (fullDayEvent && eventEndMoment <= now.startOf("day")) {
return;
}
}
// It exceeds the maximumNumberOfDays limit, so skip.
if (eventStartMoment > futureLocalMoment) {
return;
}
if (CalendarFetcherUtils.timeFilterApplies(now, eventEndMoment, eventFilterUntil)) {
return;
}
// Every thing is good. Add it to the list.
newEvents.push({
title: title,
startDate: eventStartMoment.format("x"),
endDate: eventEndMoment.format("x"),
fullDayEvent: fullDayEvent,
recurringEvent: false,
class: event.class,
firstYear: event.start.getFullYear(),
location: location,
geo: geo,
description: description
});
}
}
});
newEvents.sort(function (a, b) {
return a.startDate - b.startDate;
});
return newEvents;
},
/**
* Gets the title from the event.
* @param {object} event The event object to check.
* @returns {string} The title of the event, or "Event" if no title is found.
*/
getTitleFromEvent (event) {
let title = "Event";
if (event.summary) {
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
} else if (event.description) {
title = event.description;
}
return title;
},
/**
* Checks if an event is a fullday event.
* @param {object} event The event object to check.
* @returns {boolean} True if the event is a fullday event, false otherwise
*/
isFullDayEvent (event) {
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
return true;
}
const start = event.start || 0;
const startDate = new Date(start);
const end = event.end || 0;
if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
// Is 24 hours, and starts on the middle of the night.
return true;
}
return false;
},
/**
* Determines if the user defined time filter should apply
* @param {moment.Moment} now Date object using previously created object for consistency
* @param {moment.Moment} endDate Moment object representing the event end date
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
* @returns {boolean} True if the event should be filtered out, false otherwise
*/
timeFilterApplies (now, endDate, filter) {
if (filter) {
const until = filter.split(" "),
value = parseInt(until[0]),
increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js
filterUntil = moment(endDate.format()).subtract(value, increment);
return now < filterUntil;
}
return false;
},
/**
* Determines if the user defined title filter should apply
* @param {string} title the title of the event
* @param {string} filter the string to look for, can be a regex also
* @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
* @param {string} regexFlags flags that should be applied to the regex
* @returns {boolean} True if the title should be filtered out, false otherwise
*/
titleFilterApplies (title, filter, useRegex, regexFlags) {
if (useRegex) {
let regexFilter = filter;
// Assume if leading slash, there is also trailing slash
if (filter[0] === "/") {
// Strip leading and trailing slashes
regexFilter = filter.substr(1).slice(0, -1);
}
return new RegExp(regexFilter, regexFlags).test(title);
} else {
return title.includes(filter);
}
}
};
if (typeof module !== "undefined") {
module.exports = CalendarFetcherUtils;
}

View File

@@ -1,610 +1,125 @@
/* MagicMirror²
* Calendar Util Methods
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
/**
* @external Moment
*/
const path = require("path");
const moment = require("moment");
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
const Log = require("../../../js/logger");
const CalendarUtils = {
/**
* Calculate the time correction, either dst/std or full day in cases where
* utc time is day before plus offset
*
* @param {object} event the event which needs adjustement
* @param {Date} date the date on which this event happens
* @returns {number} the necessary adjustment in hours
* Capitalize the first letter of a string
* @param {string} string The string to capitalize
* @returns {string} The capitalized string
*/
calculateTimezoneAdjustment: function (event, date) {
let adjustHours = 0;
// if a timezone was specified
if (!event.start.tz) {
Log.debug(" if no tz, guess based on now");
event.start.tz = moment.tz.guess();
}
Log.debug(`initial tz=${event.start.tz}`);
// if there is a start date specified
if (event.start.tz) {
// if this is a windows timezone
if (event.start.tz.includes(" ")) {
// use the lookup table to get theIANA name as moment and date don't know MS timezones
let tz = CalendarUtils.getIanaTZFromMS(event.start.tz);
Log.debug(`corrected TZ=${tz}`);
// watch out for unregistered windows timezone names
// if we had a successful lookup
if (tz) {
// change the timezone to the IANA name
event.start.tz = tz;
// Log.debug("corrected timezone="+event.start.tz)
}
}
Log.debug(`corrected tz=${event.start.tz}`);
let current_offset = 0; // offset from TZ string or calculated
let mm = 0; // date with tz or offset
let start_offset = 0; // utc offset of created with tz
// if there is still an offset, lookup failed, use it
if (event.start.tz.startsWith("(")) {
const regex = /[+|-]\d*:\d*/;
const start_offsetString = event.start.tz.match(regex).toString().split(":");
let start_offset = parseInt(start_offsetString[0]);
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
adjustHours = start_offset;
Log.debug(`defined offset=${start_offset} hours`);
current_offset = start_offset;
event.start.tz = "";
Log.debug(`ical offset=${current_offset} date=${date}`);
mm = moment(date);
let x = parseInt(moment(new Date()).utcOffset());
Log.debug(`net mins=${current_offset * 60 - x}`);
mm = mm.add(x - current_offset * 60, "minutes");
adjustHours = (current_offset * 60 - x) / 60;
event.start = mm.toDate();
Log.debug(`adjusted date=${event.start}`);
} else {
// get the start time in that timezone
let es = moment(event.start);
// check for start date prior to start of daylight changing date
if (es.format("YYYY") < 2007) {
es.set("year", 2013); // if so, use a closer date
}
Log.debug(`start date/time=${es.toDate()}`);
start_offset = moment.tz(es, event.start.tz).utcOffset();
Log.debug(`start offset=${start_offset}`);
Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`);
// get the specified date in that timezone
mm = moment.tz(moment(date), event.start.tz);
Log.debug(`event date=${mm.toDate()}`);
current_offset = mm.utcOffset();
}
Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`);
// if the offset is greater than 0, east of london
if (current_offset !== start_offset) {
// big offset
Log.debug("offset");
let h = parseInt(mm.format("H"));
// check if the event time is less than the offset
if (h > 0 && h < Math.abs(current_offset) / 60) {
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
// we need to fix that
//adjustHours = 24;
// Log.debug("adjusting date")
}
//-300 > -240
//if (Math.abs(current_offset) > Math.abs(start_offset)){
if (current_offset > start_offset) {
adjustHours -= 1;
Log.debug("adjust down 1 hour dst change");
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
} else if (current_offset < start_offset) {
adjustHours += 1;
Log.debug("adjust up 1 hour dst change");
}
}
}
Log.debug(`adjustHours=${adjustHours}`);
return adjustHours;
capFirst (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
/**
* Filter the events from ical according to the given config
*
* @param {object} data the calendar data from ical
* @param {object} config The configuration object
* @returns {string[]} the filtered events
* This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
* corresponding time-format to be used in the calendar display. If no number is given (or otherwise invalid input)
* it will a localeSpecification object with the system locale time format.
* @param {number} timeFormat Specifies either 12 or 24-hour time format
* @returns {moment.LocaleSpecification} formatted time
*/
filterEvents: function (data, config) {
const newEvents = [];
// limitFunction doesn't do much limiting, see comment re: the dates
// array in rrule section below as to why we need to do the filtering
// ourselves
const limitFunction = function (date, i) {
return true;
};
const eventDate = function (event, time) {
return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
};
Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
Object.entries(data).forEach(([key, event]) => {
Log.debug("Processing entry...");
const now = new Date();
const today = moment().startOf("day").toDate();
const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
let past = today;
if (config.includePastEvents) {
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
getLocaleSpecification (timeFormat) {
switch (timeFormat) {
case 12: {
return { longDateFormat: { LT: "h:mm A" } };
}
// FIXME: Ugly fix to solve the facebook birthday issue.
// Otherwise, the recurring events only show the birthday for next year.
let isFacebookBirthday = false;
if (typeof event.uid !== "undefined") {
if (event.uid.indexOf("@facebook.com") !== -1) {
isFacebookBirthday = true;
}
case 24: {
return { longDateFormat: { LT: "HH:mm" } };
}
default: {
return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
}
}
},
if (event.type === "VEVENT") {
Log.debug(`Event:\n${JSON.stringify(event)}`);
let startDate = eventDate(event, "start");
let endDate;
/**
* Shortens a string if it's longer than maxLength and add an ellipsis to the end
* @param {string} string Text string to shorten
* @param {number} maxLength The max length of the string
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
* @returns {string} The shortened string
*/
shorten (string, maxLength, wrapEvents, maxTitleLines) {
if (typeof string !== "string") {
return "";
}
if (typeof event.end !== "undefined") {
endDate = eventDate(event, "end");
} else if (typeof event.duration !== "undefined") {
endDate = startDate.clone().add(moment.duration(event.duration));
if (wrapEvents === true) {
const words = string.split(" ");
let temp = "";
let currentLine = "";
let line = 0;
for (let i = 0; i < words.length; i++) {
const word = words[i];
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
// max - 1 to account for a space
currentLine += `${word} `;
} else {
if (!isFacebookBirthday) {
// make copy of start date, separate storage area
endDate = moment(startDate.format("x"), "x");
} else {
endDate = moment(startDate).add(1, "days");
}
}
Log.debug(`start: ${startDate.toDate()}`);
Log.debug(`end:: ${endDate.toDate()}`);
// Calculate the duration of the event for use with recurring events.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
Log.debug(`duration: ${duration}`);
// FIXME: Since the parsed json object from node-ical comes with time information
// this check could be removed (?)
if (event.start.length === 8) {
startDate = startDate.startOf("day");
}
const title = CalendarUtils.getTitleFromEvent(event);
Log.debug(`title: ${title}`);
let excluded = false,
dateFilter = null;
for (let f in config.excludedEvents) {
let filter = config.excludedEvents[f],
testTitle = title.toLowerCase(),
until = null,
useRegex = false,
regexFlags = "g";
if (filter instanceof Object) {
if (typeof filter.until !== "undefined") {
until = filter.until;
}
if (typeof filter.regex !== "undefined") {
useRegex = filter.regex;
}
// If additional advanced filtering is added in, this section
// must remain last as we overwrite the filter object with the
// filterBy string
if (filter.caseSensitive) {
filter = filter.filterBy;
testTitle = title;
} else if (useRegex) {
filter = filter.filterBy;
testTitle = title;
regexFlags += "i";
} else {
filter = filter.filterBy.toLowerCase();
}
} else {
filter = filter.toLowerCase();
}
if (CalendarUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
if (until) {
dateFilter = until;
} else {
excluded = true;
line++;
if (line > maxTitleLines - 1) {
if (i < words.length) {
currentLine += "…";
}
break;
}
}
if (excluded) {
return;
}
const location = event.location || false;
const geo = event.geo || false;
const description = event.description || false;
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
const rule = event.rrule;
let addedEvents = 0;
const pastMoment = moment(past);
const futureMoment = moment(future);
// can cause problems with e.g. birthdays before 1900
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
rule.origOptions.dtstart.setYear(1900);
rule.options.dtstart.setYear(1900);
}
// For recurring events, get the set of start dates that fall within the range
// of dates we're looking for.
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
let pastLocal = 0;
let futureLocal = 0;
if (CalendarUtils.isFullDayEvent(event)) {
Log.debug("fullday");
// if full day event, only use the date part of the ranges
pastLocal = pastMoment.toDate();
futureLocal = futureMoment.toDate();
Log.debug(`pastLocal: ${pastLocal}`);
Log.debug(`futureLocal: ${futureLocal}`);
if (currentLine.length > 0) {
temp += `${currentLine}<br>${word} `;
} else {
// if we want past events
if (config.includePastEvents) {
// use the calculated past time for the between from
pastLocal = pastMoment.toDate();
} else {
// otherwise use NOW.. cause we shouldn't use any before now
pastLocal = moment().toDate(); //now
}
futureLocal = futureMoment.toDate(); // future
temp += `${word}<br>`;
}
Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
// The "dates" array contains the set of dates within our desired date range range that are valid
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
// had its date changed from outside the range to inside the range. For the time being,
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
// because the logic below will filter out any recurrences that don't actually belong within
// our display range.
// Would be great if there was a better way to handle this.
Log.debug(`event.recurrences: ${event.recurrences}`);
if (event.recurrences !== undefined) {
for (let r in event.recurrences) {
// Only add dates that weren't already in the range we added from the rrule so that
// we don"t double-add those events.
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
dates.push(new Date(r));
}
}
}
// Loop through the set of date entries to see which recurrences should be added to our event list.
for (let d in dates) {
let date = dates[d];
// Remove the time information of each date by using its substring, using the following method:
// .toISOString().substring(0,10).
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
const dateKey = date.toISOString().substring(0, 10);
let curEvent = event;
let showRecurrence = true;
// Get the offset of today where we are processing
// This will be the correction, we need to apply.
let nowOffset = new Date().getTimezoneOffset();
// For full day events, the time might be off from RRULE/Luxon problem
// Get time zone offset of the rule calculated event
let dateoffset = date.getTimezoneOffset();
// Reduce the time by the following offset.
Log.debug(` recurring date is ${date} offset is ${dateoffset}`);
let dh = moment(date).format("HH");
Log.debug(` recurring date is ${date} offset is ${dateoffset / 60} Hour is ${dh}`);
if (CalendarUtils.isFullDayEvent(event)) {
Log.debug("Fullday");
// If the offset is negative (east of GMT), where the problem is
if (dateoffset < 0) {
if (dh < Math.abs(dateoffset / 60)) {
// if the rrule byweekday WAS explicitly set , correct it
// reduce the time by the offset
if (curEvent.rrule.origOptions.byweekday !== undefined) {
// Apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
}
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug(`new recurring date1 fulldate is ${date}`);
}
} else {
// if the timezones are the same, correct date if needed
//if (event.start.tz === moment.tz.guess()) {
// if the date hour is less than the offset
if (24 - dh <= Math.abs(dateoffset / 60)) {
// if the rrule byweekday WAS explicitly set , correct it
if (curEvent.rrule.origOptions.byweekday !== undefined) {
// apply the correction to the date/time back to right day
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
}
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug(`new recurring date2 fulldate is ${date}`);
}
//}
}
} else {
// not full day, but luxon can still screw up the date on the rule processing
// we need to correct the date to get back to the right event for
if (dateoffset < 0) {
// if the date hour is less than the offset
if (dh <= Math.abs(dateoffset / 60)) {
// if the rrule byweekday WAS explicitly set , correct it
if (curEvent.rrule.origOptions.byweekday !== undefined) {
// Reduce the time by t:
// Apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
}
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug(`new recurring date1 is ${date}`);
}
} else {
// if the timezones are the same, correct date if needed
//if (event.start.tz === moment.tz.guess()) {
// if the date hour is less than the offset
if (24 - dh <= Math.abs(dateoffset / 60)) {
// if the rrule byweekday WAS explicitly set , correct it
if (curEvent.rrule.origOptions.byweekday !== undefined) {
// apply the correction to the date/time back to right day
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
}
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug(`new recurring date2 is ${date}`);
}
//}
}
}
startDate = moment(date);
Log.debug(`Corrected startDate: ${startDate.toDate()}`);
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
curEvent = curEvent.recurrences[dateKey];
startDate = moment(curEvent.start);
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
}
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
}
Log.debug(`duration: ${duration}`);
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (startDate.format("x") === endDate.format("x")) {
endDate = endDate.endOf("day");
}
const recurrenceTitle = CalendarUtils.getTitleFromEvent(curEvent);
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
// it to the event list.
if (endDate.isBefore(past) || startDate.isAfter(future)) {
showRecurrence = false;
}
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
showRecurrence = false;
}
if (showRecurrence === true) {
Log.debug(`saving event: ${description}`);
addedEvents++;
newEvents.push({
title: recurrenceTitle,
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: CalendarUtils.isFullDayEvent(event),
recurringEvent: true,
class: event.class,
firstYear: event.start.getFullYear(),
location: location,
geo: geo,
description: description
});
}
}
// End recurring event parsing.
} else {
// Single event.
const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event);
// Log.debug("full day event")
if (config.includePastEvents) {
// Past event is too far in the past, so skip.
if (endDate < past) {
return;
}
} else {
// It's not a fullday event, and it is in the past, so skip.
if (!fullDayEvent && endDate < new Date()) {
return;
}
// It's a fullday event, and it is before today, So skip.
if (fullDayEvent && endDate <= today) {
return;
}
}
// It exceeds the maximumNumberOfDays limit, so skip.
if (startDate > future) {
return;
}
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
return;
}
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
endDate = endDate.endOf("day");
}
// get correction for date saving and dst change between now and then
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, startDate.toDate());
// Every thing is good. Add it to the list.
newEvents.push({
title: title,
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: fullDayEvent,
class: event.class,
location: location,
geo: geo,
description: description
});
currentLine = "";
}
}
});
newEvents.sort(function (a, b) {
return a.startDate - b.startDate;
});
return newEvents;
},
/**
* Lookup iana tz from windows
*
* @param {string} msTZName the timezone name to lookup
* @returns {string|null} the iana name or null of none is found
*/
getIanaTZFromMS: function (msTZName) {
// Get hash entry
const he = zoneTable[msTZName];
// If found return iana name, else null
return he ? he.iana[0] : null;
},
/**
* Gets the title from the event.
*
* @param {object} event The event object to check.
* @returns {string} The title of the event, or "Event" if no title is found.
*/
getTitleFromEvent: function (event) {
let title = "Event";
if (event.summary) {
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
} else if (event.description) {
title = event.description;
}
return title;
},
/**
* Checks if an event is a fullday event.
*
* @param {object} event The event object to check.
* @returns {boolean} True if the event is a fullday event, false otherwise
*/
isFullDayEvent: function (event) {
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
return true;
}
const start = event.start || 0;
const startDate = new Date(start);
const end = event.end || 0;
if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
// Is 24 hours, and starts on the middle of the night.
return true;
}
return false;
},
/**
* Determines if the user defined time filter should apply
*
* @param {Date} now Date object using previously created object for consistency
* @param {Moment} endDate Moment object representing the event end date
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
* @returns {boolean} True if the event should be filtered out, false otherwise
*/
timeFilterApplies: function (now, endDate, filter) {
if (filter) {
const until = filter.split(" "),
value = parseInt(until[0]),
increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js
filterUntil = moment(endDate.format()).subtract(value, increment);
return now < filterUntil.format("x");
}
return false;
},
/**
* Determines if the user defined title filter should apply
*
* @param {string} title the title of the event
* @param {string} filter the string to look for, can be a regex also
* @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
* @param {string} regexFlags flags that should be applied to the regex
* @returns {boolean} True if the title should be filtered out, false otherwise
*/
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
if (useRegex) {
// Assume if leading slash, there is also trailing slash
if (filter[0] === "/") {
// Strip leading and trailing slashes
filter = filter.substr(1).slice(0, -1);
}
filter = new RegExp(filter, regexFlags);
return filter.test(title);
return (temp + currentLine).trim();
} else {
return title.includes(filter);
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
return `${string.trim().slice(0, maxLength)}`;
} else {
return string.trim();
}
}
},
/**
* Transforms the title of an event for usage.
* Replaces parts of the text as defined in config.titleReplace.
* @param {string} title The title to transform.
* @param {object} titleReplace object definition of parts to be replaced in the title
* object definition:
* search: {string,required} RegEx in format //x or simple string to be searched. For (birthday) year calcluation, the element matching the year must be in a RegEx group
* replace: {string,required} Replacement string, may contain match group references (latter is required for year calculation)
* yearmatchgroup: {number,optional} match group for year element
* @returns {string} The transformed title.
*/
titleTransform (title, titleReplace) {
let transformedTitle = title;
for (let tr in titleReplace) {
let transform = titleReplace[tr];
if (typeof transform === "object") {
if (typeof transform.search !== "undefined" && transform.search !== "" && typeof transform.replace !== "undefined") {
let regParts = transform.search.match(/^\/(.+)\/([gim]*)$/);
let needle = new RegExp(transform.search, "g");
if (regParts) {
// the parsed pattern is a regexp with flags.
needle = new RegExp(regParts[1], regParts[2]);
}
let replacement = transform.replace;
if (typeof transform.yearmatchgroup !== "undefined" && transform.yearmatchgroup !== "") {
const yearmatch = [...title.matchAll(needle)];
if (yearmatch[0].length >= transform.yearmatchgroup + 1 && yearmatch[0][transform.yearmatchgroup] * 1 >= 1900) {
let calcage = new Date().getFullYear() - yearmatch[0][transform.yearmatchgroup] * 1;
let searchstr = `$${transform.yearmatchgroup}`;
replacement = replacement.replace(searchstr, calcage);
}
}
transformedTitle = transformedTitle.replace(needle, replacement);
}
}
}
return transformedTitle;
}
};

View File

@@ -1,9 +1,7 @@
/* CalendarFetcher Tester
/*
* CalendarFetcher Tester
* use this script with `node debug.js` to test the fetcher without the need
* of starting the MagicMirror² core. Adjust the values below to your desire.
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
// Alias modules mentioned in package.js under _moduleAliases.
require("module-alias/register");

View File

@@ -1,22 +1,16 @@
/* MagicMirror²
* Node Helper: Calendar
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const NodeHelper = require("node_helper");
const Log = require("logger");
const CalendarFetcher = require("./calendarfetcher");
module.exports = NodeHelper.create({
// Override start method.
start: function () {
start () {
Log.log(`Starting node helper for: ${this.name}`);
this.fetchers = [];
},
// Override socketNotificationReceived method.
socketNotificationReceived: function (notification, payload) {
socketNotificationReceived (notification, payload) {
if (notification === "ADD_CALENDAR") {
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.selfSignedCert, payload.id);
} else if (notification === "FETCH_CALENDAR") {
@@ -33,7 +27,6 @@ module.exports = NodeHelper.create({
/**
* Creates a fetcher for a new url if it doesn't exist yet.
* Otherwise it reuses the existing one.
*
* @param {string} url The url of the calendar
* @param {number} fetchInterval How often does the calendar needs to be fetched in ms
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
@@ -44,7 +37,7 @@ module.exports = NodeHelper.create({
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
* @param {string} identifier ID of the module
*/
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
createFetcher (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
try {
new URL(url);
} catch (error) {
@@ -54,9 +47,14 @@ module.exports = NodeHelper.create({
}
let fetcher;
let fetchIntervalCorrected;
if (typeof this.fetchers[identifier + url] === "undefined") {
Log.log(`Create new calendarfetcher for url: ${url} - Interval: ${fetchInterval}`);
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
if (fetchInterval < 60000) {
Log.warn(`fetchInterval for url ${url} must be >= 60000`);
fetchIntervalCorrected = 60000;
}
Log.log(`Create new calendarfetcher for url: ${url} - Interval: ${fetchIntervalCorrected || fetchInterval}`);
fetcher = new CalendarFetcher(url, fetchIntervalCorrected || fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
fetcher.onReceive((fetcher) => {
this.broadcastEvents(fetcher, identifier);
@@ -86,7 +84,7 @@ module.exports = NodeHelper.create({
* @param {object} fetcher the fetcher associated with the calendar
* @param {string} identifier the identifier of the calendar
*/
broadcastEvents: function (fetcher, identifier) {
broadcastEvents (fetcher, identifier) {
this.sendSocketNotification("CALENDAR_EVENTS", {
id: identifier,
url: fetcher.url(),

View File

@@ -1,11 +1,5 @@
/* global SunCalc */
/* global SunCalc, formatTime */
/* MagicMirror²
* Module: Clock
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
Module.register("clock", {
// Module config defaults.
defaults: {
@@ -20,7 +14,7 @@ Module.register("clock", {
clockBold: false,
showDate: true,
showTime: true,
showWeek: false,
showWeek: false, // options: true, false, 'short'
dateFormat: "dddd, LL",
sendNotifications: false,
@@ -29,23 +23,23 @@ Module.register("clock", {
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",
secondsColor: "#888888", // DEPRECATED, use CSS instead. Class "clock-second-digital" for digital clock, "clock-second" for analog clock.
showSunTimes: false,
showMoonTimes: false,
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: function () {
getScripts () {
return ["moment.js", "moment-timezone.js", "suncalc.js"];
},
// Define styles.
getStyles: function () {
getStyles () {
return ["clock_styles.css"];
},
// Define start sequence.
start: function () {
start () {
Log.info(`Starting module: ${this.name}`);
// Schedule update interval.
@@ -94,7 +88,7 @@ Module.register("clock", {
moment.locale(config.language);
},
// Override dom generator.
getDom: function () {
getDom () {
const wrapper = document.createElement("div");
wrapper.classList.add("clock-grid");
@@ -105,13 +99,14 @@ Module.register("clock", {
analogWrapper.className = "clock-circle";
const digitalWrapper = document.createElement("div");
digitalWrapper.className = "digital";
digitalWrapper.style.gridArea = "center";
/************************************
* 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");
@@ -121,39 +116,40 @@ Module.register("clock", {
// Style Wrappers
dateWrapper.className = "date normal medium";
timeWrapper.className = "time bright large light";
secondsWrapper.className = "seconds dimmed";
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.
// The moment().format("h") method has a bug on the Raspberry Pi.
// So we need to generate the timestring manually.
// See issue: https://github.com/MichMich/MagicMirror/issues/181
let timeString;
const now = moment();
if (this.config.timezone) {
now.tz(this.config.timezone);
}
let hourSymbol = "HH";
if (this.config.timeFormat !== 24) {
hourSymbol = "h";
}
if (this.config.clockBold) {
timeString = now.format(`${hourSymbol}[<span class="bold">]mm[</span>]`);
} else {
timeString = now.format(`${hourSymbol}:mm`);
}
if (this.config.showDate) {
dateWrapper.innerHTML = now.format(this.config.dateFormat);
digitalWrapper.appendChild(dateWrapper);
}
if (this.config.displayType !== "analog" && this.config.showTime) {
timeWrapper.innerHTML = timeString;
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");
@@ -169,42 +165,34 @@ Module.register("clock", {
digitalWrapper.appendChild(timeWrapper);
}
/**
* Format the time according to the config
*
* @param {object} config The config of the module
* @param {object} time time to format
* @returns {string} The formatted time string
*/
function formatTime(config, time) {
let formatString = `${hourSymbol}:mm`;
if (config.showPeriod && config.timeFormat !== 24) {
formatString += config.showPeriodUpper ? "A" : "a";
}
return moment(time).format(formatString);
}
/****************************************************************
* 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 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;
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>`;
}
const untilNextEvent = moment.duration(moment(nextEvent).diff(now));
const untilNextEventString = `${untilNextEvent.hours()}h ${untilNextEvent.minutes()}m`;
sunWrapper.innerHTML =
`<span class="${isVisible ? "bright" : ""}"><i class="fas fa-sun" aria-hidden="true"></i> ${untilNextEventString}</span>` +
`<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>`;
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);
}
@@ -223,16 +211,25 @@ Module.register("clock", {
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)}%`;
moonWrapper.innerHTML =
`<span class="${isVisible ? "bright" : ""}"><i class="fas fa-moon" aria-hidden="true"></i> ${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>`;
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) {
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
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);
}
@@ -258,7 +255,7 @@ Module.register("clock", {
analogWrapper.style.background = `url(${this.data.path}faces/${this.config.analogFace}.svg)`;
analogWrapper.style.backgroundSize = "100%";
// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
// 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") {
@@ -285,7 +282,7 @@ Module.register("clock", {
clockSecond.id = "clock-second";
clockSecond.style.transform = `rotate(${second}deg)`;
clockSecond.className = "clock-second";
clockSecond.style.backgroundColor = this.config.secondsColor;
clockSecond.style.backgroundColor = this.config.secondsColor; /* DEPRECATED, to be removed in a future version , use CSS instead */
clockFace.appendChild(clockSecond);
}
analogWrapper.appendChild(clockFace);
@@ -296,9 +293,14 @@ Module.register("clock", {
*/
if (this.config.displayType === "analog") {
// Display only an analog clock
if (this.config.analogShowDate === "top") {
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 === "bottom") {
} else if (this.config.analogShowDate === "top") {
wrapper.classList.add("clock-grid-top");
}
wrapper.appendChild(analogWrapper);

View File

@@ -78,7 +78,12 @@
left: 50%;
margin: -38% -1px 0 0; /* numbers must match negative length & thickness */
padding: 38% 1px 0 0; /* indicator length & thickness */
background: var(--color-text);
/* background: #888888 !important; */
/* use this instead of secondsColor */
/* have to use !important, because the code explicitly sets the color currently */
transform-origin: 50% 100%;
}
@@ -91,3 +96,15 @@
.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

@@ -1,9 +1,5 @@
/* MagicMirror²
* Module: Compliments
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
/* global Cron */
Module.register("compliments", {
// Module config defaults.
defaults: {
@@ -16,24 +12,31 @@ Module.register("compliments", {
},
updateInterval: 30000,
remoteFile: null,
remoteFileRefreshInterval: 0,
fadeSpeed: 4000,
morningStartTime: 3,
morningEndTime: 12,
afternoonStartTime: 12,
afternoonEndTime: 17,
random: true
random: true,
specialDayUnique: false
},
urlSuffix: "",
compliments_new: null,
refreshMinimumDelay: 15 * 60 * 60 * 1000, // 15 minutes
lastIndexUsed: -1,
// Set currentweather from module
currentWeatherType: "",
cron_regex: /^(((\d+,)+\d+|((\d+|[*])[/]\d+|((JAN|FEB|APR|MA[RY]|JU[LN]|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|APR|MA[RY]|JU[LN]|AUG|SEP|OCT|NOV|DEC))?))|(\d+-\d+)|\d+(-\d+)?[/]\d+(-\d+)?|\d+|[*]|(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?) ?){5}$/i,
date_regex: "[1-9.][0-9.][0-9.]{2}-([0][1-9]|[1][0-2])-([1-2][0-9]|[0][1-9]|[3][0-1])",
pre_defined_types: ["anytime", "morning", "afternoon", "evening"],
// Define required scripts.
getScripts: function () {
return ["moment.js"];
getScripts () {
return ["croner.js", "moment.js"];
},
// Define start sequence.
start: async function () {
async start () {
Log.info(`Starting module: ${this.name}`);
this.lastComplimentIndex = -1;
@@ -42,22 +45,71 @@ Module.register("compliments", {
const response = await this.loadComplimentFile();
this.config.compliments = JSON.parse(response);
this.updateDom();
if (this.config.remoteFileRefreshInterval !== 0) {
if ((this.config.remoteFileRefreshInterval >= this.refreshMinimumDelay) || window.mmTestMode === "true") {
setInterval(async () => {
const response = await this.loadComplimentFile();
if (response) {
this.compliments_new = JSON.parse(response);
}
else {
Log.error(`${this.name} remoteFile refresh failed`);
}
},
this.config.remoteFileRefreshInterval);
} else {
Log.error(`${this.name} remoteFileRefreshInterval less than minimum`);
}
}
}
let minute_sync_delay = 1;
// loop thru all the configured when events
for (let m of Object.keys(this.config.compliments)) {
// if it is a cron entry
if (this.isCronEntry(m)) {
// we need to synch our interval cycle to the minute
minute_sync_delay = (60 - (moment().second())) * 1000;
break;
}
}
// Schedule update timer. sync to the minute start (if needed), so minute based events happen on the minute start
setTimeout(() => {
setInterval(() => {
this.updateDom(this.config.fadeSpeed);
}, this.config.updateInterval);
},
minute_sync_delay);
},
// Schedule update timer.
setInterval(() => {
this.updateDom(this.config.fadeSpeed);
}, this.config.updateInterval);
// check to see if this entry could be a cron entry wich contains spaces
isCronEntry (entry) {
return entry.includes(" ");
},
/**
* @param {string} cronExpression The cron expression. See https://croner.56k.guru/usage/pattern/
* @param {Date} [timestamp] The timestamp to check. Defaults to the current time.
* @returns {number} The number of seconds until the next cron run.
*/
getSecondsUntilNextCronRun (cronExpression, timestamp = new Date()) {
// Required for seconds precision
const adjustedTimestamp = new Date(timestamp.getTime() - 1000);
// https://www.npmjs.com/package/croner
const cronJob = new Cron(cronExpression);
const nextRunTime = cronJob.nextRun(adjustedTimestamp);
const secondsDelta = (nextRunTime - adjustedTimestamp) / 1000;
return secondsDelta;
},
/**
* Generate a random index for a list of compliments.
*
* @param {string[]} compliments Array with compliments.
* @returns {number} a random index of given array
*/
randomIndex: function (compliments) {
if (compliments.length === 1) {
randomIndex (compliments) {
if (compliments.length <= 1) {
return 0;
}
@@ -78,59 +130,105 @@ Module.register("compliments", {
/**
* Retrieve an array of compliments for the time of the day.
*
* @returns {string[]} array with compliments for the time of the day.
*/
complimentArray: function () {
const hour = moment().hour();
const date = moment().format("YYYY-MM-DD");
complimentArray () {
const now = moment();
const hour = now.hour();
const date = now.format("YYYY-MM-DD");
let compliments = [];
// Add time of day compliments
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
compliments = [...this.config.compliments.morning];
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime && this.config.compliments.hasOwnProperty("afternoon")) {
compliments = [...this.config.compliments.afternoon];
} else if (this.config.compliments.hasOwnProperty("evening")) {
compliments = [...this.config.compliments.evening];
let timeOfDay;
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime) {
timeOfDay = "morning";
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime) {
timeOfDay = "afternoon";
} else {
timeOfDay = "evening";
}
if (timeOfDay && this.config.compliments.hasOwnProperty(timeOfDay)) {
compliments = [...this.config.compliments[timeOfDay]];
}
// Add compliments based on weather
if (this.currentWeatherType in this.config.compliments) {
Array.prototype.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
// if the predefine list doesn't include it (yet)
if (!this.pre_defined_types.includes(this.currentWeatherType)) {
// add it
this.pre_defined_types.push(this.currentWeatherType);
}
}
// Add compliments for anytime
Array.prototype.push.apply(compliments, this.config.compliments.anytime);
// Add compliments for special days
for (let entry in this.config.compliments) {
if (new RegExp(entry).test(date)) {
Array.prototype.push.apply(compliments, this.config.compliments[entry]);
// get the list of just date entry keys
let temp_list = Object.keys(this.config.compliments).filter((k) => {
if (this.pre_defined_types.includes(k)) return false;
else return true;
});
let date_compliments = [];
// Add compliments for special day/times
for (let entry of temp_list) {
// check if this could be a cron type entry
if (this.isCronEntry(entry)) {
// make sure the regex is valid
if (new RegExp(this.cron_regex).test(entry)) {
// check if we are in the time range for the cron entry
if (this.getSecondsUntilNextCronRun(entry, now.set("seconds", 0).toDate()) <= 1) {
// if so, use its notice entries
Array.prototype.push.apply(date_compliments, this.config.compliments[entry]);
}
} else Log.error(`compliments cron syntax invalid=${JSON.stringify(entry)}`);
} else if (new RegExp(entry).test(date)) {
Array.prototype.push.apply(date_compliments, this.config.compliments[entry]);
}
}
// if we found any date compliments
if (date_compliments.length) {
// and the special flag is true
if (this.config.specialDayUnique) {
// clear the non-date compliments if any
compliments.length = 0;
}
// put the date based compliments on the list
Array.prototype.push.apply(compliments, date_compliments);
}
return compliments;
},
/**
* Retrieve a file from the local filesystem
*
* @returns {Promise} Resolved when the file is loaded
*/
loadComplimentFile: async function () {
async loadComplimentFile () {
const isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0,
url = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile);
const response = await fetch(url);
return await response.text();
// because we may be fetching the same url,
// we need to force the server to not give us the cached result
// create an extra property (ignored by the server handler) just so the url string is different
// that will never be the same, using the ms value of date
if (isRemote && this.config.remoteFileRefreshInterval !== 0) this.urlSuffix = `?dummy=${Date.now()}`;
//
try {
const response = await fetch(url + this.urlSuffix);
return await response.text();
} catch (error) {
Log.info(`${this.name} fetch failed error=`, error);
}
},
/**
* Retrieve a random compliment.
*
* @returns {string} a compliment
*/
getRandomCompliment: function () {
getRandomCompliment () {
// get the current time of day compliments list
const compliments = this.complimentArray();
// variable for index to next message to display
@@ -149,7 +247,7 @@ Module.register("compliments", {
},
// Override dom generator.
getDom: function () {
getDom () {
const wrapper = document.createElement("div");
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line";
// get the compliment text
@@ -173,11 +271,32 @@ Module.register("compliments", {
compliment.lastElementChild.remove();
wrapper.appendChild(compliment);
}
// if a new set of compliments was loaded from the refresh task
// we do this here to make sure no other function is using the compliments list
if (this.compliments_new) {
// use them
if (JSON.stringify(this.config.compliments) !== JSON.stringify(this.compliments_new)) {
// only reset if the contents changes
this.config.compliments = this.compliments_new;
// reset the index
this.lastIndexUsed = -1;
}
// clear new file list so we don't waste cycles comparing between refreshes
this.compliments_new = null;
}
// only in test mode
if (window.mmTestMode === "true") {
// check for (undocumented) remoteFile2 to test new file load
if (this.config.remoteFile2 !== null && this.config.remoteFileRefreshInterval !== 0) {
// switch the file so that next time it will be loaded from a changed file
this.config.remoteFile = this.config.remoteFile2;
}
}
return wrapper;
},
// Override notification handler.
notificationReceived: function (notification, payload, sender) {
notificationReceived (notification, payload, sender) {
if (notification === "CURRENTWEATHER_TYPE") {
this.currentWeatherType = payload.type;
}

View File

@@ -1,8 +1,6 @@
/* MagicMirror² Default Modules List
/*
* Default Modules List
* Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const defaultModules = ["alert", "calendar", "clock", "compliments", "helloworld", "newsfeed", "updatenotification", "weather"];

View File

@@ -1,20 +1,14 @@
/* MagicMirror²
* Module: HelloWorld
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
Module.register("helloworld", {
// Default module config.
defaults: {
text: "Hello World!"
},
getTemplate: function () {
getTemplate () {
return "helloworld.njk";
},
getTemplateData: function () {
getTemplateData () {
return this.config;
}
});

View File

@@ -2,4 +2,4 @@
Use ` | safe` to allow html tages within the text string.
https://mozilla.github.io/nunjucks/templating.html#autoescaping
-->
<div>{{text | safe}}</div>
<div>{{ text | safe }}</div>

View File

@@ -1,3 +1,3 @@
<div>
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
</div>
</div>

View File

@@ -1,9 +1,3 @@
/* MagicMirror²
* Module: NewsFeed
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
Module.register("newsfeed", {
// Default module config.
defaults: {
@@ -42,34 +36,34 @@ Module.register("newsfeed", {
dangerouslyDisableAutoEscaping: false
},
getUrlPrefix: function (item) {
getUrlPrefix (item) {
if (item.useCorsProxy) {
return `${location.protocol}//${location.host}/cors?url=`;
return `${location.protocol}//${location.host}${config.basePath}cors?url=`;
} else {
return "";
}
},
// Define required scripts.
getScripts: function () {
getScripts () {
return ["moment.js"];
},
//Define required styles.
getStyles: function () {
getStyles () {
return ["newsfeed.css"];
},
// Define required translations.
getTranslations: function () {
getTranslations () {
// The translations for the default modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionary.
// Therefore we can just return false. Otherwise we should have returned a dictionary.
// If you're trying to build your own module including translations, check out the documentation.
return false;
},
// Define start sequence.
start: function () {
start () {
Log.info(`Starting module: ${this.name}`);
// Set locale.
@@ -87,7 +81,7 @@ Module.register("newsfeed", {
},
// Override socket notification handler.
socketNotificationReceived: function (notification, payload) {
socketNotificationReceived (notification, payload) {
if (notification === "NEWS_ITEMS") {
this.generateFeed(payload);
@@ -107,7 +101,7 @@ Module.register("newsfeed", {
},
//Override fetching of template name
getTemplate: function () {
getTemplate () {
if (this.config.feedUrl) {
return "oldconfig.njk";
} else if (this.config.showFullArticle) {
@@ -117,28 +111,34 @@ Module.register("newsfeed", {
},
//Override template data and return whats used for the current template
getTemplateData: function () {
getTemplateData () {
if (this.activeItem >= this.newsItems.length) {
this.activeItem = 0;
}
this.activeItemCount = this.newsItems.length;
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
if (this.config.showFullArticle) {
this.activeItemHash = this.newsItems[this.activeItem]?.hash;
return {
url: this.getActiveItemURL()
};
}
if (this.error) {
this.activeItemHash = undefined;
return {
error: this.error
};
}
if (this.newsItems.length === 0) {
this.activeItemHash = undefined;
return {
empty: true
};
}
if (this.activeItem >= this.newsItems.length) {
this.activeItem = 0;
}
const item = this.newsItems[this.activeItem];
this.activeItemHash = item.hash;
const items = this.newsItems.map(function (item) {
item.publishDate = moment(new Date(item.pubdate)).fromNow();
return item;
@@ -150,13 +150,13 @@ Module.register("newsfeed", {
sourceTitle: item.sourceTitle,
publishDate: moment(new Date(item.pubdate)).fromNow(),
title: item.title,
url: this.getUrlPrefix(item) + item.url,
url: this.getActiveItemURL(),
description: item.description,
items: items
};
},
getActiveItemURL: function () {
getActiveItemURL () {
const item = this.newsItems[this.activeItem];
if (item) {
return typeof item.url === "string" ? this.getUrlPrefix(item) + item.url : this.getUrlPrefix(item) + item.url.href;
@@ -168,7 +168,7 @@ Module.register("newsfeed", {
/**
* Registers the feeds to be used by the backend.
*/
registerFeeds: function () {
registerFeeds () {
for (let feed of this.config.feeds) {
this.sendSocketNotification("ADD_FEED", {
feed: feed,
@@ -177,19 +177,31 @@ Module.register("newsfeed", {
}
},
/**
* Gets a feed property by name
* @param {object} feed A feed object.
* @param {string} property The name of the property.
* @returns {*} The value of the specified property for the feed.
*/
getFeedProperty (feed, property) {
let res = this.config[property];
const f = this.config.feeds.find((feedItem) => feedItem.url === feed);
if (f && f[property]) res = f[property];
return res;
},
/**
* Generate an ordered list of items for this configured module.
*
* @param {object} feeds An object with feeds returned by the node helper.
*/
generateFeed: function (feeds) {
generateFeed (feeds) {
let newsItems = [];
for (let feed in feeds) {
const feedItems = feeds[feed];
if (this.subscribedToFeed(feed)) {
for (let item of feedItems) {
item.sourceTitle = this.titleForFeed(feed);
if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) {
if (!(this.getFeedProperty(feed, "ignoreOldItems") && Date.now() - new Date(item.pubdate) > this.getFeedProperty(feed, "ignoreOlderThan"))) {
newsItems.push(item);
}
}
@@ -272,11 +284,10 @@ Module.register("newsfeed", {
/**
* Check if this module is configured to show this feed.
*
* @param {string} feedUrl Url of the feed to check.
* @returns {boolean} True if it is subscribed, false otherwise
*/
subscribedToFeed: function (feedUrl) {
subscribedToFeed (feedUrl) {
for (let feed of this.config.feeds) {
if (feed.url === feedUrl) {
return true;
@@ -287,11 +298,10 @@ Module.register("newsfeed", {
/**
* Returns title for the specific feed url.
*
* @param {string} feedUrl Url of the feed
* @returns {string} The title of the feed
*/
titleForFeed: function (feedUrl) {
titleForFeed (feedUrl) {
for (let feed of this.config.feeds) {
if (feed.url === feedUrl) {
return feed.title || "";
@@ -303,7 +313,7 @@ Module.register("newsfeed", {
/**
* Schedule visual update.
*/
scheduleUpdateInterval: function () {
scheduleUpdateInterval () {
this.updateDom(this.config.animationSpeed);
// Broadcast NewsFeed if needed
@@ -315,8 +325,27 @@ Module.register("newsfeed", {
if (this.timer) clearInterval(this.timer);
this.timer = setInterval(() => {
this.activeItem++;
this.updateDom(this.config.animationSpeed);
/*
* When animations are enabled, don't update the DOM unless we are actually changing what we are displaying.
* (Animating from a headline to itself is unsightly.)
* Cases:
*
* Number of items | Number of items | Display
* at last update | right now | Behaviour
* ----------------------------------------------------
* 0 | 0 | do not update
* 0 | >0 | update
* 1 | 0 or >1 | update
* 1 | 1 | update only if item details (hash value) changed
* >1 | any | update
*
* (N.B. We set activeItemCount and activeItemHash in getTemplateData().)
*/
if (this.newsItems.length > 1 || this.newsItems.length !== this.activeItemCount || this.activeItemHash !== this.newsItems[0]?.hash) {
this.activeItem++; // this is OK if newsItems.Length==1; getTemplateData will wrap it around
this.updateDom(this.config.animationSpeed);
}
// Broadcast NewsFeed if needed
if (this.config.broadcastNewsFeeds) {
@@ -325,7 +354,7 @@ Module.register("newsfeed", {
}, this.config.updateInterval);
},
resetDescrOrFullArticleAndTimer: function () {
resetDescrOrFullArticleAndTimer () {
this.isShowingDescription = this.config.showDescription;
this.config.showFullArticle = false;
this.scrollPosition = 0;
@@ -336,7 +365,7 @@ Module.register("newsfeed", {
}
},
notificationReceived: function (notification, payload, sender) {
notificationReceived (notification, payload, sender) {
const before = this.activeItem;
if (notification === "MODULE_DOM_CREATED" && this.config.hideLoading) {
this.hide();
@@ -397,7 +426,7 @@ Module.register("newsfeed", {
}
},
showFullArticle: function () {
showFullArticle () {
this.isShowingDescription = !this.isShowingDescription;
this.config.showFullArticle = !this.isShowingDescription;
// make bottom bar align to top to allow scrolling

View File

@@ -1,27 +1,31 @@
{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %}
{% if dangerouslyDisableAutoEscaping %}
{{ text | safe}}
{% else %}
{{ text }}
{% endif %}
{% if dangerouslyDisableAutoEscaping -%}
{{ text | safe }}
{%- else -%}
{{ text }}
{%- endif %}
{% endmacro %}
{% macro escapeTitle(title, url, dangerouslyDisableAutoEscaping=false, showTitleAsUrl=false) %}
{% if dangerouslyDisableAutoEscaping %}
{% if showTitleAsUrl %}
<a href="{{ url }}" style="text-decoration:none;color:#ffffff" target="_blank">{{ title | safe }}</a>
{% if dangerouslyDisableAutoEscaping %}
{% if showTitleAsUrl %}
<a href="{{ url }}"
style="text-decoration:none;
color:#ffffff"
target="_blank">{{ title | safe }}</a>
{% else %}
{{ title | safe }}
{% endif %}
{% else %}
{{ title | safe}}
{% if showTitleAsUrl %}
<a href="{{ url }}"
style="text-decoration:none;
color:#ffffff"
target="_blank">{{ title }}</a>
{% else %}
{{ title }}
{% endif %}
{% endif %}
{% else %}
{% if showTitleAsUrl %}
<a href="{{ url }}" style="text-decoration:none;color:#ffffff" target="_blank">{{ title }}</a>
{% else %}
{{ title }}
{% endif %}
{% endif %}
{% endmacro %}
{% if loaded %}
{% if config.showAsList %}
<ul class="newsfeed-list">
@@ -30,11 +34,9 @@
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
<div class="newsfeed-source light small dimmed">
{% if item.sourceTitle and config.showSourceTitle %}
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
{% endif %}
{% if config.showPublishDate %}
{{ item.publishDate }}:
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}:{% endif %}
{% endif %}
{% if config.showPublishDate %}{{ item.publishDate }}:{% endif %}
</div>
{% endif %}
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
@@ -43,7 +45,7 @@
{% if config.showDescription %}
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
{% if config.truncDescription %}
{{ escapeText(item.description | truncate(config.lengthDescription), config.dangerouslyDisableAutoEscaping) }}
{{ escapeText(item.description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
{% else %}
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
{% endif %}
@@ -57,11 +59,9 @@
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
<div class="newsfeed-source light small dimmed">
{% if sourceTitle and config.showSourceTitle %}
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}: {% endif %}
{% endif %}
{% if config.showPublishDate %}
{{ publishDate }}:
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}:{% endif %}
{% endif %}
{% if config.showPublishDate %}{{ publishDate }}:{% endif %}
</div>
{% endif %}
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
@@ -70,7 +70,7 @@
{% if config.showDescription %}
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
{% if config.truncDescription %}
{{ escapeText(description | truncate(config.lengthDescription), config.dangerouslyDisableAutoEscaping) }}
{{ escapeText(description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
{% else %}
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
{% endif %}
@@ -79,15 +79,11 @@
</div>
{% endif %}
{% elseif empty %}
<div class="small dimmed">
{{ "NEWSFEED_NO_ITEMS" | translate | safe }}
</div>
<div class="small dimmed">{{ "NEWSFEED_NO_ITEMS" | translate | safe }}</div>
{% elseif error %}
<div class="small dimmed">
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}
</div>
{% else %}
<div class="small dimmed">
{{ "LOADING" | translate | safe }}
</div>
<div class="small dimmed">{{ "LOADING" | translate | safe }}</div>
{% endif %}

View File

@@ -1,20 +1,13 @@
/* MagicMirror²
* Node Helper: Newsfeed - NewsfeedFetcher
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const stream = require("stream");
const crypto = require("node:crypto");
const stream = require("node:stream");
const FeedMe = require("feedme");
const iconv = require("iconv-lite");
const fetch = require("fetch");
const { htmlToText } = require("html-to-text");
const Log = require("logger");
const NodeHelper = require("node_helper");
/**
* Responsible for requesting an update on the set interval and broadcasting the data.
*
* @param {string} url URL of the news feed.
* @param {number} reloadInterval Reload interval in milliseconds.
* @param {string} encoding Encoding of the feed.
@@ -25,12 +18,13 @@ const NodeHelper = require("node_helper");
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings, useCorsProxy) {
let reloadTimer = null;
let items = [];
let reloadIntervalMS = reloadInterval;
let fetchFailedCallback = function () {};
let itemsReceivedCallback = function () {};
if (reloadInterval < 1000) {
reloadInterval = 1000;
if (reloadIntervalMS < 1000) {
reloadIntervalMS = 1000;
}
/* private methods */
@@ -48,19 +42,27 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
parser.on("item", (item) => {
const title = item.title;
let description = item.description || item.summary || item.content || "";
const pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
const pubdate = item.pubdate || item.published || item.updated || item["dc:date"] || item["a10:updated"];
const url = item.url || item.link || "";
if (title && pubdate) {
const regex = /(<([^>]+)>)/gi;
description = description.toString().replace(regex, "");
// Convert HTML entities, codes and tag
description = htmlToText(description, {
wordwrap: false,
selectors: [
{ selector: "a", options: { ignoreHref: true, noAnchorUrl: true } },
{ selector: "br", format: "inlineSurround", options: { prefix: " " } },
{ selector: "img", format: "skip" }
]
});
items.push({
title: title,
description: description,
pubdate: pubdate,
url: url,
useCorsProxy: useCorsProxy
useCorsProxy: useCorsProxy,
hash: crypto.createHash("sha256").update(`${pubdate} :: ${title} :: ${url}`).digest("hex")
});
} else if (logFeedWarnings) {
Log.warn("Can't parse feed item:");
@@ -89,9 +91,9 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
try {
// 86400000 = 24 hours is mentioned in the docs as maximum value:
const ttlms = Math.min(minutes * 60 * 1000, 86400000);
if (ttlms > reloadInterval) {
reloadInterval = ttlms;
Log.info(`Newsfeed-Fetcher: reloadInterval set to ttl=${reloadInterval} for url ${url}`);
if (ttlms > reloadIntervalMS) {
reloadIntervalMS = ttlms;
Log.info(`Newsfeed-Fetcher: reloadInterval set to ttl=${reloadIntervalMS} for url ${url}`);
}
} catch (error) {
Log.warn(`Newsfeed-Fetcher: feed ttl is no valid integer=${minutes} for url ${url}`);
@@ -126,22 +128,24 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
* Schedule the timer for the next update.
*/
const scheduleTimer = function () {
clearTimeout(reloadTimer);
reloadTimer = setTimeout(function () {
fetchNews();
}, reloadInterval);
if (process.env.JEST_WORKER_ID === undefined) {
// only set timer when not running in jest
clearTimeout(reloadTimer);
reloadTimer = setTimeout(function () {
fetchNews();
}, reloadIntervalMS);
}
};
/* public methods */
/**
* Update the reload interval, but only if we need to increase the speed.
*
* @param {number} interval Interval for the update in milliseconds.
*/
this.setReloadInterval = function (interval) {
if (interval > 1000 && interval < reloadInterval) {
reloadInterval = interval;
if (interval > 1000 && interval < reloadIntervalMS) {
reloadIntervalMS = interval;
}
};

View File

@@ -1,23 +1,16 @@
/* MagicMirror²
* Node Helper: Newsfeed
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const NodeHelper = require("node_helper");
const Log = require("logger");
const NewsfeedFetcher = require("./newsfeedfetcher");
module.exports = NodeHelper.create({
// Override start method.
start: function () {
start () {
Log.log(`Starting node helper for: ${this.name}`);
this.fetchers = [];
},
// Override socketNotificationReceived received.
socketNotificationReceived: function (notification, payload) {
socketNotificationReceived (notification, payload) {
if (notification === "ADD_FEED") {
this.createFetcher(payload.feed, payload.config);
}
@@ -26,11 +19,10 @@ module.exports = NodeHelper.create({
/**
* Creates a fetcher for a new feed if it doesn't exist yet.
* Otherwise it reuses the existing one.
*
* @param {object} feed The feed object
* @param {object} config The configuration object
*/
createFetcher: function (feed, config) {
createFetcher (feed, config) {
const url = feed.url || "";
const encoding = feed.encoding || "UTF-8";
const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
@@ -77,7 +69,7 @@ module.exports = NodeHelper.create({
* Creates an object with all feed items of the different registered feeds,
* and broadcasts these using sendSocketNotification.
*/
broadcastFeeds: function () {
broadcastFeeds () {
const feeds = {};
for (let f in this.fetchers) {
feeds[f] = this.fetchers[f].items();

View File

@@ -1,3 +1,3 @@
<div class="small bright">
{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}
</div>
</div>

View File

@@ -1,27 +1,28 @@
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const fs = require("fs");
const path = require("path");
const util = require("node:util");
const exec = util.promisify(require("node:child_process").exec);
const fs = require("node:fs");
const path = require("node:path");
const Log = require("logger");
const BASE_DIR = path.normalize(`${__dirname}/../../../`);
class GitHelper {
constructor() {
constructor () {
this.gitRepos = [];
this.gitResultList = [];
}
getRefRegex(branch) {
getRefRegex (branch) {
return new RegExp(`s*([a-z,0-9]+[.][.][a-z,0-9]+) ${branch}`, "g");
}
async execShell(command) {
async execShell (command) {
const { stdout = "", stderr = "" } = await exec(command);
return { stdout, stderr };
}
async isGitRepo(moduleFolder) {
async isGitRepo (moduleFolder) {
const { stderr } = await this.execShell(`cd ${moduleFolder} && git remote -v`);
if (stderr) {
@@ -33,7 +34,7 @@ class GitHelper {
return true;
}
async add(moduleName) {
async add (moduleName) {
let moduleFolder = BASE_DIR;
if (moduleName !== "MagicMirror") {
@@ -58,7 +59,7 @@ class GitHelper {
}
}
async getStatusInfo(repo) {
async getStatusInfo (repo) {
let gitInfo = {
module: repo.module,
behind: 0, // commits behind
@@ -93,27 +94,30 @@ class GitHelper {
// ## develop...origin/develop
// ## master...origin/master [behind 8]
// ## master...origin/master [ahead 8, behind 1]
// ## HEAD (no branch)
status = status.match(/## (.*)\.\.\.([^ ]*)(?: .*behind (\d+))?/);
// examples for status:
// [ '## develop...origin/develop', 'develop', 'origin/develop' ]
// [ '## master...origin/master [behind 8]', 'master', 'origin/master', '8' ]
// [ '## master...origin/master [ahead 8, behind 1]', 'master', 'origin/master', '1' ]
gitInfo.current = status[1];
gitInfo.tracking = status[2];
if (status) {
gitInfo.current = status[1];
gitInfo.tracking = status[2];
if (status[3]) {
// git fetch was already called before so `git status -sb` delivers already the behind number
gitInfo.behind = parseInt(status[3]);
gitInfo.isBehindInStatus = true;
if (status[3]) {
// git fetch was already called before so `git status -sb` delivers already the behind number
gitInfo.behind = parseInt(status[3]);
gitInfo.isBehindInStatus = true;
}
}
return gitInfo;
}
async getRepoInfo(repo) {
async getRepoInfo (repo) {
const gitInfo = await this.getStatusInfo(repo);
if (!gitInfo) {
if (!gitInfo || !gitInfo.current) {
return;
}
@@ -124,7 +128,7 @@ class GitHelper {
const { stderr } = await this.execShell(`cd ${repo.folder} && git fetch -n --dry-run`);
// example output:
// From https://github.com/MichMich/MagicMirror
// From https://github.com/MagicMirrorOrg/MagicMirror
// e40ddd4..06389e3 develop -> origin/develop
// here the result is in stderr (this is a git default, don't ask why ...)
const matches = stderr.match(this.getRefRegex(gitInfo.current));
@@ -170,22 +174,39 @@ class GitHelper {
}
}
async getRepos() {
const gitResultList = [];
async getRepos () {
this.gitResultList = [];
for (const repo of this.gitRepos) {
try {
const gitInfo = await this.getRepoInfo(repo);
if (gitInfo) {
gitResultList.push(gitInfo);
this.gitResultList.push(gitInfo);
}
} catch (e) {
Log.error(`Failed to retrieve repo info for ${repo.module}: ${e}`);
}
}
return gitResultList;
return this.gitResultList;
}
async checkUpdates () {
var updates = [];
const allRepos = await this.gitResultList.map((module) => {
return new Promise((resolve) => {
if (module.behind > 0 && module.module !== "MagicMirror") {
Log.info(`Update found for module: ${module.module}`);
updates.push(module);
}
resolve(module);
});
});
await Promise.all(allRepos);
return updates;
}
}

View File

@@ -1,6 +1,9 @@
const fs = require("node:fs");
const path = require("node:path");
const NodeHelper = require("node_helper");
const defaultModules = require("../defaultmodules");
const GitHelper = require("./git_helper");
const UpdateHelper = require("./update_helper");
const ONE_MINUTE = 60 * 1000;
@@ -11,9 +14,25 @@ module.exports = NodeHelper.create({
updateProcessStarted: false,
gitHelper: new GitHelper(),
updateHelper: null,
async configureModules(modules) {
for (const moduleName of modules) {
getModules (modules) {
if (this.config.useModulesFromConfig) {
return modules;
} else {
// get modules from modules-directory
const moduleDir = path.normalize(`${__dirname}/../../`);
const getDirectories = (source) => {
return fs.readdirSync(source, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory() && dirent.name !== "default")
.map((dirent) => dirent.name);
};
return getDirectories(moduleDir);
}
},
async configureModules (modules) {
for (const moduleName of this.getModules(modules)) {
if (!this.ignoreUpdateChecking(moduleName)) {
await this.gitHelper.add(moduleName);
}
@@ -24,38 +43,68 @@ module.exports = NodeHelper.create({
}
},
async socketNotificationReceived(notification, payload) {
if (notification === "CONFIG") {
this.config = payload;
} else if (notification === "MODULES") {
// if this is the 1st time thru the update check process
if (!this.updateProcessStarted) {
this.updateProcessStarted = true;
await this.configureModules(payload);
await this.performFetch();
}
async socketNotificationReceived (notification, payload) {
switch (notification) {
case "CONFIG":
this.config = payload;
this.updateHelper = new UpdateHelper(this.config);
await this.updateHelper.check_PM2_Process();
break;
case "MODULES":
// if this is the 1st time thru the update check process
if (!this.updateProcessStarted) {
this.updateProcessStarted = true;
await this.configureModules(payload);
await this.performFetch();
}
break;
case "SCAN_UPDATES":
// 1st time of check allows to force new scan
if (this.updateProcessStarted) {
clearTimeout(this.updateTimer);
await this.performFetch();
}
break;
}
},
async performFetch() {
async performFetch () {
const repos = await this.gitHelper.getRepos();
for (const repo of repos) {
this.sendSocketNotification("STATUS", repo);
this.sendSocketNotification("REPO_STATUS", repo);
}
const updates = await this.gitHelper.checkUpdates();
if (this.config.sendUpdatesNotifications && updates.length) {
this.sendSocketNotification("UPDATES", updates);
}
if (updates.length) {
const updateResult = await this.updateHelper.parse(updates);
for (const update of updateResult) {
if (update.inProgress) {
this.sendSocketNotification("UPDATE_STATUS", update);
}
}
}
this.scheduleNextFetch(this.config.updateInterval);
},
scheduleNextFetch(delay) {
scheduleNextFetch (delay) {
clearTimeout(this.updateTimer);
this.updateTimer = setTimeout(() => {
this.performFetch();
}, Math.max(delay, ONE_MINUTE));
this.updateTimer = setTimeout(
() => {
this.performFetch();
},
Math.max(delay, ONE_MINUTE)
);
},
ignoreUpdateChecking(moduleName) {
ignoreUpdateChecking (moduleName) {
// Should not check for updates for default modules
if (defaultModules.includes(moduleName)) {
return true;

View File

@@ -0,0 +1,232 @@
const Exec = require("node:child_process").exec;
const Spawn = require("node:child_process").spawn;
const fs = require("node:fs");
const Log = require("logger");
/*
* class Updater
* Allow to self updating 3rd party modules from command defined in config
*
* [constructor] read value in config:
* updates: [ // array of modules update commands
* {
* <module name>: <update command>
* },
* {
* ...
* }
* ],
* updateTimeout: 2 * 60 * 1000, // max update duration
* updateAutorestart: false // autoRestart MM when update done ?
*
* [main command]: parse(<Array of modules>):
* parse if module update is needed
* --> Apply ONLY one update (first of the module list)
* --> auto-restart MagicMirror or wait manual restart by user
* return array with modules update state information for `updatenotification` module displayer information
* [
* {
* name = <module-name>, // name of the module
* updateCommand = <update command>, // update command (if found)
* inProgress = <boolean>, // an update if in progress for this module
* error = <boolean>, // an error if detected when updating
* updated = <boolean>, // updated successfully
* needRestart = <boolean> // manual restart of MagicMirror is required by user
* },
* {
* ...
* }
* ]
*/
class Updater {
constructor (config) {
this.updates = config.updates;
this.timeout = config.updateTimeout;
this.autoRestart = config.updateAutorestart;
this.moduleList = {};
this.updating = false;
this.usePM2 = false; // don't use pm2 by default
this.PM2Id = null; // pm2 process number
this.version = global.version;
this.root_path = global.root_path;
Log.info("updatenotification: Updater Class Loaded!");
}
// [main command] parse if module update is needed
async parse (modules) {
var parser = modules.map(async (module) => {
if (this.moduleList[module.module] === undefined) {
this.moduleList[module.module] = {};
this.moduleList[module.module].name = module.module;
this.moduleList[module.module].updateCommand = await this.applyCommand(module.module);
this.moduleList[module.module].inProgress = false;
this.moduleList[module.module].error = null;
this.moduleList[module.module].updated = false;
this.moduleList[module.module].needRestart = false;
}
if (!this.moduleList[module.module].inProgress) {
if (!this.updating) {
if (!this.moduleList[module.module].updateCommand) {
this.updating = false;
} else {
this.updating = true;
this.moduleList[module.module].inProgress = true;
Object.assign(this.moduleList[module.module], await this.updateProcess(this.moduleList[module.module]));
}
}
}
});
await Promise.all(parser);
let updater = Object.values(this.moduleList);
Log.debug("updatenotification Update Result:", updater);
return updater;
}
/*
* module updater with his proper command
* return object as result
* {
* error: <boolean>, // if error detected
* updated: <boolean>, // if updated successfully
* needRestart: <boolean> // if magicmirror restart required
* };
*/
updateProcess (module) {
let Result = {
error: false,
updated: false,
needRestart: false
};
let Command = null;
const Path = `${this.root_path}/modules/`;
const modulePath = Path + module.name;
if (module.updateCommand) {
Command = module.updateCommand;
} else {
Log.warn(`updatenotification: Update of ${module.name} is not supported.`);
return Result;
}
Log.info(`updatenotification: Updating ${module.name}...`);
return new Promise((resolve) => {
Exec(Command, { cwd: modulePath, timeout: this.timeout }, (error, stdout, stderr) => {
if (error) {
Log.error(`updatenotification: exec error: ${error}`);
Result.error = true;
} else {
Log.info(`updatenotification: Update logs of ${module.name}: ${stdout}`);
Result.updated = true;
if (this.autoRestart) {
Log.info("updatenotification: Update done");
setTimeout(() => this.restart(), 3000);
} else {
Log.info("updatenotification: Update done, don't forget to restart MagicMirror!");
Result.needRestart = true;
}
}
resolve(Result);
});
});
}
// restart rules (pm2 or node --run start)
restart () {
if (this.usePM2) this.pm2Restart();
else this.nodeRestart();
}
// restart MagicMiror with "pm2": use PM2Id for restart it
pm2Restart () {
Log.info("updatenotification: PM2 will restarting MagicMirror...");
const pm2 = require("pm2");
pm2.restart(this.PM2Id, (err, proc) => {
if (err) {
Log.error("updatenotification:[PM2] restart Error", err);
}
});
}
// restart MagicMiror with "node --run start"
nodeRestart () {
Log.info("updatenotification: Restarting MagicMirror...");
const out = process.stdout;
const err = process.stderr;
const subprocess = Spawn("node --run start", { cwd: this.root_path, shell: true, detached: true, stdio: ["ignore", out, err] });
subprocess.unref(); // detach the newly launched process from the master process
process.exit();
}
// Check using pm2
check_PM2_Process () {
Log.info("updatenotification: Checking PM2 using...");
return new Promise((resolve) => {
if (fs.existsSync("/.dockerenv")) {
Log.info("updatenotification: Running in docker container, not using PM2 ...");
resolve(false);
return;
}
if (process.env.unique_id === undefined) {
Log.info("updatenotification: [PM2] You are not using pm2");
resolve(false);
return;
}
Log.debug(`updatenotification: [PM2] Search for pm2 id: ${process.env.pm_id} -- name: ${process.env.name} -- unique_id: ${process.env.unique_id}`);
const pm2 = require("pm2");
pm2.connect((err) => {
if (err) {
Log.error("updatenotification: [PM2]", err);
resolve(false);
return;
}
pm2.list((err, list) => {
if (err) {
Log.error("updatenotification: [PM2] Can't get process List!");
resolve(false);
return;
}
list.forEach((pm) => {
Log.debug(`updatenotification: [PM2] found pm2 process id: ${pm.pm_id} -- name: ${pm.name} -- unique_id: ${pm.pm2_env.unique_id}`);
if (pm.pm2_env.status === "online" && process.env.name === pm.name && +process.env.pm_id === +pm.pm_id && process.env.unique_id === pm.pm2_env.unique_id) {
this.PM2Id = pm.pm_id;
this.usePM2 = true;
Log.info(`updatenotification: [PM2] You are using pm2 with id: ${this.PM2Id} (${pm.name})`);
resolve(true);
} else {
Log.debug(`updatenotification: [PM2] pm2 process id: ${pm.pm_id} don't match...`);
}
});
pm2.disconnect();
if (!this.usePM2) {
Log.info("updatenotification: [PM2] You are not using pm2");
resolve(false);
}
});
});
});
}
// check if module is MagicMirror
isMagicMirror (module) {
if (module === "MagicMirror") return true;
return false;
}
// search update module command
applyCommand (module) {
if (this.isMagicMirror(module.module) || !this.updates.length) return null;
let command = null;
this.updates.forEach((updater) => {
if (updater[module]) command = updater[module];
});
return command;
}
}
module.exports = Updater;

View File

@@ -1,20 +1,21 @@
/* MagicMirror²
* Module: UpdateNotification
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
Module.register("updatenotification", {
defaults: {
updateInterval: 10 * 60 * 1000, // every 10 minutes
refreshInterval: 24 * 60 * 60 * 1000, // one day
ignoreModules: []
ignoreModules: [],
sendUpdatesNotifications: false,
updates: [],
updateTimeout: 2 * 60 * 1000, // max update duration
updateAutorestart: false, // autoRestart MM when update done ?
useModulesFromConfig: true // if `false` iterate over modules directory
},
suspended: false,
moduleList: {},
needRestart: false,
updates: [],
start() {
start () {
Log.info(`Starting module: ${this.name}`);
this.addFilters();
setInterval(() => {
@@ -23,41 +24,54 @@ Module.register("updatenotification", {
}, this.config.refreshInterval);
},
suspend() {
suspend () {
this.suspended = true;
},
resume() {
resume () {
this.suspended = false;
this.updateDom(2);
},
notificationReceived(notification) {
if (notification === "DOM_OBJECTS_CREATED") {
this.sendSocketNotification("CONFIG", this.config);
this.sendSocketNotification("MODULES", Object.keys(Module.definitions));
notificationReceived (notification) {
switch (notification) {
case "DOM_OBJECTS_CREATED":
this.sendSocketNotification("CONFIG", this.config);
this.sendSocketNotification("MODULES", Object.keys(Module.definitions));
break;
case "SCAN_UPDATES":
this.sendSocketNotification("SCAN_UPDATES");
break;
}
},
socketNotificationReceived(notification, payload) {
if (notification === "STATUS") {
this.updateUI(payload);
socketNotificationReceived (notification, payload) {
switch (notification) {
case "REPO_STATUS":
this.updateUI(payload);
break;
case "UPDATES":
this.sendNotification("UPDATES", payload);
break;
case "UPDATE_STATUS":
this.updatesNotifier(payload);
break;
}
},
getStyles() {
getStyles () {
return [`${this.name}.css`];
},
getTemplate() {
getTemplate () {
return `${this.name}.njk`;
},
getTemplateData() {
return { moduleList: this.moduleList, suspended: this.suspended };
getTemplateData () {
return { moduleList: this.moduleList, updatesList: this.updates, suspended: this.suspended, needRestart: this.needRestart };
},
updateUI(payload) {
updateUI (payload) {
if (payload && payload.behind > 0) {
// if we haven't seen info for this module
if (this.moduleList[payload.module] === undefined) {
@@ -75,7 +89,7 @@ Module.register("updatenotification", {
}
},
addFilters() {
addFilters () {
this.nunjucksEnvironment().addFilter("diffLink", (text, status) => {
if (status.module !== "MagicMirror") {
return text;
@@ -83,7 +97,31 @@ Module.register("updatenotification", {
const localRef = status.hash;
const remoteRef = status.tracking.replace(/.*\//, "");
return `<a href="https://github.com/MichMich/MagicMirror/compare/${localRef}...${remoteRef}" class="xsmall dimmed difflink" target="_blank">${text}</a>`;
return `<a href="https://github.com/MagicMirrorOrg/MagicMirror/compare/${localRef}...${remoteRef}" class="xsmall dimmed difflink" target="_blank">${text}</a>`;
});
},
updatesNotifier (payload, done = true) {
if (this.updates[payload.name] === undefined) {
this.updates[payload.name] = {
name: payload.name,
done: done
};
if (payload.error) {
this.sendSocketNotification("UPDATE_ERROR", payload.name);
this.updates[payload.name].done = false;
} else {
if (payload.updated) {
delete this.moduleList[payload.name];
this.updates[payload.name].done = true;
}
if (payload.needRestart) {
this.needRestart = true;
}
}
this.updateDom(2);
}
}
});

View File

@@ -1,15 +1,41 @@
{% if not suspended %}
{% for name, status in moduleList %}
<div class="small bright">
<i class="fas fa-exclamation-circle"></i>
<span>
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "MagicMirror" else "UPDATE_NOTIFICATION_MODULE" %}
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
</span>
</div>
<div class="xsmall dimmed">
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
</div>
{% endfor %}
{% if needRestart %}
<div class="small bright">
<i class="fas fa-rotate"></i>
<span>
{% set restartTextLabel = "UPDATE_NOTIFICATION_NEED-RESTART" %}
{{ restartTextLabel | translate() | safe }}
</span>
</div>
{% endif %}
{% for name, status in moduleList %}
<div class="small bright">
<i class="fas fa-exclamation-circle"></i>
<span>
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "MagicMirror" else "UPDATE_NOTIFICATION_MODULE" %}
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
</span>
</div>
<div class="xsmall dimmed">
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
</div>
{% endfor %}
{% for name, status in updatesList %}
<div class="small bright">
{% if status.done %}
<i class="fas fa-check" style="color: lightgreen;"></i>
<span>
{% set updateTextLabel = "UPDATE_NOTIFICATION_DONE" %}
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
</span>
{% else %}
<i class="fas fa-xmark" style="color: red;"></i>
<span>
{% set updateTextLabel = "UPDATE_NOTIFICATION_ERROR" %}
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
</span>
{% endif %}
</div>
{% endfor %}
{% endif %}

View File

@@ -1,21 +1,23 @@
/**
* A function to make HTTP requests via the server to avoid CORS-errors.
*
* @param {string} url the url to fetch from
* @param {string} type what contenttype to expect in the response, can be "json" or "xml"
* @param {boolean} useCorsProxy A flag to indicate
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @param {string} basePath The base path, default is "/"
* @returns {Promise} resolved when the fetch is done. The response headers is placed in a headers-property (provided the response does not already contain a headers-property).
*/
async function performWebRequest(url, type = "json", useCorsProxy = false, requestHeaders = undefined, expectedResponseHeaders = undefined) {
async function performWebRequest (url, type = "json", useCorsProxy = false, requestHeaders = undefined, expectedResponseHeaders = undefined, basePath = "/") {
const request = {};
let requestUrl;
if (useCorsProxy) {
url = getCorsUrl(url, requestHeaders, expectedResponseHeaders);
requestUrl = getCorsUrl(url, requestHeaders, expectedResponseHeaders, basePath);
} else {
requestUrl = url;
request.headers = getHeadersToSend(requestHeaders);
}
const response = await fetch(url, request);
const response = await fetch(requestUrl, request);
const data = await response.text();
if (type === "xml") {
@@ -33,17 +35,17 @@ async function performWebRequest(url, type = "json", useCorsProxy = false, reque
/**
* Gets a URL that will be used when calling the CORS-method on the server.
*
* @param {string} url the url to fetch from
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @param {string} basePath The base path, default is "/"
* @returns {string} to be used as URL when calling CORS-method on server.
*/
const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders) {
const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders, basePath = "/") {
if (!url || url.length < 1) {
throw new Error(`Invalid URL: ${url}`);
} else {
let corsUrl = `${location.protocol}//${location.host}/cors?`;
let corsUrl = `${location.protocol}//${location.host}${basePath}cors?`;
const requestHeaderString = getRequestHeaderString(requestHeaders);
if (requestHeaderString) corsUrl = `${corsUrl}sendheaders=${requestHeaderString}`;
@@ -64,7 +66,6 @@ const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders) {
/**
* Gets the part of the CORS URL that represents the HTTP headers to send.
*
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @returns {string} to be used as request-headers component in CORS URL.
*/
@@ -85,7 +86,6 @@ const getRequestHeaderString = function (requestHeaders) {
/**
* Gets headers and values to attach to the web request.
*
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @returns {object} An object specifying name and value of the headers.
*/
@@ -102,7 +102,6 @@ const getHeadersToSend = (requestHeaders) => {
/**
* Gets the part of the CORS URL that represents the expected HTTP headers to receive.
*
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
*/
@@ -123,7 +122,6 @@ const getExpectedResponseHeadersString = function (expectedResponseHeaders) {
/**
* Gets the values for the expected headers from the response.
*
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @param {Response} response the HTTP response
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
@@ -141,7 +139,35 @@ const getHeadersFromResponse = (expectedResponseHeaders, response) => {
return responseHeaders;
};
if (typeof module !== "undefined")
module.exports = {
performWebRequest
};
/**
* Format the time according to the config
* @param {object} config The config of the module
* @param {object} time time to format
* @returns {string} The formatted time string
*/
const formatTime = (config, time) => {
let date = moment(time);
if (config.timezone) {
date = date.tz(config.timezone);
}
if (config.timeFormat !== 24) {
if (config.showPeriod) {
if (config.showPeriodUpper) {
return date.format("h:mm A");
} else {
return date.format("h:mm a");
}
} else {
return date.format("h:mm");
}
}
return date.format("HH:mm");
};
if (typeof module !== "undefined") module.exports = {
performWebRequest,
formatTime
};

View File

@@ -1,3 +1,8 @@
{% macro humidity() %}
{% if current.humidity %}
<span class="humidity"><span>{{ current.humidity | decimalSymbol }}</span><sup>&nbsp;<i class="wi wi-humidity humidity-icon"></i></sup></span>
{% endif %}
{% endmacro %}
{% if current %}
{% if not config.onlyTemp %}
<div class="normal medium">
@@ -7,7 +12,7 @@
{% if config.showWindDirection %}
<sup>
{% if config.showWindDirectionAsArrow %}
<i class="fas fa-long-arrow-alt-down" style="transform:rotate({{ current.windFromDirection }}deg);"></i>
<i class="fas fa-long-arrow-alt-down" style="transform:rotate({{ current.windFromDirection }}deg)"></i>
{% else %}
{{ current.cardinalWindDirection() | translate }}
{% endif %}
@@ -15,8 +20,8 @@
</sup>
{% endif %}
</span>
{% if config.showHumidity and current.humidity %}
<span>{{ current.humidity | decimalSymbol }}</span><sup>&nbsp;<i class="wi wi-humidity humidity-icon"></i></sup>
{% if config.showHumidity === "wind" %}
{{ humidity() }}
{% endif %}
{% if config.showSun %}
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
@@ -28,56 +33,69 @@
{% endif %}
</span>
{% endif %}
{% if config.showUVIndex %}
<td class="align-right bright uv-index">
<div class="wi dimmed wi-hot"></div>
{{ current.uv_index }}
</td>
{% endif %}
</div>
{% endif %}
<div class="large light">
<span class="wi weathericon wi-{{current.weatherType}}"></span>
<span class="bright">
{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}
</span>
</div>
<div class="normal light indoor">
{% if config.showIndoorTemperature and indoor.temperature %}
<div>
<span class="fas fa-home"></span>
<span class="bright">
{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}
</span>
</div>
<div class="large">
{% if config.showIndoorTemperature and indoor.temperature or config.showIndoorHumidity and indoor.humidity %}
<span class="medium fas fa-home"></span>
<span style="display: inline-block">
{% if config.showIndoorTemperature and indoor.temperature %}
<sup class="small" style="position: relative; display: block; text-align: left;">
<span>
{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}
</span>
</sup>
{% endif %}
{% if config.showIndoorHumidity and indoor.humidity %}
<sub class="small" style="position: relative; display: block; text-align: left;">
<span>
{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}
</span>
</sub>
{% endif %}
</span>
{% endif %}
{% if config.showIndoorHumidity and indoor.humidity %}
<div>
<span class="fas fa-tint"></span>
<span class="bright">
{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}
</span>
</div>
<span class="light wi weathericon wi-{{ current.weatherType }}"></span>
<span class="light bright">{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}</span>
{% if config.showHumidity === "temp" %}
<span class="medium bright">{{ humidity() }}</span>
{% endif %}
</div>
{% if (config.showFeelsLike or config.showPrecipitationAmount or config.showPrecipitationProbability) and not config.onlyTemp %}
<div class="normal medium feelslike">
{% if config.showFeelsLike %}
<span class="dimmed">
{% if config.showHumidity === "feelslike" %}
{{ humidity() }}
{% endif %}
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
</span><br/>
</span>
<br />
{% endif %}
{% if config.showPrecipitationAmount and current.precipitationAmount %}
{% if config.showPrecipitationAmount and current.precipitationAmount %}
<span class="dimmed">
<span class="precipitationLeadText">{{ "PRECIP_AMOUNT" | translate }}</span> {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }}
</span><br/>
</span>
<br />
{% endif %}
{% if config.showPrecipitationProbability and current.precipitationProbability %}
{% if config.showPrecipitationProbability and current.precipitationProbability %}
<span class="dimmed">
<span class="precipitationLeadText">{{ "PRECIP_POP" | translate }}</span> {{ current.precipitationProbability }}%
</span>
{% endif %}
</div>
{% endif %}
{% if config.showHumidity === "below" %}
<span class="medium dimmed">{{ humidity() }}</span>
{% endif %}
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate }}
</div>
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `current` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{current | dump}}</div> -->

View File

@@ -7,15 +7,18 @@
{% endif %}
{% set forecast = forecast.slice(0, numSteps) %}
{% for f in forecast %}
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
<tr {% if config.colored %}class="colored"{% endif %}
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
{% if (currentStep == 0) and config.ignoreToday == false and config.absoluteDates == false %}
<td class="day">{{ "TODAY" | translate }}</td>
{% elif (currentStep == 1) and config.ignoreToday == false and config.absoluteDates == false %}
<td class="day">{{ "TOMORROW" | translate }}</td>
{% else %}
<td class="day">{{ f.date.format('ddd') }}</td>
<td class="day">{{ f.date.format("ddd") }}</td>
{% endif %}
<td class="bright weather-icon"><span class="wi weathericon wi-{{ f.weatherType }}"></span></td>
<td class="bright weather-icon">
<span class="wi weathericon wi-{{ f.weatherType }}"></span>
</td>
<td class="align-right bright max-temp">
{{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}
</td>
@@ -29,7 +32,13 @@
{% endif %}
{% if config.showPrecipitationProbability %}
<td class="align-right bright precipitation-prob">
{{ f.precipitationProbability | unit("precip", "%") }}
{{ f.precipitationProbability | unit('precip', '%') }}
</td>
{% endif %}
{% if config.showUVIndex %}
<td class="align-right dimmed uv-index">
{{ f.uv_index }}
<span class="wi dimmed weathericon wi-hot"></span>
</td>
{% endif %}
</tr>
@@ -37,10 +46,7 @@
{% endfor %}
</table>
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate }}
</div>
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `forecast` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{forecast | dump}}</div> -->

View File

@@ -4,31 +4,49 @@
<table class="{{ config.tableClass }}">
{% set hours = hourly.slice(0, numSteps) %}
{% for hour in hours %}
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
<tr {% if config.colored %}class="colored"{% endif %}
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
<td class="day">{{ hour.date | formatTime }}</td>
<td class="bright weather-icon"><span class="wi weathericon wi-{{ hour.weatherType }}"></span></td>
<td class="bright weather-icon">
<span class="wi weathericon wi-{{ hour.weatherType }}"></span>
</td>
<td class="align-right bright">
{{ hour.temperature | roundValue | unit("temperature") }}
</td>
{% if config.showPrecipitationAmount %}
<td class="align-right bright precipitation-amount">
{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}
{% if config.showUVIndex %}
<td class="align-right bright uv-index">
{% if hour.uv_index!=0 %}
{{ hour.uv_index }}
<span class="wi weathericon wi-hot"></span>
{% endif %}
</td>
{% endif %}
{% if config.showHumidity != "none" %}
<td class="align-left bright humidity-hourly">
{{ hour.humidity }}
<span class="wi wi-humidity humidity-icon"></span>
</td>
{% endif %}
{% if config.showPrecipitationAmount %}
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
<td class="align-right bright precipitation-amount">
{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}
</td>
{% endif %}
{% endif %}
{% if config.showPrecipitationProbability %}
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
<td class="align-right bright precipitation-prob">
{{ hour.precipitationProbability | unit("precip", "%") }}
{{ hour.precipitationProbability | unit('precip', '%') }}
</td>
{% endif %}
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
{% endfor %}
</table>
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate }}
</div>
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{hourly | dump}}</div> -->

Some files were not shown because too many files have changed in this diff Show More