Release v2.25.0 (#3214)

## [2.25.0] - 2023-10-01

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

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

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

### Added

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

### Removed

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

### Updated

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

### Fixed

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

---------

Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
Co-authored-by: Malte Hallström <46646495+SkySails@users.noreply.github.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: veeck <michael@veeck.de>
Co-authored-by: dWoolridge <dwoolridge@charter.net>
Co-authored-by: Johan <jojjepersson@yahoo.se>
Co-authored-by: Dario Mratovich <dario_mratovich@hotmail.com>
Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com>
Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com>
Co-authored-by: Naveen <172697+naveensrinivasan@users.noreply.github.com>
Co-authored-by: buxxi <buxxi@omfilm.net>
Co-authored-by: Thomas Hirschberger <47733292+Tom-Hirschberger@users.noreply.github.com>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com>
Co-authored-by: Dave Child <dave@addedbytes.com>
Co-authored-by: grenagit <46225780+grenagit@users.noreply.github.com>
Co-authored-by: Grena <grena@grenabox.fr>
Co-authored-by: Magnus Marthinsen <magmar@online.no>
Co-authored-by: Patrick <psieg@users.noreply.github.com>
Co-authored-by: Piotr Rajnisz <56397164+rajniszp@users.noreply.github.com>
Co-authored-by: Suthep Yonphimai <tomzt@users.noreply.github.com>
Co-authored-by: CarJem Generations (Carter Wallace) <cwallacecs@gmail.com>
Co-authored-by: Nicholas Fogal <nfogal.misc@gmail.com>
Co-authored-by: JakeBinney <126349119+JakeBinney@users.noreply.github.com>
Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com>
Co-authored-by: Oscar Björkman <17575446+oscarb@users.noreply.github.com>
Co-authored-by: Ismar Slomic <ismar@slomic.no>
Co-authored-by: Jørgen Veum-Wahlberg <jorgen.wahlberg@amedia.no>
Co-authored-by: Eddie Hung <6740044+eddiehung@users.noreply.github.com>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: bugsounet <bugsounet@bugsounet.fr>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Knapoc <Knapoc@users.noreply.github.com>
Co-authored-by: sam detweiler <sdetweil@gmail.com>
Co-authored-by: veeck <michael.veeck@nebenan.de>
Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
Co-authored-by: NolanKingdon <27908974+NolanKingdon@users.noreply.github.com>
Co-authored-by: J. Kenzal Hunter <kenzal.hunter@gmail.com>
Co-authored-by: Teddy <teddy.payet@gmail.com>
Co-authored-by: TeddyStarinvest <teddy.payet@starinvest.com>
Co-authored-by: martingron <61826403+martingron@users.noreply.github.com>
Co-authored-by: dgoth <132394363+dgoth@users.noreply.github.com>
This commit is contained in:
Michael Teeuw 2023-10-01 20:13:41 +02:00 committed by GitHub
parent e87f50e64a
commit 343e7de7bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 4643 additions and 8570 deletions

View file

@ -43,7 +43,7 @@ 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 16 or later (recommended is 18).
**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.

View file

@ -22,6 +22,10 @@ If you are facing an issue or found a bug while trying to install MagicMirror²
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
@ -31,7 +35,7 @@ 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 16 or later (recommended is 18).
**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.

View file

@ -3,7 +3,6 @@ Hello and thank you for wanting to contribute to the MagicMirror² project
**Please make sure that you have followed these 4 rules before submitting your Pull Request:**
> 1. Base your pull requests against the `develop` branch.
>
> 2. Include these infos in the description:
>
> - Does the pull request solve a **related** issue?
@ -13,7 +12,6 @@ Hello and thank you for wanting to contribute to the MagicMirror² project
>
> 3. Please run `npm run lint:prettier` before submitting so that
> style issues are fixed.
>
> 4. Don't forget to add an entry about your changes to
> the CHANGELOG.md file.

View file

@ -5,3 +5,21 @@ updates:
schedule:
interval: "weekly"
target-branch: "develop"
- 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"

View file

@ -18,10 +18,10 @@ jobs:
timeout-minutes: 30
strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
node-version: [18.x, 20.x]
steps:
- name: "Checkout code"
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: "Use Node.js ${{ matrix.node-version }}"
uses: actions/setup-node@v3
with:

View file

@ -18,7 +18,7 @@ jobs:
timeout-minutes: 30
steps:
- name: "Checkout code"
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: "Install dependencies"
run: |
npm ci

View file

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

2
.npmrc Normal file
View file

@ -0,0 +1,2 @@
engine-strict=true
audit=false

View file

@ -5,6 +5,61 @@ 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².
## [2.25.0] - 2023-10-01
Thanks to: @bugsounet, @dgoth, @dependabot, @kenzal, @Knapoc, @KristjanESPERANTO, @martingron, @NolanKingdon, @Paranoid93, @TeddyStarinvest and @Ybbet.
Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome!
> ⚠️ This release needs nodejs version >= `v18`, older releases have reached end of life and will not work!
### Added
- Added UV Index support to OpenWeatherMap
- Added 'hideDuplicates' flag to the calendar module
- Added `allowOverrideNotification` to weather module to enable sending current weather objects with the `CURRENT_WEATHER_OVERRIDE` notification to supplement/replace the current weather displayed
- Added optional AnimateCSS animate for `hide()`, `show()`, `updateDom()`
- Added AnimateIn and animateOut in module config definition
- Apply AnimateIn rules on the first start
- Added automatic client page reload when server was restarted by setting `reloadAfterServerRestart: true` in `config.js`, per default `false` (#3105)
- Added eventClass option for customEvents on the default calendar
- Added AnimateCSS integration in tests suite (#3206)
- Added npm dependabot [Reserved to developer] (#3210)
- Added improved logging for calendar (#3110)
### Removed
- **Breaking Change**: Removed `digest` authentication method from calendar module (which was already broken since release `2.15.0`)
### Updated
- Update roboto fonts to version v5
- Update issue template
- Update dev/dependencies incl. electron to v26
- Replace pretty-quick by lint-staged (<https://github.com/azz/pretty-quick/issues/164>)
- Update engine node >=18. v16 reached it's end of life. (#3170)
- Update typescript definition for modules
- Cleaned up nunjuck templates
- Replace `node-fetch` with internal fetch (#2649) and remove `digest-fetch`
- Update the French translation according to the English file.
- Update dependabot incl. vendor/fonts (monthly check)
- Renew `package-lock.json` for release
### Fixed
- Fix engine check on npm install (#3135)
- Fix undefined formatTime method in clock module (#3143)
- Fix clientonly startup fails after async added (#3151)
- Fix electron width/heigth when using xrandr under bullseye
- Fix time issue with certain recurring events in calendar module
- Fix ipWhiteList test (#3179)
- Fix newsfeed: Convert HTML entities, codes and tag in description (#3191)
- Respect width/height (no fullscreen) if set in electronOptions (together with `fullscreen: false`) in `config.js` (#3174)
- Fix: AnimateCSS merge hide() and show() animated css class when we do multiple call
- Fix `Uncaught SyntaxError: Identifier 'getCorsUrl' has already been declared (at utils.js:1:1)` when using `clock` and `weather` module (#3204)
- Fix overriding `config.js` when running tests (#3201)
- Fix issue in weathergov provider with probability of precipitation not showing up on hourly or daily forecast
## [2.24.0] - 2023-07-01
Thanks to: @angeldeejay, @bugsounet, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @eddiehung, @grenagit, @Hirschberger, @ismarslomic, @JakeBinney, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @oscarb, @OWL4C, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt
@ -46,6 +101,7 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
- Fix date not shown when clock in analog mode (#3100)
- Fix envcanada today percentage-of-precipitation (#3106)
- Fix updatenotification where no branch is checked out but e.g. a version tag (#3130)
- Fix yr weather provider after changes in yr API (#3189)
## [2.23.0] - 2023-04-04

View file

@ -84,6 +84,7 @@
.then(function (configReturn) {
// Pass along the server config via an environment variable
const env = Object.create(process.env);
env.clientonly = true; // set to pass to electron.js
const options = { env: env };
configReturn.address = config.address;
configReturn.port = config.port;

View file

@ -7,31 +7,31 @@
"name": "magicmirror-fonts",
"license": "MIT",
"dependencies": {
"@fontsource/roboto": "^4.5.8",
"@fontsource/roboto-condensed": "^4.5.9"
"@fontsource/roboto": "^5.0.8",
"@fontsource/roboto-condensed": "^5.0.8"
}
},
"node_modules/@fontsource/roboto": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz",
"integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA=="
"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": "4.5.9",
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-4.5.9.tgz",
"integrity": "sha512-ql4sQq+h8puBVildZ5ssjYf8DWDONYDe3PD3Bu/p1ZW9GnRETRNPPcCTs/q62HIl3QimwwkiKWynn6wZhQaetg=="
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.0.8.tgz",
"integrity": "sha512-xAXYY+ys24OZ/eOfXJZILPu2xOB7c0ZruM4cd4TSzX3WGj4dZbXYwCEowLldKbZye6LTqiltpFLP/g/Ne0qGLg=="
}
},
"dependencies": {
"@fontsource/roboto": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz",
"integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA=="
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.8.tgz",
"integrity": "sha512-XxPltXs5R31D6UZeLIV1td3wTXU3jzd3f2DLsXI8tytMGBkIsGcc9sIyiupRtA8y73HAhuSCeweOoBqf6DbWCA=="
},
"@fontsource/roboto-condensed": {
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-4.5.9.tgz",
"integrity": "sha512-ql4sQq+h8puBVildZ5ssjYf8DWDONYDe3PD3Bu/p1ZW9GnRETRNPPcCTs/q62HIl3QimwwkiKWynn6wZhQaetg=="
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.0.8.tgz",
"integrity": "sha512-xAXYY+ys24OZ/eOfXJZILPu2xOB7c0ZruM4cd4TSzX3WGj4dZbXYwCEowLldKbZye6LTqiltpFLP/g/Ne0qGLg=="
}
}
}

View file

@ -10,7 +10,7 @@
"url": "https://github.com/MichMich/MagicMirror/issues"
},
"dependencies": {
"@fontsource/roboto": "^4.5.8",
"@fontsource/roboto-condensed": "^4.5.9"
"@fontsource/roboto": "^5.0.8",
"@fontsource/roboto-condensed": "^5.0.8"
}
}

View file

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

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<title>MagicMirror²</title>
@ -13,6 +13,7 @@
<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" />
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
<script type="text/javascript">
@ -45,6 +46,7 @@
<script type="text/javascript" src="#CONFIG_FILE#"></script>
<script type="text/javascript" src="vendor/vendor.js"></script>
<script type="text/javascript" src="modules/default/defaultmodules.js"></script>
<script type="text/javascript" src="modules/default/utils.js"></script>
<script type="text/javascript" src="js/logger.js"></script>
<script type="text/javascript" src="translations/translations.js"></script>
<script type="text/javascript" src="js/translator.js"></script>
@ -52,6 +54,7 @@
<script type="text/javascript" src="js/module.js"></script>
<script type="text/javascript" src="js/loader.js"></script>
<script type="text/javascript" src="js/socketclient.js"></script>
<script type="text/javascript" src="js/animateCSS.js"></script>
<script type="text/javascript" src="js/main.js"></script>
</body>
</html>

164
js/animateCSS.js Normal file
View file

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

View file

@ -29,6 +29,11 @@ const defaults = {
// e.g. you need to add `frameguard: false` for embedding MagicMirror in another website, see https://github.com/MichMich/MagicMirror/issues/2847
httpHeaders: { contentSecurityPolicy: false, crossOriginOpenerPolicy: false, crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false },
// properties for checking if server is alive and has same startup-timestamp, the check is per default enabled
// (interval 30 seconds). If startup-timestamp has changed the client reloads the magicmirror webpage.
checkServerInterval: 30 * 1000,
reloadAfterServerRestart: false,
modules: [
{
module: "updatenotification",

View file

@ -25,11 +25,20 @@ let mainWindow;
*
*/
function createWindow() {
// see https://www.electronjs.org/docs/latest/api/screen
// Create a window that fills the screen's available work area.
let electronSize = (800, 600);
try {
electronSize = electron.screen.getPrimaryDisplay().workAreaSize;
} catch {
Log.warn("Could not get display size, using defaults ...");
}
let electronSwitchesDefaults = ["autoplay-policy", "no-user-gesture-required"];
app.commandLine.appendSwitch(...new Set(electronSwitchesDefaults, config.electronSwitches));
let electronOptionsDefaults = {
width: 800,
height: 600,
width: electronSize.width,
height: electronSize.height,
x: 0,
y: 0,
darkTheme: true,
@ -50,6 +59,7 @@ function createWindow() {
electronOptionsDefaults.frame = false;
electronOptionsDefaults.transparent = true;
electronOptionsDefaults.hasShadow = false;
electronOptionsDefaults.fullscreen = true;
}
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
@ -121,7 +131,6 @@ function createWindow() {
});
mainWindow.once("ready-to-show", () => {
mainWindow.setFullScreen(true);
mainWindow.show();
});
}
@ -168,6 +177,13 @@ app.on("certificate-error", (event, webContents, url, error, certificate, callba
callback(true);
});
if (process.env.clientonly) {
app.whenReady().then(() => {
Log.log("Launching client viewer application.");
createWindow();
});
}
// Start the core application if server is run on localhost
// This starts all node helpers and starts the webserver.
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {

View file

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

View file

@ -88,6 +88,8 @@ const Loader = (function () {
path: `${moduleFolder}/`,
file: `${moduleName}.js`,
position: moduleData.position,
animateIn: moduleData.animateIn,
animateOut: moduleData.animateOut,
hiddenOnStartup: moduleData.hiddenOnStartup,
header: moduleData.header,
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,

View file

@ -1,4 +1,4 @@
/* global Loader, defaults, Translator */
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut */
/* MagicMirror²
* Main System
@ -22,6 +22,10 @@ const MM = (function () {
return;
}
let haveAnimateIn = null;
// check if have valid animateIn in module definition (module.data.animateIn)
if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateIn = module.data.animateIn;
const wrapper = selectWrapper(module.data.position);
const dom = document.createElement("div");
@ -50,7 +54,12 @@ const MM = (function () {
moduleContent.className = "module-content";
dom.appendChild(moduleContent);
const domCreationPromise = updateDom(module, 0);
// create the domCreationPromise with AnimateCSS (with animateIn of module definition)
// or just display it
var domCreationPromise;
if (haveAnimateIn) domCreationPromise = updateDom(module, { options: { speed: 1000, animate: { in: haveAnimateIn } } }, true);
else domCreationPromise = updateDom(module, 0);
domCreationPromises.push(domCreationPromise);
domCreationPromise
.then(function () {
@ -101,11 +110,30 @@ const MM = (function () {
/**
* Update the dom for a specific module.
* @param {Module} module The module that needs an update.
* @param {number} [speed] The (optional) number of microseconds for the animation.
* @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates)
* @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start of MagicMirror)
* @returns {Promise} Resolved when the dom is fully updated.
*/
const updateDom = function (module, speed) {
const updateDom = function (module, updateOptions, createAnimatedDom = false) {
return new Promise(function (resolve) {
let speed = updateOptions;
let animateOut = null;
let animateIn = null;
if (typeof updateOptions === "object") {
if (typeof updateOptions.options === "object" && updateOptions.options.speed !== undefined) {
speed = updateOptions.options.speed;
Log.debug(`updateDom: ${module.identifier} Has speed in object: ${speed}`);
if (typeof updateOptions.options.animate === "object") {
animateOut = updateOptions.options.animate.out;
animateIn = updateOptions.options.animate.in;
Log.debug(`updateDom: ${module.identifier} Has animate in object: out->${animateOut}, in->${animateIn}`);
}
} else {
Log.debug(`updateDom: ${module.identifier} Has no speed in object`);
speed = 0;
}
}
const newHeader = module.getHeader();
let newContentPromise = module.getDom();
@ -116,7 +144,7 @@ const MM = (function () {
newContentPromise
.then(function (newContent) {
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom);
updatePromise.then(resolve).catch(Log.error);
})
@ -130,9 +158,12 @@ const MM = (function () {
* @param {number} [speed] The (optional) number of microseconds for the animation.
* @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated.
* @param {string} [animateOut] AnimateCss animation name before hidden
* @param {string} [animateIn] AnimateCss animation name on show
* @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start)
* @returns {Promise} Resolved when the module dom has been updated.
*/
const updateDomWithContent = function (module, speed, newHeader, newContent) {
const updateDomWithContent = function (module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom = false) {
return new Promise(function (resolve) {
if (module.hidden || !speed) {
updateModuleContent(module, newHeader, newContent);
@ -151,13 +182,28 @@ const MM = (function () {
return;
}
hideModule(module, speed / 2, function () {
if (createAnimatedDom && animateIn !== null) {
Log.debug(`${module.identifier} createAnimatedDom (${animateIn})`);
updateModuleContent(module, newHeader, newContent);
if (!module.hidden) {
showModule(module, speed / 2);
showModule(module, speed, null, { animate: animateIn });
}
resolve();
});
return;
}
hideModule(
module,
speed / 2,
function () {
updateModuleContent(module, newHeader, newContent);
if (!module.hidden) {
showModule(module, speed / 2, null, { animate: animateIn });
}
resolve();
},
{ animate: animateOut }
);
});
};
@ -234,11 +280,51 @@ const MM = (function () {
const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) {
clearTimeout(module.showHideTimer);
// reset all animations if needed
if (module.hasAnimateOut) {
removeAnimateCSS(module.identifier, module.hasAnimateOut);
Log.debug(`${module.identifier} Force remove animateOut (in hide): ${module.hasAnimateOut}`);
module.hasAnimateOut = false;
}
if (module.hasAnimateIn) {
removeAnimateCSS(module.identifier, module.hasAnimateIn);
Log.debug(`${module.identifier} Force remove animateIn (in hide): ${module.hasAnimateIn}`);
module.hasAnimateIn = false;
}
// haveAnimateName for verify if we are using AninateCSS library
// we check AnimateCSSOut Array for validate it
// and finaly return the animate name or `null` (for default MM² animation)
let haveAnimateName = null;
// check if have valid animateOut in module definition (module.data.animateOut)
if (module.data.animateOut && AnimateCSSOut.indexOf(module.data.animateOut) !== -1) haveAnimateName = module.data.animateOut;
// can't be override with options.animate
else if (options.animate && AnimateCSSOut.indexOf(options.animate) !== -1) haveAnimateName = options.animate;
if (haveAnimateName) {
// with AnimateCSS
Log.debug(`${module.identifier} Has animateOut: ${haveAnimateName}`);
module.hasAnimateOut = haveAnimateName;
addAnimateCSS(module.identifier, haveAnimateName, speed / 1000);
module.showHideTimer = setTimeout(function () {
removeAnimateCSS(module.identifier, haveAnimateName);
Log.debug(`${module.identifier} Remove animateOut: ${module.hasAnimateOut}`);
// AnimateCSS is now done
moduleWrapper.style.opacity = 0;
moduleWrapper.classList.add("hidden");
moduleWrapper.style.position = "fixed";
module.hasAnimateOut = false;
updateWrapperStates();
if (typeof callback === "function") {
callback();
}
}, speed);
} else {
// default MM² Animate
moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
moduleWrapper.style.opacity = 0;
moduleWrapper.classList.add("hidden");
clearTimeout(module.showHideTimer);
module.showHideTimer = setTimeout(function () {
// To not take up any space, we just make the position absolute.
// since it's fade out anyway, we can see it lay above or
@ -252,6 +338,7 @@ const MM = (function () {
callback();
}
}, speed);
}
} else {
// invoke callback even if no content, issue 1308
if (typeof callback === "function") {
@ -285,6 +372,17 @@ const MM = (function () {
}
return;
}
// reset all animations if needed
if (module.hasAnimateOut) {
removeAnimateCSS(module.identifier, module.hasAnimateOut);
Log.debug(`${module.identifier} Force remove animateOut (in show): ${module.hasAnimateOut}`);
module.hasAnimateOut = false;
}
if (module.hasAnimateIn) {
removeAnimateCSS(module.identifier, module.hasAnimateIn);
Log.debug(`${module.identifier} Force remove animateIn (in show): ${module.hasAnimateIn}`);
module.hasAnimateIn = false;
}
module.hidden = false;
@ -296,7 +394,18 @@ const MM = (function () {
const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) {
moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
clearTimeout(module.showHideTimer);
// haveAnimateName for verify if we are using AninateCSS library
// we check AnimateCSSIn Array for validate it
// and finaly return the animate name or `null` (for default MM² animation)
let haveAnimateName = null;
// check if have valid animateOut in module definition (module.data.animateIn)
if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateName = module.data.animateIn;
// can't be override with options.animate
else if (options.animate && AnimateCSSIn.indexOf(options.animate) !== -1) haveAnimateName = options.animate;
if (!haveAnimateName) moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
// Restore the position. See hideModule() for more info.
moduleWrapper.style.position = "static";
moduleWrapper.classList.remove("hidden");
@ -307,12 +416,27 @@ const MM = (function () {
const dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
moduleWrapper.style.opacity = 1;
clearTimeout(module.showHideTimer);
if (haveAnimateName) {
// with AnimateCSS
Log.debug(`${module.identifier} Has animateIn: ${haveAnimateName}`);
module.hasAnimateIn = haveAnimateName;
addAnimateCSS(module.identifier, haveAnimateName, speed / 1000);
module.showHideTimer = setTimeout(function () {
removeAnimateCSS(module.identifier, haveAnimateName);
Log.debug(`${module.identifier} Remove animateIn: ${haveAnimateName}`);
module.hasAnimateIn = false;
if (typeof callback === "function") {
callback();
}
}, speed);
} else {
// default MM² Animate
module.showHideTimer = setTimeout(function () {
if (typeof callback === "function") {
callback();
}
}, speed);
}
} else {
// invoke callback
if (typeof callback === "function") {
@ -477,12 +601,33 @@ const MM = (function () {
*/
modulesStarted: function (moduleObjects) {
modules = [];
let startUp = "";
moduleObjects.forEach((module) => modules.push(module));
Log.info("All modules started!");
sendNotification("ALL_MODULES_STARTED");
createDomObjects();
if (config.reloadAfterServerRestart) {
setInterval(async () => {
// if server startup time has changed (which means server was restarted)
// the client reloads the mm page
try {
const res = await fetch(`${location.protocol}//${location.host}/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");
}
} catch (err) {
Log.error(`MagicMirror not reachable: ${err}`);
}
}, config.checkServerInterval);
}
},
/**
@ -514,9 +659,9 @@ const MM = (function () {
/**
* Update the dom for a specific module.
* @param {Module} module The module that needs an update.
* @param {number} [speed] The number of microseconds for the animation.
* @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates)
*/
updateDom: function (module, speed) {
updateDom: function (module, updateOptions) {
if (!(module instanceof Module)) {
Log.error("updateDom: Sender should be a module.");
return;
@ -528,7 +673,7 @@ const MM = (function () {
}
// Further implementation is done in the private method.
updateDom(module, speed);
updateDom(module, updateOptions);
},
/**

View file

@ -193,7 +193,7 @@ const Module = Class.extend({
},
/*********************************************
* The methods below don"t need subclassing. *
* The methods below don't need subclassing. *
*********************************************/
/**
@ -205,6 +205,8 @@ const Module = Class.extend({
this.name = data.name;
this.identifier = data.identifier;
this.hidden = false;
this.hasAnimateIn = false;
this.hasAnimateOut = false;
this.setConfig(data.config, data.configDeepMerge);
},
@ -327,10 +329,10 @@ const Module = Class.extend({
/**
* Request an (animated) update of the module.
* @param {number} [speed] The speed of the animation.
* @param {number|object} [updateOptions] The speed of the animation or object with for updateOptions (speed/animates)
*/
updateDom: function (speed) {
MM.updateDom(this, speed);
updateDom: function (updateOptions) {
MM.updateDom(this, updateOptions);
},
/**

View file

@ -15,7 +15,7 @@ const socketio = require("socket.io");
const Log = require("logger");
const Utils = require("./utils");
const { cors, getConfig, getHtml, getVersion } = require("./server_functions");
const { cors, getConfig, getHtml, getVersion, getStartup } = require("./server_functions");
/**
* Server
@ -91,6 +91,8 @@ function Server(config) {
app.get("/config", (req, res) => getConfig(req, res));
app.get("/startup", (req, res) => getStartup(req, res));
app.get("/", (req, res) => getHtml(req, res));
server.on("listening", () => {

View file

@ -1,7 +1,7 @@
const fs = require("fs");
const path = require("path");
const Log = require("logger");
const fetch = require("./fetch");
const startUp = new Date();
/**
* Gets the config.
@ -12,6 +12,15 @@ function getConfig(req, res) {
res.send(config);
}
/**
* Gets the startup time.
* @param {Request} req - the request
* @param {Response} res - the result
*/
function getStartup(req, res) {
res.send(startUp);
}
/**
* A method that forwards HTTP Get-methods to the internet to avoid CORS-errors.
*
@ -118,4 +127,4 @@ function getVersion(req, res) {
res.send(global.version);
}
module.exports = { cors, getConfig, getHtml, getVersion };
module.exports = { cors, getConfig, getHtml, getVersion, getStartup };

View file

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

View file

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

View file

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

View file

@ -39,9 +39,10 @@ Module.register("calendar", {
hidePrivate: false,
hideOngoing: false,
hideTime: false,
hideDuplicates: true,
showTimeToday: false,
colored: false,
customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
customEvents: [], // Array of {keyword: "", symbol: "", color: "", eventClass: ""} where Keyword is a regexp and symbol/color/eventClass are to be applied for matched
tableClass: "small",
calendars: [
{
@ -154,11 +155,14 @@ Module.register("calendar", {
// Refresh the DOM every minute if needed: When using relative date format for events that start
// or end in less than an hour, the date shows minute granularity and we want to keep that accurate.
setTimeout(() => {
setTimeout(
() => {
setInterval(() => {
this.updateDom(1);
}, ONE_MINUTE);
}, ONE_MINUTE - (new Date() % ONE_MINUTE));
},
ONE_MINUTE - (new Date() % ONE_MINUTE)
);
},
// Override socket notification handler.
@ -317,12 +321,12 @@ Module.register("calendar", {
}
}
// Color events if custom color is specified
// Color events if custom color or eventClass are specified
if (this.config.customEvents.length > 0) {
for (let ev in this.config.customEvents) {
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
if (needle.test(event.title)) {
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
// Respect parameter ColoredSymbolOnly also for custom events
if (this.config.coloredText) {
eventWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
@ -333,6 +337,9 @@ Module.register("calendar", {
}
break;
}
if (typeof this.config.customEvents[ev].eventClass !== "undefined" && this.config.customEvents[ev].eventClass !== "") {
eventWrapper.className += ` ${this.config.customEvents[ev].eventClass}`;
}
}
}
}
@ -571,13 +578,14 @@ Module.register("calendar", {
if (this.config.hideOngoing && event.startDate < now) {
continue;
}
if (this.listContainsEvent(events, event)) {
if (this.config.hideDuplicates && this.listContainsEvent(events, event)) {
continue;
}
if (--remainingEntries < 0) {
break;
}
}
event.url = calendarUrl;
event.today = event.startDate >= today && event.startDate < today + ONE_DAY;
event.dayBeforeYesterday = event.startDate >= today - ONE_DAY * 2 && event.startDate < today - ONE_DAY;
@ -662,7 +670,7 @@ Module.register("calendar", {
listContainsEvent: function (eventList, event) {
for (const evt of eventList) {
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) {
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate) && parseInt(evt.endDate) === parseInt(event.endDate)) {
return true;
}
}

View file

@ -6,9 +6,7 @@
*/
const https = require("https");
const digest = require("digest-fetch");
const ical = require("node-ical");
const fetch = require("fetch");
const Log = require("logger");
const NodeHelper = require("node_helper");
const CalendarFetcherUtils = require("./calendarfetcherutils");
@ -39,7 +37,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
clearTimeout(reloadTimer);
reloadTimer = null;
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
let fetcher = null;
let httpsAgent = null;
let headers = {
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`
@ -53,17 +50,12 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
if (auth) {
if (auth.method === "bearer") {
headers.Authorization = `Bearer ${auth.pass}`;
} else if (auth.method === "digest") {
fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, agent: httpsAgent });
} else {
headers.Authorization = `Basic ${Buffer.from(`${auth.user}:${auth.pass}`).toString("base64")}`;
}
}
if (fetcher === null) {
fetcher = fetch(url, { headers: headers, agent: httpsAgent });
}
fetcher
fetch(url, { headers: headers, agent: httpsAgent })
.then(NodeHelper.checkFetchStatus)
.then((response) => response.text())
.then((responseData) => {
@ -115,7 +107,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
* Broadcast the existing events.
*/
this.broadcastEvents = function () {
Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events.`);
Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events from ${url}.`);
eventsReceivedCallback(this);
};

View file

@ -313,6 +313,9 @@ const CalendarFetcherUtils = {
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();

View file

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

View file

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

View file

@ -8,7 +8,7 @@
const stream = require("stream");
const FeedMe = require("feedme");
const iconv = require("iconv-lite");
const fetch = require("fetch");
const { htmlToText } = require("html-to-text");
const Log = require("logger");
const NodeHelper = require("node_helper");
@ -54,6 +54,8 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
if (title && pubdate) {
const regex = /(<([^>]+)>)/gi;
description = description.toString().replace(regex, "");
// Convert HTML entities, codes and tag
description = htmlToText(description, { wordwrap: false });
items.push({
title: title,

View file

@ -65,9 +65,12 @@ module.exports = NodeHelper.create({
scheduleNextFetch(delay) {
clearTimeout(this.updateTimer);
this.updateTimer = setTimeout(() => {
this.updateTimer = setTimeout(
() => {
this.performFetch();
}, Math.max(delay, ONE_MINUTE));
},
Math.max(delay, ONE_MINUTE)
);
},
ignoreUpdateChecking(moduleName) {

View file

@ -7,7 +7,8 @@
{% if config.showWindDirection %}
<sup>
{% if config.showWindDirectionAsArrow %}
<i class="fas fa-long-arrow-alt-down" style="transform:rotate({{ current.windFromDirection }}deg);"></i>
<i class="fas fa-long-arrow-alt-down"
style="transform:rotate({{ current.windFromDirection }}deg)"></i>
{% else %}
{{ current.cardinalWindDirection() | translate }}
{% endif %}
@ -37,26 +38,20 @@
</div>
{% endif %}
<div class="large light">
<span class="wi weathericon wi-{{current.weatherType}}"></span>
<span class="bright">
{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}
</span>
<span class="wi weathericon wi-{{ current.weatherType }}"></span>
<span class="bright">{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}</span>
</div>
<div class="normal light indoor">
{% if config.showIndoorTemperature and indoor.temperature %}
<div>
<span class="fas fa-home"></span>
<span class="bright">
{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}
</span>
<span class="bright">{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}</span>
</div>
{% endif %}
{% if config.showIndoorHumidity and indoor.humidity %}
<div>
<span class="fas fa-tint"></span>
<span class="bright">
{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}
</span>
<span class="bright">{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}</span>
</div>
{% endif %}
</div>
@ -65,12 +60,14 @@
{% if config.showFeelsLike %}
<span class="dimmed">
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
</span><br/>
</span>
<br />
{% endif %}
{% if config.showPrecipitationAmount and current.precipitationAmount %}
<span class="dimmed">
<span class="precipitationLeadText">{{ "PRECIP_AMOUNT" | translate }}</span> {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }}
</span><br/>
</span>
<br />
{% endif %}
{% if config.showPrecipitationProbability and current.precipitationProbability %}
<span class="dimmed">
@ -80,10 +77,7 @@
</div>
{% endif %}
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate }}
</div>
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `current` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{current | dump}}</div> -->

View file

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

View file

@ -4,9 +4,12 @@
<table class="{{ config.tableClass }}">
{% set hours = hourly.slice(0, numSteps) %}
{% for hour in hours %}
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
<tr {% if config.colored %}class="colored"{% endif %}
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
<td class="day">{{ hour.date | formatTime }}</td>
<td class="bright weather-icon"><span class="wi weathericon wi-{{ hour.weatherType }}"></span></td>
<td class="bright weather-icon">
<span class="wi weathericon wi-{{ hour.weatherType }}"></span>
</td>
<td class="align-right bright">
{{ hour.temperature | roundValue | unit("temperature") }}
</td>
@ -25,7 +28,7 @@
{% endif %}
{% if config.showPrecipitationProbability %}
<td class="align-right bright precipitation-prob">
{{ hour.precipitationProbability | unit("precip", "%") }}
{{ hour.precipitationProbability | unit('precip', '%') }}
</td>
{% endif %}
</tr>
@ -33,10 +36,7 @@
{% endfor %}
</table>
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate }}
</div>
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{hourly | dump}}</div> -->

View file

@ -295,6 +295,7 @@ WeatherProvider.register("openweathermap", {
current.temperature = data.current.temp;
current.weatherType = this.convertWeatherType(data.current.weather[0].icon);
current.humidity = data.current.humidity;
current.uv_index = data.current.uvi;
if (data.current.hasOwnProperty("rain") && !isNaN(data.current["rain"]["1h"])) {
current.rain = data.current["rain"]["1h"];
precip = true;
@ -323,6 +324,7 @@ WeatherProvider.register("openweathermap", {
weather.windFromDirection = hour.wind_deg;
weather.weatherType = this.convertWeatherType(hour.weather[0].icon);
weather.precipitationProbability = hour.pop ? hour.pop * 100 : undefined;
weather.uv_index = hour.uvi;
precip = false;
if (hour.hasOwnProperty("rain") && !isNaN(hour.rain["1h"])) {
weather.rain = hour.rain["1h"];
@ -355,6 +357,7 @@ WeatherProvider.register("openweathermap", {
weather.windFromDirection = day.wind_deg;
weather.weatherType = this.convertWeatherType(day.weather[0].icon);
weather.precipitationProbability = day.pop ? day.pop * 100 : undefined;
weather.uv_index = day.uvi;
precip = false;
if (!isNaN(day.rain)) {
weather.rain = day.rain;

View file

@ -0,0 +1,112 @@
/* global Class, WeatherObject */
/*
* Wrapper class to enable overrides of currentOverrideWeatherObject.
*
* Sits between the weather.js module and the provider implementations to allow us to
* combine the incoming data from the CURRENT_WEATHER_OVERRIDE notification with the
* existing data received from the current api provider. If no notifications have
* been received then the api provider's data is used.
*
* The intent is to allow partial WeatherObjects from local sensors to augment or
* replace the WeatherObjects from the api providers.
*
* This class shares the signature of WeatherProvider, and passes any methods not
* concerning the current weather directly to the api provider implementation that
* is currently in use.
*/
const OverrideWrapper = Class.extend({
baseProvider: null,
providerName: "localWrapper",
notificationWeatherObject: null,
currentOverrideWeatherObject: null,
init(baseProvider) {
this.baseProvider = baseProvider;
// Binding the scope of current weather functions so any fetchData calls with
// setCurrentWeather nested in them call this classes implementation instead
// of the provider's default
this.baseProvider.setCurrentWeather = this.setCurrentWeather.bind(this);
this.baseProvider.currentWeather = this.currentWeather.bind(this);
},
/* Unchanged Api Provider Methods */
setConfig(config) {
this.baseProvider.setConfig(config);
},
start() {
this.baseProvider.start();
},
fetchCurrentWeather() {
this.baseProvider.fetchCurrentWeather();
},
fetchWeatherForecast() {
this.baseProvider.fetchWeatherForecast();
},
fetchWeatherHourly() {
this.baseProvider.fetchEatherHourly();
},
weatherForecast() {
this.baseProvider.weatherForecast();
},
weatherHourly() {
this.baseProvider.weatherHourly();
},
fetchedLocation() {
this.baseProvider.fetchedLocation();
},
setWeatherForecast(weatherForecastArray) {
this.baseProvider.setWeatherForecast(weatherForecastArray);
},
setWeatherHourly(weatherHourlyArray) {
this.baseProvider.setWeatherHourly(weatherHourlyArray);
},
setFetchedLocation(name) {
this.baseProvider.setFetchedLocation(name);
},
updateAvailable() {
this.baseProvider.updateAvailable();
},
async fetchData(url, type = "json", requestHeaders = undefined, expectedResponseHeaders = undefined) {
this.baseProvider.fetchData(url, type, requestHeaders, expectedResponseHeaders);
},
/* Override Methods */
/**
* Override to return this scope's
* @returns {WeatherObject} The current weather object. May or may not contain overridden data.
*/
currentWeather() {
return this.currentOverrideWeatherObject;
},
/**
* Override to combine the overrideWeatherObejct provided in the
* notificationReceived method with the currentOverrideWeatherObject provided by the
* api provider fetchData implementation.
* @param {WeatherObject} currentWeatherObject - the api provider weather object
*/
setCurrentWeather(currentWeatherObject) {
this.currentOverrideWeatherObject = Object.assign(currentWeatherObject, this.notificationWeatherObject);
},
/**
* Updates the overrideWeatherObject, calls setCurrentWeather to combine it with
* the existing current weather object provided by the base provider, and signals
* that an update is ready.
* @param {WeatherObject} payload - the weather object received from the CURRENT_WEATHER_OVERRIDE
* notification. Represents information to augment the
* existing currentOverrideWeatherObject with.
*/
notificationReceived(payload) {
this.notificationWeatherObject = payload;
// setCurrentWeather combines the newly received notification weather with
// the existing weather object we return for current weather
this.setCurrentWeather(this.currentOverrideWeatherObject);
this.updateAvailable();
}
});

View file

@ -182,6 +182,12 @@ WeatherProvider.register("weathergov", {
weather.windSpeed = WeatherUtils.convertWindToMs(weather.windSpeed);
weather.windFromDirection = forecast.windDirection;
weather.temperature = forecast.temperature;
//assign probability of precipitation
if (forecast.probabilityOfPrecipitation.value === null) {
weather.precipitationProbability = 0;
} else {
weather.precipitationProbability = forecast.probabilityOfPrecipitation.value;
}
// use the forecast isDayTime attribute to help build the weatherType label
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
@ -238,8 +244,6 @@ WeatherProvider.register("weathergov", {
* fetch forecast information for daily forecast.
*/
fetchForecastDaily(forecasts) {
const precipitationProbabilityRegEx = "Chance of precipitation is ([0-9]+?)%";
// initial variable declaration
const days = [];
// variables for temperature range and rain
@ -262,8 +266,12 @@ WeatherProvider.register("weathergov", {
minTemp = [];
maxTemp = [];
const precipitation = new RegExp(precipitationProbabilityRegEx, "g").exec(forecast.detailedForecast);
if (precipitation) weather.precipitationProbability = precipitation[1];
//assign probability of precipitation
if (forecast.probabilityOfPrecipitation.value === null) {
weather.precipitationProbability = 0;
} else {
weather.precipitationProbability = forecast.probabilityOfPrecipitation.value;
}
// set new date
date = moment(forecast.startTime).format("YYYY-MM-DD");

View file

@ -352,8 +352,7 @@ WeatherProvider.register("yr", {
if (hours.length < 2) {
hours = `0${hours}`;
}
return `${this.config.apiBase}/sunrise/2.0/.json?date=${date}&days=${days}&height=${altitude}&lat=${lat}&lon=${lon}&offset=${utcOffsetPrefix}${hours}%3A${minutes}`;
return `${this.config.apiBase}/sunrise/2.3/sun?lat=${lat}&lon=${lon}&date=${date}&offset=${utcOffsetPrefix}${hours}%3A${minutes}`;
},
cacheStellarData(data) {
@ -362,8 +361,6 @@ WeatherProvider.register("yr", {
getWeatherDataFrom(forecast, stellarData, units) {
const weather = new WeatherObject();
const stellarTimesToday = stellarData?.today ? this.getStellarTimesFrom(stellarData.today, moment().format("YYYY-MM-DD")) : undefined;
const stellarTimesTomorrow = stellarData?.tomorrow ? this.getStellarTimesFrom(stellarData.tomorrow, moment().add(1, "days").format("YYYY-MM-DD")) : undefined;
weather.date = moment(forecast.time);
weather.windSpeed = forecast.data.instant.details.wind_speed;
@ -377,10 +374,8 @@ WeatherProvider.register("yr", {
weather.precipitationProbability = forecast.precipitationProbability;
weather.precipitationUnits = units.precipitation_amount;
if (stellarTimesToday) {
weather.sunset = moment(stellarTimesToday.sunset.time);
weather.sunrise = weather.sunset < moment() && stellarTimesTomorrow ? moment(stellarTimesTomorrow.sunrise.time) : moment(stellarTimesToday.sunrise.time);
}
weather.sunrise = stellarData?.today?.properties?.sunrise?.time;
weather.sunset = stellarData?.today?.properties?.sunset?.time;
return weather;
},

View file

@ -23,6 +23,7 @@ Module.register("weather", {
showHumidity: false,
showIndoorHumidity: false,
showIndoorTemperature: false,
allowOverrideNotification: false,
showPeriod: true,
showPeriodUpper: false,
showPrecipitationAmount: false,
@ -61,7 +62,7 @@ Module.register("weather", {
// Return the scripts that are necessary for the weather module.
getScripts: function () {
return ["moment.js", this.file("../utils.js"), "weatherutils.js", "weatherprovider.js", "weatherobject.js", "suncalc.js", this.file(`providers/${this.config.weatherProvider.toLowerCase()}.js`)];
return ["moment.js", "weatherutils.js", "weatherobject.js", this.file("providers/overrideWrapper.js"), "weatherprovider.js", "suncalc.js", this.file(`providers/${this.config.weatherProvider.toLowerCase()}.js`)];
},
// Override getHeader method.
@ -119,6 +120,8 @@ Module.register("weather", {
} else if (notification === "INDOOR_HUMIDITY") {
this.indoorHumidity = this.roundValue(payload);
this.updateDom(300);
} else if (notification === "CURRENT_WEATHER_OVERRIDE" && this.config.allowOverrideNotification) {
this.weatherProvider.notificationReceived(payload);
}
},

View file

@ -1,4 +1,4 @@
/* global Class, performWebRequest */
/* global Class, performWebRequest, OverrideWrapper */
/* MagicMirror²
* Module: Weather
@ -164,5 +164,9 @@ WeatherProvider.initialize = function (providerIdentifier, delegate) {
provider.providerName = pi;
}
if (config.allowOverrideNotification) {
return new OverrideWrapper(provider);
}
return provider;
};

10738
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "magicmirror",
"version": "2.24.0",
"version": "2.25.0",
"description": "The open source modular smart mirror platform.",
"main": "js/electron.js",
"scripts": {
@ -25,7 +25,7 @@
"lint:prettier": "prettier . --write",
"lint:js": "eslint 'js/**/*.js' 'modules/default/**/*.js' 'clientonly/*.js' 'serveronly/*.js' 'translations/*.js' 'vendor/*.js' 'tests/**/*.js' 'config/*' --config .eslintrc.json --fix",
"lint:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json --fix",
"lint:staged": "pretty-quick --staged",
"lint:staged": "lint-staged",
"prepare": "[ -f node_modules/.bin/husky ] && husky install || echo no husky installed."
},
"repository": {
@ -49,52 +49,55 @@
},
"homepage": "https://magicmirror.builders",
"devDependencies": {
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.2",
"eslint-plugin-jsdoc": "^46.4.2",
"eslint-plugin-prettier": "^4.2.1",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jest": "^27.4.2",
"eslint-plugin-jsdoc": "^46.8.2",
"eslint-plugin-prettier": "^5.0.0",
"express-basic-auth": "^1.2.1",
"husky": "^8.0.3",
"jest": "^29.5.0",
"jest": "^29.7.0",
"jsdom": "^22.1.0",
"lint-staged": "^14.0.1",
"lodash": "^4.17.21",
"playwright": "^1.35.1",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3",
"sinon": "^15.2.0",
"stylelint": "^15.9.0",
"stylelint-config-standard": "^33.0.0",
"stylelint-prettier": "^3.0.0",
"playwright": "^1.38.1",
"prettier": "^3.0.3",
"sinon": "^16.0.0",
"stylelint": "^15.10.3",
"stylelint-config-standard": "^34.0.0",
"stylelint-prettier": "^4.0.2",
"suncalc": "^1.9.0"
},
"optionalDependencies": {
"electron": "^25.2.0"
"electron": "^26.2.4"
},
"dependencies": {
"colors": "^1.4.0",
"console-stamp": "^3.1.1",
"digest-fetch": "^2.0.3",
"console-stamp": "^3.1.2",
"envsub": "^4.1.0",
"eslint": "^8.43.0",
"eslint": "^8.50.0",
"express": "^4.18.2",
"express-ipfilter": "^1.3.1",
"feedme": "^2.0.2",
"helmet": "^7.0.0",
"html-to-text": "^9.0.5",
"iconv-lite": "^0.6.3",
"luxon": "^1.28.1",
"module-alias": "^2.2.3",
"moment": "^2.29.4",
"node-fetch": "^2.6.12",
"node-ical": "^0.16.1",
"socket.io": "^4.7.1"
"socket.io": "^4.7.2"
},
"lint-staged": {
"*": "prettier --write",
"*.js": "eslint",
"*.css": "stylelint"
},
"_moduleAliases": {
"node_helper": "js/node_helper.js",
"logger": "js/logger.js",
"fetch": "js/fetch.js"
"logger": "js/logger.js"
},
"engines": {
"node": ">=16"
"node": ">=18"
}
}

View file

@ -4,7 +4,8 @@
* MIT Licensed.
*/
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
ipWhitelist: []
ipWhitelist: [],
port: 8282
});
/*************** DO NOT EDIT THE LINE BELOW ***************/

View file

@ -11,7 +11,7 @@ let config = {
module: "calendar",
position: "bottom_bar",
config: {
customEvents: [{ keyword: "CustomEvent", symbol: "dice" }],
customEvents: [{ keyword: "CustomEvent", symbol: "dice", eventClass: "undo" }],
calendars: [
{
maximumEntries: 5,

View file

@ -0,0 +1,36 @@
/* MagicMirror² Test config for multiple calendar events having the same name and start date/time
*
* By Paranoid93 https://github.com/Paranoid93/
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
maximumEntries: 30,
hideDuplicates: false,
calendars: [
{
maximumEntries: 15,
maximumNumberOfDays: 10000,
url: "http://localhost:8080/tests/mocks/calendar_test.ics" // contains 11 events
},
{
maximumEntries: 15,
maximumNumberOfDays: 10000,
url: "http://localhost:8080/tests/mocks/calendar_test_clone.ics" // clone of upper calendar
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View file

@ -0,0 +1,24 @@
/* MagicMirror² Test config for default clock module
*
* By Johan Hammar
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "clock",
position: "middle_center",
config: {
showSunTimes: true,
showMoonTimes: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View file

@ -0,0 +1,28 @@
/* MagicMirror² Test config sample for AnimateCSS integration with compliments module
*
* By bugsounet https://github.com/bugsounet
* 09/2023
* MIT Licensed.
*/
let config = {
modules: [
{
module: "compliments",
position: "lower_third",
animateIn: "flipInX",
animateOut: "flipOutX",
config: {
compliments: {
anytime: ["AnimateCSS Testing..."]
},
updateInterval: 2000,
fadeSpeed: 1000
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View file

@ -0,0 +1,29 @@
/* MagicMirror² Test config sample for AnimateCSS integration with compliments module
* --> if animation name is not an AnimateCSS animation
* --> must fallback to default (no animation)
* By bugsounet https://github.com/bugsounet
* 09/2023
* MIT Licensed.
*/
let config = {
modules: [
{
module: "compliments",
position: "lower_third",
animateIn: "foo",
animateOut: "bar",
config: {
compliments: {
anytime: ["AnimateCSS Testing..."]
},
updateInterval: 2000,
fadeSpeed: 1000
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View file

@ -0,0 +1,28 @@
/* MagicMirror² Test config sample for AnimateCSS integration with compliments module
* --> inversed name animation : in for out and vice versa (must return no animation)
* By bugsounet https://github.com/bugsounet
* 09/2023
* MIT Licensed.
*/
let config = {
modules: [
{
module: "compliments",
position: "lower_third",
animateIn: "flipOutX",
animateOut: "flipInX",
config: {
compliments: {
anytime: ["AnimateCSS Testing..."]
},
updateInterval: 2000,
fadeSpeed: 1000
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View file

@ -4,7 +4,8 @@
* MIT Licensed.
*/
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
ipWhitelist: ["x.x.x.x"]
ipWhitelist: ["x.x.x.x"],
port: 8181
});
/*************** DO NOT EDIT THE LINE BELOW ***************/

View file

@ -0,0 +1,78 @@
/* AnimateCSS integration Test with compliments module
*
* By bugsounet https://github.com/bugsounet
* and helped by khassel
* 09/2023
* MIT Licensed.
*/
const helpers = require("./helpers/global-setup.js");
describe("AnimateCSS integration Test", () => {
// define config file for testing
let testConfigFile = "tests/configs/modules/compliments/compliments_animateCSS.js";
// define config file to fallback to default: wrong animation name (must return no animation)
let testConfigFileFallbackToDefault = "tests/configs/modules/compliments/compliments_animateCSS_fallbackToDefault.js";
// define config file with an inversed name animation : in for out and vice versa (must return no animation)
let testConfigFileInvertedAnimationName = "tests/configs/modules/compliments/compliments_animateCSS_invertedAnimationName.js";
// define config file with no animation defined
let testConfigByDefault = "tests/configs/modules/compliments/compliments_anytime.js";
/**
* move similar tests in function doTest
* @param {string} [animationIn] animation in name of AnimateCSS to test.
* @param {string} [animationOut] animation out name of AnimateCSS to test.
*/
const doTest = async (animationIn, animationOut) => {
await helpers.getDocument();
let elem = await helpers.waitForElement(`.compliments`);
expect(elem).not.toBe(null);
let styles = window.getComputedStyle(elem);
if (animationIn && animationIn !== "") {
expect(styles._values["animation-name"]).toBe(animationIn);
} else {
expect(styles._values["animation-name"]).toBe(undefined);
}
if (animationOut && animationOut !== "") {
elem = await helpers.waitForElement(`.compliments.animate__animated.animate__${animationOut}`);
expect(elem).not.toBe(null);
styles = window.getComputedStyle(elem);
expect(styles._values["animation-name"]).toBe(animationOut);
} else {
expect(styles._values["animation-name"]).toBe(undefined);
}
};
afterEach(async () => {
await helpers.stopApplication();
});
describe("animateIn and animateOut Test", () => {
it("with flipInX and flipOutX animation", async () => {
await helpers.startApplication(testConfigFile);
await doTest("flipInX", "flipOutX");
});
});
describe("use animateOut name for animateIn (vice versa) Test", () => {
it("without animation", async () => {
await helpers.startApplication(testConfigFileInvertedAnimationName);
await doTest();
});
});
describe("false Animation name test", () => {
it("without animation", async () => {
await helpers.startApplication(testConfigFileFallbackToDefault);
await doTest();
});
});
describe("no Animation defined test", () => {
it("without animation", async () => {
await helpers.startApplication(testConfigByDefault);
await doTest();
});
});
});

View file

@ -10,12 +10,12 @@ describe("App environment", () => {
});
it("get request from http://localhost:8080 should return 200", async () => {
const res = await helpers.fetch("http://localhost:8080");
const res = await fetch("http://localhost:8080");
expect(res.status).toBe(200);
});
it("get request from http://localhost:8080/nothing should return 404", async () => {
const res = await helpers.fetch("http://localhost:8080/nothing");
const res = await fetch("http://localhost:8080/nothing");
expect(res.status).toBe(404);
});

View file

@ -22,7 +22,7 @@ describe("All font files from roboto.css should be downloadable", () => {
test.each(fontFiles)("should return 200 HTTP code for file '%s'", async (fontFile) => {
const fontUrl = `http://localhost:8080/fonts/${fontFile}`;
const res = await helpers.fetch(fontUrl);
const res = await fetch(fontUrl);
expect(res.status).toBe(200);
});
});

View file

@ -1,5 +1,4 @@
const jsdom = require("jsdom");
const corefetch = require("fetch");
exports.startApplication = async (configFilename, exec) => {
jest.resetModules();
@ -31,7 +30,8 @@ exports.getDocument = () => {
const url = `http://${config.address || "localhost"}:${config.port || "8080"}`;
jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" }).then((dom) => {
dom.window.name = "jsdom";
dom.window.fetch = corefetch;
global.window = dom.window;
dom.window.fetch = fetch;
dom.window.onload = () => {
global.document = dom.window.document;
resolve();
@ -80,14 +80,6 @@ exports.waitForAllElements = (selector) => {
});
};
exports.fetch = (url) => {
return new Promise((resolve) => {
corefetch(url).then((res) => {
resolve(res);
});
});
};
exports.testMatch = async (element, regex) => {
const elem = await this.waitForElement(element);
expect(elem).not.toBe(null);

View file

@ -13,7 +13,6 @@ exports.getText = async (element, result) => {
};
exports.startApp = async (configFileName, additionalMockData) => {
injectMockData(configFileName, additionalMockData);
await helpers.startApplication("");
await helpers.startApplication(injectMockData(configFileName, additionalMockData));
await helpers.getDocument();
};

View file

@ -10,7 +10,7 @@ describe("ipWhitelist directive configuration", () => {
});
it("should return 403", async () => {
const res = await helpers.fetch("http://localhost:8080");
const res = await fetch("http://localhost:8181");
expect(res.status).toBe(403);
});
});
@ -24,7 +24,7 @@ describe("ipWhitelist directive configuration", () => {
});
it("should return 200", async () => {
const res = await helpers.fetch("http://localhost:8080");
const res = await fetch("http://localhost:8282");
expect(res.status).toBe(200);
});
});

View file

@ -60,6 +60,10 @@ describe("Calendar module", () => {
await testElementLength(".calendar .event .fa-dice", 1);
});
it("should show a customEvent calendar eventClass in one event", async () => {
await testElementLength(".calendar .event.undo", 1);
});
it("should show two custom icons for repeating events", async () => {
await testElementLength(".calendar .event .fa-undo", 2);
});
@ -80,6 +84,17 @@ describe("Calendar module", () => {
});
});
describe("Events from multiple calendars", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/show-duplicates-in-calendar.js");
await helpers.getDocument();
});
it("should show multiple events with the same title and start time from different calendars", async () => {
await testElementLength(".calendar .event", 22);
});
});
process.setMaxListeners(0);
for (let i = -12; i < 12; i++) {
describe("Recurring event per timezone", () => {

View file

@ -71,11 +71,28 @@ describe("Clock module", () => {
});
it("should not show the time when digital clock is shown", async () => {
const elem = await document.querySelector(".clock .digital .time");
const elem = document.querySelector(".clock .digital .time");
expect(elem).toBe(null);
});
});
describe("with showSun/MoonTime enabled", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/clock_showSunMoon.js");
await helpers.getDocument();
});
it("should show the sun times", async () => {
const elem = await helpers.waitForElement(".clock .digital .sun");
expect(elem).not.toBe(null);
});
it("should show the moon times", async () => {
const elem = await helpers.waitForElement(".clock .digital .moon");
expect(elem).not.toBe(null);
});
});
describe("with showWeek config enabled", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/clock_showWeek.js");

View file

@ -25,8 +25,8 @@ describe("Newsfeed module", () => {
it("should NOT show the newsfeed description", async () => {
await helpers.waitForElement(".newsfeed");
const element = document.querySelector(".newsfeed .newsfeed-desc");
expect(element).toBe(null);
const elem = document.querySelector(".newsfeed .newsfeed-desc");
expect(elem).toBe(null);
});
});

View file

@ -1,9 +1,11 @@
const helpers = require("../helpers/global-setup");
const weatherFunc = require("../helpers/weather-functions");
const { cleanupMockData } = require("../../utils/weather_mocker");
describe("Weather module", () => {
afterAll(async () => {
await helpers.stopApplication();
await cleanupMockData();
});
describe("Current weather", () => {
@ -48,7 +50,7 @@ describe("Weather module", () => {
it("should render windDirection with an arrow", async () => {
const elem = await helpers.waitForElement(".weather .normal.medium sup i.fa-long-arrow-alt-down");
expect(elem).not.toBe(null);
expect(elem.outerHTML).toContain("transform:rotate(250deg);");
expect(elem.outerHTML).toContain("transform:rotate(250deg)");
});
it("should render humidity", async () => {

View file

@ -1,9 +1,11 @@
const helpers = require("../helpers/global-setup");
const weatherFunc = require("../helpers/weather-functions");
const { cleanupMockData } = require("../../utils/weather_mocker");
describe("Weather module: Weather Forecast", () => {
afterAll(async () => {
await helpers.stopApplication();
await cleanupMockData();
});
describe("Default configuration", () => {

View file

@ -1,9 +1,11 @@
const helpers = require("../helpers/global-setup");
const weatherFunc = require("../helpers/weather-functions");
const { cleanupMockData } = require("../../utils/weather_mocker");
describe("Weather module: Weather Hourly Forecast", () => {
afterAll(async () => {
await helpers.stopApplication();
await cleanupMockData();
});
describe("Default configuration", () => {

View file

@ -10,7 +10,7 @@ describe("port directive configuration", () => {
});
it("should return 200", async () => {
const res = await helpers.fetch("http://localhost:8090");
const res = await fetch("http://localhost:8090");
expect(res.status).toBe(200);
});
});
@ -24,7 +24,7 @@ describe("port directive configuration", () => {
});
it("should return 200", async () => {
const res = await helpers.fetch("http://localhost:8100");
const res = await fetch("http://localhost:8100");
expect(res.status).toBe(200);
});
});

View file

@ -17,12 +17,12 @@ describe("App environment", () => {
});
it("get request from http://localhost:8080 should return 200", async () => {
const res = await helpers.fetch("http://localhost:8080");
const res = await fetch("http://localhost:8080");
expect(res.status).toBe(200);
});
it("get request from http://localhost:8080/nothing should return 404", async () => {
const res = await helpers.fetch("http://localhost:8080/nothing");
const res = await fetch("http://localhost:8080/nothing");
expect(res.status).toBe(404);
});
});

View file

@ -1,3 +1,4 @@
const fs = require("fs");
const helpers = require("./helpers/global-setup");
describe("templated config with port variable", () => {
@ -6,10 +7,15 @@ describe("templated config with port variable", () => {
});
afterAll(async () => {
await helpers.stopApplication();
try {
fs.unlinkSync("tests/configs/port_variable.js");
} catch (err) {
// do nothing
}
});
it("should return 200", async () => {
const res = await helpers.fetch("http://localhost:8090");
const res = await fetch("http://localhost:8090");
expect(res.status).toBe(200);
});
});

View file

@ -14,7 +14,7 @@ describe("Vendors", () => {
Object.keys(vendors).forEach((vendor) => {
it(`should return 200 HTTP code for vendor "${vendor}"`, async () => {
const urlVendor = `http://localhost:8080/vendor/${vendors[vendor]}`;
const res = await helpers.fetch(urlVendor);
const res = await fetch(urlVendor);
expect(res.status).toBe(200);
});
});
@ -22,7 +22,7 @@ describe("Vendors", () => {
Object.keys(vendors).forEach((vendor) => {
it(`should return 404 HTTP code for vendor https://localhost/"${vendor}"`, async () => {
const urlVendor = `http://localhost:8080/${vendors[vendor]}`;
const res = await helpers.fetch(urlVendor);
const res = await fetch(urlVendor);
expect(res.status).toBe(404);
});
});

View file

@ -13,7 +13,6 @@ exports.getText = async (element, result) => {
).toBe(result);
};
exports.startApp = async (configFileNameName, systemDate) => {
injectMockData(configFileNameName);
await helpers.startApplication("", systemDate);
exports.startApp = async (configFileName, systemDate) => {
await helpers.startApplication(injectMockData(configFileName), systemDate);
};

View file

@ -1,9 +1,11 @@
const helpers = require("../helpers/global-setup");
const weatherHelper = require("../helpers/weather-setup");
const { cleanupMockData } = require("../../utils/weather_mocker");
describe("Weather module", () => {
afterEach(async () => {
await helpers.stopApplication();
await cleanupMockData();
});
describe("Current weather with sunrise", () => {

View file

@ -0,0 +1,190 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:MagicMirrorTest
X-WR-TIMEZONE:America/Santiago
X-WR-CALDESC:Testing propose MagicMirror
BEGIN:VTIMEZONE
TZID:America/Santiago
X-LIC-LOCATION:America/Santiago
BEGIN:STANDARD
TZOFFSETFROM:-0300
TZOFFSETTO:-0400
TZNAME:-04
DTSTART:19700510T000000
RDATE:19700510T030000
RDATE:19710509T030000
RDATE:19720514T030000
RDATE:19730513T030000
RDATE:19740512T030000
RDATE:19750511T030000
RDATE:19760509T030000
RDATE:19770515T030000
RDATE:19780514T030000
RDATE:19790513T030000
RDATE:19800511T030000
RDATE:19810510T030000
RDATE:19820509T030000
RDATE:19830515T030000
RDATE:19840513T030000
RDATE:19850512T030000
RDATE:19860511T030000
RDATE:19870510T030000
RDATE:19880515T030000
RDATE:19890514T030000
RDATE:19900513T030000
RDATE:19910512T030000
RDATE:19920510T030000
RDATE:19930509T030000
RDATE:19940515T030000
RDATE:19950514T030000
RDATE:19960512T030000
RDATE:19970511T030000
RDATE:19980510T030000
RDATE:19990509T030000
RDATE:20000514T030000
RDATE:20010513T030000
RDATE:20020512T030000
RDATE:20030511T030000
RDATE:20040509T030000
RDATE:20050515T030000
RDATE:20060514T030000
RDATE:20070513T030000
RDATE:20080511T030000
RDATE:20090510T030000
RDATE:20100509T030000
RDATE:20110515T030000
RDATE:20120513T030000
RDATE:20130512T030000
RDATE:20140511T030000
RDATE:20150510T030000
RDATE:20160515T030000
RDATE:20170514T030000
RDATE:20180513T030000
RDATE:20190512T030000
RDATE:20200510T030000
RDATE:20210509T030000
RDATE:20220515T030000
RDATE:20230514T030000
RDATE:20240512T030000
RDATE:20250511T030000
RDATE:20260510T030000
RDATE:20270509T030000
RDATE:20280514T030000
RDATE:20290513T030000
RDATE:20300512T030000
RDATE:20310511T030000
RDATE:20320509T030000
RDATE:20330515T030000
RDATE:20340514T030000
RDATE:20350513T030000
RDATE:20360511T030000
RDATE:20370510T030000
END:STANDARD
BEGIN:STANDARD
TZOFFSETFROM:-0300
TZOFFSETTO:-0400
TZNAME:-04
DTSTART:20380509T000000
RRULE:FREQ=YEARLY;BYMONTH=5;BYDAY=2SU
END:STANDARD
BEGIN:DAYLIGHT
TZOFFSETFROM:-0400
TZOFFSETTO:-0300
TZNAME:-03
DTSTART:19700809T000000
RDATE:19700809T040000
RDATE:19710815T040000
RDATE:19720813T040000
RDATE:19730812T040000
RDATE:19740811T040000
RDATE:19750810T040000
RDATE:19760815T040000
RDATE:19770814T040000
RDATE:19780813T040000
RDATE:19790812T040000
RDATE:19800810T040000
RDATE:19810809T040000
RDATE:19820815T040000
RDATE:19830814T040000
RDATE:19840812T040000
RDATE:19850811T040000
RDATE:19860810T040000
RDATE:19870809T040000
RDATE:19880814T040000
RDATE:19890813T040000
RDATE:19900812T040000
RDATE:19910811T040000
RDATE:19920809T040000
RDATE:19930815T040000
RDATE:19940814T040000
RDATE:19950813T040000
RDATE:19960811T040000
RDATE:19970810T040000
RDATE:19980809T040000
RDATE:19990815T040000
RDATE:20000813T040000
RDATE:20010812T040000
RDATE:20020811T040000
RDATE:20030810T040000
RDATE:20040815T040000
RDATE:20050814T040000
RDATE:20060813T040000
RDATE:20070812T040000
RDATE:20080810T040000
RDATE:20090809T040000
RDATE:20100815T040000
RDATE:20110814T040000
RDATE:20120812T040000
RDATE:20130811T040000
RDATE:20140810T040000
RDATE:20150809T040000
RDATE:20160814T040000
RDATE:20170813T040000
RDATE:20180812T040000
RDATE:20190811T040000
RDATE:20200809T040000
RDATE:20210815T040000
RDATE:20220814T040000
RDATE:20230813T040000
RDATE:20240811T040000
RDATE:20250810T040000
RDATE:20260809T040000
RDATE:20270815T040000
RDATE:20280813T040000
RDATE:20290812T040000
RDATE:20300811T040000
RDATE:20310810T040000
RDATE:20320815T040000
RDATE:20330814T040000
RDATE:20340813T040000
RDATE:20350812T040000
RDATE:20360810T040000
RDATE:20370809T040000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZOFFSETFROM:-0400
TZOFFSETTO:-0300
TZNAME:-03
DTSTART:20380815T000000
RRULE:FREQ=YEARLY;BYMONTH=8;BYDAY=2SU
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=America/Santiago:20170309T100000
DTEND;TZID=America/Santiago:20170309T110000
RRULE:FREQ=MONTHLY;INTERVAL=30;BYMONTHDAY=9
DTSTAMP:20170310T172720Z
UID:80rl9kuu5bq49gme99eklov27k@google.com
CREATED:20170310T172400Z
DESCRIPTION:
LAST-MODIFIED:20170310T172400Z
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:TestEvent
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR

View file

@ -8,13 +8,9 @@ describe("server_functions tests", () => {
let corsResponse;
let request;
jest.mock("node-fetch");
let nodefetch = require("node-fetch");
let fetchMock;
beforeEach(() => {
nodefetch.mockReset();
fetchResponseHeadersGet = jest.fn(() => {});
fetchResponseHeadersText = jest.fn(() => {});
fetchResponse = {
@ -23,10 +19,11 @@ describe("server_functions tests", () => {
},
text: fetchResponseHeadersText
};
jest.mock("node-fetch", () => jest.fn());
nodefetch.mockImplementation(() => fetchResponse);
// eslint-disable-next-line
fetch = jest.fn();
fetch.mockImplementation(() => fetchResponse);
fetchMock = nodefetch;
fetchMock = fetch;
corsResponse = {
set: jest.fn(() => {}),

View file

@ -1,11 +1,8 @@
global.moment = require("moment-timezone");
const { performWebRequest, formatTime } = require("../../../../modules/default/utils");
const nodeVersion = process.version.match(/^v(\d+)\.*/)[1];
describe("Default modules utils tests", () => {
describe("performWebRequest", () => {
if (nodeVersion > 18) {
const locationHost = "localhost:8080";
const locationProtocol = "http";
@ -105,9 +102,6 @@ describe("Default modules utils tests", () => {
expect(response.headers[0].value).toBe("value1");
});
});
} else {
test("Always ok, need one test", () => {});
}
});
describe("formatTime", () => {

View file

@ -1,5 +1,7 @@
const fs = require("fs");
const path = require("path");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const _ = require("lodash");
/**
@ -35,9 +37,16 @@ const injectMockData = (configFileName, extendedData = {}) => {
} else {
mockWeather = readMockData("current", extendedData);
}
let content = fs.readFileSync(path.resolve(`${__dirname}../../../${configFileName}`)).toString();
let content = fs.readFileSync(configFileName).toString();
content = content.replace("#####WEATHERDATA#####", mockWeather);
fs.writeFileSync(path.resolve(`${__dirname}../../../config/config.js`), content);
const tempFile = configFileName.replace(".js", "_temp.js");
fs.writeFileSync(tempFile, content);
return tempFile;
};
module.exports = { injectMockData };
const cleanupMockData = async () => {
const tempDir = path.resolve(`${__dirname}/../configs`).toString();
await exec(`find ${tempDir} -type f -name *_temp.js -delete`);
};
module.exports = { injectMockData, cleanupMockData };

View file

@ -29,9 +29,16 @@
"FEELS": "Ressenti {DEGREE}",
"PRECIP_POP": "Probabilité de précipitations",
"PRECIP_AMOUNT": "Quantité des précipitations",
"MODULE_CONFIG_CHANGED": "Les options de configuration du module {MODULE_NAME} ont changé.\nVeuillez consulter la documentation.",
"MODULE_CONFIG_ERROR": "Erreur dans le module {MODULE_NAME}. {ERROR}",
"MODULE_ERROR_MALFORMED_URL": "URL mal formée.",
"MODULE_ERROR_NO_CONNECTION": "Pas de connexion Internet.",
"MODULE_ERROR_UNAUTHORIZED": "L'autorisation à échouée.",
"MODULE_ERROR_UNSPECIFIED": "Consultez les journaux pour plus de détails.",
"NEWSFEED_NO_ITEMS": "Aucune nouvelle pour le moment.",
"UPDATE_NOTIFICATION": "Une mise à jour de MagicMirror² est disponible",
"UPDATE_NOTIFICATION_MODULE": "Une mise à jour est disponible pour le module {MODULE_NAME}.",

25
vendor/package-lock.json generated vendored
View file

@ -7,7 +7,8 @@
"name": "magicmirror-vendors",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.0",
"@fortawesome/fontawesome-free": "^6.4.2",
"animate.css": "^4.1.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"nunjucks": "^3.2.4",
@ -16,9 +17,9 @@
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz",
"integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz",
"integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
@ -29,6 +30,11 @@
"resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
"integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA=="
},
"node_modules/animate.css": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ=="
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
@ -98,15 +104,20 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz",
"integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ=="
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz",
"integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg=="
},
"a-sync-waterfall": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
"integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA=="
},
"animate.css": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ=="
},
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",

3
vendor/package.json vendored
View file

@ -10,7 +10,8 @@
"url": "https://github.com/MichMich/MagicMirror/issues"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.0",
"@fortawesome/fontawesome-free": "^6.4.2",
"animate.css": "^4.1.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"nunjucks": "^3.2.4",