Compare commits

..

5 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
251 changed files with 10296 additions and 4904 deletions

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.
The ESLint configuration is in our `eslint.config.mjs` file.
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 18 or later (recommended is 20).
**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`.

View File

@@ -1,52 +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'm having troubles installing or configuring foreign modules
Please open an issue in the module repository or ask for help in the [forum](https://forum.magicmirror.builders/)
---
## 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 18 or later (recommended is 20).
**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.

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

@@ -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,4 +1,4 @@
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:**
@@ -10,7 +10,7 @@ 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.

View File

@@ -18,23 +18,3 @@ updates:
- "Skip Changelog"
- "dependencies"
- "javascript"
- package-ecosystem: "npm"
directory: "/vendor"
schedule:
interval: "monthly"
target-branch: "develop"
labels:
- "Skip Changelog"
- "dependencies"
- "javascript"
- package-ecosystem: "npm"
directory: "/fonts"
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,13 +13,37 @@ 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: [20.9.0, 20.x, 22.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@v4
- name: "Use Node.js ${{ matrix.node-version }}"
@@ -28,15 +52,18 @@ jobs:
node-version: ${{ matrix.node-version }}
check-latest: true
cache: "npm"
- name: "Install dependencies"
- name: "Install MagicMirror²"
run: |
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
- name: "Run tests"
run: |
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
touch css/custom.css
npm run test:prettier
npm run test:js
npm run test:css
npm run test
export WAYLAND_DISPLAY=wayland-0
node --run test

View File

@@ -16,5 +16,3 @@ jobs:
uses: actions/checkout@v4
- name: "Dependency Review"
uses: actions/dependency-review-action@v4
with:
allow-ghsas: GHSA-8hc4-vh64-cxmj

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.9.0, 20.x, 22.x]
node-version: [22.14.0, 22.x, 24.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -18,13 +18,15 @@ jobs:
node-version: ${{ matrix.node-version }}
check-latest: true
- name: Install MagicMirror
run: npm run install-mm
run: node --run install-mm
- name: Install @electron/rebuild
run: npm install @electron/rebuild
- name: Install node-libgpiod deps
run: sudo apt-get install gpiod libgpiod2 libgpiod-dev
- name: Install some test library to be rebuilded
run: npm install node-libgpiod node-pty drivelist
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

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)"

8
.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
@@ -67,6 +65,8 @@ Temporary Items
/css/*
!/css/custom.css.sample
!/css/main.css
!/css/roboto.css
!/css/font-awesome.css
# Ignore users config file but keep the sample.
/config/*
@@ -84,3 +84,7 @@ Temporary Items
# Ignore positions file (#3518)
js/positions.js
# Ignore lock files other than package-lock.json
pnpm-lock.yaml
yarn.lock

View File

@@ -1,5 +1,5 @@
#!/bin/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
}

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
}
}

View File

@@ -1,10 +1,174 @@
# MagicMirror² Change Log
# Changelog
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/#donate) With your help we can continue to improve the MagicMirror².
## [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)
## [2.31.0] - 2025-04-01
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 (#3740)
- 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
## [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 (#3665)
- [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
## [2.29.0] - 2024-10-01
Thanks to: @bugsounet, @dkallen78, @jargordon, @khassel, @KristjanESPERANTO, @MarcLandis, @rejas, @ryan-d-williams, @sdetweil, @skpanagiotis.
@@ -13,7 +177,7 @@ Thanks to: @bugsounet, @dkallen78, @jargordon, @khassel, @KristjanESPERANTO, @Ma
### 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)
- [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)
@@ -39,13 +203,13 @@ Thanks to: @bugsounet, @dkallen78, @jargordon, @khassel, @KristjanESPERANTO, @Ma
- [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
- [core] Fix discovering module positions twice after #3450
### Fixed
- Fixed `checks` badge in README.md
- [docs] 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)
- [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)
@@ -103,7 +267,7 @@ For more info, please read the following post: [A New Chapter for MagicMirror: T
### Updated
- Update updatenotification (update_helper.js): Recode with pm2 library (#3332)
- [updatenotification] Recode update_helper.js 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)
@@ -117,7 +281,7 @@ For more info, please read the following post: [A New Chapter for MagicMirror: T
### Fixed
- Correct apiBase of weathergov weatherProvider to match documentation (#2926)
- [weather] 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)
@@ -269,7 +433,7 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
### Updated
- Added support for precipitation probability with openmeteo weather-provider
- [weather] Added support for precipitation probability with openmeteo weather-provider
- Update electron to v25.2 and other dependencies
- Use node v20 in github workflow (replacing v14)
- Refactor formatTime into common util function for default modules
@@ -442,7 +606,7 @@ Special thanks to the following contributors: @eouia, @khassel, @kolbyjack, @Kri
### Added
- Added a new config option `httpHeaders` used by helmet (see https://helmetjs.github.io/). You can now set own httpHeaders which will override the defaults in `js/defaults.js` which is useful e.g. if you want to embed MagicMirror into another website (solves #2847).
- Added a new config option `httpHeaders` used by helmet (see <https://helmetjs.github.io/>). You can now set own httpHeaders which will override the defaults in `js/defaults.js` which is useful e.g. if you want to embed MagicMirror into another website (solves #2847).
- Show endDate for calendar events when dateHeader is enabled and showEnd is set to true (#2192).
- Added the notification emitting from the weather module on information updated.
- Use recommended file extension for YAML files (#2864).
@@ -1357,7 +1521,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
### Fixed
- Fix instruction in README for using automatically installer script.
- Bug of duplicated compliments as described in [here](https://forum.magicmirror.builders/topic/2381/compliments-module-stops-cycling-compliments).
- Bug of [duplicated compliments](https://forum.magicmirror.builders/topic/2381/compliments-module-stops-cycling-compliments).
- Fix double message about port when server is starting
- Corrected Swedish translations for TODAY/TOMORROW/DAYAFTERTOMORROW.
- Removed unused import from js/electron.js
@@ -1606,3 +1770,52 @@ It includes (but is not limited to) the following features:
### Initial release of MagicMirror
This was part of the blogpost: [https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the)
[2.32.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.31.0...v2.32.0
[2.31.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.30.0...v2.31.0
[2.30.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.29.0...v2.30.0
[2.29.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.28.0...v2.29.0
[2.28.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.27.0...v2.28.0
[2.27.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.26.0...v2.27.0
[2.26.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.25.0...v2.26.0
[2.25.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.24.0...v2.25.0
[2.24.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.23.0...v2.24.0
[2.23.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.22.0...v2.23.0
[2.22.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.21.0...v2.22.0
[2.21.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.20.0...v2.21.0
[2.20.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.19.0...v2.20.0
[2.19.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.18.0...v2.19.0
[2.18.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.17.1...v2.18.0
[2.17.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.17.0...v2.17.1
[2.17.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.16.0...v2.17.0
[2.16.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.15.0...v2.16.0
[2.15.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.14.0...v2.15.0
[2.14.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.13.0...v2.14.0
[2.13.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.12.0...v2.13.0
[2.12.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.11.0...v2.12.0
[2.11.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.10.1...v2.11.0
[2.10.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.10.0...v2.10.1
[2.10.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.9.0...v2.10.0
[2.9.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.8.0...v2.9.0
[2.8.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.7.1...v2.8.0
[2.7.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.7.0...v2.7.1
[2.7.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.6.0...v2.7.0
[2.6.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.5.0...v2.6.0
[2.5.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.4.1...v2.5.0
[2.4.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.4.0...v2.4.1
[2.4.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.3.1...v2.4.0
[2.3.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.3.0...v2.3.1
[2.3.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.2.2...v2.3.0
[2.2.2]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.2.1...v2.2.2
[2.2.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.2.0...v2.2.1
[2.2.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.1.3...v2.2.0
[2.1.3]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.1.2...v2.1.3
[2.1.2]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.1.1...v2.1.2
[2.1.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.1.0...v2.1.1
[2.1.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.0.5...v2.1.0
[2.0.5]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.0.4...v2.0.5
[2.0.4]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.0.3...v2.0.4
[2.0.3]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.0.2...v2.0.3
[2.0.2]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.0.1...v2.0.2
[2.0.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/MagicMirrorOrg/MagicMirror/releases/tag/v2.0.0

View File

@@ -1,3 +1,5 @@
# Collaboration
This document describes how collaborators of this repository should work together.
## Pull Requests
@@ -28,12 +30,16 @@ Are done by
### Deployment steps
- [ ] pull latest `develop` branch
- [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0`
- [ ] test `develop` branch
- [ ] update `CHANGELOG.md`
- [ ] add all contributor names: `...`
- [ ] add min. node version: > ⚠️ This release needs nodejs version `v20` or `v22`, minimum version is `v20.9.0`
- [ ] commit and push all changes
- [ ] 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`
@@ -42,17 +48,20 @@ Are done by
- [ ] create new release with
- [ ] corresponding version tag `v2.xx.0`
- [ ] a release name: `...`
- [ ] description of the PR is the section of the `CHANGELOG.md`
- [ ] 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`
- [ ] commit and publish `develop` branch
- [ ] 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-2024 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,14 +1,14 @@
![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/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">
</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/MagicMirrorOrg/MagicMirror/graphs/contributors).
@@ -24,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)
@@ -49,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

