mirror of
https://github.com/MagicMirrorOrg/MagicMirror.git
synced 2026-03-16 03:41:32 +08:00
Compare commits
220 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b742e839be | ||
|
|
b0c5924019 | ||
|
|
62b0f7f26e | ||
|
|
8e0b8468d3 | ||
|
|
39a614e0de | ||
|
|
9c9a5359dd | ||
|
|
c24de64d77 | ||
|
|
94c3c699e8 | ||
|
|
53fc814ff8 | ||
|
|
5ea8a3469a | ||
|
|
118e21238c | ||
|
|
d20d9a7ef8 | ||
|
|
b8e0e2a791 | ||
|
|
a927eb20d9 | ||
|
|
aad3eefc62 | ||
|
|
ee1960ced0 | ||
|
|
0b70274a1a | ||
|
|
4e7b68a69d | ||
|
|
786ea86e0e | ||
|
|
d397568062 | ||
|
|
a7af76b619 | ||
|
|
319a921f75 | ||
|
|
d5406f4900 | ||
|
|
55cd03576f | ||
|
|
9d97724401 | ||
|
|
74854387cd | ||
|
|
e77f10b86d | ||
|
|
6ffdc7b55b | ||
|
|
7098f1e41f | ||
|
|
679a413788 | ||
|
|
247115d2e4 | ||
|
|
70ddd80632 | ||
|
|
203e8647d4 | ||
|
|
3fe5ad4b3d | ||
|
|
b300191609 | ||
|
|
296df06c21 | ||
|
|
fe882bf92a | ||
|
|
2a6e2aacdc | ||
|
|
3a01acd389 | ||
|
|
a8d06ae74e | ||
|
|
04f0df269a | ||
|
|
f80889d953 | ||
|
|
6815dfa02b | ||
|
|
bbc27f5ae2 | ||
|
|
f46b226940 | ||
|
|
764ca3ac5c | ||
|
|
0e2da630d5 | ||
|
|
a0b444d6c4 | ||
|
|
5d2ddbd3dd | ||
|
|
b067711ede | ||
|
|
66b29ec26e | ||
|
|
6ea94e4512 | ||
|
|
290b350856 | ||
|
|
9566d6c9a0 | ||
|
|
6b204cda25 | ||
|
|
e530c783f8 | ||
|
|
a3c2e7b816 | ||
|
|
ad665a7a33 | ||
|
|
95ec3096e0 | ||
|
|
a67a0b677c | ||
|
|
8b1c279c07 | ||
|
|
4eccce3f77 | ||
|
|
af0fe37f70 | ||
|
|
e5adbea49c | ||
|
|
7127979c6f | ||
|
|
fa7c7fc8cf | ||
|
|
91fd931a58 | ||
|
|
7a1591b2d6 | ||
|
|
f2957f90df | ||
|
|
ffdf321e23 | ||
|
|
79e99e18ea | ||
|
|
a92b3d3f71 | ||
|
|
5cbdd28db3 | ||
|
|
9d49196e69 | ||
|
|
ef20fe2d11 | ||
|
|
c0a5f35a00 | ||
|
|
2ad463b6c7 | ||
|
|
200db181d5 | ||
|
|
7ba96aeb98 | ||
|
|
7c64d8fce6 | ||
|
|
7dcea98e99 | ||
|
|
59e9d765e2 | ||
|
|
156db32c76 | ||
|
|
58cdfa3cb1 | ||
|
|
49c72d8dfc | ||
|
|
1bd146f52e | ||
|
|
948910d304 | ||
|
|
0b97639341 | ||
|
|
f802c85a38 | ||
|
|
62eb23ba6a | ||
|
|
4b0e0aa48f | ||
|
|
e9f1bd9d7a | ||
|
|
46bca1bc6d | ||
|
|
2b6720e6e5 | ||
|
|
ea818bf899 | ||
|
|
0e00e64493 | ||
|
|
3c35d346ee | ||
|
|
675e4d4f67 | ||
|
|
c1850f2577 | ||
|
|
e985e99036 | ||
|
|
b7371538bc | ||
|
|
a56b92990d | ||
|
|
c7405b76b3 | ||
|
|
eceec8285d | ||
|
|
0573d6e772 | ||
|
|
babd22b04f | ||
|
|
432d900ecd | ||
|
|
83315f1fed | ||
|
|
e09d60d1d1 | ||
|
|
d832d795df | ||
|
|
a41aa48dd1 | ||
|
|
b80485b52f | ||
|
|
7e58b38ddf | ||
|
|
979f4ec664 | ||
|
|
4e3369062e | ||
|
|
77f9c86774 | ||
|
|
dee3cd3da7 | ||
|
|
09f117c3d9 | ||
|
|
32192d1698 | ||
|
|
2c7beeaaaf | ||
|
|
0d3ad9812c | ||
|
|
b7eb21e48f | ||
|
|
9703226c73 | ||
|
|
cc11b77f24 | ||
|
|
c5a8b85f4e | ||
|
|
fa40a3e8e8 | ||
|
|
6223584392 | ||
|
|
b5a22bc09b | ||
|
|
4ef030af5f | ||
|
|
5f38c53260 | ||
|
|
d5395ee3f8 | ||
|
|
ab0876f07a | ||
|
|
d276a7ddb9 | ||
|
|
8f8945d418 | ||
|
|
6d779235cf | ||
|
|
beea754514 | ||
|
|
c6db22524a | ||
|
|
23ee155ded | ||
|
|
1b2785cc56 | ||
|
|
b5b61246e6 | ||
|
|
498b440174 | ||
|
|
fe0b915a5d | ||
|
|
2b792cdbb8 | ||
|
|
a23769156e | ||
|
|
6d86ffade4 | ||
|
|
390e5d6490 | ||
|
|
b08a4737af | ||
|
|
bf28e63709 | ||
|
|
fb22a76796 | ||
|
|
81244d961e | ||
|
|
65aa1b0ddc | ||
|
|
88c7e42368 | ||
|
|
e24dfa6b1a | ||
|
|
a65ee86501 | ||
|
|
4b478a5a5e | ||
|
|
1dc0a0d5b5 | ||
|
|
bf279d9a57 | ||
|
|
42d42ef452 | ||
|
|
ed90f0546f | ||
|
|
a8dc563a31 | ||
|
|
58b9ddcd9f | ||
|
|
7198ae5eae | ||
|
|
f6dcfb5ca3 | ||
|
|
157e74ce7c | ||
|
|
67e4dbaacd | ||
|
|
2e2962d492 | ||
|
|
cd4ba428da | ||
|
|
ee8695637b | ||
|
|
4244c05764 | ||
|
|
c714399b4d | ||
|
|
8d9f132666 | ||
|
|
d2327d3d6f | ||
|
|
29e3ec06cb | ||
|
|
877f8ad380 | ||
|
|
6e80e5a295 | ||
|
|
7bc91a742f | ||
|
|
fc303146a5 | ||
|
|
2908c15ea6 | ||
|
|
a975b44fbb | ||
|
|
4fc38bd5bb | ||
|
|
c99f660d98 | ||
|
|
cd739b6912 | ||
|
|
0ebedd0fb8 | ||
|
|
e9be668d1b | ||
|
|
76d9042e60 | ||
|
|
2fec314ff5 | ||
|
|
3124b0a9c5 | ||
|
|
a2624442cc | ||
|
|
eee289aee8 | ||
|
|
abbae90a8f | ||
|
|
bd0b3c00ad | ||
|
|
b9b7d2c95d | ||
|
|
0b01e9dbe0 | ||
|
|
4fecffc3df | ||
|
|
4d47c0837f | ||
|
|
3879949f58 | ||
|
|
f25abfd2f8 | ||
|
|
7058fc5fd8 | ||
|
|
00bc6eb28c | ||
|
|
f79d3f007d | ||
|
|
c191ff0032 | ||
|
|
dde88601a6 | ||
|
|
2d3940a4ff | ||
|
|
64ed5a54cb | ||
|
|
7bd944391e | ||
|
|
ad4dbd786a | ||
|
|
fc59ed20e3 | ||
|
|
835c893205 | ||
|
|
7bbf8c19db | ||
|
|
1eb2965b2b | ||
|
|
85a9f14178 | ||
|
|
a328ce537f | ||
|
|
21ae79b386 | ||
|
|
d5e855dd6d | ||
|
|
a86e27a12c | ||
|
|
f434be3d44 | ||
|
|
ce4906d13b | ||
|
|
8212d30c4c | ||
|
|
f04d578704 | ||
|
|
7694d6fa86 |
@@ -1 +0,0 @@
|
||||
modules/default/calendar/vendor/*
|
||||
@@ -1,83 +0,0 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:@stylistic/all-extends", "plugin:import/recommended", "plugin:jest/recommended", "plugin:jsdoc/recommended"],
|
||||
"plugins": [],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2023": true,
|
||||
"jest/globals": true,
|
||||
"node": true
|
||||
},
|
||||
"globals": {
|
||||
"config": true,
|
||||
"Log": true,
|
||||
"MM": true,
|
||||
"Module": true,
|
||||
"moment": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2023,
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"eqeqeq": "error",
|
||||
"import/order": "error",
|
||||
"import/extensions": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"jest/consistent-test-it": "warn",
|
||||
"jest/expect-expect": "warn",
|
||||
"jest/no-done-callback": "warn",
|
||||
"jest/prefer-expect-resolves": "warn",
|
||||
"jest/prefer-mock-promise-shorthand": "warn",
|
||||
"jest/prefer-to-be": "warn",
|
||||
"jest/prefer-to-have-length": "warn",
|
||||
"no-param-reassign": "error",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-throw-literal": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "error",
|
||||
"object-shorthand": ["error", "methods"],
|
||||
"prefer-template": "error",
|
||||
"@stylistic/array-element-newline": ["error", "consistent"],
|
||||
"@stylistic/arrow-parens": ["error", "always"],
|
||||
"@stylistic/brace-style": "off",
|
||||
"@stylistic/comma-dangle": ["error", "never"],
|
||||
"@stylistic/dot-location": ["error", "property"],
|
||||
"@stylistic/function-call-argument-newline": ["error", "consistent"],
|
||||
"@stylistic/function-paren-newline": ["error", "consistent"],
|
||||
"@stylistic/implicit-arrow-linebreak": ["error", "beside"],
|
||||
"@stylistic/max-statements-per-line": ["error", { "max": 2 }],
|
||||
"@stylistic/multiline-ternary": ["error", "always-multiline"],
|
||||
"@stylistic/newline-per-chained-call": ["error", { "ignoreChainWithDepth": 4 }],
|
||||
"@stylistic/no-extra-parens": "off",
|
||||
"@stylistic/no-tabs": "off",
|
||||
"@stylistic/object-curly-spacing": ["error", "always"],
|
||||
"@stylistic/object-property-newline": ["error", { "allowAllPropertiesOnSameLine": true }],
|
||||
"@stylistic/operator-linebreak": ["error", "before"],
|
||||
"@stylistic/padded-blocks": "off",
|
||||
"@stylistic/quote-props": ["error", "as-needed"],
|
||||
"@stylistic/quotes": ["error", "double"],
|
||||
"@stylistic/indent": ["error", "tab"],
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
"@stylistic/space-before-function-paren": ["error", "always"],
|
||||
"@stylistic/spaced-comment": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["config/config.js*"],
|
||||
"rules": {
|
||||
"@stylistic/comma-dangle": "off",
|
||||
"@stylistic/indent": "off",
|
||||
"@stylistic/no-multi-spaces": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/configs/modules/weather/*.js"],
|
||||
"rules": {
|
||||
"@stylistic/quotes": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
137
.github/CODE_OF_CONDUCT.md
vendored
Normal file
137
.github/CODE_OF_CONDUCT.md
vendored
Normal 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
|
||||
62
.github/CONTRIBUTING.md
vendored
62
.github/CONTRIBUTING.md
vendored
@@ -6,55 +6,43 @@ We hold our code to standard, and these standards are documented below.
|
||||
|
||||
## Linters
|
||||
|
||||
We use prettier for automatic linting of all our files: `npm run lint:prettier`.
|
||||
We use [prettier](https://prettier.io/) for automatic formatting a lot all our files. The configuration is in our `prettier.config.mjs` file.
|
||||
|
||||
To run prettier, use `node --run lint:prettier`.
|
||||
|
||||
### JavaScript: Run ESLint
|
||||
|
||||
We use [ESLint](https://eslint.org) on our JavaScript files.
|
||||
We use [ESLint](https://eslint.org) to lint our JavaScript files. The configuration is in our `eslint.config.mjs` file.
|
||||
|
||||
Our ESLint configuration is in our `.eslintrc.json` and `.eslintignore` files.
|
||||
|
||||
To run ESLint, use `npm run lint:js`.
|
||||
To run ESLint, use `node --run lint:js`.
|
||||
|
||||
### CSS: Run StyleLint
|
||||
|
||||
We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our `.stylelintrc` file.
|
||||
We use [StyleLint](https://stylelint.io) to lint our CSS. The configuration is in our `stylelint.config.mjs` file.
|
||||
|
||||
To run StyleLint, use `npm run lint:css`.
|
||||
To run StyleLint, use `node --run lint:css`.
|
||||
|
||||
### Markdown: Run markdownlint
|
||||
|
||||
We use [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) to lint our markdown files. The configuration is in our `.markdownlint.json` file.
|
||||
|
||||
To run markdownlint, use `node --run lint:markdown`.
|
||||
|
||||
## Testing
|
||||
|
||||
We use [Jest](https://jestjs.io) for JavaScript testing.
|
||||
We use [Vitest](https://vitest.dev) 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`.
|
||||
The `package.json` scripts expose finer-grained test commands:
|
||||
|
||||
## Submitting Issues
|
||||
- `test:unit` – run unit tests only
|
||||
- `test:e2e` – execute browser-driven end-to-end tests
|
||||
- `test:electron` – launch the Electron-based regression suite
|
||||
- `test:coverage` – collect coverage while running every suite
|
||||
- `test:watch` – keep Vitest in watch mode for fast local feedback
|
||||
- `test:ui` – open the Vitest UI dashboard (needs OS file-watch support enabled)
|
||||
- `test:calendar` – run the legacy calendar debug helper
|
||||
- `test:css`, `test:markdown`, `test:prettier`, `test:spelling`, `test:js` – lint-only scripts that enforce formatting, spelling, markdown style, and ESLint.
|
||||
|
||||
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.
|
||||
You can invoke any script with `node --run <script>` (or `npm run <script>`). Individual files can still be targeted directly, e.g. `npx vitest run tests/e2e/env_spec.js`.
|
||||
|
||||
52
.github/ISSUE_TEMPLATE.md
vendored
52
.github/ISSUE_TEMPLATE.md
vendored
@@ -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
154
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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.
|
||||
41
.github/ISSUE_TEMPLATE/change_request.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/change_request.yml
vendored
Normal 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
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
||||
67
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
67
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal 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.
|
||||
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,6 +1,6 @@
|
||||
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:**
|
||||
**Please make sure that you have followed these 3 rules before submitting your Pull Request:**
|
||||
|
||||
> 1. Base your pull requests against the `develop` branch.
|
||||
> 2. Include these infos in the description:
|
||||
@@ -10,10 +10,8 @@ Hello and thank you for wanting to contribute to the MagicMirror² project
|
||||
> - What does the pull request accomplish? Use a list if needed.
|
||||
> - If it includes major visual changes please add screenshots.
|
||||
>
|
||||
> 3. Please run `npm run lint:prettier` before submitting so that
|
||||
> 3. Please run `node --run lint:prettier` before submitting so that
|
||||
> style issues are fixed.
|
||||
> 4. Don't forget to add an entry about your changes to
|
||||
> the CHANGELOG.md file.
|
||||
|
||||
**Note**: Sometimes the development moves very fast. It is highly
|
||||
recommended that you update your branch of `develop` before creating a
|
||||
|
||||
10
.github/codecov.yaml
vendored
10
.github/codecov.yaml
vendored
@@ -1,10 +0,0 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# advanced settings
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
threshold: 0%
|
||||
target: 0
|
||||
17
.github/dependabot.yaml
vendored
17
.github/dependabot.yaml
vendored
@@ -5,21 +5,14 @@ updates:
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "dependencies"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/vendor"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fonts"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "javascript"
|
||||
|
||||
19
.github/stale.yaml
vendored
19
.github/stale.yaml
vendored
@@ -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
|
||||
69
.github/workflows/automated-tests.yaml
vendored
69
.github/workflows/automated-tests.yaml
vendored
@@ -12,30 +12,65 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
code-style-check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x, 21.x]
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Use Node.js ${{ matrix.node-version }}"
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: "Use Node.js"
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: lts/*
|
||||
cache: "npm"
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
npm run install-mm:dev
|
||||
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: [22.21.1, 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@v6
|
||||
- name: "Use Node.js ${{ matrix.node-version }}"
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
check-latest: true
|
||||
cache: "npm"
|
||||
- name: "Install MagicMirror²"
|
||||
run: |
|
||||
node --run install-mm:dev
|
||||
- name: "Install Playwright browsers"
|
||||
run: |
|
||||
npx playwright install --with-deps chromium
|
||||
- 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
|
||||
|
||||
@@ -13,6 +13,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
28
.github/workflows/electron-rebuild.yaml
vendored
Normal file
28
.github/workflows/electron-rebuild.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: "Electron Rebuild Testing"
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
rebuild:
|
||||
name: Run electron-rebuild
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.21.1, 22.x, 24.x]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
- name: "Use Node.js ${{ matrix.node-version }}"
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
check-latest: true
|
||||
- name: Install MagicMirror
|
||||
run: node --run install-mm
|
||||
- name: Install @electron/rebuild
|
||||
run: npm install @electron/rebuild
|
||||
- name: Install test library (serialport) to be rebuilt
|
||||
run: npm install serialport
|
||||
- name: Run electron-rebuild
|
||||
run: npx electron-rebuild
|
||||
continue-on-error: false
|
||||
24
.github/workflows/enforce-pullrequest-rules.yaml
vendored
24
.github/workflows/enforce-pullrequest-rules.yaml
vendored
@@ -1,28 +1,26 @@
|
||||
# This workflow enforces on every pull request:
|
||||
# - the update of our CHANGELOG.md file, see: https://github.com/dangoslen/changelog-enforcer
|
||||
# - that the PR is not based against master, taken from https://github.com/oppia/oppia-android/pull/2832/files
|
||||
# This workflow enforces on every pull request that the PR is not based against master,
|
||||
# taken from https://github.com/oppia/oppia-android/blob/develop/.github/workflows/static_checks.yml
|
||||
|
||||
name: "Enforce Pull-Request Rules"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
push:
|
||||
branches-ignore:
|
||||
- develop
|
||||
- master
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: "Enforce changelog"
|
||||
uses: dangoslen/changelog-enforcer@v3
|
||||
with:
|
||||
changeLogPath: "CHANGELOG.md"
|
||||
skipLabels: "Skip Changelog"
|
||||
- name: "Enforce develop branch"
|
||||
if: ${{ github.base_ref == 'master' && !contains(github.event.pull_request.labels.*.name, 'mastermerge') }}
|
||||
- name: "Branch is not based on develop"
|
||||
if: ${{ github.base_ref != 'develop' && !contains(github.event.pull_request.labels.*.name, 'mastermerge') }}
|
||||
run: |
|
||||
echo "This PR is based against the master branch and not a release or hotfix."
|
||||
echo "Please don't do this. Switch the branch to 'develop'."
|
||||
echo "Current base branch: $BASE_BRANCH"
|
||||
echo "Note: PRs should only ever be merged into develop so please rebase your branch on develop and try again."
|
||||
exit 1
|
||||
env:
|
||||
BASE_BRANCH: ${{ github.base_ref }}
|
||||
|
||||
33
.github/workflows/release-notes.yaml
vendored
Normal file
33
.github/workflows/release-notes.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# This workflow writes a draft release on GitHub named `unreleased` after every push on develop
|
||||
|
||||
name: "Create Release Notes"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release-notes:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: "0"
|
||||
- name: "Use Node.js"
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: "npm"
|
||||
- name: "Create Markdown content"
|
||||
run: |
|
||||
export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
|
||||
node js/releasenotes.js
|
||||
31
.github/workflows/spellcheck.yaml
vendored
Normal file
31
.github/workflows/spellcheck.yaml
vendored
Normal 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@v6
|
||||
with:
|
||||
ref: develop
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
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
22
.github/workflows/stale.yaml
vendored
Normal 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@v10
|
||||
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)"
|
||||
31
.gitignore
vendored
31
.gitignore
vendored
@@ -9,10 +9,7 @@ lib-cov
|
||||
coverage
|
||||
.lock-wscript
|
||||
build/Release
|
||||
/node_modules/**/*
|
||||
fonts/node_modules/**/*
|
||||
vendor/node_modules/**/*
|
||||
!/tests/node_modules/**/*
|
||||
node_modules
|
||||
jspm_modules
|
||||
.npm
|
||||
.node_repl_history
|
||||
@@ -58,17 +55,19 @@ Temporary Items
|
||||
.Trash-*
|
||||
|
||||
# Ignore all modules except the default modules.
|
||||
/modules/**
|
||||
/modules/*
|
||||
!/modules/default
|
||||
!/modules/default/**
|
||||
!/modules/README.md**
|
||||
|
||||
# Ignore changes to the custom css files.
|
||||
/css/custom.css
|
||||
# Ignore changes to the custom css files but keep the sample and main.
|
||||
/css/*
|
||||
!/css/custom.css.sample
|
||||
!/css/font-awesome.css
|
||||
!/css/main.css
|
||||
!/css/roboto.css
|
||||
|
||||
# Ignore users config file but keep the sample.
|
||||
/config/*
|
||||
!/config/config.js.sample
|
||||
config
|
||||
!config/config.js.sample
|
||||
|
||||
# Vim
|
||||
## swap
|
||||
@@ -79,3 +78,13 @@ Temporary Items
|
||||
*.orig
|
||||
*.rej
|
||||
*.bak
|
||||
|
||||
# Ignore positions file (#3518)
|
||||
js/positions.js
|
||||
|
||||
# Ignore lock files other than package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
# Vitest temporary test files
|
||||
tests/**/.tmp/
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ -f "$(dirname "$0")/_/husky.sh" ] && . "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
if command -v npm &> /dev/null; then
|
||||
npm run lint:staged
|
||||
if command -v npx &> /dev/null; then
|
||||
npx lint-staged
|
||||
fi
|
||||
|
||||
6
.markdownlint.json
Normal file
6
.markdownlint.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"line_length": false,
|
||||
"no-duplicate-heading": false,
|
||||
"no-inline-html": false,
|
||||
"no-trailing-punctuation": false
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
*.js
|
||||
*.mjs
|
||||
.husky/pre-commit
|
||||
.prettierignore
|
||||
/config
|
||||
/coverage
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"trailingComma": "none"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": ["stylelint-config-standard"],
|
||||
"plugins": ["stylelint-prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": true
|
||||
}
|
||||
}
|
||||
586
CHANGELOG.md
586
CHANGELOG.md
@@ -1,11 +1,367 @@
|
||||
# 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/).
|
||||
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror².
|
||||
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).
|
||||
|
||||
## [2.26.0] - 01-01-2024
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/#donate) With your help we can continue to improve the MagicMirror².
|
||||
|
||||
## Obsolete
|
||||
|
||||
This file is no longer being updated. Release notes are now automatically generated via a GitHub action.
|
||||
|
||||
## [2.33.0] - 2025-10-01
|
||||
|
||||
Thanks to: @Crazylegstoo, @dathbe, @m-idler, @plebcity, @khassel, @KristjanESPERANTO, @rejas and @sdetweil!
|
||||
|
||||
> ⚠️ This release needs nodejs version `v22.18.0 or higher`
|
||||
|
||||
### Added
|
||||
|
||||
- Add configuration option for `User-Agent`, used by calendar & news module (#3255)
|
||||
- [linter] Add prettier plugin for nunjuck templates (#3887)
|
||||
- [core] Add clear log for occupied port at startup (#3890)
|
||||
|
||||
### Changed
|
||||
|
||||
- [clock] Add CSS to prevent line breaking of sunset/sunrise time display (#3816)
|
||||
- [core] Enhance system information logging format and include additional env and RAM details (#3839, #3843)
|
||||
- [refactor] Add new file `js/module_functions.js` to move code used in several modules to one place (#3837)
|
||||
- [refactor] Use global.root_path where possible and add tests for config:check (#3883, #3885, #3886, #3889)
|
||||
- [tests] refactor: simplify jest config file (#3844)
|
||||
- [tests] refactor: extract constants for weather electron tests (#3845)
|
||||
- [tests] refactor: add `setupDOMEnvironment` helper function to eliminate repetitive JSDOM setup code (#3860)
|
||||
- [tests] replace `console` with `Log` in calendar `debug.js` to avoid exception in eslint config (#3846)
|
||||
- [tests] speed up e2e tests, cleanup and stabilize weather e2e tests, update snapshot url (#3847, #3848, #3861)
|
||||
- [tests] refactor translation tests (#3866)
|
||||
- Remove `sinon` dependency in favor of Jest native mocking
|
||||
- Unify test helper functions across translation test suites
|
||||
- Rename `setupDOMEnvironment` to `createTranslationTestEnvironment` for consistency
|
||||
- Simplify DOM setup by removing unnecessary Promise/async patterns
|
||||
- Avoid potential port conflicts by using port 3001 for translator unit tests
|
||||
- Improve test reliability and maintainability
|
||||
- [tests] add alert module tests for different welcome_message configurations (#3867)
|
||||
- [lint-staged] use `prettier --write --ignore-unknown` in `lint-staged` to avoid errors on unsupported files (#3888)
|
||||
|
||||
### Updated
|
||||
|
||||
- [calendar] Update defaultSymbol name and also the link to the icon search site (#3879)
|
||||
- [core] Update dependencies including electron to v38 as well as github actions (#3831, #3849, #3857, #3858, #3872, #3876, #3882, #3891, #3896)
|
||||
- [weather] Update feels_like temperature calculation formula (#3869)
|
||||
- [weather] Update null value handling for weather type (#3892)
|
||||
- [layout] Update styles for weather and calendar (#3894)
|
||||
|
||||
### Fixed
|
||||
|
||||
- [calendar] Fixed broken unittest that only broke on the 1st of July and 1st of january (#3830)
|
||||
- [clock] Fixed missing icons when no other modules with icons is loaded (#3834)
|
||||
- [weather] Fixed handling of empty values in weathergov providers handling of precipitationAmount (#3859)
|
||||
- [calendar] Fix regression handling of limit days (#3840)
|
||||
- [calendar] Fixed regression of calendarfetcherutils.shouldEventBeExcluded (#3841)
|
||||
- [core] Fixed socket.io timeout when server is slow to send notification, notification lost at client (#3380)
|
||||
- [tests] refactor AnimateCSS tests after jsdom 27 upgrade (#3891)
|
||||
- [weather] Use `apparent_temperature` data from openmeteo's hourly weather for current feelsLikeTemp (#3868).
|
||||
- [weather] Updated envcanada Provider to use new database/URL schema for accessing weather data (#3878).
|
||||
|
||||
## [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.
|
||||
|
||||
> ⚠️ This release needs nodejs version `v20` or `v22`, minimum version is `v20.9.0`
|
||||
|
||||
### Added
|
||||
|
||||
- [compliments] Added support for cron type date/time format entries mm hh DD MM dow (minutes/hours/days/months and day of week) see <https://crontab.cronhub.io> for construction (#3481)
|
||||
- [core] Check config at every start of MagicMirror² (#3450)
|
||||
- [core] Add spelling check (cspell): `npm run test:spelling` and handle spelling issues (#3544)
|
||||
- [core] removed `config.paths.vendor` (could not work because `vendor` is hardcoded in `index.html`), renamed `config.paths.modules` to `config.foreignModulesDir`, added variable `MM_CUSTOMCSS_FILE` which - if set - overrides `config.customCss`, added variable `MM_MODULES_DIR` which - if set - overrides `config.foreignModulesDir`, added test for `MM_MODULES_DIR` (#3530)
|
||||
- [core] elements are now removed from `index.html` when loading script or stylesheet files fails
|
||||
- [core] Added `MODULE_DOM_UPDATED` notification each time the DOM is re-rendered via `updateDom` (#3534)
|
||||
- [tests] added minimal needed node version to tests (currently v20.9.0) to avoid releases with wrong node version info
|
||||
- [tests] Added `node-libgpiod` library to electron-rebuild tests (#3563)
|
||||
|
||||
### Removed
|
||||
|
||||
- [core] removed installer only files (#3492)
|
||||
- [core] removed raspberry object from systeminformation (#3505)
|
||||
- [linter] removed `eslint-plugin-import`, because it doesn't support ESLint v9. We will reenter it later when it does.
|
||||
- [tests] removed `onoff` library from electron-rebuild tests (#3563)
|
||||
|
||||
### Updated
|
||||
|
||||
- [weather] Updated `apiVersion` default from 2.5 to 3.0 (#3424)
|
||||
- [core] Updated dependencies including stylistic-eslint
|
||||
- [core] nail down `node-ical` version to `0.18.0` with exception `allow-ghsas: GHSA-8hc4-vh64-cxmj` in `dep-review.yaml` (which should removed after next `node-ical` update)
|
||||
- [core] Updated SocketIO catch all to new API
|
||||
- [core] Allow custom modules positions by scanning index.html for the defined regions, instead of hard coded (PR #3518 fixes issue #3504)
|
||||
- [core] Detail optimizations in `config_check.js`
|
||||
- [core] Updated minimal needed node version in `package.json` (currently v20.9.0) (#3559) and except for v21 (no security updates) (#3561)
|
||||
- [linter] Switch to ESLint v9 and flat config and replace `eslint-plugin-unicorn` by `@eslint/js`
|
||||
- [core] Fix discovering module positions twice after #3450
|
||||
|
||||
### Fixed
|
||||
|
||||
- [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)
|
||||
- [weather] Fixed issue for respecting unit config on broadcasted notifications
|
||||
- [tests] Fixes calendar test by moving it from e2e to electron with fixed date (#3532)
|
||||
- [calendar] fixed sliceMultiDayEvents getting wrong count and displaying incorrect entries, Europe/Berlin (#3542)
|
||||
- [tests] ignore `js/positions.js` when linting (this file is created at runtime)
|
||||
- [calendar] fixed sliceMultiDayEvents showing previous day without config enabled
|
||||
|
||||
## [2.28.0] - 2024-07-01
|
||||
|
||||
Thanks to: @btoconnor, @bugsounet, @JasonStieber, @khassel, @kleinmantara and @WallysWellies.
|
||||
|
||||
> ⚠️ This release needs nodejs version >= v20.9.0
|
||||
|
||||
### Added
|
||||
|
||||
- [calendar] Added config option "showEndsOnlyWithDuration" for default calendar
|
||||
- [compliments] Added `specialDayUnique` config option, defaults to `false` (#3465)
|
||||
- [weather] Provider weathergov: Use `precipitationLast3Hours` if `precipitationLastHour` is `null` (#3124)
|
||||
|
||||
### Removed
|
||||
|
||||
- [tests] delete node v18 support (#3462)
|
||||
|
||||
### Updated
|
||||
|
||||
- [core] Update dependencies including electron to v31
|
||||
- [core] use node >= v20 (#3462)
|
||||
- [core] Update `config.js.sample` to use openmeteo as weather provider which needs no api key
|
||||
- [tests] Use latest@version of node for `automated-tests.yaml` (#3483)
|
||||
- [updatenotification] Avoid using pm2 when running in docker container
|
||||
|
||||
### Fixed
|
||||
|
||||
- [core] Fixed crash possibility if `module: <name>` is not defined and on `position: <position>` mistake (#3445)
|
||||
- [weather] Fixed precipitationProbability in forecast for provider openmeteo (#3446)
|
||||
- [weather] Fixed type=daily for provider openmeteo having no data when running after 23:00 (#3449)
|
||||
- [weather] Fixed type=daily for provider openmeteo showing nightly icons in forecast when current time is "nightly" (#3458)
|
||||
- [weather] Fixed forecast and hourly weather for provider openmeteo to use real temperatures, not apparent temperatures (#3466)
|
||||
- [tests] Fixed e2e tests running in docker container which needs `address: "0.0.0.0"` (#3479)
|
||||
|
||||
## [2.27.0] - 2024-04-01
|
||||
|
||||
Thanks to: @bugsounet, @crazyscot, @illimarkangur, @jkriegshauser, @khassel, @KristjanESPERANTO, @Paranoid93, @rejas, @sdetweil and @vppencilsharpener.
|
||||
|
||||
This release marks the first release without Michael Teeuw (@michmich). A very special thanks to him for creating MagicMirror and leading the project for so many years.
|
||||
|
||||
For more info, please read the following post: [A New Chapter for MagicMirror: The Community Takes the Lead](https://forum.magicmirror.builders/topic/18329/a-new-chapter-for-magicmirror-the-community-takes-the-lead).
|
||||
|
||||
### Added
|
||||
|
||||
- Output of system information to the console for troubleshooting (#3328 and #3337), ignore errors under aarch64 (#3349)
|
||||
- [linter] Add `eslint-plugin-package-json` to lint the `package.json` files (#3368)
|
||||
- [weather] `showHumidity` config is now a string describing where to show this element. Supported values: "wind", "temp", "feelslike", "below", "none". (#3330)
|
||||
- electron-rebuild test suite for electron and 3rd party modules compatibility (#3392)
|
||||
- Create MM² icon and attach it to electron process (#3407)
|
||||
|
||||
### Updated
|
||||
|
||||
- [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)
|
||||
- Update pm2 to v5.3.1 with no allow-ghsas (#3364)
|
||||
- [core] Update husky and let lint-staged fix ESLint issues
|
||||
- [core] Update dependencies including electron to v29 (#3357) and node-ical
|
||||
- Update translations for estonian (#3371)
|
||||
- Update electron to v29 and update other dependencies
|
||||
- [calendar] fullDay events over several days now show the left days from the first day on and 'today' on the last day
|
||||
- [weather] Update layout of current weather indoor values
|
||||
|
||||
### Fixed
|
||||
|
||||
- [weather] Correct apiBase of weathergov weatherProvider to match documentation (#2926)
|
||||
- Worked around several issues in the RRULE library that were causing deleted calendar events to still show, some
|
||||
initial and recurring events to not show, and some event times to be off an hour. (#3291)
|
||||
- Skip changelog requirement when running tests for dependency updates (#3320)
|
||||
- Display precipitation probability when it is 0% instead of blank/empty (#3345)
|
||||
- [newsfeed] Suppress unsightly animation cases when there are 0 or 1 active news items (#3336)
|
||||
- [newsfeed] Always compute the feed item URL using the same helper function (#3336)
|
||||
- Ignore all custom css files (#3359)
|
||||
- [newsfeed] Fix newsfeed stall issue introduced by #3336 (#3361)
|
||||
- Changed `log.debug` to `log.log` in `app.js` where logLevel is not set because config is not loaded at this time (#3353)
|
||||
- [calendar] deny fetch interval < 60000 and set 60000 in this case (prevent fetch loop failed) (#3382)
|
||||
- added message in case where config.js is missing the module.export line PR #3383
|
||||
- Fixed an issue where recurring events could extend past their recurrence end date (#3393)
|
||||
- Don't display any `npm WARN <....>` on install (#3399)
|
||||
- [core] Moved suncalc dependency to production from dev, as it is used by clock module
|
||||
- [compliments] Fix mirror not responding anymore when no compliments are to be shown (#3385)
|
||||
- [core] Fixed mastermerge workflow (#3415)
|
||||
|
||||
### Deleted
|
||||
|
||||
- Unneeded file headers (#3358)
|
||||
- Removed codecov.yaml
|
||||
|
||||
## [2.26.0] - 2024-01-01
|
||||
|
||||
Thanks to: @bnitkin, @bugsounet, @dependabot, @jkriegshauser, @kaennchenstruggle, @KristjanESPERANTO and @Ybbet.
|
||||
|
||||
@@ -32,7 +388,7 @@ This release also marks the latest release by Michael Teeuw. For more info, plea
|
||||
- Update electron to v27 and update other dependencies as well as github actions
|
||||
- Update newsfeed: Use `html-to-text` instead of regex for transform description
|
||||
- Review ESLint config (#3269)
|
||||
- Updated dependencies
|
||||
- Update dependencies
|
||||
- Clock module: optionally display current moon phase in addition to rise/set times
|
||||
- electron is now per default started without gpu, if needed it must be enabled with new env var `ELECTRON_ENABLE_GPU=1` on startup (#3226)
|
||||
- Replace prettier by stylistic in ESLint config to lint JavaScript (and disable some rules for `config/config.js*` files)
|
||||
@@ -48,8 +404,8 @@ This release also marks the latest release by Michael Teeuw. For more info, plea
|
||||
- Fix issue template (#3167)
|
||||
- Fix #3256 filter out bad results from rrule.between
|
||||
- Fix calendar events sometimes not respecting deleted events (#3250)
|
||||
- Fix electron loadurl locally on Windows when address "0.0.0.0" (#2550)
|
||||
- Fix updatanotification (update_helper.js): catch error if reponse is not an JSON format (check PM2)
|
||||
- Fix electron loadURL locally on Windows when address "0.0.0.0" (#2550)
|
||||
- Fix updatenotification (update_helper.js): catch error if response is not an JSON format (check PM2)
|
||||
- Fix missing typeof in calendar module
|
||||
- Fix style issues after prettier update
|
||||
- Fix calendar test (#3291) by moving "Exdate check" from e2e to electron to run on a Thursday
|
||||
@@ -123,9 +479,9 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
|
||||
- Added UV Index to hourly and current Weather, with support for Openmeteo
|
||||
- Added tests for serveronly
|
||||
- Set Timezone `Europe/Berlin` in unit tests (needed for new formatTime tests)
|
||||
- Added no-param-reassign eslint rule and fix warnings
|
||||
- updatenotification: Added `sendUpdatesNotifications` feature. Broadcast update with `UPDATES` notification to other modules
|
||||
- updatenotification: allow force scanning with `SCAN_UPDATES` notification from other modules
|
||||
- [linter] Added no-param-reassign eslint rule and fix warnings
|
||||
- [updatenotification] Added `sendUpdatesNotifications` feature. Broadcast update with `UPDATES` notification to other modules
|
||||
- [updatenotification] Allow force scanning with `SCAN_UPDATES` notification from other modules
|
||||
- Added per-calendar fetchInterval
|
||||
|
||||
### Removed
|
||||
@@ -134,7 +490,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
|
||||
@@ -307,7 +663,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/defauls.js` which is useful e.g. if you want to embed MagicMirror into annother 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).
|
||||
@@ -356,7 +712,7 @@ Special thanks to the following contributors: @10bias, @CFenner, @JHWelch, @k1rd
|
||||
- Fix minor console output issue for loading translations (#2814).
|
||||
- Don't adjust startDate for full day events if endDate is in the past.
|
||||
- Fix windspeed conversion error in openweathermap provider. (#2812)
|
||||
- Fix conflicting parms turning off showEnd for full day events. (#2629)
|
||||
- Fix conflicting parameter turning off showEnd for full day events. (#2629)
|
||||
- Fix regression, calendar.maximumEntries not used to filter calendar level entries (#2868)
|
||||
|
||||
## [2.18.0] - 2022-01-01
|
||||
@@ -388,7 +744,7 @@ Special thanks to the following contributors: @AmpioRosso, @eouia, @fewieden, @j
|
||||
### Fixed
|
||||
|
||||
- Fixed wrong file `kr.json` to `ko.json`. Use language code 'ko' instead of 'kr' for Korean language.
|
||||
- Fixed `feels_like` data from openweathermaps current weather being ignored (#2678).
|
||||
- [weather] Fixed `feels_like` data from openweathermap's current weather being ignored (#2678).
|
||||
- Fixed chaotic newsfeed display after network connection loss thanks to @jalibu (#2638).
|
||||
- Fixed incorrect time zone correction of recurring full day events (#2632 and #2634).
|
||||
- Fixed e2e tests by increasing testTimeout.
|
||||
@@ -425,8 +781,8 @@ Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khas
|
||||
- Updated github templates.
|
||||
- Actually test all js and css files when lint script is run.
|
||||
- Updated jsdocs and print warnings during testing too.
|
||||
- Updated weathergov provider to try fetching not just current, but also foreacst, when API URLs available.
|
||||
- Refactored clock layout.
|
||||
- Updated weathergov provider to try fetching not just current, but also forecast, when API URLs available.
|
||||
- [clock] Refactored clock layout.
|
||||
- Refactored methods from weather-providers into weatherobject (isDaytime, updateSunTime).
|
||||
- Use of `logger.js` in jest tests.
|
||||
- Run prettier over all relevant files.
|
||||
@@ -652,12 +1008,12 @@ Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura
|
||||
### Fixed
|
||||
|
||||
- Fix backward compatibility issues for Safari < 11.
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast depending on the endpoint (forecast/daily or forecast)". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- Fix calendar display. Account for current timezone. [#2068](https://github.com/MichMich/MagicMirror/issues/2068)
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast depending on the endpoint (forecast/daily or forecast)". [#2018](https://github.com/MagicMirrorOrg/MagicMirror/issues/2018)
|
||||
- Fix calendar display. Account for current timezone. [#2068](https://github.com/MagicMirrorOrg/MagicMirror/issues/2068)
|
||||
- Fix logLevel being set before loading config.
|
||||
- Fix incorrect namespace links in svg clockfaces. [#2072](https://github.com/MichMich/MagicMirror/issues/2072)
|
||||
- Fix weather/providers/weathergov for API guidelines. [#2045](https://github.com/MichMich/MagicMirror/issues/2045)
|
||||
- Fix "undefined" in weather modules header. [#1985](https://github.com/MichMich/MagicMirror/issues/1985)
|
||||
- Fix incorrect namespace links in svg clockfaces. [#2072](https://github.com/MagicMirrorOrg/MagicMirror/issues/2072)
|
||||
- Fix weather/providers/weathergov for API guidelines. [#2045](https://github.com/MagicMirrorOrg/MagicMirror/issues/2045)
|
||||
- Fix "undefined" in weather modules header. [#1985](https://github.com/MagicMirrorOrg/MagicMirror/issues/1985)
|
||||
- Fix #2110, #2111, #2118: Recurring full day events should not use timezone adjustment. Just compare month/day.
|
||||
|
||||
## [2.12.0] - 2020-07-01
|
||||
@@ -691,14 +1047,14 @@ Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryan
|
||||
|
||||
### Fixed
|
||||
|
||||
- The broken modules due to Socket.io change from last release. [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
|
||||
- Add backward compatibility for old module code in socketclient.js. [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
|
||||
- Support multiple instances of calendar module with different config. [#1109](https://github.com/MichMich/MagicMirror/issues/1109)
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- Throw error when check_config fails. [#1928](https://github.com/MichMich/MagicMirror/issues/1928)
|
||||
- Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MichMich/MagicMirror/issues/2050)
|
||||
- Updated ical library to the latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926)
|
||||
- Fix config check after merge of prettier [#2109](https://github.com/MichMich/MagicMirror/issues/2109)
|
||||
- The broken modules due to Socket.io change from last release. [#1973](https://github.com/MagicMirrorOrg/MagicMirror/issues/1973)
|
||||
- Add backward compatibility for old module code in socketclient.js. [#1973](https://github.com/MagicMirrorOrg/MagicMirror/issues/1973)
|
||||
- Support multiple instances of calendar module with different config. [#1109](https://github.com/MagicMirrorOrg/MagicMirror/issues/1109)
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MagicMirrorOrg/MagicMirror/issues/2018)
|
||||
- Throw error when check_config fails. [#1928](https://github.com/MagicMirrorOrg/MagicMirror/issues/1928)
|
||||
- Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MagicMirrorOrg/MagicMirror/issues/2050)
|
||||
- Updated ical library to the latest version. [#1926](https://github.com/MagicMirrorOrg/MagicMirror/issues/1926)
|
||||
- Fix config check after merge of prettier [#2109](https://github.com/MagicMirrorOrg/MagicMirror/issues/2109)
|
||||
|
||||
## [2.11.0] - 2020-04-01
|
||||
|
||||
@@ -706,7 +1062,7 @@ Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryan
|
||||
|
||||
In the past years the project has grown a lot. This came with a huge downside: poor maintainability. If I let the project continue the way it was, it would eventually crash and burn. More important: I would completely lose the drive and interest to continue the project. Because of this the decision was made to simplify the core by removing all side features like automatic installers and support for exotic platforms. This release (2.11.0) is the first real release that will reflect (parts) of these changes. As a result of this, some things might break. So before you continue make sure to backup your installation. Your config, your modules or better yet: your full MagicMirror² folder. In other words: update at your own risk.
|
||||
|
||||
For more information regarding this major change, please check issue [#1860](https://github.com/MichMich/MagicMirror/issues/1860).
|
||||
For more information regarding this major change, please check issue [#1860](https://github.com/MagicMirrorOrg/MagicMirror/issues/1860).
|
||||
|
||||
### Deleted
|
||||
|
||||
@@ -736,8 +1092,8 @@ For more information regarding this major change, please check issue [#1860](htt
|
||||
- Fix calendar time offset for recurring events crossing Daylight Savings Time (ISSUE #1798)
|
||||
- Fix regression in currentweather module causing 'undefined' to show up when config.hideTemp is false
|
||||
- Fix FEELS translation for Croatian
|
||||
- Fixed weather tests [#1840](https://github.com/MichMich/MagicMirror/issues/1840)
|
||||
- Fixed Socket.io can't be used with Reverse Proxy in serveronly mode [#1934](https://github.com/MichMich/MagicMirror/issues/1934)
|
||||
- Fixed weather tests [#1840](https://github.com/MagicMirrorOrg/MagicMirror/issues/1840)
|
||||
- Fixed Socket.io can't be used with Reverse Proxy in serveronly mode [#1934](https://github.com/MagicMirrorOrg/MagicMirror/issues/1934)
|
||||
- Fix update checking skipping 3rd party modules the first time
|
||||
|
||||
### Changed
|
||||
@@ -775,7 +1131,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
### Updated
|
||||
|
||||
- Updated lower bound of `lodash` and `helmet` dependencies for security patches.
|
||||
- Updated compliments.js to handle newline in text, as textfields to not interpolate contents.
|
||||
- Updated compliments.js to handle newline in text, as text fields to not interpolate contents.
|
||||
- Updated raspberry.sh installer script to handle new platform issues, split node/npm, pm2, and screen saver changes.
|
||||
- Improve handling for armv6l devices, where electron support has gone away, add optional serveronly config option.
|
||||
- Improved run-start.sh to handle for serveronly mode, by choice, or when electron not available.
|
||||
@@ -805,14 +1161,14 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
|
||||
- Updatenotification module: Display update notification for a limited (configurable) time.
|
||||
- Enabled e2e/vendor_spec.js tests.
|
||||
- The css/custom.css will be renamed after the next release. We've added into `run-start.sh` an instruction by GIT to ignore with `--skip-worktree` and `rm --cached`. [#1540](https://github.com/MichMich/MagicMirror/issues/1540)
|
||||
- The css/custom.css will be renamed after the next release. We've added into `run-start.sh` an instruction by GIT to ignore with `--skip-worktree` and `rm --cached`. [#1540](https://github.com/MagicMirrorOrg/MagicMirror/issues/1540)
|
||||
- Disable sending of notification CLOCK_SECOND when displaySeconds is false.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Updatenotification module: Properly handle race conditions, prevent crash.
|
||||
- Send `NEWS_FEED` notification also for the first news messages which are shown.
|
||||
- Fixed issue where weather module would not refresh data after a network or API outage. [#1722](https://github.com/MichMich/MagicMirror/issues/1722)
|
||||
- Fixed issue where weather module would not refresh data after a network or API outage. [#1722](https://github.com/MagicMirrorOrg/MagicMirror/issues/1722)
|
||||
- Fixed weatherforecast module not displaying rain amount on fallback endpoint.
|
||||
- Notifications CLOCK_SECOND & CLOCK_MINUTE being from startup instead of matched against the clock and avoid drifting.
|
||||
|
||||
@@ -827,7 +1183,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- Russian translation for “Feels”
|
||||
- Calendar module: added `nextDaysRelative` config option
|
||||
- Add `broadcastPastEvents` config option for calendars to include events from the past `maximumNumberOfDays` in event broadcasts
|
||||
- Added feature to broadcast news feed items `NEWS_FEED` and updated news items `NEWS_FEED_UPDATED` in default [newsfeed](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/newsfeed) module (when news is updated) with documented default and `config.js` options in [README.md](https://github.com/MichMich/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||
- Added feature to broadcast news feed items `NEWS_FEED` and updated news items `NEWS_FEED_UPDATED` in default [newsfeed](https://github.com/MagicMirrorOrg/MagicMirror/tree/develop/modules/default/newsfeed) module (when news is updated) with documented default and `config.js` options in [README.md](https://github.com/MagicMirrorOrg/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||
- Added notifications to default `clock` module broadcasting `CLOCK_SECOND` and `CLOCK_MINUTE` for the respective time elapsed.
|
||||
- Added UK Met Office Datapoint feed as a provider in the default weather module.
|
||||
- Added new provider class
|
||||
@@ -836,7 +1192,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- Use Feels Like temp from feed if present
|
||||
- Optionally display probability of precipitation (PoP) in current weather (UK Met Office data)
|
||||
- Automatically try to fix eslint errors by passing `--fix` option to it
|
||||
- Added sunrise and sunset times to weathergov weather-provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705)
|
||||
- Added sunrise and sunset times to weathergov weather-provider [#1705](https://github.com/MagicMirrorOrg/MagicMirror/issues/1705)
|
||||
- Added "useLocationAsHeader" to display "location" in `config.js` as header when location name is not returned
|
||||
- Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc
|
||||
|
||||
@@ -845,18 +1201,18 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- English translation for "Feels" to "Feels like"
|
||||
- Fixed the example calendar url in `config.js.sample`
|
||||
- Updated `ical.js` to solve various calendar issues.
|
||||
- Updated weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
|
||||
- Updated weather city list url [#1676](https://github.com/MagicMirrorOrg/MagicMirror/issues/1676)
|
||||
- Only update clock once per minute when seconds aren't shown
|
||||
- Updated weather-provider documentation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed uncaught exception, race condition on module update
|
||||
- Fixed issue [#1696](https://github.com/MichMich/MagicMirror/issues/1696), some ical files start date to not parse to date type
|
||||
- Fixed issue [#1696](https://github.com/MagicMirrorOrg/MagicMirror/issues/1696), some ical files start date to not parse to date type
|
||||
- Allowance HTML5 autoplay-policy (policy is changed from Chrome 66 updates)
|
||||
- Handle SIGTERM messages
|
||||
- Fixes sliceMultiDayEvents so it respects maximumNumberOfDays
|
||||
- Minor types in default NewsFeed [README.md](https://github.com/MichMich/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||
- Minor types in default NewsFeed [README.md](https://github.com/MagicMirrorOrg/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||
- Fix typos and small syntax errors, cleanup dependencies, remove multiple-empty-lines, add semi-rule
|
||||
- Fixed issues with calendar not displaying one-time changes to repeating events
|
||||
- Updated the fetchedLocationName variable in currentweather.js so that city shows up in the header
|
||||
@@ -894,41 +1250,41 @@ Fixed `package.json` version number.
|
||||
|
||||
### Updated
|
||||
|
||||
- Bumped the Electron dependency to v3.0.13 to support the most recent Raspbian. [#1500](https://github.com/MichMich/MagicMirror/issues/1500)
|
||||
- Bumped the Electron dependency to v3.0.13 to support the most recent Raspbian. [#1500](https://github.com/MagicMirrorOrg/MagicMirror/issues/1500)
|
||||
- Updated modernizr code in alert module, fixed a small typo there too
|
||||
- More verbose error message on console if the config is malformed
|
||||
- Updated installer script to install Node.js version 10.x
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed temperature displays in currentweather and weatherforecast modules [#1503](https://github.com/MichMich/MagicMirror/issues/1503), [#1511](https://github.com/MichMich/MagicMirror/issues/1511).
|
||||
- Fixed unhandled error on bad git data in updatenotification module [#1285](https://github.com/MichMich/MagicMirror/issues/1285).
|
||||
- Weather forecast now works with openweathermap in new weather module. Daily data are displayed, see issue [#1504](https://github.com/MichMich/MagicMirror/issues/1504).
|
||||
- Fixed temperature displays in currentweather and weatherforecast modules [#1503](https://github.com/MagicMirrorOrg/MagicMirror/issues/1503), [#1511](https://github.com/MagicMirrorOrg/MagicMirror/issues/1511).
|
||||
- Fixed unhandled error on bad git data in updatenotification module [#1285](https://github.com/MagicMirrorOrg/MagicMirror/issues/1285).
|
||||
- Weather forecast now works with openweathermap in new weather module. Daily data are displayed, see issue [#1504](https://github.com/MagicMirrorOrg/MagicMirror/issues/1504).
|
||||
- Fixed analogue clock border display issue where non-black backgrounds used (previous fix for issue 611)
|
||||
- Fixed compatibility issues caused when modules request different versions of Font Awesome, see issue [#1522](https://github.com/MichMich/MagicMirror/issues/1522). MagicMirror² now uses [Font Awesome 5 with v4 shims included for backwards compatibility](https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4#shims).
|
||||
- Fixed compatibility issues caused when modules request different versions of Font Awesome, see issue [#1522](https://github.com/MagicMirrorOrg/MagicMirror/issues/1522). MagicMirror² now uses [Font Awesome 5 with v4 shims included for backwards compatibility](https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4#shims).
|
||||
- Installation script problems with raspbian
|
||||
- Calendar: only show repeating count if the event is actually repeating [#1534](https://github.com/MichMich/MagicMirror/pull/1534)
|
||||
- Calendar: only show repeating count if the event is actually repeating [#1534](https://github.com/MagicMirrorOrg/MagicMirror/pull/1534)
|
||||
- Calendar: Fix exdate handling when multiple values are specified (comma separated)
|
||||
- Calendar: Fix relative date handling for fulldate events, calculate difference always from start of day [#1572](https://github.com/MichMich/MagicMirror/issues/1572)
|
||||
- Calendar: Fix relative date handling for fulldate events, calculate difference always from start of day [#1572](https://github.com/MagicMirrorOrg/MagicMirror/issues/1572)
|
||||
- Fix null dereference in moduleNeedsUpdate when the module isn't visible
|
||||
- Calendar: Fixed event end times by setting default calendarEndTime to "LT" (Local time format). [#1479]
|
||||
- Calendar: Fixed missing calendar fetchers after server process restarts [#1589](https://github.com/MichMich/MagicMirror/issues/1589)
|
||||
- Calendar: Fixed missing calendar fetchers after server process restarts [#1589](https://github.com/MagicMirrorOrg/MagicMirror/issues/1589)
|
||||
- Notification: fixed background color (was white text on white background)
|
||||
- Use getHeader instead of data.header when creating the DOM so overwriting the function also propagates into it
|
||||
- Fix documentation of `useKMPHwind` option in currentweather
|
||||
|
||||
### New weather module
|
||||
|
||||
- Fixed weather forecast table display [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Fixed weather forecast table display [#1499](https://github.com/MagicMirrorOrg/MagicMirror/issues/1499).
|
||||
- Dimmed loading indicator for weather forecast.
|
||||
- Implemented config option `decimalSymbol` [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Aligned indoor values in current weather vertical [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Implemented config option `decimalSymbol` [#1499](https://github.com/MagicMirrorOrg/MagicMirror/issues/1499).
|
||||
- Aligned indoor values in current weather vertical [#1499](https://github.com/MagicMirrorOrg/MagicMirror/issues/1499).
|
||||
- Added humidity support to nunjuck unit filter.
|
||||
- Do not display degree symbol for temperature in Kelvin [#1503](https://github.com/MichMich/MagicMirror/issues/1503).
|
||||
- Weather forecast now works with openweathermap for both, `/forecast` and `/forecast/daily`, in new weather module. If you use the `/forecast`-weatherEndpoint, the hourly data are converted to daily data, see issues [#1504](https://github.com/MichMich/MagicMirror/issues/1504), [#1513](https://github.com/MichMich/MagicMirror/issues/1513).
|
||||
- Added fade, fadePoint and maxNumberOfDays properties to the forecast mode [#1516](https://github.com/MichMich/MagicMirror/issues/1516)
|
||||
- Fixed Loading string and decimalSymbol string replace [#1538](https://github.com/MichMich/MagicMirror/issues/1538)
|
||||
- Show Snow amounts in new weather module [#1545](https://github.com/MichMich/MagicMirror/issues/1545)
|
||||
- Do not display degree symbol for temperature in Kelvin [#1503](https://github.com/MagicMirrorOrg/MagicMirror/issues/1503).
|
||||
- Weather forecast now works with openweathermap for both, `/forecast` and `/forecast/daily`, in new weather module. If you use the `/forecast`-weatherEndpoint, the hourly data are converted to daily data, see issues [#1504](https://github.com/MagicMirrorOrg/MagicMirror/issues/1504), [#1513](https://github.com/MagicMirrorOrg/MagicMirror/issues/1513).
|
||||
- Added fade, fadePoint and maxNumberOfDays properties to the forecast mode [#1516](https://github.com/MagicMirrorOrg/MagicMirror/issues/1516)
|
||||
- Fixed Loading string and decimalSymbol string replace [#1538](https://github.com/MagicMirrorOrg/MagicMirror/issues/1538)
|
||||
- Show Snow amounts in new weather module [#1545](https://github.com/MagicMirrorOrg/MagicMirror/issues/1545)
|
||||
- Added weather.gov as a new weather-provider for US locations
|
||||
|
||||
## [2.6.0] - 2019-01-01
|
||||
@@ -953,7 +1309,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Screenshot for the weather forecast module
|
||||
- Portuguese translation for "Feels"
|
||||
- Croatian translation
|
||||
- Fading for dateheaders timeFormat in Calendar [#1464](https://github.com/MichMich/MagicMirror/issues/1464)
|
||||
- Fading for dateheaders timeFormat in Calendar [#1464](https://github.com/MagicMirrorOrg/MagicMirror/issues/1464)
|
||||
- Documentation for the existing `scale` option in the Weather Forecast module.
|
||||
|
||||
### Fixed
|
||||
@@ -962,7 +1318,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Fixed Polish translation for Single Update Info
|
||||
- Ignore entries with unparseable details in the calendar module
|
||||
- Bug showing FullDayEvents one day too long in calendar fixed
|
||||
- Bug in newsfeed when `removeStartTags` is used on the description [#1478](https://github.com/MichMich/MagicMirror/issues/1478)
|
||||
- Bug in newsfeed when `removeStartTags` is used on the description [#1478](https://github.com/MagicMirrorOrg/MagicMirror/issues/1478)
|
||||
|
||||
### Updated
|
||||
|
||||
@@ -995,8 +1351,8 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Fixed mixup between german and spanish translation for newsfeed.
|
||||
- Fixed close dates to be absolute, if no configured in the config.js - module Calendar
|
||||
- Fixed the updatenotification module message about new commits in the repository, so they can be correctly localized in singular and plural form.
|
||||
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MichMich/MagicMirror/issues/1374)
|
||||
- Fix calendar parsing issue for Midori on RasperryPi Zero w, related to issue #694.
|
||||
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MagicMirrorOrg/MagicMirror/issues/1374)
|
||||
- Fix calendar parsing issue for Midori on Raspberry Pi Zero w, related to issue #694.
|
||||
- Fix weather city ID link in sample config
|
||||
- Fixed issue with clientonly not updating with IP address and port provided on command line.
|
||||
|
||||
@@ -1017,7 +1373,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
## [2.4.0] - 2018-07-01
|
||||
|
||||
⚠️ **Warning:** This release includes an updated version of Electron. This requires a Raspberry Pi configuration change to allow the best performance and prevent the CPU from overheating. Please read the information on the [MagicMirror² Wiki](https://github.com/michmich/magicmirror/wiki/configuring-the-raspberry-pi#enable-the-open-gl-driver-to-decrease-electrons-cpu-usage).
|
||||
⚠️ **Warning:** This release includes an updated version of Electron. This requires a Raspberry Pi configuration change to allow the best performance and prevent the CPU from overheating. Please read the information on the [MagicMirror² Wiki](https://github.com/MagicMirrorOrg/MagicMirror/wiki/configuring-the-raspberry-pi#enable-the-open-gl-driver-to-decrease-electrons-cpu-usage).
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
|
||||
|
||||
@@ -1039,20 +1395,20 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
- Upgrade to Electron 2.0.0.
|
||||
- Remove yarn-or-npm which breaks production builds.
|
||||
- Invoke module suspend even if no dom content. [#1308](https://github.com/MichMich/MagicMirror/issues/1308)
|
||||
- Invoke module suspend even if no dom content. [#1308](https://github.com/MagicMirrorOrg/MagicMirror/issues/1308)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue where wind chill could not be displayed in Fahrenheit. [#1247](https://github.com/MichMich/MagicMirror/issues/1247)
|
||||
- Fixed issues where a module crashes when it tries to dismiss a non existing alert. [#1240](https://github.com/MichMich/MagicMirror/issues/1240)
|
||||
- Fixed issue where wind chill could not be displayed in Fahrenheit. [#1247](https://github.com/MagicMirrorOrg/MagicMirror/issues/1247)
|
||||
- Fixed issues where a module crashes when it tries to dismiss a non existing alert. [#1240](https://github.com/MagicMirrorOrg/MagicMirror/issues/1240)
|
||||
- In default module currentWeather/currentWeather.js line 296, 300, self.config.animationSpeed can not be found because the notificationReceived function does not have "self" variable.
|
||||
- Fixed browser-side code to work on the Midori browser.
|
||||
- Fixed issue where heat index was reporting incorrect values in Celsius and Fahrenheit. [#1263](https://github.com/MichMich/MagicMirror/issues/1263)
|
||||
- Fixed issue where heat index was reporting incorrect values in Celsius and Fahrenheit. [#1263](https://github.com/MagicMirrorOrg/MagicMirror/issues/1263)
|
||||
- Fixed weatherforecast to use dt_txt field instead of dt to handle timezones better
|
||||
- Newsfeed now remembers to show the description when `"ARTICLE_LESS_DETAILS"` is called if the user wants to always show the description. [#1282](https://github.com/MichMich/MagicMirror/issues/1282)
|
||||
- Newsfeed now remembers to show the description when `"ARTICLE_LESS_DETAILS"` is called if the user wants to always show the description. [#1282](https://github.com/MagicMirrorOrg/MagicMirror/issues/1282)
|
||||
- `clientonly/*.js` is now linted, and one linting error is fixed
|
||||
- Fix issue #1196 by changing underscore to hyphen in locale id, in align with momentjs.
|
||||
- Fixed issue where heat index and wind chill were reporting incorrect values in Kelvin. [#1263](https://github.com/MichMich/MagicMirror/issues/1263)
|
||||
- Fix issue #1196 by changing underscore to hyphen in locale id, in align with moment.js.
|
||||
- Fixed issue where heat index and wind chill were reporting incorrect values in Kelvin. [#1263](https://github.com/MagicMirrorOrg/MagicMirror/issues/1263)
|
||||
|
||||
### Updated
|
||||
|
||||
@@ -1064,7 +1420,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
### Fixed
|
||||
|
||||
- Downgrade electron to 1.4.15 to solve the black screen issue.[#1243](https://github.com/MichMich/MagicMirror/issues/1243)
|
||||
- Downgrade electron to 1.4.15 to solve the black screen issue.[#1243](https://github.com/MagicMirrorOrg/MagicMirror/issues/1243)
|
||||
|
||||
## [2.3.0] - 2018-04-01
|
||||
|
||||
@@ -1076,7 +1432,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Implement Danger.js to notify contributors when CHANGELOG.md is missing in PR.
|
||||
- Allow scrolling in full page article view of default newsfeed module with gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures)
|
||||
- Changed 'compliments.js' - Updated DOM if remote compliments are loaded instead of waiting one updateInterval to show custom compliments
|
||||
- Automated unit tests utils, deprecated, translator, cloneObject(lockstrings)
|
||||
- Automated unit tests utils, deprecated, translator, cloneObject(lockStrings)
|
||||
- Automated integration tests translations
|
||||
- Add advanced filtering to the excludedEvents configuration of the default calendar module
|
||||
- New currentweather module config option: `showFeelsLike`: Shows how it actually feels like. (wind chill or heat index)
|
||||
@@ -1180,7 +1536,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
### Fixed
|
||||
|
||||
- Fixed issue with incorrect alignment of analog clock when displayed in the center column of the MM.
|
||||
- Fixed ipWhitelist behaviour to make empty whitelist ([]) allow any and all hosts access to the MM.
|
||||
- Fixed ipWhitelist behavior to make empty whitelist ([]) allow any and all hosts access to the MM.
|
||||
- Fixed issue with calendar module where 'excludedEvents' count towards 'maximumEntries'.
|
||||
- Fixed issue with calendar module where global configuration of maximumEntries was not overridden by calendar specific config (see module doc).
|
||||
- Fixed issue where `this.file(filename)` returns a path with two hashes.
|
||||
@@ -1190,8 +1546,8 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
### Changed
|
||||
|
||||
- Revert Docker related changes in favor of [docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror). All Docker images are outsourced. ([#856](https://github.com/MichMich/MagicMirror/pull/856))
|
||||
- Change Docker base image (Debian + Node) to an arm based distro (AlpineARM + Node) ([#846](https://github.com/MichMich/MagicMirror/pull/846))
|
||||
- Revert Docker related changes in favor of [docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror). All Docker images are outsourced. ([#856](https://github.com/MagicMirrorOrg/MagicMirror/pull/856))
|
||||
- Change Docker base image (Debian + Node) to an arm based distro (AlpineARM + Node) ([#846](https://github.com/MagicMirrorOrg/MagicMirror/pull/846))
|
||||
- Fix the dockerfile to have it running from the first time.
|
||||
|
||||
### Added
|
||||
@@ -1222,7 +1578,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
|
||||
@@ -1238,7 +1594,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add `anytime` group for Compliments module.
|
||||
- Compliments module can use remoteFile without default daytime arrays defined.
|
||||
- Installer: Use init config.js from config.js.sample.
|
||||
- Switched out `rrule` package for `rrule-alt` and fixes in `ical.js` in order to fix calendar issues. ([#565](https://github.com/MichMich/MagicMirror/issues/565))
|
||||
- Switched out `rrule` package for `rrule-alt` and fixes in `ical.js` in order to fix calendar issues. ([#565](https://github.com/MagicMirrorOrg/MagicMirror/issues/565))
|
||||
- Make mouse events pass through the region fullscreen_above to modules below.
|
||||
- Scaled the splash screen down to make it a bit more subtle.
|
||||
- Replace HTML tables with markdown tables in README files.
|
||||
@@ -1251,7 +1607,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
### Added
|
||||
|
||||
- Added Docker support (Pull Request [#673](https://github.com/MichMich/MagicMirror/pull/673)).
|
||||
- Added Docker support (Pull Request [#673](https://github.com/MagicMirrorOrg/MagicMirror/pull/673)).
|
||||
- Calendar-specific support for `maximumEntries`, and `maximumNumberOfDays`.
|
||||
- Add loaded function to modules, providing an async callback.
|
||||
- Made default newsfeed module aware of gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures)
|
||||
@@ -1285,7 +1641,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Added multiple calendar icon support.
|
||||
- Added tests for Translations, dev argument, version, dev console.
|
||||
- Added test anytime feature compliments module.
|
||||
- Added test ipwhitelist configuration directive.
|
||||
- Added test ipWhitelist configuration directive.
|
||||
- Added test for calendar module: default, basic-auth, backward compatibility, fail-basic-auth.
|
||||
- Added meta tags to support fullscreen mode on iOS (for server mode)
|
||||
- Added `ignoreOldItems` and `ignoreOlderThan` options to the News Feed module
|
||||
@@ -1297,7 +1653,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Updated .gitignore to not ignore default modules folder.
|
||||
- Remove white flash on boot up.
|
||||
- Added `update` in Raspberry Pi installation script.
|
||||
- Fix an issue where the analog clock looked scrambled. ([#611](https://github.com/MichMich/MagicMirror/issues/611))
|
||||
- Fix an issue where the analog clock looked scrambled. ([#611](https://github.com/MagicMirrorOrg/MagicMirror/issues/611))
|
||||
- If units are set to imperial, the showRainAmount option of weatherforecast will show the correct unit.
|
||||
- Module currentWeather: check if temperature received from api is defined.
|
||||
- Fix an issue with module hidden status changing to `true` although lock string prevented showing it.
|
||||
@@ -1314,18 +1670,18 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Finnish translation.
|
||||
- Danish translation.
|
||||
- Turkish translation.
|
||||
- Option to limit access to certain IP addresses based on the value of `ipWhitelist` in the `config.js`, default is access from localhost only (Issue [#456](https://github.com/MichMich/MagicMirror/issues/456)).
|
||||
- Option to limit access to certain IP addresses based on the value of `ipWhitelist` in the `config.js`, default is access from localhost only (Issue [#456](https://github.com/MagicMirrorOrg/MagicMirror/issues/456)).
|
||||
- Added ability to change the point of time when calendar events get relative.
|
||||
- Add Splash screen on boot.
|
||||
- Add option to show humidity in currentWeather module.
|
||||
- Add VSCode IntelliSense support.
|
||||
- Module API: Add Visibility locking to module system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#visibility-locking) for more information.
|
||||
- Module API: Method to overwrite the module's header. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#getheader) for more information.
|
||||
- Module API: Option to define the minimum MagicMirror² version to run a module. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#requiresversion) for more information.
|
||||
- Calendar module now broadcasts the event list to all other modules using the notification system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/calendar) for more information.
|
||||
- Possibility to use the calendar feed as the source for the weather (currentweather & weatherforecast) location data. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/weatherforecast) for more information.
|
||||
- Module API: Add Visibility locking to module system. [See documentation](https://github.com/MagicMirrorOrg/MagicMirror/tree/develop/modules#visibility-locking) for more information.
|
||||
- Module API: Method to overwrite the module's header. [See documentation](https://github.com/MagicMirrorOrg/MagicMirror/tree/develop/modules#getheader) for more information.
|
||||
- Module API: Option to define the minimum MagicMirror² version to run a module. [See documentation](https://github.com/MagicMirrorOrg/MagicMirror/tree/develop/modules#requiresversion) for more information.
|
||||
- Calendar module now broadcasts the event list to all other modules using the notification system. [See documentation](https://github.com/MagicMirrorOrg/MagicMirror/tree/develop/modules/default/calendar) for more information.
|
||||
- Possibility to use the calendar feed as the source for the weather (currentweather & weatherforecast) location data. [See documentation](https://github.com/MagicMirrorOrg/MagicMirror/tree/develop/modules/default/weatherforecast) for more information.
|
||||
- Added option to show rain amount in the weatherforecast default module
|
||||
- Add module `updatenotification` to get an update whenever a new version is available. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
|
||||
- Add module `updatenotification` to get an update whenever a new version is available. [See documentation](https://github.com/MagicMirrorOrg/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
|
||||
- Add the ability to set timezone on the date display in the Clock Module
|
||||
- Ability to set date format in calendar module
|
||||
- Possibility to use currentweather for the compliments
|
||||
@@ -1395,11 +1751,11 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
### Fixed
|
||||
|
||||
- Prevent `getModules()` selectors from returning duplicate entries.
|
||||
- Append endpoints of weather modules with `/` to retrieve the correct data. (Issue [#337](https://github.com/MichMich/MagicMirror/issues/337))
|
||||
- Append endpoints of weather modules with `/` to retrieve the correct data. (Issue [#337](https://github.com/MagicMirrorOrg/MagicMirror/issues/337))
|
||||
- Corrected grammar in `module.js` from 'suspend' to 'suspended'.
|
||||
- Fixed openweathermap.org URL in config sample.
|
||||
- Prevent currentweather module from crashing when received data object is incorrect.
|
||||
- Fix issue where translation loading prevented the UI start-up when the language was set to 'en'. (Issue [#388](https://github.com/MichMich/MagicMirror/issues/388))
|
||||
- Fix issue where translation loading prevented the UI start-up when the language was set to 'en'. (Issue [#388](https://github.com/MagicMirrorOrg/MagicMirror/issues/388))
|
||||
|
||||
### Updated
|
||||
|
||||
@@ -1419,8 +1775,8 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
### Fixed
|
||||
|
||||
- Edit Alert Module to display title & message if they are provided in the notification (Issue [#300](https://github.com/MichMich/MagicMirror/issues/300))
|
||||
- Removed 'null' reference from updateModuleContent(). This fixes recent Edge and Internet Explorer browser displays (Issue [#319](https://github.com/MichMich/MagicMirror/issues/319))
|
||||
- Edit Alert Module to display title & message if they are provided in the notification (Issue [#300](https://github.com/MagicMirrorOrg/MagicMirror/issues/300))
|
||||
- Removed 'null' reference from updateModuleContent(). This fixes recent Edge and Internet Explorer browser displays (Issue [#319](https://github.com/MagicMirrorOrg/MagicMirror/issues/319))
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -1437,7 +1793,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
### Fixed
|
||||
|
||||
- Added reference to Italian Translation.
|
||||
- Added the missing NE translation to all languages. [#344](https://github.com/MichMich/MagicMirror/issues/344)
|
||||
- Added the missing NE translation to all languages. [#344](https://github.com/MagicMirrorOrg/MagicMirror/issues/344)
|
||||
- Added proper User-Agent string to calendar call.
|
||||
|
||||
### Changed
|
||||
@@ -1471,3 +1827,53 @@ 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.33.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.32.0...v2.33.0
|
||||
[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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Collaboration
|
||||
|
||||
This document describes how collaborators of this repository should work together.
|
||||
|
||||
## Pull Requests
|
||||
@@ -5,7 +7,7 @@ This document describes how collaborators of this repository should work togethe
|
||||
- never merge your own PR's
|
||||
- never merge without someone having approved (approving and merging from same person is allowed)
|
||||
- wait for all approvals requested (or the author decides something different in the comments)
|
||||
- never merge to `master`, except for releases (because of update notification)
|
||||
- merge to `master` only for releases or other urgent issues (update notification is only triggered by tags)
|
||||
- merges to master should be tagged with the "mastermerge" label so that the test runs through
|
||||
|
||||
## Issues
|
||||
@@ -15,4 +17,55 @@ This document describes how collaborators of this repository should work togethe
|
||||
|
||||
## Releases
|
||||
|
||||
- are done by @MichMich only
|
||||
Are done by
|
||||
|
||||
- [ ] @rejas
|
||||
- [ ] @sdetweil
|
||||
- [ ] @khassel
|
||||
- [ ] @KristjanESPERANTO
|
||||
|
||||
### Pre-Deployment steps
|
||||
|
||||
- [ ] update dependencies (a few days before)
|
||||
|
||||
### Deployment steps
|
||||
|
||||
- [ ] pull latest `develop` branch
|
||||
- [ ] create `prep-release` branch from `develop`
|
||||
- [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0`
|
||||
- [ ] test `prep-release` branch
|
||||
- [ ] 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`
|
||||
- [ ] review the content of the automatically generated draft release named `unreleased`
|
||||
- [ ] check contributor names
|
||||
- [ ] check auto generated min. node version and adjust it for better readability if necessary
|
||||
- [ ] check if all elements are assigned to the correct category
|
||||
- [ ] change release name to `v2.xx.0`
|
||||
- [ ] after successful test run via github actions: create pull request from `develop` to `master` branch
|
||||
- [ ] add label `mastermerge`
|
||||
- [ ] title of the PR is `Release 2.xx.0`
|
||||
- [ ] description of the PR is the body of the draft release with name `v2.xx.0`
|
||||
- [ ] after PR tests run without issues, merge PR
|
||||
- [ ] edit draft release with name `v2.xx.0`
|
||||
- [ ] set corresponding version tag `v2.xx.0` (with `Select tag` and then `Create new tag`)
|
||||
- [ ] update release link in `Compare to previous Release` by replacing `develop` with new tag `v2.xx.0`
|
||||
- [ ] publish the release (button at the bottom)
|
||||
|
||||
### Draft new development release
|
||||
|
||||
- [ ] checkout `develop` branch
|
||||
- [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0-develop`
|
||||
- [ ] 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)
|
||||
- [ ] use a clean environment (e.g. container)
|
||||
- [ ] clone this repository with the new `master` branch and `cd` into the local repository directory
|
||||
- [ ] log in to npm with `npm login --auth-type legacy` which will ask for username and password and one-time-password which is sent via mail
|
||||
- [ ] execute `npm publish`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright © 2016-2022 Michael Teeuw
|
||||
Copyright © 2016-2026 Michael Teeuw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
|
||||
26
README.md
26
README.md
@@ -1,20 +1,22 @@
|
||||

|
||||
# 
|
||||
|
||||
<p style="text-align: center">
|
||||
<a href="https://choosealicense.com/licenses/mit">
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/michmich/magicmirror/automated-tests.yaml" alt="GitHub Actions">
|
||||
<img src="https://img.shields.io/github/checks-status/michmich/magicmirror/master" alt="Build Status">
|
||||
<a href="https://github.com/MichMich/MagicMirror">
|
||||
<img src="https://img.shields.io/github/stars/michmich/magicmirror?style=social">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/magicmirrororg/magicmirror/automated-tests.yaml" alt="GitHub Actions">
|
||||
<img src="https://img.shields.io/github/check-runs/magicmirrororg/magicmirror/master" alt="Build Status">
|
||||
<a href="https://github.com/MagicMirrorOrg/MagicMirror">
|
||||
<img src="https://img.shields.io/github/stars/magicmirrororg/magicmirror?style=social" alt="GitHub Stars">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
|
||||
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MagicMirrorOrg/MagicMirror/graphs/contributors).
|
||||
|
||||
MagicMirror² focuses on a modular plugin system and uses [Electron](https://www.electronjs.org/) as an application wrapper. So no more web server or browser installs necessary!
|
||||
|
||||

|
||||
|
||||
## Documentation
|
||||
|
||||
For the full documentation including **[installation instructions](https://docs.magicmirror.builders/getting-started/installation.html)**, please visit our dedicated documentation website: [https://docs.magicmirror.builders](https://docs.magicmirror.builders).
|
||||
@@ -24,7 +26,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)
|
||||
@@ -41,7 +43,7 @@ For the full contribution guidelines, check out: [https://docs.magicmirror.build
|
||||
|
||||
## Enjoying MagicMirror? Consider a donation!
|
||||
|
||||
MagicMirror² is opensource and free. That doesn't mean we don't need any money.
|
||||
MagicMirror² is Open Source and free. That doesn't mean we don't need any money.
|
||||
|
||||
Please consider a donation to help us cover the ongoing costs like webservers and email services.
|
||||
If we receive enough donations we might even be able to free up some working hours and spend some extra time improving the MagicMirror² core.
|
||||
@@ -49,5 +51,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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +40,7 @@
|
||||
// Return new pending promise
|
||||
return new Promise((resolve, reject) => {
|
||||
// Select http or https module, depending on requested url
|
||||
const lib = url.startsWith("https") ? require("https") : require("http");
|
||||
const lib = url.startsWith("https") ? require("node:https") : require("node:http");
|
||||
const request = lib.get(url, (response) => {
|
||||
let configData = "";
|
||||
|
||||
@@ -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("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) {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
/* MagicMirror² Config Sample
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
/* Config Sample
|
||||
*
|
||||
* For more information on how you can configure this file
|
||||
* see https://docs.magicmirror.builders/configuration/introduction.html
|
||||
@@ -31,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",
|
||||
@@ -70,11 +71,10 @@ let config = {
|
||||
module: "weather",
|
||||
position: "top_right",
|
||||
config: {
|
||||
weatherProvider: "openweathermap",
|
||||
weatherProvider: "openmeteo",
|
||||
type: "current",
|
||||
location: "New York",
|
||||
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
apiKey: "YOUR_OPENWEATHER_API_KEY"
|
||||
lat: 40.776676,
|
||||
lon: -73.971321
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -82,11 +82,10 @@ let config = {
|
||||
position: "top_right",
|
||||
header: "Weather Forecast",
|
||||
config: {
|
||||
weatherProvider: "openweathermap",
|
||||
weatherProvider: "openmeteo",
|
||||
type: "forecast",
|
||||
location: "New York",
|
||||
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
apiKey: "YOUR_OPENWEATHER_API_KEY"
|
||||
lat: 40.776676,
|
||||
lon: -73.971321
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
356
cspell.config.json
Normal file
356
cspell.config.json
Normal file
@@ -0,0 +1,356 @@
|
||||
{
|
||||
"version": "0.2",
|
||||
"language": "en",
|
||||
"words": [
|
||||
"aarch",
|
||||
"Adak",
|
||||
"Alvinger",
|
||||
"Ampio",
|
||||
"andrezibaia",
|
||||
"angeldeejay",
|
||||
"apikey",
|
||||
"apiontek",
|
||||
"armv",
|
||||
"ashishtank",
|
||||
"autoplay",
|
||||
"Autorestart",
|
||||
"beada",
|
||||
"Behaviour",
|
||||
"Binney",
|
||||
"bluemanos",
|
||||
"bnitkin",
|
||||
"bokmål",
|
||||
"bouncyflip",
|
||||
"boxspinner",
|
||||
"Brasileiro",
|
||||
"Brento",
|
||||
"browserwindow",
|
||||
"bryanzzhu",
|
||||
"btoconnor",
|
||||
"bughaver",
|
||||
"bugsounet",
|
||||
"buxxi",
|
||||
"byday",
|
||||
"calcage",
|
||||
"calendarfetcher",
|
||||
"calendarfetcherutils",
|
||||
"calendarutils",
|
||||
"calevents",
|
||||
"chamakura",
|
||||
"Citypage",
|
||||
"cjbrunner",
|
||||
"clearsky",
|
||||
"clientonly",
|
||||
"clockfaces",
|
||||
"cloudcover",
|
||||
"cmdline",
|
||||
"codac",
|
||||
"Codrops",
|
||||
"cornerexpand",
|
||||
"Crazylegstoo",
|
||||
"crazyscot",
|
||||
"Creepin",
|
||||
"currentweather",
|
||||
"CUSTOMCSS",
|
||||
"customregions",
|
||||
"cxmj",
|
||||
"Cymraeg",
|
||||
"dariom",
|
||||
"darksky",
|
||||
"dataheaders",
|
||||
"Datamart",
|
||||
"dateheader",
|
||||
"dateheaders",
|
||||
"datekey",
|
||||
"dathbe",
|
||||
"davide",
|
||||
"DAYAFTERTOMORROW",
|
||||
"DAYBEFOREYESTERDAY",
|
||||
"defaultmodules",
|
||||
"Deificit",
|
||||
"Descr",
|
||||
"dewpoint",
|
||||
"dgoth",
|
||||
"difflink",
|
||||
"dismissttl",
|
||||
"Displayer",
|
||||
"dkallen",
|
||||
"drivelist",
|
||||
"DTEND",
|
||||
"DTSTAMP",
|
||||
"DTSTART",
|
||||
"Duffman",
|
||||
"earlman",
|
||||
"easyas",
|
||||
"eddiehung",
|
||||
"Edgardos",
|
||||
"Ekristoffe",
|
||||
"elec",
|
||||
"elif",
|
||||
"eltociear",
|
||||
"endfor",
|
||||
"endmacro",
|
||||
"envcanada",
|
||||
"envsub",
|
||||
"envsubst",
|
||||
"eouia",
|
||||
"Evapotranspration",
|
||||
"exdate",
|
||||
"exdates",
|
||||
"expectedheaders",
|
||||
"exploader",
|
||||
"ezeholz",
|
||||
"Fadesteps",
|
||||
"Faizan",
|
||||
"feedme",
|
||||
"feelslike",
|
||||
"Fenner",
|
||||
"Feuchte",
|
||||
"fewieden",
|
||||
"fixuppm",
|
||||
"flopp",
|
||||
"fontawesome",
|
||||
"fontface",
|
||||
"forecastweather",
|
||||
"fortawesome",
|
||||
"frameguard",
|
||||
"freezinglevel",
|
||||
"Frysk",
|
||||
"fullarticle",
|
||||
"fulldate",
|
||||
"fullday",
|
||||
"fullscreen",
|
||||
"geraki",
|
||||
"Gevoelstemperatuur",
|
||||
"GHSA",
|
||||
"ghsas",
|
||||
"grenagit",
|
||||
"Halfclear",
|
||||
"heavyrain",
|
||||
"heavyrainandthunder",
|
||||
"heavyrainshowers",
|
||||
"heavyrainshowersandthunder",
|
||||
"heavysleet",
|
||||
"heavysleetshowersandthunder",
|
||||
"heavysnow",
|
||||
"heavysnowandthunder",
|
||||
"Heiko",
|
||||
"Hirschberger",
|
||||
"hourlyweather",
|
||||
"humidex",
|
||||
"Hwind",
|
||||
"ical",
|
||||
"illimarkangur",
|
||||
"Ingan",
|
||||
"ipfilter",
|
||||
"ismarslomic",
|
||||
"jakemulley",
|
||||
"jakobsarwary",
|
||||
"jalibu",
|
||||
"jargordon",
|
||||
"jetson",
|
||||
"jkriegshauser",
|
||||
"jsdocs",
|
||||
"jsonlint",
|
||||
"jupadin",
|
||||
"kaennchenstruggle",
|
||||
"Kalenderwoche",
|
||||
"kenzal",
|
||||
"Keyport",
|
||||
"khassel",
|
||||
"Kingdon",
|
||||
"kioskmode",
|
||||
"klaernie",
|
||||
"kleinmantara",
|
||||
"Kmph",
|
||||
"Knapoc",
|
||||
"Koepke",
|
||||
"kolbyjack",
|
||||
"Komplex",
|
||||
"krekos",
|
||||
"Kristjan",
|
||||
"krukle",
|
||||
"labwc",
|
||||
"Landis",
|
||||
"larryare",
|
||||
"Lastberechnung",
|
||||
"letsencrypt",
|
||||
"libgpiod",
|
||||
"Lightspeed",
|
||||
"loadingcircle",
|
||||
"locationforecast",
|
||||
"lockstring",
|
||||
"lstrip",
|
||||
"Luciella",
|
||||
"luxon",
|
||||
"lxsession",
|
||||
"magicmirror",
|
||||
"martingron",
|
||||
"marvai",
|
||||
"mastermerge",
|
||||
"matchtype",
|
||||
"maxentries",
|
||||
"Meteo",
|
||||
"michaelteeuw",
|
||||
"michmich",
|
||||
"Midori",
|
||||
"mirontoli",
|
||||
"MISSINGLANG",
|
||||
"mixasgr",
|
||||
"MMPM",
|
||||
"modernizr",
|
||||
"modulename",
|
||||
"multiday",
|
||||
"Mystara",
|
||||
"Ñandú",
|
||||
"nathannaveen",
|
||||
"naveensrinivasan",
|
||||
"nbsp",
|
||||
"ndom",
|
||||
"Nerfzooka",
|
||||
"NEWSFEED",
|
||||
"newsfeedfetcher",
|
||||
"newsfetcher",
|
||||
"newsitems",
|
||||
"nfogal",
|
||||
"njwilliams",
|
||||
"nonrepeating",
|
||||
"Norsk",
|
||||
"nunjuck",
|
||||
"odroid",
|
||||
"oemel",
|
||||
"oldconfig",
|
||||
"onecall",
|
||||
"onevent",
|
||||
"openmeteo",
|
||||
"openmeto",
|
||||
"openweathermap",
|
||||
"oraclesean",
|
||||
"oscarb",
|
||||
"pcat",
|
||||
"philnagel",
|
||||
"pirateweather",
|
||||
"plained",
|
||||
"plebcity",
|
||||
"pmax",
|
||||
"pmean",
|
||||
"pmedian",
|
||||
"pmin",
|
||||
"Português",
|
||||
"PRECIP",
|
||||
"Problema",
|
||||
"psieg",
|
||||
"pubdate",
|
||||
"radokristof",
|
||||
"rajniszp",
|
||||
"rebuilded",
|
||||
"Reis",
|
||||
"rejas",
|
||||
"relativehumidity",
|
||||
"Resig",
|
||||
"roboto",
|
||||
"rohitdharavath",
|
||||
"Rosso",
|
||||
"Rothfusz",
|
||||
"rrule",
|
||||
"savvadam",
|
||||
"sdetweil",
|
||||
"searchstr",
|
||||
"sendheaders",
|
||||
"serveronly",
|
||||
"sexualized",
|
||||
"Sitecode",
|
||||
"skpanagiotis",
|
||||
"SMHI",
|
||||
"Snille",
|
||||
"snowandthunder",
|
||||
"snowshowersandthunder",
|
||||
"socketclient",
|
||||
"socketio",
|
||||
"spectron",
|
||||
"Starinvest",
|
||||
"stationid",
|
||||
"STEADMAN",
|
||||
"sthuber",
|
||||
"Stieber",
|
||||
"strinner",
|
||||
"stylelintrc",
|
||||
"sunaction",
|
||||
"suncalc",
|
||||
"suntimes",
|
||||
"symboltest",
|
||||
"systeminformation",
|
||||
"tada",
|
||||
"taglist",
|
||||
"Teeuw",
|
||||
"Teil",
|
||||
"TESTMODE",
|
||||
"thomasrockhu",
|
||||
"thumbslider",
|
||||
"timeformat",
|
||||
"titlereplacestr",
|
||||
"titlesearchstr",
|
||||
"todaytemp",
|
||||
"tomzt",
|
||||
"trunc",
|
||||
"ttlms",
|
||||
"ukmetoffice",
|
||||
"ukmetofficedatahub",
|
||||
"unitless",
|
||||
"unixtime",
|
||||
"unparseable",
|
||||
"updatenotification",
|
||||
"uxdt",
|
||||
"Vaice",
|
||||
"veeck",
|
||||
"verjaardag",
|
||||
"VEVENT",
|
||||
"vgtu",
|
||||
"Vitest",
|
||||
"Voelt",
|
||||
"Vorberechnung",
|
||||
"vppencilsharpener",
|
||||
"Wallys",
|
||||
"Weatherbit",
|
||||
"weathercode",
|
||||
"WEATHERDATA",
|
||||
"Weatherflow",
|
||||
"weatherforecast",
|
||||
"weathergov",
|
||||
"weathericon",
|
||||
"weathericons",
|
||||
"weatherobject",
|
||||
"weatherprovider",
|
||||
"weatherutils",
|
||||
"webcal",
|
||||
"winddirection",
|
||||
"windgusts",
|
||||
"windspeed",
|
||||
"Woolridge",
|
||||
"worktree",
|
||||
"Wsymb",
|
||||
"xlarge",
|
||||
"xmark",
|
||||
"xrandr",
|
||||
"xsmall",
|
||||
"xsorifc",
|
||||
"xwindows",
|
||||
"xxxe",
|
||||
"Ybbet",
|
||||
"yearmatch",
|
||||
"yearmatchgroup"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"css/roboto.css",
|
||||
"node_modules/**",
|
||||
"modules/!(default)/**",
|
||||
"modules/default/**/translations/!(en).json",
|
||||
"modules/default/calendar/windowsZones.json",
|
||||
"modules/default/clock/faces/*.svg",
|
||||
"modules/default/weather/providers/yr.js",
|
||||
"tests/mocks/**",
|
||||
"tests/e2e/modules/clock_es_spec.js",
|
||||
"translations/**"
|
||||
],
|
||||
"dictionaries": ["node"]
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
/* MagicMirror² Custom CSS Sample
|
||||
/* Custom CSS Sample
|
||||
*
|
||||
* Change color and fonts here.
|
||||
*
|
||||
* Beware that properties cannot be unitless, so for example write '--gap-body: 0px;' instead of just '--gap-body: 0;'
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/* Uncomment and adjust accordingly if you want to import another font from the google-fonts-api: */
|
||||
@@ -18,7 +16,7 @@
|
||||
|
||||
--font-primary: "Roboto Condensed";
|
||||
--font-secondary: "Roboto";
|
||||
|
||||
|
||||
--font-size: 20px;
|
||||
--font-size-small: 0.75rem;
|
||||
|
||||
@@ -26,6 +24,6 @@
|
||||
--gap-body-right: 60px;
|
||||
--gap-body-bottom: 60px;
|
||||
--gap-body-left: 60px;
|
||||
|
||||
|
||||
--gap-modules: 30px;
|
||||
}
|
||||
|
||||
25
css/main.css
25
css/main.css
@@ -239,3 +239,28 @@ sup {
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Container Definitions.
|
||||
*/
|
||||
|
||||
.region .container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.region .container.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.region.left .flex {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.region.center .flex {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.region.right .flex {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
151
eslint.config.mjs
Normal file
151
eslint.config.mjs
Normal file
@@ -0,0 +1,151 @@
|
||||
import {defineConfig, globalIgnores} from "eslint/config";
|
||||
import globals from "globals";
|
||||
import {flatConfigs as importX} from "eslint-plugin-import-x";
|
||||
import js from "@eslint/js";
|
||||
import jsdocPlugin from "eslint-plugin-jsdoc";
|
||||
import packageJson from "eslint-plugin-package-json";
|
||||
import playwright from "eslint-plugin-playwright";
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
import vitest from "eslint-plugin-vitest";
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(["config/**", "modules/**/*", "!modules/default/**", "js/positions.js"]),
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...vitest.environments.env.globals,
|
||||
Log: "readonly",
|
||||
MM: "readonly",
|
||||
Module: "readonly",
|
||||
config: "readonly",
|
||||
moment: "readonly"
|
||||
}
|
||||
},
|
||||
plugins: {js, stylistic, vitest},
|
||||
extends: [importX.recommended, vitest.configs.recommended, "js/recommended", jsdocPlugin.configs["flat/recommended"], "stylistic/all"],
|
||||
rules: {
|
||||
"@stylistic/array-element-newline": ["error", "consistent"],
|
||||
"@stylistic/arrow-parens": ["error", "always"],
|
||||
"@stylistic/brace-style": "off",
|
||||
"@stylistic/comma-dangle": ["error", "never"],
|
||||
"@stylistic/dot-location": ["error", "property"],
|
||||
"@stylistic/function-call-argument-newline": ["error", "consistent"],
|
||||
"@stylistic/function-paren-newline": ["error", "consistent"],
|
||||
"@stylistic/implicit-arrow-linebreak": ["error", "beside"],
|
||||
"@stylistic/indent": ["error", "tab"],
|
||||
"@stylistic/max-statements-per-line": ["error", {max: 2}],
|
||||
"@stylistic/multiline-comment-style": "off",
|
||||
"@stylistic/multiline-ternary": ["error", "always-multiline"],
|
||||
"@stylistic/newline-per-chained-call": ["error", {ignoreChainWithDepth: 4}],
|
||||
"@stylistic/no-extra-parens": "off",
|
||||
"@stylistic/no-tabs": "off",
|
||||
"@stylistic/object-curly-spacing": ["error", "always"],
|
||||
"@stylistic/object-property-newline": ["error", {allowAllPropertiesOnSameLine: true}],
|
||||
"@stylistic/operator-linebreak": ["error", "before"],
|
||||
"@stylistic/padded-blocks": "off",
|
||||
"@stylistic/quote-props": ["error", "as-needed"],
|
||||
"@stylistic/quotes": ["error", "double"],
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
"@stylistic/space-before-function-paren": ["error", "always"],
|
||||
"@stylistic/spaced-comment": "off",
|
||||
"dot-notation": "error",
|
||||
eqeqeq: "error",
|
||||
"id-length": "off",
|
||||
"import-x/extensions": "error",
|
||||
"import-x/newline-after-import": "error",
|
||||
"import-x/order": "error",
|
||||
"init-declarations": "off",
|
||||
"vitest/consistent-test-it": "warn",
|
||||
"vitest/expect-expect": [
|
||||
"warn",
|
||||
{
|
||||
assertFunctionNames: [
|
||||
"expect",
|
||||
"testElementLength",
|
||||
"testTextContain",
|
||||
"doTest",
|
||||
"runAnimationTest",
|
||||
"waitForAnimationClass",
|
||||
"assertNoAnimationWithin"
|
||||
]
|
||||
}
|
||||
],
|
||||
"vitest/prefer-to-be": "warn",
|
||||
"vitest/prefer-to-have-length": "warn",
|
||||
"max-lines-per-function": ["warn", 400],
|
||||
"max-statements": "off",
|
||||
"no-global-assign": "off",
|
||||
"no-inline-comments": "off",
|
||||
"no-magic-numbers": "off",
|
||||
"no-param-reassign": "error",
|
||||
"no-plusplus": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-ternary": "off",
|
||||
"no-throw-literal": "error",
|
||||
"no-undefined": "off",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "error",
|
||||
"no-warning-comments": "off",
|
||||
"object-shorthand": ["error", "methods"],
|
||||
"one-var": "off",
|
||||
"prefer-template": "error",
|
||||
"sort-keys": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
ignores: [
|
||||
"clientonly/index.js",
|
||||
"js/logger.js",
|
||||
"tests/**/*.js"
|
||||
],
|
||||
rules: {"no-console": "error"}
|
||||
},
|
||||
{
|
||||
files: ["**/package.json"],
|
||||
plugins: {packageJson},
|
||||
extends: ["packageJson/recommended"]
|
||||
},
|
||||
{
|
||||
files: ["**/*.mjs"],
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
globals: {
|
||||
...globals.node
|
||||
},
|
||||
sourceType: "module"
|
||||
},
|
||||
plugins: {js, stylistic},
|
||||
extends: [importX.recommended, "js/all", "stylistic/all"],
|
||||
rules: {
|
||||
"@stylistic/array-element-newline": "off",
|
||||
"@stylistic/indent": ["error", "tab"],
|
||||
"@stylistic/object-property-newline": ["error", {allowAllPropertiesOnSameLine: true}],
|
||||
"@stylistic/padded-blocks": ["error", "never"],
|
||||
"@stylistic/quote-props": ["error", "as-needed"],
|
||||
"import-x/no-unresolved": ["error", {ignore: ["eslint/config"]}],
|
||||
"max-lines-per-function": ["error", 100],
|
||||
"no-magic-numbers": "off",
|
||||
"one-var": ["error", "never"],
|
||||
"sort-keys": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["tests/configs/modules/weather/*.js"],
|
||||
rules: {
|
||||
"@stylistic/quotes": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["tests/e2e/**/*.js"],
|
||||
extends: [playwright.configs["flat/recommended"]],
|
||||
rules: {
|
||||
"playwright/no-standalone-expect": "off"
|
||||
}
|
||||
}
|
||||
]);
|
||||
25
fonts/package-lock.json
generated
25
fonts/package-lock.json
generated
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "magicmirror-fonts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@fontsource/roboto-condensed": "^5.0.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/roboto": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.8.tgz",
|
||||
"integrity": "sha512-XxPltXs5R31D6UZeLIV1td3wTXU3jzd3f2DLsXI8tytMGBkIsGcc9sIyiupRtA8y73HAhuSCeweOoBqf6DbWCA=="
|
||||
},
|
||||
"node_modules/@fontsource/roboto-condensed": {
|
||||
"version": "5.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.0.14.tgz",
|
||||
"integrity": "sha512-ZNBHUhE5/3z9efMIjpBblFsfLHgGotJjlzKd5Q8DODbkRRWy6Yh+JnbiaJZ8zwQyHyYBNOolk57BG4BcjSzrRg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"description": "Package for fonts use by MagicMirror² Core.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/MichMich/MagicMirror.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/MichMich/MagicMirror/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@fontsource/roboto-condensed": "^5.0.14"
|
||||
}
|
||||
}
|
||||
10
index.html
10
index.html
@@ -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>
|
||||
@@ -55,6 +56,7 @@
|
||||
<script type="text/javascript" src="js/loader.js"></script>
|
||||
<script type="text/javascript" src="js/socketclient.js"></script>
|
||||
<script type="text/javascript" src="js/animateCSS.js"></script>
|
||||
<script type="text/javascript" src="js/positions.js"></script>
|
||||
<script type="text/javascript" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
# This file is still here to keep PM2 working on older installations.
|
||||
cd ~/MagicMirror
|
||||
DISPLAY=:0 npm start
|
||||
@@ -1,33 +1,41 @@
|
||||
module.exports = async () => {
|
||||
return {
|
||||
verbose: true,
|
||||
testTimeout: 20000,
|
||||
testSequencer: "<rootDir>/tests/utils/test_sequencer.js",
|
||||
projects: [
|
||||
{
|
||||
displayName: "unit",
|
||||
globalSetup: "<rootDir>/tests/unit/helpers/global-setup.js",
|
||||
moduleNameMapper: {
|
||||
logger: "<rootDir>/js/logger.js"
|
||||
},
|
||||
testMatch: ["**/tests/unit/**/*.[jt]s?(x)"],
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks", "<rootDir>/tests/unit/helpers"]
|
||||
},
|
||||
{
|
||||
displayName: "electron",
|
||||
testMatch: ["**/tests/electron/**/*.[jt]s?(x)"],
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers"]
|
||||
},
|
||||
{
|
||||
displayName: "e2e",
|
||||
setupFilesAfterEnv: ["<rootDir>/tests/e2e/helpers/mock-console.js"],
|
||||
testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"],
|
||||
modulePaths: ["<rootDir>/js/"],
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers", "<rootDir>/tests/e2e/mocks"]
|
||||
}
|
||||
],
|
||||
collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/default/**/*.js", "./serveronly/**/*.js"],
|
||||
coverageReporters: ["lcov", "text"],
|
||||
coverageProvider: "v8"
|
||||
};
|
||||
const aliasMapper = {
|
||||
logger: "<rootDir>/js/logger.js"
|
||||
};
|
||||
|
||||
const config = {
|
||||
verbose: true,
|
||||
testTimeout: 20000,
|
||||
testSequencer: "<rootDir>/tests/utils/test_sequencer.js",
|
||||
projects: [
|
||||
{
|
||||
displayName: "unit",
|
||||
globalSetup: "<rootDir>/tests/unit/helpers/global-setup.js",
|
||||
moduleNameMapper: aliasMapper,
|
||||
testMatch: ["**/tests/unit/**/*.[jt]s?(x)"],
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks", "<rootDir>/tests/unit/helpers"]
|
||||
},
|
||||
{
|
||||
displayName: "electron",
|
||||
testMatch: ["**/tests/electron/**/*.[jt]s?(x)"],
|
||||
moduleNameMapper: aliasMapper,
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers"]
|
||||
},
|
||||
{
|
||||
displayName: "e2e",
|
||||
testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"],
|
||||
modulePaths: ["<rootDir>/js/"],
|
||||
moduleNameMapper: aliasMapper,
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers", "<rootDir>/tests/e2e/mocks"]
|
||||
}
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
"<rootDir>/clientonly/**/*.js",
|
||||
"<rootDir>/js/**/*.js",
|
||||
"<rootDir>/modules/default/**/*.js",
|
||||
"<rootDir>/serveronly/**/*.js"
|
||||
],
|
||||
coverageReporters: ["lcov", "text"],
|
||||
coverageProvider: "v8"
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
31
js/alias-resolver.js
Normal file
31
js/alias-resolver.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// Internal alias mapping for default and 3rd party modules.
|
||||
// Provides short require identifiers: "logger" and "node_helper".
|
||||
// For a future ESM migration, replace this with a public export/import surface.
|
||||
|
||||
const path = require("node:path");
|
||||
const Module = require("module");
|
||||
|
||||
const root = path.join(__dirname, "..");
|
||||
|
||||
// Keep this list minimal; do not add new aliases without architectural review.
|
||||
const ALIASES = {
|
||||
logger: "js/logger.js",
|
||||
node_helper: "js/node_helper.js"
|
||||
};
|
||||
|
||||
// Resolve to absolute paths now.
|
||||
const resolved = Object.fromEntries(
|
||||
Object.entries(ALIASES).map(([k, rel]) => [k, path.join(root, rel)])
|
||||
);
|
||||
|
||||
// Prevent multiple patching if this file is required more than once.
|
||||
if (!Module._mmAliasPatched) {
|
||||
const origResolveFilename = Module._resolveFilename;
|
||||
Module._resolveFilename = function (request, parent, isMain, options) {
|
||||
if (Object.prototype.hasOwnProperty.call(resolved, request)) {
|
||||
return resolved[request];
|
||||
}
|
||||
return origResolveFilename.call(this, request, parent, isMain, options);
|
||||
};
|
||||
Module._mmAliasPatched = true; // non-enumerable marker would be overkill here
|
||||
}
|
||||
@@ -1,10 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* AnimateCSS System from https://animate.style/
|
||||
* by @bugsounet
|
||||
* for Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/* enumeration of animations in Array **/
|
||||
const AnimateCSSIn = [
|
||||
// Attention seekers
|
||||
@@ -139,7 +132,7 @@ function addAnimateCSS (element, animation, animationTime) {
|
||||
const node = document.getElementById(element);
|
||||
if (!node) {
|
||||
// don't execute animate: we don't find div
|
||||
Log.warn("addAnimateCSS: node not found for", element);
|
||||
Log.warn("node not found for adding", element);
|
||||
return;
|
||||
}
|
||||
node.style.setProperty("--animate-duration", `${animationTime}s`);
|
||||
@@ -156,9 +149,10 @@ function removeAnimateCSS (element, animation) {
|
||||
const node = document.getElementById(element);
|
||||
if (!node) {
|
||||
// don't execute animate: we don't find div
|
||||
Log.warn("removeAnimateCSS: node not found for", element);
|
||||
Log.warn("node not found for removing", element);
|
||||
return;
|
||||
}
|
||||
node.classList.remove("animate__animated", animationName);
|
||||
node.style.removeProperty("--animate-duration");
|
||||
}
|
||||
if (typeof window === "undefined") module.exports = { AnimateCSSIn, AnimateCSSOut };
|
||||
|
||||
144
js/app.js
144
js/app.js
@@ -1,35 +1,38 @@
|
||||
/* MagicMirror²
|
||||
* The Core App (Server)
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
// Load lightweight internal alias resolver
|
||||
require("./alias-resolver");
|
||||
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const envsub = require("envsub");
|
||||
const Log = require("logger");
|
||||
|
||||
const Server = require(`${__dirname}/server`);
|
||||
const Utils = require(`${__dirname}/utils`);
|
||||
const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`);
|
||||
|
||||
// Get version number.
|
||||
global.version = require(`${__dirname}/../package.json`).version;
|
||||
Log.log(`Starting MagicMirror: v${global.version}`);
|
||||
|
||||
// global absolute root path
|
||||
global.root_path = path.resolve(`${__dirname}/../`);
|
||||
|
||||
const Server = require(`${__dirname}/server`);
|
||||
const Utils = require(`${__dirname}/utils`);
|
||||
|
||||
const defaultModules = require(`${global.root_path}/modules/default/defaultmodules`);
|
||||
// used to control fetch timeout for node_helpers
|
||||
const { setGlobalDispatcher, Agent } = require("undici");
|
||||
const { getEnvVarsAsObj, getConfigFilePath } = require("#server_functions");
|
||||
// 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(`${global.root_path}/package.json`).version;
|
||||
global.mmTestMode = process.env.mmTestMode === "true";
|
||||
Log.log(`Starting MagicMirror: v${global.version}`);
|
||||
|
||||
// Log system information.
|
||||
Utils.logSystemInformation(global.version);
|
||||
|
||||
if (process.env.MM_CONFIG_FILE) {
|
||||
global.configuration_file = process.env.MM_CONFIG_FILE.replace(`${global.root_path}/`, "");
|
||||
}
|
||||
|
||||
// FIXME: Hotfix Pull Request
|
||||
// https://github.com/MichMich/MagicMirror/pull/673
|
||||
// https://github.com/MagicMirrorOrg/MagicMirror/pull/673
|
||||
if (process.env.MM_PORT) {
|
||||
global.mmPort = process.env.MM_PORT;
|
||||
}
|
||||
@@ -37,10 +40,13 @@ if (process.env.MM_PORT) {
|
||||
// The next part is here to prevent a major exception when there
|
||||
// is no internet connection. This could probable be solved better.
|
||||
process.on("uncaughtException", function (err) {
|
||||
Log.error("Whoops! There was an uncaught exception...");
|
||||
Log.error(err);
|
||||
Log.error("MagicMirror² will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
|
||||
Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
|
||||
// ignore strange exceptions under aarch64 coming from systeminformation:
|
||||
if (!err.stack.includes("node_modules/systeminformation")) {
|
||||
Log.error("Whoops! There was an uncaught exception...");
|
||||
Log.error(err);
|
||||
Log.error("MagicMirror² will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
|
||||
Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MagicMirrorOrg/MagicMirror/issues");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -59,18 +65,22 @@ function App () {
|
||||
async function loadConfig () {
|
||||
Log.log("Loading config ...");
|
||||
const defaults = require(`${__dirname}/defaults`);
|
||||
if (global.mmTestMode) {
|
||||
// if we are running in test mode
|
||||
defaults.address = "0.0.0.0";
|
||||
}
|
||||
|
||||
// For this check proposed to TestSuite
|
||||
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
|
||||
const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`);
|
||||
const configFilename = getConfigFilePath();
|
||||
let templateFile = `${configFilename}.template`;
|
||||
|
||||
// check if templateFile exists
|
||||
try {
|
||||
fs.accessSync(templateFile, fs.F_OK);
|
||||
fs.accessSync(templateFile, fs.constants.F_OK);
|
||||
} catch (err) {
|
||||
templateFile = null;
|
||||
Log.debug("config template file not exists, no envsubst");
|
||||
Log.log("config template file not exists, no envsubst");
|
||||
}
|
||||
|
||||
if (templateFile) {
|
||||
@@ -91,7 +101,7 @@ function App () {
|
||||
envFiles.push(configEnvFile);
|
||||
}
|
||||
} catch (err) {
|
||||
Log.debug(`${configEnvFile} does not exist. ${err.message}`);
|
||||
Log.log(`${configEnvFile} does not exist. ${err.message}`);
|
||||
}
|
||||
|
||||
let options = {
|
||||
@@ -113,18 +123,23 @@ function App () {
|
||||
}
|
||||
}
|
||||
|
||||
require(`${global.root_path}/js/check_config.js`);
|
||||
|
||||
try {
|
||||
fs.accessSync(configFilename, fs.F_OK);
|
||||
fs.accessSync(configFilename, fs.constants.F_OK);
|
||||
const c = require(configFilename);
|
||||
if (Object.keys(c).length === 0) {
|
||||
Log.error("WARNING! Config file appears empty, maybe missing module.exports last line?");
|
||||
}
|
||||
checkDeprecatedOptions(c);
|
||||
return Object.assign(defaults, c);
|
||||
} catch (e) {
|
||||
if (e.code === "ENOENT") {
|
||||
Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
|
||||
Log.error("WARNING! Could not find config file. Please create one. Starting with default configuration.");
|
||||
} else if (e instanceof ReferenceError || e instanceof SyntaxError) {
|
||||
Log.error(Utils.colors.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`));
|
||||
Log.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`);
|
||||
} else {
|
||||
Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
|
||||
Log.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,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(Utils.colors.warn(`WARNING! Your config is using deprecated options: ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`));
|
||||
Log.warn(`WARNING! Your config is using deprecated option(s): ${usedDeprecated.join(", ")}. Check README and Documentation 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 Documentation for more up-to-date ways of getting the same functionality.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,16 +180,25 @@ function App () {
|
||||
function loadModule (module) {
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${__dirname}/../modules/${module}`;
|
||||
const env = getEnvVarsAsObj();
|
||||
let moduleFolder = path.resolve(`${global.root_path}/${env.modulesDir}`, module);
|
||||
|
||||
if (defaultModules.includes(moduleName)) {
|
||||
moduleFolder = `${__dirname}/../modules/default/${module}`;
|
||||
const defaultModuleFolder = path.resolve(`${global.root_path}/modules/default/`, module);
|
||||
if (!global.mmTestMode) {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
} else {
|
||||
// running in test mode, allow defaultModules placed under moduleDir for testing
|
||||
if (env.modulesDir === "modules" || env.modulesDir === "tests/mocks") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const moduleFile = `${moduleFolder}/${module}.js`;
|
||||
const moduleFile = `${moduleFolder}/${moduleName}.js`;
|
||||
|
||||
try {
|
||||
fs.accessSync(moduleFile, fs.R_OK);
|
||||
fs.accessSync(moduleFile, fs.constants.R_OK);
|
||||
} catch (e) {
|
||||
Log.warn(`No ${moduleFile} found for module: ${moduleName}.`);
|
||||
}
|
||||
@@ -171,14 +207,21 @@ function App () {
|
||||
|
||||
let loadHelper = true;
|
||||
try {
|
||||
fs.accessSync(helperPath, fs.R_OK);
|
||||
fs.accessSync(helperPath, fs.constants.R_OK);
|
||||
} catch (e) {
|
||||
loadHelper = false;
|
||||
Log.log(`No helper found for module: ${moduleName}.`);
|
||||
}
|
||||
|
||||
// if the helper was found
|
||||
if (loadHelper) {
|
||||
const Module = require(helperPath);
|
||||
let Module;
|
||||
try {
|
||||
Module = require(helperPath);
|
||||
} catch (e) {
|
||||
Log.error(`Error when loading ${moduleName}:`, e.message);
|
||||
return;
|
||||
}
|
||||
let m = new Module();
|
||||
|
||||
if (m.requiresVersion) {
|
||||
@@ -249,13 +292,28 @@ function App () {
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
// get the used module positions
|
||||
Utils.getModulePositions();
|
||||
|
||||
let modules = [];
|
||||
for (const module of config.modules) {
|
||||
if (!modules.includes(module.module) && !module.disabled) {
|
||||
modules.push(module.module);
|
||||
if (module.disabled) continue;
|
||||
if (module.module) {
|
||||
if (Utils.moduleHasValidPosition(module.position) || typeof (module.position) === "undefined") {
|
||||
// Only add this module to be loaded if it is not a duplicate (repeated instance of the same module)
|
||||
if (!modules.includes(module.module)) {
|
||||
modules.push(module.module);
|
||||
}
|
||||
} else {
|
||||
Log.warn("Invalid module position found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`);
|
||||
}
|
||||
} else {
|
||||
Log.warn("No module name found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
setGlobalDispatcher(new Agent({ connect: { timeout: fetch_timeout } }));
|
||||
|
||||
await loadModules(modules);
|
||||
|
||||
httpServer = new Server(config);
|
||||
@@ -306,7 +364,7 @@ function App () {
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error(`Error when stopping node_helper for module ${nodeHelper.name}:`);
|
||||
console.error(error);
|
||||
Log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/* MagicMirror²
|
||||
*
|
||||
* Check the configuration file for errors
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { Linter } = require("eslint");
|
||||
// Ensure internal require aliases (e.g., "logger") resolve when this file is run as a standalone script
|
||||
require("./alias-resolver");
|
||||
|
||||
const linter = new Linter();
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const { styleText } = require("node:util");
|
||||
const Ajv = require("ajv");
|
||||
const globals = require("globals");
|
||||
const { Linter } = require("eslint");
|
||||
const Log = require("logger");
|
||||
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const Utils = require(`${rootPath}/js/utils.js`);
|
||||
|
||||
const linter = new Linter({ configType: "flat" });
|
||||
const ajv = new Ajv();
|
||||
|
||||
/**
|
||||
* Returns a string with path of configuration file.
|
||||
* Check if set by environment variable MM_CONFIG_FILE
|
||||
@@ -31,42 +31,127 @@ function getConfigFile () {
|
||||
function checkConfigFile () {
|
||||
const configFileName = getConfigFile();
|
||||
|
||||
// Check if file is present
|
||||
if (fs.existsSync(configFileName) === false) {
|
||||
Log.error(Utils.colors.error("File not found: "), configFileName);
|
||||
throw new Error("No config file present!");
|
||||
}
|
||||
|
||||
// Check permission
|
||||
// Check if file exists and is accessible
|
||||
try {
|
||||
fs.accessSync(configFileName, fs.F_OK);
|
||||
} catch (e) {
|
||||
Log.error(Utils.colors.error(e));
|
||||
throw new Error("No permission to access config file!");
|
||||
fs.accessSync(configFileName, fs.constants.R_OK);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
Log.error(`File not found: ${configFileName}`);
|
||||
} else if (error.code === "EACCES") {
|
||||
Log.error(`No permission to read config file: ${configFileName}`);
|
||||
} else {
|
||||
Log.error(`Cannot access config file: ${configFileName}\n${error.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate syntax of the configuration file.
|
||||
Log.info(Utils.colors.info("Checking file... "), configFileName);
|
||||
Log.info(`Checking config file ${configFileName} ...`);
|
||||
|
||||
// I'm not sure if all ever is utf-8
|
||||
const configFile = fs.readFileSync(configFileName, "utf-8");
|
||||
|
||||
// Explicitly tell linter that he might encounter es6 syntax ("let config = {...}")
|
||||
const errors = linter.verify(configFile, {
|
||||
env: {
|
||||
es6: true
|
||||
}
|
||||
});
|
||||
const errors = linter.verify(
|
||||
configFile,
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-sparse-arrays": "error",
|
||||
"no-undef": "error"
|
||||
}
|
||||
},
|
||||
configFileName
|
||||
);
|
||||
|
||||
if (errors.length === 0) {
|
||||
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
||||
Log.info(styleText("green", "Your configuration file doesn't contain syntax errors :)"));
|
||||
validateModulePositions(configFileName);
|
||||
} else {
|
||||
Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
|
||||
let errorMessage = "Your configuration file contains syntax errors :(";
|
||||
|
||||
for (const error of errors) {
|
||||
Log.error(`Line ${error.line} column ${error.column}: ${error.message}`);
|
||||
errorMessage += `\nLine ${error.line} column ${error.column}: ${error.message}`;
|
||||
}
|
||||
Log.error(errorMessage);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkConfigFile();
|
||||
/**
|
||||
*
|
||||
* @param {string} configFileName - The path and filename of the configuration file to validate.
|
||||
*/
|
||||
function validateModulePositions (configFileName) {
|
||||
Log.info("Checking modules structure configuration ...");
|
||||
|
||||
const positionList = Utils.getModulePositions();
|
||||
|
||||
// Make Ajv schema configuration of modules config
|
||||
// Only scan "module" and "position"
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
modules: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
module: {
|
||||
type: "string"
|
||||
},
|
||||
position: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
required: ["module"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Scan all modules
|
||||
const validate = ajv.compile(schema);
|
||||
const data = require(configFileName);
|
||||
|
||||
const valid = validate(data);
|
||||
if (valid) {
|
||||
Log.info(styleText("green", "Your modules structure configuration doesn't contain errors :)"));
|
||||
|
||||
// Check for unknown positions (warning only, not an error)
|
||||
if (data.modules) {
|
||||
for (const [index, module] of data.modules.entries()) {
|
||||
if (module.position && !positionList.includes(module.position)) {
|
||||
Log.warn(`Module ${index} ("${module.module}") uses unknown position: "${module.position}"`);
|
||||
Log.warn(`Known positions are: ${positionList.join(", ")}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const module = validate.errors[0].instancePath.split("/")[2];
|
||||
const position = validate.errors[0].instancePath.split("/")[3];
|
||||
let errorMessage = "This module configuration contains errors:";
|
||||
errorMessage += `\n${JSON.stringify(data.modules[module], null, 2)}`;
|
||||
if (position) {
|
||||
errorMessage += `\n${position}: ${validate.errors[0].message}`;
|
||||
errorMessage += `\n${JSON.stringify(validate.errors[0].params.allowedValues, null, 2).slice(1, -1)}`;
|
||||
} else {
|
||||
errorMessage += validate.errors[0].message;
|
||||
}
|
||||
Log.error(errorMessage);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
checkConfigFile();
|
||||
} catch (error) {
|
||||
const message = error && error.message ? error.message : error;
|
||||
Log.error(`Unexpected error: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
21
js/class.js
21
js/class.js
@@ -1,6 +1,7 @@
|
||||
/* global Class, xyz */
|
||||
|
||||
/* Simple JavaScript Inheritance
|
||||
/*
|
||||
* Simple JavaScript Inheritance
|
||||
* By John Resig https://johnresig.com/
|
||||
*
|
||||
* Inspired by base2 and Prototype
|
||||
@@ -22,8 +23,10 @@
|
||||
Class.extend = function (prop) {
|
||||
let _super = this.prototype;
|
||||
|
||||
// Instantiate a base class (but only create the instance,
|
||||
// don't run the init constructor)
|
||||
/*
|
||||
* Instantiate a base class (but only create the instance,
|
||||
* don't run the init constructor)
|
||||
*/
|
||||
initializing = true;
|
||||
const prototype = new this();
|
||||
initializing = false;
|
||||
@@ -42,12 +45,16 @@
|
||||
return function () {
|
||||
const tmp = this._super;
|
||||
|
||||
// Add a new ._super() method that is the same method
|
||||
// but on the super-class
|
||||
/*
|
||||
* Add a new ._super() method that is the same method
|
||||
* but on the super-class
|
||||
*/
|
||||
this._super = _super[name];
|
||||
|
||||
// The method only need to be bound temporarily, so we
|
||||
// remove it when we're done executing
|
||||
/*
|
||||
* The method only need to be bound temporarily, so we
|
||||
* remove it when we're done executing
|
||||
*/
|
||||
const ret = fn.apply(this, arguments);
|
||||
this._super = tmp;
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/* global mmPort */
|
||||
|
||||
/* MagicMirror²
|
||||
* Config Defaults
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const address = "localhost";
|
||||
let port = 8080;
|
||||
if (typeof mmPort !== "undefined") {
|
||||
@@ -25,8 +19,9 @@ const defaults = {
|
||||
units: "metric",
|
||||
zoom: 1,
|
||||
customCss: "css/custom.css",
|
||||
foreignModulesDir: "modules",
|
||||
// httpHeaders used by helmet, see https://helmetjs.github.io/. You can add other/more object values by overriding this in config.js,
|
||||
// e.g. you need to add `frameguard: false` for embedding MagicMirror in another website, see https://github.com/MichMich/MagicMirror/issues/2847
|
||||
// e.g. you need to add `frameguard: false` for embedding MagicMirror in another website, see https://github.com/MagicMirrorOrg/MagicMirror/issues/2847
|
||||
httpHeaders: { contentSecurityPolicy: false, crossOriginOpenerPolicy: false, crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false },
|
||||
|
||||
// properties for checking if server is alive and has same startup-timestamp, the check is per default enabled
|
||||
@@ -67,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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -75,15 +70,10 @@ const defaults = {
|
||||
position: "bottom_bar",
|
||||
classes: "xsmall dimmed",
|
||||
config: {
|
||||
text: "www.michaelteeuw.nl"
|
||||
text: "https://magicmirror.builders/"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
paths: {
|
||||
modules: "modules",
|
||||
vendor: "vendor"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
/* MagicMirror² Deprecated Config Options List
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* Olex S. original idea this deprecated option
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
configs: ["kioskmode"]
|
||||
configs: ["kioskmode"],
|
||||
clock: ["secondsColor"]
|
||||
};
|
||||
|
||||
@@ -8,9 +8,12 @@ const Log = require("./logger");
|
||||
let config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
// Module to control application life.
|
||||
const app = electron.app;
|
||||
// Per default electron is started with --disable-gpu flag, if you want the gpu enabled,
|
||||
// you must set the env var ELECTRON_ENABLE_GPU=1 on startup.
|
||||
// See https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering for more info.
|
||||
|
||||
/*
|
||||
* Per default electron is started with --disable-gpu flag, if you want the gpu enabled,
|
||||
* you must set the env var ELECTRON_ENABLE_GPU=1 on startup.
|
||||
* See https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering for more info.
|
||||
*/
|
||||
if (process.env.ELECTRON_ENABLE_GPU !== "1") {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
@@ -18,16 +21,21 @@ if (process.env.ELECTRON_ENABLE_GPU !== "1") {
|
||||
// Module to create native browser window.
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
/*
|
||||
* Keep a global reference of the window object, if you don't, the window will
|
||||
* be closed automatically when the JavaScript object is garbage collected.
|
||||
*/
|
||||
let mainWindow;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function createWindow () {
|
||||
// see https://www.electronjs.org/docs/latest/api/screen
|
||||
// Create a window that fills the screen's available work area.
|
||||
|
||||
/*
|
||||
* see https://www.electronjs.org/docs/latest/api/screen
|
||||
* Create a window that fills the screen's available work area.
|
||||
*/
|
||||
let electronSize = (800, 600);
|
||||
try {
|
||||
electronSize = electron.screen.getPrimaryDisplay().workAreaSize;
|
||||
@@ -40,6 +48,7 @@ function createWindow () {
|
||||
let electronOptionsDefaults = {
|
||||
width: electronSize.width,
|
||||
height: electronSize.height,
|
||||
icon: "mm2.png",
|
||||
x: 0,
|
||||
y: 0,
|
||||
darkTheme: true,
|
||||
@@ -51,8 +60,10 @@ function createWindow () {
|
||||
backgroundColor: "#000000"
|
||||
};
|
||||
|
||||
// DEPRECATED: "kioskmode" backwards compatibility, to be removed
|
||||
// settings these options directly instead provides cleaner interface
|
||||
/*
|
||||
* DEPRECATED: "kioskmode" backwards compatibility, to be removed
|
||||
* settings these options directly instead provides cleaner interface
|
||||
*/
|
||||
if (config.kioskmode) {
|
||||
electronOptionsDefaults.kiosk = true;
|
||||
} else {
|
||||
@@ -65,14 +76,33 @@ function createWindow () {
|
||||
|
||||
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
|
||||
|
||||
if (process.env.MOCK_DATE !== undefined) {
|
||||
// if we are running tests and we want to mock the current date
|
||||
const fakeNow = new Date(process.env.MOCK_DATE).valueOf();
|
||||
Date = class extends Date {
|
||||
constructor (...args) {
|
||||
if (args.length === 0) {
|
||||
super(fakeNow);
|
||||
} else {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
};
|
||||
const __DateNowOffset = fakeNow - Date.now();
|
||||
const __DateNow = Date.now;
|
||||
Date.now = () => __DateNow() + __DateNowOffset;
|
||||
}
|
||||
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow(electronOptions);
|
||||
|
||||
// and load the index.html of the app.
|
||||
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
|
||||
/*
|
||||
* and load the index.html of the app.
|
||||
* If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
|
||||
*/
|
||||
|
||||
let prefix;
|
||||
if ((config["tls"] !== null && config["tls"]) || config.useHttps) {
|
||||
if ((config.tls !== null && config.tls) || config.useHttps) {
|
||||
prefix = "https://";
|
||||
} else {
|
||||
prefix = "http://";
|
||||
@@ -82,10 +112,10 @@ 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
|
||||
if (process.env.mmTestMode) {
|
||||
// if we are running tests
|
||||
const devtools = new BrowserWindow(electronOptions);
|
||||
mainWindow.webContents.setDevToolsWebContents(devtools.webContents);
|
||||
}
|
||||
@@ -121,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])));
|
||||
}
|
||||
|
||||
@@ -139,8 +169,8 @@ function createWindow () {
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on("window-all-closed", function () {
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
if (process.env.mmTestMode) {
|
||||
// if we are running tests
|
||||
app.quit();
|
||||
} else {
|
||||
createWindow();
|
||||
@@ -148,14 +178,18 @@ app.on("window-all-closed", function () {
|
||||
});
|
||||
|
||||
app.on("activate", function () {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
|
||||
/*
|
||||
* On OS X it's common to re-create a window in the app when the
|
||||
* dock icon is clicked and there are no other windows open.
|
||||
*/
|
||||
if (mainWindow === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
/* This method will be called when SIGINT is received and will call
|
||||
/*
|
||||
* This method will be called when SIGINT is received and will call
|
||||
* each node_helper's stop function if it exists. Added to fix #1056
|
||||
*
|
||||
* Note: this is only used if running Electron. Otherwise
|
||||
@@ -186,8 +220,10 @@ if (process.env.clientonly) {
|
||||
});
|
||||
}
|
||||
|
||||
// Start the core application if server is run on localhost
|
||||
// This starts all node helpers and starts the webserver.
|
||||
/*
|
||||
* Start the core application if server is run on localhost
|
||||
* This starts all node helpers and starts the webserver.
|
||||
*/
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
|
||||
core.start().then((c) => {
|
||||
config = c;
|
||||
|
||||
63
js/ip_access_control.js
Normal file
63
js/ip_access_control.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const ipaddr = require("ipaddr.js");
|
||||
const Log = require("logger");
|
||||
|
||||
/**
|
||||
* Checks if a client IP matches any entry in the whitelist
|
||||
* @param {string} clientIp - The IP address to check
|
||||
* @param {string[]} whitelist - Array of IP addresses or CIDR ranges
|
||||
* @returns {boolean} True if IP is allowed
|
||||
*/
|
||||
function isAllowed (clientIp, whitelist) {
|
||||
try {
|
||||
const addr = ipaddr.process(clientIp);
|
||||
|
||||
return whitelist.some((entry) => {
|
||||
try {
|
||||
// CIDR notation
|
||||
if (entry.includes("/")) {
|
||||
const [rangeAddr, prefixLen] = ipaddr.parseCIDR(entry);
|
||||
return addr.match(rangeAddr, prefixLen);
|
||||
}
|
||||
|
||||
// Single IP address - let ipaddr.process normalize both
|
||||
const allowedAddr = ipaddr.process(entry);
|
||||
return addr.toString() === allowedAddr.toString();
|
||||
} catch (err) {
|
||||
Log.warn(`Invalid whitelist entry: ${entry}`);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
Log.warn(`Failed to parse client IP: ${clientIp}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Express middleware for IP whitelisting
|
||||
* @param {string[]} whitelist - Array of allowed IP addresses or CIDR ranges
|
||||
* @returns {import("express").RequestHandler} Express middleware function
|
||||
*/
|
||||
function ipAccessControl (whitelist) {
|
||||
// Empty whitelist means allow all
|
||||
if (!Array.isArray(whitelist) || whitelist.length === 0) {
|
||||
return function (req, res, next) {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
return function (req, res, next) {
|
||||
const clientIp = req.ip || req.socket.remoteAddress;
|
||||
|
||||
if (isAllowed(clientIp, whitelist)) {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
next();
|
||||
} else {
|
||||
Log.log(`IP ${clientIp} is not allowed to access the mirror`);
|
||||
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { ipAccessControl };
|
||||
98
js/loader.js
98
js/loader.js
@@ -1,11 +1,5 @@
|
||||
/* global defaultModules, vendor */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module and File loaders.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Loader = (function () {
|
||||
|
||||
/* Create helper variables */
|
||||
@@ -16,6 +10,38 @@ const Loader = (function () {
|
||||
|
||||
/* Private Methods */
|
||||
|
||||
/**
|
||||
* Get environment variables from config.
|
||||
* @returns {object} Env vars with modulesDir and customCss paths from config.
|
||||
*/
|
||||
const getEnvVarsFromConfig = function () {
|
||||
return {
|
||||
modulesDir: config.foreignModulesDir || "modules",
|
||||
customCss: config.customCss || "css/custom.css"
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve object of env variables.
|
||||
* @returns {object} with key: values as assembled in js/server_functions.js
|
||||
*/
|
||||
const getEnvVars = async function () {
|
||||
// In test mode, skip server fetch and use config values directly
|
||||
if (typeof process !== "undefined" && process.env && process.env.mmTestMode === "true") {
|
||||
return getEnvVarsFromConfig();
|
||||
}
|
||||
|
||||
// In production, fetch env vars from server
|
||||
try {
|
||||
const res = await fetch(new URL("env", `${location.origin}${config.basePath}`));
|
||||
return JSON.parse(await res.text());
|
||||
} catch (error) {
|
||||
// Fallback to config values if server fetch fails
|
||||
Log.error("Unable to retrieve env configuration", error);
|
||||
return getEnvVarsFromConfig();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Loops through all modules and requests start for every module.
|
||||
*/
|
||||
@@ -56,26 +82,36 @@ const Loader = (function () {
|
||||
* @returns {object[]} module data as configured in config
|
||||
*/
|
||||
const getAllModules = function () {
|
||||
return config.modules;
|
||||
const AllModules = config.modules.filter((module) => (module.module !== undefined) && (MM.getAvailableModulePositions.indexOf(module.position) > -1 || typeof (module.position) === "undefined"));
|
||||
return AllModules;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate array with module information including module paths.
|
||||
* @returns {object[]} Module information.
|
||||
*/
|
||||
const getModuleData = function () {
|
||||
const getModuleData = async function () {
|
||||
const modules = getAllModules();
|
||||
const moduleFiles = [];
|
||||
const envVars = await getEnvVars();
|
||||
|
||||
modules.forEach(function (moduleData, index) {
|
||||
const module = moduleData.module;
|
||||
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${config.paths.modules}/${module}`;
|
||||
let moduleFolder = `${envVars.modulesDir}/${module}`;
|
||||
|
||||
if (defaultModules.indexOf(moduleName) !== -1) {
|
||||
moduleFolder = `${config.paths.modules}/default/${module}`;
|
||||
const defaultModuleFolder = `modules/default/${module}`;
|
||||
if (window.name !== "jsdom") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
} else {
|
||||
// running in test mode, allow defaultModules placed under moduleDir for testing
|
||||
if (envVars.modulesDir === "modules") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleData.disabled === true) {
|
||||
@@ -95,7 +131,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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,6 +208,7 @@ const Loader = (function () {
|
||||
};
|
||||
script.onerror = function () {
|
||||
Log.error("Error on loading script:", fileName);
|
||||
script.remove();
|
||||
resolve();
|
||||
};
|
||||
document.getElementsByTagName("body")[0].appendChild(script);
|
||||
@@ -188,6 +226,7 @@ const Loader = (function () {
|
||||
};
|
||||
stylesheet.onerror = function () {
|
||||
Log.error("Error on loading stylesheet:", fileName);
|
||||
stylesheet.remove();
|
||||
resolve();
|
||||
};
|
||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
||||
@@ -202,27 +241,22 @@ const Loader = (function () {
|
||||
* Load all modules as defined in the config.
|
||||
*/
|
||||
async loadModules () {
|
||||
let moduleData = 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(config.customCss);
|
||||
// custom.css loaded. Start all modules.
|
||||
await startModules();
|
||||
}
|
||||
};
|
||||
await loadNextModule();
|
||||
// Load all modules
|
||||
for (const module of moduleData) {
|
||||
await loadModule(module);
|
||||
}
|
||||
|
||||
// Load custom.css
|
||||
// Since this happens after loading the modules,
|
||||
// it overwrites the default styles.
|
||||
await loadFile(customCss);
|
||||
|
||||
// Start all modules.
|
||||
await startModules();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -249,7 +283,7 @@ const Loader = (function () {
|
||||
// This file is available in the vendor folder.
|
||||
// Load it from this vendor folder.
|
||||
loadedFiles.push(fileName.toLowerCase());
|
||||
return loadFile(`${config.paths.vendor}/${vendor[fileName]}`);
|
||||
return loadFile(`${vendor[fileName]}`);
|
||||
}
|
||||
|
||||
// File not loaded yet.
|
||||
|
||||
82
js/logger.js
82
js/logger.js
@@ -1,19 +1,71 @@
|
||||
/* MagicMirror²
|
||||
* Log
|
||||
*
|
||||
* This logger is very simple, but needs to be extended.
|
||||
* This system can eventually be used to push the log messages to an external target.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
// This logger is very simple, but needs to be extended.
|
||||
(function (root, factory) {
|
||||
if (typeof exports === "object") {
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
if (process.env.mmTestMode !== "true") {
|
||||
const { styleText } = require("node:util");
|
||||
|
||||
// add timestamps in front of log messages
|
||||
require("console-stamp")(console, {
|
||||
pattern: "yyyy-mm-dd HH:MM:ss.l",
|
||||
include: ["debug", "log", "info", "warn", "error"]
|
||||
format: ":date(yyyy-mm-dd HH:MM:ss.l) :label(7) :pre() :msg",
|
||||
tokens: {
|
||||
pre: () => {
|
||||
const err = new Error();
|
||||
Error.prepareStackTrace = (_, stack) => stack;
|
||||
const stack = err.stack;
|
||||
Error.prepareStackTrace = undefined;
|
||||
try {
|
||||
for (const line of stack) {
|
||||
const file = line.getFileName();
|
||||
if (file && !file.includes("node:") && !file.includes("js/logger.js") && !file.includes("node_modules")) {
|
||||
const filename = file.replace(/.*\/(.*).js/, "$1");
|
||||
const filepath = file.replace(/.*\/(.*)\/.*.js/, "$1");
|
||||
if (filepath === "js") {
|
||||
return styleText("grey", `[${filename}]`);
|
||||
} else {
|
||||
return styleText("grey", `[${filepath}]`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
return styleText("grey", "[unknown]");
|
||||
}
|
||||
},
|
||||
label: (arg) => {
|
||||
const { method, defaultTokens } = arg;
|
||||
let label = defaultTokens.label(arg);
|
||||
switch (method) {
|
||||
case "error":
|
||||
label = styleText("red", label);
|
||||
break;
|
||||
case "warn":
|
||||
label = styleText("yellow", label);
|
||||
break;
|
||||
case "debug":
|
||||
label = styleText("bgBlue", label);
|
||||
break;
|
||||
case "info":
|
||||
label = styleText("blue", label);
|
||||
break;
|
||||
}
|
||||
return label;
|
||||
},
|
||||
msg: (arg) => {
|
||||
const { method, defaultTokens } = arg;
|
||||
let msg = defaultTokens.msg(arg);
|
||||
switch (method) {
|
||||
case "error":
|
||||
msg = styleText("red", msg);
|
||||
break;
|
||||
case "warn":
|
||||
msg = styleText("yellow", msg);
|
||||
break;
|
||||
case "info":
|
||||
msg = styleText("blue", msg);
|
||||
break;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Node, CommonJS-like
|
||||
@@ -26,8 +78,8 @@
|
||||
let logLevel;
|
||||
let enableLog;
|
||||
if (typeof exports === "object") {
|
||||
// in nodejs and not running with jest
|
||||
enableLog = process.env.JEST_WORKER_ID === undefined;
|
||||
// in nodejs and not running in test mode
|
||||
enableLog = process.env.mmTestMode !== "true";
|
||||
} else {
|
||||
// in browser and not running with jsdom
|
||||
enableLog = typeof window === "object" && window.name !== "jsdom";
|
||||
@@ -45,7 +97,7 @@
|
||||
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
|
||||
time: Function.prototype.bind.call(console.time, console),
|
||||
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
|
||||
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
|
||||
timeStamp: console.timeStamp ? Function.prototype.bind.call(console.timeStamp, console) : function () {}
|
||||
};
|
||||
|
||||
logLevel.setLogLevel = function (newLevel) {
|
||||
|
||||
71
js/main.js
71
js/main.js
@@ -1,11 +1,5 @@
|
||||
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut */
|
||||
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut, modulePositions, io */
|
||||
|
||||
/* MagicMirror²
|
||||
* Main System
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const MM = (function () {
|
||||
let modules = [];
|
||||
|
||||
@@ -36,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);
|
||||
|
||||
@@ -94,7 +90,7 @@ const MM = (function () {
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {object} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
* @param {Module} [sendTo] The (optional) module to send the notification to.
|
||||
*/
|
||||
@@ -266,13 +262,12 @@ const MM = (function () {
|
||||
* Hide the module.
|
||||
* @param {Module} module The module to hide.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {Promise} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
const hideModule = function (module, speed, callback, options = {}) {
|
||||
// set lockString if set in options.
|
||||
if (options.lockString) {
|
||||
// Log.log("Has lockstring: " + options.lockString);
|
||||
if (module.lockStrings.indexOf(options.lockString) === -1) {
|
||||
module.lockStrings.push(options.lockString);
|
||||
}
|
||||
@@ -292,9 +287,9 @@ const MM = (function () {
|
||||
Log.debug(`${module.identifier} Force remove animateIn (in hide): ${module.hasAnimateIn}`);
|
||||
module.hasAnimateIn = false;
|
||||
}
|
||||
// haveAnimateName for verify if we are using AninateCSS library
|
||||
// haveAnimateName for verify if we are using AnimateCSS library
|
||||
// we check AnimateCSSOut Array for validate it
|
||||
// and finaly return the animate name or `null` (for default MM² animation)
|
||||
// and finally return the animate name or `null` (for default MM² animation)
|
||||
let haveAnimateName = null;
|
||||
// check if have valid animateOut in module definition (module.data.animateOut)
|
||||
if (module.data.animateOut && AnimateCSSOut.indexOf(module.data.animateOut) !== -1) haveAnimateName = module.data.animateOut;
|
||||
@@ -351,7 +346,7 @@ const MM = (function () {
|
||||
* Show the module.
|
||||
* @param {Module} module The module to show.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {Promise} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
const showModule = function (module, speed, callback, options = {}) {
|
||||
@@ -363,7 +358,7 @@ const MM = (function () {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are no more lockstrings set, or the force option is set.
|
||||
// Check if there are no more lockStrings set, or the force option is set.
|
||||
// Otherwise cancel show action.
|
||||
if (module.lockStrings.length !== 0 && options.force !== true) {
|
||||
Log.log(`Will not show ${module.name}. LockStrings active: ${module.lockStrings.join(",")}`);
|
||||
@@ -386,7 +381,7 @@ const MM = (function () {
|
||||
|
||||
module.hidden = false;
|
||||
|
||||
// If forced show, clean current lockstrings.
|
||||
// If forced show, clean current lockStrings.
|
||||
if (module.lockStrings.length !== 0 && options.force === true) {
|
||||
Log.log(`Force show of module: ${module.name}`);
|
||||
module.lockStrings = [];
|
||||
@@ -396,9 +391,9 @@ const MM = (function () {
|
||||
if (moduleWrapper !== null) {
|
||||
clearTimeout(module.showHideTimer);
|
||||
|
||||
// haveAnimateName for verify if we are using AninateCSS library
|
||||
// haveAnimateName for verify if we are using AnimateCSS library
|
||||
// we check AnimateCSSIn Array for validate it
|
||||
// and finaly return the animate name or `null` (for default MM² animation)
|
||||
// and finally return the animate name or `null` (for default MM² animation)
|
||||
let haveAnimateName = null;
|
||||
// check if have valid animateOut in module definition (module.data.animateIn)
|
||||
if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateName = module.data.animateIn;
|
||||
@@ -456,10 +451,9 @@ const MM = (function () {
|
||||
* an ugly top margin. By using this function, the top bar will be hidden if the
|
||||
* update notification is not visible.
|
||||
*/
|
||||
const updateWrapperStates = function () {
|
||||
const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
|
||||
positions.forEach(function (position) {
|
||||
const updateWrapperStates = function () {
|
||||
modulePositions.forEach(function (position) {
|
||||
const wrapper = selectWrapper(position);
|
||||
const moduleWrappers = wrapper.getElementsByClassName("module");
|
||||
|
||||
@@ -470,7 +464,8 @@ const MM = (function () {
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.style.display = showWrapper ? "block" : "none";
|
||||
// move container definitions to main CSS
|
||||
wrapper.className = showWrapper ? "container" : "container hidden";
|
||||
});
|
||||
};
|
||||
|
||||
@@ -556,7 +551,7 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Walks thru a collection of modules and executes the callback with the module as an argument.
|
||||
* @param {Function} callback The function to execute with the module as an argument.
|
||||
* @param {module} callback The function to execute with the module as an argument.
|
||||
*/
|
||||
const enumerate = function (callback) {
|
||||
modules.map(function (module) {
|
||||
@@ -610,18 +605,30 @@ const MM = (function () {
|
||||
|
||||
createDomObjects();
|
||||
|
||||
// Setup global socket listener for RELOAD event (watch mode)
|
||||
if (typeof io !== "undefined") {
|
||||
const socket = io("/", {
|
||||
path: `${config.basePath || "/"}socket.io`
|
||||
});
|
||||
|
||||
socket.on("RELOAD", () => {
|
||||
Log.warn("Reload notification received from server");
|
||||
window.location.reload(true);
|
||||
});
|
||||
}
|
||||
|
||||
if (config.reloadAfterServerRestart) {
|
||||
setInterval(async () => {
|
||||
// if server startup time has changed (which means server was restarted)
|
||||
// the client reloads the mm page
|
||||
try {
|
||||
const res = await fetch(`${location.protocol}//${location.host}/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}`);
|
||||
@@ -633,7 +640,7 @@ const MM = (function () {
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {object} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
*/
|
||||
sendNotification (notification, payload, sender) {
|
||||
@@ -673,7 +680,10 @@ const MM = (function () {
|
||||
}
|
||||
|
||||
// Further implementation is done in the private method.
|
||||
updateDom(module, updateOptions);
|
||||
updateDom(module, updateOptions).then(function () {
|
||||
// Once the update is complete and rendered, send a notification to the module that the DOM has been updated
|
||||
sendNotification("MODULE_DOM_UPDATED", null, null, module);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -689,7 +699,7 @@ const MM = (function () {
|
||||
* Hide the module.
|
||||
* @param {Module} module The module to hide.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {Promise} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
hideModule (module, speed, callback, options) {
|
||||
@@ -701,13 +711,16 @@ const MM = (function () {
|
||||
* Show the module.
|
||||
* @param {Module} module The module to show.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {Promise} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
showModule (module, speed, callback, options) {
|
||||
// do not change module.hidden yet, only if we really show it later
|
||||
showModule(module, speed, callback, options);
|
||||
}
|
||||
},
|
||||
|
||||
// Return all available module positions.
|
||||
getAvailableModulePositions: modulePositions
|
||||
};
|
||||
}());
|
||||
|
||||
|
||||
68
js/module.js
68
js/module.js
@@ -1,17 +1,16 @@
|
||||
/* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */
|
||||
|
||||
/* MagicMirror²
|
||||
/*
|
||||
* Module Blueprint.
|
||||
* @typedef {Object} Module
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Module = Class.extend({
|
||||
|
||||
/*********************************************************
|
||||
* All methods (and properties) below can be subclassed. *
|
||||
*********************************************************/
|
||||
/**
|
||||
*********************************************************
|
||||
* All methods (and properties) below can be overridden. *
|
||||
*********************************************************
|
||||
*/
|
||||
|
||||
// Set the minimum MagicMirror² module version for this module.
|
||||
requiresVersion: "2.0.0",
|
||||
@@ -22,20 +21,23 @@ const Module = Class.extend({
|
||||
// Timer reference used for showHide animation callbacks.
|
||||
showHideTimer: null,
|
||||
|
||||
// Array to store lockStrings. These strings are used to lock
|
||||
// visibility when hiding and showing module.
|
||||
/*
|
||||
* Array to store lockStrings. These strings are used to lock
|
||||
* visibility when hiding and showing module.
|
||||
*/
|
||||
lockStrings: [],
|
||||
|
||||
// Storage of the nunjucks Environment,
|
||||
// This should not be referenced directly.
|
||||
// Use the nunjucksEnvironment() to get it.
|
||||
/*
|
||||
* Storage of the nunjucks Environment,
|
||||
* This should not be referenced directly.
|
||||
* Use the nunjucksEnvironment() to get it.
|
||||
*/
|
||||
_nunjucksEnvironment: null,
|
||||
|
||||
/**
|
||||
* Called when the module is instantiated.
|
||||
*/
|
||||
init () {
|
||||
//Log.log(this.defaults);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -65,7 +67,7 @@ const Module = Class.extend({
|
||||
* Returns a map of translation files the module requires to be loaded.
|
||||
*
|
||||
* return Map<String, String> -
|
||||
* @returns {*} A map with langKeys and filenames.
|
||||
* @returns {Map} A map with langKeys and filenames.
|
||||
*/
|
||||
getTranslations () {
|
||||
return false;
|
||||
@@ -73,8 +75,8 @@ const Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Generates the dom which needs to be displayed. This method is called by the MagicMirror² core.
|
||||
* This method can to be subclassed if the module wants to display info on the mirror.
|
||||
* Alternatively, the getTemplate method could be subclassed.
|
||||
* This method can to be overridden if the module wants to display info on the mirror.
|
||||
* Alternatively, the getTemplate method could be overridden.
|
||||
* @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
|
||||
*/
|
||||
getDom () {
|
||||
@@ -107,7 +109,7 @@ const Module = Class.extend({
|
||||
/**
|
||||
* Generates the header string which needs to be displayed if a user has a header configured for this module.
|
||||
* This method is called by the MagicMirror² core, but only if the user has configured a default header for the module.
|
||||
* This method needs to be subclassed if the module wants to display modified headers on the mirror.
|
||||
* This method needs to be overridden if the module wants to display modified headers on the mirror.
|
||||
* @returns {string} The header to display above the header.
|
||||
*/
|
||||
getHeader () {
|
||||
@@ -116,8 +118,8 @@ const Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Returns the template for the module which is used by the default getDom implementation.
|
||||
* This method needs to be subclassed if the module wants to use a template.
|
||||
* It can either return a template sting, or a template filename.
|
||||
* This method needs to be overridden if the module wants to use a template.
|
||||
* It can either return a template string, or a template filename.
|
||||
* If the string ends with '.html' it's considered a file from within the module's folder.
|
||||
* @returns {string} The template string of filename.
|
||||
*/
|
||||
@@ -127,7 +129,7 @@ const Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Returns the data to be used in the template.
|
||||
* This method needs to be subclassed if the module wants to use a custom data.
|
||||
* This method needs to be overridden if the module wants to use a custom data.
|
||||
* @returns {object} The data for the template
|
||||
*/
|
||||
getTemplateData () {
|
||||
@@ -137,14 +139,14 @@ const Module = Class.extend({
|
||||
/**
|
||||
* Called by the MagicMirror² core when a notification arrives.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {object} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
*/
|
||||
notificationReceived (notification, payload, sender) {
|
||||
if (sender) {
|
||||
// Log.log(this.name + " received a module notification: " + notification + " from sender: " + sender.name);
|
||||
Log.debug(`${this.name} received a module notification: ${notification} from sender: ${sender.name}`);
|
||||
} else {
|
||||
// Log.log(this.name + " received a system notification: " + notification);
|
||||
Log.debug(`${this.name} received a system notification: ${notification}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -173,7 +175,7 @@ const Module = Class.extend({
|
||||
/**
|
||||
* Called when a socket notification arrives.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {object} payload The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived (notification, payload) {
|
||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||
@@ -193,9 +195,11 @@ const Module = Class.extend({
|
||||
Log.log(`${this.name} is resumed.`);
|
||||
},
|
||||
|
||||
/*********************************************
|
||||
* The methods below don't need subclassing. *
|
||||
*********************************************/
|
||||
/**
|
||||
***********************************************
|
||||
* The methods below should not be overridden. *
|
||||
***********************************************
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set the module data.
|
||||
@@ -339,7 +343,7 @@ const Module = Class.extend({
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {object} payload The payload of the notification.
|
||||
*/
|
||||
sendNotification (notification, payload) {
|
||||
MM.sendNotification(notification, payload, this);
|
||||
@@ -348,7 +352,7 @@ const Module = Class.extend({
|
||||
/**
|
||||
* Send a socket notification to the node helper.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {object} payload The payload of the notification.
|
||||
*/
|
||||
sendSocketNotification (notification, payload) {
|
||||
this.socket().sendNotification(notification, payload);
|
||||
@@ -357,7 +361,7 @@ const Module = Class.extend({
|
||||
/**
|
||||
* Hide this module.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {Promise} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
hide (speed, callback, options = {}) {
|
||||
@@ -384,7 +388,7 @@ const Module = Class.extend({
|
||||
/**
|
||||
* Show this module.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {Promise} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
show (speed, callback, options) {
|
||||
@@ -410,7 +414,7 @@ const Module = Class.extend({
|
||||
});
|
||||
|
||||
/**
|
||||
* Merging MagicMirror² (or other) default/config script by @bugsounet
|
||||
* Merging MagicMirror² (or other) default/config script by `@bugsounet`
|
||||
* Merge 2 objects or/with array
|
||||
*
|
||||
* Usage:
|
||||
|
||||
18
js/module_functions.js
Normal file
18
js/module_functions.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Schedule the timer for the next update
|
||||
* @param {object} timer The timer of the module
|
||||
* @param {bigint} intervalMS interval in milliseconds
|
||||
* @param {Promise} callback function to call when the timer expires
|
||||
*/
|
||||
const scheduleTimer = function (timer, intervalMS, callback) {
|
||||
if (process.env.mmTestMode !== "true") {
|
||||
// only set timer when not running in test mode
|
||||
let tmr = timer;
|
||||
clearTimeout(tmr);
|
||||
tmr = setTimeout(function () {
|
||||
callback();
|
||||
}, intervalMS);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { scheduleTimer };
|
||||
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Node Helper Superclass
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const express = require("express");
|
||||
const Log = require("logger");
|
||||
const Class = require("./class");
|
||||
@@ -33,7 +27,7 @@ const NodeHelper = Class.extend({
|
||||
/**
|
||||
* This method is called when a socket notification arrives.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {object} payload The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived (notification, payload) {
|
||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||
@@ -55,7 +49,8 @@ const NodeHelper = Class.extend({
|
||||
this.path = path;
|
||||
},
|
||||
|
||||
/* sendSocketNotification(notification, payload)
|
||||
/*
|
||||
* sendSocketNotification(notification, payload)
|
||||
* Send a socket notification to the node helper.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
@@ -65,7 +60,8 @@ const NodeHelper = Class.extend({
|
||||
this.io.of(this.name).emit(notification, payload);
|
||||
},
|
||||
|
||||
/* setExpressApp(app)
|
||||
/*
|
||||
* setExpressApp(app)
|
||||
* Sets the express app object for this module.
|
||||
* This allows you to host files from the created webserver.
|
||||
*
|
||||
@@ -77,7 +73,8 @@ const NodeHelper = Class.extend({
|
||||
app.use(`/${this.name}`, express.static(`${this.path}/public`));
|
||||
},
|
||||
|
||||
/* setSocketIO(io)
|
||||
/*
|
||||
* setSocketIO(io)
|
||||
* Sets the socket io object for this module.
|
||||
* Binds message receiver.
|
||||
*
|
||||
@@ -89,20 +86,9 @@ const NodeHelper = Class.extend({
|
||||
Log.log(`Connecting socket for: ${this.name}`);
|
||||
|
||||
io.of(this.name).on("connection", (socket) => {
|
||||
// add a catch all event.
|
||||
const onevent = socket.onevent;
|
||||
socket.onevent = function (packet) {
|
||||
const args = packet.data || [];
|
||||
onevent.call(this, packet); // original call
|
||||
packet.data = ["*"].concat(args);
|
||||
onevent.call(this, packet); // additional call to catch-all
|
||||
};
|
||||
|
||||
// register catch all.
|
||||
socket.on("*", (notification, payload) => {
|
||||
if (notification !== "*") {
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
}
|
||||
socket.onAny((notification, payload) => {
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -127,8 +113,11 @@ NodeHelper.checkFetchError = function (error) {
|
||||
let error_type = "MODULE_ERROR_UNSPECIFIED";
|
||||
if (error.code === "EAI_AGAIN") {
|
||||
error_type = "MODULE_ERROR_NO_CONNECTION";
|
||||
} else if (error.message === "Unauthorized") {
|
||||
error_type = "MODULE_ERROR_UNAUTHORIZED";
|
||||
} else {
|
||||
const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
|
||||
if (message.includes("unauthorized") || message.includes("http 401") || message.includes("http 403")) {
|
||||
error_type = "MODULE_ERROR_UNAUTHORIZED";
|
||||
}
|
||||
}
|
||||
return error_type;
|
||||
};
|
||||
|
||||
198
js/releasenotes.js
Normal file
198
js/releasenotes.js
Normal file
@@ -0,0 +1,198 @@
|
||||
/* eslint no-console: "off" */
|
||||
const util = require("node:util");
|
||||
const exec = util.promisify(require("node:child_process").exec);
|
||||
const fs = require("node:fs");
|
||||
|
||||
const createReleaseNotes = async () => {
|
||||
let repoName = "MagicMirrorOrg/MagicMirror";
|
||||
if (process.env.GITHUB_REPOSITORY) {
|
||||
repoName = process.env.GITHUB_REPOSITORY;
|
||||
}
|
||||
const baseUrl = `https://api.github.com/repos/${repoName}`;
|
||||
|
||||
const getOptions = (type) => {
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
return { method: `${type}`, headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } };
|
||||
} else {
|
||||
return { method: `${type}` };
|
||||
}
|
||||
};
|
||||
|
||||
const execShell = async (command) => {
|
||||
const { stdout = "", stderr = "" } = await exec(command);
|
||||
if (stderr) console.error(`Error in execShell executing command ${command}: ${stderr}`);
|
||||
return stdout;
|
||||
};
|
||||
|
||||
// Check Draft Release
|
||||
const draftReleases = [];
|
||||
const jsonReleases = await fetch(`${baseUrl}/releases`, getOptions("GET")).then((res) => res.json());
|
||||
for (const rel of jsonReleases) {
|
||||
if (rel.draft && rel.tag_name === "" && rel.published_at === null && rel.name === "unreleased") draftReleases.push(rel);
|
||||
}
|
||||
|
||||
let draftReleaseId = 0;
|
||||
if (draftReleases.length > 1) {
|
||||
throw new Error("More than one draft release found, exiting.");
|
||||
} else {
|
||||
if (draftReleases[0]) draftReleaseId = draftReleases[0].id;
|
||||
}
|
||||
|
||||
// Get last Git Tag
|
||||
const gitTag = await execShell("git describe --tags `git rev-list --tags --max-count=1`");
|
||||
const lastTag = gitTag.toString().replaceAll("\n", "");
|
||||
console.info(`latest tag is ${lastTag}`);
|
||||
|
||||
// Get Git Commits
|
||||
const gitOut = await execShell(`git log develop --pretty=format:"%H --- %s" --after="$(git log -1 --format=%aI ${lastTag})"`);
|
||||
console.info(gitOut);
|
||||
const commits = gitOut.toString().split("\n");
|
||||
|
||||
// Get Node engine version from package.json
|
||||
const nodeVersion = JSON.parse(fs.readFileSync("package.json")).engines.node;
|
||||
|
||||
// Search strings
|
||||
const labelArr = ["alert", "calendar", "clock", "compliments", "helloworld", "newsfeed", "updatenotification", "weather", "envcanada", "openmeteo", "openweathermap", "smhi", "ukmetoffice", "yr", "eslint", "bump", "dependencies", "deps", "logg", "translation", "test", "ci"];
|
||||
|
||||
// Map search strings to categories
|
||||
const getFirstLabel = (text) => {
|
||||
let res;
|
||||
labelArr.every((item) => {
|
||||
const labelIncl = text.includes(item);
|
||||
if (labelIncl) {
|
||||
switch (item) {
|
||||
case "ci":
|
||||
case "test":
|
||||
res = "testing";
|
||||
break;
|
||||
case "logg":
|
||||
res = "logging";
|
||||
break;
|
||||
case "eslint":
|
||||
case "bump":
|
||||
case "deps":
|
||||
res = "dependencies";
|
||||
break;
|
||||
case "envcanada":
|
||||
case "openmeteo":
|
||||
case "openweathermap":
|
||||
case "smhi":
|
||||
case "ukmetoffice":
|
||||
case "yr":
|
||||
case "weather":
|
||||
res = "modules/weather";
|
||||
break;
|
||||
case "alert":
|
||||
res = "modules/alert";
|
||||
break;
|
||||
case "calendar":
|
||||
res = "modules/calendar";
|
||||
break;
|
||||
case "clock":
|
||||
res = "modules/clock";
|
||||
break;
|
||||
case "compliments":
|
||||
res = "modules/compliments";
|
||||
break;
|
||||
case "helloworld":
|
||||
res = "modules/helloworld";
|
||||
break;
|
||||
case "newsfeed":
|
||||
res = "modules/newsfeed";
|
||||
break;
|
||||
case "updatenotification":
|
||||
res = "modules/updatenotification";
|
||||
break;
|
||||
default:
|
||||
res = item;
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (!res) res = "core";
|
||||
return res;
|
||||
};
|
||||
|
||||
const grouped = {};
|
||||
const contrib = [];
|
||||
const sha = [];
|
||||
|
||||
// Loop through each Commit
|
||||
for (const item of commits) {
|
||||
|
||||
const cm = item.trim();
|
||||
// ignore `prepare release` line
|
||||
if (cm.length > 0 && !cm.match(/^.* --- prepare .*-develop$/gi)) {
|
||||
|
||||
const [ref, title] = cm.split(" --- ");
|
||||
|
||||
const groupTitle = getFirstLabel(title.toLowerCase());
|
||||
|
||||
if (!grouped[groupTitle]) {
|
||||
grouped[groupTitle] = [];
|
||||
}
|
||||
|
||||
grouped[groupTitle].push(`- ${title}`);
|
||||
|
||||
sha.push(ref);
|
||||
}
|
||||
}
|
||||
|
||||
// function to remove duplicates
|
||||
const sortedArr = (arr) => {
|
||||
return arr.filter((item,
|
||||
index) => (arr.indexOf(item) === index && item !== "@dependabot[bot]")).sort(function (a, b) {
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
});
|
||||
};
|
||||
|
||||
// Get Contributors logins
|
||||
for (const ref of sha) {
|
||||
const jsonRes = await fetch(`${baseUrl}/commits/${ref}`, getOptions("GET")).then((res) => res.json());
|
||||
|
||||
if (jsonRes && jsonRes.author && jsonRes.author.login) contrib.push(`@${jsonRes.author.login}`);
|
||||
}
|
||||
|
||||
// Build Markdown content
|
||||
let markdown = "## Release Notes\n";
|
||||
|
||||
markdown += `Thanks to: ${sortedArr(contrib).join(", ")}\n`;
|
||||
markdown += `> ⚠️ This release needs nodejs version ${nodeVersion}\n`;
|
||||
markdown += "\n";
|
||||
markdown += `[Compare to previous Release ${lastTag}](https://github.com/${repoName}/compare/${lastTag}...develop)\n\n`;
|
||||
|
||||
const sorted = Object.keys(grouped)
|
||||
.sort() // Sort the keys alphabetically
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = grouped[key]; // Rebuild the object with sorted keys
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
for (const group in sorted) {
|
||||
markdown += `\n### [${group}]\n`;
|
||||
markdown += `${sorted[group].join("\n")}\n`;
|
||||
}
|
||||
|
||||
console.info(markdown);
|
||||
|
||||
// Create Github Release
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
if (draftReleaseId > 0) {
|
||||
// delete release
|
||||
await fetch(`${baseUrl}/releases/${draftReleaseId}`, getOptions("DELETE"));
|
||||
console.info(`Old Release with id ${draftReleaseId} deleted.`);
|
||||
}
|
||||
|
||||
const relContent = getOptions("POST");
|
||||
relContent.body = JSON.stringify(
|
||||
{ tag_name: "", name: "unreleased", body: `${markdown}`, draft: true }
|
||||
);
|
||||
const createRelease = await fetch(`${baseUrl}/releases`, relContent).then((res) => res.json());
|
||||
console.info(`New release created with id ${createRelease.id}, GitHub-Url: ${createRelease.html_url}`);
|
||||
}
|
||||
};
|
||||
|
||||
createReleaseNotes();
|
||||
83
js/server.js
83
js/server.js
@@ -1,21 +1,16 @@
|
||||
/* MagicMirror²
|
||||
* Server
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const path = require("path");
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
const https = require("node:https");
|
||||
const path = require("node:path");
|
||||
const express = require("express");
|
||||
const ipfilter = require("express-ipfilter").IpFilter;
|
||||
const helmet = require("helmet");
|
||||
const socketio = require("socket.io");
|
||||
|
||||
const Log = require("logger");
|
||||
const Utils = require("./utils");
|
||||
const { cors, getConfig, getHtml, getVersion, getStartup } = require("./server_functions");
|
||||
const { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars } = require("#server_functions");
|
||||
|
||||
const { ipAccessControl } = require(`${__dirname}/ip_access_control`);
|
||||
|
||||
const vendor = require(`${__dirname}/vendor`);
|
||||
|
||||
/**
|
||||
* Server
|
||||
@@ -48,7 +43,9 @@ function Server (config) {
|
||||
origin: /.*$/,
|
||||
credentials: true
|
||||
},
|
||||
allowEIO3: true
|
||||
allowEIO3: true,
|
||||
pingInterval: 120000, // server → client ping every 2 mins
|
||||
pingTimeout: 120000 // wait up to 2 mins for client pong
|
||||
});
|
||||
|
||||
server.on("connection", (socket) => {
|
||||
@@ -59,29 +56,46 @@ function Server (config) {
|
||||
});
|
||||
|
||||
Log.log(`Starting server on port ${port} ... `);
|
||||
|
||||
// Add explicit error handling BEFORE calling listen so we can give user-friendly feedback
|
||||
server.once("error", (err) => {
|
||||
if (err && err.code === "EADDRINUSE") {
|
||||
const bindAddr = config.address || "localhost";
|
||||
const portInUseMessage = [
|
||||
"",
|
||||
"────────────────────────────────────────────────────────────────",
|
||||
` PORT IN USE: ${bindAddr}:${port}`,
|
||||
"",
|
||||
" Another process (most likely another MagicMirror instance)",
|
||||
" is already using this port.",
|
||||
"",
|
||||
" Stop the other process (free the port) or use a different port.",
|
||||
"────────────────────────────────────────────────────────────────"
|
||||
].join("\n");
|
||||
Log.error(portInUseMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.error("Failed to start server:", err);
|
||||
});
|
||||
|
||||
server.listen(port, config.address || "localhost");
|
||||
|
||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
Log.warn("You're using a full whitelist configuration to allow for all IPs");
|
||||
}
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) {
|
||||
if (err === undefined) {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
return next();
|
||||
}
|
||||
Log.log(err.message);
|
||||
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
|
||||
});
|
||||
});
|
||||
|
||||
app.use(ipAccessControl(config.ipWhitelist));
|
||||
app.use(helmet(config.httpHeaders));
|
||||
app.use("/js", express.static(__dirname));
|
||||
|
||||
// TODO add tests directory only when running tests?
|
||||
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs", "/tests/mocks"];
|
||||
for (const directory of directories) {
|
||||
let directories = ["/config", "/css", "/modules", "/node_modules/animate.css", "/node_modules/@fontsource", "/node_modules/@fortawesome", "/translations", "/tests/configs", "/tests/mocks"];
|
||||
for (const [key, value] of Object.entries(vendor)) {
|
||||
const dirArr = value.split("/");
|
||||
if (dirArr[0] === "node_modules") directories.push(`/${dirArr[0]}/${dirArr[1]}`);
|
||||
}
|
||||
const uniqDirs = [...new Set(directories)];
|
||||
for (const directory of uniqDirs) {
|
||||
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
||||
}
|
||||
|
||||
@@ -93,8 +107,17 @@ function Server (config) {
|
||||
|
||||
app.get("/startup", (req, res) => getStartup(req, res));
|
||||
|
||||
app.get("/env", (req, res) => getEnvVars(req, res));
|
||||
|
||||
app.get("/", (req, res) => getHtml(req, res));
|
||||
|
||||
// Reload endpoint for watch mode - triggers browser reload
|
||||
app.get("/reload", (req, res) => {
|
||||
Log.info("Reload request received, notifying all clients");
|
||||
io.emit("RELOAD");
|
||||
res.status(200).send("OK");
|
||||
});
|
||||
|
||||
server.on("listening", () => {
|
||||
resolve({
|
||||
app,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const Log = require("logger");
|
||||
|
||||
const startUp = new Date();
|
||||
@@ -30,6 +30,7 @@ function getStartup (req, res) {
|
||||
* Only the url-param of the input request url is required. It must be the last parameter.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
* @returns {Promise<void>} A promise that resolves when the response is sent
|
||||
*/
|
||||
async function cors (req, res) {
|
||||
try {
|
||||
@@ -40,26 +41,32 @@ async function cors (req, res) {
|
||||
if (!match) {
|
||||
url = `invalid url: ${req.url}`;
|
||||
Log.error(url);
|
||||
res.send(url);
|
||||
return res.status(400).send(url);
|
||||
} else {
|
||||
url = match[1];
|
||||
|
||||
const headersToSend = getHeadersToSend(req.url);
|
||||
const expectedRecievedHeaders = geExpectedRecievedHeaders(req.url);
|
||||
|
||||
const expectedReceivedHeaders = geExpectedReceivedHeaders(req.url);
|
||||
Log.log(`cors url: ${url}`);
|
||||
const response = await fetch(url, { headers: headersToSend });
|
||||
|
||||
for (const header of expectedRecievedHeaders) {
|
||||
const headerValue = response.headers.get(header);
|
||||
if (header) res.set(header, headerValue);
|
||||
const response = await fetch(url, { headers: headersToSend });
|
||||
if (response.ok) {
|
||||
for (const header of expectedReceivedHeaders) {
|
||||
const headerValue = response.headers.get(header);
|
||||
if (header) res.set(header, headerValue);
|
||||
}
|
||||
const data = await response.text();
|
||||
res.send(data);
|
||||
} else {
|
||||
throw new Error(`Response status: ${response.status}`);
|
||||
}
|
||||
const data = await response.text();
|
||||
res.send(data);
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error(error);
|
||||
res.send(error);
|
||||
// Only log errors in non-test environments to keep test output clean
|
||||
if (process.env.mmTestMode !== "true") {
|
||||
Log.error(`Error in CORS request: ${error}`);
|
||||
}
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +76,7 @@ async function cors (req, res) {
|
||||
* @returns {object} An object specifying name and value of the headers.
|
||||
*/
|
||||
function getHeadersToSend (url) {
|
||||
const headersToSend = { "User-Agent": `Mozilla/5.0 MagicMirror/${global.version}` };
|
||||
const headersToSend = { "User-Agent": getUserAgent() };
|
||||
const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (headersToSendMatch) {
|
||||
const headers = headersToSendMatch[1].split(",");
|
||||
@@ -89,16 +96,16 @@ function getHeadersToSend (url) {
|
||||
* @param {string} url - The url containing the expected headers from the response.
|
||||
* @returns {string[]} headers - The name of the expected headers.
|
||||
*/
|
||||
function geExpectedRecievedHeaders (url) {
|
||||
const expectedRecievedHeaders = ["Content-Type"];
|
||||
const expectedRecievedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (expectedRecievedHeadersMatch) {
|
||||
const headers = expectedRecievedHeadersMatch[1].split(",");
|
||||
function geExpectedReceivedHeaders (url) {
|
||||
const expectedReceivedHeaders = ["Content-Type"];
|
||||
const expectedReceivedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (expectedReceivedHeadersMatch) {
|
||||
const headers = expectedReceivedHeadersMatch[1].split(",");
|
||||
for (const header of headers) {
|
||||
expectedRecievedHeaders.push(header);
|
||||
expectedReceivedHeaders.push(header);
|
||||
}
|
||||
}
|
||||
return expectedRecievedHeaders;
|
||||
return expectedReceivedHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,6 +116,7 @@ function geExpectedRecievedHeaders (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") {
|
||||
@@ -128,4 +136,69 @@ function getVersion (req, res) {
|
||||
res.send(global.version);
|
||||
}
|
||||
|
||||
module.exports = { cors, getConfig, getHtml, getVersion, getStartup };
|
||||
/**
|
||||
* Gets the preferred `User-Agent`
|
||||
* @returns {string} `User-Agent` to be used
|
||||
*/
|
||||
function getUserAgent () {
|
||||
const defaultUserAgent = `Mozilla/5.0 (Node.js ${Number(process.version.match(/^v(\d+\.\d+)/)[1])}) MagicMirror/${global.version}`;
|
||||
|
||||
if (typeof config === "undefined") {
|
||||
return defaultUserAgent;
|
||||
}
|
||||
|
||||
switch (typeof config.userAgent) {
|
||||
case "function":
|
||||
return config.userAgent();
|
||||
case "string":
|
||||
return config.userAgent;
|
||||
default:
|
||||
return defaultUserAgent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets environment variables needed in the browser.
|
||||
* @returns {object} environment variables key: values
|
||||
*/
|
||||
function getEnvVarsAsObj () {
|
||||
const obj = { modulesDir: `${config.foreignModulesDir}`, customCss: `${config.customCss}` };
|
||||
if (process.env.MM_MODULES_DIR) {
|
||||
obj.modulesDir = process.env.MM_MODULES_DIR.replace(`${global.root_path}/`, "");
|
||||
}
|
||||
if (process.env.MM_CUSTOMCSS_FILE) {
|
||||
obj.customCss = process.env.MM_CUSTOMCSS_FILE.replace(`${global.root_path}/`, "");
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets environment variables needed in the browser.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getEnvVars (req, res) {
|
||||
const obj = getEnvVarsAsObj();
|
||||
res.send(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config file path from environment or default location
|
||||
* @returns {string} The absolute config file path
|
||||
*/
|
||||
function getConfigFilePath () {
|
||||
// Ensure root_path is set (for standalone contexts like watcher)
|
||||
if (!global.root_path) {
|
||||
global.root_path = path.resolve(`${__dirname}/../`);
|
||||
}
|
||||
|
||||
// Check environment variable if global not set
|
||||
if (!global.configuration_file && process.env.MM_CONFIG_FILE) {
|
||||
global.configuration_file = process.env.MM_CONFIG_FILE;
|
||||
}
|
||||
|
||||
return path.resolve(global.configuration_file || `${global.root_path}/config/config.js`);
|
||||
}
|
||||
|
||||
module.exports = { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj, getUserAgent, getConfigFilePath };
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/* global io */
|
||||
|
||||
/* MagicMirror²
|
||||
* TODO add description
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const MMSocket = function (moduleName) {
|
||||
if (typeof moduleName !== "string") {
|
||||
throw new Error("Please set the module name for the MMSocket.");
|
||||
@@ -19,7 +13,9 @@ const MMSocket = function (moduleName) {
|
||||
base = config.basePath;
|
||||
}
|
||||
this.socket = io(`/${this.moduleName}`, {
|
||||
path: `${base}socket.io`
|
||||
path: `${base}socket.io`,
|
||||
pingInterval: 120000, // send pings every 2 mins
|
||||
pingTimeout: 120000 // wait up to 2 mins for a pong
|
||||
});
|
||||
|
||||
let notificationCallback = function () {};
|
||||
|
||||
@@ -1,38 +1,26 @@
|
||||
/* global translations */
|
||||
|
||||
/* MagicMirror²
|
||||
* Translator (l10n)
|
||||
*
|
||||
* By Christopher Fenner https://github.com/CFenner
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Translator = (function () {
|
||||
|
||||
/**
|
||||
* Load a JSON file via XHR.
|
||||
* Load a JSON file via fetch.
|
||||
* @param {string} file Path of the file we want to load.
|
||||
* @returns {Promise<object>} the translations in the specified file
|
||||
*/
|
||||
async function loadJSON (file) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
return new Promise(function (resolve) {
|
||||
xhr.overrideMimeType("application/json");
|
||||
xhr.open("GET", file, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
// needs error handler try/catch at least
|
||||
let fileinfo = null;
|
||||
try {
|
||||
fileinfo = JSON.parse(xhr.responseText);
|
||||
} catch (exception) {
|
||||
// nothing here, but don't die
|
||||
Log.error(` loading json file =${file} failed`);
|
||||
}
|
||||
resolve(fileinfo);
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
});
|
||||
const baseHref = document.baseURI;
|
||||
const url = new URL(file, baseHref);
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unexpected response status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (exception) {
|
||||
Log.error(`Loading json file =${file} failed`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -73,22 +61,18 @@ const Translator = (function () {
|
||||
}
|
||||
|
||||
if (this.translations[module.name] && key in this.translations[module.name]) {
|
||||
// Log.log("Got translation for " + key + " from module translation: ");
|
||||
return createStringFromTemplate(this.translations[module.name][key], variables);
|
||||
}
|
||||
|
||||
if (key in this.coreTranslations) {
|
||||
// Log.log("Got translation for " + key + " from core translation.");
|
||||
return createStringFromTemplate(this.coreTranslations[key], variables);
|
||||
}
|
||||
|
||||
if (this.translationsFallback[module.name] && key in this.translationsFallback[module.name]) {
|
||||
// Log.log("Got translation for " + key + " from module translation fallback.");
|
||||
return createStringFromTemplate(this.translationsFallback[module.name][key], variables);
|
||||
}
|
||||
|
||||
if (key in this.coreTranslationsFallback) {
|
||||
// Log.log("Got translation for " + key + " from core translation fallback.");
|
||||
return createStringFromTemplate(this.coreTranslationsFallback[key], variables);
|
||||
}
|
||||
|
||||
@@ -102,7 +86,7 @@ const Translator = (function () {
|
||||
* @param {boolean} isFallback Flag to indicate fallback translations.
|
||||
*/
|
||||
async load (module, file, isFallback) {
|
||||
Log.log(`${module.name} - Load translation${isFallback ? " fallback" : ""}: ${file}`);
|
||||
Log.log(`[translator] ${module.name} - Load translation${isFallback ? " fallback" : ""}: ${file}`);
|
||||
|
||||
if (this.translationsFallback[module.name]) {
|
||||
return;
|
||||
@@ -119,10 +103,10 @@ const Translator = (function () {
|
||||
*/
|
||||
async loadCoreTranslations (lang) {
|
||||
if (lang in translations) {
|
||||
Log.log(`Loading core translation file: ${translations[lang]}`);
|
||||
Log.log(`[translator] Loading core translation file: ${translations[lang]}`);
|
||||
this.coreTranslations = await loadJSON(translations[lang]);
|
||||
} else {
|
||||
Log.log("Configured language not found in core translations.");
|
||||
Log.log("[translator] Configured language not found in core translations.");
|
||||
}
|
||||
|
||||
await this.loadCoreTranslationsFallback();
|
||||
@@ -135,7 +119,7 @@ const Translator = (function () {
|
||||
async loadCoreTranslationsFallback () {
|
||||
let first = Object.keys(translations)[0];
|
||||
if (first) {
|
||||
Log.log(`Loading core translation fallback file: ${translations[first]}`);
|
||||
Log.log(`[translator] Loading core translation fallback file: ${translations[first]}`);
|
||||
this.coreTranslationsFallback = await loadJSON(translations[first]);
|
||||
}
|
||||
}
|
||||
|
||||
92
js/utils.js
92
js/utils.js
@@ -1,16 +1,84 @@
|
||||
/* MagicMirror²
|
||||
* Utils
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const colors = require("colors/safe");
|
||||
const os = require("node:os");
|
||||
const fs = require("node:fs");
|
||||
const si = require("systeminformation");
|
||||
const Log = require("logger");
|
||||
|
||||
const modulePositions = []; // will get list from index.html
|
||||
const regionRegEx = /"region ([^"]*)/i;
|
||||
const indexFileName = "index.html";
|
||||
const discoveredPositionsJSFilename = "js/positions.js";
|
||||
|
||||
module.exports = {
|
||||
colors: {
|
||||
warn: colors.yellow,
|
||||
error: colors.red,
|
||||
info: colors.blue,
|
||||
pass: colors.green
|
||||
|
||||
async logSystemInformation (mirrorVersion) {
|
||||
try {
|
||||
const system = await si.system();
|
||||
const osInfo = await si.osInfo();
|
||||
const versions = await si.versions();
|
||||
|
||||
const usedNodeVersion = process.version.replace("v", "");
|
||||
const installedNodeVersion = versions.node;
|
||||
const totalRam = (os.totalmem() / 1024 / 1024).toFixed(2);
|
||||
const freeRam = (os.freemem() / 1024 / 1024).toFixed(2);
|
||||
const usedRam = ((os.totalmem() - os.freemem()) / 1024 / 1024).toFixed(2);
|
||||
|
||||
let systemDataString = [
|
||||
"\n#### System Information ####",
|
||||
`- SYSTEM: manufacturer: ${system.manufacturer}; model: ${system.model}; virtual: ${system.virtual}; MM: ${mirrorVersion}`,
|
||||
`- OS: platform: ${osInfo.platform}; distro: ${osInfo.distro}; release: ${osInfo.release}; arch: ${osInfo.arch}; kernel: ${versions.kernel}`,
|
||||
`- VERSIONS: electron: ${process.versions.electron}; used node: ${usedNodeVersion}; installed node: ${installedNodeVersion}; npm: ${versions.npm}; pm2: ${versions.pm2}`,
|
||||
`- ENV: XDG_SESSION_TYPE: ${process.env.XDG_SESSION_TYPE}; MM_CONFIG_FILE: ${process.env.MM_CONFIG_FILE}`,
|
||||
` WAYLAND_DISPLAY: ${process.env.WAYLAND_DISPLAY}; DISPLAY: ${process.env.DISPLAY}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`,
|
||||
`- RAM: total: ${totalRam} MB; free: ${freeRam} MB; used: ${usedRam} MB`,
|
||||
`- OTHERS: uptime: ${Math.floor(os.uptime() / 60)} minutes; timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}`
|
||||
].join("\n");
|
||||
Log.info(systemDataString);
|
||||
|
||||
// Return is currently only for tests
|
||||
return systemDataString;
|
||||
} catch (error) {
|
||||
Log.error(error);
|
||||
}
|
||||
},
|
||||
|
||||
// return all available module positions
|
||||
getAvailableModulePositions () {
|
||||
return modulePositions;
|
||||
},
|
||||
|
||||
// return if position is on modulePositions Array (true/false)
|
||||
moduleHasValidPosition (position) {
|
||||
if (this.getAvailableModulePositions().indexOf(position) === -1) return false;
|
||||
return true;
|
||||
},
|
||||
|
||||
getModulePositions () {
|
||||
// if not already discovered
|
||||
if (modulePositions.length === 0) {
|
||||
// get the lines of the index.html
|
||||
const lines = fs.readFileSync(indexFileName).toString().split("\n");
|
||||
// loop thru the lines
|
||||
lines.forEach((line) => {
|
||||
// run the regex on each line
|
||||
const results = regionRegEx.exec(line);
|
||||
// if the regex returned something
|
||||
if (results && results.length > 0) {
|
||||
// get the position parts and replace space with underscore
|
||||
const positionName = results[1].replace(" ", "_");
|
||||
// add it to the list only if not already present (avoid duplicates)
|
||||
if (!modulePositions.includes(positionName)) {
|
||||
modulePositions.push(positionName);
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
fs.writeFileSync(discoveredPositionsJSFilename, `const modulePositions=${JSON.stringify(modulePositions)}`);
|
||||
}
|
||||
catch (error) {
|
||||
Log.error("unable to write js/positions.js with the discovered module positions\nmake the MagicMirror/js folder writeable by the user starting MagicMirror");
|
||||
}
|
||||
}
|
||||
// return the list to the caller
|
||||
return modulePositions;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Vendor File Definition
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const vendor = {
|
||||
"moment.js": "node_modules/moment/min/moment-with-locales.js",
|
||||
"moment-timezone.js": "node_modules/moment-timezone/builds/moment-timezone-with-data.js",
|
||||
@@ -11,7 +5,8 @@ const vendor = {
|
||||
"weather-icons-wind.css": "node_modules/weathericons/css/weather-icons-wind.css",
|
||||
"font-awesome.css": "css/font-awesome.css",
|
||||
"nunjucks.js": "node_modules/nunjucks/browser/nunjucks.min.js",
|
||||
"suncalc.js": "node_modules/suncalc/suncalc.js"
|
||||
"suncalc.js": "node_modules/suncalc/suncalc.js",
|
||||
"croner.js": "node_modules/croner/dist/croner.umd.js"
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
@@ -1,11 +1,5 @@
|
||||
/* global NotificationFx */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: alert
|
||||
*
|
||||
* By Paul-Vincent Roll https://paulvincentroll.com/
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("alert", {
|
||||
alerts: {},
|
||||
|
||||
@@ -31,10 +25,13 @@ 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",
|
||||
nl: "translations/nl.json",
|
||||
pt: "translations/pt.json",
|
||||
"pt-br": "translations/pt-br.json",
|
||||
ru: "translations/ru.json",
|
||||
th: "translations/th.json"
|
||||
};
|
||||
@@ -129,7 +126,7 @@ Module.register("alert", {
|
||||
return new Promise((resolve) => {
|
||||
this.nunjucksEnvironment().render(this.getTemplate(type), data, function (err, res) {
|
||||
if (err) {
|
||||
Log.error("Failed to render alert", err);
|
||||
Log.error("[alert] Failed to render alert", err);
|
||||
}
|
||||
|
||||
resolve(res);
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
// notice, warning, error, success
|
||||
// will add class ns-type-warning, ns-type-error or ns-type-success
|
||||
type: "notice",
|
||||
// if the user doesn´t close the notification then we remove it
|
||||
// if the user doesn't close the notification then we remove it
|
||||
// after the following time
|
||||
ttl: 6000,
|
||||
al_no: "ns-box",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
font-size: 70%;
|
||||
position: relative;
|
||||
display: table;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
@@ -35,7 +35,7 @@
|
||||
top: 40%;
|
||||
width: 40%;
|
||||
height: auto;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{% if imageUrl or imageFA %}
|
||||
{% set imageHeight = imageHeight if imageHeight else "80px" %}
|
||||
{% if imageUrl %}
|
||||
<img src="{{ imageUrl }}"
|
||||
height="{{ imageHeight }}"
|
||||
style="margin-bottom: 10px" />
|
||||
{% else %}
|
||||
<span class="bright fas fa-{{ imageFA }}"
|
||||
style="margin-bottom: 10px;
|
||||
font-size: {{ imageHeight }}"></span>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% set imageHeight = imageHeight if imageHeight else "80px" %}
|
||||
{% if imageUrl %}
|
||||
<img src="{{ imageUrl }}" height="{{ imageHeight }}" style="margin-bottom: 10px" />
|
||||
{% else %}
|
||||
<span
|
||||
class="bright fas fa-{{ imageFA }}"
|
||||
style="margin-bottom: 10px;
|
||||
font-size: {{ imageHeight }}"
|
||||
></span>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% endif %}
|
||||
|
||||
4
modules/default/alert/translations/el.json
Normal file
4
modules/default/alert/translations/el.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror² Ειδοποίηση",
|
||||
"welcome": "Καλώς ήρθατε, η εκκίνηση ήταν επιτυχής!"
|
||||
}
|
||||
4
modules/default/alert/translations/eo.json
Normal file
4
modules/default/alert/translations/eo.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror²-sciigo",
|
||||
"welcome": "Bonvenon, lanĉo sukcesis!"
|
||||
}
|
||||
4
modules/default/alert/translations/pt-br.json
Normal file
4
modules/default/alert/translations/pt-br.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sysTitle": "Notificação do MagicMirror²",
|
||||
"welcome": "Bem-vindo, o sistema iniciou com sucesso!"
|
||||
}
|
||||
4
modules/default/alert/translations/pt.json
Normal file
4
modules/default/alert/translations/pt.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sysTitle": "Notificação do MagicMirror²",
|
||||
"welcome": "Bem-vindo, o sistema iniciou com sucesso!"
|
||||
}
|
||||
@@ -2,23 +2,14 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding-left: 0;
|
||||
padding-right: 10px;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.calendar .symbol span {
|
||||
padding-top: 4px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.calendar .title {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
vertical-align: top;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.calendar .time {
|
||||
padding-left: 30px;
|
||||
padding-left: 20px;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/* global CalendarUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Calendar
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("calendar", {
|
||||
// Define module defaults
|
||||
defaults: {
|
||||
@@ -14,7 +8,7 @@ Module.register("calendar", {
|
||||
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
||||
pastDaysCount: 0,
|
||||
displaySymbol: true,
|
||||
defaultSymbol: "calendar-alt", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
||||
defaultSymbol: "calendar-days", // Fontawesome Symbol see https://fontawesome.com/search?ic=free&o=r
|
||||
defaultSymbolClassName: "fas fa-fw fa-",
|
||||
showLocation: false,
|
||||
displayRepeatingCountTitle: false,
|
||||
@@ -35,6 +29,7 @@ Module.register("calendar", {
|
||||
dateEndFormat: "LT",
|
||||
fullDayEventDateFormat: "MMM Do",
|
||||
showEnd: false,
|
||||
showEndsOnlyWithDuration: false,
|
||||
getRelative: 6,
|
||||
hidePrivate: false,
|
||||
hideOngoing: false,
|
||||
@@ -42,6 +37,7 @@ Module.register("calendar", {
|
||||
hideDuplicates: true,
|
||||
showTimeToday: false,
|
||||
colored: false,
|
||||
forceUseCurrentTime: false,
|
||||
tableClass: "small",
|
||||
calendars: [
|
||||
{
|
||||
@@ -72,8 +68,6 @@ Module.register("calendar", {
|
||||
updateOnFetch: true
|
||||
},
|
||||
|
||||
requiresVersion: "2.1.0",
|
||||
|
||||
// Define required scripts.
|
||||
getStyles () {
|
||||
return ["calendar.css", "font-awesome.css"];
|
||||
@@ -81,14 +75,17 @@ Module.register("calendar", {
|
||||
|
||||
// Define required scripts.
|
||||
getScripts () {
|
||||
return ["calendarutils.js", "moment.js"];
|
||||
return ["calendarutils.js", "moment.js", "moment-timezone.js"];
|
||||
},
|
||||
|
||||
// Define required translations.
|
||||
getTranslations () {
|
||||
// The translations for the default modules are defined in the core translation files.
|
||||
// Therefore we can just return false. Otherwise we should have returned a dictionary.
|
||||
// If you're trying to build your own module including translations, check out the documentation.
|
||||
|
||||
/*
|
||||
* The translations for the default modules are defined in the core translation files.
|
||||
* Therefore we can just return false. Otherwise we should have returned a dictionary.
|
||||
* If you're trying to build your own module including translations, check out the documentation.
|
||||
*/
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -97,12 +94,12 @@ Module.register("calendar", {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
if (this.config.colored) {
|
||||
Log.warn("Your are using the deprecated config values 'colored'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
||||
Log.warn("[calendar] Your are using the deprecated config values 'colored'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
||||
this.config.coloredText = true;
|
||||
this.config.coloredSymbol = true;
|
||||
}
|
||||
if (this.config.coloredSymbolOnly) {
|
||||
Log.warn("Your are using the deprecated config values 'coloredSymbolOnly'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
||||
Log.warn("[calendar] Your are using the deprecated config values 'coloredSymbolOnly'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
||||
this.config.coloredText = false;
|
||||
this.config.coloredSymbol = true;
|
||||
}
|
||||
@@ -144,7 +141,7 @@ Module.register("calendar", {
|
||||
|
||||
// we check user and password here for backwards compatibility with old configs
|
||||
if (calendar.user && calendar.pass) {
|
||||
Log.warn("Deprecation warning: Please update your calendar authentication configuration.");
|
||||
Log.warn("[calendar] Deprecation warning: Please update your calendar authentication configuration.");
|
||||
Log.warn("https://docs.magicmirror.builders/modules/calendar.html#configuration-options");
|
||||
calendar.auth = {
|
||||
user: calendar.user,
|
||||
@@ -152,14 +149,16 @@ Module.register("calendar", {
|
||||
};
|
||||
}
|
||||
|
||||
// tell helper to start a fetcher for this calendar
|
||||
// fetcher till cycle
|
||||
/*
|
||||
* tell helper to start a fetcher for this calendar
|
||||
* fetcher till cycle
|
||||
*/
|
||||
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
});
|
||||
|
||||
// for backward compatibility titleReplace
|
||||
if (typeof this.config.titleReplace !== "undefined") {
|
||||
Log.warn("Deprecation warning: Please consider upgrading your calendar titleReplace configuration to customEvents.");
|
||||
Log.warn("[calendar] Deprecation warning: Please consider upgrading your calendar titleReplace configuration to customEvents.");
|
||||
for (const [titlesearchstr, titlereplacestr] of Object.entries(this.config.titleReplace)) {
|
||||
this.config.customEvents.push({ keyword: ".*", transform: { search: titlesearchstr, replace: titlereplacestr } });
|
||||
}
|
||||
@@ -168,11 +167,16 @@ 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;
|
||||
@@ -180,13 +184,27 @@ Module.register("calendar", {
|
||||
|
||||
if (notification === "CALENDAR_EVENTS") {
|
||||
if (this.hasCalendarURL(payload.url)) {
|
||||
this.calendarData[payload.url] = payload.events;
|
||||
// have we received events for this url
|
||||
if (!this.calendarData[payload.url]) {
|
||||
// no, setup the structure to hold the info
|
||||
this.calendarData[payload.url] = { events: null, checksum: null };
|
||||
}
|
||||
// save the event list
|
||||
this.calendarData[payload.url].events = payload.events;
|
||||
|
||||
this.error = null;
|
||||
this.loaded = true;
|
||||
|
||||
if (this.config.broadcastEvents) {
|
||||
this.broadcastEvents();
|
||||
}
|
||||
// if the checksum is the same
|
||||
if (this.calendarData[payload.url].checksum === payload.checksum) {
|
||||
// then don't update the UI
|
||||
return;
|
||||
}
|
||||
// haven't seen or the checksum is different
|
||||
this.calendarData[payload.url].checksum = payload.checksum;
|
||||
|
||||
if (!this.config.updateOnFetch) {
|
||||
if (this.calendarDisplayer[payload.url] === undefined) {
|
||||
@@ -195,7 +213,7 @@ Module.register("calendar", {
|
||||
// set this calendar as displayed
|
||||
this.calendarDisplayer[payload.url] = true;
|
||||
} else {
|
||||
Log.debug("[Calendar] DOM not updated waiting self update()");
|
||||
Log.debug("[calendar] DOM not updated waiting self update()");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -211,11 +229,6 @@ Module.register("calendar", {
|
||||
|
||||
// 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;
|
||||
@@ -247,7 +260,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");
|
||||
@@ -304,15 +319,12 @@ Module.register("calendar", {
|
||||
}
|
||||
|
||||
const symbolClass = this.symbolClassForUrl(event.url);
|
||||
symbolWrapper.className = `symbol align-right ${symbolClass}`;
|
||||
symbolWrapper.className = `symbol ${symbolClass}`;
|
||||
|
||||
const symbols = this.symbolsForEvent(event);
|
||||
symbols.forEach((s, index) => {
|
||||
symbols.forEach((s) => {
|
||||
const symbol = document.createElement("span");
|
||||
symbol.className = s;
|
||||
if (index > 0) {
|
||||
symbol.style.paddingLeft = "5px";
|
||||
}
|
||||
symbolWrapper.appendChild(symbol);
|
||||
});
|
||||
eventWrapper.appendChild(symbolWrapper);
|
||||
@@ -329,10 +341,12 @@ 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}`;
|
||||
if (yearDiff > 0) {
|
||||
repeatingCountTitle = `, ${yearDiff} ${repeatingCountTitle}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,11 +398,15 @@ Module.register("calendar", {
|
||||
timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`;
|
||||
timeWrapper.style.paddingLeft = "2px";
|
||||
timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left";
|
||||
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
|
||||
timeWrapper.innerHTML = eventStartDateMoment.format("LT");
|
||||
|
||||
// Add endDate to dataheaders if showEnd is enabled
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(moment(event.endDate, "x").format("LT"))}`;
|
||||
if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) {
|
||||
// no duration here, don't display end
|
||||
} else {
|
||||
timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(eventEndDateMoment.format("LT"))}`;
|
||||
}
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
@@ -400,32 +418,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) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||
// and has a duration
|
||||
if (event.startDate !== event.endDate) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.dateEndFormat));
|
||||
}
|
||||
}
|
||||
|
||||
// For full day events we use the fullDayEventDateFormat
|
||||
if (event.fullDayEvent) {
|
||||
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||
event.endDate -= ONE_SECOND;
|
||||
timeWrapper.innerHTML = 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
|
||||
@@ -433,9 +462,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"));
|
||||
}
|
||||
@@ -443,23 +472,25 @@ Module.register("calendar", {
|
||||
}
|
||||
} else {
|
||||
// Show relative times
|
||||
if (event.startDate >= now || (event.fullDayEvent && event.today)) {
|
||||
if (eventStartDateMoment.isSameOrAfter(now) || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) {
|
||||
// Use relative time
|
||||
if (!this.config.hideTime && !event.fullDayEvent) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
|
||||
Log.debug("[calendar] 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("[calendar] 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) {
|
||||
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") {
|
||||
@@ -467,23 +498,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("[calendar] event fullday");
|
||||
} else if (eventStartDateMoment.diff(now, "h") < this.config.getRelative) {
|
||||
Log.info("[calendar] 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)
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -562,97 +595,113 @@ 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 future = now.clone().startOf("day").add(this.config.maximumNumberOfDays, "days");
|
||||
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day");
|
||||
const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
||||
let events = [];
|
||||
|
||||
for (const calendarUrl in this.calendarData) {
|
||||
const calendar = this.calendarData[calendarUrl];
|
||||
const calendar = this.calendarData[calendarUrl].events;
|
||||
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,
|
||||
/*
|
||||
* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
|
||||
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
|
||||
*/
|
||||
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / ONE_DAY) + 1;
|
||||
const maxCount = eventEndDateMoment.diff(eventStartDateMoment, "days");
|
||||
if (this.config.sliceMultiDayEvents && maxCount > 1) {
|
||||
const splitEvents = [];
|
||||
let midnight
|
||||
= moment(event.startDate, "x")
|
||||
= eventStartDateMoment
|
||||
.clone()
|
||||
.startOf("day")
|
||||
.add(1, "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 = midnight;
|
||||
thisEvent.today = this.timestampToMoment(thisEvent.startDate).isSame(now, "d");
|
||||
thisEvent.tomorrow = this.timestampToMoment(thisEvent.startDate).isSame(now.clone().add(1, "days"), "d");
|
||||
thisEvent.endDate = midnight.clone().subtract(1, "day").format("x");
|
||||
thisEvent.title += ` (${count}/${maxCount})`;
|
||||
splitEvents.push(thisEvent);
|
||||
|
||||
event.startDate = midnight;
|
||||
event.startDate = midnight.format("x");
|
||||
count += 1;
|
||||
midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
|
||||
midnight = midnight.clone().add(1, "day").endOf("day"); // next day
|
||||
}
|
||||
// Last day
|
||||
event.title += ` (${count}/${maxCount})`;
|
||||
event.today += event.startDate >= today && event.startDate < today + ONE_DAY;
|
||||
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
|
||||
event.today += this.timestampToMoment(event.startDate).isSame(now, "d");
|
||||
event.tomorrow = this.timestampToMoment(event.startDate).isSame(now.clone().add(1, "days"), "d");
|
||||
splitEvents.push(event);
|
||||
|
||||
for (let splitEvent of splitEvents) {
|
||||
if (splitEvent.endDate > now && splitEvent.endDate <= future) {
|
||||
events.push(splitEvent);
|
||||
if (this.timestampToMoment(splitEvent.endDate).isAfter(now) && this.timestampToMoment(splitEvent.endDate).isSameOrBefore(future)) {
|
||||
by_url_calevents.push(splitEvent);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
events.push(event);
|
||||
by_url_calevents.push(event);
|
||||
}
|
||||
}
|
||||
if (limitNumberOfEntries) {
|
||||
// sort entries before clipping
|
||||
by_url_calevents.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
Log.debug(`[calendar] pushing ${by_url_calevents.length} events to total with room for ${remainingEntries}`);
|
||||
events = events.concat(by_url_calevents.slice(0, remainingEntries));
|
||||
Log.debug(`[calendar] events for calendar=${events.length}`);
|
||||
} else {
|
||||
events = events.concat(by_url_calevents);
|
||||
}
|
||||
}
|
||||
|
||||
Log.info(`[calendar] sorting events count=${events.length}`);
|
||||
events.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
@@ -661,33 +710,32 @@ Module.register("calendar", {
|
||||
return events;
|
||||
}
|
||||
|
||||
// Limit the number of days displayed
|
||||
// If limitDays is set > 0, limit display to that number of days
|
||||
if (this.config.limitDays > 0) {
|
||||
let newEvents = [];
|
||||
let lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
|
||||
let days = 0;
|
||||
for (const ev of events) {
|
||||
let eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
|
||||
// if date of event is later than lastdate
|
||||
// check if we already are showing max unique days
|
||||
if (eventDate > lastDate) {
|
||||
// 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--;
|
||||
}
|
||||
days++;
|
||||
if (days > this.config.limitDays) {
|
||||
continue;
|
||||
} else {
|
||||
lastDate = eventDate;
|
||||
}
|
||||
/*
|
||||
* Limit the number of days displayed
|
||||
* If limitDays is set > 0, limit display to that number of days
|
||||
*/
|
||||
if (this.config.limitDays > 0 && events.length > 0) { // watch out for initial display before events arrive from helper
|
||||
// Group all events by date, events on the same date will be in a list with the key being the date.
|
||||
const eventsByDate = Object.groupBy(events, (ev) => this.timestampToMoment(ev.startDate).format("YYYY-MM-DD"));
|
||||
const newEvents = [];
|
||||
let currentDate = moment();
|
||||
let daysCollected = 0;
|
||||
|
||||
while (daysCollected < this.config.limitDays) {
|
||||
const dateStr = currentDate.format("YYYY-MM-DD");
|
||||
// Check if there are events on the currentDate
|
||||
if (eventsByDate[dateStr] && eventsByDate[dateStr].length > 0) {
|
||||
// If there are any events today then get all those events and select the currently active events and the events that are starting later in the day.
|
||||
newEvents.push(...eventsByDate[dateStr].filter((ev) => this.timestampToMoment(ev.endDate).isAfter(moment())));
|
||||
// Since we found a day with events, increase the daysCollected by 1
|
||||
daysCollected++;
|
||||
}
|
||||
newEvents.push(ev);
|
||||
// Search for the next day
|
||||
currentDate.add(1, "day");
|
||||
}
|
||||
events = newEvents;
|
||||
}
|
||||
|
||||
Log.info(`[calendar] slicing events total maxCount=${this.config.maximumEntries}`);
|
||||
return events.slice(0, this.config.maximumEntries);
|
||||
},
|
||||
|
||||
@@ -842,7 +890,7 @@ Module.register("calendar", {
|
||||
* @param {string} url The calendar url
|
||||
* @param {string} property The property to look for
|
||||
* @param {string} defaultValue The value if the property is not found
|
||||
* @returns {*} The property
|
||||
* @returns {string} The property
|
||||
*/
|
||||
getCalendarProperty (url, property, defaultValue) {
|
||||
for (const calendar of this.config.calendars) {
|
||||
@@ -858,9 +906,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;
|
||||
},
|
||||
@@ -871,7 +923,7 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Broadcasts the events to all other modules for reuse.
|
||||
* The all events available in one array, sorted on startdate.
|
||||
* The all events available in one array, sorted on startDate.
|
||||
*/
|
||||
broadcastEvents () {
|
||||
const eventList = this.createEventList(false);
|
||||
@@ -898,7 +950,7 @@ Module.register("calendar", {
|
||||
setTimeout(
|
||||
() => {
|
||||
setInterval(() => {
|
||||
Log.debug("[Calendar] self update");
|
||||
Log.debug("[calendar] self update");
|
||||
if (this.config.updateOnFetch) {
|
||||
this.updateDom(1);
|
||||
} else {
|
||||
|
||||
@@ -1,147 +1,222 @@
|
||||
/* MagicMirror²
|
||||
* Node Helper: Calendar - CalendarFetcher
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
const https = require("https");
|
||||
const https = require("node:https");
|
||||
const ical = require("node-ical");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
const CalendarFetcherUtils = require("./calendarfetcherutils");
|
||||
const { getUserAgent } = require("#server_functions");
|
||||
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
const THIRTY_MINUTES = 30 * 60 * 1000;
|
||||
const MAX_SERVER_BACKOFF = 3;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url The url of the calendar to fetch
|
||||
* @param {number} reloadInterval Time in ms the calendar is fetched again
|
||||
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
|
||||
* @param {number} maximumEntries The maximum number of events fetched.
|
||||
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
|
||||
* @param {object} auth The object containing options for authentication against the calendar.
|
||||
* @param {boolean} includePastEvents If true events from the past maximumNumberOfDays will be fetched too
|
||||
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
|
||||
* CalendarFetcher - Fetches and parses iCal calendar data with MagicMirror-focused error handling
|
||||
* @class
|
||||
*/
|
||||
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents, selfSignedCert) {
|
||||
let reloadTimer = null;
|
||||
let events = [];
|
||||
|
||||
let fetchFailedCallback = function () {};
|
||||
let eventsReceivedCallback = function () {};
|
||||
class CalendarFetcher {
|
||||
|
||||
/**
|
||||
* Initiates calendar fetch.
|
||||
* Creates a new CalendarFetcher instance
|
||||
* @param {string} url - The URL of the calendar to fetch
|
||||
* @param {number} reloadInterval - Time in ms between fetches
|
||||
* @param {string[]} excludedEvents - Event titles to exclude
|
||||
* @param {number} maximumEntries - Maximum number of events to return
|
||||
* @param {number} maximumNumberOfDays - Maximum days in the future to fetch
|
||||
* @param {object} auth - Authentication options {method: 'basic'|'bearer', user, pass}
|
||||
* @param {boolean} includePastEvents - Whether to include past events
|
||||
* @param {boolean} selfSignedCert - Whether to accept self-signed certificates
|
||||
*/
|
||||
const fetchCalendar = () => {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
let httpsAgent = null;
|
||||
let headers = {
|
||||
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`
|
||||
};
|
||||
constructor (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents, selfSignedCert) {
|
||||
this.url = url;
|
||||
this.reloadInterval = reloadInterval;
|
||||
this.excludedEvents = excludedEvents;
|
||||
this.maximumEntries = maximumEntries;
|
||||
this.maximumNumberOfDays = maximumNumberOfDays;
|
||||
this.auth = auth;
|
||||
this.includePastEvents = includePastEvents;
|
||||
this.selfSignedCert = selfSignedCert;
|
||||
|
||||
if (selfSignedCert) {
|
||||
httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
this.events = [];
|
||||
this.reloadTimer = null;
|
||||
this.serverErrorCount = 0;
|
||||
this.lastFetch = null;
|
||||
this.fetchFailedCallback = () => {};
|
||||
this.eventsReceivedCallback = () => {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any pending reload timer
|
||||
*/
|
||||
clearReloadTimer () {
|
||||
if (this.reloadTimer) {
|
||||
clearTimeout(this.reloadTimer);
|
||||
this.reloadTimer = null;
|
||||
}
|
||||
if (auth) {
|
||||
if (auth.method === "bearer") {
|
||||
headers.Authorization = `Bearer ${auth.pass}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the next fetch respecting MagicMirror test mode
|
||||
* @param {number} delay - Delay in milliseconds
|
||||
*/
|
||||
scheduleNextFetch (delay) {
|
||||
const nextDelay = Math.max(delay || this.reloadInterval, this.reloadInterval);
|
||||
if (process.env.mmTestMode === "true") {
|
||||
return;
|
||||
}
|
||||
this.reloadTimer = setTimeout(() => this.fetchCalendar(), nextDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the options object for fetch
|
||||
* @returns {object} Options object containing headers (and agent if needed)
|
||||
*/
|
||||
getRequestOptions () {
|
||||
const headers = { "User-Agent": getUserAgent() };
|
||||
const options = { headers };
|
||||
|
||||
if (this.selfSignedCert) {
|
||||
options.agent = new https.Agent({ rejectUnauthorized: false });
|
||||
}
|
||||
|
||||
if (this.auth) {
|
||||
if (this.auth.method === "bearer") {
|
||||
headers.Authorization = `Bearer ${this.auth.pass}`;
|
||||
} else {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${auth.user}:${auth.pass}`).toString("base64")}`;
|
||||
headers.Authorization = `Basic ${Buffer.from(`${this.auth.user}:${this.auth.pass}`).toString("base64")}`;
|
||||
}
|
||||
}
|
||||
|
||||
fetch(url, { headers: headers, agent: httpsAgent })
|
||||
.then(NodeHelper.checkFetchStatus)
|
||||
.then((response) => response.text())
|
||||
.then((responseData) => {
|
||||
let data = [];
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the Retry-After header value
|
||||
* @param {string} retryAfter - The Retry-After header value
|
||||
* @returns {number|null} Milliseconds to wait or null if parsing failed
|
||||
*/
|
||||
parseRetryAfter (retryAfter) {
|
||||
const seconds = Number(retryAfter);
|
||||
if (!Number.isNaN(seconds) && seconds >= 0) {
|
||||
return seconds * 1000;
|
||||
}
|
||||
|
||||
const retryDate = Date.parse(retryAfter);
|
||||
if (!Number.isNaN(retryDate)) {
|
||||
return Math.max(0, retryDate - Date.now());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the retry delay for a non-ok response
|
||||
* @param {Response} response - The fetch Response object
|
||||
* @returns {{delay: number, error: Error}} Error describing the issue and computed retry delay
|
||||
*/
|
||||
getDelayForResponse (response) {
|
||||
const { status, statusText = "" } = response;
|
||||
let delay = this.reloadInterval;
|
||||
|
||||
if (status === 401 || status === 403) {
|
||||
delay = Math.max(this.reloadInterval * 5, THIRTY_MINUTES);
|
||||
Log.error(`${this.url} - Authentication failed (${status}). Waiting ${Math.round(delay / 60000)} minutes before retry.`);
|
||||
} else if (status === 429) {
|
||||
const retryAfter = response.headers.get("retry-after");
|
||||
const parsed = retryAfter ? this.parseRetryAfter(retryAfter) : null;
|
||||
delay = parsed !== null ? Math.max(parsed, this.reloadInterval) : Math.max(this.reloadInterval * 2, FIFTEEN_MINUTES);
|
||||
Log.warn(`${this.url} - Rate limited (429). Retrying in ${Math.round(delay / 60000)} minutes.`);
|
||||
} else if (status >= 500) {
|
||||
this.serverErrorCount = Math.min(this.serverErrorCount + 1, MAX_SERVER_BACKOFF);
|
||||
delay = this.reloadInterval * Math.pow(2, this.serverErrorCount);
|
||||
Log.error(`${this.url} - Server error (${status}). Retry #${this.serverErrorCount} in ${Math.round(delay / 60000)} minutes.`);
|
||||
} else if (status >= 400) {
|
||||
delay = Math.max(this.reloadInterval * 2, FIFTEEN_MINUTES);
|
||||
Log.error(`${this.url} - Client error (${status}). Retrying in ${Math.round(delay / 60000)} minutes.`);
|
||||
} else {
|
||||
Log.error(`${this.url} - Unexpected HTTP status ${status}.`);
|
||||
}
|
||||
|
||||
return {
|
||||
delay,
|
||||
error: new Error(`HTTP ${status} ${statusText}`.trim())
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and processes calendar data
|
||||
*/
|
||||
async fetchCalendar () {
|
||||
this.clearReloadTimer();
|
||||
|
||||
let nextDelay = this.reloadInterval;
|
||||
try {
|
||||
const response = await fetch(this.url, this.getRequestOptions());
|
||||
if (!response.ok) {
|
||||
const { delay, error } = this.getDelayForResponse(response);
|
||||
nextDelay = delay;
|
||||
this.fetchFailedCallback(this, error);
|
||||
} else {
|
||||
this.serverErrorCount = 0;
|
||||
const responseData = await response.text();
|
||||
try {
|
||||
data = ical.parseICS(responseData);
|
||||
Log.debug(`parsed data=${JSON.stringify(data)}`);
|
||||
events = CalendarFetcherUtils.filterEvents(data, {
|
||||
excludedEvents,
|
||||
includePastEvents,
|
||||
maximumEntries,
|
||||
maximumNumberOfDays
|
||||
const parsed = ical.parseICS(responseData);
|
||||
Log.debug(`Parsed iCal data from ${this.url} with ${Object.keys(parsed).length} entries.`);
|
||||
this.events = CalendarFetcherUtils.filterEvents(parsed, {
|
||||
excludedEvents: this.excludedEvents,
|
||||
includePastEvents: this.includePastEvents,
|
||||
maximumEntries: this.maximumEntries,
|
||||
maximumNumberOfDays: this.maximumNumberOfDays
|
||||
});
|
||||
this.lastFetch = Date.now();
|
||||
this.broadcastEvents();
|
||||
} catch (error) {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
return;
|
||||
Log.error(`${this.url} - iCal parsing failed: ${error.message}`);
|
||||
this.fetchFailedCallback(this, error);
|
||||
}
|
||||
this.broadcastEvents();
|
||||
scheduleTimer();
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
});
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error(`${this.url} - Fetch failed: ${error.message}`);
|
||||
this.fetchFailedCallback(this, error);
|
||||
}
|
||||
|
||||
this.scheduleNextFetch(nextDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the timer for the next update.
|
||||
* Check if enough time has passed since the last fetch to warrant a new one.
|
||||
* Uses reloadInterval as the threshold to respect user's configured fetchInterval.
|
||||
* @returns {boolean} True if a new fetch should be performed
|
||||
*/
|
||||
const scheduleTimer = function () {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchCalendar();
|
||||
}, reloadInterval);
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
shouldRefetch () {
|
||||
if (!this.lastFetch) {
|
||||
return true;
|
||||
}
|
||||
const timeSinceLastFetch = Date.now() - this.lastFetch;
|
||||
return timeSinceLastFetch >= this.reloadInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate fetchCalendar();
|
||||
* Broadcasts the current events to listeners
|
||||
*/
|
||||
this.startFetch = function () {
|
||||
fetchCalendar();
|
||||
};
|
||||
broadcastEvents () {
|
||||
Log.info(`Broadcasting ${this.events.length} events from ${this.url}.`);
|
||||
this.eventsReceivedCallback(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast the existing events.
|
||||
* Sets the callback for successful event fetches
|
||||
* @param {(fetcher: CalendarFetcher) => void} callback - Called when events are received
|
||||
*/
|
||||
this.broadcastEvents = function () {
|
||||
Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events from ${url}.`);
|
||||
eventsReceivedCallback(this);
|
||||
};
|
||||
onReceive (callback) {
|
||||
this.eventsReceivedCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the on success callback
|
||||
* @param {Function} callback The on success callback.
|
||||
* Sets the callback for fetch failures
|
||||
* @param {(fetcher: CalendarFetcher, error: Error) => void} callback - Called when a fetch fails
|
||||
*/
|
||||
this.onReceive = function (callback) {
|
||||
eventsReceivedCallback = callback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the on error callback
|
||||
* @param {Function} callback The on error callback.
|
||||
*/
|
||||
this.onError = function (callback) {
|
||||
fetchFailedCallback = callback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the url of this fetcher.
|
||||
* @returns {string} The url of this fetcher.
|
||||
*/
|
||||
this.url = function () {
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns current available events for this fetcher.
|
||||
* @returns {object[]} The current available events for this fetcher.
|
||||
*/
|
||||
this.events = function () {
|
||||
return events;
|
||||
};
|
||||
};
|
||||
onError (callback) {
|
||||
this.fetchFailedCallback = callback;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CalendarFetcher;
|
||||
|
||||
@@ -1,121 +1,93 @@
|
||||
/* MagicMirror²
|
||||
* Calendar Fetcher Util Methods
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external Moment
|
||||
*/
|
||||
const path = require("path");
|
||||
const moment = require("moment");
|
||||
const moment = require("moment-timezone");
|
||||
|
||||
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
|
||||
const Log = require("../../../js/logger");
|
||||
const Log = require("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
|
||||
* @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}`);
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Log.debug(`corrected tz=${event.start.tz}`);
|
||||
let current_offset = 0; // offset from TZ string or calculated
|
||||
let mm = 0; // date with tz or offset
|
||||
let start_offset = 0; // utc offset of created with tz
|
||||
// if there is still an offset, lookup failed, use it
|
||||
if (event.start.tz.startsWith("(")) {
|
||||
const regex = /[+|-]\d*:\d*/;
|
||||
const start_offsetString = event.start.tz.match(regex).toString().split(":");
|
||||
let start_offset = parseInt(start_offsetString[0]);
|
||||
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
|
||||
adjustHours = start_offset;
|
||||
Log.debug(`defined offset=${start_offset} hours`);
|
||||
current_offset = start_offset;
|
||||
event.start.tz = "";
|
||||
Log.debug(`ical offset=${current_offset} date=${date}`);
|
||||
mm = moment(date);
|
||||
let x = parseInt(moment(new Date()).utcOffset());
|
||||
Log.debug(`net mins=${current_offset * 60 - x}`);
|
||||
|
||||
mm = mm.add(x - current_offset * 60, "minutes");
|
||||
adjustHours = (current_offset * 60 - x) / 60;
|
||||
event.start = mm.toDate();
|
||||
Log.debug(`adjusted date=${event.start}`);
|
||||
} else {
|
||||
// get the start time in that timezone
|
||||
let es = moment(event.start);
|
||||
// check for start date prior to start of daylight changing date
|
||||
if (es.format("YYYY") < 2007) {
|
||||
es.set("year", 2013); // if so, use a closer date
|
||||
}
|
||||
Log.debug(`start date/time=${es.toDate()}`);
|
||||
start_offset = moment.tz(es, event.start.tz).utcOffset();
|
||||
Log.debug(`start offset=${start_offset}`);
|
||||
|
||||
Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`);
|
||||
|
||||
// get the specified date in that timezone
|
||||
mm = moment.tz(moment(date), event.start.tz);
|
||||
Log.debug(`event date=${mm.toDate()}`);
|
||||
current_offset = mm.utcOffset();
|
||||
}
|
||||
Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`);
|
||||
|
||||
// if the offset is greater than 0, east of london
|
||||
if (current_offset !== start_offset) {
|
||||
// big offset
|
||||
Log.debug("offset");
|
||||
let h = parseInt(mm.format("H"));
|
||||
// check if the event time is less than the offset
|
||||
if (h > 0 && h < Math.abs(current_offset) / 60) {
|
||||
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
|
||||
// we need to fix that
|
||||
//adjustHours = 24;
|
||||
// Log.debug("adjusting date")
|
||||
}
|
||||
//-300 > -240
|
||||
//if (Math.abs(current_offset) > Math.abs(start_offset)){
|
||||
if (current_offset > start_offset) {
|
||||
adjustHours -= 1;
|
||||
Log.debug("adjust down 1 hour dst change");
|
||||
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
|
||||
} else if (current_offset < start_offset) {
|
||||
adjustHours += 1;
|
||||
Log.debug("adjust up 1 hour dst change");
|
||||
}
|
||||
shouldEventBeExcluded (config, title) {
|
||||
for (const filterConfig of config.excludedEvents) {
|
||||
const match = CalendarFetcherUtils.checkEventAgainstFilter(title, filterConfig);
|
||||
if (match) {
|
||||
return {
|
||||
excluded: !match.until,
|
||||
until: match.until
|
||||
};
|
||||
}
|
||||
}
|
||||
Log.debug(`adjustHours=${adjustHours}`);
|
||||
return adjustHours;
|
||||
|
||||
return {
|
||||
excluded: false,
|
||||
until: null
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
const isFullDayEvent = CalendarFetcherUtils.isFullDayEvent(event);
|
||||
const eventTimezone = event.start.tz || CalendarFetcherUtils.getLocalTimezone();
|
||||
|
||||
// rrule.js interprets years < 1900 as offsets from 1900, causing issues with some birthday calendars
|
||||
if (rule.origOptions?.dtstart?.getFullYear() < 1900) {
|
||||
rule.origOptions.dtstart.setFullYear(1900);
|
||||
}
|
||||
if (rule.options?.dtstart?.getFullYear() < 1900) {
|
||||
rule.options.dtstart.setFullYear(1900);
|
||||
}
|
||||
|
||||
// Expand search window to include ongoing events
|
||||
const oneDayInMs = 24 * 60 * 60 * 1000;
|
||||
const searchFromDate = pastLocalMoment.clone().subtract(Math.max(durationInMs, oneDayInMs), "milliseconds").toDate();
|
||||
const searchToDate = futureLocalMoment.clone().add(1, "days").toDate();
|
||||
|
||||
// For all-day events, extend "until" to end of day to include the final occurrence
|
||||
if (isFullDayEvent && rule.options?.until) {
|
||||
rule.options.until = moment(rule.options.until).endOf("day").toDate();
|
||||
}
|
||||
|
||||
// Clear tzid to prevent rrule.js from double-adjusting times
|
||||
if (rule.options) {
|
||||
rule.options.tzid = null;
|
||||
}
|
||||
|
||||
const dates = rule.between(searchFromDate, searchToDate, true) || [];
|
||||
|
||||
// Convert dates to moments in the appropriate timezone
|
||||
// rrule.js returns UTC dates with tzid cleared, so we interpret them in the event's original timezone
|
||||
return dates.map((date) => {
|
||||
if (isFullDayEvent) {
|
||||
// For all-day events, anchor to calendar day in event's timezone
|
||||
return moment.tz(date, eventTimezone).startOf("day");
|
||||
}
|
||||
// For timed events, preserve the time in the event's original timezone
|
||||
return moment.tz(date, "UTC").tz(eventTimezone, true);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -127,32 +99,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], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
const startMoment = event[time].tz ? moment.tz(event[time], event[time].tz) : moment.tz(event[time], CalendarFetcherUtils.getLocalTimezone());
|
||||
return CalendarFetcherUtils.isFullDayEvent(event) ? startMoment.startOf("day") : startMoment;
|
||||
};
|
||||
|
||||
Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
|
||||
|
||||
const now = moment();
|
||||
const pastLocalMoment = config.includePastEvents ? now.clone().startOf("day").subtract(config.maximumNumberOfDays, "days") : now;
|
||||
const futureLocalMoment
|
||||
= now
|
||||
.clone()
|
||||
.startOf("day")
|
||||
.add(config.maximumNumberOfDays, "days")
|
||||
// Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
.subtract(1, "seconds");
|
||||
|
||||
Object.entries(data).forEach(([key, event]) => {
|
||||
Log.debug("Processing entry...");
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day").toDate();
|
||||
const future
|
||||
= moment()
|
||||
.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();
|
||||
let past = today;
|
||||
|
||||
if (config.includePastEvents) {
|
||||
past = moment().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.
|
||||
@@ -165,351 +138,79 @@ const CalendarFetcherUtils = {
|
||||
}
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
Log.debug(`Event:\n${JSON.stringify(event)}`);
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
Log.debug(`Event:\n${JSON.stringify(event, null, 2)}`);
|
||||
let eventStartMoment = eventDate(event, "start");
|
||||
let eventEndMoment;
|
||||
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
eventEndMoment = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
endDate = startDate.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
|
||||
endDate = moment(startDate.format("x"), "x");
|
||||
eventEndMoment = eventStartMoment.clone();
|
||||
} else {
|
||||
endDate = moment(startDate).add(1, "days");
|
||||
eventEndMoment = eventStartMoment.clone().add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug(`start: ${startDate.toDate()}`);
|
||||
Log.debug(`end:: ${endDate.toDate()}`);
|
||||
Log.debug(`start: ${eventStartMoment.toDate()}`);
|
||||
Log.debug(`end: ${eventEndMoment.toDate()}`);
|
||||
|
||||
// Calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
Log.debug(`duration: ${duration}`);
|
||||
|
||||
// FIXME: Since the parsed json object from node-ical comes with time information
|
||||
// this check could be removed (?)
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
const title = 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 durationMs = eventEndMoment.valueOf() - eventStartMoment.valueOf();
|
||||
Log.debug(`duration: ${durationMs}`);
|
||||
|
||||
const location = event.location || false;
|
||||
const geo = event.geo || false;
|
||||
const description = event.description || false;
|
||||
|
||||
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||
const rule = event.rrule;
|
||||
|
||||
const pastMoment = moment(past);
|
||||
const futureMoment = moment(future);
|
||||
|
||||
// can cause problems with e.g. birthdays before 1900
|
||||
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
|
||||
rule.origOptions.dtstart.setYear(1900);
|
||||
rule.options.dtstart.setYear(1900);
|
||||
}
|
||||
|
||||
// For recurring events, get the set of start dates that fall within the range
|
||||
// of dates we're looking for.
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
let pastLocal = 0;
|
||||
let futureLocal = 0;
|
||||
if (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().toDate(); //now
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
}
|
||||
Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
|
||||
let dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
|
||||
dates = dates.filter((d) => {
|
||||
if (JSON.stringify(d) === "null") return false;
|
||||
else return true;
|
||||
});
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. For the time being,
|
||||
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
|
||||
// because the logic below will filter out any recurrences that don't actually belong within
|
||||
// our display range.
|
||||
// Would be great if there was a better way to handle this.
|
||||
Log.debug(`event.recurrences: ${event.recurrences}`);
|
||||
if (event.recurrences !== undefined) {
|
||||
for (let r in event.recurrences) {
|
||||
// Only add dates that weren't already in the range we added from the rrule so that
|
||||
// we don"t double-add those events.
|
||||
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
|
||||
dates.push(new Date(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||
for (let d in dates) {
|
||||
let date = dates[d];
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
|
||||
// set the time information in the date to equal the time information in the event
|
||||
date.setUTCHours(curEvent.start.getUTCHours(), curEvent.start.getUTCMinutes(), curEvent.start.getUTCSeconds(), curEvent.start.getUTCMilliseconds());
|
||||
|
||||
// Get the offset of today where we are processing
|
||||
// This will be the correction, we need to apply.
|
||||
let nowOffset = new Date().getTimezoneOffset();
|
||||
// For full day events, the time might be off from RRULE/Luxon problem
|
||||
// Get time zone offset of the rule calculated event
|
||||
let dateoffset = date.getTimezoneOffset();
|
||||
|
||||
// Reduce the time by the following offset.
|
||||
Log.debug(` recurring date is ${date} offset is ${dateoffset}`);
|
||||
|
||||
let dh = moment(date).format("HH");
|
||||
Log.debug(` recurring date is ${date} offset is ${dateoffset / 60} Hour is ${dh}`);
|
||||
|
||||
if (CalendarFetcherUtils.isFullDayEvent(event)) {
|
||||
Log.debug("Fullday");
|
||||
// If the offset is negative (east of GMT), where the problem is
|
||||
if (dateoffset < 0) {
|
||||
if (dh < Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
// reduce the time by the offset
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// Apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`new recurring date1 fulldate is ${date}`);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
//if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`new recurring date2 fulldate is ${date}`);
|
||||
}
|
||||
//}
|
||||
}
|
||||
} else {
|
||||
// not full day, but luxon can still screw up the date on the rule processing
|
||||
// we need to correct the date to get back to the right event for
|
||||
if (dateoffset < 0) {
|
||||
// if the date hour is less than the offset
|
||||
if (dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// Reduce the time by t:
|
||||
// Apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`new recurring date1 is ${date}`);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
//if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`new recurring date2 is ${date}`);
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
startDate = moment(date);
|
||||
Log.debug(`Corrected startDate: ${startDate.toDate()}`);
|
||||
|
||||
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, 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);
|
||||
|
||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateKey];
|
||||
startDate = moment(curEvent.start);
|
||||
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
||||
}
|
||||
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
Log.debug(`duration: ${duration}`);
|
||||
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
if (startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
|
||||
const recurrenceTitle = 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 (endDate.isBefore(past) || startDate.isAfter(future)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
Log.debug(`saving event: ${description}`);
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event),
|
||||
recurringEvent: true,
|
||||
class: event.class,
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
// End recurring event parsing.
|
||||
let instances = [];
|
||||
if (event.rrule && typeof event.rrule !== "undefined" && !isFacebookBirthday) {
|
||||
instances = CalendarFetcherUtils.expandRecurringEvent(event, pastLocalMoment, futureLocalMoment, durationMs);
|
||||
} else {
|
||||
// Single event.
|
||||
const fullDayEvent = isFacebookBirthday ? true : CalendarFetcherUtils.isFullDayEvent(event);
|
||||
// Log.debug("full day event")
|
||||
|
||||
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
|
||||
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
let end = eventEndMoment;
|
||||
if (fullDayEvent && eventStartMoment.valueOf() === end.valueOf()) {
|
||||
end = end.endOf("day");
|
||||
}
|
||||
|
||||
if (config.includePastEvents) {
|
||||
// Past event is too far in the past, so skip.
|
||||
if (endDate < past) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// It's not a fullday event, and it is in the past, so skip.
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
return;
|
||||
}
|
||||
instances.push({
|
||||
event: event,
|
||||
startMoment: eventStartMoment,
|
||||
endMoment: end,
|
||||
isRecurring: false
|
||||
});
|
||||
}
|
||||
|
||||
// It's a fullday event, and it is before today, So skip.
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
return;
|
||||
}
|
||||
for (const instance of instances) {
|
||||
const { event: instanceEvent, startMoment, endMoment, isRecurring } = instance;
|
||||
|
||||
// Filter logic
|
||||
if (endMoment.isBefore(pastLocalMoment) || startMoment.isAfter(futureLocalMoment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// It exceeds the maximumNumberOfDays limit, so skip.
|
||||
if (startDate > future) {
|
||||
return;
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, eventFilterUntil)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
return;
|
||||
}
|
||||
const title = CalendarFetcherUtils.getTitleFromEvent(instanceEvent);
|
||||
const fullDay = isFacebookBirthday ? true : CalendarFetcherUtils.isFullDayEvent(event);
|
||||
|
||||
// get correction for date saving and dst change between now and then
|
||||
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startDate.toDate());
|
||||
// Every thing is good. Add it to the list.
|
||||
Log.debug(`saving event: ${title}`);
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: fullDayEvent,
|
||||
startDate: startMoment.format("x"),
|
||||
endDate: endMoment.format("x"),
|
||||
fullDayEvent: fullDay,
|
||||
recurringEvent: isRecurring,
|
||||
class: event.class,
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: instanceEvent.location || location,
|
||||
geo: instanceEvent.geo || geo,
|
||||
description: instanceEvent.description || description
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -522,18 +223,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.
|
||||
@@ -573,8 +262,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
|
||||
*/
|
||||
@@ -585,7 +274,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.format("x");
|
||||
return now < filterUntil;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -611,6 +300,106 @@ const CalendarFetcherUtils = {
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Expands a recurring event into individual event instances.
|
||||
* @param {object} event The recurring event object
|
||||
* @param {moment.Moment} pastLocalMoment The past date limit
|
||||
* @param {moment.Moment} futureLocalMoment The future date limit
|
||||
* @param {number} durationMs The duration of the event in milliseconds
|
||||
* @returns {object[]} Array of event instances
|
||||
*/
|
||||
expandRecurringEvent (event, pastLocalMoment, futureLocalMoment, durationMs) {
|
||||
const moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(event, pastLocalMoment, futureLocalMoment, durationMs);
|
||||
const instances = [];
|
||||
|
||||
for (const startMoment of moments) {
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
let recurringEventStartMoment = startMoment.clone().tz(CalendarFetcherUtils.getLocalTimezone());
|
||||
let recurringEventEndMoment = recurringEventStartMoment.clone().add(durationMs, "ms");
|
||||
|
||||
const dateKey = recurringEventStartMoment.tz("UTC").format("YYYY-MM-DD");
|
||||
|
||||
// Check for overrides
|
||||
if (curEvent.recurrences !== undefined) {
|
||||
if (curEvent.recurrences[dateKey] !== undefined) {
|
||||
curEvent = curEvent.recurrences[dateKey];
|
||||
// Re-calculate start/end based on override
|
||||
const start = curEvent.start;
|
||||
const end = curEvent.end;
|
||||
const localTimezone = CalendarFetcherUtils.getLocalTimezone();
|
||||
|
||||
recurringEventStartMoment = (start.tz ? moment(start).tz(start.tz) : moment(start)).tz(localTimezone);
|
||||
recurringEventEndMoment = (end.tz ? moment(end).tz(end.tz) : moment(end)).tz(localTimezone);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for exceptions
|
||||
if (curEvent.exdate !== undefined) {
|
||||
if (curEvent.exdate[dateKey] !== undefined) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (recurringEventStartMoment.valueOf() === recurringEventEndMoment.valueOf()) {
|
||||
recurringEventEndMoment = recurringEventEndMoment.endOf("day");
|
||||
}
|
||||
|
||||
if (showRecurrence) {
|
||||
instances.push({
|
||||
event: curEvent,
|
||||
startMoment: recurringEventStartMoment,
|
||||
endMoment: recurringEventEndMoment,
|
||||
isRecurring: true
|
||||
});
|
||||
}
|
||||
}
|
||||
return instances;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if an event title matches a specific filter configuration.
|
||||
* @param {string} title The event title to check
|
||||
* @param {string|object} filterConfig The filter configuration (string or object)
|
||||
* @returns {object|null} Object with {until: string|null} if matched, null otherwise
|
||||
*/
|
||||
checkEventAgainstFilter (title, filterConfig) {
|
||||
let filter = filterConfig;
|
||||
let testTitle = title.toLowerCase();
|
||||
let until = null;
|
||||
let useRegex = false;
|
||||
let regexFlags = "g";
|
||||
|
||||
if (filter instanceof Object) {
|
||||
if (typeof filter.until !== "undefined") {
|
||||
until = filter.until;
|
||||
}
|
||||
|
||||
if (typeof filter.regex !== "undefined") {
|
||||
useRegex = filter.regex;
|
||||
}
|
||||
|
||||
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)) {
|
||||
return { until };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Calendar Util Methods
|
||||
*
|
||||
* By Rejas
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const CalendarUtils = {
|
||||
|
||||
/**
|
||||
@@ -94,7 +88,7 @@ const CalendarUtils = {
|
||||
* @param {string} title The title to transform.
|
||||
* @param {object} titleReplace object definition of parts to be replaced in the title
|
||||
* object definition:
|
||||
* search: {string,required} RegEx in format //x or simple string to be searched. For (birthday) year calcluation, the element matching the year must be in a RegEx group
|
||||
* search: {string,required} RegEx in format //x or simple string to be searched. For (birthday) year calculation, the element matching the year must be in a RegEx group
|
||||
* replace: {string,required} Replacement string, may contain match group references (latter is required for year calculation)
|
||||
* yearmatchgroup: {number,optional} match group for year element
|
||||
* @returns {string} The transformed title.
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/* CalendarFetcher Tester
|
||||
/*
|
||||
* CalendarFetcher Tester
|
||||
* use this script with `node debug.js` to test the fetcher without the need
|
||||
* of starting the MagicMirror² core. Adjust the values below to your desire.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
// Load internal alias resolver
|
||||
require("../../../js/alias-resolver");
|
||||
const Log = require("logger");
|
||||
|
||||
const CalendarFetcher = require("./calendarfetcher");
|
||||
|
||||
@@ -22,22 +21,20 @@ const auth = {
|
||||
pass: pass
|
||||
};
|
||||
|
||||
console.log("Create fetcher ...");
|
||||
Log.log("Create fetcher ...");
|
||||
|
||||
const fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maximumNumberOfDays, auth);
|
||||
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
console.log(fetcher.events());
|
||||
console.log("------------------------------------------------------------");
|
||||
Log.log(fetcher.events);
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
fetcher.onError(function (fetcher, error) {
|
||||
console.log("Fetcher error:");
|
||||
console.log(error);
|
||||
Log.log("Fetcher error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
fetcher.startFetch();
|
||||
|
||||
console.log("Create fetcher done! ");
|
||||
Log.log("Create fetcher done! ");
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
/* MagicMirror²
|
||||
* Node Helper: Calendar
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const zlib = require("node:zlib");
|
||||
const NodeHelper = require("node_helper");
|
||||
const Log = require("logger");
|
||||
const CalendarFetcher = require("./calendarfetcher");
|
||||
@@ -22,11 +17,11 @@ module.exports = NodeHelper.create({
|
||||
} else if (notification === "FETCH_CALENDAR") {
|
||||
const key = payload.id + payload.url;
|
||||
if (typeof this.fetchers[key] === "undefined") {
|
||||
Log.error("Calendar Error. No fetcher exists with key: ", key);
|
||||
Log.error("No fetcher exists with key: ", key);
|
||||
this.sendSocketNotification("CALENDAR_ERROR", { error_type: "MODULE_ERROR_UNSPECIFIED" });
|
||||
return;
|
||||
}
|
||||
this.fetchers[key].startFetch();
|
||||
this.fetchers[key].fetchCalendar();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -47,22 +42,27 @@ module.exports = NodeHelper.create({
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
Log.error("Calendar Error. Malformed calendar url: ", url, error);
|
||||
Log.error("Malformed calendar url: ", url, error);
|
||||
this.sendSocketNotification("CALENDAR_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" });
|
||||
return;
|
||||
}
|
||||
|
||||
let fetcher;
|
||||
let fetchIntervalCorrected;
|
||||
if (typeof this.fetchers[identifier + url] === "undefined") {
|
||||
Log.log(`Create new calendarfetcher for url: ${url} - Interval: ${fetchInterval}`);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
|
||||
if (fetchInterval < 60000) {
|
||||
Log.warn(`fetchInterval for url ${url} must be >= 60000`);
|
||||
fetchIntervalCorrected = 60000;
|
||||
}
|
||||
Log.log(`Create new calendarfetcher for url: ${url} - Interval: ${fetchIntervalCorrected || fetchInterval}`);
|
||||
fetcher = new CalendarFetcher(url, fetchIntervalCorrected || fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
|
||||
|
||||
fetcher.onReceive((fetcher) => {
|
||||
this.broadcastEvents(fetcher, identifier);
|
||||
});
|
||||
|
||||
fetcher.onError((fetcher, error) => {
|
||||
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
|
||||
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url, error);
|
||||
let error_type = NodeHelper.checkFetchError(error);
|
||||
this.sendSocketNotification("CALENDAR_ERROR", {
|
||||
id: identifier,
|
||||
@@ -71,13 +71,18 @@ module.exports = NodeHelper.create({
|
||||
});
|
||||
|
||||
this.fetchers[identifier + url] = fetcher;
|
||||
fetcher.fetchCalendar();
|
||||
} else {
|
||||
Log.log(`Use existing calendarfetcher for url: ${url}`);
|
||||
fetcher = this.fetchers[identifier + url];
|
||||
fetcher.broadcastEvents();
|
||||
// Check if calendar data is stale and needs refresh
|
||||
if (fetcher.shouldRefetch()) {
|
||||
Log.log(`Calendar data is stale, fetching fresh data for url: ${url}`);
|
||||
fetcher.fetchCalendar();
|
||||
} else {
|
||||
fetcher.broadcastEvents();
|
||||
}
|
||||
}
|
||||
|
||||
fetcher.startFetch();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -86,10 +91,12 @@ module.exports = NodeHelper.create({
|
||||
* @param {string} identifier the identifier of the calendar
|
||||
*/
|
||||
broadcastEvents (fetcher, identifier) {
|
||||
const checksum = zlib.crc32(Buffer.from(JSON.stringify(fetcher.events), "utf8"));
|
||||
this.sendSocketNotification("CALENDAR_EVENTS", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
events: fetcher.events()
|
||||
url: fetcher.url,
|
||||
events: fetcher.events,
|
||||
checksum: checksum
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/* global SunCalc, formatTime */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Clock
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("clock", {
|
||||
// Module config defaults.
|
||||
defaults: {
|
||||
@@ -20,7 +14,7 @@ Module.register("clock", {
|
||||
clockBold: false,
|
||||
showDate: true,
|
||||
showTime: true,
|
||||
showWeek: false,
|
||||
showWeek: false, // options: true, false, 'short'
|
||||
dateFormat: "dddd, LL",
|
||||
sendNotifications: false,
|
||||
|
||||
@@ -29,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
|
||||
@@ -42,7 +36,7 @@ Module.register("clock", {
|
||||
},
|
||||
// Define styles.
|
||||
getStyles () {
|
||||
return ["clock_styles.css"];
|
||||
return ["clock_styles.css", "font-awesome.css"];
|
||||
},
|
||||
// Define start sequence.
|
||||
start () {
|
||||
@@ -105,13 +99,14 @@ Module.register("clock", {
|
||||
analogWrapper.className = "clock-circle";
|
||||
const digitalWrapper = document.createElement("div");
|
||||
digitalWrapper.className = "digital";
|
||||
digitalWrapper.style.gridArea = "center";
|
||||
|
||||
/************************************
|
||||
* Create wrappers for DIGITAL clock
|
||||
*/
|
||||
const dateWrapper = document.createElement("div");
|
||||
const timeWrapper = document.createElement("div");
|
||||
const hoursWrapper = document.createElement("span");
|
||||
const minutesWrapper = document.createElement("span");
|
||||
const secondsWrapper = document.createElement("sup");
|
||||
const periodWrapper = document.createElement("span");
|
||||
const sunWrapper = document.createElement("div");
|
||||
@@ -121,39 +116,40 @@ Module.register("clock", {
|
||||
// Style Wrappers
|
||||
dateWrapper.className = "date normal medium";
|
||||
timeWrapper.className = "time bright large light";
|
||||
secondsWrapper.className = "seconds dimmed";
|
||||
hoursWrapper.className = "clock-hour-digital";
|
||||
minutesWrapper.className = "clock-minute-digital";
|
||||
secondsWrapper.className = "clock-second-digital dimmed";
|
||||
sunWrapper.className = "sun dimmed small";
|
||||
moonWrapper.className = "moon dimmed small";
|
||||
weekWrapper.className = "week dimmed medium";
|
||||
|
||||
// Set content of wrappers.
|
||||
// The moment().format("h") method has a bug on the Raspberry Pi.
|
||||
// So we need to generate the timestring manually.
|
||||
// See issue: https://github.com/MichMich/MagicMirror/issues/181
|
||||
let timeString;
|
||||
const now = moment();
|
||||
if (this.config.timezone) {
|
||||
now.tz(this.config.timezone);
|
||||
}
|
||||
|
||||
let hourSymbol = "HH";
|
||||
if (this.config.timeFormat !== 24) {
|
||||
hourSymbol = "h";
|
||||
}
|
||||
|
||||
if (this.config.clockBold) {
|
||||
timeString = now.format(`${hourSymbol}[<span class="bold">]mm[</span>]`);
|
||||
} else {
|
||||
timeString = now.format(`${hourSymbol}:mm`);
|
||||
}
|
||||
|
||||
if (this.config.showDate) {
|
||||
dateWrapper.innerHTML = now.format(this.config.dateFormat);
|
||||
digitalWrapper.appendChild(dateWrapper);
|
||||
}
|
||||
|
||||
if (this.config.displayType !== "analog" && this.config.showTime) {
|
||||
timeWrapper.innerHTML = timeString;
|
||||
let hourSymbol = "HH";
|
||||
if (this.config.timeFormat !== 24) {
|
||||
hourSymbol = "h";
|
||||
}
|
||||
|
||||
hoursWrapper.innerHTML = now.format(hourSymbol);
|
||||
minutesWrapper.innerHTML = now.format("mm");
|
||||
|
||||
timeWrapper.appendChild(hoursWrapper);
|
||||
if (this.config.clockBold) {
|
||||
minutesWrapper.classList.add("bold");
|
||||
} else {
|
||||
timeWrapper.innerHTML += ":";
|
||||
}
|
||||
timeWrapper.appendChild(minutesWrapper);
|
||||
secondsWrapper.innerHTML = now.format("ss");
|
||||
if (this.config.showPeriodUpper) {
|
||||
periodWrapper.innerHTML = now.format("A");
|
||||
@@ -175,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);
|
||||
}
|
||||
|
||||
@@ -215,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);
|
||||
}
|
||||
|
||||
@@ -247,7 +255,7 @@ Module.register("clock", {
|
||||
analogWrapper.style.background = `url(${this.data.path}faces/${this.config.analogFace}.svg)`;
|
||||
analogWrapper.style.backgroundSize = "100%";
|
||||
|
||||
// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
|
||||
// The following line solves issue: https://github.com/MagicMirrorOrg/MagicMirror/issues/611
|
||||
// analogWrapper.style.border = "1px solid black";
|
||||
analogWrapper.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
|
||||
} else if (this.config.analogFace !== "none") {
|
||||
@@ -274,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);
|
||||
|
||||
@@ -78,16 +78,41 @@
|
||||
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%;
|
||||
}
|
||||
|
||||
.module.clock .digital {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.module.clock .sun,
|
||||
.module.clock .moon {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.module.clock .sun > *,
|
||||
.module.clock .moon > * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.module.clock .clock-hour-digital {
|
||||
color: var(--color-text-bright);
|
||||
}
|
||||
|
||||
.module.clock .clock-minute-digital {
|
||||
color: var(--color-text-bright);
|
||||
}
|
||||
|
||||
.module.clock .clock-second-digital {
|
||||
color: var(--color-text-dimmed);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
/* MagicMirror²
|
||||
* Module: Compliments
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
/* global Cron */
|
||||
|
||||
Module.register("compliments", {
|
||||
// Module config defaults.
|
||||
defaults: {
|
||||
@@ -16,20 +12,26 @@ Module.register("compliments", {
|
||||
},
|
||||
updateInterval: 30000,
|
||||
remoteFile: null,
|
||||
remoteFileRefreshInterval: 0,
|
||||
fadeSpeed: 4000,
|
||||
morningStartTime: 3,
|
||||
morningEndTime: 12,
|
||||
afternoonStartTime: 12,
|
||||
afternoonEndTime: 17,
|
||||
random: true
|
||||
random: true,
|
||||
specialDayUnique: false
|
||||
},
|
||||
compliments_new: null,
|
||||
refreshMinimumDelay: 15 * 60 * 1000, // 15 minutes
|
||||
lastIndexUsed: -1,
|
||||
// Set currentweather from module
|
||||
currentWeatherType: "",
|
||||
|
||||
cron_regex: /^(((\d+,)+\d+|((\d+|[*])[/]\d+|((JAN|FEB|APR|MA[RY]|JU[LN]|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|APR|MA[RY]|JU[LN]|AUG|SEP|OCT|NOV|DEC))?))|(\d+-\d+)|\d+(-\d+)?[/]\d+(-\d+)?|\d+|[*]|(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?) ?){5}$/i,
|
||||
date_regex: "[1-9.][0-9.][0-9.]{2}-([0][1-9]|[1][0-2])-([1-2][0-9]|[0][1-9]|[3][0-1])",
|
||||
pre_defined_types: ["anytime", "morning", "afternoon", "evening"],
|
||||
// Define required scripts.
|
||||
getScripts () {
|
||||
return ["moment.js"];
|
||||
return ["croner.js", "moment.js"];
|
||||
},
|
||||
|
||||
// Define start sequence.
|
||||
@@ -42,12 +44,62 @@ 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(`[compliments] ${this.name} remoteFile refresh failed`);
|
||||
}
|
||||
},
|
||||
this.config.remoteFileRefreshInterval);
|
||||
} else {
|
||||
Log.error(`[compliments] ${this.name} remoteFileRefreshInterval less than minimum`);
|
||||
}
|
||||
}
|
||||
}
|
||||
let minute_sync_delay = 1;
|
||||
// loop thru all the configured when events
|
||||
for (let m of Object.keys(this.config.compliments)) {
|
||||
// if it is a cron entry
|
||||
if (this.isCronEntry(m)) {
|
||||
// we need to synch our interval cycle to the minute
|
||||
minute_sync_delay = (60 - (moment().second())) * 1000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Schedule update timer. sync to the minute start (if needed), so minute based events happen on the minute start
|
||||
setTimeout(() => {
|
||||
setInterval(() => {
|
||||
this.updateDom(this.config.fadeSpeed);
|
||||
}, this.config.updateInterval);
|
||||
},
|
||||
minute_sync_delay);
|
||||
},
|
||||
|
||||
// Schedule update timer.
|
||||
setInterval(() => {
|
||||
this.updateDom(this.config.fadeSpeed);
|
||||
}, this.config.updateInterval);
|
||||
// check to see if this entry could be a cron entry which contains spaces
|
||||
isCronEntry (entry) {
|
||||
return entry.includes(" ");
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} cronExpression The cron expression. See https://croner.56k.guru/usage/pattern/
|
||||
* @param {Date} [timestamp] The timestamp to check. Defaults to the current time.
|
||||
* @returns {number} The number of seconds until the next cron run.
|
||||
*/
|
||||
getSecondsUntilNextCronRun (cronExpression, timestamp = new Date()) {
|
||||
// Required for seconds precision
|
||||
const adjustedTimestamp = new Date(timestamp.getTime() - 1000);
|
||||
|
||||
// https://www.npmjs.com/package/croner
|
||||
const cronJob = new Cron(cronExpression);
|
||||
const nextRunTime = cronJob.nextRun(adjustedTimestamp);
|
||||
|
||||
const secondsDelta = (nextRunTime - adjustedTimestamp) / 1000;
|
||||
return secondsDelta;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -56,7 +108,7 @@ Module.register("compliments", {
|
||||
* @returns {number} a random index of given array
|
||||
*/
|
||||
randomIndex (compliments) {
|
||||
if (compliments.length === 1) {
|
||||
if (compliments.length <= 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -80,46 +132,108 @@ Module.register("compliments", {
|
||||
* @returns {string[]} array with compliments for the time of the day.
|
||||
*/
|
||||
complimentArray () {
|
||||
const hour = moment().hour();
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
const now = moment();
|
||||
const hour = now.hour();
|
||||
const date = now.format("YYYY-MM-DD");
|
||||
let compliments = [];
|
||||
|
||||
// Add time of day compliments
|
||||
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
|
||||
compliments = [...this.config.compliments.morning];
|
||||
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime && this.config.compliments.hasOwnProperty("afternoon")) {
|
||||
compliments = [...this.config.compliments.afternoon];
|
||||
} else if (this.config.compliments.hasOwnProperty("evening")) {
|
||||
compliments = [...this.config.compliments.evening];
|
||||
let timeOfDay;
|
||||
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime) {
|
||||
timeOfDay = "morning";
|
||||
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime) {
|
||||
timeOfDay = "afternoon";
|
||||
} else {
|
||||
timeOfDay = "evening";
|
||||
}
|
||||
|
||||
if (timeOfDay && this.config.compliments.hasOwnProperty(timeOfDay)) {
|
||||
compliments = [...this.config.compliments[timeOfDay]];
|
||||
}
|
||||
|
||||
// Add compliments based on weather
|
||||
if (this.currentWeatherType in this.config.compliments) {
|
||||
Array.prototype.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
|
||||
// if the predefine list doesn't include it (yet)
|
||||
if (!this.pre_defined_types.includes(this.currentWeatherType)) {
|
||||
// add it
|
||||
this.pre_defined_types.push(this.currentWeatherType);
|
||||
}
|
||||
}
|
||||
|
||||
// Add compliments for anytime
|
||||
Array.prototype.push.apply(compliments, this.config.compliments.anytime);
|
||||
|
||||
// Add compliments for special days
|
||||
for (let entry in this.config.compliments) {
|
||||
if (new RegExp(entry).test(date)) {
|
||||
Array.prototype.push.apply(compliments, this.config.compliments[entry]);
|
||||
// get the list of just date entry keys
|
||||
let temp_list = Object.keys(this.config.compliments).filter((k) => {
|
||||
if (this.pre_defined_types.includes(k)) return false;
|
||||
else return true;
|
||||
});
|
||||
|
||||
let date_compliments = [];
|
||||
// Add compliments for special day/times
|
||||
for (let entry of temp_list) {
|
||||
// check if this could be a cron type entry
|
||||
if (this.isCronEntry(entry)) {
|
||||
// make sure the regex is valid
|
||||
if (new RegExp(this.cron_regex).test(entry)) {
|
||||
// check if we are in the time range for the cron entry
|
||||
if (this.getSecondsUntilNextCronRun(entry, now.set("seconds", 0).toDate()) <= 1) {
|
||||
// if so, use its notice entries
|
||||
Array.prototype.push.apply(date_compliments, this.config.compliments[entry]);
|
||||
}
|
||||
} else Log.error(`[compliments] cron syntax invalid=${JSON.stringify(entry)}`);
|
||||
} else if (new RegExp(entry).test(date)) {
|
||||
Array.prototype.push.apply(date_compliments, this.config.compliments[entry]);
|
||||
}
|
||||
}
|
||||
|
||||
// if we found any date compliments
|
||||
if (date_compliments.length) {
|
||||
// and the special flag is true
|
||||
if (this.config.specialDayUnique) {
|
||||
// clear the non-date compliments if any
|
||||
compliments.length = 0;
|
||||
}
|
||||
// put the date based compliments on the list
|
||||
Array.prototype.push.apply(compliments, date_compliments);
|
||||
}
|
||||
|
||||
return compliments;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a file from the local filesystem
|
||||
* @returns {Promise} Resolved when the file is loaded
|
||||
* @returns {Promise<string|null>} Resolved with file content or null on error
|
||||
*/
|
||||
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();
|
||||
const { remoteFile, remoteFileRefreshInterval } = this.config;
|
||||
const isRemote = remoteFile.startsWith("http://") || remoteFile.startsWith("https://");
|
||||
let url = isRemote ? remoteFile : this.file(remoteFile);
|
||||
|
||||
try {
|
||||
// Validate URL
|
||||
const urlObj = new URL(url);
|
||||
// Add cache-busting parameter to remote URLs to prevent cached responses
|
||||
if (isRemote && remoteFileRefreshInterval !== 0) {
|
||||
urlObj.searchParams.set("dummy", Date.now());
|
||||
}
|
||||
url = urlObj.toString();
|
||||
} catch {
|
||||
Log.warn(`[compliments] Invalid URL: ${url}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
Log.error(`[compliments] HTTP error: ${response.status} ${response.statusText}`);
|
||||
return null;
|
||||
}
|
||||
return await response.text();
|
||||
} catch (error) {
|
||||
Log.info("[compliments] fetch failed:", error.message);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -169,6 +283,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;
|
||||
},
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/* MagicMirror² Default Modules List
|
||||
/*
|
||||
* Default Modules List
|
||||
* Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const defaultModules = ["alert", "calendar", "clock", "compliments", "helloworld", "newsfeed", "updatenotification", "weather"];
|
||||
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Module: HelloWorld
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("helloworld", {
|
||||
// Default module config.
|
||||
defaults: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Use ` | safe` to allow html tages within the text string.
|
||||
Use ` | safe` to allow html tags within the text string.
|
||||
https://mozilla.github.io/nunjucks/templating.html#autoescaping
|
||||
-->
|
||||
<div>{{ text | safe }}</div>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<div>
|
||||
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
|
||||
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Module: NewsFeed
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("newsfeed", {
|
||||
// Default module config.
|
||||
defaults: {
|
||||
@@ -44,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 "";
|
||||
}
|
||||
@@ -63,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;
|
||||
},
|
||||
@@ -118,27 +112,33 @@ Module.register("newsfeed", {
|
||||
|
||||
//Override template data and return whats used for the current template
|
||||
getTemplateData () {
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
this.activeItem = 0;
|
||||
}
|
||||
this.activeItemCount = this.newsItems.length;
|
||||
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
|
||||
if (this.config.showFullArticle) {
|
||||
this.activeItemHash = this.newsItems[this.activeItem]?.hash;
|
||||
return {
|
||||
url: this.getActiveItemURL()
|
||||
};
|
||||
}
|
||||
if (this.error) {
|
||||
this.activeItemHash = undefined;
|
||||
return {
|
||||
error: this.error
|
||||
};
|
||||
}
|
||||
if (this.newsItems.length === 0) {
|
||||
this.activeItemHash = undefined;
|
||||
return {
|
||||
empty: true
|
||||
};
|
||||
}
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
this.activeItem = 0;
|
||||
}
|
||||
|
||||
const item = this.newsItems[this.activeItem];
|
||||
this.activeItemHash = item.hash;
|
||||
|
||||
const items = this.newsItems.map(function (item) {
|
||||
item.publishDate = moment(new Date(item.pubdate)).fromNow();
|
||||
return item;
|
||||
@@ -150,7 +150,7 @@ Module.register("newsfeed", {
|
||||
sourceTitle: item.sourceTitle,
|
||||
publishDate: moment(new Date(item.pubdate)).fromNow(),
|
||||
title: item.title,
|
||||
url: this.getUrlPrefix(item) + item.url,
|
||||
url: this.getActiveItemURL(),
|
||||
description: item.description,
|
||||
items: items
|
||||
};
|
||||
@@ -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 {string} 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);
|
||||
}
|
||||
}
|
||||
@@ -312,8 +325,27 @@ Module.register("newsfeed", {
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
this.activeItem++;
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
|
||||
/*
|
||||
* When animations are enabled, don't update the DOM unless we are actually changing what we are displaying.
|
||||
* (Animating from a headline to itself is unsightly.)
|
||||
* Cases:
|
||||
*
|
||||
* Number of items | Number of items | Display
|
||||
* at last update | right now | Behaviour
|
||||
* ----------------------------------------------------
|
||||
* 0 | 0 | do not update
|
||||
* 0 | >0 | update
|
||||
* 1 | 0 or >1 | update
|
||||
* 1 | 1 | update only if item details (hash value) changed
|
||||
* >1 | any | update
|
||||
*
|
||||
* (N.B. We set activeItemCount and activeItemHash in getTemplateData().)
|
||||
*/
|
||||
if (this.newsItems.length > 1 || this.newsItems.length !== this.activeItemCount || this.activeItemHash !== this.newsItems[0]?.hash) {
|
||||
this.activeItem++; // this is OK if newsItems.Length==1; getTemplateData will wrap it around
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
}
|
||||
|
||||
// Broadcast NewsFeed if needed
|
||||
if (this.config.broadcastNewsFeeds) {
|
||||
@@ -343,7 +375,7 @@ Module.register("newsfeed", {
|
||||
this.activeItem = 0;
|
||||
}
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
||||
Log.debug(`[newsfeed] going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
||||
this.updateDom(100);
|
||||
} else if (notification === "ARTICLE_PREVIOUS") {
|
||||
this.activeItem--;
|
||||
@@ -351,7 +383,7 @@ Module.register("newsfeed", {
|
||||
this.activeItem = this.newsItems.length - 1;
|
||||
}
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
||||
Log.debug(`[newsfeed] going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
||||
this.updateDom(100);
|
||||
}
|
||||
// if "more details" is received the first time: show article summary, on second time show full article
|
||||
@@ -360,8 +392,8 @@ Module.register("newsfeed", {
|
||||
if (this.config.showFullArticle === true) {
|
||||
this.scrollPosition += this.config.scrollLength;
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
Log.debug(`${this.name} - scrolling down`);
|
||||
Log.debug(`${this.name} - ARTICLE_MORE_DETAILS, scroll position: ${this.config.scrollLength}`);
|
||||
Log.debug("[newsfeed] scrolling down");
|
||||
Log.debug(`[newsfeed] ARTICLE_MORE_DETAILS, scroll position: ${this.config.scrollLength}`);
|
||||
} else {
|
||||
this.showFullArticle();
|
||||
}
|
||||
@@ -369,12 +401,12 @@ Module.register("newsfeed", {
|
||||
if (this.config.showFullArticle === true) {
|
||||
this.scrollPosition -= this.config.scrollLength;
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
Log.debug(`${this.name} - scrolling up`);
|
||||
Log.debug(`${this.name} - ARTICLE_SCROLL_UP, scroll position: ${this.config.scrollLength}`);
|
||||
Log.debug("[newsfeed] scrolling up");
|
||||
Log.debug(`[newsfeed] ARTICLE_SCROLL_UP, scroll position: ${this.config.scrollLength}`);
|
||||
}
|
||||
} else if (notification === "ARTICLE_LESS_DETAILS") {
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.debug(`${this.name} - showing only article titles again`);
|
||||
Log.debug("[newsfeed] showing only article titles again");
|
||||
this.updateDom(100);
|
||||
} else if (notification === "ARTICLE_TOGGLE_FULL") {
|
||||
if (this.config.showFullArticle) {
|
||||
@@ -403,7 +435,7 @@ Module.register("newsfeed", {
|
||||
}
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
Log.debug(`${this.name} - showing ${this.isShowingDescription ? "article description" : "full article"}`);
|
||||
Log.debug(`[newsfeed] showing ${this.isShowingDescription ? "article description" : "full article"}`);
|
||||
this.updateDom(100);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,89 +1,89 @@
|
||||
{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %}
|
||||
{% if dangerouslyDisableAutoEscaping -%}
|
||||
{{ text | safe }}
|
||||
{%- else -%}
|
||||
{{ text }}
|
||||
{%- endif %}
|
||||
{% if dangerouslyDisableAutoEscaping -%}
|
||||
{{ text | safe }}
|
||||
{%- else -%}
|
||||
{{ text }}
|
||||
{%- endif %}
|
||||
{% endmacro %}
|
||||
{% macro escapeTitle(title, url, dangerouslyDisableAutoEscaping=false, showTitleAsUrl=false) %}
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a
|
||||
href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
color:#ffffff"
|
||||
target="_blank">{{ title | safe }}</a>
|
||||
{% else %}
|
||||
{{ title | safe }}
|
||||
{% endif %}
|
||||
target="_blank"
|
||||
>{{ title | safe }}</a
|
||||
>
|
||||
{% else %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
color:#ffffff"
|
||||
target="_blank">{{ title }}</a>
|
||||
{% else %}
|
||||
{{ title }}
|
||||
{% endif %}
|
||||
{{ title | safe }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a
|
||||
href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
color:#ffffff"
|
||||
target="_blank"
|
||||
>{{ title }}</a
|
||||
>
|
||||
{% else %}
|
||||
{{ title }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% if loaded %}
|
||||
{% if config.showAsList %}
|
||||
<ul class="newsfeed-list">
|
||||
{% for item in items %}
|
||||
<li>
|
||||
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if item.sourceTitle and config.showSourceTitle %}
|
||||
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ item.publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
{{ escapeTitle(item.title, item.url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}
|
||||
</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(item.description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div>
|
||||
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if sourceTitle and config.showSourceTitle %}
|
||||
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
{{ escapeTitle(title, url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}
|
||||
{% if config.showAsList %}
|
||||
<ul class="newsfeed-list">
|
||||
{% for item in items %}
|
||||
<li>
|
||||
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if item.sourceTitle and config.showSourceTitle %}
|
||||
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ item.publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">{{ escapeTitle(item.title, item.url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(item.description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div>
|
||||
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if sourceTitle and config.showSourceTitle %}
|
||||
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif empty %}
|
||||
<div class="small dimmed">{{ "NEWSFEED_NO_ITEMS" | translate | safe }}</div>
|
||||
{% elseif error %}
|
||||
<div class="small dimmed">
|
||||
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">{{ escapeTitle(title, url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif empty %}
|
||||
<div class="small dimmed">{{ "NEWSFEED_NO_ITEMS" | translate | safe }}</div>
|
||||
{% elseif error %}
|
||||
<div class="small dimmed">{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}</div>
|
||||
{% else %}
|
||||
<div class="small dimmed">{{ "LOADING" | translate | safe }}</div>
|
||||
<div class="small dimmed">{{ "LOADING" | translate | safe }}</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
/* MagicMirror²
|
||||
* Node Helper: Newsfeed - NewsfeedFetcher
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
const stream = require("stream");
|
||||
const crypto = require("node:crypto");
|
||||
const stream = require("node:stream");
|
||||
const FeedMe = require("feedme");
|
||||
const iconv = require("iconv-lite");
|
||||
const { htmlToText } = require("html-to-text");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
const { getUserAgent } = require("#server_functions");
|
||||
const { scheduleTimer } = require("#module_functions");
|
||||
|
||||
/**
|
||||
* Responsible for requesting an update on the set interval and broadcasting the data.
|
||||
@@ -67,11 +63,11 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
description: description,
|
||||
pubdate: pubdate,
|
||||
url: url,
|
||||
useCorsProxy: useCorsProxy
|
||||
useCorsProxy: useCorsProxy,
|
||||
hash: crypto.createHash("sha256").update(`${pubdate} :: ${title} :: ${url}`).digest("hex")
|
||||
});
|
||||
} else if (logFeedWarnings) {
|
||||
Log.warn("Can't parse feed item:");
|
||||
Log.warn(item);
|
||||
Log.warn("Can't parse feed item:", item);
|
||||
Log.warn(`Title: ${title}`);
|
||||
Log.warn(`Description: ${description}`);
|
||||
Log.warn(`Pubdate: ${pubdate}`);
|
||||
@@ -84,12 +80,12 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
|
||||
parser.on("error", (error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
scheduleTimer(reloadTimer, reloadIntervalMS, fetchNews);
|
||||
});
|
||||
|
||||
//"end" event is not broadcast if the feed is empty but "finish" is used for both
|
||||
parser.on("finish", () => {
|
||||
scheduleTimer();
|
||||
scheduleTimer(reloadTimer, reloadIntervalMS, fetchNews);
|
||||
});
|
||||
|
||||
parser.on("ttl", (minutes) => {
|
||||
@@ -98,16 +94,15 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
const ttlms = Math.min(minutes * 60 * 1000, 86400000);
|
||||
if (ttlms > reloadIntervalMS) {
|
||||
reloadIntervalMS = ttlms;
|
||||
Log.info(`Newsfeed-Fetcher: reloadInterval set to ttl=${reloadIntervalMS} for url ${url}`);
|
||||
Log.info(`reloadInterval set to ttl=${reloadIntervalMS} for url ${url}`);
|
||||
}
|
||||
} catch (error) {
|
||||
Log.warn(`Newsfeed-Fetcher: feed ttl is no valid integer=${minutes} for url ${url}`);
|
||||
Log.warn(`feed ttl is no valid integer=${minutes} for url ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
const headers = {
|
||||
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`,
|
||||
"User-Agent": getUserAgent(),
|
||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache"
|
||||
};
|
||||
@@ -125,20 +120,10 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
scheduleTimer(reloadTimer, reloadIntervalMS, fetchNews);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
const scheduleTimer = function () {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchNews();
|
||||
}, reloadIntervalMS);
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
|
||||
/**
|
||||
@@ -163,10 +148,10 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
*/
|
||||
this.broadcastItems = function () {
|
||||
if (items.length <= 0) {
|
||||
Log.info("Newsfeed-Fetcher: No items to broadcast yet.");
|
||||
Log.info("No items to broadcast yet.");
|
||||
return;
|
||||
}
|
||||
Log.info(`Newsfeed-Fetcher: Broadcasting ${items.length} items.`);
|
||||
Log.info(`Broadcasting ${items.length} items.`);
|
||||
itemsReceivedCallback(this);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Node Helper: Newsfeed
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
const NodeHelper = require("node_helper");
|
||||
const Log = require("logger");
|
||||
const NewsfeedFetcher = require("./newsfeedfetcher");
|
||||
@@ -39,7 +32,7 @@ module.exports = NodeHelper.create({
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
Log.error("Newsfeed Error. Malformed newsfeed url: ", url, error);
|
||||
Log.error("Error: Malformed newsfeed url: ", url, error);
|
||||
this.sendSocketNotification("NEWSFEED_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" });
|
||||
return;
|
||||
}
|
||||
@@ -54,7 +47,7 @@ module.exports = NodeHelper.create({
|
||||
});
|
||||
|
||||
fetcher.onError((fetcher, error) => {
|
||||
Log.error("Newsfeed Error. Could not fetch newsfeed: ", url, error);
|
||||
Log.error("Error: Could not fetch newsfeed: ", url, error);
|
||||
let error_type = NodeHelper.checkFetchError(error);
|
||||
this.sendSocketNotification("NEWSFEED_ERROR", {
|
||||
error_type
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
<div class="small bright">
|
||||
{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}
|
||||
</div>
|
||||
<div class="small bright">{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}</div>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
const util = require("util");
|
||||
const exec = util.promisify(require("child_process").exec);
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const util = require("node:util");
|
||||
const exec = util.promisify(require("node:child_process").exec);
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const Log = require("logger");
|
||||
|
||||
const BASE_DIR = path.normalize(`${__dirname}/../../../`);
|
||||
|
||||
class GitHelper {
|
||||
constructor () {
|
||||
this.gitRepos = [];
|
||||
@@ -35,10 +33,10 @@ class GitHelper {
|
||||
}
|
||||
|
||||
async add (moduleName) {
|
||||
let moduleFolder = BASE_DIR;
|
||||
let moduleFolder = `${global.root_path}`;
|
||||
|
||||
if (moduleName !== "MagicMirror") {
|
||||
moduleFolder = `${moduleFolder}modules/${moduleName}`;
|
||||
moduleFolder = `${moduleFolder}/modules/${moduleName}`;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -128,7 +126,7 @@ class GitHelper {
|
||||
const { stderr } = await this.execShell(`cd ${repo.folder} && git fetch -n --dry-run`);
|
||||
|
||||
// example output:
|
||||
// From https://github.com/MichMich/MagicMirror
|
||||
// From https://github.com/MagicMirrorOrg/MagicMirror
|
||||
// e40ddd4..06389e3 develop -> origin/develop
|
||||
// here the result is in stderr (this is a git default, don't ask why ...)
|
||||
const matches = stderr.match(this.getRefRegex(gitInfo.current));
|
||||
@@ -185,7 +183,10 @@ class GitHelper {
|
||||
this.gitResultList.push(gitInfo);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error(`Failed to retrieve repo info for ${repo.module}: ${e}`);
|
||||
// Only log errors in non-test environments to keep test output clean
|
||||
if (process.env.mmTestMode !== "true") {
|
||||
Log.error(`Failed to retrieve repo info for ${repo.module}: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const NodeHelper = require("node_helper");
|
||||
const defaultModules = require("../defaultmodules");
|
||||
|
||||
const defaultModules = require(`${global.root_path}/modules/default/defaultmodules`);
|
||||
const GitHelper = require("./git_helper");
|
||||
const UpdateHelper = require("./update_helper");
|
||||
|
||||
@@ -14,8 +17,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(`${global.root_path}/modules`);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const Exec = require("child_process").exec;
|
||||
const Spawn = require("child_process").spawn;
|
||||
const commandExists = require("command-exists");
|
||||
const Exec = require("node:child_process").exec;
|
||||
const Spawn = require("node:child_process").spawn;
|
||||
const fs = require("node:fs");
|
||||
|
||||
const Log = require("logger");
|
||||
|
||||
/* class Updater
|
||||
/*
|
||||
* class Updater
|
||||
* Allow to self updating 3rd party modules from command defined in config
|
||||
*
|
||||
* [constructor] read value in config:
|
||||
@@ -45,11 +47,11 @@ 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!");
|
||||
Log.info("Updater Class Loaded!");
|
||||
}
|
||||
|
||||
// [main command] parse if module update is needed
|
||||
@@ -79,17 +81,19 @@ class Updater {
|
||||
|
||||
await Promise.all(parser);
|
||||
let updater = Object.values(this.moduleList);
|
||||
Log.debug("updatenotification Update Result:", updater);
|
||||
Log.debug("Update Result:", updater);
|
||||
return updater;
|
||||
}
|
||||
|
||||
// module updater with his proper command
|
||||
// return object as result
|
||||
//{
|
||||
// error: <boolean>, // if error detected
|
||||
// updated: <boolean>, // if updated successfully
|
||||
// needRestart: <boolean> // if magicmirror restart required
|
||||
//};
|
||||
/*
|
||||
* module updater with his proper command
|
||||
* return object as result
|
||||
* {
|
||||
* error: <boolean>, // if error detected
|
||||
* updated: <boolean>, // if updated successfully
|
||||
* needRestart: <boolean> // if magicmirror restart required
|
||||
* };
|
||||
*/
|
||||
updateProcess (module) {
|
||||
let Result = {
|
||||
error: false,
|
||||
@@ -103,24 +107,24 @@ class Updater {
|
||||
if (module.updateCommand) {
|
||||
Command = module.updateCommand;
|
||||
} else {
|
||||
Log.warn(`updatenotification: Update of ${module.name} is not supported.`);
|
||||
Log.warn(`Update of ${module.name} is not supported.`);
|
||||
return Result;
|
||||
}
|
||||
Log.info(`updatenotification: Updating ${module.name}...`);
|
||||
Log.info(`Updating ${module.name}...`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Exec(Command, { cwd: modulePath, timeout: this.timeout }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
Log.error(`updatenotification: exec error: ${error}`);
|
||||
Log.error(`exec error: ${error}`);
|
||||
Result.error = true;
|
||||
} else {
|
||||
Log.info(`updatenotification: Update logs of ${module.name}: ${stdout}`);
|
||||
Log.info(`Update logs of ${module.name}: ${stdout}`);
|
||||
Result.updated = true;
|
||||
if (this.autoRestart) {
|
||||
Log.info("updatenotification: Update done");
|
||||
Log.info("Update done");
|
||||
setTimeout(() => this.restart(), 3000);
|
||||
} else {
|
||||
Log.info("updatenotification: Update done, don't forget to restart MagicMirror!");
|
||||
Log.info("Update done, don't forget to restart MagicMirror!");
|
||||
Result.needRestart = true;
|
||||
}
|
||||
}
|
||||
@@ -129,83 +133,81 @@ 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 MagicMirror with "pm2": use PM2Id for restart it
|
||||
pm2Restart () {
|
||||
Log.info("updatenotification: PM2 will restarting MagicMirror...");
|
||||
Exec(`pm2 restart ${this.PM2}`, (err, std, sde) => {
|
||||
Log.info("[PM2] restarting MagicMirror...");
|
||||
const pm2 = require("pm2");
|
||||
pm2.restart(this.PM2Id, (err, proc) => {
|
||||
if (err) {
|
||||
Log.error("updatenotification:[PM2] restart Error", err);
|
||||
Log.error("[PM2] restart Error", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// restart MagicMiror with "npm start"
|
||||
npmRestart () {
|
||||
Log.info("updatenotification: Restarting MagicMirror...");
|
||||
// restart MagicMirror with "node --run start"
|
||||
nodeRestart () {
|
||||
Log.info("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();
|
||||
}
|
||||
|
||||
// Check using pm2
|
||||
check_PM2_Process () {
|
||||
Log.info("updatenotification: Checking PM2 using...");
|
||||
Log.info("Checking PM2 using...");
|
||||
return new Promise((resolve) => {
|
||||
commandExists("pm2")
|
||||
.then(async () => {
|
||||
var PM2_List = await this.PM2_GetList();
|
||||
if (!PM2_List) {
|
||||
Log.error("updatenotification: [PM2] Can't get process List!");
|
||||
this.usePM2 = false;
|
||||
if (fs.existsSync("/.dockerenv")) {
|
||||
Log.info("[PM2] Running in docker container, not using PM2 ...");
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.unique_id === undefined) {
|
||||
Log.info("[PM2] You are not using pm2");
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.debug(`[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("[PM2]", err);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
pm2.list((err, list) => {
|
||||
if (err) {
|
||||
Log.error("[PM2] Can't get process List!");
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
PM2_List.forEach((pm) => {
|
||||
if (pm.pm2_env.version === this.version && pm.pm2_env.status === "online" && pm.pm2_env.PWD.includes(this.root_path)) {
|
||||
this.PM2 = pm.name;
|
||||
list.forEach((pm) => {
|
||||
Log.debug(`[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: You are using pm2 with", this.PM2);
|
||||
Log.info(`[PM2] You are using pm2 with id: ${this.PM2Id} (${pm.name})`);
|
||||
resolve(true);
|
||||
} else {
|
||||
Log.debug(`[PM2] pm2 process id: ${pm.pm_id} don't match...`);
|
||||
}
|
||||
});
|
||||
if (!this.PM2) {
|
||||
Log.info("updatenotification: You are not using pm2");
|
||||
this.usePM2 = false;
|
||||
pm2.disconnect();
|
||||
if (!this.usePM2) {
|
||||
Log.info("[PM2] You are not using pm2");
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
Log.info("updatenotification: You are not using pm2");
|
||||
this.usePM2 = false;
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get the list of pm2 process
|
||||
PM2_GetList () {
|
||||
return new Promise((resolve) => {
|
||||
Exec("pm2 jlist", (err, std, sde) => {
|
||||
if (err) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let result = JSON.parse(std);
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
Log.error("updatenotification: [PM2] can't GetList!");
|
||||
Log.debug("updatenotification: [PM2] GetList is not an JSON format", e);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -218,7 +220,7 @@ class Updater {
|
||||
|
||||
// search update module command
|
||||
applyCommand (module) {
|
||||
if (this.isMagicMirror(module.module)) return null;
|
||||
if (this.isMagicMirror(module.module) || !this.updates.length) return null;
|
||||
let command = null;
|
||||
this.updates.forEach((updater) => {
|
||||
if (updater[module]) command = updater[module];
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Module: UpdateNotification
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("updatenotification", {
|
||||
defaults: {
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
@@ -12,13 +6,14 @@ 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,
|
||||
moduleList: {},
|
||||
needRestart: false,
|
||||
updates: {},
|
||||
updates: [],
|
||||
|
||||
start () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
@@ -102,7 +97,7 @@ Module.register("updatenotification", {
|
||||
|
||||
const localRef = status.hash;
|
||||
const remoteRef = status.tracking.replace(/.*\//, "");
|
||||
return `<a href="https://github.com/MichMich/MagicMirror/compare/${localRef}...${remoteRef}" class="xsmall dimmed difflink" target="_blank">${text}</a>`;
|
||||
return `<a href="https://github.com/MagicMirrorOrg/MagicMirror/compare/${localRef}...${remoteRef}" class="xsmall dimmed difflink" target="_blank">${text}</a>`;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
{% if not suspended %}
|
||||
{% if needRestart %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-rotate"></i>
|
||||
<span>
|
||||
{% set restartTextLabel = "UPDATE_NOTIFICATION_NEED-RESTART" %}
|
||||
{{ restartTextLabel | translate() | safe }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for name, status in moduleList %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>
|
||||
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "MagicMirror" else "UPDATE_NOTIFICATION_MODULE" %}
|
||||
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="xsmall dimmed">
|
||||
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
|
||||
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for name, status in updatesList %}
|
||||
<div class="small bright">
|
||||
{% if status.done %}
|
||||
<i class="fas fa-check" style="color: lightgreen;"></i>
|
||||
<span>
|
||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_DONE" %}
|
||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||
</span>
|
||||
{% else %}
|
||||
<i class="fas fa-xmark" style="color: red;"></i>
|
||||
<span>
|
||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_ERROR" %}
|
||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if needRestart %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-rotate"></i>
|
||||
<span>
|
||||
{% set restartTextLabel = "UPDATE_NOTIFICATION_NEED-RESTART" %}
|
||||
{{ restartTextLabel | translate() | safe }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for name, status in moduleList %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>
|
||||
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "MagicMirror" else "UPDATE_NOTIFICATION_MODULE" %}
|
||||
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="xsmall dimmed">
|
||||
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
|
||||
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for name, status in updatesList %}
|
||||
<div class="small bright">
|
||||
{% if status.done %}
|
||||
<i class="fas fa-check" style="color: LightGreen;"></i>
|
||||
<span>
|
||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_DONE" %}
|
||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||
</span>
|
||||
{% else %}
|
||||
<i class="fas fa-xmark" style="color: red;"></i>
|
||||
<span>
|
||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_ERROR" %}
|
||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user