@@ -28,7 +28,7 @@
});
// determine if "--use-tls"-flag was provided
config["tls"] = process.argv.indexOf("--use-tls") > 0;
config.tls = process.argv.indexOf("--use-tls") > 0;
}
/**
@@ -83,6 +83,17 @@
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
@@ -94,7 +105,7 @@
// Spawn electron application
const electron = require("electron");
const child = require("node: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) {

View File

@@ -28,7 +28,11 @@ let config = {
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",

View File

@@ -38,6 +38,7 @@
"currentweather",
"CUSTOMCSS",
"customregions",
"cxmj",
"Cymraeg",
"dariom",
"darksky",
@@ -48,13 +49,19 @@
"DAYBEFOREYESTERDAY",
"defaultmodules",
"dgoth",
"dkallen",
"drivelist",
"DTEND",
"DTSTAMP",
"DTSTART",
"Duffman",
"earlman",
"easyas",
"eddiehung",
"Edgardos",
"Ekristoffe",
"elec",
"eltociear",
"envcanada",
"envsub",
"envsubst",
@@ -78,9 +85,12 @@
"fulldate",
"fullday",
"fullscreen",
"geraki",
"Gevoelstemperatuur",
"GHSA",
"ghsas",
"grenagit",
"Heiko",
"Hirschberger",
"hourlyweather",
"Hwind",
@@ -92,12 +102,14 @@
"jakemulley",
"jakobsarwary",
"jalibu",
"jargordon",
"jetson",
"jkriegshauser",
"jsdocs",
"jsonlint",
"jupadin",
"kaennchenstruggle",
"Kalenderwoche",
"kenzal",
"Keyport",
"khassel",
@@ -112,8 +124,11 @@
"krekos",
"Kristjan",
"krukle",
"labwc",
"Landis",
"larryare",
"letsencrypt",
"libgpiod",
"Lightspeed",
"locationforecast",
"lockstring",
@@ -125,12 +140,15 @@
"martingron",
"marvai",
"mastermerge",
"matchtype",
"maxentries",
"Meteo",
"michaelteeuw",
"michmich",
"Midori",
"mirontoli",
"MISSINGLANG",
"mixasgr",
"MMPM",
"modernizr",
"modulename",
@@ -145,6 +163,7 @@
"newsitems",
"nfogal",
"njwilliams",
"nonrepeating",
"Norsk",
"nunjuck",
"odroid",
@@ -162,6 +181,7 @@
"psieg",
"radokristof",
"rajniszp",
"rebuilded",
"Reis",
"rejas",
"Resig",
@@ -169,9 +189,12 @@
"rohitdharavath",
"Rosso",
"rrule",
"savvadam",
"sdetweil",
"sendheaders",
"serveronly",
"sexualized",
"skpanagiotis",
"SMHI",
"Snille",
"socketclient",
@@ -185,10 +208,12 @@
"sunaction",
"suncalc",
"suntimes",
"symboltest",
"systeminformation",
"tada",
"taglist",
"Teeuw",
"TESTMODE",
"thomasrockhu",
"tomzt",
"ukmetoffice",
@@ -217,11 +242,12 @@
"xlarge",
"xrandr",
"xsmall",
"xsorifc",
"xwindows",
"xxxe",
"Ybbet",
"yearmatchgroup"
],
"ignorePaths": ["node_modules/**", "modules/**", "vendor/node_modules/**", "translations/**", "tests/mocks/**", "tests/e2e/modules/clock_es_spec.js", "fonts/roboto.css"],
"ignorePaths": ["node_modules/**", "modules/**", "translations/**", "tests/mocks/**", "tests/e2e/modules/clock_es_spec.js", "css/roboto.css"],
"dictionaries": ["node"]
}

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;
}

View File

@@ -2,11 +2,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -14,11 +14,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -26,11 +26,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -38,11 +38,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -50,11 +50,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -62,11 +62,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -74,11 +74,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -86,11 +86,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -98,11 +98,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -110,11 +110,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -122,11 +122,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -134,11 +134,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -146,11 +146,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -158,11 +158,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -170,11 +170,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -182,11 +182,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -194,11 +194,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -206,11 +206,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -218,11 +218,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -230,11 +230,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -242,11 +242,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -254,11 +254,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -266,11 +266,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -278,11 +278,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -290,11 +290,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -302,11 +302,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -314,11 +314,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -326,11 +326,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -338,11 +338,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -350,11 +350,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -362,11 +362,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -374,11 +374,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -386,11 +386,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -398,11 +398,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -410,11 +410,11 @@
@font-face {
font-family: Roboto;
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -422,11 +422,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -434,11 +434,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -446,11 +446,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -458,11 +458,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -470,11 +470,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -482,11 +482,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -494,11 +494,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -506,11 +506,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -518,11 +518,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -530,11 +530,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -542,11 +542,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -554,11 +554,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -566,11 +566,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -578,11 +578,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -590,11 +590,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -602,11 +602,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -614,11 +614,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -626,11 +626,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -638,11 +638,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -650,11 +650,11 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}
@@ -662,10 +662,10 @@
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-display: var(--fontsource-display, swap);
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");
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;
}

View File

@@ -1,10 +1,14 @@
import eslintPluginJest from "eslint-plugin-jest";
import eslintPluginJs from "@eslint/js";
import eslintPluginStylistic from "@stylistic/eslint-plugin";
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";
const config = [
eslintPluginJs.configs.recommended,
export default defineConfig([
globalIgnores(["config/**", "modules/**/*", "!modules/default/**", "js/positions.js"]),
{
files: ["**/*.js"],
languageOptions: {
@@ -12,7 +16,6 @@ const config = [
globals: {
...globals.browser,
...globals.node,
...globals.jest,
Log: "readonly",
MM: "readonly",
Module: "readonly",
@@ -20,13 +23,9 @@ const config = [
moment: "readonly"
}
},
plugins: {
...eslintPluginStylistic.configs["all-flat"].plugins,
...eslintPluginJest.configs["flat/recommended"].plugins
},
plugins: {js, jsdoc, stylistic},
extends: [importX.recommended, jest.configs["flat/recommended"], "js/recommended", jsdoc.configs["flat/recommended"], "stylistic/all"],
rules: {
...eslintPluginStylistic.configs["all-flat"].rules,
...eslintPluginJest.configs["flat/recommended"].rules,
"@stylistic/array-element-newline": ["error", "consistent"],
"@stylistic/arrow-parens": ["error", "always"],
"@stylistic/brace-style": "off",
@@ -51,8 +50,12 @@ const config = [
"@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",
@@ -60,7 +63,7 @@ const config = [
"jest/prefer-mock-promise-shorthand": "warn",
"jest/prefer-to-be": "warn",
"jest/prefer-to-have-length": "warn",
"max-lines-per-function": ["warn", 350],
"max-lines-per-function": ["warn", 400],
"max-statements": "off",
"no-global-assign": "off",
"no-inline-comments": "off",
@@ -71,16 +74,31 @@ const config = [
"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-destructuring": "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: {
@@ -90,21 +108,19 @@ const config = [
},
sourceType: "module"
},
plugins: {
...eslintPluginStylistic.configs["all-flat"].plugins
},
plugins: {js, stylistic},
extends: [importX.recommended, "js/all", "stylistic/all"],
rules: {
...eslintPluginStylistic.configs["all-flat"].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"],
"func-style": "off",
"import/namespace": "off",
"import-x/no-unresolved": ["error", {ignore: ["eslint/config"]}],
"max-lines-per-function": ["error", 100],
"no-magic-numbers": "off",
"one-var": "off",
"prefer-destructuring": "off"
"one-var": ["error", "never"],
"sort-keys": "off"
}
},
{
@@ -112,10 +128,5 @@ const config = [
rules: {
"@stylistic/quotes": "off"
}
},
{
ignores: ["config/**", "modules/**", "!modules/default/**", "js/positions.js"]
}
];
export default config;
]);

View File

@@ -1,29 +0,0 @@
{
"name": "magicmirror-fonts",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "magicmirror-fonts",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@fontsource/roboto": "^5.1.0",
"@fontsource/roboto-condensed": "^5.1.0"
}
},
"node_modules/@fontsource/roboto": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.1.0.tgz",
"integrity": "sha512-cFRRC1s6RqPygeZ8Uw/acwVHqih8Czjt6Q0MwoUoDe9U3m4dH1HmNDRBZyqlMSFwgNAUKgFImncKdmDHyKpwdg==",
"license": "Apache-2.0"
},
"node_modules/@fontsource/roboto-condensed": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.1.0.tgz",
"integrity": "sha512-cTS62X9bgR6H+3qRtaDwt0I+3ocitMPalyr2OrzJtilIcuEo4my8UA4VVhOgr0OI2Sk9JNrNYcSxkv0k4XuKtQ==",
"license": "OFL-1.1"
}
}
}

View File

@@ -1,17 +0,0 @@
{
"name": "magicmirror-fonts",
"version": "1.0.0",
"description": "Package for fonts use by MagicMirror² core.",
"bugs": {
"url": "https://github.com/MagicMirrorOrg/MagicMirror/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/MagicMirrorOrg/MagicMirror"
},
"license": "MIT",
"dependencies": {
"@fontsource/roboto": "^5.1.0",
"@fontsource/roboto-condensed": "^5.1.0"
}
}

View File

@@ -12,12 +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="vendor/node_modules/animate.css/animate.min.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>
@@ -41,10 +42,10 @@
</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>

View File

@@ -20,7 +20,6 @@ module.exports = async () => {
},
{
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"]

View File

@@ -155,3 +155,4 @@ function removeAnimateCSS (element, animation) {
node.classList.remove("animate__animated", animationName);
node.style.removeProperty("--animate-duration");
}
if (typeof window === "undefined") module.exports = { AnimateCSSIn, AnimateCSSOut };

View File

@@ -11,8 +11,14 @@ 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.
@@ -71,7 +77,7 @@ 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.log("config template file not exists, no envsubst");
@@ -120,7 +126,7 @@ 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?");
@@ -147,11 +153,23 @@ function App () {
*/
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(`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.`);
}
}
}
}
@@ -163,24 +181,24 @@ function App () {
const elements = module.split("/");
const moduleName = elements[elements.length - 1];
const env = getEnvVarsAsObj();
let moduleFolder = `${__dirname}/../${env.modulesDir}/${module}`;
let moduleFolder = path.resolve(`${__dirname}/../${env.modulesDir}`, module);
if (defaultModules.includes(moduleName)) {
const defaultModuleFolder = `${__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") {
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}.`);
}
@@ -189,7 +207,7 @@ 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}.`);
@@ -197,7 +215,13 @@ function App () {
// 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) {
@@ -288,6 +312,8 @@ function App () {
}
}
setGlobalDispatcher(new Agent({ connect: { timeout: fetch_timeout } }));
await loadModules(modules);
httpServer = new Server(config);
@@ -338,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,7 +1,7 @@
const path = require("node:path");
const fs = require("node:fs");
const { styleText } = require("node:util");
const Ajv = require("ajv");
const colors = require("ansis");
const globals = require("globals");
const { Linter } = require("eslint");
@@ -35,7 +35,7 @@ function checkConfigFile () {
// Check permission
try {
fs.accessSync(configFileName, fs.F_OK);
fs.accessSync(configFileName, fs.constants.F_OK);
} catch (error) {
throw new Error(`${error}\nNo permission to access config file!`);
}
@@ -54,13 +54,14 @@ function checkConfigFile () {
globals: {
...globals.node
}
}
},
rules: { "no-undef": "error" }
},
configFileName
);
if (errors.length === 0) {
Log.info(colors.green("Your configuration file doesn't contain syntax errors :)"));
Log.info(styleText("green", "Your configuration file doesn't contain syntax errors :)"));
validateModulePositions(configFileName);
} else {
let errorMessage = "Your configuration file contains syntax errors :(";
@@ -72,6 +73,10 @@ function checkConfigFile () {
}
}
/**
*
* @param {string} configFileName - The path and filename of the configuration file to validate.
*/
function validateModulePositions (configFileName) {
Log.info("Checking modules structure configuration ...");
@@ -107,7 +112,7 @@ function validateModulePositions (configFileName) {
const valid = validate(data);
if (valid) {
Log.info(colors.green("Your modules structure configuration doesn't contain errors :)"));
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];

View File

@@ -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,7 +70,7 @@ const defaults = {
position: "bottom_bar",
classes: "xsmall dimmed",
config: {
text: "www.michaelteeuw.nl"
text: "https://magicmirror.builders/"
}
}
]

View File

@@ -1,3 +1,4 @@
module.exports = {
configs: ["kioskmode"]
configs: ["kioskmode"],
clock: ["secondsColor"]
};

View File

@@ -76,6 +76,23 @@ function createWindow () {
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);
@@ -85,7 +102,7 @@ function createWindow () {
*/
let prefix;
if ((config["tls"] !== null && config["tls"]) || config.useHttps) {
if ((config.tls !== null && config.tls) || config.useHttps) {
prefix = "https://";
} else {
prefix = "http://";
@@ -95,7 +112,7 @@ function createWindow () {
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
@@ -134,11 +151,11 @@ 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) {
if (config.ignoreXOriginHeader || false) {
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !(/x-frame-options/i).test(header[0])));
}
if (config["ignoreContentSecurityPolicy"] || false) {
if (config.ignoreContentSecurityPolicy || false) {
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !(/content-security-policy/i).test(header[0])));
}

View File

@@ -15,7 +15,7 @@ const Loader = (function () {
* @returns {object} with key: values as assembled in js/server_functions.js
*/
const getEnvVars = async function () {
const res = await fetch(`${location.protocol}//${location.host}/env`);
const res = await fetch(`${location.protocol}//${location.host}${config.basePath}env`);
return JSON.parse(await res.text());
};
@@ -108,7 +108,8 @@ const Loader = (function () {
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
});
});
@@ -217,29 +218,22 @@ const Loader = (function () {
* Load all modules as defined in the config.
*/
async loadModules () {
let moduleData = await getModuleData();
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(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();
},
/**
@@ -266,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(`vendor/${vendor[fileName]}`);
return loadFile(`${vendor[fileName]}`);
}
// File not loaded yet.

View File

@@ -2,7 +2,7 @@
(function (root, factory) {
if (typeof exports === "object") {
if (process.env.JEST_WORKER_ID === undefined) {
const colors = require("ansis");
const { styleText } = require("node:util");
// add timestamps in front of log messages
require("console-stamp")(console, {
@@ -11,26 +11,35 @@
label: (arg) => {
const { method, defaultTokens } = arg;
let label = defaultTokens.label(arg);
if (method === "error") {
label = colors.red(label);
} else if (method === "warn") {
label = colors.yellow(label);
} else if (method === "debug") {
label = colors.bgBlue(label);
} else if (method === "info") {
label = colors.blue(label);
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);
if (method === "error") {
msg = colors.red(msg);
} else if (method === "warn") {
msg = colors.yellow(msg);
} else if (method === "info") {
msg = colors.blue(msg);
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;
}

View File

@@ -30,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);
@@ -463,7 +465,8 @@ const MM = (function () {
}
});
wrapper.style.display = showWrapper ? "block" : "none";
// move container definitions to main CSS
wrapper.className = showWrapper ? "container" : "container hidden";
});
};
@@ -608,13 +611,13 @@ const MM = (function () {
// 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}/startup`);
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);
console.warn("Refreshing Website because server was restarted");
Log.warn("Refreshing Website because server was restarted");
}
} catch (err) {
Log.error(`MagicMirror not reachable: ${err}`);

View File

@@ -6,10 +6,11 @@ const express = require("express");
const ipfilter = require("express-ipfilter").IpFilter;
const helmet = require("helmet");
const socketio = require("socket.io");
const Log = require("logger");
const { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars } = require("./server_functions");
const vendor = require(`${__dirname}/vendor`);
/**
* Server
* @param {object} config The MM config
@@ -72,12 +73,13 @@ function Server (config) {
app.use(helmet(config.httpHeaders));
app.use("/js", express.static(__dirname));
let directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations"];
if (process.env.JEST_WORKER_ID !== undefined) {
// add tests directories only when running tests
directories.push("/tests/configs", "/tests/mocks");
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]}`);
}
for (const directory of directories) {
const uniqDirs = [...new Set(directories)];
for (const directory of uniqDirs) {
app.use(directory, express.static(path.resolve(global.root_path + directory)));
}

View File

@@ -109,6 +109,7 @@ function geExpectedReceivedHeaders (url) {
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") {

View File

@@ -19,15 +19,16 @@ module.exports = {
let installedNodeVersion = execSync("node -v", { encoding: "utf-8" }).replace("v", "").replace(/(?:\r\n|\r|\n)/g, "");
const staticData = await si.get({
system: "manufacturer, model, raspberry, virtual",
system: "manufacturer, model, virtual",
osInfo: "platform, distro, release, arch",
versions: "kernel, node, npm, pm2"
});
let systemDataString = "System information:";
systemDataString += `\n### SYSTEM: manufacturer: ${staticData["system"]["manufacturer"]}; model: ${staticData["system"]["model"]}; virtual: ${staticData["system"]["virtual"]}`;
systemDataString += `\n### OS: platform: ${staticData["osInfo"]["platform"]}; distro: ${staticData["osInfo"]["distro"]}; release: ${staticData["osInfo"]["release"]}; arch: ${staticData["osInfo"]["arch"]}; kernel: ${staticData["versions"]["kernel"]}`;
systemDataString += `\n### VERSIONS: electron: ${process.versions.electron}; used node: ${staticData["versions"]["node"]}; installed node: ${installedNodeVersion}; npm: ${staticData["versions"]["npm"]}; pm2: ${staticData["versions"]["pm2"]}`;
systemDataString += `\n### OTHER: timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`;
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
@@ -52,7 +53,7 @@ module.exports = {
// if not already discovered
if (modulePositions.length === 0) {
// get the lines of the index.html
const lines = fs.readFileSync(indexFileName).toString().split(os.EOL);
const lines = fs.readFileSync(indexFileName).toString().split("\n");
// loop thru the lines
lines.forEach((line) => {
// run the regex on each line
@@ -65,7 +66,12 @@ module.exports = {
modulePositions.push(positionName);
}
});
fs.writeFileSync(discoveredPositionsJSFilename, `const modulePositions=${JSON.stringify(modulePositions)}`);
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

@@ -6,7 +6,7 @@ const vendor = {
"font-awesome.css": "css/font-awesome.css",
"nunjucks.js": "node_modules/nunjucks/browser/nunjucks.min.js",
"suncalc.js": "node_modules/suncalc/suncalc.js",
"croner.js": "node_modules/croner/dist/croner.umd.min.js"
"croner.js": "node_modules/croner/dist/croner.umd.js"
};
if (typeof module !== "undefined") {

View File

@@ -25,6 +25,7 @@ Module.register("alert", {
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",

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

@@ -77,7 +77,7 @@ Module.register("calendar", {
// Define required scripts.
getScripts () {
return ["calendarutils.js", "moment.js"];
return ["calendarutils.js", "moment.js", "moment-timezone.js"];
},
// Define required translations.
@@ -168,12 +168,17 @@ Module.register("calendar", {
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 (notification, payload) {
if (notification === "FETCH_CALENDAR") {
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
}
if (this.identifier !== payload.id) {
return;
@@ -210,18 +215,9 @@ Module.register("calendar", {
this.updateDom(this.config.animationSpeed);
},
eventEndingWithinNextFullTimeUnit (event, ONE_DAY) {
const now = new Date();
return event.endDate - now <= ONE_DAY;
},
// Override dom generator.
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;
@@ -253,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");
@@ -335,7 +333,7 @@ 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}`;
@@ -390,14 +388,14 @@ 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) {
if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) {
// no duration here, don't display end
} else {
timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(moment(event.endDate, "x").format("LT"))}`;
timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(eventEndDateMoment.format("LT"))}`;
}
}
@@ -410,36 +408,43 @@ 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 = CalendarUtils.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) {
if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) {
// no duration here, don't display end
} else {
// and has a duation
if (event.startDate !== event.endDate) {
timeWrapper.innerHTML += "-";
timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
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 = CalendarUtils.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 = 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 = CalendarUtils.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
@@ -447,9 +452,9 @@ Module.register("calendar", {
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
} else if (event.yesterday) {
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
} else if (event.tomorrow) {
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
} else if (event.dayAfterTomorrow) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
}
@@ -457,23 +462,25 @@ Module.register("calendar", {
}
} else {
// Show relative times
if (event.startDate >= now || (event.fullDayEvent && this.eventEndingWithinNextFullTimeUnit(event, ONE_DAY))) {
if (eventStartDateMoment.isSameOrAfter(now) || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) {
// Use relative time
if (!this.config.hideTime && !event.fullDayEvent) {
timeWrapper.innerHTML = CalendarUtils.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 = CalendarUtils.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 || (event.fullDayEvent && this.eventEndingWithinNextFullTimeUnit(event, ONE_DAY))) {
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") {
@@ -481,23 +488,25 @@ Module.register("calendar", {
}
} else if (event.yesterday) {
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
} else if (event.tomorrow) {
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
} else if (event.dayAfterTomorrow) {
if (this.translate("DAYAFTERTOMORROW") !== "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 = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
timeWrapper.innerHTML = `${CalendarUtils.capFirst(eventStartDateMoment.fromNow())}`;
}
} else {
// Ongoing event
timeWrapper.innerHTML = CalendarUtils.capFirst(
this.translate("RUNNING", {
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
timeUntilEnd: eventEndDateMoment.fromNow(true)
})
);
}
@@ -576,106 +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 (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;
let now = moment();
let today = now.clone().startOf("day");
let future = now.clone().startOf("day").add(this.config.maximumNumberOfDays, "days");
let now, today, future;
if (this.config.forceUseCurrentTime || this.defaults.forceUseCurrentTime) {
now = new Date();
today = moment().startOf("day");
future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
} else {
now = new Date(Date.now()); // Can use overridden time
today = moment(now).startOf("day");
future = moment(now).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.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;
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.round((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")
= eventStartDateMoment
.clone()
.startOf("day")
.add(1, "day")
.endOf("day")
.format("x");
.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 = moment(midnight, "x").clone().subtract(1, "day").format("x");
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").endOf("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;
});
@@ -690,16 +707,16 @@ Module.register("calendar", {
*/
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");
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 > lastDate) {
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--;
@@ -715,7 +732,7 @@ Module.register("calendar", {
}
events = newEvents;
}
Log.info(`slicing events total maxcount=${this.config.maximumEntries}`);
return events.slice(0, this.config.maximumEntries);
},
@@ -886,9 +903,13 @@ Module.register("calendar", {
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;
},

View File

@@ -56,7 +56,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
try {
data = ical.parseICS(responseData);
Log.debug(`parsed data=${JSON.stringify(data)}`);
Log.debug(`parsed data=${JSON.stringify(data, null, 2)}`);
events = CalendarFetcherUtils.filterEvents(data, {
excludedEvents,
includePastEvents,
@@ -81,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 */

View File

@@ -1,114 +1,130 @@
/**
* @external Moment
*/
const path = require("node:path");
const moment = require("moment");
const moment = require("moment-timezone");
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
const Log = require("../../../js/logger");
const CalendarFetcherUtils = {
/**
* 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 adjustment
* @param {Date} date the date on which this event happens
* @returns {number} the necessary adjustment in hours
* 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.
*/
calculateTimezoneAdjustment (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}`);
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 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 = CalendarFetcherUtils.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)
if (filter instanceof Object) {
if (typeof filter.until !== "undefined") {
until = filter.until;
}
}
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 = 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}`);
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 {
// 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();
filter = filter.toLowerCase();
}
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");
if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
if (until) {
filter.until = until;
} else {
filter.excluded = true;
}
break;
}
}
Log.debug(`adjustHours=${adjustHours}`);
return adjustHours;
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)));
},
/**
@@ -120,34 +136,33 @@ const CalendarFetcherUtils = {
filterEvents (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 CalendarFetcherUtils.isFullDayEvent(event) ? moment(event[time]).startOf("day") : moment(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 = new Date(Date.now());
const todayLocal = moment(now).startOf("day").toDate();
const futureLocalDate
= moment(now)
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, "seconds") // Subtract 1 second so that events that start on the middle of the night will not repeat.
.toDate();
// 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...");
let pastLocalDate = todayLocal;
if (config.includePastEvents) {
pastLocalDate = moment(now).startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
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.
@@ -160,259 +175,103 @@ const CalendarFetcherUtils = {
}
if (event.type === "VEVENT") {
Log.debug(`Event:\n${JSON.stringify(event)}`);
let startMoment = eventDate(event, "start");
let endMoment;
Log.debug(`Event:\n${JSON.stringify(event, null, 2)}`);
let eventStartMoment = eventDate(event, "start");
let eventEndMoment;
if (typeof event.end !== "undefined") {
endMoment = eventDate(event, "end");
eventEndMoment = eventDate(event, "end");
} else if (typeof event.duration !== "undefined") {
endMoment = startMoment.clone().add(moment.duration(event.duration));
eventEndMoment = eventStartMoment.clone().add(moment.duration(event.duration));
} else {
if (!isFacebookBirthday) {
// make copy of start date, separate storage area
endMoment = moment(startMoment.valueOf());
eventEndMoment = eventStartMoment.clone();
} else {
endMoment = moment(startMoment).add(1, "days");
eventEndMoment = eventStartMoment.clone().add(1, "days");
}
}
Log.debug(`start: ${startMoment.toDate()}`);
Log.debug(`end:: ${endMoment.toDate()}`);
Log.debug(`start: ${eventStartMoment.toDate()}`);
Log.debug(`end:: ${eventEndMoment.toDate()}`);
// Calculate the duration of the event for use with recurring events.
const durationMs = endMoment.valueOf() - startMoment.valueOf();
const durationMs = eventEndMoment.valueOf() - eventStartMoment.valueOf();
Log.debug(`duration: ${durationMs}`);
// FIXME: Since the parsed json object from node-ical comes with time information
// this check could be removed (?)
if (event.start.length === 8) {
startMoment = startMoment.startOf("day");
}
const title = CalendarFetcherUtils.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 (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
if (until) {
dateFilter = until;
} else {
excluded = true;
}
break;
}
}
if (excluded) {
return;
}
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) {
const rule = event.rrule;
// Recurring event.
let moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(event, pastLocalMoment, futureLocalMoment, durationMs);
const pastMoment = moment(pastLocalDate);
const futureMoment = moment(futureLocalDate);
// 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;
let futureLocal;
if (CalendarFetcherUtils.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}`);
} 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(now).toDate(); //now
}
futureLocal = futureMoment.toDate(); // future
}
Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
const hasByWeekdayRule = rule.options.byweekday !== undefined && rule.options.byweekday !== null;
const oneDayInMs = 24 * 60 * 60 * 1000;
Log.debug(`RRule: ${rule.toString()}`);
rule.options.tzid = null; // RRule gets *very* confused with timezones
let dates = rule.between(new Date(pastLocal.valueOf() - oneDayInMs), new Date(futureLocal.valueOf() + oneDayInMs), true, () => { return true; });
Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
dates = dates.filter((d) => {
if (JSON.stringify(d) === "null") return false;
else return true;
});
// RRule can generate dates with an incorrect recurrence date. Process the array here and apply date correction.
if (hasByWeekdayRule) {
Log.debug("Rule has byweekday, checking for correction");
dates.forEach((date, index, arr) => {
// NOTE: getTimezoneOffset() is negative of the expected value. For America/Los_Angeles under DST (GMT-7),
// this value is +420. For Australia/Sydney under DST (GMT+11), this value is -660.
const tzOffset = date.getTimezoneOffset() / 60;
const hour = date.getHours();
if ((tzOffset < 0) && (hour < -tzOffset)) { // east of GMT
Log.debug(`East of GMT (tzOffset: ${tzOffset}) and hour=${hour} < ${-tzOffset}, Subtracting 1 day from ${date}`);
arr[index] = new Date(date.valueOf() - oneDayInMs);
} else if ((tzOffset > 0) && (hour >= (24 - tzOffset))) { // west of GMT
Log.debug(`West of GMT (tzOffset: ${tzOffset}) and hour=${hour} >= 24-${tzOffset}, Adding 1 day to ${date}`);
arr[index] = new Date(date.valueOf() + oneDayInMs);
}
});
// Adjusting the dates could push it beyond the 'until' date, so filter those out here.
if (rule.options.until !== null) {
dates = dates.filter((date) => {
return date.valueOf() <= rule.options.until.valueOf();
});
}
}
// The dates array from rrule can be confused by DST. If the event was created during DST and we
// are querying a date range during non-DST, rrule can have the incorrect time for the date range.
// Reprocess the array here computing and applying the time offset.
dates.forEach((date, index, arr) => {
let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date);
if (adjustHours !== 0) {
Log.debug(`Applying timezone adjustment hours=${adjustHours} to ${date}`);
arr[index] = new Date(date.valueOf() + (adjustHours * 60 * 60 * 1000));
}
});
// 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 dateKey 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.
let d = new Date(dateKey);
if (!moment(d).isBetween(pastMoment, futureMoment)) {
dates.push(d);
}
}
}
// Lastly, sometimes rrule doesn't include the event.start even if it is in the requested range. Ensure
// inclusion here. Unfortunately dates.includes() doesn't find it so we have to do forEach().
{
let found = false;
dates.forEach((d) => { if (d.valueOf() === event.start.valueOf()) found = true; });
if (!found) {
Log.debug(`event.start=${event.start} was not included in results from rrule; adding`);
dates.splice(0, 0, event.start);
}
}
// 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];
// 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 curDurationMs = durationMs;
let showRecurrence = true;
let recurringEventStartMoment = moments[m].tz(CalendarFetcherUtils.getLocalTimezone()).clone();
let recurringEventEndMoment = recurringEventStartMoment.clone().add(durationMs, "ms");
startMoment = moment(date);
// 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/).
// This must be done after `date` is adjusted
const dateKey = date.toISOString().substring(0, 10);
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 && 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];
startMoment = moment(curEvent.start);
curDurationMs = curEvent.end.valueOf() - startMoment.valueOf();
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.
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;
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;
}
}
Log.debug(`duration: ${curDurationMs}`);
endMoment = moment(startMoment.valueOf() + curDurationMs);
if (startMoment.valueOf() === endMoment.valueOf()) {
endMoment = endMoment.endOf("day");
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 (endMoment.isBefore(pastLocal) || startMoment.isAfter(futureLocal)) {
if (recurringEventEndMoment.isBefore(pastLocalMoment) || recurringEventStartMoment.isAfter(futureLocalMoment)) {
showRecurrence = false;
}
if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, dateFilter)) {
if (CalendarFetcherUtils.timeFilterApplies(now, recurringEventEndMoment, eventFilterUntil)) {
showRecurrence = false;
}
if (showRecurrence === true) {
Log.debug(`saving event: ${description}`);
Log.debug(`saving event: ${recurrenceTitle}`);
newEvents.push({
title: recurrenceTitle,
startDate: startMoment.format("x"),
endDate: endMoment.format("x"),
startDate: recurringEventStartMoment.format("x"),
endDate: recurringEventEndMoment.format("x"),
fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event),
recurringEvent: true,
class: event.class,
@@ -421,7 +280,10 @@ const CalendarFetcherUtils = {
geo: geo,
description: description
});
} else {
Log.debug("not saving event ", recurrenceTitle, eventStartMoment);
}
Log.debug(" ");
}
// End recurring event parsing.
} else {
@@ -430,49 +292,45 @@ const CalendarFetcherUtils = {
// 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 && startMoment.valueOf() === endMoment.valueOf()) {
endMoment = endMoment.endOf("day");
if (fullDayEvent && eventStartMoment.valueOf() === eventEndMoment.valueOf()) {
eventEndMoment = eventEndMoment.endOf("day");
}
if (config.includePastEvents) {
// Past event is too far in the past, so skip.
if (endMoment < pastLocalDate) {
if (eventEndMoment < pastLocalMoment) {
return;
}
} else {
// It's not a fullday event, and it is in the past, so skip.
if (!fullDayEvent && endMoment < now) {
if (!fullDayEvent && eventEndMoment < now) {
return;
}
// It's a fullday event, and it is before today, So skip.
if (fullDayEvent && endMoment <= todayLocal) {
if (fullDayEvent && eventEndMoment <= now.startOf("day")) {
return;
}
}
// It exceeds the maximumNumberOfDays limit, so skip.
if (startMoment > futureLocalDate) {
if (eventStartMoment > futureLocalMoment) {
return;
}
if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, dateFilter)) {
if (CalendarFetcherUtils.timeFilterApplies(now, eventEndMoment, eventFilterUntil)) {
return;
}
// get correction for date saving and dst change between now and then
let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startMoment.toDate());
// This shouldn't happen
if (adjustHours) {
Log.warn(`Unexpected timezone adjustment of ${adjustHours} hours on non-recurring event`);
}
// Every thing is good. Add it to the list.
newEvents.push({
title: title,
startDate: startMoment.add(adjustHours, "hours").format("x"),
endDate: endMoment.add(adjustHours, "hours").format("x"),
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
@@ -488,18 +346,6 @@ const CalendarFetcherUtils = {
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 (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.
@@ -539,8 +385,8 @@ const CalendarFetcherUtils = {
/**
* 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 {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
*/
@@ -551,7 +397,7 @@ const CalendarFetcherUtils = {
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.toDate();
return now < filterUntil;
}
return false;

View File

@@ -14,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,
@@ -23,9 +23,9 @@ 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,
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
@@ -105,6 +105,8 @@ Module.register("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");
@@ -114,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/MagicMirrorOrg/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");
@@ -168,21 +171,28 @@ Module.register("clock", {
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);
}
@@ -208,13 +218,18 @@ Module.register("clock", {
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>`;
+ `<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);
}
@@ -267,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);

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

@@ -12,6 +12,7 @@ Module.register("compliments", {
},
updateInterval: 30000,
remoteFile: null,
remoteFileRefreshInterval: 0,
fadeSpeed: 4000,
morningStartTime: 3,
morningEndTime: 12,
@@ -20,6 +21,9 @@ Module.register("compliments", {
random: true,
specialDayUnique: false
},
urlSuffix: "",
compliments_new: null,
refreshMinimumDelay: 15 * 60 * 60 * 1000, // 15 minutes
lastIndexUsed: -1,
// Set currentweather from module
currentWeatherType: "",
@@ -41,6 +45,22 @@ 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
@@ -119,12 +139,17 @@ Module.register("compliments", {
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
@@ -185,8 +210,18 @@ Module.register("compliments", {
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);
}
},
/**
@@ -236,6 +271,27 @@ 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;
},

View File

@@ -38,7 +38,7 @@ Module.register("newsfeed", {
getUrlPrefix (item) {
if (item.useCorsProxy) {
return `${location.protocol}//${location.host}/cors?url=`;
return `${location.protocol}//${location.host}${config.basePath}cors?url=`;
} else {
return "";
}
@@ -57,7 +57,7 @@ Module.register("newsfeed", {
// Define required translations.
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;
},
@@ -177,6 +177,19 @@ 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.
@@ -188,7 +201,7 @@ Module.register("newsfeed", {
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);
}
}

View File

@@ -128,10 +128,13 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
* Schedule the timer for the next update.
*/
const scheduleTimer = function () {
clearTimeout(reloadTimer);
reloadTimer = setTimeout(function () {
fetchNews();
}, reloadIntervalMS);
if (process.env.JEST_WORKER_ID === undefined) {
// only set timer when not running in jest
clearTimeout(reloadTimer);
reloadTimer = setTimeout(function () {
fetchNews();
}, reloadIntervalMS);
}
};
/* public methods */

View File

@@ -1,3 +1,5 @@
const fs = require("node:fs");
const path = require("node:path");
const NodeHelper = require("node_helper");
const defaultModules = require("../defaultmodules");
const GitHelper = require("./git_helper");
@@ -14,8 +16,23 @@ module.exports = NodeHelper.create({
gitHelper: new GitHelper(),
updateHelper: null,
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 modules) {
for (const moduleName of this.getModules(modules)) {
if (!this.ignoreUpdateChecking(moduleName)) {
await this.gitHelper.add(moduleName);
}

View File

@@ -47,8 +47,8 @@ class Updater {
this.autoRestart = config.updateAutorestart;
this.moduleList = {};
this.updating = false;
this.usePM2 = false;
this.PM2 = null;
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!");
@@ -133,30 +133,30 @@ class Updater {
});
}
// restart rules (pm2 or npm start)
// restart rules (pm2 or node --run start)
restart () {
if (this.usePM2) this.pm2Restart();
else this.npmRestart();
else this.nodeRestart();
}
// restart MagicMiror with "pm2"
// restart MagicMiror with "pm2": use PM2Id for restart it
pm2Restart () {
Log.info("updatenotification: PM2 will restarting MagicMirror...");
const pm2 = require("pm2");
pm2.restart(this.PM2, (err, proc) => {
pm2.restart(this.PM2Id, (err, proc) => {
if (err) {
Log.error("updatenotification:[PM2] restart Error", err);
}
});
}
// restart MagicMiror with "npm start"
npmRestart () {
// restart MagicMiror with "node --run start"
nodeRestart () {
Log.info("updatenotification: Restarting MagicMirror...");
const out = process.stdout;
const err = process.stderr;
const subprocess = Spawn("npm start", { cwd: this.root_path, shell: true, detached: true, stdio: ["ignore", out, err] });
subprocess.unref();
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();
}
@@ -166,38 +166,45 @@ class Updater {
return new Promise((resolve) => {
if (fs.existsSync("/.dockerenv")) {
Log.info("updatenotification: Running in docker container, not using PM2 ...");
this.usePM2 = false;
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);
this.usePM2 = false;
resolve(false);
return;
}
pm2.list((err, list) => {
if (err) {
Log.error("updatenotification: [PM2] Can't get process List!");
this.usePM2 = false;
resolve(false);
return;
}
list.forEach((pm) => {
if (pm.pm2_env.version === this.version && pm.pm2_env.status === "online" && pm.pm2_env.pm_cwd.includes(`${this.root_path}/`)) {
this.PM2 = pm.name;
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", this.PM2);
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.PM2) {
if (!this.usePM2) {
Log.info("updatenotification: [PM2] You are not using pm2");
this.usePM2 = false;
resolve(false);
}
});

View File

@@ -6,7 +6,8 @@ Module.register("updatenotification", {
sendUpdatesNotifications: false,
updates: [],
updateTimeout: 2 * 60 * 1000, // max update duration
updateAutorestart: false // autoRestart MM when update done ?
updateAutorestart: false, // autoRestart MM when update done ?
useModulesFromConfig: true // if `false` iterate over modules directory
},
suspended: false,

View File

@@ -5,13 +5,14 @@
* @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) {
requestUrl = getCorsUrl(url, requestHeaders, expectedResponseHeaders);
requestUrl = getCorsUrl(url, requestHeaders, expectedResponseHeaders, basePath);
} else {
requestUrl = url;
request.headers = getHeadersToSend(requestHeaders);
@@ -37,13 +38,14 @@ async function performWebRequest (url, type = "json", useCorsProxy = false, requ
* @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}`;

View File

@@ -21,15 +21,25 @@
{% 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 %}
<td class="align-right bright precipitation-amount">
{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}
</td>
{% 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 %}

View File

@@ -244,17 +244,17 @@ WeatherProvider.register("openmeteo", {
.add(Math.max(0, Math.min(7, this.config.maxNumberOfDays)), "days")
.endOf("day");
params["start_date"] = startDate.format("YYYY-MM-DD");
params.start_date = startDate.format("YYYY-MM-DD");
switch (this.config.type) {
case "hourly":
case "daily":
case "forecast":
params["end_date"] = endDate.format("YYYY-MM-DD");
params.end_date = endDate.format("YYYY-MM-DD");
break;
case "current":
params["current_weather"] = true;
params["end_date"] = params["start_date"];
params.current_weather = true;
params.end_date = params.start_date;
break;
default:
// Failsafe
@@ -262,7 +262,7 @@ WeatherProvider.register("openmeteo", {
}
return Object.keys(params)
.filter((key) => (params[key] ? true : false))
.filter((key) => (!!params[key]))
.map((key) => {
switch (key) {
case "hourly":
@@ -470,7 +470,7 @@ WeatherProvider.register("openmeteo", {
61: "rain-slight-intensity",
63: "rain-moderate-intensity",
65: "rain-heavy-intensity",
66: "freezing-rain-light-heavy-intensity",
66: "freezing-rain-light-intensity",
67: "freezing-rain-heavy-intensity",
71: "snow-fall-slight-intensity",
73: "snow-fall-moderate-intensity",

View File

@@ -17,10 +17,13 @@ WeatherProvider.register("openweathermap", {
defaults: {
apiVersion: "3.0",
apiBase: "https://api.openweathermap.org/data/",
weatherEndpoint: "", // can be "onecall", "forecast" or "weather" (for current)
// weatherEndpoint is "/onecall" since API 3.0
// "/onecall", "/forecast" or "/weather" only for pro customers
weatherEndpoint: "/onecall",
locationID: false,
location: false,
lat: 0, // the onecall endpoint needs lat / lon values, it doesn't support the locationId
// the /onecall endpoint needs lat / lon values, it doesn't support the locationId
lat: 0,
lon: 0,
apiKey: ""
},
@@ -90,30 +93,6 @@ WeatherProvider.register("openweathermap", {
.finally(() => this.updateAvailable());
},
/**
* Overrides method for setting config to check if endpoint is correct for hourly
* @param {object} config The configuration object
*/
setConfig (config) {
this.config = config;
if (!this.config.weatherEndpoint) {
switch (this.config.type) {
case "hourly":
this.config.weatherEndpoint = "/onecall";
break;
case "daily":
case "forecast":
this.config.weatherEndpoint = "/forecast";
break;
case "current":
this.config.weatherEndpoint = "/weather";
break;
default:
Log.error("weatherEndpoint not configured and could not resolve it based on type");
}
}
},
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */
/*
* Gets the complete url for the request
@@ -306,12 +285,12 @@ WeatherProvider.register("openweathermap", {
current.weatherType = this.convertWeatherType(data.current.weather[0].icon);
current.humidity = data.current.humidity;
current.uv_index = data.current.uvi;
if (data.current.hasOwnProperty("rain") && !isNaN(data.current["rain"]["1h"])) {
current.rain = data.current["rain"]["1h"];
if (data.current.hasOwnProperty("rain") && !isNaN(data.current.rain["1h"])) {
current.rain = data.current.rain["1h"];
precip = true;
}
if (data.current.hasOwnProperty("snow") && !isNaN(data.current["snow"]["1h"])) {
current.snow = data.current["snow"]["1h"];
if (data.current.hasOwnProperty("snow") && !isNaN(data.current.snow["1h"])) {
current.snow = data.current.snow["1h"];
precip = true;
}
if (precip) {

View File

@@ -25,14 +25,20 @@ WeatherProvider.register("weatherflow", {
const currentWeather = new WeatherObject();
currentWeather.date = moment();
// Other available values: air_density, brightness, delta_t, dew_point,
// pressure_trend (i.e. rising/falling), sea_level_pressure, wind gust, and more.
currentWeather.humidity = data.current_conditions.relative_humidity;
currentWeather.temperature = data.current_conditions.air_temperature;
currentWeather.feelsLikeTemp = data.current_conditions.feels_like;
currentWeather.windSpeed = WeatherUtils.convertWindToMs(data.current_conditions.wind_avg);
currentWeather.windFromDirection = data.current_conditions.wind_direction;
currentWeather.weatherType = data.forecast.daily[0].icon;
currentWeather.weatherType = this.convertWeatherType(data.current_conditions.icon);
currentWeather.uv_index = data.current_conditions.uv;
currentWeather.sunrise = moment.unix(data.forecast.daily[0].sunrise);
currentWeather.sunset = moment.unix(data.forecast.daily[0].sunset);
this.setCurrentWeather(currentWeather);
this.fetchedLocationName = data.location_name;
})
.catch(function (request) {
Log.error("Could not load data ... ", request);
@@ -52,13 +58,27 @@ WeatherProvider.register("weatherflow", {
weather.minTemperature = forecast.air_temp_low;
weather.maxTemperature = forecast.air_temp_high;
weather.precipitationProbability = forecast.precip_probability;
weather.weatherType = forecast.icon;
weather.snow = 0;
weather.weatherType = this.convertWeatherType(forecast.icon);
// Must manually build UV and Precipitation from hourly
weather.precipitationAmount = 0.0; // This will sum up rain and snow
weather.precipitationUnits = "mm";
weather.uv_index = 0;
for (const hour of data.forecast.hourly) {
const hour_time = moment.unix(hour.time);
if (hour_time.day() === weather.date.day()) { // Iterate though until day is reached
// Get data from today
weather.uv_index = Math.max(weather.uv_index, hour.uv);
weather.precipitationAmount += (hour.precip ?? 0);
} else if (hour_time.diff(weather.date) >= 86400) {
break; // No more data to be found
}
}
days.push(weather);
}
this.setWeatherForecast(days);
this.fetchedLocationName = data.location_name;
})
.catch(function (request) {
Log.error("Could not load data ... ", request);
@@ -66,6 +86,63 @@ WeatherProvider.register("weatherflow", {
.finally(() => this.updateAvailable());
},
fetchWeatherHourly () {
this.fetchData(this.getUrl())
.then((data) => {
const hours = [];
for (const hour of data.forecast.hourly) {
const weather = new WeatherObject();
weather.date = moment.unix(hour.time);
weather.temperature = hour.air_temperature;
weather.feelsLikeTemp = hour.feels_like;
weather.humidity = hour.relative_humidity;
weather.windSpeed = hour.wind_avg;
weather.windFromDirection = hour.wind_direction;
weather.weatherType = this.convertWeatherType(hour.icon);
weather.precipitationProbability = hour.precip_probability;
weather.precipitationAmount = hour.precip; // NOTE: precipitation type is available
weather.precipitationUnits = "mm"; // Hardcoded via request, TODO: Add conversion
weather.uv_index = hour.uv;
hours.push(weather);
if (hours.length >= 48) break; // 10 days of hours are available, best to trim down.
}
this.setWeatherHourly(hours);
this.fetchedLocationName = data.location_name;
})
.catch(function (request) {
Log.error("Could not load data ... ", request);
})
.finally(() => this.updateAvailable());
},
convertWeatherType (weatherType) {
const weatherTypes = {
"clear-day": "day-sunny",
"clear-night": "night-clear",
cloudy: "cloudy",
foggy: "fog",
"partly-cloudy-day": "day-cloudy",
"partly-cloudy-night": "night-alt-cloudy",
"possibly-rainy-day": "day-rain",
"possibly-rainy-night": "night-alt-rain",
"possibly-sleet-day": "day-sleet",
"possibly-sleet-night": "night-alt-sleet",
"possibly-snow-day": "day-snow",
"possibly-snow-night": "night-alt-snow",
"possibly-thunderstorm-day": "day-thunderstorm",
"possibly-thunderstorm-night": "night-alt-thunderstorm",
rainy: "rain",
sleet: "sleet",
snow: "snow",
thunderstorm: "thunderstorm",
windy: "strong-wind"
};
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
},
// Create a URL from the config and base URL.
getUrl () {
return `${this.config.apiBase}better_forecast?station_id=${this.config.stationid}&units_temp=c&units_wind=kph&units_pressure=mb&units_precip=mm&units_distance=km&token=${this.config.token}`;

View File

@@ -23,6 +23,10 @@ WeatherProvider.register("yr", {
Log.error("The Yr weather provider requires local storage.");
throw new Error("Local storage not available");
}
if (this.config.updateInterval < 600000) {
Log.warn("The Yr weather provider requires a minimum update interval of 10 minutes (600 000 ms). The configuration has been adjusted to meet this requirement.");
this.delegate.config.updateInterval = 600000;
}
Log.info(`Weather provider: ${this.providerName} started.`);
},
@@ -34,7 +38,7 @@ WeatherProvider.register("yr", {
})
.catch((error) => {
Log.error(error);
throw new Error(error);
this.updateAvailable();
});
},
@@ -119,7 +123,12 @@ WeatherProvider.register("yr", {
})
.catch((err) => {
Log.error(err);
reject("Unable to get weather data from Yr.");
if (weatherData) {
Log.warn("Using outdated cached weather data.");
resolve(weatherData);
} else {
reject("Unable to get weather data from Yr.");
}
})
.finally(() => {
localStorage.removeItem("yrIsFetchingWeatherData");
@@ -497,7 +506,7 @@ WeatherProvider.register("yr", {
})
.catch((error) => {
Log.error(error);
throw new Error(error);
this.updateAvailable();
});
},
@@ -530,7 +539,15 @@ WeatherProvider.register("yr", {
getHourlyForecastFrom (weatherData) {
const series = [];
const now = moment({
year: moment().year(),
month: moment().month(),
day: moment().date(),
hour: moment().hour()
});
for (const forecast of weatherData.properties.timeseries) {
if (now.isAfter(moment(forecast.time))) continue;
forecast.symbol = forecast.data.next_1_hours?.summary?.symbol_code;
forecast.precipitationAmount = forecast.data.next_1_hours?.details?.precipitation_amount;
forecast.precipitationProbability = forecast.data.next_1_hours?.details?.probability_of_precipitation;
@@ -600,7 +617,7 @@ WeatherProvider.register("yr", {
})
.catch((error) => {
Log.error(error);
throw new Error(error);
this.updateAvailable();
});
}
});

View File

@@ -31,6 +31,7 @@
.weather .precipitation-amount,
.weather .precipitation-prob,
.weather .humidity-hourly,
.weather .uv-index {
padding-left: 20px;
padding-right: 0;

View File

@@ -14,7 +14,8 @@ Module.register("weather", {
updateInterval: 10 * 60 * 1000, // every 10 minutes
animationSpeed: 1000,
showFeelsLike: true,
showHumidity: "none", // this is now a string; see current.njk
showHumidity: "none", // possible options for "current" weather are "none", "wind", "temp", "feelslike" or "below", for "hourly" weather "none" or "true"
hideZeroes: false, // hide zeroes (and empty columns) in hourly, currently only for precipitation
showIndoorHumidity: false,
showIndoorTemperature: false,
allowOverrideNotification: false,

View File

@@ -119,7 +119,7 @@ const WeatherProvider = Class.extend({
return JSON.parse(data);
}
const useCorsProxy = typeof this.config.useCorsProxy !== "undefined" && this.config.useCorsProxy;
return performWebRequest(url, type, useCorsProxy, requestHeaders, expectedResponseHeaders);
return performWebRequest(url, type, useCorsProxy, requestHeaders, expectedResponseHeaders, config.basePath);
}
});

View File

@@ -128,14 +128,14 @@ const WeatherUtils = {
} else if (tempInF > 80 && humidity > 40) {
feelsLike
= -42.379
+ 2.04901523 * tempInF
+ 10.14333127 * humidity
- 0.22475541 * tempInF * humidity
- 6.83783 * Math.pow(10, -3) * tempInF * tempInF
- 5.481717 * Math.pow(10, -2) * humidity * humidity
+ 1.22874 * Math.pow(10, -3) * tempInF * tempInF * humidity
+ 8.5282 * Math.pow(10, -4) * tempInF * humidity * humidity
- 1.99 * Math.pow(10, -6) * tempInF * tempInF * humidity * humidity;
+ 2.04901523 * tempInF
+ 10.14333127 * humidity
- 0.22475541 * tempInF * humidity
- 6.83783 * Math.pow(10, -3) * tempInF * tempInF
- 5.481717 * Math.pow(10, -2) * humidity * humidity
+ 1.22874 * Math.pow(10, -3) * tempInF * tempInF * humidity
+ 8.5282 * Math.pow(10, -4) * tempInF * humidity * humidity
- 1.99 * Math.pow(10, -6) * tempInF * tempInF * humidity * humidity;
}
return ((feelsLike - 32) * 5) / 9;

8528
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "magicmirror",
"version": "2.29.0",
"version": "2.32.0",
"description": "The open source modular smart mirror platform.",
"keywords": [
"magic mirror",
@@ -22,32 +22,38 @@
"contributors": [
"https://github.com/MagicMirrorOrg/MagicMirror/graphs/contributors"
],
"type": "commonjs",
"main": "js/electron.js",
"scripts": {
"start": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js",
"start:dev": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js dev",
"server": "node ./serveronly",
"config:check": "node js/check_config.js",
"install-mm": "npm install --no-audit --no-fund --no-update-notifier --only=prod --omit=dev",
"install-mm:dev": "npm install --no-audit --no-fund --no-update-notifier",
"install-vendor": "echo \"Installing vendor files ...\n\" && cd vendor && npm install --loglevel=error --no-audit --no-fund --no-update-notifier",
"install-fonts": "echo \"Installing fonts ...\n\" && cd fonts && npm install --loglevel=error --no-audit --no-fund --no-update-notifier",
"postinstall": "npm run install-vendor && npm run install-fonts && echo \"MagicMirror² installation finished successfully! \n\"",
"test": "NODE_ENV=test jest -i --forceExit",
"test:coverage": "NODE_ENV=test jest --coverage -i --verbose false --forceExit",
"test:electron": "NODE_ENV=test jest --selectProjects electron -i --forceExit",
"test:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit",
"test:unit": "NODE_ENV=test jest --selectProjects unit",
"test:prettier": "prettier . --check",
"test:js": "eslint .",
"test:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json",
"test:calendar": "node ./modules/default/calendar/debug.js",
"test:spelling": "cspell . --gitignore",
"config:check": "node js/check_config.js",
"lint:css": "stylelint 'css/main.css' 'css/roboto.css' 'css/font-awesome.css' 'modules/default/**/*.css' --fix",
"lint:js": "eslint --fix",
"lint:markdown": "markdownlint-cli2 . --fix",
"lint:prettier": "prettier . --write",
"lint:js": "eslint . --fix",
"lint:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json --fix",
"lint:staged": "lint-staged",
"prepare": "[ -f node_modules/.bin/husky ] && husky || echo no husky installed."
"postinstall": "git clean -df fonts vendor",
"prepare": "[ -f node_modules/.bin/husky ] && husky || echo no husky installed.",
"server": "node ./serveronly",
"start": "node --run start:x11",
"start:dev": "node --run start:x11 -- dev",
"start:wayland": "WAYLAND_DISPLAY=\"${WAYLAND_DISPLAY:=wayland-1}\" ./node_modules/.bin/electron js/electron.js --enable-features=UseOzonePlatform --ozone-platform=wayland",
"start:wayland:dev": "node --run start:wayland -- dev",
"start:windows": ".\\node_modules\\.bin\\electron js\\electron.js",
"start:windows:dev": "node --run start:windows -- dev",
"start:x11": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js",
"start:x11:dev": "node --run start:x11 -- dev",
"test": "NODE_ENV=test jest -i --forceExit",
"test:calendar": "node ./modules/default/calendar/debug.js",
"test:coverage": "NODE_ENV=test jest --coverage -i --verbose false --forceExit",
"test:css": "stylelint 'css/main.css' 'css/roboto.css' 'css/font-awesome.css' 'modules/default/**/*.css'",
"test:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit",
"test:electron": "NODE_ENV=test jest --selectProjects electron -i --forceExit",
"test:js": "eslint",
"test:markdown": "markdownlint-cli2 .",
"test:prettier": "prettier . --check",
"test:spelling": "cspell . --gitignore",
"test:unit": "NODE_ENV=test jest --selectProjects unit"
},
"lint-staged": {
"*": "prettier --write",
@@ -55,49 +61,58 @@
"*.css": "stylelint --fix"
},
"dependencies": {
"@fontsource/roboto": "^5.2.6",
"@fontsource/roboto-condensed": "^5.2.6",
"@fortawesome/fontawesome-free": "^6.7.2",
"ajv": "^8.17.1",
"ansis": "^3.3.2",
"animate.css": "^4.1.1",
"console-stamp": "^3.1.2",
"croner": "^9.1.0",
"envsub": "^4.1.0",
"eslint": "^9.11.1",
"express": "^4.21.0",
"eslint": "^9.30.0",
"express": "^5.1.0",
"express-ipfilter": "^1.3.2",
"feedme": "^2.0.2",
"helmet": "^7.1.0",
"helmet": "^8.1.0",
"html-to-text": "^9.0.5",
"iconv-lite": "^0.6.3",
"module-alias": "^2.2.3",
"moment": "^2.30.1",
"node-ical": "0.18.0",
"pm2": "^5.4.2",
"socket.io": "^4.8.0",
"moment-timezone": "^0.6.0",
"node-ical": "^0.20.1",
"nunjucks": "^3.2.4",
"pm2": "^6.0.8",
"socket.io": "^4.8.1",
"suncalc": "^1.9.0",
"systeminformation": "^5.23.5"
"systeminformation": "^5.27.7",
"undici": "^7.11.0",
"weathericons": "^2.1.0"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@stylistic/eslint-plugin": "^2.8.0",
"cspell": "^8.14.4",
"eslint-plugin-jest": "^28.8.3",
"eslint-plugin-jsdoc": "^50.3.0",
"eslint-plugin-package-json": "^0.15.3",
"@stylistic/eslint-plugin": "^5.1.0",
"cspell": "^9.1.2",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-jsdoc": "^51.2.3",
"eslint-plugin-package-json": "^0.42.0",
"express-basic-auth": "^1.2.1",
"husky": "^9.1.6",
"jest": "^29.7.0",
"jsdom": "^25.0.1",
"lint-staged": "^15.2.10",
"playwright": "^1.47.2",
"prettier": "^3.3.3",
"sinon": "^19.0.2",
"stylelint": "^16.9.0",
"stylelint-config-standard": "^36.0.1",
"stylelint-prettier": "^5.0.2"
"husky": "^9.1.7",
"jest": "^30.0.3",
"jsdom": "^26.1.0",
"lint-staged": "^16.1.2",
"markdownlint-cli2": "^0.18.1",
"playwright": "^1.53.1",
"prettier": "^3.6.2",
"sinon": "^21.0.0",
"stylelint": "^16.21.0",
"stylelint-config-standard": "^38.0.0",
"stylelint-prettier": "^5.0.3"
},
"optionalDependencies": {
"electron": "^31.6.0"
"electron": "^36.6.0"
},
"engines": {
"node": ">=20.9.0 <21 || 22"
"node": ">=22.14.0"
},
"_moduleAliases": {
"node_helper": "js/node_helper.js",

13
prettier.config.mjs Normal file
View File

@@ -0,0 +1,13 @@
const config = {
overrides: [
{
files: "*.md",
options: {
parser: "markdown"
}
}
],
trailingComma: "none"
};
export default config;

View File

@@ -4,5 +4,5 @@ const Log = require("../js/logger");
app.start().then((config) => {
const bindAddress = config.address ? config.address : "localhost";
const httpType = config.useHttps ? "https" : "http";
Log.info(`\n>>> Ready to go! Please point your browser to: ${httpType}://${bindAddress}:${config.port} <<<`);
Log.info(`\n>>> Ready to go! Please point your browser to: ${httpType}://${bindAddress}:${global.mmPort || config.port} <<<`);
});

7
stylelint.config.mjs Normal file
View File

@@ -0,0 +1,7 @@
const config = {
extends: ["stylelint-config-standard", "stylelint-prettier/recommended"],
root: true,
rules: {}
};
export default config;

View File

@@ -1,4 +1,6 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
modules:
// Using exotic content. This is why don't accept go to JSON configuration file
(() => {

View File

@@ -1,16 +1,19 @@
exports.configFactory = (options) => {
return Object.assign(
{
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false
}
},
if (typeof exports === "object") {
// running in nodejs (not in browser)
exports.configFactory = (options) => {
return Object.assign(
{
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false
}
},
modules: []
},
options
);
};
modules: []
},
options
);
};
}

View File

@@ -1,4 +1,6 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
modules: [
{
module: "alert",

View File

@@ -0,0 +1,35 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
units: "metric",
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
hideDuplicates: false,
maximumEntries: 100,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
maximumNumberOfDays: 28,
calendars: [
{
maximumEntries: 100,
url: "http://localhost:8080/tests/mocks/3_move_first_allday_repeating_event.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -1,4 +1,6 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 12,
modules: [

View File

@@ -1,4 +1,6 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 12,
logLevel: ["INFO", "LOG", "WARN", "ERROR", "DEBUG"],
modules: [

View File

@@ -1,4 +1,6 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 12,
modules: [

View File

@@ -0,0 +1,33 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
maximumNumberOfDays: 28,
showEnd: true,
calendars: [
{
maximumEntries: 100,
url: "http://localhost:8080/tests/mocks/end_of_day_berlin_moved.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,33 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
maximumNumberOfDays: 28,
showEnd: true,
calendars: [
{
maximumEntries: 100,
url: "http://localhost:8080/tests/mocks/RepeatingEvent.Oct21.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,33 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
maximumNumberOfDays: 28,
showEnd: true,
calendars: [
{
maximumEntries: 100,
url: "http://localhost:8080/tests/mocks/whole_day_moved_over_dst_change_berlin.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -1,4 +1,6 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 12,
modules: [

View File

@@ -0,0 +1,33 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
maximumNumberOfDays: 28,
showEnd: true,
calendars: [
{
maximumEntries: 100,
url: "http://localhost:8080/tests/mocks/chicago-nyedge.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,33 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
maximumNumberOfDays: 20,
calendars: [
{
maximumEntries: 100,
//url: "http://localhost:8080/tests/mocks/chicago_late_in_timezone.ics"
url: "http://localhost:8080/tests/mocks/chicago_late_in_timezone.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,39 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 12,
foreignModulesDir: "tests/mocks",
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
maximumEntries: 1,
calendars: [
{
fetchInterval: 10000, //7 * 24 * 60 * 60 * 1000,
symbol: ["calendar-check", "google"],
url: "http://localhost:8080/tests/mocks/12_events.ics"
}
]
}
},
{
module: "testNotification",
position: "bottom_bar",
config: {
debug: true,
match: {
matchtype: "count",
notificationID: "CALENDAR_EVENTS"
}
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -1,4 +1,6 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 12,
modules: [
@@ -11,6 +13,8 @@ let config = {
calendars: [
{
maximumEntries: 5,
pastDaysCount: 5,
broadcastPastEvents: true,
maximumNumberOfDays: 10000,
symbol: "birthday-cake",
fullDaySymbol: "calendar-day",

View File

@@ -1,4 +1,6 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 12,
modules: [

View File

@@ -0,0 +1,34 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
dateEndFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
maximumNumberOfDays: 28,
showEnd: true,
calendars: [
{
maximumEntries: 100,
url: "http://localhost:8080/tests/mocks/diff_tz_start_end.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,33 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
maximumNumberOfDays: 28,
showEnd: true,
calendars: [
{
maximumEntries: 100,
url: "http://localhost:8080/tests/mocks/end_of_day_berlin_moved.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,33 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
dateEndFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
showEnd: true,
calendars: [
{
maximumEntries: 100,
url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_non_repeating.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,34 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
dateEndFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
showEnd: true,
showEndsOnlyWithDuration: true,
calendars: [
{
maximumEntries: 100,
url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_non_repeating.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,33 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
maximumNumberOfDays: 28,
showEnd: true,
calendars: [
{
maximumEntries: 100,
url: "http://localhost:8080/tests/mocks/exdate_and_recurrence_together.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -8,6 +8,8 @@
* See tests/electron/modules/calendar_spec.js
*/
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 12,
modules: [
@@ -28,10 +30,6 @@ let config = {
]
};
Date.now = () => {
return new Date("19 Oct 2023 12:30:00 GMT-07:00").valueOf();
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;

View File

@@ -8,6 +8,8 @@
* See tests/electron/modules/calendar_spec.js
*/
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 12,
modules: [
@@ -28,10 +30,6 @@ let config = {
]
};
Date.now = () => {
return new Date("19 Oct 2023 12:30:00 GMT-07:00").valueOf();
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;

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