mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-09-16 09:51:03 +00:00
Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
343e7de7bd | ||
|
e87f50e64a | ||
|
abe5c08a52 | ||
|
f14e956166 | ||
|
2eaf9dfeeb |
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
|
||||
"plugins": ["prettier", "jsdoc", "jest"],
|
||||
"plugins": ["prettier", "import", "jsdoc", "jest"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"es2022": true,
|
||||
"jest/globals": true,
|
||||
"node": true
|
||||
},
|
||||
@@ -16,16 +16,19 @@
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2020,
|
||||
"ecmaVersion": 2022,
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"eqeqeq": "error",
|
||||
"import/order": "error",
|
||||
"no-param-reassign": "error",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-throw-literal": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "error"
|
||||
"no-useless-return": "error",
|
||||
"prefer-template": "error"
|
||||
}
|
||||
}
|
||||
|
56
.gitattributes
vendored
Normal file
56
.gitattributes
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
# .gitattributes snippet to force users to use same line endings for project.
|
||||
#
|
||||
# Handle line endings automatically for files detected as text
|
||||
# and leave all files detected as binary untouched.
|
||||
* text=auto
|
||||
|
||||
#
|
||||
# The above will handle all files NOT found below
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
# https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes
|
||||
|
||||
|
||||
|
||||
# These files are text and should be normalized (Convert crlf => lf)
|
||||
*.php text
|
||||
*.css text
|
||||
*.scss text
|
||||
*.js text
|
||||
*.json text
|
||||
*.htm text
|
||||
*.html text
|
||||
*.xml text
|
||||
*.txt text
|
||||
*.ini text
|
||||
*.inc text
|
||||
*.pl text
|
||||
*.rb text
|
||||
*.py text
|
||||
*.scm text
|
||||
*.sql text
|
||||
.htaccess text
|
||||
*.sh text
|
||||
Dockerfile* text
|
||||
*.yml text
|
||||
*.yaml text
|
||||
*.md text
|
||||
*.markdown text
|
||||
|
||||
# These files are binary and should be left untouched
|
||||
# (binary is a macro for -text -diff)
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.mov binary
|
||||
*.mp4 binary
|
||||
*.mp3 binary
|
||||
*.flv binary
|
||||
*.fla binary
|
||||
*.swf binary
|
||||
*.gz binary
|
||||
*.zip binary
|
||||
*.7z binary
|
||||
*.ttf binary
|
||||
*.pyc binary
|
9
.github/CONTRIBUTING.md
vendored
9
.github/CONTRIBUTING.md
vendored
@@ -6,7 +6,7 @@ We hold our code to standard, and these standards are documented below.
|
||||
|
||||
## Linters
|
||||
|
||||
If you wish to run our linters, use `npm run lint` without any arguments.
|
||||
We use prettier for automatic linting of all our files: `npm run lint:prettier`.
|
||||
|
||||
### JavaScript: Run ESLint
|
||||
|
||||
@@ -18,7 +18,7 @@ To run ESLint, use `npm run lint:js`.
|
||||
|
||||
### CSS: Run StyleLint
|
||||
|
||||
We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file.
|
||||
We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our `.stylelintrc` file.
|
||||
|
||||
To run StyleLint, use `npm run lint:css`.
|
||||
|
||||
@@ -28,7 +28,8 @@ We use [Jest](https://jestjs.io) for JavaScript testing.
|
||||
|
||||
To run all tests, use `npm run test`.
|
||||
|
||||
The specific test commands are defined in `package.json`. So you can also run the specific tests with other commands, e.g. `npm run test:unit` or `npx jest tests/e2e/env_spec.js`.
|
||||
The specific test commands are defined in `package.json`.
|
||||
So you can also run the specific tests with other commands, e.g. `npm run test:unit` or `npx jest tests/e2e/env_spec.js`.
|
||||
|
||||
## Submitting Issues
|
||||
|
||||
@@ -42,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 14 or later (recommended is 16).
|
||||
**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.
|
||||
|
||||
|
@@ -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 14 or later (recommended is 16).
|
||||
**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.
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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.
|
||||
|
||||
|
4
.github/codecov.yaml
vendored
4
.github/codecov.yaml
vendored
@@ -4,3 +4,7 @@ coverage:
|
||||
default:
|
||||
# advanced settings
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
threshold: 0%
|
||||
target: 0
|
||||
|
19
.github/dependabot.yaml
vendored
19
.github/dependabot.yaml
vendored
@@ -4,3 +4,22 @@ updates:
|
||||
directory: "/"
|
||||
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"
|
||||
|
14
.github/workflows/automated-tests.yaml
vendored
14
.github/workflows/automated-tests.yaml
vendored
@@ -18,20 +18,22 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x, 16.x, 18.x]
|
||||
node-version: [18.x, 20.x]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Use Node.js ${{ matrix.node-version }}"
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "npm"
|
||||
- name: Install dependencies and run tests
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
npm run install-mm:dev
|
||||
- name: "Run tests"
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm run install-mm:dev
|
||||
touch css/custom.css
|
||||
npm run test:prettier
|
||||
npm run test:js
|
||||
|
12
.github/workflows/codecov-test-suites.yaml
vendored
12
.github/workflows/codecov-test-suites.yaml
vendored
@@ -17,16 +17,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Install dependencies and run coverage
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
npm ci
|
||||
- name: "Run coverage"
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm ci
|
||||
touch css/custom.css
|
||||
npm run test:coverage
|
||||
- name: Upload coverage results to codecov
|
||||
- name: "Upload coverage results to codecov"
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
|
12
.github/workflows/depsreview.yaml
vendored
12
.github/workflows/depsreview.yaml
vendored
@@ -1,4 +1,8 @@
|
||||
name: "Dependency Review"
|
||||
# This workflow scans your pull requests for dependency changes, and will raise an error if any vulnerabilities or invalid licenses are being introduced.
|
||||
# For more information see: https://github.com/actions/dependency-review-action
|
||||
|
||||
name: "Review Dependencies"
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
@@ -8,7 +12,7 @@ jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v3
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v2
|
||||
uses: actions/dependency-review-action@v3
|
||||
|
19
.github/workflows/enforce-changelog.yaml
vendored
19
.github/workflows/enforce-changelog.yaml
vendored
@@ -1,19 +0,0 @@
|
||||
# This workflow enforces the update of a changelog file on every pull request
|
||||
# For more information see: https://github.com/dangoslen/changelog-enforcer
|
||||
|
||||
name: "Enforce Changelog"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Enforce changelog️
|
||||
uses: dangoslen/changelog-enforcer@v3
|
||||
with:
|
||||
changeLogPath: "CHANGELOG.md"
|
||||
skipLabels: "Skip Changelog"
|
28
.github/workflows/enforce-pullrequest-rules.yaml
vendored
Normal file
28
.github/workflows/enforce-pullrequest-rules.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# This workflow enforces on every pull request:
|
||||
# - the update of our CHANGELOG.md file, see: https://github.com/dangoslen/changelog-enforcer
|
||||
# - that the PR is not based against master, taken from https://github.com/oppia/oppia-android/pull/2832/files
|
||||
|
||||
name: "Enforce Pull-Request Rules"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: "Enforce changelog"
|
||||
uses: dangoslen/changelog-enforcer@v3
|
||||
with:
|
||||
changeLogPath: "CHANGELOG.md"
|
||||
skipLabels: "Skip Changelog"
|
||||
- name: "Enforce develop branch"
|
||||
if: ${{ github.base_ref == 'master' && !contains(github.event.pull_request.labels.*.name, 'mastermerge') }}
|
||||
run: |
|
||||
echo "This PR is based against the master branch and not a release or hotfix."
|
||||
echo "Please don't do this. Switch the branch to 'develop'."
|
||||
exit 1
|
||||
env:
|
||||
BASE_BRANCH: ${{ github.base_ref }}
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": ["stylelint-prettier/recommended"],
|
||||
"extends": ["stylelint-config-standard"],
|
||||
"plugins": ["stylelint-prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": true
|
||||
|
191
CHANGELOG.md
191
CHANGELOG.md
@@ -5,6 +5,164 @@ 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
|
||||
|
||||
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!
|
||||
|
||||
### Added
|
||||
|
||||
- Added UV Index to hourly and current Weather, with support for Openmeteo
|
||||
- Added tests for serveronly
|
||||
- Set Timezone `Europe/Berlin` in unit tests (needed for new formatTime tests)
|
||||
- Added no-param-reassign eslint rule and fix warnings
|
||||
- updatenotification: Added `sendUpdatesNotifications` feature. Broadcast update with `UPDATES` notification to other modules
|
||||
- updatenotification: allow force scanning with `SCAN_UPDATES` notification from other modules
|
||||
- Added per-calendar fetchInterval
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896, second attempt ...)
|
||||
|
||||
### Updated
|
||||
|
||||
- Added support for precipitation probability with openmeteo weather-provider
|
||||
- Update electron to v25.2 and other dependencies
|
||||
- Use node v20 in github workflow (replacing v14)
|
||||
- Refactor formatTime into common util function for default modules
|
||||
- Refactor some calendar methods into own class and added tests for them
|
||||
- Split install and run commands in github actions
|
||||
- Changed `fetchInterval` of calendar in `config.js.sample` to 7 days so we not to request example calendar too frequently
|
||||
- Changed default calendar fetchInterval to one hour
|
||||
- Changed calendar url in sample config
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix envcanada hourly forecast time (#3080)
|
||||
- Fix electron not running under windows after async changes (#3083)
|
||||
- Fix style issues after eslint-plugin-jsdoc update
|
||||
- Fix don't filter out ongoing full day events (#3095)
|
||||
- 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
|
||||
|
||||
Thanks to: @angeldeejay, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @grenagit, @Hirschberger, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt.
|
||||
|
||||
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!
|
||||
|
||||
### Added
|
||||
|
||||
- Added increments for hourly forecasts in weather module (#2996)
|
||||
- Added tests for hourly weather forecast
|
||||
- Added possibility to ignore MagicMirror repo in updatenotification module
|
||||
- Added Pirate Weather as new weather-provider (#3005)
|
||||
- Added possibility to use your own templates in Alert module
|
||||
- Added error message if `<modulename>.js` file is missing in module folder to get a hint in the logs (#2403)
|
||||
- Added possibility to use environment variables in `config.js` (#1756)
|
||||
- Added option `pastDaysCount` to default calendar module to control of how many days past events should be displayed
|
||||
- Added thai language to alert module
|
||||
- Added option `sendNotifications` in clock module (#3056)
|
||||
- Added tests for some weather utils
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed darksky weather-provider
|
||||
- Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896)
|
||||
|
||||
### Updated
|
||||
|
||||
- Use develop as target branch for dependabot
|
||||
- Update issue template, contributing doc and sample config
|
||||
- The weather modules clearly separates precipitation amount and probability (risk of rain/snow)
|
||||
- This requires all providers that only supports probability to change the config from `showPrecipitationAmount` to `showPrecipitationProbability`.
|
||||
- Update tests for weather and calendar module
|
||||
- Changed updatenotification module for MagicMirror repo only: Send only notifications for `master` if there is a tag on a newer commit
|
||||
- Update dates in Calendar widgets every minute
|
||||
- Cleanup jest coverage for patches
|
||||
- Update `stylelint` dependencies, switch to `stylelint-config-standard` and handle `stylelint` issues, update `main.css` matching new rules
|
||||
- Update Eslint config, add new rule and handle issue
|
||||
- Convert lots of callbacks to async/await
|
||||
- Revise require imports (#3071 and #3072)
|
||||
- Use `config.js-old` instead of file with timestamp suffix when backing up config with a `config.template` in use (#3104)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix wrong day labels in envcanada forecast (#2987)
|
||||
- Fix for missing default class name prefix for customEvents in calendar
|
||||
- Fix electron flashing white screen on startup (#1919)
|
||||
- Fix weathergov provider hourly forecast (#3008)
|
||||
- Fix message display with HTML code into alert module (#2828)
|
||||
- Fix typo in french translation
|
||||
- Yr wind direction is no longer inverted
|
||||
- Fix async node_helper stopping electron start (#2487)
|
||||
- The wind direction arrow now points in the direction the wind is flowing, not into the wind (#3019)
|
||||
- Fix precipitation css styles and rounding value
|
||||
- Fix wrong vertical alignment of calendar title column when wrapEvents is true (#3053)
|
||||
- Fix empty news feed stopping the reload forever
|
||||
- Fix e2e tests (failed after async changes) by running calendar and newsfeed tests last
|
||||
- Lint: Use template literals instead of string concatenation
|
||||
- Fix default alert module to render HTML for title and message
|
||||
- Fix Open-Meteo wind speed units
|
||||
|
||||
## [2.22.0] - 2023-01-01
|
||||
|
||||
Thanks to: @angeldeejay, @buxxi, @dariom, @dWoolridge, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @retroflex, @SkySails and @Tom.
|
||||
@@ -13,19 +171,22 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
|
||||
|
||||
### Added
|
||||
|
||||
- Added new calendar options for colored entries and improved styling (#3033)
|
||||
- Added test for remoteFile option in compliments module
|
||||
- Added hourlyWeather functionality to Weather.gov weather provider
|
||||
- Removed weatherEndpoint definition from weathergov.js (not used)
|
||||
- Added hourlyWeather functionality to Weather.gov weather-provider
|
||||
- Added css class names "today" and "tomorrow" for default calendar
|
||||
- Added Collaboration.md
|
||||
- Added new github action for dependency review (#2862)
|
||||
- Added a WeatherProvider for Open-Meteo
|
||||
- Added Yr as a weather provider
|
||||
- Added Yr as a weather-provider
|
||||
- Added config options "ignoreXOriginHeader" and "ignoreContentSecurityPolicy"
|
||||
- Added thai language
|
||||
- Added workflow rule to make sure PRs are based against develop
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed usage of internal fetch function of node until it is more stable
|
||||
- Removed weatherEndpoint definition from weathergov.js (not used)
|
||||
|
||||
### Updated
|
||||
|
||||
@@ -35,18 +196,18 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
|
||||
- Updated da translation
|
||||
- Rework weather module
|
||||
- Make sure smhi provider api only gets a maximum of 6 digits coordinates (#2955)
|
||||
- Use fetch instead of XMLHttpRequest in weatherprovider (#2935)
|
||||
- Reworked how weatherproviders handle units (#2849)
|
||||
- Use fetch instead of XMLHttpRequest in weather-provider (#2935)
|
||||
- Reworked how weather-providers handle units (#2849)
|
||||
- Use unix() method for parsing times, fix suntimes on the way (#2950)
|
||||
- Refactor conversion functions into utils class (#2958)
|
||||
- The `cors`-method in `server.js` now supports sending and recieving HTTP headers
|
||||
- The `cors`-method in `server.js` now supports sending and receiving HTTP headers
|
||||
- Replace `…` by `…`
|
||||
- Cleanup compliments module
|
||||
- Updated dependencies including electron to v22 (#2903)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Correctly show apparent temperature in SMHI weather provider
|
||||
- Correctly show apparent temperature in SMHI weather-provider
|
||||
- Ensure updatenotification module isn't shown when local is _ahead_ of remote
|
||||
- Handle node_helper errors during startup (#2944)
|
||||
- Possibility to change FontAwesome class in calendar, so icons like `fab fa-facebook-square` works.
|
||||
@@ -65,7 +226,7 @@ Special thanks to: @BKeyport, @buxxi, @davide125, @khassel, @kolbyjack, @krukle,
|
||||
- Added possibility to fetch calendars through socket notifications.
|
||||
- New scripts `install-mm` (and `install-mm:dev`) for simplifying mm installation (now: `npm run install-mm`) and adding params `--no-audit --no-fund --no-update-notifier` for less noise.
|
||||
- New `showTimeToday` option in calendar module shows time for current-day events even if `timeFormat` is `"relative"`.
|
||||
- Added hourly forecasts, apparent temperature & custom location name to SMHI weather provider.
|
||||
- Added hourly forecasts, apparent temperature & custom location name to SMHI weather-provider.
|
||||
- Added new electron tests for calendar and moved some compliments tests from `e2e` to `electron` because of date mocking, removed mock stuff from compliments module.
|
||||
|
||||
### Removed
|
||||
@@ -121,7 +282,7 @@ Special thanks to the following contributors: @10bias, @CFenner, @JHWelch, @k1rd
|
||||
- Added test for new weather forecast `absoluteDates` property.
|
||||
- The modules get a class hidden added/removed if they get hidden/shown which will also toggle pointer-events.
|
||||
- Added new config option `showTitleAsUrl` to newsfeed module. If set, the displayed title is a link to the article which is useful when running in a browser and you want to read this article.
|
||||
- Added internal cors proxy to get weather providers working without public proxies (fixes #2714). The new url `http(s)://address:port/cors?url=https://whatever-to-proxy` can be used in other modules too.
|
||||
- Added internal cors proxy to get weather-providers working without public proxies (fixes #2714). The new url `http(s)://address:port/cors?url=https://whatever-to-proxy` can be used in other modules too.
|
||||
- Added a WeatherProvider for Weatherflow.
|
||||
- Added new env var `ELECTRON_DISABLE_GPU` which disable gpu under electron if set (fixes #2831).
|
||||
- Added missing Czech translations.
|
||||
@@ -215,7 +376,7 @@ Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khas
|
||||
- Updated jsdocs and print warnings during testing too.
|
||||
- Updated weathergov provider to try fetching not just current, but also foreacst, when API URLs available.
|
||||
- Refactored clock layout.
|
||||
- Refactored methods from weatherproviders into weatherobject (isDaytime, updateSunTime).
|
||||
- Refactored methods from weather-providers into weatherobject (isDaytime, updateSunTime).
|
||||
- Use of `logger.js` in jest tests.
|
||||
- Run prettier over all relevant files.
|
||||
- Move tests needing electron in new category `electron`, use `server only` mode in `e2e` tests.
|
||||
@@ -313,7 +474,7 @@ Special thanks to the following contributors: @EdgardosReis, @MystaraTheGreat, @
|
||||
- Code cleanup for FEELS like and added {DEGREE} placeholder for FEELSLIKE for each language.
|
||||
- Converted newsfeed module to use templates.
|
||||
- Updated documentation and help screen about invalid config files.
|
||||
- Moving weather provider specific code and configuration into each provider and making hourly part of the interface.
|
||||
- Moving weather-provider specific code and configuration into each provider and making hourly part of the interface.
|
||||
- Bump electron to v11 and enable contextIsolation.
|
||||
- Don't update the DOM when a module is not displayed.
|
||||
- Cleaned up jsdoc and tests.
|
||||
@@ -395,7 +556,7 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
|
||||
- Rename Greek translation to correct ISO 639-1 alpha-2 code (gr > el). (#2155)
|
||||
- Add a space after icons of sunrise and sunset. (#2169)
|
||||
- Fix calendar when no DTEND record found in event, startDate overlay when endDate set. (#2177)
|
||||
- Fix windspeed conversion error in ukmetoffice weather provider. (#2189)
|
||||
- Fix windspeed conversion error in ukmetoffice weather-provider. (#2189)
|
||||
- Fix console.debug not having timestamps. (#2199)
|
||||
- Fix calendar full day event east of UTC start time. (#2200)
|
||||
- Fix non-fullday recurring rule processing. (#2216)
|
||||
@@ -624,7 +785,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- Use Feels Like temp from feed if present
|
||||
- Optionally display probability of precipitation (PoP) in current weather (UK Met Office data)
|
||||
- Automatically try to fix eslint errors by passing `--fix` option to it
|
||||
- Added sunrise and sunset times to weathergov weather provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705)
|
||||
- Added sunrise and sunset times to weathergov weather-provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705)
|
||||
- Added "useLocationAsHeader" to display "location" in `config.js` as header when location name is not returned
|
||||
- Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc
|
||||
|
||||
@@ -635,7 +796,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- Updated `ical.js` to solve various calendar issues.
|
||||
- Updated weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
|
||||
- Only update clock once per minute when seconds aren't shown
|
||||
- Updated weatherprovider documentation.
|
||||
- Updated weather-provider documentation.
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -717,7 +878,7 @@ Fixed `package.json` version number.
|
||||
- Added fade, fadePoint and maxNumberOfDays properties to the forecast mode [#1516](https://github.com/MichMich/MagicMirror/issues/1516)
|
||||
- Fixed Loading string and decimalSymbol string replace [#1538](https://github.com/MichMich/MagicMirror/issues/1538)
|
||||
- Show Snow amounts in new weather module [#1545](https://github.com/MichMich/MagicMirror/issues/1545)
|
||||
- Added weather.gov as a new weather provider for US locations
|
||||
- Added weather.gov as a new weather-provider for US locations
|
||||
|
||||
## [2.6.0] - 2019-01-01
|
||||
|
||||
|
@@ -5,8 +5,14 @@ This document describes how collaborators of this repository should work togethe
|
||||
- never merge your own PR's
|
||||
- never merge without someone having approved (approving and merging from same person is allowed)
|
||||
- wait for all approvals requested (or the author decides something different in the comments)
|
||||
- never merge to `master`, except for releases (because of update notification)
|
||||
- merges to master should be tagged with the "mastermerge" label so that the test runs through
|
||||
|
||||
## Issues
|
||||
|
||||
- "real" Issues are closed if the problem is solved and the fix is released
|
||||
- unrelated Issues (e.g. related to a foreign module) are closed immediately with a comment to open an issue in the module repository or to discuss this further in the forum or discord
|
||||
|
||||
## Releases
|
||||
|
||||
- are done by @MichMich only
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<a href="https://choosealicense.com/licenses/mit">
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/workflow/status/michmich/magicmirror/Run%20Automated%20Tests" alt="GitHub Actions">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/michmich/magicmirror/automated-tests.yaml" alt="GitHub Actions">
|
||||
<img src="https://img.shields.io/github/checks-status/michmich/magicmirror/master" alt="Build Status">
|
||||
<a href="https://codecov.io/gh/MichMich/MagicMirror">
|
||||
<img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/>
|
||||
|
@@ -11,7 +11,6 @@
|
||||
/**
|
||||
* Get command line parameters
|
||||
* Assumes that a cmdline parameter is defined with `--key [value]`
|
||||
*
|
||||
* @param {string} key key to look for at the command line
|
||||
* @param {string} defaultValue value if no key is given at the command line
|
||||
* @returns {string} the value of the parameter
|
||||
@@ -33,7 +32,6 @@
|
||||
|
||||
/**
|
||||
* Gets the config from the specified server url
|
||||
*
|
||||
* @param {string} url location where the server is running.
|
||||
* @returns {Promise} the config
|
||||
*/
|
||||
@@ -63,7 +61,6 @@
|
||||
|
||||
/**
|
||||
* Print a message to the console in case of errors
|
||||
*
|
||||
* @param {string} message error message to print
|
||||
* @param {number} code error code for the exit call
|
||||
*/
|
||||
@@ -87,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;
|
||||
|
@@ -6,17 +6,21 @@
|
||||
* For more information on how you can configure this file
|
||||
* see https://docs.magicmirror.builders/configuration/introduction.html
|
||||
* and https://docs.magicmirror.builders/modules/configuration.html
|
||||
*
|
||||
* You can use environment variables using a `config.js.template` file instead of `config.js`
|
||||
* which will be converted to `config.js` while starting. For more information
|
||||
* see https://docs.magicmirror.builders/configuration/introduction.html#enviromnent-variables
|
||||
*/
|
||||
let config = {
|
||||
address: "localhost", // Address to listen on, can be:
|
||||
address: "localhost", // Address to listen on, can be:
|
||||
// - "localhost", "127.0.0.1", "::1" to listen on loopback interface
|
||||
// - another specific IPv4/6 to listen on a specific interface
|
||||
// - "0.0.0.0", "::" to listen on any interface
|
||||
// Default, when address config is left out or empty, is "localhost"
|
||||
port: 8080,
|
||||
basePath: "/", // The URL path where MagicMirror² is hosted. If you are using a Reverse proxy
|
||||
// you must set the sub path here. basePath must end with a /
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses
|
||||
basePath: "/", // The URL path where MagicMirror² is hosted. If you are using a Reverse proxy
|
||||
// you must set the sub path here. basePath must end with a /
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses
|
||||
// or add a specific IPv4 of 192.168.1.5 :
|
||||
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"],
|
||||
// or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format :
|
||||
@@ -31,11 +35,6 @@ let config = {
|
||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
// serverOnly: true/false/"local" ,
|
||||
// local for armv6l processors, default
|
||||
// starts serveronly and then starts chrome browser
|
||||
// false, default for all NON-armv6l devices
|
||||
// true, force serveronly mode, because you want to.. no UI on this device
|
||||
|
||||
modules: [
|
||||
{
|
||||
@@ -56,8 +55,9 @@ let config = {
|
||||
config: {
|
||||
calendars: [
|
||||
{
|
||||
fetchInterval: 7 * 24 * 60 * 60 * 1000,
|
||||
symbol: "calendar-check",
|
||||
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics"
|
||||
url: "https://ics.calendarlabs.com/76/mm3137/US_Holidays.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -3,22 +3,18 @@
|
||||
--color-text-dimmed: #666;
|
||||
--color-text-bright: #fff;
|
||||
--color-background: #000;
|
||||
|
||||
--font-primary: "Roboto Condensed";
|
||||
--font-secondary: "Roboto";
|
||||
|
||||
--font-size: 20px;
|
||||
--font-size-xsmall: 0.75rem;
|
||||
--font-size-small: 1rem;
|
||||
--font-size-medium: 1.5rem;
|
||||
--font-size-large: 3.25rem;
|
||||
--font-size-xlarge: 3.75rem;
|
||||
|
||||
--gap-body-top: 60px;
|
||||
--gap-body-right: 60px;
|
||||
--gap-body-bottom: 60px;
|
||||
--gap-body-left: 60px;
|
||||
|
||||
--gap-modules: 30px;
|
||||
}
|
||||
|
||||
@@ -175,10 +171,7 @@ sup {
|
||||
|
||||
.region.fullscreen {
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--gap-body-top));
|
||||
left: calc(-1 * var(--gap-body-left));
|
||||
right: calc(-1 * var(--gap-body-right));
|
||||
bottom: calc(-1 * var(--gap-body-bottom));
|
||||
inset: calc(-1 * var(--gap-body-top)) calc(-1 * var(--gap-body-right)) calc(-1 * var(--gap-body-bottom)) calc(-1 * var(--gap-body-left));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
28
fonts/package-lock.json
generated
28
fonts/package-lock.json
generated
@@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
662
fonts/roboto.css
662
fonts/roboto.css
@@ -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;
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -2,30 +2,31 @@ module.exports = async () => {
|
||||
return {
|
||||
verbose: true,
|
||||
testTimeout: 20000,
|
||||
testSequencer: "<rootDir>/tests/configs/test_sequencer.js",
|
||||
testSequencer: "<rootDir>/tests/utils/test_sequencer.js",
|
||||
projects: [
|
||||
{
|
||||
displayName: "unit",
|
||||
globalSetup: "<rootDir>/tests/unit/helpers/global-setup.js",
|
||||
moduleNameMapper: {
|
||||
logger: "<rootDir>/js/logger.js"
|
||||
},
|
||||
testMatch: ["**/tests/unit/**/*.[jt]s?(x)"],
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks"]
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks", "<rootDir>/tests/unit/helpers"]
|
||||
},
|
||||
{
|
||||
displayName: "electron",
|
||||
testMatch: ["**/tests/electron/**/*.[jt]s?(x)"],
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers/"]
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers"]
|
||||
},
|
||||
{
|
||||
displayName: "e2e",
|
||||
setupFilesAfterEnv: ["<rootDir>/tests/e2e/helpers/mock-console.js"],
|
||||
testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"],
|
||||
modulePaths: ["<rootDir>/js/"],
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers/", "<rootDir>/tests/e2e/mocks"]
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers", "<rootDir>/tests/e2e/mocks"]
|
||||
}
|
||||
],
|
||||
collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/**/*.js", "./serveronly/**/*.js"],
|
||||
collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/default/**/*.js", "./serveronly/**/*.js"],
|
||||
coverageReporters: ["lcov", "text"],
|
||||
coverageProvider: "v8"
|
||||
};
|
||||
|
164
js/animateCSS.js
Normal file
164
js/animateCSS.js
Normal 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");
|
||||
}
|
235
js/app.js
235
js/app.js
@@ -10,6 +10,7 @@ require("module-alias/register");
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const envsub = require("envsub");
|
||||
const Log = require("logger");
|
||||
const Server = require(`${__dirname}/server`);
|
||||
const Utils = require(`${__dirname}/utils`);
|
||||
@@ -17,7 +18,7 @@ const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`)
|
||||
|
||||
// Get version number.
|
||||
global.version = require(`${__dirname}/../package.json`).version;
|
||||
Log.log("Starting MagicMirror: v" + global.version);
|
||||
Log.log(`Starting MagicMirror: v${global.version}`);
|
||||
|
||||
// global absolute root path
|
||||
global.root_path = path.resolve(`${__dirname}/../`);
|
||||
@@ -43,7 +44,6 @@ process.on("uncaughtException", function (err) {
|
||||
|
||||
/**
|
||||
* The core app.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
function App() {
|
||||
@@ -51,25 +51,72 @@ function App() {
|
||||
let httpServer;
|
||||
|
||||
/**
|
||||
* Loads the config file. Combines it with the defaults, and runs the
|
||||
* callback with the found config as argument.
|
||||
*
|
||||
* @param {Function} callback Function to be called after loading the config
|
||||
* Loads the config file. Combines it with the defaults and returns the config
|
||||
* @async
|
||||
* @returns {Promise<object>} the loaded config or the defaults if something goes wrong
|
||||
*/
|
||||
function loadConfig(callback) {
|
||||
async function loadConfig() {
|
||||
Log.log("Loading config ...");
|
||||
const defaults = require(`${__dirname}/defaults`);
|
||||
|
||||
// For this check proposed to TestSuite
|
||||
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
|
||||
const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`);
|
||||
let templateFile = `${configFilename}.template`;
|
||||
|
||||
// check if templateFile exists
|
||||
try {
|
||||
fs.accessSync(templateFile, fs.F_OK);
|
||||
} catch (err) {
|
||||
templateFile = null;
|
||||
Log.debug("config template file not exists, no envsubst");
|
||||
}
|
||||
|
||||
if (templateFile) {
|
||||
// save current config.js
|
||||
try {
|
||||
if (fs.existsSync(configFilename)) {
|
||||
fs.copyFileSync(configFilename, `${configFilename}-old`);
|
||||
}
|
||||
} catch (err) {
|
||||
Log.warn(`Could not copy ${configFilename}: ${err.message}`);
|
||||
}
|
||||
|
||||
// check if config.env exists
|
||||
const envFiles = [];
|
||||
const configEnvFile = `${configFilename.substr(0, configFilename.lastIndexOf("."))}.env`;
|
||||
try {
|
||||
if (fs.existsSync(configEnvFile)) {
|
||||
envFiles.push(configEnvFile);
|
||||
}
|
||||
} catch (err) {
|
||||
Log.debug(`${configEnvFile} does not exist. ${err.message}`);
|
||||
}
|
||||
|
||||
let options = {
|
||||
all: true,
|
||||
diff: false,
|
||||
envFiles: envFiles,
|
||||
protect: false,
|
||||
syntax: "default",
|
||||
system: true
|
||||
};
|
||||
|
||||
// envsubst variables in templateFile and create new config.js
|
||||
// naming for envsub must be templateFile and outputFile
|
||||
const outputFile = configFilename;
|
||||
try {
|
||||
await envsub({ templateFile, outputFile, options });
|
||||
} catch (err) {
|
||||
Log.error(`Could not envsubst variables: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
fs.accessSync(configFilename, fs.F_OK);
|
||||
const c = require(configFilename);
|
||||
checkDeprecatedOptions(c);
|
||||
const config = Object.assign(defaults, c);
|
||||
callback(config);
|
||||
return Object.assign(defaults, c);
|
||||
} catch (e) {
|
||||
if (e.code === "ENOENT") {
|
||||
Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
|
||||
@@ -78,14 +125,14 @@ function App() {
|
||||
} else {
|
||||
Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
|
||||
}
|
||||
callback(defaults);
|
||||
}
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the config for deprecated options and throws a warning in the logs
|
||||
* if it encounters one option from the deprecated.js list
|
||||
*
|
||||
* @param {object} userConfig The user config
|
||||
*/
|
||||
function checkDeprecatedOptions(userConfig) {
|
||||
@@ -100,11 +147,9 @@ function App() {
|
||||
|
||||
/**
|
||||
* Loads a specific module.
|
||||
*
|
||||
* @param {string} module The name of the module (including subpath).
|
||||
* @param {Function} callback Function to be called after loading
|
||||
*/
|
||||
function loadModule(module, callback) {
|
||||
function loadModule(module) {
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${__dirname}/../modules/${module}`;
|
||||
@@ -113,6 +158,14 @@ function App() {
|
||||
moduleFolder = `${__dirname}/../modules/default/${module}`;
|
||||
}
|
||||
|
||||
const moduleFile = `${moduleFolder}/${module}.js`;
|
||||
|
||||
try {
|
||||
fs.accessSync(moduleFile, fs.R_OK);
|
||||
} catch (e) {
|
||||
Log.warn(`No ${moduleFile} found for module: ${moduleName}.`);
|
||||
}
|
||||
|
||||
const helperPath = `${moduleFolder}/node_helper.js`;
|
||||
|
||||
let loadHelper = true;
|
||||
@@ -141,44 +194,27 @@ function App() {
|
||||
m.setPath(path.resolve(moduleFolder));
|
||||
nodeHelpers.push(m);
|
||||
|
||||
m.loaded(callback);
|
||||
} else {
|
||||
callback();
|
||||
m.loaded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all modules.
|
||||
*
|
||||
* @param {Module[]} modules All modules to be loaded
|
||||
* @param {Function} callback Function to be called after loading
|
||||
* @returns {Promise} A promise that is resolved when all modules been loaded
|
||||
*/
|
||||
function loadModules(modules, callback) {
|
||||
async function loadModules(modules) {
|
||||
Log.log("Loading module helpers ...");
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function loadNextModule() {
|
||||
if (modules.length > 0) {
|
||||
const nextModule = modules[0];
|
||||
loadModule(nextModule, function () {
|
||||
modules = modules.slice(1);
|
||||
loadNextModule();
|
||||
});
|
||||
} else {
|
||||
// All modules are loaded
|
||||
Log.log("All module helpers loaded.");
|
||||
callback();
|
||||
}
|
||||
for (let module of modules) {
|
||||
await loadModule(module);
|
||||
}
|
||||
|
||||
loadNextModule();
|
||||
Log.log("All module helpers loaded.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* @param {string} a Version number a.
|
||||
* @param {string} b Version number b.
|
||||
* @returns {number} A positive number if a is larger than b, a negative
|
||||
@@ -203,58 +239,53 @@ function App() {
|
||||
/**
|
||||
* Start the core app.
|
||||
*
|
||||
* It loads the config, then it loads all modules. When it's done it
|
||||
* executes the callback with the config as argument.
|
||||
*
|
||||
* @param {Function} callback Function to be called after start
|
||||
* It loads the config, then it loads all modules.
|
||||
* @async
|
||||
* @returns {Promise<object>} the config used
|
||||
*/
|
||||
this.start = function (callback) {
|
||||
loadConfig(function (c) {
|
||||
config = c;
|
||||
this.start = async function () {
|
||||
config = await loadConfig();
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
let modules = [];
|
||||
|
||||
for (const module of config.modules) {
|
||||
if (!modules.includes(module.module) && !module.disabled) {
|
||||
modules.push(module.module);
|
||||
}
|
||||
let modules = [];
|
||||
for (const module of config.modules) {
|
||||
if (!modules.includes(module.module) && !module.disabled) {
|
||||
modules.push(module.module);
|
||||
}
|
||||
}
|
||||
|
||||
loadModules(modules, async function () {
|
||||
httpServer = new Server(config);
|
||||
const { app, io } = await httpServer.open();
|
||||
Log.log("Server started ...");
|
||||
await loadModules(modules);
|
||||
|
||||
const nodePromises = [];
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
nodeHelper.setExpressApp(app);
|
||||
nodeHelper.setSocketIO(io);
|
||||
httpServer = new Server(config);
|
||||
const { app, io } = await httpServer.open();
|
||||
Log.log("Server started ...");
|
||||
|
||||
try {
|
||||
nodePromises.push(nodeHelper.start());
|
||||
} catch (error) {
|
||||
Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`);
|
||||
Log.error(error);
|
||||
}
|
||||
}
|
||||
const nodePromises = [];
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
nodeHelper.setExpressApp(app);
|
||||
nodeHelper.setSocketIO(io);
|
||||
|
||||
Promise.allSettled(nodePromises).then((results) => {
|
||||
// Log errors that happened during async node_helper startup
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
Log.error(result.reason);
|
||||
}
|
||||
});
|
||||
try {
|
||||
nodePromises.push(nodeHelper.start());
|
||||
} catch (error) {
|
||||
Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`);
|
||||
Log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
Log.log("Sockets connected & modules started ...");
|
||||
if (typeof callback === "function") {
|
||||
callback(config);
|
||||
}
|
||||
});
|
||||
});
|
||||
const results = await Promise.allSettled(nodePromises);
|
||||
|
||||
// Log errors that happened during async node_helper startup
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
Log.error(result.reason);
|
||||
}
|
||||
});
|
||||
|
||||
Log.log("Sockets connected & modules started ...");
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -262,16 +293,40 @@ function App() {
|
||||
* exists.
|
||||
*
|
||||
* Added to fix #1056
|
||||
*
|
||||
* @param {Function} callback Function to be called after the app has stopped
|
||||
* @returns {Promise} A promise that is resolved when all node_helpers and
|
||||
* the http server has been closed
|
||||
*/
|
||||
this.stop = function (callback) {
|
||||
for (const nodeHelper of nodeHelpers) {
|
||||
if (typeof nodeHelper.stop === "function") {
|
||||
nodeHelper.stop();
|
||||
this.stop = async function () {
|
||||
const nodePromises = [];
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
try {
|
||||
if (typeof nodeHelper.stop === "function") {
|
||||
nodePromises.push(nodeHelper.stop());
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error(`Error when stopping node_helper for module ${nodeHelper.name}:`);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
httpServer.close().then(callback);
|
||||
|
||||
const results = await Promise.allSettled(nodePromises);
|
||||
|
||||
// Log errors that happened during async node_helper stopping
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
Log.error(result.reason);
|
||||
}
|
||||
});
|
||||
|
||||
Log.log("Node_helpers stopped ...");
|
||||
|
||||
// To be able to stop the app even if it hasn't been started (when
|
||||
// running with Electron against another server)
|
||||
if (!httpServer) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return httpServer.close();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -281,12 +336,12 @@ function App() {
|
||||
* Note: this is only used if running `server-only`. Otherwise
|
||||
* this.stop() is called by app.on("before-quit"... in `electron.js`
|
||||
*/
|
||||
process.on("SIGINT", () => {
|
||||
process.on("SIGINT", async () => {
|
||||
Log.log("[SIGINT] Received. Shutting down server...");
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 3000); // Force quit after 3 seconds
|
||||
this.stop();
|
||||
await this.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
@@ -294,12 +349,12 @@ function App() {
|
||||
* Listen to SIGTERM signals so we can stop everything when we
|
||||
* are asked to stop by the OS.
|
||||
*/
|
||||
process.on("SIGTERM", () => {
|
||||
process.on("SIGTERM", async () => {
|
||||
Log.log("[SIGTERM] Received. Shutting down server...");
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 3000); // Force quit after 3 seconds
|
||||
this.stop();
|
||||
await this.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
@@ -5,11 +5,11 @@
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Linter = require("eslint").Linter;
|
||||
const linter = new Linter();
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { Linter } = require("eslint");
|
||||
|
||||
const linter = new Linter();
|
||||
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
@@ -18,7 +18,6 @@ const Utils = require(`${rootPath}/js/utils.js`);
|
||||
/**
|
||||
* Returns a string with path of configuration file.
|
||||
* Check if set by environment variable MM_CONFIG_FILE
|
||||
*
|
||||
* @returns {string} path and filename of the config file
|
||||
*/
|
||||
function getConfigFile() {
|
||||
|
@@ -82,7 +82,6 @@
|
||||
|
||||
/**
|
||||
* Define the clone method for later use. Helper Method.
|
||||
*
|
||||
* @param {object} obj Object to be cloned
|
||||
* @returns {object} the cloned object
|
||||
*/
|
||||
|
@@ -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",
|
||||
|
@@ -1,8 +1,8 @@
|
||||
"use strict";
|
||||
|
||||
const electron = require("electron");
|
||||
const core = require("./app.js");
|
||||
const Log = require("logger");
|
||||
const core = require("./app");
|
||||
const Log = require("./logger");
|
||||
|
||||
// Config
|
||||
let config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
@@ -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,
|
||||
@@ -46,8 +55,11 @@ function createWindow() {
|
||||
if (config.kioskmode) {
|
||||
electronOptionsDefaults.kiosk = true;
|
||||
} else {
|
||||
electronOptionsDefaults.show = false;
|
||||
electronOptionsDefaults.frame = false;
|
||||
electronOptionsDefaults.transparent = true;
|
||||
electronOptionsDefaults.hasShadow = false;
|
||||
electronOptionsDefaults.fullscreen = true;
|
||||
electronOptionsDefaults.autoHideMenuBar = true;
|
||||
}
|
||||
|
||||
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
|
||||
@@ -117,14 +129,11 @@ function createWindow() {
|
||||
|
||||
callback({ responseHeaders: curHeaders });
|
||||
});
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
app.on("ready", function () {
|
||||
Log.log("Launching application.");
|
||||
createWindow();
|
||||
});
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
}
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on("window-all-closed", function () {
|
||||
@@ -150,27 +159,39 @@ app.on("activate", function () {
|
||||
* Note: this is only used if running Electron. Otherwise
|
||||
* core.stop() is called by process.on("SIGINT"... in `app.js`
|
||||
*/
|
||||
app.on("before-quit", (event) => {
|
||||
app.on("before-quit", async (event) => {
|
||||
Log.log("Shutting down server...");
|
||||
event.preventDefault();
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 3000); // Force-quit after 3 seconds.
|
||||
core.stop();
|
||||
await core.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
/* handle errors from self signed certificates */
|
||||
|
||||
/**
|
||||
* Handle errors from self-signed certificates
|
||||
*/
|
||||
app.on("certificate-error", (event, webContents, url, error, certificate, callback) => {
|
||||
event.preventDefault();
|
||||
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)) {
|
||||
core.start(function (c) {
|
||||
core.start().then((c) => {
|
||||
config = c;
|
||||
app.whenReady().then(() => {
|
||||
Log.log("Launching application.");
|
||||
createWindow();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
28
js/fetch.js
28
js/fetch.js
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* Helper class to provide either third party fetch library or (if node >= 18)
|
||||
* return internal node fetch implementation.
|
||||
*
|
||||
* Attention: After some discussion we always return the third party
|
||||
* implementation until the node implementation is stable and more tested
|
||||
*
|
||||
* @see https://github.com/MichMich/MagicMirror/pull/2952
|
||||
* @see https://github.com/MichMich/MagicMirror/issues/2649
|
||||
* @param {string} url to be fetched
|
||||
* @param {object} options object e.g. for headers
|
||||
* @class
|
||||
*/
|
||||
async function fetch(url, options = {}) {
|
||||
// const nodeVersion = process.version.match(/^v(\d+)\.*/)[1];
|
||||
// if (nodeVersion >= 18) {
|
||||
// // node version >= 18
|
||||
// return global.fetch(url, options);
|
||||
// } else {
|
||||
// // node version < 18
|
||||
// const nodefetch = require("node-fetch");
|
||||
// return nodefetch(url, options);
|
||||
// }
|
||||
const nodefetch = require("node-fetch");
|
||||
return nodefetch(url, options);
|
||||
}
|
||||
|
||||
module.exports = fetch;
|
232
js/loader.js
232
js/loader.js
@@ -16,48 +16,35 @@ const Loader = (function () {
|
||||
/* Private Methods */
|
||||
|
||||
/**
|
||||
* Loops thru all modules and requests load for every module.
|
||||
* Loops through all modules and requests start for every module.
|
||||
*/
|
||||
const loadModules = function () {
|
||||
let moduleData = getModuleData();
|
||||
|
||||
const loadNextModule = function () {
|
||||
if (moduleData.length > 0) {
|
||||
const nextModule = moduleData[0];
|
||||
loadModule(nextModule, function () {
|
||||
moduleData = moduleData.slice(1);
|
||||
loadNextModule();
|
||||
});
|
||||
} else {
|
||||
// All modules loaded. Load custom.css
|
||||
// This is done after all the modules so we can
|
||||
// overwrite all the defined styles.
|
||||
|
||||
loadFile(config.customCss, function () {
|
||||
// custom.css loaded. Start all modules.
|
||||
startModules();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadNextModule();
|
||||
};
|
||||
|
||||
/**
|
||||
* Loops thru all modules and requests start for every module.
|
||||
*/
|
||||
const startModules = function () {
|
||||
const startModules = async function () {
|
||||
const modulePromises = [];
|
||||
for (const module of moduleObjects) {
|
||||
module.start();
|
||||
try {
|
||||
modulePromises.push(module.start());
|
||||
} catch (error) {
|
||||
Log.error(`Error when starting node_helper for module ${module.name}:`);
|
||||
Log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(modulePromises);
|
||||
|
||||
// Log errors that happened during async node_helper startup
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
Log.error(result.reason);
|
||||
}
|
||||
});
|
||||
|
||||
// Notify core of loaded modules.
|
||||
MM.modulesStarted(moduleObjects);
|
||||
|
||||
// Starting modules also hides any modules that have requested to be initially hidden
|
||||
for (const thisModule of moduleObjects) {
|
||||
if (thisModule.data.hiddenOnStartup) {
|
||||
Log.info("Initially hiding " + thisModule.name);
|
||||
Log.info(`Initially hiding ${thisModule.name}`);
|
||||
thisModule.hide();
|
||||
}
|
||||
}
|
||||
@@ -65,7 +52,6 @@ const Loader = (function () {
|
||||
|
||||
/**
|
||||
* Retrieve list of all modules.
|
||||
*
|
||||
* @returns {object[]} module data as configured in config
|
||||
*/
|
||||
const getAllModules = function () {
|
||||
@@ -74,7 +60,6 @@ const Loader = (function () {
|
||||
|
||||
/**
|
||||
* Generate array with module information including module paths.
|
||||
*
|
||||
* @returns {object[]} Module information.
|
||||
*/
|
||||
const getModuleData = function () {
|
||||
@@ -86,10 +71,10 @@ const Loader = (function () {
|
||||
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = config.paths.modules + "/" + module;
|
||||
let moduleFolder = `${config.paths.modules}/${module}`;
|
||||
|
||||
if (defaultModules.indexOf(moduleName) !== -1) {
|
||||
moduleFolder = config.paths.modules + "/default/" + module;
|
||||
moduleFolder = `${config.paths.modules}/default/${module}`;
|
||||
}
|
||||
|
||||
if (moduleData.disabled === true) {
|
||||
@@ -98,16 +83,18 @@ const Loader = (function () {
|
||||
|
||||
moduleFiles.push({
|
||||
index: index,
|
||||
identifier: "module_" + index + "_" + module,
|
||||
identifier: `module_${index}_${module}`,
|
||||
name: moduleName,
|
||||
path: moduleFolder + "/",
|
||||
file: moduleName + ".js",
|
||||
path: `${moduleFolder}/`,
|
||||
file: `${moduleName}.js`,
|
||||
position: moduleData.position,
|
||||
animateIn: moduleData.animateIn,
|
||||
animateOut: moduleData.animateOut,
|
||||
hiddenOnStartup: moduleData.hiddenOnStartup,
|
||||
header: moduleData.header,
|
||||
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,
|
||||
config: moduleData.config,
|
||||
classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module
|
||||
classes: typeof moduleData.classes !== "undefined" ? `${moduleData.classes} ${module}` : module
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,110 +102,95 @@ const Loader = (function () {
|
||||
};
|
||||
|
||||
/**
|
||||
* Load modules via ajax request and create module objects.s
|
||||
*
|
||||
* Load modules via ajax request and create module objects.
|
||||
* @param {object} module Information about the module we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise<void>} resolved when module is loaded
|
||||
*/
|
||||
const loadModule = function (module, callback) {
|
||||
const loadModule = async function (module) {
|
||||
const url = module.path + module.file;
|
||||
|
||||
const afterLoad = function () {
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const afterLoad = async function () {
|
||||
const moduleObject = Module.create(module.name);
|
||||
if (moduleObject) {
|
||||
bootstrapModule(module, moduleObject, function () {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
await bootstrapModule(module, moduleObject);
|
||||
}
|
||||
};
|
||||
|
||||
if (loadedModuleFiles.indexOf(url) !== -1) {
|
||||
afterLoad();
|
||||
await afterLoad();
|
||||
} else {
|
||||
loadFile(url, function () {
|
||||
loadedModuleFiles.push(url);
|
||||
afterLoad();
|
||||
});
|
||||
await loadFile(url);
|
||||
loadedModuleFiles.push(url);
|
||||
await afterLoad();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Bootstrap modules by setting the module data and loading the scripts & styles.
|
||||
*
|
||||
* @param {object} module Information about the module we want to load.
|
||||
* @param {Module} mObj Modules instance.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
const bootstrapModule = function (module, mObj, callback) {
|
||||
Log.info("Bootstrapping module: " + module.name);
|
||||
|
||||
const bootstrapModule = async function (module, mObj) {
|
||||
Log.info(`Bootstrapping module: ${module.name}`);
|
||||
mObj.setData(module);
|
||||
|
||||
mObj.loadScripts(function () {
|
||||
Log.log("Scripts loaded for: " + module.name);
|
||||
mObj.loadStyles(function () {
|
||||
Log.log("Styles loaded for: " + module.name);
|
||||
mObj.loadTranslations(function () {
|
||||
Log.log("Translations loaded for: " + module.name);
|
||||
moduleObjects.push(mObj);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
await mObj.loadScripts();
|
||||
Log.log(`Scripts loaded for: ${module.name}`);
|
||||
|
||||
await mObj.loadStyles();
|
||||
Log.log(`Styles loaded for: ${module.name}`);
|
||||
|
||||
await mObj.loadTranslations();
|
||||
Log.log(`Translations loaded for: ${module.name}`);
|
||||
|
||||
moduleObjects.push(mObj);
|
||||
};
|
||||
|
||||
/**
|
||||
* Load a script or stylesheet by adding it to the dom.
|
||||
*
|
||||
* @param {string} fileName Path of the file we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise} resolved when the file is loaded
|
||||
*/
|
||||
const loadFile = function (fileName, callback) {
|
||||
const loadFile = async function (fileName) {
|
||||
const extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
|
||||
let script, stylesheet;
|
||||
|
||||
switch (extension.toLowerCase()) {
|
||||
case "js":
|
||||
Log.log("Load script: " + fileName);
|
||||
script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = fileName;
|
||||
script.onload = function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
script.onerror = function () {
|
||||
Log.error("Error on loading script:", fileName);
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementsByTagName("body")[0].appendChild(script);
|
||||
break;
|
||||
return new Promise((resolve) => {
|
||||
Log.log(`Load script: ${fileName}`);
|
||||
script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = fileName;
|
||||
script.onload = function () {
|
||||
resolve();
|
||||
};
|
||||
script.onerror = function () {
|
||||
Log.error("Error on loading script:", fileName);
|
||||
resolve();
|
||||
};
|
||||
document.getElementsByTagName("body")[0].appendChild(script);
|
||||
});
|
||||
case "css":
|
||||
Log.log("Load stylesheet: " + fileName);
|
||||
stylesheet = document.createElement("link");
|
||||
stylesheet.rel = "stylesheet";
|
||||
stylesheet.type = "text/css";
|
||||
stylesheet.href = fileName;
|
||||
stylesheet.onload = function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
stylesheet.onerror = function () {
|
||||
Log.error("Error on loading stylesheet:", fileName);
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
return new Promise((resolve) => {
|
||||
Log.log(`Load stylesheet: ${fileName}`);
|
||||
|
||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
||||
break;
|
||||
stylesheet = document.createElement("link");
|
||||
stylesheet.rel = "stylesheet";
|
||||
stylesheet.type = "text/css";
|
||||
stylesheet.href = fileName;
|
||||
stylesheet.onload = function () {
|
||||
resolve();
|
||||
};
|
||||
stylesheet.onerror = function () {
|
||||
Log.error("Error on loading stylesheet:", fileName);
|
||||
resolve();
|
||||
};
|
||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -227,22 +199,40 @@ const Loader = (function () {
|
||||
/**
|
||||
* Load all modules as defined in the config.
|
||||
*/
|
||||
loadModules: function () {
|
||||
loadModules();
|
||||
loadModules: async function () {
|
||||
let moduleData = getModuleData();
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>} when all modules are loaded
|
||||
*/
|
||||
const loadNextModule = async function () {
|
||||
if (moduleData.length > 0) {
|
||||
const nextModule = moduleData[0];
|
||||
await loadModule(nextModule);
|
||||
moduleData = moduleData.slice(1);
|
||||
await loadNextModule();
|
||||
} else {
|
||||
// All modules loaded. Load custom.css
|
||||
// This is done after all the modules so we can
|
||||
// overwrite all the defined styles.
|
||||
await loadFile(config.customCss);
|
||||
// custom.css loaded. Start all modules.
|
||||
await startModules();
|
||||
}
|
||||
};
|
||||
await loadNextModule();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load a file (script or stylesheet).
|
||||
* Prevent double loading and search for files in the vendor folder.
|
||||
*
|
||||
* @param {string} fileName Path of the file we want to load.
|
||||
* @param {Module} module The module that calls the loadFile function.
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise} resolved when the file is loaded
|
||||
*/
|
||||
loadFile: function (fileName, module, callback) {
|
||||
loadFileForModule: async function (fileName, module) {
|
||||
if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) {
|
||||
Log.log("File already loaded: " + fileName);
|
||||
callback();
|
||||
Log.log(`File already loaded: ${fileName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -250,22 +240,20 @@ const Loader = (function () {
|
||||
// This is an absolute or relative path.
|
||||
// Load it and then return.
|
||||
loadedFiles.push(fileName.toLowerCase());
|
||||
loadFile(fileName, callback);
|
||||
return;
|
||||
return loadFile(fileName);
|
||||
}
|
||||
|
||||
if (vendor[fileName] !== undefined) {
|
||||
// This file is available in the vendor folder.
|
||||
// Load it from this vendor folder.
|
||||
loadedFiles.push(fileName.toLowerCase());
|
||||
loadFile(config.paths.vendor + "/" + vendor[fileName], callback);
|
||||
return;
|
||||
return loadFile(`${config.paths.vendor}/${vendor[fileName]}`);
|
||||
}
|
||||
|
||||
// File not loaded yet.
|
||||
// Load it based on the module path.
|
||||
loadedFiles.push(fileName.toLowerCase());
|
||||
loadFile(module.file(fileName), callback);
|
||||
return loadFile(module.file(fileName));
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
253
js/main.js
253
js/main.js
@@ -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");
|
||||
@@ -29,7 +33,7 @@ const MM = (function () {
|
||||
dom.className = module.name;
|
||||
|
||||
if (typeof module.data.classes === "string") {
|
||||
dom.className = "module " + dom.className + " " + module.data.classes;
|
||||
dom.className = `module ${dom.className} ${module.data.classes}`;
|
||||
}
|
||||
|
||||
dom.opacity = 0;
|
||||
@@ -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 () {
|
||||
@@ -68,7 +77,6 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Select the wrapper dom object for a specific position.
|
||||
*
|
||||
* @param {string} position The name of the position.
|
||||
* @returns {HTMLElement | void} the wrapper element
|
||||
*/
|
||||
@@ -85,7 +93,6 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
@@ -102,13 +109,31 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Update the dom for a specific module.
|
||||
*
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
||||
* @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates)
|
||||
* @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start of MagicMirror)
|
||||
* @returns {Promise} Resolved when the dom is fully updated.
|
||||
*/
|
||||
const updateDom = function (module, speed) {
|
||||
const updateDom = function (module, updateOptions, createAnimatedDom = false) {
|
||||
return new Promise(function (resolve) {
|
||||
let speed = updateOptions;
|
||||
let animateOut = null;
|
||||
let animateIn = null;
|
||||
if (typeof updateOptions === "object") {
|
||||
if (typeof updateOptions.options === "object" && updateOptions.options.speed !== undefined) {
|
||||
speed = updateOptions.options.speed;
|
||||
Log.debug(`updateDom: ${module.identifier} Has speed in object: ${speed}`);
|
||||
if (typeof updateOptions.options.animate === "object") {
|
||||
animateOut = updateOptions.options.animate.out;
|
||||
animateIn = updateOptions.options.animate.in;
|
||||
Log.debug(`updateDom: ${module.identifier} Has animate in object: out->${animateOut}, in->${animateIn}`);
|
||||
}
|
||||
} else {
|
||||
Log.debug(`updateDom: ${module.identifier} Has no speed in object`);
|
||||
speed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const newHeader = module.getHeader();
|
||||
let newContentPromise = module.getDom();
|
||||
|
||||
@@ -119,7 +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);
|
||||
})
|
||||
@@ -129,14 +154,16 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Update the dom with the specified content
|
||||
*
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
* @param {string} [animateOut] AnimateCss animation name before hidden
|
||||
* @param {string} [animateIn] AnimateCss animation name on show
|
||||
* @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start)
|
||||
* @returns {Promise} Resolved when the module dom has been updated.
|
||||
*/
|
||||
const updateDomWithContent = function (module, speed, newHeader, newContent) {
|
||||
const updateDomWithContent = function (module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom = false) {
|
||||
return new Promise(function (resolve) {
|
||||
if (module.hidden || !speed) {
|
||||
updateModuleContent(module, newHeader, newContent);
|
||||
@@ -155,19 +182,33 @@ const MM = (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
hideModule(module, speed / 2, function () {
|
||||
if (createAnimatedDom && animateIn !== null) {
|
||||
Log.debug(`${module.identifier} createAnimatedDom (${animateIn})`);
|
||||
updateModuleContent(module, newHeader, newContent);
|
||||
if (!module.hidden) {
|
||||
showModule(module, speed / 2);
|
||||
showModule(module, speed, null, { animate: animateIn });
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
hideModule(
|
||||
module,
|
||||
speed / 2,
|
||||
function () {
|
||||
updateModuleContent(module, newHeader, newContent);
|
||||
if (!module.hidden) {
|
||||
showModule(module, speed / 2, null, { animate: animateIn });
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
{ animate: animateOut }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the content has changed.
|
||||
*
|
||||
* @param {Module} module The module to check.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
@@ -198,7 +239,6 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Update the content of a module on screen.
|
||||
*
|
||||
* @param {Module} module The module to check.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
@@ -224,15 +264,12 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Hide the module.
|
||||
*
|
||||
* @param {Module} module The module to hide.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
const hideModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
|
||||
const hideModule = function (module, speed, callback, options = {}) {
|
||||
// set lockString if set in options.
|
||||
if (options.lockString) {
|
||||
// Log.log("Has lockstring: " + options.lockString);
|
||||
@@ -243,24 +280,65 @@ const MM = (function () {
|
||||
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper !== null) {
|
||||
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
||||
moduleWrapper.style.opacity = 0;
|
||||
moduleWrapper.classList.add("hidden");
|
||||
|
||||
clearTimeout(module.showHideTimer);
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
// To not take up any space, we just make the position absolute.
|
||||
// since it's fade out anyway, we can see it lay above or
|
||||
// below other modules. This works way better than adjusting
|
||||
// the .display property.
|
||||
moduleWrapper.style.position = "fixed";
|
||||
// reset all animations if needed
|
||||
if (module.hasAnimateOut) {
|
||||
removeAnimateCSS(module.identifier, module.hasAnimateOut);
|
||||
Log.debug(`${module.identifier} Force remove animateOut (in hide): ${module.hasAnimateOut}`);
|
||||
module.hasAnimateOut = false;
|
||||
}
|
||||
if (module.hasAnimateIn) {
|
||||
removeAnimateCSS(module.identifier, module.hasAnimateIn);
|
||||
Log.debug(`${module.identifier} Force remove animateIn (in hide): ${module.hasAnimateIn}`);
|
||||
module.hasAnimateIn = false;
|
||||
}
|
||||
// haveAnimateName for verify if we are using 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;
|
||||
|
||||
updateWrapperStates();
|
||||
if (haveAnimateName) {
|
||||
// with AnimateCSS
|
||||
Log.debug(`${module.identifier} Has animateOut: ${haveAnimateName}`);
|
||||
module.hasAnimateOut = haveAnimateName;
|
||||
addAnimateCSS(module.identifier, haveAnimateName, speed / 1000);
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
removeAnimateCSS(module.identifier, haveAnimateName);
|
||||
Log.debug(`${module.identifier} Remove animateOut: ${module.hasAnimateOut}`);
|
||||
// AnimateCSS is now done
|
||||
moduleWrapper.style.opacity = 0;
|
||||
moduleWrapper.classList.add("hidden");
|
||||
moduleWrapper.style.position = "fixed";
|
||||
module.hasAnimateOut = false;
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
updateWrapperStates();
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
} else {
|
||||
// default MM² Animate
|
||||
moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
|
||||
moduleWrapper.style.opacity = 0;
|
||||
moduleWrapper.classList.add("hidden");
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
// To not take up any space, we just make the position absolute.
|
||||
// since it's fade out anyway, we can see it lay above or
|
||||
// below other modules. This works way better than adjusting
|
||||
// the .display property.
|
||||
moduleWrapper.style.position = "fixed";
|
||||
|
||||
updateWrapperStates();
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
}
|
||||
} else {
|
||||
// invoke callback even if no content, issue 1308
|
||||
if (typeof callback === "function") {
|
||||
@@ -271,15 +349,12 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Show the module.
|
||||
*
|
||||
* @param {Module} module The module to show.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
const showModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
|
||||
const showModule = function (module, speed, callback, options = {}) {
|
||||
// remove lockString if set in options.
|
||||
if (options.lockString) {
|
||||
const index = module.lockStrings.indexOf(options.lockString);
|
||||
@@ -291,24 +366,46 @@ const MM = (function () {
|
||||
// Check if there are no more lockstrings set, or the force option is set.
|
||||
// Otherwise cancel show action.
|
||||
if (module.lockStrings.length !== 0 && options.force !== true) {
|
||||
Log.log("Will not show " + module.name + ". LockStrings active: " + module.lockStrings.join(","));
|
||||
Log.log(`Will not show ${module.name}. LockStrings active: ${module.lockStrings.join(",")}`);
|
||||
if (typeof options.onError === "function") {
|
||||
options.onError(new Error("LOCK_STRING_ACTIVE"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// reset all animations if needed
|
||||
if (module.hasAnimateOut) {
|
||||
removeAnimateCSS(module.identifier, module.hasAnimateOut);
|
||||
Log.debug(`${module.identifier} Force remove animateOut (in show): ${module.hasAnimateOut}`);
|
||||
module.hasAnimateOut = false;
|
||||
}
|
||||
if (module.hasAnimateIn) {
|
||||
removeAnimateCSS(module.identifier, module.hasAnimateIn);
|
||||
Log.debug(`${module.identifier} Force remove animateIn (in show): ${module.hasAnimateIn}`);
|
||||
module.hasAnimateIn = false;
|
||||
}
|
||||
|
||||
module.hidden = false;
|
||||
|
||||
// If forced show, clean current lockstrings.
|
||||
if (module.lockStrings.length !== 0 && options.force === true) {
|
||||
Log.log("Force show of module: " + module.name);
|
||||
Log.log(`Force show of module: ${module.name}`);
|
||||
module.lockStrings = [];
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -319,12 +416,27 @@ const MM = (function () {
|
||||
const dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
|
||||
moduleWrapper.style.opacity = 1;
|
||||
|
||||
clearTimeout(module.showHideTimer);
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
if (haveAnimateName) {
|
||||
// with AnimateCSS
|
||||
Log.debug(`${module.identifier} Has animateIn: ${haveAnimateName}`);
|
||||
module.hasAnimateIn = haveAnimateName;
|
||||
addAnimateCSS(module.identifier, haveAnimateName, speed / 1000);
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
removeAnimateCSS(module.identifier, haveAnimateName);
|
||||
Log.debug(`${module.identifier} Remove animateIn: ${haveAnimateName}`);
|
||||
module.hasAnimateIn = false;
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
} else {
|
||||
// default MM² Animate
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
}
|
||||
} else {
|
||||
// invoke callback
|
||||
if (typeof callback === "function") {
|
||||
@@ -380,13 +492,11 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Adds special selectors on a collection of modules.
|
||||
*
|
||||
* @param {Module[]} modules Array of modules.
|
||||
*/
|
||||
const setSelectionMethodsForModules = function (modules) {
|
||||
/**
|
||||
* Filter modules with the specified classes.
|
||||
*
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
@@ -396,7 +506,6 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Filter modules without the specified classes.
|
||||
*
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
@@ -406,7 +515,6 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Filters a collection of modules based on classname(s).
|
||||
*
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
* @param {boolean} include if the filter should include or exclude the modules with the specific classes.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
@@ -435,7 +543,6 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Removes a module instance from the collection.
|
||||
*
|
||||
* @param {object} module The module instance to remove from the collection.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
@@ -450,7 +557,6 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Walks thru a collection of modules and executes the callback with the module as an argument.
|
||||
*
|
||||
* @param {Function} callback The function to execute with the module as an argument.
|
||||
*/
|
||||
const enumerate = function (callback) {
|
||||
@@ -479,34 +585,53 @@ const MM = (function () {
|
||||
/**
|
||||
* Main init method.
|
||||
*/
|
||||
init: function () {
|
||||
init: async function () {
|
||||
Log.info("Initializing MagicMirror².");
|
||||
loadConfig();
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
Translator.loadCoreTranslations(config.language);
|
||||
Loader.loadModules();
|
||||
await Translator.loadCoreTranslations(config.language);
|
||||
await Loader.loadModules();
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets called when all modules are started.
|
||||
*
|
||||
* @param {Module[]} moduleObjects All module instances.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
@@ -533,11 +658,10 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Update the dom for a specific module.
|
||||
*
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The number of microseconds for the animation.
|
||||
* @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates)
|
||||
*/
|
||||
updateDom: function (module, speed) {
|
||||
updateDom: function (module, updateOptions) {
|
||||
if (!(module instanceof Module)) {
|
||||
Log.error("updateDom: Sender should be a module.");
|
||||
return;
|
||||
@@ -549,12 +673,11 @@ const MM = (function () {
|
||||
}
|
||||
|
||||
// Further implementation is done in the private method.
|
||||
updateDom(module, speed);
|
||||
updateDom(module, updateOptions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a collection of all modules currently active.
|
||||
*
|
||||
* @returns {Module[]} A collection of all modules currently active.
|
||||
*/
|
||||
getModules: function () {
|
||||
@@ -564,7 +687,6 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Hide the module.
|
||||
*
|
||||
* @param {Module} module The module to hide.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
@@ -577,7 +699,6 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Show the module.
|
||||
*
|
||||
* @param {Module} module The module to show.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
|
143
js/module.js
143
js/module.js
@@ -25,7 +25,7 @@ const Module = Class.extend({
|
||||
// visibility when hiding and showing module.
|
||||
lockStrings: [],
|
||||
|
||||
// Storage of the nunjuck Environment,
|
||||
// Storage of the nunjucks Environment,
|
||||
// This should not be referenced directly.
|
||||
// Use the nunjucksEnvironment() to get it.
|
||||
_nunjucksEnvironment: null,
|
||||
@@ -40,13 +40,12 @@ const Module = Class.extend({
|
||||
/**
|
||||
* Called when the module is started.
|
||||
*/
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
start: async function () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a list of scripts the module requires to be loaded.
|
||||
*
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getScripts: function () {
|
||||
@@ -55,7 +54,6 @@ const Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Returns a list of stylesheets the module requires to be loaded.
|
||||
*
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getStyles: function () {
|
||||
@@ -66,7 +64,6 @@ const Module = Class.extend({
|
||||
* Returns a map of translation files the module requires to be loaded.
|
||||
*
|
||||
* return Map<String, String> -
|
||||
*
|
||||
* @returns {*} A map with langKeys and filenames.
|
||||
*/
|
||||
getTranslations: function () {
|
||||
@@ -77,7 +74,6 @@ const Module = Class.extend({
|
||||
* Generates the dom which needs to be displayed. This method is called by the MagicMirror² core.
|
||||
* This method can to be subclassed if the module wants to display info on the mirror.
|
||||
* Alternatively, the getTemplate method could be subclassed.
|
||||
*
|
||||
* @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
|
||||
*/
|
||||
getDom: function () {
|
||||
@@ -111,7 +107,6 @@ const Module = Class.extend({
|
||||
* Generates the header string which needs to be displayed if a user has a header configured for this module.
|
||||
* This method is called by the MagicMirror² core, but only if the user has configured a default header for the module.
|
||||
* This method needs to be subclassed if the module wants to display modified headers on the mirror.
|
||||
*
|
||||
* @returns {string} The header to display above the header.
|
||||
*/
|
||||
getHeader: function () {
|
||||
@@ -123,17 +118,15 @@ const Module = Class.extend({
|
||||
* This method needs to be subclassed if the module wants to use a template.
|
||||
* It can either return a template sting, or a template filename.
|
||||
* If the string ends with '.html' it's considered a file from within the module's folder.
|
||||
*
|
||||
* @returns {string} The template string of filename.
|
||||
*/
|
||||
getTemplate: function () {
|
||||
return '<div class="normal">' + this.name + '</div><div class="small dimmed">' + this.identifier + "</div>";
|
||||
return `<div class="normal">${this.name}</div><div class="small dimmed">${this.identifier}</div>`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the data to be used in the template.
|
||||
* This method needs to be subclassed if the module wants to use a custom data.
|
||||
*
|
||||
* @returns {object} The data for the template
|
||||
*/
|
||||
getTemplateData: function () {
|
||||
@@ -142,7 +135,6 @@ const Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Called by the MagicMirror² core when a notification arrives.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
@@ -158,7 +150,6 @@ const Module = Class.extend({
|
||||
/**
|
||||
* Returns the nunjucks environment for the current module.
|
||||
* The environment is checked in the _nunjucksEnvironment instance variable.
|
||||
*
|
||||
* @returns {object} The Nunjucks Environment
|
||||
*/
|
||||
nunjucksEnvironment: function () {
|
||||
@@ -180,35 +171,33 @@ const Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Called when a socket notification arrives.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
|
||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the module is hidden.
|
||||
*/
|
||||
suspend: function () {
|
||||
Log.log(this.name + " is suspended.");
|
||||
Log.log(`${this.name} is suspended.`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the module is shown.
|
||||
*/
|
||||
resume: function () {
|
||||
Log.log(this.name + " is resumed.");
|
||||
Log.log(`${this.name} is resumed.`);
|
||||
},
|
||||
|
||||
/*********************************************
|
||||
* The methods below don"t need subclassing. *
|
||||
* The methods below don't need subclassing. *
|
||||
*********************************************/
|
||||
|
||||
/**
|
||||
* Set the module data.
|
||||
*
|
||||
* @param {object} data The module data
|
||||
*/
|
||||
setData: function (data) {
|
||||
@@ -216,13 +205,14 @@ 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);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the module config and combine it with the module defaults.
|
||||
*
|
||||
* @param {object} config The combined module config.
|
||||
* @param {boolean} deep Merge module config in deep.
|
||||
*/
|
||||
@@ -233,7 +223,6 @@ const Module = Class.extend({
|
||||
/**
|
||||
* Returns a socket object. If it doesn't exist, it's created.
|
||||
* It also registers the notification callback.
|
||||
*
|
||||
* @returns {MMSocket} a socket object
|
||||
*/
|
||||
socket: function () {
|
||||
@@ -250,62 +239,56 @@ const Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Retrieve the path to a module file.
|
||||
*
|
||||
* @param {string} file Filename
|
||||
* @returns {string} the file path
|
||||
*/
|
||||
file: function (file) {
|
||||
return (this.data.path + "/" + file).replace("//", "/");
|
||||
return `${this.data.path}/${file}`.replace("//", "/");
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all required stylesheets by requesting the MM object to load the files.
|
||||
*
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadStyles: function (callback) {
|
||||
this.loadDependencies("getStyles", callback);
|
||||
loadStyles: function () {
|
||||
return this.loadDependencies("getStyles");
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all required scripts by requesting the MM object to load the files.
|
||||
*
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadScripts: function (callback) {
|
||||
this.loadDependencies("getScripts", callback);
|
||||
loadScripts: function () {
|
||||
return this.loadDependencies("getScripts");
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to load all dependencies.
|
||||
*
|
||||
* @param {string} funcName Function name to call to get scripts or styles.
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadDependencies: function (funcName, callback) {
|
||||
loadDependencies: async function (funcName) {
|
||||
let dependencies = this[funcName]();
|
||||
|
||||
const loadNextDependency = () => {
|
||||
const loadNextDependency = async () => {
|
||||
if (dependencies.length > 0) {
|
||||
const nextDependency = dependencies[0];
|
||||
Loader.loadFile(nextDependency, this, () => {
|
||||
dependencies = dependencies.slice(1);
|
||||
loadNextDependency();
|
||||
});
|
||||
await Loader.loadFileForModule(nextDependency, this);
|
||||
dependencies = dependencies.slice(1);
|
||||
await loadNextDependency();
|
||||
} else {
|
||||
callback();
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
loadNextDependency();
|
||||
await loadNextDependency();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all translations.
|
||||
*
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadTranslations(callback) {
|
||||
loadTranslations: async function () {
|
||||
const translations = this.getTranslations() || {};
|
||||
const language = config.language.toLowerCase();
|
||||
|
||||
@@ -313,7 +296,6 @@ const Module = Class.extend({
|
||||
const fallbackLanguage = languages[0];
|
||||
|
||||
if (languages.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -321,22 +303,18 @@ const Module = Class.extend({
|
||||
const translationsFallbackFile = translations[fallbackLanguage];
|
||||
|
||||
if (!translationFile) {
|
||||
Translator.load(this, translationsFallbackFile, true, callback);
|
||||
return;
|
||||
return Translator.load(this, translationsFallbackFile, true);
|
||||
}
|
||||
|
||||
Translator.load(this, translationFile, false, () => {
|
||||
if (translationFile !== translationsFallbackFile) {
|
||||
Translator.load(this, translationsFallbackFile, true, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
await Translator.load(this, translationFile, false);
|
||||
|
||||
if (translationFile !== translationsFallbackFile) {
|
||||
return Translator.load(this, translationsFallbackFile, true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Request the translation for a given key with optional variables and default value.
|
||||
*
|
||||
* @param {string} key The key of the string to translate
|
||||
* @param {string|object} [defaultValueOrVariables] The default value or variables for translating.
|
||||
* @param {string} [defaultValue] The default value with variables.
|
||||
@@ -351,16 +329,14 @@ 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);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
@@ -370,7 +346,6 @@ const Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Send a socket notification to the node helper.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
@@ -380,55 +355,55 @@ const Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Hide this module.
|
||||
*
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
hide: function (speed, callback, options) {
|
||||
if (typeof callback === "object") {
|
||||
options = callback;
|
||||
callback = function () {};
|
||||
}
|
||||
hide: function (speed, callback, options = {}) {
|
||||
let usedCallback = callback || function () {};
|
||||
let usedOptions = options;
|
||||
|
||||
callback = callback || function () {};
|
||||
options = options || {};
|
||||
if (typeof callback === "object") {
|
||||
Log.error("Parameter mismatch in module.hide: callback is not an optional parameter!");
|
||||
usedOptions = callback;
|
||||
usedCallback = function () {};
|
||||
}
|
||||
|
||||
MM.hideModule(
|
||||
this,
|
||||
speed,
|
||||
() => {
|
||||
this.suspend();
|
||||
callback();
|
||||
usedCallback();
|
||||
},
|
||||
options
|
||||
usedOptions
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show this module.
|
||||
*
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
show: function (speed, callback, options) {
|
||||
if (typeof callback === "object") {
|
||||
options = callback;
|
||||
callback = function () {};
|
||||
}
|
||||
let usedCallback = callback || function () {};
|
||||
let usedOptions = options;
|
||||
|
||||
callback = callback || function () {};
|
||||
options = options || {};
|
||||
if (typeof callback === "object") {
|
||||
Log.error("Parameter mismatch in module.show: callback is not an optional parameter!");
|
||||
usedOptions = callback;
|
||||
usedCallback = function () {};
|
||||
}
|
||||
|
||||
MM.showModule(
|
||||
this,
|
||||
speed,
|
||||
() => {
|
||||
this.resume();
|
||||
callback();
|
||||
usedCallback();
|
||||
},
|
||||
options
|
||||
usedOptions
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -452,7 +427,6 @@ const Module = Class.extend({
|
||||
* -------
|
||||
*
|
||||
* Todo: idea of Mich determinate what do you want to merge or not
|
||||
*
|
||||
* @param {object} result the initial object
|
||||
* @returns {object} the merged config
|
||||
*/
|
||||
@@ -498,15 +472,15 @@ Module.create = function (name) {
|
||||
|
||||
Module.register = function (name, moduleDefinition) {
|
||||
if (moduleDefinition.requiresVersion) {
|
||||
Log.log("Check MagicMirror² version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.mmVersion);
|
||||
Log.log(`Check MagicMirror² version for module '${name}' - Minimum version: ${moduleDefinition.requiresVersion} - Current version: ${window.mmVersion}`);
|
||||
if (cmpVersions(window.mmVersion, moduleDefinition.requiresVersion) >= 0) {
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
Log.warn("Version is incorrect. Skip module: '" + name + "'");
|
||||
Log.warn(`Version is incorrect. Skip module: '${name}'`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Log.log("Module registered: " + name);
|
||||
Log.log(`Module registered: ${name}`);
|
||||
Module.definitions[name] = moduleDefinition;
|
||||
};
|
||||
|
||||
@@ -514,7 +488,6 @@ window.Module = Module;
|
||||
|
||||
/**
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* @param {string} a Version number a.
|
||||
* @param {string} b Version number b.
|
||||
* @returns {number} A positive number if a is larger than b, a negative
|
||||
|
@@ -4,18 +4,17 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Class = require("./class.js");
|
||||
const Log = require("logger");
|
||||
const express = require("express");
|
||||
const Log = require("logger");
|
||||
const Class = require("./class");
|
||||
|
||||
const NodeHelper = Class.extend({
|
||||
init() {
|
||||
Log.log("Initializing new module helper ...");
|
||||
},
|
||||
|
||||
loaded(callback) {
|
||||
loaded() {
|
||||
Log.log(`Module helper loaded: ${this.name}`);
|
||||
callback();
|
||||
},
|
||||
|
||||
start() {
|
||||
@@ -33,7 +32,6 @@ const NodeHelper = Class.extend({
|
||||
|
||||
/**
|
||||
* This method is called when a socket notification arrives.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
@@ -43,7 +41,6 @@ const NodeHelper = Class.extend({
|
||||
|
||||
/**
|
||||
* Set the module name.
|
||||
*
|
||||
* @param {string} name Module name.
|
||||
*/
|
||||
setName(name) {
|
||||
@@ -52,7 +49,6 @@ const NodeHelper = Class.extend({
|
||||
|
||||
/**
|
||||
* Set the module path.
|
||||
*
|
||||
* @param {string} path Module path.
|
||||
*/
|
||||
setPath(path) {
|
||||
@@ -124,7 +120,6 @@ NodeHelper.checkFetchStatus = function (response) {
|
||||
/**
|
||||
* Look at the specified error and return an appropriate error type, that
|
||||
* can be translated to a detailed error message
|
||||
*
|
||||
* @param {Error} error the error from fetching something
|
||||
* @returns {string} the string of the detailed error message in the translations
|
||||
*/
|
||||
|
24
js/server.js
24
js/server.js
@@ -4,19 +4,21 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const ipfilter = require("express-ipfilter").IpFilter;
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const path = require("path");
|
||||
const express = require("express");
|
||||
const ipfilter = require("express-ipfilter").IpFilter;
|
||||
const helmet = require("helmet");
|
||||
const socketio = require("socket.io");
|
||||
|
||||
const Log = require("logger");
|
||||
const Utils = require("./utils.js");
|
||||
const { cors, getConfig, getHtml, getVersion } = require("./server_functions.js");
|
||||
const Utils = require("./utils");
|
||||
const { cors, getConfig, getHtml, getVersion, getStartup } = require("./server_functions");
|
||||
|
||||
/**
|
||||
* Server
|
||||
*
|
||||
* @param {object} config The MM config
|
||||
* @class
|
||||
*/
|
||||
@@ -28,7 +30,6 @@ function Server(config) {
|
||||
|
||||
/**
|
||||
* Opens the server for incoming connections
|
||||
*
|
||||
* @returns {Promise} A promise that is resolved when the server listens to connections
|
||||
*/
|
||||
this.open = function () {
|
||||
@@ -38,11 +39,11 @@ function Server(config) {
|
||||
key: fs.readFileSync(config.httpsPrivateKey),
|
||||
cert: fs.readFileSync(config.httpsCertificate)
|
||||
};
|
||||
server = require("https").Server(options, app);
|
||||
server = https.Server(options, app);
|
||||
} else {
|
||||
server = require("http").Server(app);
|
||||
server = http.Server(app);
|
||||
}
|
||||
const io = require("socket.io")(server, {
|
||||
const io = socketio(server, {
|
||||
cors: {
|
||||
origin: /.*$/,
|
||||
credentials: true
|
||||
@@ -90,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", () => {
|
||||
@@ -103,7 +106,6 @@ function Server(config) {
|
||||
|
||||
/**
|
||||
* Closes the server and destroys all lingering connections to it.
|
||||
*
|
||||
* @returns {Promise} A promise that resolves when server has successfully shut down
|
||||
*/
|
||||
this.close = function () {
|
||||
|
@@ -1,11 +1,10 @@
|
||||
const fetch = require("./fetch");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Log = require("logger");
|
||||
const startUp = new Date();
|
||||
|
||||
/**
|
||||
* Gets the config.
|
||||
*
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
@@ -14,23 +13,31 @@ function getConfig(req, res) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A method that forewards HTTP Get-methods to the internet to avoid CORS-errors.
|
||||
* Gets the startup time.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getStartup(req, res) {
|
||||
res.send(startUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* A method that forwards HTTP Get-methods to the internet to avoid CORS-errors.
|
||||
*
|
||||
* Example input request url: /cors?sendheaders=header1:value1,header2:value2&expectedheaders=header1,header2&url=http://www.test.com/path?param1=value1
|
||||
*
|
||||
* Only the url-param of the input request url is required. It must be the last parameter.
|
||||
*
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
async function cors(req, res) {
|
||||
try {
|
||||
const urlRegEx = "url=(.+?)$";
|
||||
let url = "";
|
||||
let url;
|
||||
|
||||
const match = new RegExp(urlRegEx, "g").exec(req.url);
|
||||
if (!match) {
|
||||
url = "invalid url: " + req.url;
|
||||
url = `invalid url: ${req.url}`;
|
||||
Log.error(url);
|
||||
res.send(url);
|
||||
} else {
|
||||
@@ -39,7 +46,7 @@ async function cors(req, res) {
|
||||
const headersToSend = getHeadersToSend(req.url);
|
||||
const expectedRecievedHeaders = geExpectedRecievedHeaders(req.url);
|
||||
|
||||
Log.log("cors url: " + url);
|
||||
Log.log(`cors url: ${url}`);
|
||||
const response = await fetch(url, { headers: headersToSend });
|
||||
|
||||
for (const header of expectedRecievedHeaders) {
|
||||
@@ -56,13 +63,12 @@ async function cors(req, res) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets headers and values to attatch to the web request.
|
||||
*
|
||||
* Gets headers and values to attach to the web request.
|
||||
* @param {string} url - The url containing the headers and values to send.
|
||||
* @returns {object} An object specifying name and value of the headers.
|
||||
*/
|
||||
function getHeadersToSend(url) {
|
||||
const headersToSend = { "User-Agent": "Mozilla/5.0 MagicMirror/" + global.version };
|
||||
const headersToSend = { "User-Agent": `Mozilla/5.0 MagicMirror/${global.version}` };
|
||||
const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (headersToSendMatch) {
|
||||
const headers = headersToSendMatch[1].split(",");
|
||||
@@ -79,7 +85,6 @@ function getHeadersToSend(url) {
|
||||
|
||||
/**
|
||||
* Gets the headers expected from the response.
|
||||
*
|
||||
* @param {string} url - The url containing the expected headers from the response.
|
||||
* @returns {string[]} headers - The name of the expected headers.
|
||||
*/
|
||||
@@ -97,7 +102,6 @@ function geExpectedRecievedHeaders(url) {
|
||||
|
||||
/**
|
||||
* Gets the HTML to display the magic mirror.
|
||||
*
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
@@ -116,7 +120,6 @@ function getHtml(req, res) {
|
||||
|
||||
/**
|
||||
* Gets the MagicMirror version.
|
||||
*
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
@@ -124,4 +127,4 @@ function getVersion(req, res) {
|
||||
res.send(global.version);
|
||||
}
|
||||
|
||||
module.exports = { cors, getConfig, getHtml, getVersion };
|
||||
module.exports = { cors, getConfig, getHtml, getVersion, getStartup };
|
||||
|
@@ -18,8 +18,8 @@ const MMSocket = function (moduleName) {
|
||||
if (typeof config !== "undefined" && typeof config.basePath !== "undefined") {
|
||||
base = config.basePath;
|
||||
}
|
||||
this.socket = io("/" + this.moduleName, {
|
||||
path: base + "socket.io"
|
||||
this.socket = io(`/${this.moduleName}`, {
|
||||
path: `${base}socket.io`
|
||||
});
|
||||
|
||||
let notificationCallback = function () {};
|
||||
@@ -44,10 +44,7 @@ const MMSocket = function (moduleName) {
|
||||
notificationCallback = callback;
|
||||
};
|
||||
|
||||
this.sendNotification = (notification, payload) => {
|
||||
if (typeof payload === "undefined") {
|
||||
payload = {};
|
||||
}
|
||||
this.sendNotification = (notification, payload = {}) => {
|
||||
this.socket.emit(notification, payload);
|
||||
};
|
||||
};
|
||||
|
@@ -9,28 +9,29 @@
|
||||
const Translator = (function () {
|
||||
/**
|
||||
* Load a JSON file via XHR.
|
||||
*
|
||||
* @param {string} file Path of the file we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise<object>} the translations in the specified file
|
||||
*/
|
||||
function loadJSON(file, callback) {
|
||||
async function loadJSON(file) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.overrideMimeType("application/json");
|
||||
xhr.open("GET", file, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
// needs error handler try/catch at least
|
||||
let fileinfo = null;
|
||||
try {
|
||||
fileinfo = JSON.parse(xhr.responseText);
|
||||
} catch (exception) {
|
||||
// nothing here, but don't die
|
||||
Log.error(" loading json file =" + file + " failed");
|
||||
return new Promise(function (resolve) {
|
||||
xhr.overrideMimeType("application/json");
|
||||
xhr.open("GET", file, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
// needs error handler try/catch at least
|
||||
let fileinfo = null;
|
||||
try {
|
||||
fileinfo = JSON.parse(xhr.responseText);
|
||||
} catch (exception) {
|
||||
// nothing here, but don't die
|
||||
Log.error(` loading json file =${file} failed`);
|
||||
}
|
||||
resolve(fileinfo);
|
||||
}
|
||||
callback(fileinfo);
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
};
|
||||
xhr.send(null);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -41,21 +42,17 @@ const Translator = (function () {
|
||||
|
||||
/**
|
||||
* Load a translation for a given key for a given module.
|
||||
*
|
||||
* @param {Module} module The module to load the translation for.
|
||||
* @param {string} key The key of the text to translate.
|
||||
* @param {object} variables The variables to use within the translation template (optional)
|
||||
* @returns {string} the translated key
|
||||
*/
|
||||
translate: function (module, key, variables) {
|
||||
variables = variables || {}; //Empty object by default
|
||||
|
||||
translate: function (module, key, variables = {}) {
|
||||
/**
|
||||
* Combines template and variables like:
|
||||
* template: "Please wait for {timeToWait} before continuing with {work}."
|
||||
* variables: {timeToWait: "2 hours", work: "painting"}
|
||||
* to: "Please wait for 2 hours before continuing with painting."
|
||||
*
|
||||
* @param {string} template Text with placeholder
|
||||
* @param {object} variables Variables for the placeholder
|
||||
* @returns {string} the template filled with the variables
|
||||
@@ -64,11 +61,12 @@ const Translator = (function () {
|
||||
if (Object.prototype.toString.call(template) !== "[object String]") {
|
||||
return template;
|
||||
}
|
||||
let templateToUse = template;
|
||||
if (variables.fallback && !template.match(new RegExp("{.+}"))) {
|
||||
template = variables.fallback;
|
||||
templateToUse = variables.fallback;
|
||||
}
|
||||
return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
|
||||
return varName in variables ? variables[varName] : "{" + varName + "}";
|
||||
return templateToUse.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
|
||||
return varName in variables ? variables[varName] : `{${varName}}`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,56 +95,46 @@ const Translator = (function () {
|
||||
|
||||
/**
|
||||
* Load a translation file (json) and remember the data.
|
||||
*
|
||||
* @param {Module} module The module to load the translation file for.
|
||||
* @param {string} file Path of the file we want to load.
|
||||
* @param {boolean} isFallback Flag to indicate fallback translations.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
load(module, file, isFallback, callback) {
|
||||
async load(module, file, isFallback) {
|
||||
Log.log(`${module.name} - Load translation${isFallback ? " fallback" : ""}: ${file}`);
|
||||
|
||||
if (this.translationsFallback[module.name]) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
loadJSON(module.file(file), (json) => {
|
||||
const property = isFallback ? "translationsFallback" : "translations";
|
||||
this[property][module.name] = json;
|
||||
callback();
|
||||
});
|
||||
const json = await loadJSON(module.file(file));
|
||||
const property = isFallback ? "translationsFallback" : "translations";
|
||||
this[property][module.name] = json;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the core translations.
|
||||
*
|
||||
* @param {string} lang The language identifier of the core language.
|
||||
*/
|
||||
loadCoreTranslations: function (lang) {
|
||||
loadCoreTranslations: async function (lang) {
|
||||
if (lang in translations) {
|
||||
Log.log("Loading core translation file: " + translations[lang]);
|
||||
loadJSON(translations[lang], (translations) => {
|
||||
this.coreTranslations = translations;
|
||||
});
|
||||
Log.log(`Loading core translation file: ${translations[lang]}`);
|
||||
this.coreTranslations = await loadJSON(translations[lang]);
|
||||
} else {
|
||||
Log.log("Configured language not found in core translations.");
|
||||
}
|
||||
|
||||
this.loadCoreTranslationsFallback();
|
||||
await this.loadCoreTranslationsFallback();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the core translations fallback.
|
||||
* Load the core translations' fallback.
|
||||
* The first language defined in translations.js will be used.
|
||||
*/
|
||||
loadCoreTranslationsFallback: function () {
|
||||
loadCoreTranslationsFallback: async function () {
|
||||
let first = Object.keys(translations)[0];
|
||||
if (first) {
|
||||
Log.log("Loading core translation fallback file: " + translations[first]);
|
||||
loadJSON(translations[first], (translations) => {
|
||||
this.coreTranslationsFallback = translations;
|
||||
});
|
||||
Log.log(`Loading core translation fallback file: ${translations[first]}`);
|
||||
this.coreTranslationsFallback = await loadJSON(translations[first]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -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: {
|
||||
|
@@ -35,7 +35,8 @@ Module.register("alert", {
|
||||
fr: "translations/fr.json",
|
||||
hu: "translations/hu.json",
|
||||
nl: "translations/nl.json",
|
||||
ru: "translations/ru.json"
|
||||
ru: "translations/ru.json",
|
||||
th: "translations/th.json"
|
||||
};
|
||||
},
|
||||
|
||||
@@ -43,7 +44,7 @@ Module.register("alert", {
|
||||
return `templates/${type}.njk`;
|
||||
},
|
||||
|
||||
start() {
|
||||
async start() {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
if (this.config.effect === "slide") {
|
||||
@@ -52,7 +53,7 @@ Module.register("alert", {
|
||||
|
||||
if (this.config.welcome_message) {
|
||||
const message = this.config.welcome_message === true ? this.translate("welcome") : this.config.welcome_message;
|
||||
this.showNotification({ title: this.translate("sysTitle"), message });
|
||||
await this.showNotification({ title: this.translate("sysTitle"), message });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -69,7 +70,7 @@ Module.register("alert", {
|
||||
},
|
||||
|
||||
async showNotification(notification) {
|
||||
const message = await this.renderMessage("notification", notification);
|
||||
const message = await this.renderMessage(notification.templateName || "notification", notification);
|
||||
|
||||
new NotificationFx({
|
||||
message,
|
||||
@@ -90,7 +91,7 @@ Module.register("alert", {
|
||||
this.toggleBlur(true);
|
||||
}
|
||||
|
||||
const message = await this.renderMessage("alert", alert);
|
||||
const message = await this.renderMessage(alert.templateName || "alert", alert);
|
||||
|
||||
// Store alert in this.alerts
|
||||
this.alerts[sender.name] = new NotificationFx({
|
||||
|
@@ -9,13 +9,11 @@
|
||||
*
|
||||
* Copyright 2014, Codrops
|
||||
* https://tympanus.net/codrops/
|
||||
*
|
||||
* @param {object} window The window object
|
||||
*/
|
||||
(function (window) {
|
||||
/**
|
||||
* Extend one object with another one
|
||||
*
|
||||
* @param {object} a The object to extend
|
||||
* @param {object} b The object which extends the other, overwrites existing keys
|
||||
* @returns {object} The merged object
|
||||
@@ -31,7 +29,6 @@
|
||||
|
||||
/**
|
||||
* NotificationFx constructor
|
||||
*
|
||||
* @param {object} options The configuration options
|
||||
* @class
|
||||
*/
|
||||
@@ -80,7 +77,7 @@
|
||||
NotificationFx.prototype._init = function () {
|
||||
// create HTML structure
|
||||
this.ntf = document.createElement("div");
|
||||
this.ntf.className = this.options.al_no + " ns-" + this.options.layout + " ns-effect-" + this.options.effect + " ns-type-" + this.options.type;
|
||||
this.ntf.className = `${this.options.al_no} ns-${this.options.layout} ns-effect-${this.options.effect} ns-type-${this.options.type}`;
|
||||
let strinner = '<div class="ns-box-inner">';
|
||||
strinner += this.options.message;
|
||||
strinner += "</div>";
|
||||
@@ -124,7 +121,6 @@
|
||||
|
||||
/**
|
||||
* Dismiss the notification
|
||||
*
|
||||
* @param {boolean} [close] call the onClose callback at the end
|
||||
*/
|
||||
NotificationFx.prototype.dismiss = function (close = true) {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/* Based on work by https://tympanus.net/codrops/licensing/ */
|
||||
|
||||
.ns-box {
|
||||
background-color: rgba(0, 0, 0, 0.93);
|
||||
background-color: rgb(0 0 0 / 93%);
|
||||
padding: 17px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
@@ -55,15 +55,15 @@
|
||||
|
||||
.ns-effect-flip.ns-show,
|
||||
.ns-effect-flip.ns-hide {
|
||||
animation-name: animFlipFront;
|
||||
animation-name: anim-flip-front;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
.ns-effect-flip.ns-hide {
|
||||
animation-name: animFlipBack;
|
||||
animation-name: anim-flip-back;
|
||||
}
|
||||
|
||||
@keyframes animFlipFront {
|
||||
@keyframes anim-flip-front {
|
||||
0% {
|
||||
transform: perspective(1000px) rotate3d(1, 0, 0, -90deg);
|
||||
}
|
||||
@@ -73,7 +73,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animFlipBack {
|
||||
@keyframes anim-flip-back {
|
||||
0% {
|
||||
transform: perspective(1000px) rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
@@ -85,11 +85,11 @@
|
||||
|
||||
.ns-effect-bouncyflip.ns-show,
|
||||
.ns-effect-bouncyflip.ns-hide {
|
||||
animation-name: flipInX;
|
||||
animation-name: flip-in-x;
|
||||
animation-duration: 0.8s;
|
||||
}
|
||||
|
||||
@keyframes flipInX {
|
||||
@keyframes flip-in-x {
|
||||
0% {
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, -90deg);
|
||||
transition-timing-function: ease-in;
|
||||
@@ -117,11 +117,11 @@
|
||||
}
|
||||
|
||||
.ns-effect-bouncyflip.ns-hide {
|
||||
animation-name: flipInXSimple;
|
||||
animation-name: flip-in-x-simple;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes flipInXSimple {
|
||||
@keyframes flip-in-x-simple {
|
||||
0% {
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, -90deg);
|
||||
transition-timing-function: ease-in;
|
||||
@@ -141,11 +141,11 @@
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-show {
|
||||
animation-name: animLoad;
|
||||
animation-name: anim-load;
|
||||
animation-duration: 1s;
|
||||
}
|
||||
|
||||
@keyframes animLoad {
|
||||
@keyframes anim-load {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale3d(0, 0.3, 1);
|
||||
@@ -158,7 +158,7 @@
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-hide {
|
||||
animation-name: animFade;
|
||||
animation-name: anim-fade;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
@@ -170,15 +170,15 @@
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-show .ns-close {
|
||||
animation-name: animFade;
|
||||
animation-name: anim-fade;
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-show .ns-box-inner {
|
||||
animation-name: animFadeMove;
|
||||
animation-name: anim-fade-move;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
@keyframes animFadeMove {
|
||||
@keyframes anim-fade-move {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 10px, 0);
|
||||
@@ -190,7 +190,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animFade {
|
||||
@keyframes anim-fade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -202,11 +202,11 @@
|
||||
|
||||
.ns-effect-scale.ns-show,
|
||||
.ns-effect-scale.ns-hide {
|
||||
animation-name: animScale;
|
||||
animation-name: anim-scale;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes animScale {
|
||||
@keyframes anim-scale {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 40px, 0) scale3d(0.1, 0.6, 1);
|
||||
@@ -219,168 +219,169 @@
|
||||
}
|
||||
|
||||
.ns-effect-jelly.ns-show {
|
||||
animation-name: animJelly;
|
||||
animation-name: anim-jelly;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
.ns-effect-jelly.ns-hide {
|
||||
animation-name: animFade;
|
||||
animation-name: anim-fade;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes animFade {
|
||||
@keyframes anim-fade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animJelly {
|
||||
@keyframes anim-jelly {
|
||||
0% {
|
||||
transform: matrix3d(0.7, 0, 0, 0, 0, 0.7, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
2.083333% {
|
||||
transform: matrix3d(0.75266, 0, 0, 0, 0, 0.76342, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.7527, 0, 0, 0, 0, 0.7634, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
4.166667% {
|
||||
transform: matrix3d(0.81071, 0, 0, 0, 0, 0.84545, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.8107, 0, 0, 0, 0, 0.8454, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
6.25% {
|
||||
transform: matrix3d(0.86808, 0, 0, 0, 0, 0.9286, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.8681, 0, 0, 0, 0, 0.929, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
8.333333% {
|
||||
transform: matrix3d(0.92038, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9204, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
10.416667% {
|
||||
transform: matrix3d(0.96482, 0, 0, 0, 0, 1.05202, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9648, 0, 0, 0, 0, 1.052, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
12.5% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.08204, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.082, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
14.583333% {
|
||||
transform: matrix3d(1.02563, 0, 0, 0, 0, 1.09149, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0256, 0, 0, 0, 0, 1.0915, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
16.666667% {
|
||||
transform: matrix3d(1.04227, 0, 0, 0, 0, 1.08453, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0423, 0, 0, 0, 0, 1.0845, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
18.75% {
|
||||
transform: matrix3d(1.05102, 0, 0, 0, 0, 1.06666, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.051, 0, 0, 0, 0, 1.0667, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
20.833333% {
|
||||
transform: matrix3d(1.05334, 0, 0, 0, 0, 1.04355, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0533, 0, 0, 0, 0, 1.0436, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
22.916667% {
|
||||
transform: matrix3d(1.05078, 0, 0, 0, 0, 1.02012, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0508, 0, 0, 0, 0, 1.0201, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: matrix3d(1.04487, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0449, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
27.083333% {
|
||||
transform: matrix3d(1.03699, 0, 0, 0, 0, 0.98534, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.037, 0, 0, 0, 0, 0.9853, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
29.166667% {
|
||||
transform: matrix3d(1.02831, 0, 0, 0, 0, 0.97688, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0283, 0, 0, 0, 0, 0.9769, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
31.25% {
|
||||
transform: matrix3d(1.01973, 0, 0, 0, 0, 0.97422, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0197, 0, 0, 0, 0, 0.9742, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
33.333333% {
|
||||
transform: matrix3d(1.01191, 0, 0, 0, 0, 0.97618, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0119, 0, 0, 0, 0, 0.9762, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
35.416667% {
|
||||
transform: matrix3d(1.00526, 0, 0, 0, 0, 0.98122, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0053, 0, 0, 0, 0, 0.9812, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
37.5% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.98773, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.9877, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
39.583333% {
|
||||
transform: matrix3d(0.99617, 0, 0, 0, 0, 0.99433, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9962, 0, 0, 0, 0, 0.9943, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
41.666667% {
|
||||
transform: matrix3d(0.99368, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9937, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
43.75% {
|
||||
transform: matrix3d(0.99237, 0, 0, 0, 0, 1.00413, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9924, 0, 0, 0, 0, 1.0041, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
45.833333% {
|
||||
transform: matrix3d(0.99202, 0, 0, 0, 0, 1.00651, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.992, 0, 0, 0, 0, 1.0065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
47.916667% {
|
||||
transform: matrix3d(0.99241, 0, 0, 0, 0, 1.00726, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9924, 0, 0, 0, 0, 1.0073, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: matrix3d(0.99329, 0, 0, 0, 0, 1.00671, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9933, 0, 0, 0, 0, 1.0067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
52.083333% {
|
||||
transform: matrix3d(0.99447, 0, 0, 0, 0, 1.00529, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9945, 0, 0, 0, 0, 1.0053, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
54.166667% {
|
||||
transform: matrix3d(0.99577, 0, 0, 0, 0, 1.00346, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9958, 0, 0, 0, 0, 1.0035, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
56.25% {
|
||||
transform: matrix3d(0.99705, 0, 0, 0, 0, 1.0016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.997, 0, 0, 0, 0, 1.002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
58.333333% {
|
||||
transform: matrix3d(0.99822, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9982, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
60.416667% {
|
||||
transform: matrix3d(0.99921, 0, 0, 0, 0, 0.99884, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9992, 0, 0, 0, 0, 0.9989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
62.5% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.99816, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.9982, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
64.583333% {
|
||||
transform: matrix3d(1.00057, 0, 0, 0, 0, 0.99795, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0006, 0, 0, 0, 0, 0.998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
66.666667% {
|
||||
transform: matrix3d(1.00095, 0, 0, 0, 0, 0.99811, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.001, 0, 0, 0, 0, 0.9981, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
68.75% {
|
||||
transform: matrix3d(1.00114, 0, 0, 0, 0, 0.99851, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0011, 0, 0, 0, 0, 0.9985, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
70.833333% {
|
||||
transform: matrix3d(1.00119, 0, 0, 0, 0, 0.99903, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0012, 0, 0, 0, 0, 0.999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
72.916667% {
|
||||
transform: matrix3d(1.00114, 0, 0, 0, 0, 0.99955, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0011, 0, 0, 0, 0, 0.9996, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
75% {
|
||||
@@ -388,47 +389,47 @@
|
||||
}
|
||||
|
||||
77.083333% {
|
||||
transform: matrix3d(1.00083, 0, 0, 0, 0, 1.00033, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0008, 0, 0, 0, 0, 1.0003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
79.166667% {
|
||||
transform: matrix3d(1.00063, 0, 0, 0, 0, 1.00052, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0006, 0, 0, 0, 0, 1.0005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
81.25% {
|
||||
transform: matrix3d(1.00044, 0, 0, 0, 0, 1.00058, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0004, 0, 0, 0, 0, 1.0006, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
83.333333% {
|
||||
transform: matrix3d(1.00027, 0, 0, 0, 0, 1.00053, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0003, 0, 0, 0, 0, 1.0005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
85.416667% {
|
||||
transform: matrix3d(1.00012, 0, 0, 0, 0, 1.00042, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0001, 0, 0, 0, 0, 1.0004, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
87.5% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.00027, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.0003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
89.583333% {
|
||||
transform: matrix3d(0.99991, 0, 0, 0, 0, 1.00013, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9999, 0, 0, 0, 0, 1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
91.666667% {
|
||||
transform: matrix3d(0.99986, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
93.75% {
|
||||
transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99991, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
95.833333% {
|
||||
transform: matrix3d(0.99982, 0, 0, 0, 0, 0.99985, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
97.916667% {
|
||||
transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99984, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
@@ -437,162 +438,162 @@
|
||||
}
|
||||
|
||||
.ns-effect-slide-left.ns-show {
|
||||
animation-name: animSlideElasticLeft;
|
||||
animation-name: anim-slide-elastic-left;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
@keyframes animSlideElasticLeft {
|
||||
@keyframes anim-slide-elastic-left {
|
||||
0% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1000, 0, 0, 1);
|
||||
}
|
||||
|
||||
1.666667% {
|
||||
transform: matrix3d(1.92933, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -739.26805, 0, 0, 1);
|
||||
transform: matrix3d(1.9293, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -739.2681, 0, 0, 1);
|
||||
}
|
||||
|
||||
3.333333% {
|
||||
transform: matrix3d(1.96989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.82545, 0, 0, 1);
|
||||
transform: matrix3d(1.9699, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.8255, 0, 0, 1);
|
||||
}
|
||||
|
||||
5% {
|
||||
transform: matrix3d(1.70901, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.26115, 0, 0, 1);
|
||||
transform: matrix3d(1.709, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.2612, 0, 0, 1);
|
||||
}
|
||||
|
||||
6.666667% {
|
||||
transform: matrix3d(1.4235, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -218.3238, 0, 0, 1);
|
||||
transform: matrix3d(1.424, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -218.324, 0, 0, 1);
|
||||
}
|
||||
|
||||
8.333333% {
|
||||
transform: matrix3d(1.21065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.29848, 0, 0, 1);
|
||||
transform: matrix3d(1.2107, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.2985, 0, 0, 1);
|
||||
}
|
||||
|
||||
10% {
|
||||
transform: matrix3d(1.08167, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.59273, 0, 0, 1);
|
||||
transform: matrix3d(1.0817, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.5927, 0, 0, 1);
|
||||
}
|
||||
|
||||
11.666667% {
|
||||
transform: matrix3d(1.0165, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -14.72371, 0, 0, 1);
|
||||
transform: matrix3d(1.017, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -14.7237, 0, 0, 1);
|
||||
}
|
||||
|
||||
13.333333% {
|
||||
transform: matrix3d(0.99057, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.12794, 0, 0, 1);
|
||||
transform: matrix3d(0.9906, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.1279, 0, 0, 1);
|
||||
}
|
||||
|
||||
15% {
|
||||
transform: matrix3d(0.98478, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.86339, 0, 0, 1);
|
||||
transform: matrix3d(0.9848, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.8634, 0, 0, 1);
|
||||
}
|
||||
|
||||
16.666667% {
|
||||
transform: matrix3d(0.98719, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.40503, 0, 0, 1);
|
||||
transform: matrix3d(0.9872, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.405, 0, 0, 1);
|
||||
}
|
||||
|
||||
18.333333% {
|
||||
transform: matrix3d(0.9916, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.75275, 0, 0, 1);
|
||||
transform: matrix3d(0.992, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.7528, 0, 0, 1);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: matrix3d(0.99541, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.10141, 0, 0, 1);
|
||||
transform: matrix3d(0.9954, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.1014, 0, 0, 1);
|
||||
}
|
||||
|
||||
21.666667% {
|
||||
transform: matrix3d(0.99795, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 23.98271, 0, 0, 1);
|
||||
transform: matrix3d(0.998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 23.9827, 0, 0, 1);
|
||||
}
|
||||
|
||||
23.333333% {
|
||||
transform: matrix3d(0.99936, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.40752, 0, 0, 1);
|
||||
transform: matrix3d(0.9994, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.4075, 0, 0, 1);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.99558, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.9956, 0, 0, 1);
|
||||
}
|
||||
|
||||
26.666667% {
|
||||
transform: matrix3d(1.00021, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.08575, 0, 0, 1);
|
||||
transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.0858, 0, 0, 1);
|
||||
}
|
||||
|
||||
28.333333% {
|
||||
transform: matrix3d(1.00022, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.82507, 0, 0, 1);
|
||||
transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.8251, 0, 0, 1);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: matrix3d(1.00016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.23737, 0, 0, 1);
|
||||
transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.2374, 0, 0, 1);
|
||||
}
|
||||
|
||||
31.666667% {
|
||||
transform: matrix3d(1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3.27389, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3.2739, 0, 0, 1);
|
||||
}
|
||||
|
||||
33.333333% {
|
||||
transform: matrix3d(1.00005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.84893, 0, 0, 1);
|
||||
transform: matrix3d(1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.8489, 0, 0, 1);
|
||||
}
|
||||
|
||||
35% {
|
||||
transform: matrix3d(1.00002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.86364, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.8636, 0, 0, 1);
|
||||
}
|
||||
|
||||
36.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.22079, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.2208, 0, 0, 1);
|
||||
}
|
||||
|
||||
38.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16687, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1669, 0, 0, 1);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.37284, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.3728, 0, 0, 1);
|
||||
}
|
||||
|
||||
41.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.45594, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4559, 0, 0, 1);
|
||||
}
|
||||
|
||||
43.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.46116, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4612, 0, 0, 1);
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4214, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.421, 0, 0, 1);
|
||||
}
|
||||
|
||||
46.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.35963, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.3596, 0, 0, 1);
|
||||
}
|
||||
|
||||
48.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.29103, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.291, 0, 0, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.22487, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.2249, 0, 0, 1);
|
||||
}
|
||||
|
||||
51.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16624, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1662, 0, 0, 1);
|
||||
}
|
||||
|
||||
53.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.11734, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1173, 0, 0, 1);
|
||||
}
|
||||
|
||||
55% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.07854, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0785, 0, 0, 1);
|
||||
}
|
||||
|
||||
56.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.04909, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0491, 0, 0, 1);
|
||||
}
|
||||
|
||||
58.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.02773, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0277, 0, 0, 1);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.01295, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.013, 0, 0, 1);
|
||||
}
|
||||
|
||||
61.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00331, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0033, 0, 0, 1);
|
||||
}
|
||||
|
||||
63.333333% {
|
||||
@@ -600,67 +601,67 @@
|
||||
}
|
||||
|
||||
65% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00559, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0056, 0, 0, 1);
|
||||
}
|
||||
|
||||
66.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00684, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0068, 0, 0, 1);
|
||||
}
|
||||
|
||||
68.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00692, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0069, 0, 0, 1);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00632, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0063, 0, 0, 1);
|
||||
}
|
||||
|
||||
71.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00539, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0054, 0, 0, 1);
|
||||
}
|
||||
|
||||
73.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00436, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0044, 0, 0, 1);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00337, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0034, 0, 0, 1);
|
||||
}
|
||||
|
||||
76.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00249, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0025, 0, 0, 1);
|
||||
}
|
||||
|
||||
78.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00176, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0018, 0, 0, 1);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00118, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0012, 0, 0, 1);
|
||||
}
|
||||
|
||||
81.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00074, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0007, 0, 0, 1);
|
||||
}
|
||||
|
||||
83.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00042, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0004, 0, 0, 1);
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00019, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0002, 0, 0, 1);
|
||||
}
|
||||
|
||||
86.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00005, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
88.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00004, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
91.666667% {
|
||||
@@ -672,15 +673,15 @@
|
||||
}
|
||||
|
||||
95% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00009, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
96.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
98.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00007, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
@@ -689,11 +690,11 @@
|
||||
}
|
||||
|
||||
.ns-effect-slide-left.ns-hide {
|
||||
animation-name: animSlideLeft;
|
||||
animation-name: anim-slide-left;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes animSlideLeft {
|
||||
@keyframes anim-slide-left {
|
||||
0% {
|
||||
transform: translate3d(-30px, 0, 0) translate3d(-100%, 0, 0);
|
||||
}
|
||||
@@ -704,10 +705,10 @@
|
||||
}
|
||||
|
||||
.ns-effect-slide-right.ns-show {
|
||||
animation: animSlideElasticRight 2000ms linear both;
|
||||
animation: anim-slide-elastic-right 2000ms linear both;
|
||||
}
|
||||
|
||||
@keyframes animSlideElasticRight {
|
||||
@keyframes anim-slide-elastic-right {
|
||||
0% {
|
||||
transform: matrix3d(2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1000, 0, 0, 1);
|
||||
}
|
||||
@@ -786,11 +787,11 @@
|
||||
}
|
||||
|
||||
.ns-effect-slide-right.ns-hide {
|
||||
animation-name: animSlideRight;
|
||||
animation-name: anim-slide-right;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes animSlideRight {
|
||||
@keyframes anim-slide-right {
|
||||
0% {
|
||||
transform: translate3d(30px, 0, 0) translate3d(100%, 0, 0);
|
||||
}
|
||||
@@ -801,10 +802,10 @@
|
||||
}
|
||||
|
||||
.ns-effect-slide-center.ns-show {
|
||||
animation: animSlideElasticCenter 2000ms linear both;
|
||||
animation: anim-slide-elastic-center 2000ms linear both;
|
||||
}
|
||||
|
||||
@keyframes animSlideElasticCenter {
|
||||
@keyframes anim-slide-elastic-center {
|
||||
0% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0, 0, -300, 0, 1);
|
||||
}
|
||||
@@ -883,11 +884,11 @@
|
||||
}
|
||||
|
||||
.ns-effect-slide-center.ns-hide {
|
||||
animation-name: animSlideCenter;
|
||||
animation-name: anim-slide-center;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes animSlideCenter {
|
||||
@keyframes anim-slide-center {
|
||||
0% {
|
||||
transform: translate3d(0, -30px, 0) translate3d(0, -100%, 0);
|
||||
}
|
||||
@@ -899,11 +900,11 @@
|
||||
|
||||
.ns-effect-genie.ns-show,
|
||||
.ns-effect-genie.ns-hide {
|
||||
animation-name: animGenie;
|
||||
animation-name: anim-genie;
|
||||
animation-duration: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes animGenie {
|
||||
@keyframes anim-genie {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, calc(200% + 30px), 0) scale3d(0, 1, 1);
|
||||
|
@@ -1,18 +1,20 @@
|
||||
{% if imageUrl or imageFA %}
|
||||
{% set imageHeight = imageHeight if imageHeight else "80px" %}
|
||||
{% if imageUrl %}
|
||||
<img src="{{ imageUrl }}" height="{{ imageHeight }}" style="margin-bottom: 10px;"/>
|
||||
{% else %}
|
||||
<span class="bright fas fa-{{ imageFA }}" style='margin-bottom: 10px; font-size: {{ imageHeight }};'/></span>
|
||||
{% endif %}
|
||||
<br/>
|
||||
{% set imageHeight = imageHeight if imageHeight else "80px" %}
|
||||
{% if imageUrl %}
|
||||
<img src="{{ imageUrl }}"
|
||||
height="{{ imageHeight }}"
|
||||
style="margin-bottom: 10px" />
|
||||
{% else %}
|
||||
<span class="bright fas fa-{{ imageFA }}"
|
||||
style="margin-bottom: 10px;
|
||||
font-size: {{ imageHeight }}"></span>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title }}</span>
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}
|
||||
<br/>
|
||||
{% endif %}
|
||||
<span class="light bright small">{{ message }}</span>
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% endif %}
|
||||
|
@@ -1,9 +1,7 @@
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title }}</span>
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}
|
||||
<br/>
|
||||
{% endif %}
|
||||
<span class="light bright small">{{ message }}</span>
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% endif %}
|
||||
|
4
modules/default/alert/translations/th.json
Normal file
4
modules/default/alert/translations/th.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sysTitle": "การแจ้งเตือน MagicMirror²",
|
||||
"welcome": "ยินดีต้อนรับ การเริ่มต้นสำเร็จแล้ว!"
|
||||
}
|
@@ -14,6 +14,7 @@
|
||||
.calendar .title {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.calendar .time {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* global cloneObject */
|
||||
/* global CalendarUtils, cloneObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Calendar
|
||||
@@ -12,6 +12,7 @@ Module.register("calendar", {
|
||||
maximumEntries: 10, // Total Maximum Entries
|
||||
maximumNumberOfDays: 365,
|
||||
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
||||
pastDaysCount: 0,
|
||||
displaySymbol: true,
|
||||
defaultSymbol: "calendar-alt", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
||||
defaultSymbolClassName: "fas fa-fw fa-",
|
||||
@@ -20,11 +21,11 @@ Module.register("calendar", {
|
||||
defaultRepeatingCountTitle: "",
|
||||
maxTitleLength: 25,
|
||||
maxLocationTitleLength: 25,
|
||||
wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
|
||||
wrapEvents: false, // Wrap events to multiple lines breaking at maxTitleLength
|
||||
wrapLocationEvents: false,
|
||||
maxTitleLines: 3,
|
||||
maxEventTitleLines: 3,
|
||||
fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
|
||||
fetchInterval: 60 * 60 * 1000, // Update every hour
|
||||
animationSpeed: 2000,
|
||||
fade: true,
|
||||
urgency: 7,
|
||||
@@ -38,10 +39,10 @@ Module.register("calendar", {
|
||||
hidePrivate: false,
|
||||
hideOngoing: false,
|
||||
hideTime: false,
|
||||
hideDuplicates: true,
|
||||
showTimeToday: false,
|
||||
colored: false,
|
||||
coloredSymbolOnly: 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: [
|
||||
{
|
||||
@@ -61,7 +62,13 @@ Module.register("calendar", {
|
||||
sliceMultiDayEvents: false,
|
||||
broadcastPastEvents: false,
|
||||
nextDaysRelative: false,
|
||||
selfSignedCert: false
|
||||
selfSignedCert: false,
|
||||
coloredText: false,
|
||||
coloredBorder: false,
|
||||
coloredSymbol: false,
|
||||
coloredBackground: false,
|
||||
limitDaysNeverSkip: false,
|
||||
flipDateHeaderTitle: false
|
||||
},
|
||||
|
||||
requiresVersion: "2.1.0",
|
||||
@@ -73,7 +80,7 @@ Module.register("calendar", {
|
||||
|
||||
// Define required scripts.
|
||||
getScripts: function () {
|
||||
return ["moment.js"];
|
||||
return ["calendarutils.js", "moment.js"];
|
||||
},
|
||||
|
||||
// Define required translations.
|
||||
@@ -86,10 +93,23 @@ Module.register("calendar", {
|
||||
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
if (this.config.colored) {
|
||||
Log.warn("Your are using the deprecated config values 'colored'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
||||
this.config.coloredText = true;
|
||||
this.config.coloredSymbol = true;
|
||||
}
|
||||
if (this.config.coloredSymbolOnly) {
|
||||
Log.warn("Your are using the deprecated config values 'coloredSymbolOnly'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
||||
this.config.coloredText = false;
|
||||
this.config.coloredSymbol = true;
|
||||
}
|
||||
|
||||
// Set locale.
|
||||
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
|
||||
moment.updateLocale(config.language, CalendarUtils.getLocaleSpecification(config.timeFormat));
|
||||
|
||||
// clear data holder before start
|
||||
this.calendarData = {};
|
||||
@@ -103,6 +123,7 @@ Module.register("calendar", {
|
||||
const calendarConfig = {
|
||||
maximumEntries: calendar.maximumEntries,
|
||||
maximumNumberOfDays: calendar.maximumNumberOfDays,
|
||||
pastDaysCount: calendar.pastDaysCount,
|
||||
broadcastPastEvents: calendar.broadcastPastEvents,
|
||||
selfSignedCert: calendar.selfSignedCert
|
||||
};
|
||||
@@ -131,6 +152,17 @@ Module.register("calendar", {
|
||||
// fetcher till cycle
|
||||
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
});
|
||||
|
||||
// Refresh the DOM every minute if needed: When using relative date format for events that start
|
||||
// or end in less than an hour, the date shows minute granularity and we want to keep that accurate.
|
||||
setTimeout(
|
||||
() => {
|
||||
setInterval(() => {
|
||||
this.updateDom(1);
|
||||
}, ONE_MINUTE);
|
||||
},
|
||||
ONE_MINUTE - (new Date() % ONE_MINUTE)
|
||||
);
|
||||
},
|
||||
|
||||
// Override socket notification handler.
|
||||
@@ -175,13 +207,13 @@ Module.register("calendar", {
|
||||
|
||||
if (this.error) {
|
||||
wrapper.innerHTML = this.error;
|
||||
wrapper.className = this.config.tableClass + " dimmed";
|
||||
wrapper.className = `${this.config.tableClass} dimmed`;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (events.length === 0) {
|
||||
wrapper.innerHTML = this.loaded ? this.translate("EMPTY") : this.translate("LOADING");
|
||||
wrapper.className = this.config.tableClass + " dimmed";
|
||||
wrapper.className = `${this.config.tableClass} dimmed`;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@@ -204,9 +236,12 @@ Module.register("calendar", {
|
||||
if (this.config.timeFormat === "dateheaders") {
|
||||
if (lastSeenDate !== dateAsString) {
|
||||
const dateRow = document.createElement("tr");
|
||||
dateRow.className = "normal";
|
||||
dateRow.className = "dateheader normal";
|
||||
if (event.today) dateRow.className += " today";
|
||||
else if (event.dayBeforeYesterday) dateRow.className += " dayBeforeYesterday";
|
||||
else if (event.yesterday) dateRow.className += " yesterday";
|
||||
else if (event.tomorrow) dateRow.className += " tomorrow";
|
||||
else if (event.dayAfterTomorrow) dateRow.className += " dayAfterTomorrow";
|
||||
|
||||
const dateCell = document.createElement("td");
|
||||
dateCell.colSpan = "3";
|
||||
@@ -227,23 +262,34 @@ Module.register("calendar", {
|
||||
|
||||
const eventWrapper = document.createElement("tr");
|
||||
|
||||
if (this.config.colored && !this.config.coloredSymbolOnly) {
|
||||
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||
if (this.config.coloredText) {
|
||||
eventWrapper.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
|
||||
}
|
||||
|
||||
eventWrapper.className = "normal event";
|
||||
if (this.config.coloredBackground) {
|
||||
eventWrapper.style.backgroundColor = this.colorForUrl(event.url, true);
|
||||
}
|
||||
|
||||
if (this.config.coloredBorder) {
|
||||
eventWrapper.style.borderColor = this.colorForUrl(event.url, false);
|
||||
}
|
||||
|
||||
eventWrapper.className = "event-wrapper normal event";
|
||||
if (event.today) eventWrapper.className += " today";
|
||||
else if (event.dayBeforeYesterday) eventWrapper.className += " dayBeforeYesterday";
|
||||
else if (event.yesterday) eventWrapper.className += " yesterday";
|
||||
else if (event.tomorrow) eventWrapper.className += " tomorrow";
|
||||
else if (event.dayAfterTomorrow) eventWrapper.className += " dayAfterTomorrow";
|
||||
|
||||
const symbolWrapper = document.createElement("td");
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
if (this.config.colored && this.config.coloredSymbolOnly) {
|
||||
symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||
if (this.config.coloredSymbol) {
|
||||
symbolWrapper.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
|
||||
}
|
||||
|
||||
const symbolClass = this.symbolClassForUrl(event.url);
|
||||
symbolWrapper.className = "symbol align-right " + symbolClass;
|
||||
symbolWrapper.className = `symbol align-right ${symbolClass}`;
|
||||
|
||||
const symbols = this.symbolsForEvent(event);
|
||||
symbols.forEach((s, index) => {
|
||||
@@ -271,60 +317,67 @@ Module.register("calendar", {
|
||||
const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
|
||||
yearDiff = thisYear - event.firstYear;
|
||||
|
||||
repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
|
||||
repeatingCountTitle = `, ${yearDiff} ${repeatingCountTitle}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
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.coloredSymbolOnly) {
|
||||
eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
if (this.config.coloredText) {
|
||||
eventWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
|
||||
titleWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
|
||||
}
|
||||
if (this.config.displaySymbol) {
|
||||
symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
if (this.config.displaySymbol && this.config.coloredSymbol) {
|
||||
symbolWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (typeof this.config.customEvents[ev].eventClass !== "undefined" && this.config.customEvents[ev].eventClass !== "") {
|
||||
eventWrapper.className += ` ${this.config.customEvents[ev].eventClass}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
|
||||
const transformedTitle = CalendarUtils.titleTransform(event.title, this.config.titleReplace);
|
||||
titleWrapper.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines) + repeatingCountTitle;
|
||||
|
||||
const titleClass = this.titleClassForUrl(event.url);
|
||||
|
||||
if (!this.config.colored) {
|
||||
titleWrapper.className = "title bright " + titleClass;
|
||||
if (!this.config.coloredText) {
|
||||
titleWrapper.className = `title bright ${titleClass}`;
|
||||
} else {
|
||||
titleWrapper.className = "title " + titleClass;
|
||||
titleWrapper.className = `title ${titleClass}`;
|
||||
}
|
||||
|
||||
if (this.config.timeFormat === "dateheaders") {
|
||||
if (this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
|
||||
|
||||
if (event.fullDayEvent) {
|
||||
titleWrapper.colSpan = "2";
|
||||
titleWrapper.classList.add("align-left");
|
||||
} else {
|
||||
const timeWrapper = document.createElement("td");
|
||||
timeWrapper.className = "time light align-left " + this.timeClassForUrl(event.url);
|
||||
timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`;
|
||||
timeWrapper.style.paddingLeft = "2px";
|
||||
timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left";
|
||||
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
|
||||
|
||||
// Add endDate to dataheaders if showEnd is enabled
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += " - " + moment(event.endDate, "x").format("LT");
|
||||
timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(moment(event.endDate, "x").format("LT"))}`;
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
titleWrapper.classList.add("align-right");
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right");
|
||||
}
|
||||
if (!this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
|
||||
} else {
|
||||
const timeWrapper = document.createElement("td");
|
||||
|
||||
@@ -333,38 +386,40 @@ Module.register("calendar", {
|
||||
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
// Use dateFormat
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
// Add end time if showEnd
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||
timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||
}
|
||||
// For full day events we use the fullDayEventDateFormat
|
||||
if (event.fullDayEvent) {
|
||||
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||
event.endDate -= ONE_SECOND;
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
} else if (this.config.getRelative > 0 && event.startDate < now) {
|
||||
// Ongoing and getRelative is set
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
|
||||
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||
})
|
||||
);
|
||||
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * ONE_DAY) {
|
||||
// Within urgency days
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
if (event.fullDayEvent && this.config.nextDaysRelative) {
|
||||
// Full days events within the next two days
|
||||
if (event.today) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
|
||||
} else if (event.yesterday) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
|
||||
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,12 +428,12 @@ Module.register("calendar", {
|
||||
if (event.startDate >= now || (event.fullDayEvent && event.today)) {
|
||||
// Use relative time
|
||||
if (!this.config.hideTime && !event.fullDayEvent) {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
||||
moment(event.startDate, "x").calendar(null, {
|
||||
sameDay: this.config.showTimeToday ? "LT" : "[" + this.translate("TODAY") + "]",
|
||||
nextDay: "[" + this.translate("TOMORROW") + "]",
|
||||
sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
|
||||
nextDay: `[${this.translate("TOMORROW")}]`,
|
||||
nextWeek: "dddd",
|
||||
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
|
||||
})
|
||||
@@ -387,56 +442,78 @@ Module.register("calendar", {
|
||||
if (event.fullDayEvent) {
|
||||
// Full days events within the next two days
|
||||
if (event.today) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
|
||||
} else if (event.dayBeforeYesterday) {
|
||||
if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYBEFOREYESTERDAY"));
|
||||
}
|
||||
} else if (event.yesterday) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
|
||||
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
}
|
||||
} else if (event.startDate - now < this.config.getRelative * ONE_HOUR) {
|
||||
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
} else {
|
||||
// Ongoing event
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
|
||||
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
||||
timeWrapper.className = `time light ${this.timeClassForUrl(event.url)}`;
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
}
|
||||
|
||||
wrapper.appendChild(eventWrapper);
|
||||
|
||||
// Create fade effect.
|
||||
if (index >= startFade) {
|
||||
currentFadeStep = index - startFade;
|
||||
eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
|
||||
}
|
||||
wrapper.appendChild(eventWrapper);
|
||||
|
||||
if (this.config.showLocation) {
|
||||
if (event.location !== false) {
|
||||
const locationRow = document.createElement("tr");
|
||||
locationRow.className = "normal xsmall light";
|
||||
locationRow.className = "event-wrapper-location normal xsmall light";
|
||||
if (event.today) locationRow.className += " today";
|
||||
else if (event.dayBeforeYesterday) locationRow.className += " dayBeforeYesterday";
|
||||
else if (event.yesterday) locationRow.className += " yesterday";
|
||||
else if (event.tomorrow) locationRow.className += " tomorrow";
|
||||
else if (event.dayAfterTomorrow) locationRow.className += " dayAfterTomorrow";
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
const symbolCell = document.createElement("td");
|
||||
locationRow.appendChild(symbolCell);
|
||||
}
|
||||
|
||||
if (this.config.coloredText) {
|
||||
locationRow.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
|
||||
}
|
||||
|
||||
if (this.config.coloredBackground) {
|
||||
locationRow.style.backgroundColor = this.colorForUrl(event.url, true);
|
||||
}
|
||||
|
||||
if (this.config.coloredBorder) {
|
||||
locationRow.style.borderColor = this.colorForUrl(event.url, false);
|
||||
}
|
||||
|
||||
const descCell = document.createElement("td");
|
||||
descCell.className = "location";
|
||||
descCell.colSpan = "2";
|
||||
descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines);
|
||||
|
||||
const transformedTitle = CalendarUtils.titleTransform(event.location, this.config.locationTitleReplace);
|
||||
descCell.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxLocationTitleLength, this.config.wrapLocationEvents, this.config.maxEventTitleLines);
|
||||
locationRow.appendChild(descCell);
|
||||
|
||||
wrapper.appendChild(locationRow);
|
||||
@@ -452,31 +529,8 @@ Module.register("calendar", {
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
/**
|
||||
* This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
|
||||
* corresponding timeformat to be used in the calendar display. If no number is given (or otherwise invalid input)
|
||||
* it will a localeSpecification object with the system locale time format.
|
||||
*
|
||||
* @param {number} timeFormat Specifies either 12 or 24 hour time format
|
||||
* @returns {moment.LocaleSpecification} formatted time
|
||||
*/
|
||||
getLocaleSpecification: function (timeFormat) {
|
||||
switch (timeFormat) {
|
||||
case 12: {
|
||||
return { longDateFormat: { LT: "h:mm A" } };
|
||||
}
|
||||
case 24: {
|
||||
return { longDateFormat: { LT: "HH:mm" } };
|
||||
}
|
||||
default: {
|
||||
return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if this config contains the calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {boolean} True if the calendar config contains the url, False otherwise
|
||||
*/
|
||||
@@ -492,7 +546,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Creates the sorted list of all events.
|
||||
*
|
||||
* @param {boolean} limitNumberOfEntries Whether to filter returned events for display.
|
||||
* @returns {object[]} Array with events.
|
||||
*/
|
||||
@@ -510,6 +563,7 @@ Module.register("calendar", {
|
||||
for (const calendarUrl in this.calendarData) {
|
||||
const calendar = this.calendarData[calendarUrl];
|
||||
let remainingEntries = this.maximumEntriesForUrl(calendarUrl);
|
||||
let maxPastDaysCompare = now - this.maximumPastDaysForUrl(calendarUrl) * ONE_DAY;
|
||||
for (const e in calendar) {
|
||||
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
||||
|
||||
@@ -518,22 +572,26 @@ Module.register("calendar", {
|
||||
continue;
|
||||
}
|
||||
if (limitNumberOfEntries) {
|
||||
if (event.endDate < now) {
|
||||
if (event.endDate < maxPastDaysCompare) {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
event.yesterday = event.startDate >= today - ONE_DAY && event.startDate < today;
|
||||
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
|
||||
event.dayAfterTomorrow = !event.tomorrow && event.startDate >= today + ONE_DAY * 2 && event.startDate < today + 3 * ONE_DAY;
|
||||
|
||||
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
|
||||
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
|
||||
@@ -548,7 +606,7 @@ Module.register("calendar", {
|
||||
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + ONE_DAY;
|
||||
thisEvent.tomorrow = !thisEvent.today && thisEvent.startDate >= today + ONE_DAY && thisEvent.startDate < today + 2 * ONE_DAY;
|
||||
thisEvent.endDate = midnight;
|
||||
thisEvent.title += " (" + count + "/" + maxCount + ")";
|
||||
thisEvent.title += ` (${count}/${maxCount})`;
|
||||
splitEvents.push(thisEvent);
|
||||
|
||||
event.startDate = midnight;
|
||||
@@ -556,7 +614,7 @@ Module.register("calendar", {
|
||||
midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
|
||||
}
|
||||
// Last day
|
||||
event.title += " (" + count + "/" + maxCount + ")";
|
||||
event.title += ` (${count}/${maxCount})`;
|
||||
event.today += event.startDate >= today && event.startDate < today + ONE_DAY;
|
||||
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
|
||||
splitEvents.push(event);
|
||||
@@ -592,7 +650,7 @@ Module.register("calendar", {
|
||||
// check if we already are showing max unique days
|
||||
if (eventDate > lastDate) {
|
||||
// if the only entry in the first day is a full day event that day is not counted as unique
|
||||
if (newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
||||
if (!this.config.limitDaysNeverSkip && newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
||||
days--;
|
||||
}
|
||||
days++;
|
||||
@@ -612,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;
|
||||
}
|
||||
}
|
||||
@@ -621,7 +679,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Requests node helper to add calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url to add
|
||||
* @param {object} auth The authentication method and credentials
|
||||
* @param {object} calendarConfig The config of the specific calendar
|
||||
@@ -633,7 +690,8 @@ Module.register("calendar", {
|
||||
excludedEvents: calendarConfig.excludedEvents || this.config.excludedEvents,
|
||||
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
|
||||
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
|
||||
fetchInterval: this.config.fetchInterval,
|
||||
pastDaysCount: calendarConfig.pastDaysCount || this.config.pastDaysCount,
|
||||
fetchInterval: calendarConfig.fetchInterval || this.config.fetchInterval,
|
||||
symbolClass: calendarConfig.symbolClass,
|
||||
titleClass: calendarConfig.titleClass,
|
||||
timeClass: calendarConfig.timeClass,
|
||||
@@ -645,7 +703,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the symbols for a specific event.
|
||||
*
|
||||
* @param {object} event Event to look for.
|
||||
* @returns {string[]} The symbols
|
||||
*/
|
||||
@@ -665,7 +722,9 @@ Module.register("calendar", {
|
||||
if (typeof ev.symbol !== "undefined" && ev.symbol !== "") {
|
||||
let needle = new RegExp(ev.keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
symbols[0] = ev.symbol;
|
||||
// Get the default prefix for this class name and add to the custom symbol provided
|
||||
const className = this.getCalendarProperty(event.url, "symbolClassName", this.config.defaultSymbolClassName);
|
||||
symbols[0] = className + ev.symbol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -684,7 +743,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the symbolClass for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the symbols of the calendar
|
||||
*/
|
||||
@@ -694,7 +752,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the titleClass for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the title of the calendar
|
||||
*/
|
||||
@@ -704,7 +761,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the timeClass for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the time of the calendar
|
||||
*/
|
||||
@@ -714,7 +770,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the calendar name for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The name of the calendar
|
||||
*/
|
||||
@@ -724,17 +779,16 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the color for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @param {boolean} isBg Determines if we fetch the bgColor or not
|
||||
* @returns {string} The color
|
||||
*/
|
||||
colorForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "color", "#fff");
|
||||
colorForUrl: function (url, isBg) {
|
||||
return this.getCalendarProperty(url, isBg ? "bgColor" : "color", "#fff");
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the count title for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The title
|
||||
*/
|
||||
@@ -744,7 +798,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the maximum entry count for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {number} The maximum entry count
|
||||
*/
|
||||
@@ -752,9 +805,17 @@ Module.register("calendar", {
|
||||
return this.getCalendarProperty(url, "maximumEntries", this.config.maximumEntries);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the maximum count of past days which events of should be displayed for a specific calendar url.
|
||||
* @param {string} url The calendar url
|
||||
* @returns {number} The maximum past days count
|
||||
*/
|
||||
maximumPastDaysForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "pastDaysCount", this.config.pastDaysCount);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to retrieve the property for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @param {string} property The property to look for
|
||||
* @param {string} defaultValue The value if the property is not found
|
||||
@@ -785,98 +846,6 @@ Module.register("calendar", {
|
||||
return !!this.getCalendarProperty(url, property, undefined);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortens a string if it's longer than maxLength and add a ellipsis to the end
|
||||
*
|
||||
* @param {string} string Text string to shorten
|
||||
* @param {number} maxLength The max length of the string
|
||||
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
|
||||
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
|
||||
* @returns {string} The shortened string
|
||||
*/
|
||||
shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
|
||||
if (typeof string !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (wrapEvents === true) {
|
||||
const words = string.split(" ");
|
||||
let temp = "";
|
||||
let currentLine = "";
|
||||
let line = 0;
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const word = words[i];
|
||||
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
|
||||
// max - 1 to account for a space
|
||||
currentLine += word + " ";
|
||||
} else {
|
||||
line++;
|
||||
if (line > maxTitleLines - 1) {
|
||||
if (i < words.length) {
|
||||
currentLine += "…";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentLine.length > 0) {
|
||||
temp += currentLine + "<br>" + word + " ";
|
||||
} else {
|
||||
temp += word + "<br>";
|
||||
}
|
||||
currentLine = "";
|
||||
}
|
||||
}
|
||||
|
||||
return (temp + currentLine).trim();
|
||||
} else {
|
||||
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
|
||||
return string.trim().slice(0, maxLength) + "…";
|
||||
} else {
|
||||
return string.trim();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Capitalize the first letter of a string
|
||||
*
|
||||
* @param {string} string The string to capitalize
|
||||
* @returns {string} The capitalized string
|
||||
*/
|
||||
capFirst: function (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Transforms the title of an event for usage.
|
||||
* Replaces parts of the text as defined in config.titleReplace.
|
||||
* Shortens title based on config.maxTitleLength and config.wrapEvents
|
||||
*
|
||||
* @param {string} title The title to transform.
|
||||
* @param {object} titleReplace Pairs of strings to be replaced in the title
|
||||
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
|
||||
* @param {number} maxTitleLength The max length of the string
|
||||
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
|
||||
* @returns {string} The transformed title.
|
||||
*/
|
||||
titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
|
||||
for (let needle in titleReplace) {
|
||||
const replacement = titleReplace[needle];
|
||||
|
||||
const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
|
||||
if (regParts) {
|
||||
// the parsed pattern is a regexp.
|
||||
needle = new RegExp(regParts[1], regParts[2]);
|
||||
}
|
||||
|
||||
title = title.replace(needle, replacement);
|
||||
}
|
||||
|
||||
title = this.shorten(title, maxTitleLength, wrapEvents, maxTitleLines);
|
||||
return title;
|
||||
},
|
||||
|
||||
/**
|
||||
* Broadcasts the events to all other modules for reuse.
|
||||
* The all events available in one array, sorted on startdate.
|
||||
@@ -886,7 +855,7 @@ Module.register("calendar", {
|
||||
for (const event of eventList) {
|
||||
event.symbol = this.symbolsForEvent(event);
|
||||
event.calendarName = this.calendarNameForUrl(event.url);
|
||||
event.color = this.colorForUrl(event.url);
|
||||
event.color = this.colorForUrl(event.url, false);
|
||||
delete event.url;
|
||||
}
|
||||
|
||||
|
@@ -4,13 +4,12 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const CalendarUtils = require("./calendarutils");
|
||||
|
||||
const https = require("https");
|
||||
const ical = require("node-ical");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
const ical = require("node-ical");
|
||||
const fetch = require("fetch");
|
||||
const digest = require("digest-fetch");
|
||||
const https = require("https");
|
||||
const CalendarFetcherUtils = require("./calendarfetcherutils");
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -38,10 +37,9 @@ 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
|
||||
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`
|
||||
};
|
||||
|
||||
if (selfSignedCert) {
|
||||
@@ -51,18 +49,13 @@ 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 });
|
||||
headers.Authorization = `Bearer ${auth.pass}`;
|
||||
} else {
|
||||
headers.Authorization = "Basic " + Buffer.from(auth.user + ":" + auth.pass).toString("base64");
|
||||
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) => {
|
||||
@@ -70,8 +63,8 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
try {
|
||||
data = ical.parseICS(responseData);
|
||||
Log.debug("parsed data=" + JSON.stringify(data));
|
||||
events = CalendarUtils.filterEvents(data, {
|
||||
Log.debug(`parsed data=${JSON.stringify(data)}`);
|
||||
events = CalendarFetcherUtils.filterEvents(data, {
|
||||
excludedEvents,
|
||||
includePastEvents,
|
||||
maximumEntries,
|
||||
@@ -114,13 +107,12 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
* Broadcast the existing events.
|
||||
*/
|
||||
this.broadcastEvents = function () {
|
||||
Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events.");
|
||||
Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events from ${url}.`);
|
||||
eventsReceivedCallback(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the on success callback
|
||||
*
|
||||
* @param {Function} callback The on success callback.
|
||||
*/
|
||||
this.onReceive = function (callback) {
|
||||
@@ -129,7 +121,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
/**
|
||||
* Sets the on error callback
|
||||
*
|
||||
* @param {Function} callback The on error callback.
|
||||
*/
|
||||
this.onError = function (callback) {
|
||||
@@ -138,7 +129,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
/**
|
||||
* Returns the url of this fetcher.
|
||||
*
|
||||
* @returns {string} The url of this fetcher.
|
||||
*/
|
||||
this.url = function () {
|
||||
@@ -147,7 +137,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
/**
|
||||
* Returns current available events for this fetcher.
|
||||
*
|
||||
* @returns {object[]} The current available events for this fetcher.
|
||||
*/
|
||||
this.events = function () {
|
||||
|
608
modules/default/calendar/calendarfetcherutils.js
Normal file
608
modules/default/calendar/calendarfetcherutils.js
Normal file
@@ -0,0 +1,608 @@
|
||||
/* MagicMirror²
|
||||
* Calendar Fetcher Util Methods
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external Moment
|
||||
*/
|
||||
const path = require("path");
|
||||
const moment = require("moment");
|
||||
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
|
||||
const Log = require("../../../js/logger");
|
||||
|
||||
const CalendarFetcherUtils = {
|
||||
/**
|
||||
* Calculate the time correction, either dst/std or full day in cases where
|
||||
* utc time is day before plus offset
|
||||
* @param {object} event the event which needs adjustment
|
||||
* @param {Date} date the date on which this event happens
|
||||
* @returns {number} the necessary adjustment in hours
|
||||
*/
|
||||
calculateTimezoneAdjustment: function (event, date) {
|
||||
let adjustHours = 0;
|
||||
// if a timezone was specified
|
||||
if (!event.start.tz) {
|
||||
Log.debug(" if no tz, guess based on now");
|
||||
event.start.tz = moment.tz.guess();
|
||||
}
|
||||
Log.debug(`initial tz=${event.start.tz}`);
|
||||
|
||||
// if there is a start date specified
|
||||
if (event.start.tz) {
|
||||
// if this is a windows timezone
|
||||
if (event.start.tz.includes(" ")) {
|
||||
// use the lookup table to get theIANA name as moment and date don't know MS timezones
|
||||
let tz = CalendarFetcherUtils.getIanaTZFromMS(event.start.tz);
|
||||
Log.debug(`corrected TZ=${tz}`);
|
||||
// watch out for unregistered windows timezone names
|
||||
// if we had a successful lookup
|
||||
if (tz) {
|
||||
// change the timezone to the IANA name
|
||||
event.start.tz = tz;
|
||||
// Log.debug("corrected timezone="+event.start.tz)
|
||||
}
|
||||
}
|
||||
Log.debug(`corrected tz=${event.start.tz}`);
|
||||
let current_offset = 0; // offset from TZ string or calculated
|
||||
let mm = 0; // date with tz or offset
|
||||
let start_offset = 0; // utc offset of created with tz
|
||||
// if there is still an offset, lookup failed, use it
|
||||
if (event.start.tz.startsWith("(")) {
|
||||
const regex = /[+|-]\d*:\d*/;
|
||||
const start_offsetString = event.start.tz.match(regex).toString().split(":");
|
||||
let start_offset = parseInt(start_offsetString[0]);
|
||||
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
|
||||
adjustHours = start_offset;
|
||||
Log.debug(`defined offset=${start_offset} hours`);
|
||||
current_offset = start_offset;
|
||||
event.start.tz = "";
|
||||
Log.debug(`ical offset=${current_offset} date=${date}`);
|
||||
mm = moment(date);
|
||||
let x = parseInt(moment(new Date()).utcOffset());
|
||||
Log.debug(`net mins=${current_offset * 60 - x}`);
|
||||
|
||||
mm = mm.add(x - current_offset * 60, "minutes");
|
||||
adjustHours = (current_offset * 60 - x) / 60;
|
||||
event.start = mm.toDate();
|
||||
Log.debug(`adjusted date=${event.start}`);
|
||||
} else {
|
||||
// get the start time in that timezone
|
||||
let es = moment(event.start);
|
||||
// check for start date prior to start of daylight changing date
|
||||
if (es.format("YYYY") < 2007) {
|
||||
es.set("year", 2013); // if so, use a closer date
|
||||
}
|
||||
Log.debug(`start date/time=${es.toDate()}`);
|
||||
start_offset = moment.tz(es, event.start.tz).utcOffset();
|
||||
Log.debug(`start offset=${start_offset}`);
|
||||
|
||||
Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`);
|
||||
|
||||
// get the specified date in that timezone
|
||||
mm = moment.tz(moment(date), event.start.tz);
|
||||
Log.debug(`event date=${mm.toDate()}`);
|
||||
current_offset = mm.utcOffset();
|
||||
}
|
||||
Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`);
|
||||
|
||||
// if the offset is greater than 0, east of london
|
||||
if (current_offset !== start_offset) {
|
||||
// big offset
|
||||
Log.debug("offset");
|
||||
let h = parseInt(mm.format("H"));
|
||||
// check if the event time is less than the offset
|
||||
if (h > 0 && h < Math.abs(current_offset) / 60) {
|
||||
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
|
||||
// we need to fix that
|
||||
//adjustHours = 24;
|
||||
// Log.debug("adjusting date")
|
||||
}
|
||||
//-300 > -240
|
||||
//if (Math.abs(current_offset) > Math.abs(start_offset)){
|
||||
if (current_offset > start_offset) {
|
||||
adjustHours -= 1;
|
||||
Log.debug("adjust down 1 hour dst change");
|
||||
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
|
||||
} else if (current_offset < start_offset) {
|
||||
adjustHours += 1;
|
||||
Log.debug("adjust up 1 hour dst change");
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.debug(`adjustHours=${adjustHours}`);
|
||||
return adjustHours;
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter the events from ical according to the given config
|
||||
* @param {object} data the calendar data from ical
|
||||
* @param {object} config The configuration object
|
||||
* @returns {string[]} the filtered events
|
||||
*/
|
||||
filterEvents: function (data, config) {
|
||||
const newEvents = [];
|
||||
|
||||
// limitFunction doesn't do much limiting, see comment re: the dates
|
||||
// array in rrule section below as to why we need to do the filtering
|
||||
// ourselves
|
||||
const limitFunction = function (date, i) {
|
||||
return true;
|
||||
};
|
||||
|
||||
const eventDate = function (event, time) {
|
||||
return CalendarFetcherUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
};
|
||||
|
||||
Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
|
||||
Object.entries(data).forEach(([key, event]) => {
|
||||
Log.debug("Processing entry...");
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day").toDate();
|
||||
const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
let past = today;
|
||||
|
||||
if (config.includePastEvents) {
|
||||
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
|
||||
}
|
||||
|
||||
// FIXME: Ugly fix to solve the facebook birthday issue.
|
||||
// Otherwise, the recurring events only show the birthday for next year.
|
||||
let isFacebookBirthday = false;
|
||||
if (typeof event.uid !== "undefined") {
|
||||
if (event.uid.indexOf("@facebook.com") !== -1) {
|
||||
isFacebookBirthday = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
Log.debug(`Event:\n${JSON.stringify(event)}`);
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
// make copy of start date, separate storage area
|
||||
endDate = moment(startDate.format("x"), "x");
|
||||
} else {
|
||||
endDate = moment(startDate).add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug(`start: ${startDate.toDate()}`);
|
||||
Log.debug(`end:: ${endDate.toDate()}`);
|
||||
|
||||
// Calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
Log.debug(`duration: ${duration}`);
|
||||
|
||||
// FIXME: Since the parsed json object from node-ical comes with time information
|
||||
// this check could be removed (?)
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
const title = CalendarFetcherUtils.getTitleFromEvent(event);
|
||||
Log.debug(`title: ${title}`);
|
||||
|
||||
let excluded = false,
|
||||
dateFilter = null;
|
||||
|
||||
for (let f in config.excludedEvents) {
|
||||
let filter = config.excludedEvents[f],
|
||||
testTitle = title.toLowerCase(),
|
||||
until = null,
|
||||
useRegex = false,
|
||||
regexFlags = "g";
|
||||
|
||||
if (filter instanceof Object) {
|
||||
if (typeof filter.until !== "undefined") {
|
||||
until = filter.until;
|
||||
}
|
||||
|
||||
if (typeof filter.regex !== "undefined") {
|
||||
useRegex = filter.regex;
|
||||
}
|
||||
|
||||
// If additional advanced filtering is added in, this section
|
||||
// must remain last as we overwrite the filter object with the
|
||||
// filterBy string
|
||||
if (filter.caseSensitive) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
} else if (useRegex) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
regexFlags += "i";
|
||||
} else {
|
||||
filter = filter.filterBy.toLowerCase();
|
||||
}
|
||||
} else {
|
||||
filter = filter.toLowerCase();
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
|
||||
if (until) {
|
||||
dateFilter = until;
|
||||
} else {
|
||||
excluded = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (excluded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = event.location || false;
|
||||
const geo = event.geo || false;
|
||||
const description = event.description || false;
|
||||
|
||||
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||
const rule = event.rrule;
|
||||
let addedEvents = 0;
|
||||
|
||||
const pastMoment = moment(past);
|
||||
const futureMoment = moment(future);
|
||||
|
||||
// can cause problems with e.g. birthdays before 1900
|
||||
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
|
||||
rule.origOptions.dtstart.setYear(1900);
|
||||
rule.options.dtstart.setYear(1900);
|
||||
}
|
||||
|
||||
// For recurring events, get the set of start dates that fall within the range
|
||||
// of dates we're looking for.
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
let pastLocal = 0;
|
||||
let futureLocal = 0;
|
||||
if (CalendarFetcherUtils.isFullDayEvent(event)) {
|
||||
Log.debug("fullday");
|
||||
// if full day event, only use the date part of the ranges
|
||||
pastLocal = pastMoment.toDate();
|
||||
futureLocal = futureMoment.toDate();
|
||||
|
||||
Log.debug(`pastLocal: ${pastLocal}`);
|
||||
Log.debug(`futureLocal: ${futureLocal}`);
|
||||
} else {
|
||||
// if we want past events
|
||||
if (config.includePastEvents) {
|
||||
// use the calculated past time for the between from
|
||||
pastLocal = pastMoment.toDate();
|
||||
} else {
|
||||
// otherwise use NOW.. cause we shouldn't use any before now
|
||||
pastLocal = moment().toDate(); //now
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
}
|
||||
Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
|
||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. For the time being,
|
||||
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
|
||||
// because the logic below will filter out any recurrences that don't actually belong within
|
||||
// our display range.
|
||||
// Would be great if there was a better way to handle this.
|
||||
Log.debug(`event.recurrences: ${event.recurrences}`);
|
||||
if (event.recurrences !== undefined) {
|
||||
for (let r in event.recurrences) {
|
||||
// Only add dates that weren't already in the range we added from the rrule so that
|
||||
// we don"t double-add those events.
|
||||
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
|
||||
dates.push(new Date(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||
for (let d in dates) {
|
||||
let date = dates[d];
|
||||
// Remove the time information of each date by using its substring, using the following method:
|
||||
// .toISOString().substring(0,10).
|
||||
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
|
||||
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
|
||||
const dateKey = date.toISOString().substring(0, 10);
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
|
||||
// set the time information in the date to equal the time information in the event
|
||||
date.setUTCHours(curEvent.start.getUTCHours(), curEvent.start.getUTCMinutes(), curEvent.start.getUTCSeconds(), curEvent.start.getUTCMilliseconds());
|
||||
|
||||
// Get the offset of today where we are processing
|
||||
// This will be the correction, we need to apply.
|
||||
let nowOffset = new Date().getTimezoneOffset();
|
||||
// For full day events, the time might be off from RRULE/Luxon problem
|
||||
// Get time zone offset of the rule calculated event
|
||||
let dateoffset = date.getTimezoneOffset();
|
||||
|
||||
// Reduce the time by the following offset.
|
||||
Log.debug(` recurring date is ${date} offset is ${dateoffset}`);
|
||||
|
||||
let dh = moment(date).format("HH");
|
||||
Log.debug(` recurring date is ${date} offset is ${dateoffset / 60} Hour is ${dh}`);
|
||||
|
||||
if (CalendarFetcherUtils.isFullDayEvent(event)) {
|
||||
Log.debug("Fullday");
|
||||
// If the offset is negative (east of GMT), where the problem is
|
||||
if (dateoffset < 0) {
|
||||
if (dh < Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
// reduce the time by the offset
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// Apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`new recurring date1 fulldate is ${date}`);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
//if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`new recurring date2 fulldate is ${date}`);
|
||||
}
|
||||
//}
|
||||
}
|
||||
} else {
|
||||
// not full day, but luxon can still screw up the date on the rule processing
|
||||
// we need to correct the date to get back to the right event for
|
||||
if (dateoffset < 0) {
|
||||
// if the date hour is less than the offset
|
||||
if (dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// Reduce the time by t:
|
||||
// Apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`new recurring date1 is ${date}`);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
//if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`new recurring date2 is ${date}`);
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
startDate = moment(date);
|
||||
Log.debug(`Corrected startDate: ${startDate.toDate()}`);
|
||||
|
||||
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date);
|
||||
|
||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateKey];
|
||||
startDate = moment(curEvent.start);
|
||||
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
||||
}
|
||||
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
Log.debug(`duration: ${duration}`);
|
||||
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
if (startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
|
||||
const recurrenceTitle = CalendarFetcherUtils.getTitleFromEvent(curEvent);
|
||||
|
||||
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
|
||||
// it to the event list.
|
||||
if (endDate.isBefore(past) || startDate.isAfter(future)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
Log.debug(`saving event: ${description}`);
|
||||
addedEvents++;
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event),
|
||||
recurringEvent: true,
|
||||
class: event.class,
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
// End recurring event parsing.
|
||||
} else {
|
||||
// Single event.
|
||||
const fullDayEvent = isFacebookBirthday ? true : CalendarFetcherUtils.isFullDayEvent(event);
|
||||
// Log.debug("full day event")
|
||||
|
||||
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
|
||||
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
|
||||
if (config.includePastEvents) {
|
||||
// Past event is too far in the past, so skip.
|
||||
if (endDate < past) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// It's not a fullday event, and it is in the past, so skip.
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a fullday event, and it is before today, So skip.
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// It exceeds the maximumNumberOfDays limit, so skip.
|
||||
if (startDate > future) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get correction for date saving and dst change between now and then
|
||||
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startDate.toDate());
|
||||
// Every thing is good. Add it to the list.
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: fullDayEvent,
|
||||
class: event.class,
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newEvents.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
return newEvents;
|
||||
},
|
||||
|
||||
/**
|
||||
* Lookup iana tz from windows
|
||||
* @param {string} msTZName the timezone name to lookup
|
||||
* @returns {string|null} the iana name or null of none is found
|
||||
*/
|
||||
getIanaTZFromMS: function (msTZName) {
|
||||
// Get hash entry
|
||||
const he = zoneTable[msTZName];
|
||||
// If found return iana name, else null
|
||||
return he ? he.iana[0] : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the title from the event.
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {string} The title of the event, or "Event" if no title is found.
|
||||
*/
|
||||
getTitleFromEvent: function (event) {
|
||||
let title = "Event";
|
||||
if (event.summary) {
|
||||
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
|
||||
} else if (event.description) {
|
||||
title = event.description;
|
||||
}
|
||||
|
||||
return title;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if an event is a fullday event.
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {boolean} True if the event is a fullday event, false otherwise
|
||||
*/
|
||||
isFullDayEvent: function (event) {
|
||||
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const start = event.start || 0;
|
||||
const startDate = new Date(start);
|
||||
const end = event.end || 0;
|
||||
if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
|
||||
// Is 24 hours, and starts on the middle of the night.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the user defined time filter should apply
|
||||
* @param {Date} now Date object using previously created object for consistency
|
||||
* @param {Moment} endDate Moment object representing the event end date
|
||||
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
|
||||
* @returns {boolean} True if the event should be filtered out, false otherwise
|
||||
*/
|
||||
timeFilterApplies: function (now, endDate, filter) {
|
||||
if (filter) {
|
||||
const until = filter.split(" "),
|
||||
value = parseInt(until[0]),
|
||||
increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js
|
||||
filterUntil = moment(endDate.format()).subtract(value, increment);
|
||||
|
||||
return now < filterUntil.format("x");
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the user defined title filter should apply
|
||||
* @param {string} title the title of the event
|
||||
* @param {string} filter the string to look for, can be a regex also
|
||||
* @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
|
||||
* @param {string} regexFlags flags that should be applied to the regex
|
||||
* @returns {boolean} True if the title should be filtered out, false otherwise
|
||||
*/
|
||||
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
|
||||
if (useRegex) {
|
||||
let regexFilter = filter;
|
||||
// Assume if leading slash, there is also trailing slash
|
||||
if (filter[0] === "/") {
|
||||
// Strip leading and trailing slashes
|
||||
regexFilter = filter.substr(1).slice(0, -1);
|
||||
}
|
||||
return new RegExp(regexFilter, regexFlags).test(title);
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = CalendarFetcherUtils;
|
||||
}
|
@@ -1,610 +1,114 @@
|
||||
/* MagicMirror²
|
||||
* Calendar Util Methods
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* By Rejas
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external Moment
|
||||
*/
|
||||
const moment = require("moment");
|
||||
const path = require("path");
|
||||
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
|
||||
const Log = require("../../../js/logger.js");
|
||||
|
||||
const CalendarUtils = {
|
||||
/**
|
||||
* Calculate the time correction, either dst/std or full day in cases where
|
||||
* utc time is day before plus offset
|
||||
*
|
||||
* @param {object} event the event which needs adjustement
|
||||
* @param {Date} date the date on which this event happens
|
||||
* @returns {number} the necessary adjustment in hours
|
||||
* Capitalize the first letter of a string
|
||||
* @param {string} string The string to capitalize
|
||||
* @returns {string} The capitalized string
|
||||
*/
|
||||
calculateTimezoneAdjustment: function (event, date) {
|
||||
let adjustHours = 0;
|
||||
// if a timezone was specified
|
||||
if (!event.start.tz) {
|
||||
Log.debug(" if no tz, guess based on now");
|
||||
event.start.tz = moment.tz.guess();
|
||||
}
|
||||
Log.debug("initial tz=" + event.start.tz);
|
||||
|
||||
// if there is a start date specified
|
||||
if (event.start.tz) {
|
||||
// if this is a windows timezone
|
||||
if (event.start.tz.includes(" ")) {
|
||||
// use the lookup table to get theIANA name as moment and date don't know MS timezones
|
||||
let tz = CalendarUtils.getIanaTZFromMS(event.start.tz);
|
||||
Log.debug("corrected TZ=" + tz);
|
||||
// watch out for unregistered windows timezone names
|
||||
// if we had a successful lookup
|
||||
if (tz) {
|
||||
// change the timezone to the IANA name
|
||||
event.start.tz = tz;
|
||||
// Log.debug("corrected timezone="+event.start.tz)
|
||||
}
|
||||
}
|
||||
Log.debug("corrected tz=" + event.start.tz);
|
||||
let current_offset = 0; // offset from TZ string or calculated
|
||||
let mm = 0; // date with tz or offset
|
||||
let start_offset = 0; // utc offset of created with tz
|
||||
// if there is still an offset, lookup failed, use it
|
||||
if (event.start.tz.startsWith("(")) {
|
||||
const regex = /[+|-]\d*:\d*/;
|
||||
const start_offsetString = event.start.tz.match(regex).toString().split(":");
|
||||
let start_offset = parseInt(start_offsetString[0]);
|
||||
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
|
||||
adjustHours = start_offset;
|
||||
Log.debug("defined offset=" + start_offset + " hours");
|
||||
current_offset = start_offset;
|
||||
event.start.tz = "";
|
||||
Log.debug("ical offset=" + current_offset + " date=" + date);
|
||||
mm = moment(date);
|
||||
let x = parseInt(moment(new Date()).utcOffset());
|
||||
Log.debug("net mins=" + (current_offset * 60 - x));
|
||||
|
||||
mm = mm.add(x - current_offset * 60, "minutes");
|
||||
adjustHours = (current_offset * 60 - x) / 60;
|
||||
event.start = mm.toDate();
|
||||
Log.debug("adjusted date=" + event.start);
|
||||
} else {
|
||||
// get the start time in that timezone
|
||||
let es = moment(event.start);
|
||||
// check for start date prior to start of daylight changing date
|
||||
if (es.format("YYYY") < 2007) {
|
||||
es.set("year", 2013); // if so, use a closer date
|
||||
}
|
||||
Log.debug("start date/time=" + es.toDate());
|
||||
start_offset = moment.tz(es, event.start.tz).utcOffset();
|
||||
Log.debug("start offset=" + start_offset);
|
||||
|
||||
Log.debug("start date/time w tz =" + moment.tz(moment(event.start), event.start.tz).toDate());
|
||||
|
||||
// get the specified date in that timezone
|
||||
mm = moment.tz(moment(date), event.start.tz);
|
||||
Log.debug("event date=" + mm.toDate());
|
||||
current_offset = mm.utcOffset();
|
||||
}
|
||||
Log.debug("event offset=" + current_offset + " hour=" + mm.format("H") + " event date=" + mm.toDate());
|
||||
|
||||
// if the offset is greater than 0, east of london
|
||||
if (current_offset !== start_offset) {
|
||||
// big offset
|
||||
Log.debug("offset");
|
||||
let h = parseInt(mm.format("H"));
|
||||
// check if the event time is less than the offset
|
||||
if (h > 0 && h < Math.abs(current_offset) / 60) {
|
||||
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
|
||||
// we need to fix that
|
||||
//adjustHours = 24;
|
||||
// Log.debug("adjusting date")
|
||||
}
|
||||
//-300 > -240
|
||||
//if (Math.abs(current_offset) > Math.abs(start_offset)){
|
||||
if (current_offset > start_offset) {
|
||||
adjustHours -= 1;
|
||||
Log.debug("adjust down 1 hour dst change");
|
||||
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
|
||||
} else if (current_offset < start_offset) {
|
||||
adjustHours += 1;
|
||||
Log.debug("adjust up 1 hour dst change");
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.debug("adjustHours=" + adjustHours);
|
||||
return adjustHours;
|
||||
capFirst: function (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter the events from ical according to the given config
|
||||
*
|
||||
* @param {object} data the calendar data from ical
|
||||
* @param {object} config The configuration object
|
||||
* @returns {string[]} the filtered events
|
||||
* This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
|
||||
* corresponding time-format to be used in the calendar display. If no number is given (or otherwise invalid input)
|
||||
* it will a localeSpecification object with the system locale time format.
|
||||
* @param {number} timeFormat Specifies either 12 or 24-hour time format
|
||||
* @returns {moment.LocaleSpecification} formatted time
|
||||
*/
|
||||
filterEvents: function (data, config) {
|
||||
const newEvents = [];
|
||||
|
||||
// limitFunction doesn't do much limiting, see comment re: the dates
|
||||
// array in rrule section below as to why we need to do the filtering
|
||||
// ourselves
|
||||
const limitFunction = function (date, i) {
|
||||
return true;
|
||||
};
|
||||
|
||||
const eventDate = function (event, time) {
|
||||
return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
};
|
||||
|
||||
Log.debug("There are " + Object.entries(data).length + " calendar entries.");
|
||||
Object.entries(data).forEach(([key, event]) => {
|
||||
Log.debug("Processing entry...");
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day").toDate();
|
||||
const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
let past = today;
|
||||
|
||||
if (config.includePastEvents) {
|
||||
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
|
||||
getLocaleSpecification: function (timeFormat) {
|
||||
switch (timeFormat) {
|
||||
case 12: {
|
||||
return { longDateFormat: { LT: "h:mm A" } };
|
||||
}
|
||||
|
||||
// FIXME: Ugly fix to solve the facebook birthday issue.
|
||||
// Otherwise, the recurring events only show the birthday for next year.
|
||||
let isFacebookBirthday = false;
|
||||
if (typeof event.uid !== "undefined") {
|
||||
if (event.uid.indexOf("@facebook.com") !== -1) {
|
||||
isFacebookBirthday = true;
|
||||
}
|
||||
case 24: {
|
||||
return { longDateFormat: { LT: "HH:mm" } };
|
||||
}
|
||||
default: {
|
||||
return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
Log.debug("Event:\n" + JSON.stringify(event));
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
/**
|
||||
* Shortens a string if it's longer than maxLength and add an ellipsis to the end
|
||||
* @param {string} string Text string to shorten
|
||||
* @param {number} maxLength The max length of the string
|
||||
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
|
||||
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
|
||||
* @returns {string} The shortened string
|
||||
*/
|
||||
shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
|
||||
if (typeof string !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||
if (wrapEvents === true) {
|
||||
const words = string.split(" ");
|
||||
let temp = "";
|
||||
let currentLine = "";
|
||||
let line = 0;
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const word = words[i];
|
||||
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
|
||||
// max - 1 to account for a space
|
||||
currentLine += `${word} `;
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
// make copy of start date, separate storage area
|
||||
endDate = moment(startDate.format("x"), "x");
|
||||
} else {
|
||||
endDate = moment(startDate).add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug("start: " + startDate.toDate());
|
||||
Log.debug("end:: " + endDate.toDate());
|
||||
|
||||
// Calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
Log.debug("duration: " + duration);
|
||||
|
||||
// FIXME: Since the parsed json object from node-ical comes with time information
|
||||
// this check could be removed (?)
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
const title = CalendarUtils.getTitleFromEvent(event);
|
||||
Log.debug("title: " + title);
|
||||
|
||||
let excluded = false,
|
||||
dateFilter = null;
|
||||
|
||||
for (let f in config.excludedEvents) {
|
||||
let filter = config.excludedEvents[f],
|
||||
testTitle = title.toLowerCase(),
|
||||
until = null,
|
||||
useRegex = false,
|
||||
regexFlags = "g";
|
||||
|
||||
if (filter instanceof Object) {
|
||||
if (typeof filter.until !== "undefined") {
|
||||
until = filter.until;
|
||||
}
|
||||
|
||||
if (typeof filter.regex !== "undefined") {
|
||||
useRegex = filter.regex;
|
||||
}
|
||||
|
||||
// If additional advanced filtering is added in, this section
|
||||
// must remain last as we overwrite the filter object with the
|
||||
// filterBy string
|
||||
if (filter.caseSensitive) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
} else if (useRegex) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
regexFlags += "i";
|
||||
} else {
|
||||
filter = filter.filterBy.toLowerCase();
|
||||
}
|
||||
} else {
|
||||
filter = filter.toLowerCase();
|
||||
}
|
||||
|
||||
if (CalendarUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
|
||||
if (until) {
|
||||
dateFilter = until;
|
||||
} else {
|
||||
excluded = true;
|
||||
line++;
|
||||
if (line > maxTitleLines - 1) {
|
||||
if (i < words.length) {
|
||||
currentLine += "…";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (excluded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = event.location || false;
|
||||
const geo = event.geo || false;
|
||||
const description = event.description || false;
|
||||
|
||||
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||
const rule = event.rrule;
|
||||
let addedEvents = 0;
|
||||
|
||||
const pastMoment = moment(past);
|
||||
const futureMoment = moment(future);
|
||||
|
||||
// can cause problems with e.g. birthdays before 1900
|
||||
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
|
||||
rule.origOptions.dtstart.setYear(1900);
|
||||
rule.options.dtstart.setYear(1900);
|
||||
}
|
||||
|
||||
// For recurring events, get the set of start dates that fall within the range
|
||||
// of dates we're looking for.
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
let pastLocal = 0;
|
||||
let futureLocal = 0;
|
||||
if (CalendarUtils.isFullDayEvent(event)) {
|
||||
Log.debug("fullday");
|
||||
// if full day event, only use the date part of the ranges
|
||||
pastLocal = pastMoment.toDate();
|
||||
futureLocal = futureMoment.toDate();
|
||||
|
||||
Log.debug("pastLocal: " + pastLocal);
|
||||
Log.debug("futureLocal: " + futureLocal);
|
||||
if (currentLine.length > 0) {
|
||||
temp += `${currentLine}<br>${word} `;
|
||||
} else {
|
||||
// if we want past events
|
||||
if (config.includePastEvents) {
|
||||
// use the calculated past time for the between from
|
||||
pastLocal = pastMoment.toDate();
|
||||
} else {
|
||||
// otherwise use NOW.. cause we shouldn't use any before now
|
||||
pastLocal = moment().toDate(); //now
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
temp += `${word}<br>`;
|
||||
}
|
||||
Log.debug("Search for recurring events between: " + pastLocal + " and " + futureLocal);
|
||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
Log.debug("Title: " + event.summary + ", with dates: " + JSON.stringify(dates));
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. For the time being,
|
||||
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
|
||||
// because the logic below will filter out any recurrences that don't actually belong within
|
||||
// our display range.
|
||||
// Would be great if there was a better way to handle this.
|
||||
Log.debug("event.recurrences: " + event.recurrences);
|
||||
if (event.recurrences !== undefined) {
|
||||
for (let r in event.recurrences) {
|
||||
// Only add dates that weren't already in the range we added from the rrule so that
|
||||
// we don"t double-add those events.
|
||||
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
|
||||
dates.push(new Date(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||
for (let d in dates) {
|
||||
let date = dates[d];
|
||||
// Remove the time information of each date by using its substring, using the following method:
|
||||
// .toISOString().substring(0,10).
|
||||
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
|
||||
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
|
||||
const dateKey = date.toISOString().substring(0, 10);
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
|
||||
// Get the offset of today where we are processing
|
||||
// This will be the correction, we need to apply.
|
||||
let nowOffset = new Date().getTimezoneOffset();
|
||||
// For full day events, the time might be off from RRULE/Luxon problem
|
||||
// Get time zone offset of the rule calculated event
|
||||
let dateoffset = date.getTimezoneOffset();
|
||||
|
||||
// Reduce the time by the following offset.
|
||||
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
|
||||
|
||||
let dh = moment(date).format("HH");
|
||||
Log.debug(" recurring date is " + date + " offset is " + dateoffset / 60 + " Hour is " + dh);
|
||||
|
||||
if (CalendarUtils.isFullDayEvent(event)) {
|
||||
Log.debug("Fullday");
|
||||
// If the offset is negative (east of GMT), where the problem is
|
||||
if (dateoffset < 0) {
|
||||
if (dh < Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
// reduce the time by the offset
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// Apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date1 fulldate is " + date);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
//if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date2 fulldate is " + date);
|
||||
}
|
||||
//}
|
||||
}
|
||||
} else {
|
||||
// not full day, but luxon can still screw up the date on the rule processing
|
||||
// we need to correct the date to get back to the right event for
|
||||
if (dateoffset < 0) {
|
||||
// if the date hour is less than the offset
|
||||
if (dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// Reduce the time by t:
|
||||
// Apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date1 is " + date);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
//if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date2 is " + date);
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
startDate = moment(date);
|
||||
Log.debug("Corrected startDate: " + startDate.toDate());
|
||||
|
||||
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
|
||||
|
||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateKey];
|
||||
startDate = moment(curEvent.start);
|
||||
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
||||
}
|
||||
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
Log.debug("duration: " + duration);
|
||||
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
if (startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
|
||||
const recurrenceTitle = CalendarUtils.getTitleFromEvent(curEvent);
|
||||
|
||||
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
|
||||
// it to the event list.
|
||||
if (endDate.isBefore(past) || startDate.isAfter(future)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
Log.debug("saving event: " + description);
|
||||
addedEvents++;
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: CalendarUtils.isFullDayEvent(event),
|
||||
recurringEvent: true,
|
||||
class: event.class,
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
// End recurring event parsing.
|
||||
} else {
|
||||
// Single event.
|
||||
const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event);
|
||||
// Log.debug("full day event")
|
||||
|
||||
if (config.includePastEvents) {
|
||||
// Past event is too far in the past, so skip.
|
||||
if (endDate < past) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// It's not a fullday event, and it is in the past, so skip.
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a fullday event, and it is before today, So skip.
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// It exceeds the maximumNumberOfDays limit, so skip.
|
||||
if (startDate > future) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
|
||||
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
// get correction for date saving and dst change between now and then
|
||||
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, startDate.toDate());
|
||||
// Every thing is good. Add it to the list.
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: fullDayEvent,
|
||||
class: event.class,
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
currentLine = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newEvents.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
return newEvents;
|
||||
},
|
||||
|
||||
/**
|
||||
* Lookup iana tz from windows
|
||||
*
|
||||
* @param {string} msTZName the timezone name to lookup
|
||||
* @returns {string|null} the iana name or null of none is found
|
||||
*/
|
||||
getIanaTZFromMS: function (msTZName) {
|
||||
// Get hash entry
|
||||
const he = zoneTable[msTZName];
|
||||
// If found return iana name, else null
|
||||
return he ? he.iana[0] : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the title from the event.
|
||||
*
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {string} The title of the event, or "Event" if no title is found.
|
||||
*/
|
||||
getTitleFromEvent: function (event) {
|
||||
let title = "Event";
|
||||
if (event.summary) {
|
||||
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
|
||||
} else if (event.description) {
|
||||
title = event.description;
|
||||
}
|
||||
|
||||
return title;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if an event is a fullday event.
|
||||
*
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {boolean} True if the event is a fullday event, false otherwise
|
||||
*/
|
||||
isFullDayEvent: function (event) {
|
||||
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const start = event.start || 0;
|
||||
const startDate = new Date(start);
|
||||
const end = event.end || 0;
|
||||
if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
|
||||
// Is 24 hours, and starts on the middle of the night.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the user defined time filter should apply
|
||||
*
|
||||
* @param {Date} now Date object using previously created object for consistency
|
||||
* @param {Moment} endDate Moment object representing the event end date
|
||||
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
|
||||
* @returns {boolean} True if the event should be filtered out, false otherwise
|
||||
*/
|
||||
timeFilterApplies: function (now, endDate, filter) {
|
||||
if (filter) {
|
||||
const until = filter.split(" "),
|
||||
value = parseInt(until[0]),
|
||||
increment = until[1].slice(-1) === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
|
||||
filterUntil = moment(endDate.format()).subtract(value, increment);
|
||||
|
||||
return now < filterUntil.format("x");
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the user defined title filter should apply
|
||||
*
|
||||
* @param {string} title the title of the event
|
||||
* @param {string} filter the string to look for, can be a regex also
|
||||
* @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
|
||||
* @param {string} regexFlags flags that should be applied to the regex
|
||||
* @returns {boolean} True if the title should be filtered out, false otherwise
|
||||
*/
|
||||
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
|
||||
if (useRegex) {
|
||||
// Assume if leading slash, there is also trailing slash
|
||||
if (filter[0] === "/") {
|
||||
// Strip leading and trailing slashes
|
||||
filter = filter.substr(1).slice(0, -1);
|
||||
}
|
||||
|
||||
filter = new RegExp(filter, regexFlags);
|
||||
|
||||
return filter.test(title);
|
||||
return (temp + currentLine).trim();
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
|
||||
return `${string.trim().slice(0, maxLength)}…`;
|
||||
} else {
|
||||
return string.trim();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Transforms the title of an event for usage.
|
||||
* Replaces parts of the text as defined in config.titleReplace.
|
||||
* Shortens title based on config.maxTitleLength and config.wrapEvents
|
||||
* @param {string} title The title to transform.
|
||||
* @param {object} titleReplace Pairs of strings to be replaced in the title
|
||||
* @returns {string} The transformed title.
|
||||
*/
|
||||
titleTransform: function (title, titleReplace) {
|
||||
let transformedTitle = title;
|
||||
for (let needle in titleReplace) {
|
||||
const replacement = titleReplace[needle];
|
||||
|
||||
const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
|
||||
if (regParts) {
|
||||
// the parsed pattern is a regexp.
|
||||
needle = new RegExp(regParts[1], regParts[2]);
|
||||
}
|
||||
|
||||
transformedTitle = transformedTitle.replace(needle, replacement);
|
||||
}
|
||||
return transformedTitle;
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -8,7 +8,7 @@
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
const CalendarFetcher = require("./calendarfetcher");
|
||||
|
||||
const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
|
||||
//const url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first)
|
||||
|
@@ -5,13 +5,13 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const NodeHelper = require("node_helper");
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
const Log = require("logger");
|
||||
const CalendarFetcher = require("./calendarfetcher");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.log("Starting node helper for: " + this.name);
|
||||
Log.log(`Starting node helper for: ${this.name}`);
|
||||
this.fetchers = [];
|
||||
},
|
||||
|
||||
@@ -33,7 +33,6 @@ module.exports = NodeHelper.create({
|
||||
/**
|
||||
* Creates a fetcher for a new url if it doesn't exist yet.
|
||||
* Otherwise it reuses the existing one.
|
||||
*
|
||||
* @param {string} url The url of the calendar
|
||||
* @param {number} fetchInterval How often does the calendar needs to be fetched in ms
|
||||
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
|
||||
@@ -55,7 +54,7 @@ module.exports = NodeHelper.create({
|
||||
|
||||
let fetcher;
|
||||
if (typeof this.fetchers[identifier + url] === "undefined") {
|
||||
Log.log("Create new calendarfetcher for url: " + url + " - Interval: " + fetchInterval);
|
||||
Log.log(`Create new calendarfetcher for url: ${url} - Interval: ${fetchInterval}`);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
|
||||
|
||||
fetcher.onReceive((fetcher) => {
|
||||
@@ -73,7 +72,7 @@ module.exports = NodeHelper.create({
|
||||
|
||||
this.fetchers[identifier + url] = fetcher;
|
||||
} else {
|
||||
Log.log("Use existing calendarfetcher for url: " + url);
|
||||
Log.log(`Use existing calendarfetcher for url: ${url}`);
|
||||
fetcher = this.fetchers[identifier + url];
|
||||
fetcher.broadcastEvents();
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* global SunCalc */
|
||||
/* global SunCalc, formatTime */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Clock
|
||||
@@ -22,6 +22,7 @@ Module.register("clock", {
|
||||
showTime: true,
|
||||
showWeek: false,
|
||||
dateFormat: "dddd, LL",
|
||||
sendNotifications: false,
|
||||
|
||||
/* specific to the analog clock */
|
||||
analogSize: "200px",
|
||||
@@ -45,7 +46,7 @@ Module.register("clock", {
|
||||
},
|
||||
// Define start sequence.
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
// Schedule update interval.
|
||||
this.second = moment().second();
|
||||
@@ -66,23 +67,27 @@ Module.register("clock", {
|
||||
const notificationTimer = () => {
|
||||
this.updateDom();
|
||||
|
||||
// If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
|
||||
if (this.config.displaySeconds) {
|
||||
this.second = moment().second();
|
||||
if (this.second !== 0) {
|
||||
this.sendNotification("CLOCK_SECOND", this.second);
|
||||
setTimeout(notificationTimer, delayCalculator(0));
|
||||
return;
|
||||
if (this.config.sendNotifications) {
|
||||
// If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
|
||||
if (this.config.displaySeconds) {
|
||||
this.second = moment().second();
|
||||
if (this.second !== 0) {
|
||||
this.sendNotification("CLOCK_SECOND", this.second);
|
||||
setTimeout(notificationTimer, delayCalculator(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
|
||||
this.minute = moment().minute();
|
||||
this.sendNotification("CLOCK_MINUTE", this.minute);
|
||||
}
|
||||
|
||||
// If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
|
||||
this.minute = moment().minute();
|
||||
this.sendNotification("CLOCK_MINUTE", this.minute);
|
||||
setTimeout(notificationTimer, delayCalculator(0));
|
||||
};
|
||||
|
||||
// Set the initial timeout with the amount of seconds elapsed as reducedSeconds so it will trigger when the minute changes
|
||||
// Set the initial timeout with the amount of seconds elapsed as
|
||||
// reducedSeconds, so it will trigger when the minute changes
|
||||
setTimeout(notificationTimer, delayCalculator(this.second));
|
||||
|
||||
// Set locale.
|
||||
@@ -91,13 +96,13 @@ Module.register("clock", {
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.classList.add("clockGrid");
|
||||
wrapper.classList.add("clock-grid");
|
||||
|
||||
/************************************
|
||||
* Create wrappers for analog and digital clock
|
||||
*/
|
||||
const analogWrapper = document.createElement("div");
|
||||
analogWrapper.className = "clockCircle";
|
||||
analogWrapper.className = "clock-circle";
|
||||
const digitalWrapper = document.createElement("div");
|
||||
digitalWrapper.className = "digital";
|
||||
digitalWrapper.style.gridArea = "center";
|
||||
@@ -137,9 +142,9 @@ Module.register("clock", {
|
||||
}
|
||||
|
||||
if (this.config.clockBold) {
|
||||
timeString = now.format(hourSymbol + '[<span class="bold">]mm[</span>]');
|
||||
timeString = now.format(`${hourSymbol}[<span class="bold">]mm[</span>]`);
|
||||
} else {
|
||||
timeString = now.format(hourSymbol + ":mm");
|
||||
timeString = now.format(`${hourSymbol}:mm`);
|
||||
}
|
||||
|
||||
if (this.config.showDate) {
|
||||
@@ -164,21 +169,6 @@ Module.register("clock", {
|
||||
digitalWrapper.appendChild(timeWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the time according to the config
|
||||
*
|
||||
* @param {object} config The config of the module
|
||||
* @param {object} time time to format
|
||||
* @returns {string} The formatted time string
|
||||
*/
|
||||
function formatTime(config, time) {
|
||||
let formatString = hourSymbol + ":mm";
|
||||
if (config.showPeriod && config.timeFormat !== 24) {
|
||||
formatString += config.showPeriodUpper ? "A" : "a";
|
||||
}
|
||||
return moment(time).format(formatString);
|
||||
}
|
||||
|
||||
/****************************************************************
|
||||
* Create wrappers for Sun Times, only if specified in config
|
||||
*/
|
||||
@@ -195,19 +185,11 @@ Module.register("clock", {
|
||||
nextEvent = tomorrowSunTimes.sunrise;
|
||||
}
|
||||
const untilNextEvent = moment.duration(moment(nextEvent).diff(now));
|
||||
const untilNextEventString = untilNextEvent.hours() + "h " + untilNextEvent.minutes() + "m";
|
||||
const untilNextEventString = `${untilNextEvent.hours()}h ${untilNextEvent.minutes()}m`;
|
||||
sunWrapper.innerHTML =
|
||||
'<span class="' +
|
||||
(isVisible ? "bright" : "") +
|
||||
'"><i class="fas fa-sun" aria-hidden="true"></i> ' +
|
||||
untilNextEventString +
|
||||
"</span>" +
|
||||
'<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ' +
|
||||
formatTime(this.config, sunTimes.sunrise) +
|
||||
"</span>" +
|
||||
'<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ' +
|
||||
formatTime(this.config, sunTimes.sunset) +
|
||||
"</span>";
|
||||
`<span class="${isVisible ? "bright" : ""}"><i class="fas fa-sun" aria-hidden="true"></i> ${untilNextEventString}</span>` +
|
||||
`<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunrise)}</span>` +
|
||||
`<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunset)}</span>`;
|
||||
digitalWrapper.appendChild(sunWrapper);
|
||||
}
|
||||
|
||||
@@ -226,19 +208,11 @@ Module.register("clock", {
|
||||
moonSet = nextMoonTimes.set;
|
||||
}
|
||||
const isVisible = now.isBetween(moonRise, moonSet) || moonTimes.alwaysUp === true;
|
||||
const illuminatedFractionString = Math.round(moonIllumination.fraction * 100) + "%";
|
||||
const illuminatedFractionString = `${Math.round(moonIllumination.fraction * 100)}%`;
|
||||
moonWrapper.innerHTML =
|
||||
'<span class="' +
|
||||
(isVisible ? "bright" : "") +
|
||||
'"><i class="fas fa-moon" aria-hidden="true"></i> ' +
|
||||
illuminatedFractionString +
|
||||
"</span>" +
|
||||
'<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ' +
|
||||
(moonRise ? formatTime(this.config, moonRise) : "...") +
|
||||
"</span>" +
|
||||
'<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ' +
|
||||
(moonSet ? formatTime(this.config, moonSet) : "...") +
|
||||
"</span>";
|
||||
`<span class="${isVisible ? "bright" : ""}"><i class="fas fa-moon" aria-hidden="true"></i> ${illuminatedFractionString}</span>` +
|
||||
`<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${moonRise ? formatTime(this.config, moonRise) : "..."}</span>` +
|
||||
`<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${moonSet ? formatTime(this.config, moonSet) : "..."}</span>`;
|
||||
digitalWrapper.appendChild(moonWrapper);
|
||||
}
|
||||
|
||||
@@ -266,7 +240,7 @@ Module.register("clock", {
|
||||
analogWrapper.style.height = this.config.analogSize;
|
||||
|
||||
if (this.config.analogFace !== "" && this.config.analogFace !== "simple" && this.config.analogFace !== "none") {
|
||||
analogWrapper.style.background = "url(" + this.data.path + "faces/" + this.config.analogFace + ".svg)";
|
||||
analogWrapper.style.background = `url(${this.data.path}faces/${this.config.analogFace}.svg)`;
|
||||
analogWrapper.style.backgroundSize = "100%";
|
||||
|
||||
// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
|
||||
@@ -276,16 +250,16 @@ Module.register("clock", {
|
||||
analogWrapper.style.border = "2px solid white";
|
||||
}
|
||||
const clockFace = document.createElement("div");
|
||||
clockFace.className = "clockFace";
|
||||
clockFace.className = "clock-face";
|
||||
|
||||
const clockHour = document.createElement("div");
|
||||
clockHour.id = "clockHour";
|
||||
clockHour.style.transform = "rotate(" + hour + "deg)";
|
||||
clockHour.className = "clockHour";
|
||||
clockHour.id = "clock-hour";
|
||||
clockHour.style.transform = `rotate(${hour}deg)`;
|
||||
clockHour.className = "clock-hour";
|
||||
const clockMinute = document.createElement("div");
|
||||
clockMinute.id = "clockMinute";
|
||||
clockMinute.style.transform = "rotate(" + minute + "deg)";
|
||||
clockMinute.className = "clockMinute";
|
||||
clockMinute.id = "clock-minute";
|
||||
clockMinute.style.transform = `rotate(${minute}deg)`;
|
||||
clockMinute.className = "clock-minute";
|
||||
|
||||
// Combine analog wrappers
|
||||
clockFace.appendChild(clockHour);
|
||||
@@ -293,9 +267,9 @@ Module.register("clock", {
|
||||
|
||||
if (this.config.displaySeconds) {
|
||||
const clockSecond = document.createElement("div");
|
||||
clockSecond.id = "clockSecond";
|
||||
clockSecond.style.transform = "rotate(" + second + "deg)";
|
||||
clockSecond.className = "clockSecond";
|
||||
clockSecond.id = "clock-second";
|
||||
clockSecond.style.transform = `rotate(${second}deg)`;
|
||||
clockSecond.className = "clock-second";
|
||||
clockSecond.style.backgroundColor = this.config.secondsColor;
|
||||
clockFace.appendChild(clockSecond);
|
||||
}
|
||||
@@ -307,16 +281,21 @@ Module.register("clock", {
|
||||
*/
|
||||
if (this.config.displayType === "analog") {
|
||||
// Display only an analog clock
|
||||
if (this.config.analogShowDate === "top") {
|
||||
wrapper.classList.add("clockGrid--bottom");
|
||||
} else if (this.config.analogShowDate === "bottom") {
|
||||
wrapper.classList.add("clockGrid--top");
|
||||
if (this.config.showDate) {
|
||||
// Add date to the analog clock
|
||||
dateWrapper.innerHTML = now.format(this.config.dateFormat);
|
||||
wrapper.appendChild(dateWrapper);
|
||||
}
|
||||
if (this.config.analogShowDate === "bottom") {
|
||||
wrapper.classList.add("clock-grid-bottom");
|
||||
} else if (this.config.analogShowDate === "top") {
|
||||
wrapper.classList.add("clock-grid-top");
|
||||
}
|
||||
wrapper.appendChild(analogWrapper);
|
||||
} else if (this.config.displayType === "digital") {
|
||||
wrapper.appendChild(digitalWrapper);
|
||||
} else if (this.config.displayType === "both") {
|
||||
wrapper.classList.add("clockGrid--" + this.config.analogPlacement);
|
||||
wrapper.classList.add(`clock-grid-${this.config.analogPlacement}`);
|
||||
wrapper.appendChild(analogWrapper);
|
||||
wrapper.appendChild(digitalWrapper);
|
||||
}
|
||||
|
@@ -1,37 +1,37 @@
|
||||
.clockGrid {
|
||||
.clock-grid {
|
||||
display: inline-flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.clockGrid--left {
|
||||
.clock-grid-left {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.clockGrid--right {
|
||||
.clock-grid-right {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.clockGrid--top {
|
||||
.clock-grid-top {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.clockGrid--bottom {
|
||||
.clock-grid-bottom {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.clockCircle {
|
||||
.clock-circle {
|
||||
place-self: center;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.clockFace {
|
||||
.clock-face {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.clockFace::after {
|
||||
.clock-face::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
@@ -44,7 +44,7 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.clockHour {
|
||||
.clock-hour {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
@@ -57,7 +57,7 @@
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.clockMinute {
|
||||
.clock-minute {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
@@ -70,7 +70,7 @@
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.clockSecond {
|
||||
.clock-second {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
|
@@ -33,16 +33,15 @@ Module.register("compliments", {
|
||||
},
|
||||
|
||||
// Define start sequence.
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
start: async function () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
this.lastComplimentIndex = -1;
|
||||
|
||||
if (this.config.remoteFile !== null) {
|
||||
this.loadComplimentFile().then((response) => {
|
||||
this.config.compliments = JSON.parse(response);
|
||||
this.updateDom();
|
||||
});
|
||||
const response = await this.loadComplimentFile();
|
||||
this.config.compliments = JSON.parse(response);
|
||||
this.updateDom();
|
||||
}
|
||||
|
||||
// Schedule update timer.
|
||||
@@ -53,7 +52,6 @@ Module.register("compliments", {
|
||||
|
||||
/**
|
||||
* Generate a random index for a list of compliments.
|
||||
*
|
||||
* @param {string[]} compliments Array with compliments.
|
||||
* @returns {number} a random index of given array
|
||||
*/
|
||||
@@ -79,7 +77,6 @@ Module.register("compliments", {
|
||||
|
||||
/**
|
||||
* Retrieve an array of compliments for the time of the day.
|
||||
*
|
||||
* @returns {string[]} array with compliments for the time of the day.
|
||||
*/
|
||||
complimentArray: function () {
|
||||
@@ -116,7 +113,6 @@ Module.register("compliments", {
|
||||
|
||||
/**
|
||||
* Retrieve a file from the local filesystem
|
||||
*
|
||||
* @returns {Promise} Resolved when the file is loaded
|
||||
*/
|
||||
loadComplimentFile: async function () {
|
||||
@@ -128,7 +124,6 @@ Module.register("compliments", {
|
||||
|
||||
/**
|
||||
* Retrieve a random compliment.
|
||||
*
|
||||
* @returns {string} a compliment
|
||||
*/
|
||||
getRandomCompliment: function () {
|
||||
|
@@ -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>
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<div>
|
||||
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
iframe.newsfeed-fullarticle {
|
||||
width: 100vw;
|
||||
|
||||
/* very large height value to allow scrolling */
|
||||
height: 3000px;
|
||||
top: 0;
|
||||
|
@@ -44,7 +44,7 @@ Module.register("newsfeed", {
|
||||
|
||||
getUrlPrefix: function (item) {
|
||||
if (item.useCorsProxy) {
|
||||
return location.protocol + "//" + location.host + "/cors?url=";
|
||||
return `${location.protocol}//${location.host}/cors?url=`;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
@@ -70,7 +70,7 @@ Module.register("newsfeed", {
|
||||
|
||||
// Define start sequence.
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
// Set locale.
|
||||
moment.locale(config.language);
|
||||
@@ -179,7 +179,6 @@ Module.register("newsfeed", {
|
||||
|
||||
/**
|
||||
* Generate an ordered list of items for this configured module.
|
||||
*
|
||||
* @param {object} feeds An object with feeds returned by the node helper.
|
||||
*/
|
||||
generateFeed: function (feeds) {
|
||||
@@ -272,7 +271,6 @@ Module.register("newsfeed", {
|
||||
|
||||
/**
|
||||
* Check if this module is configured to show this feed.
|
||||
*
|
||||
* @param {string} feedUrl Url of the feed to check.
|
||||
* @returns {boolean} True if it is subscribed, false otherwise
|
||||
*/
|
||||
@@ -287,7 +285,6 @@ Module.register("newsfeed", {
|
||||
|
||||
/**
|
||||
* Returns title for the specific feed url.
|
||||
*
|
||||
* @param {string} feedUrl Url of the feed
|
||||
* @returns {string} The title of the feed
|
||||
*/
|
||||
@@ -346,7 +343,7 @@ Module.register("newsfeed", {
|
||||
this.activeItem = 0;
|
||||
}
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
||||
this.updateDom(100);
|
||||
} else if (notification === "ARTICLE_PREVIOUS") {
|
||||
this.activeItem--;
|
||||
@@ -354,7 +351,7 @@ Module.register("newsfeed", {
|
||||
this.activeItem = this.newsItems.length - 1;
|
||||
}
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
||||
this.updateDom(100);
|
||||
}
|
||||
// if "more details" is received the first time: show article summary, on second time show full article
|
||||
@@ -363,8 +360,8 @@ Module.register("newsfeed", {
|
||||
if (this.config.showFullArticle === true) {
|
||||
this.scrollPosition += this.config.scrollLength;
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
Log.debug(this.name + " - scrolling down");
|
||||
Log.debug(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
|
||||
Log.debug(`${this.name} - scrolling down`);
|
||||
Log.debug(`${this.name} - ARTICLE_MORE_DETAILS, scroll position: ${this.config.scrollLength}`);
|
||||
} else {
|
||||
this.showFullArticle();
|
||||
}
|
||||
@@ -372,12 +369,12 @@ Module.register("newsfeed", {
|
||||
if (this.config.showFullArticle === true) {
|
||||
this.scrollPosition -= this.config.scrollLength;
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
Log.debug(this.name + " - scrolling up");
|
||||
Log.debug(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
|
||||
Log.debug(`${this.name} - scrolling up`);
|
||||
Log.debug(`${this.name} - ARTICLE_SCROLL_UP, scroll position: ${this.config.scrollLength}`);
|
||||
}
|
||||
} else if (notification === "ARTICLE_LESS_DETAILS") {
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.debug(this.name + " - showing only article titles again");
|
||||
Log.debug(`${this.name} - showing only article titles again`);
|
||||
this.updateDom(100);
|
||||
} else if (notification === "ARTICLE_TOGGLE_FULL") {
|
||||
if (this.config.showFullArticle) {
|
||||
@@ -406,7 +403,7 @@ Module.register("newsfeed", {
|
||||
}
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
Log.debug(this.name + " - showing " + this.isShowingDescription ? "article description" : "full article");
|
||||
Log.debug(`${this.name} - showing ${this.isShowingDescription ? "article description" : "full article"}`);
|
||||
this.updateDom(100);
|
||||
}
|
||||
});
|
||||
|
@@ -1,27 +1,31 @@
|
||||
{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %}
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{{ text | safe}}
|
||||
{% else %}
|
||||
{{ text }}
|
||||
{% endif %}
|
||||
{% if dangerouslyDisableAutoEscaping -%}
|
||||
{{ text | safe }}
|
||||
{%- else -%}
|
||||
{{ text }}
|
||||
{%- endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro escapeTitle(title, url, dangerouslyDisableAutoEscaping=false, showTitleAsUrl=false) %}
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}" style="text-decoration:none;color:#ffffff" target="_blank">{{ title | safe }}</a>
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
color:#ffffff"
|
||||
target="_blank">{{ title | safe }}</a>
|
||||
{% else %}
|
||||
{{ title | safe }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ title | safe}}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
color:#ffffff"
|
||||
target="_blank">{{ title }}</a>
|
||||
{% else %}
|
||||
{{ title }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}" style="text-decoration:none;color:#ffffff" target="_blank">{{ title }}</a>
|
||||
{% else %}
|
||||
{{ title }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% if loaded %}
|
||||
{% if config.showAsList %}
|
||||
<ul class="newsfeed-list">
|
||||
@@ -30,11 +34,9 @@
|
||||
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if item.sourceTitle and config.showSourceTitle %}
|
||||
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}
|
||||
{{ item.publishDate }}:
|
||||
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ item.publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
@@ -43,7 +45,7 @@
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(item.description | truncate(config.lengthDescription), config.dangerouslyDisableAutoEscaping) }}
|
||||
{{ escapeText(item.description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
@@ -57,11 +59,9 @@
|
||||
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if sourceTitle and config.showSourceTitle %}
|
||||
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}: {% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}
|
||||
{{ publishDate }}:
|
||||
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
@@ -70,7 +70,7 @@
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(description | truncate(config.lengthDescription), config.dangerouslyDisableAutoEscaping) }}
|
||||
{{ escapeText(description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
@@ -79,15 +79,11 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif empty %}
|
||||
<div class="small dimmed">
|
||||
{{ "NEWSFEED_NO_ITEMS" | translate | safe }}
|
||||
</div>
|
||||
<div class="small dimmed">{{ "NEWSFEED_NO_ITEMS" | translate | safe }}</div>
|
||||
{% elseif error %}
|
||||
<div class="small dimmed">
|
||||
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="small dimmed">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
</div>
|
||||
<div class="small dimmed">{{ "LOADING" | translate | safe }}</div>
|
||||
{% endif %}
|
||||
|
@@ -4,16 +4,16 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Log = require("logger");
|
||||
const FeedMe = require("feedme");
|
||||
const NodeHelper = require("node_helper");
|
||||
const fetch = require("fetch");
|
||||
const iconv = require("iconv-lite");
|
||||
|
||||
const stream = require("stream");
|
||||
const FeedMe = require("feedme");
|
||||
const iconv = require("iconv-lite");
|
||||
const { htmlToText } = require("html-to-text");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
|
||||
/**
|
||||
* Responsible for requesting an update on the set interval and broadcasting the data.
|
||||
*
|
||||
* @param {string} url URL of the news feed.
|
||||
* @param {number} reloadInterval Reload interval in milliseconds.
|
||||
* @param {string} encoding Encoding of the feed.
|
||||
@@ -24,12 +24,13 @@ const stream = require("stream");
|
||||
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings, useCorsProxy) {
|
||||
let reloadTimer = null;
|
||||
let items = [];
|
||||
let reloadIntervalMS = reloadInterval;
|
||||
|
||||
let fetchFailedCallback = function () {};
|
||||
let itemsReceivedCallback = function () {};
|
||||
|
||||
if (reloadInterval < 1000) {
|
||||
reloadInterval = 1000;
|
||||
if (reloadIntervalMS < 1000) {
|
||||
reloadIntervalMS = 1000;
|
||||
}
|
||||
|
||||
/* private methods */
|
||||
@@ -53,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,
|
||||
@@ -64,15 +67,14 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
} else if (logFeedWarnings) {
|
||||
Log.warn("Can't parse feed item:");
|
||||
Log.warn(item);
|
||||
Log.warn("Title: " + title);
|
||||
Log.warn("Description: " + description);
|
||||
Log.warn("Pubdate: " + pubdate);
|
||||
Log.warn(`Title: ${title}`);
|
||||
Log.warn(`Description: ${description}`);
|
||||
Log.warn(`Pubdate: ${pubdate}`);
|
||||
}
|
||||
});
|
||||
|
||||
parser.on("end", () => {
|
||||
this.broadcastItems();
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
parser.on("error", (error) => {
|
||||
@@ -80,22 +82,27 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
//"end" event is not broadcast if the feed is empty but "finish" is used for both
|
||||
parser.on("finish", () => {
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
parser.on("ttl", (minutes) => {
|
||||
try {
|
||||
// 86400000 = 24 hours is mentioned in the docs as maximum value:
|
||||
const ttlms = Math.min(minutes * 60 * 1000, 86400000);
|
||||
if (ttlms > reloadInterval) {
|
||||
reloadInterval = ttlms;
|
||||
Log.info("Newsfeed-Fetcher: reloadInterval set to ttl=" + reloadInterval + " for url " + url);
|
||||
if (ttlms > reloadIntervalMS) {
|
||||
reloadIntervalMS = ttlms;
|
||||
Log.info(`Newsfeed-Fetcher: reloadInterval set to ttl=${reloadIntervalMS} for url ${url}`);
|
||||
}
|
||||
} catch (error) {
|
||||
Log.warn("Newsfeed-Fetcher: feed ttl is no valid integer=" + minutes + " for url " + url);
|
||||
Log.warn(`Newsfeed-Fetcher: feed ttl is no valid integer=${minutes} for url ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
const headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version,
|
||||
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`,
|
||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache"
|
||||
};
|
||||
@@ -124,19 +131,18 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchNews();
|
||||
}, reloadInterval);
|
||||
}, reloadIntervalMS);
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
|
||||
/**
|
||||
* Update the reload interval, but only if we need to increase the speed.
|
||||
*
|
||||
* @param {number} interval Interval for the update in milliseconds.
|
||||
*/
|
||||
this.setReloadInterval = function (interval) {
|
||||
if (interval > 1000 && interval < reloadInterval) {
|
||||
reloadInterval = interval;
|
||||
if (interval > 1000 && interval < reloadIntervalMS) {
|
||||
reloadIntervalMS = interval;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -155,7 +161,7 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
Log.info("Newsfeed-Fetcher: No items to broadcast yet.");
|
||||
return;
|
||||
}
|
||||
Log.info("Newsfeed-Fetcher: Broadcasting " + items.length + " items.");
|
||||
Log.info(`Newsfeed-Fetcher: Broadcasting ${items.length} items.`);
|
||||
itemsReceivedCallback(this);
|
||||
};
|
||||
|
||||
|
@@ -6,13 +6,13 @@
|
||||
*/
|
||||
|
||||
const NodeHelper = require("node_helper");
|
||||
const NewsfeedFetcher = require("./newsfeedfetcher.js");
|
||||
const Log = require("logger");
|
||||
const NewsfeedFetcher = require("./newsfeedfetcher");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.log("Starting node helper for: " + this.name);
|
||||
Log.log(`Starting node helper for: ${this.name}`);
|
||||
this.fetchers = [];
|
||||
},
|
||||
|
||||
@@ -26,7 +26,6 @@ module.exports = NodeHelper.create({
|
||||
/**
|
||||
* Creates a fetcher for a new feed if it doesn't exist yet.
|
||||
* Otherwise it reuses the existing one.
|
||||
*
|
||||
* @param {object} feed The feed object
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
@@ -47,7 +46,7 @@ module.exports = NodeHelper.create({
|
||||
|
||||
let fetcher;
|
||||
if (typeof this.fetchers[url] === "undefined") {
|
||||
Log.log("Create new newsfetcher for url: " + url + " - Interval: " + reloadInterval);
|
||||
Log.log(`Create new newsfetcher for url: ${url} - Interval: ${reloadInterval}`);
|
||||
fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings, useCorsProxy);
|
||||
|
||||
fetcher.onReceive(() => {
|
||||
@@ -64,7 +63,7 @@ module.exports = NodeHelper.create({
|
||||
|
||||
this.fetchers[url] = fetcher;
|
||||
} else {
|
||||
Log.log("Use existing newsfetcher for url: " + url);
|
||||
Log.log(`Use existing newsfetcher for url: ${url}`);
|
||||
fetcher = this.fetchers[url];
|
||||
fetcher.setReloadInterval(reloadInterval);
|
||||
fetcher.broadcastItems();
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<div class="small bright">
|
||||
{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -9,6 +9,7 @@ const BASE_DIR = path.normalize(`${__dirname}/../../../`);
|
||||
class GitHelper {
|
||||
constructor() {
|
||||
this.gitRepos = [];
|
||||
this.gitResultList = [];
|
||||
}
|
||||
|
||||
getRefRegex(branch) {
|
||||
@@ -36,7 +37,7 @@ class GitHelper {
|
||||
async add(moduleName) {
|
||||
let moduleFolder = BASE_DIR;
|
||||
|
||||
if (moduleName !== "default") {
|
||||
if (moduleName !== "MagicMirror") {
|
||||
moduleFolder = `${moduleFolder}modules/${moduleName}`;
|
||||
}
|
||||
|
||||
@@ -68,7 +69,7 @@ class GitHelper {
|
||||
isBehindInStatus: false
|
||||
};
|
||||
|
||||
if (repo.module === "default") {
|
||||
if (repo.module === "MagicMirror") {
|
||||
// the hash is only needed for the mm repo
|
||||
const { stderr, stdout } = await this.execShell(`cd ${repo.folder} && git rev-parse HEAD`);
|
||||
|
||||
@@ -93,18 +94,21 @@ class GitHelper {
|
||||
// ## develop...origin/develop
|
||||
// ## master...origin/master [behind 8]
|
||||
// ## master...origin/master [ahead 8, behind 1]
|
||||
// ## HEAD (no branch)
|
||||
status = status.match(/## (.*)\.\.\.([^ ]*)(?: .*behind (\d+))?/);
|
||||
// examples for status:
|
||||
// [ '## develop...origin/develop', 'develop', 'origin/develop' ]
|
||||
// [ '## master...origin/master [behind 8]', 'master', 'origin/master', '8' ]
|
||||
// [ '## master...origin/master [ahead 8, behind 1]', 'master', 'origin/master', '1' ]
|
||||
gitInfo.current = status[1];
|
||||
gitInfo.tracking = status[2];
|
||||
if (status) {
|
||||
gitInfo.current = status[1];
|
||||
gitInfo.tracking = status[2];
|
||||
|
||||
if (status[3]) {
|
||||
// git fetch was already called before so `git status -sb` delivers already the behind number
|
||||
gitInfo.behind = parseInt(status[3]);
|
||||
gitInfo.isBehindInStatus = true;
|
||||
if (status[3]) {
|
||||
// git fetch was already called before so `git status -sb` delivers already the behind number
|
||||
gitInfo.behind = parseInt(status[3]);
|
||||
gitInfo.isBehindInStatus = true;
|
||||
}
|
||||
}
|
||||
|
||||
return gitInfo;
|
||||
@@ -113,15 +117,15 @@ class GitHelper {
|
||||
async getRepoInfo(repo) {
|
||||
const gitInfo = await this.getStatusInfo(repo);
|
||||
|
||||
if (!gitInfo) {
|
||||
if (!gitInfo || !gitInfo.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gitInfo.isBehindInStatus) {
|
||||
if (gitInfo.isBehindInStatus && (gitInfo.module !== "MagicMirror" || gitInfo.current !== "master")) {
|
||||
return gitInfo;
|
||||
}
|
||||
|
||||
const { stderr } = await this.execShell(`cd ${repo.folder} && git fetch --dry-run`);
|
||||
const { stderr } = await this.execShell(`cd ${repo.folder} && git fetch -n --dry-run`);
|
||||
|
||||
// example output:
|
||||
// From https://github.com/MichMich/MagicMirror
|
||||
@@ -129,16 +133,41 @@ class GitHelper {
|
||||
// here the result is in stderr (this is a git default, don't ask why ...)
|
||||
const matches = stderr.match(this.getRefRegex(gitInfo.current));
|
||||
|
||||
if (!matches || !matches[0]) {
|
||||
// no refs found, nothing to do
|
||||
return;
|
||||
// this is the default if there was no match from "git fetch -n --dry-run".
|
||||
// Its a fallback because if there was a real "git fetch", the above "git fetch -n --dry-run" would deliver nothing.
|
||||
let refDiff = `${gitInfo.current}..origin/${gitInfo.current}`;
|
||||
if (matches && matches[0]) {
|
||||
refDiff = matches[0];
|
||||
}
|
||||
|
||||
// get behind with refs
|
||||
try {
|
||||
const { stdout } = await this.execShell(`cd ${repo.folder} && git rev-list --ancestry-path --count ${matches[0]}`);
|
||||
const { stdout } = await this.execShell(`cd ${repo.folder} && git rev-list --ancestry-path --count ${refDiff}`);
|
||||
gitInfo.behind = parseInt(stdout);
|
||||
|
||||
// for MagicMirror-Repo and "master" branch avoid getting notified when no tag is in refDiff
|
||||
// so only releases are reported and we can change e.g. the README.md without sending notifications
|
||||
if (gitInfo.behind > 0 && gitInfo.module === "MagicMirror" && gitInfo.current === "master") {
|
||||
let tagList = "";
|
||||
try {
|
||||
const { stdout } = await this.execShell(`cd ${repo.folder} && git ls-remote -q --tags --refs`);
|
||||
tagList = stdout.trim();
|
||||
} catch (err) {
|
||||
Log.error(`Failed to get tag list for ${repo.module}: ${err}`);
|
||||
}
|
||||
// check if tag is between commits and only report behind > 0 if so
|
||||
try {
|
||||
const { stdout } = await this.execShell(`cd ${repo.folder} && git rev-list --ancestry-path ${refDiff}`);
|
||||
let cnt = 0;
|
||||
for (const ref of stdout.trim().split("\n")) {
|
||||
if (tagList.includes(ref)) cnt++; // tag found
|
||||
}
|
||||
if (cnt === 0) gitInfo.behind = 0;
|
||||
} catch (err) {
|
||||
Log.error(`Failed to get git revisions for ${repo.module}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
return gitInfo;
|
||||
} catch (err) {
|
||||
Log.error(`Failed to get git revisions for ${repo.module}: ${err}`);
|
||||
@@ -146,21 +175,38 @@ class GitHelper {
|
||||
}
|
||||
|
||||
async getRepos() {
|
||||
const gitResultList = [];
|
||||
this.gitResultList = [];
|
||||
|
||||
for (const repo of this.gitRepos) {
|
||||
try {
|
||||
const gitInfo = await this.getRepoInfo(repo);
|
||||
|
||||
if (gitInfo) {
|
||||
gitResultList.push(gitInfo);
|
||||
this.gitResultList.push(gitInfo);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error(`Failed to retrieve repo info for ${repo.module}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
return gitResultList;
|
||||
return this.gitResultList;
|
||||
}
|
||||
|
||||
async checkUpdates() {
|
||||
var updates = [];
|
||||
|
||||
const allRepos = await this.gitResultList.map((module) => {
|
||||
return new Promise((resolve) => {
|
||||
if (module.behind > 0 && module.module !== "MagicMirror") {
|
||||
Log.info(`Update found for module: ${module.module}`);
|
||||
updates.push(module);
|
||||
}
|
||||
resolve(module);
|
||||
});
|
||||
});
|
||||
await Promise.all(allRepos);
|
||||
|
||||
return updates;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
const GitHelper = require("./git_helper");
|
||||
const defaultModules = require("../defaultmodules");
|
||||
const NodeHelper = require("node_helper");
|
||||
const defaultModules = require("../defaultmodules");
|
||||
const GitHelper = require("./git_helper");
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
|
||||
@@ -19,19 +19,31 @@ module.exports = NodeHelper.create({
|
||||
}
|
||||
}
|
||||
|
||||
await this.gitHelper.add("default");
|
||||
if (!this.ignoreUpdateChecking("MagicMirror")) {
|
||||
await this.gitHelper.add("MagicMirror");
|
||||
}
|
||||
},
|
||||
|
||||
async socketNotificationReceived(notification, payload) {
|
||||
if (notification === "CONFIG") {
|
||||
this.config = payload;
|
||||
} else if (notification === "MODULES") {
|
||||
// if this is the 1st time thru the update check process
|
||||
if (!this.updateProcessStarted) {
|
||||
this.updateProcessStarted = true;
|
||||
await this.configureModules(payload);
|
||||
await this.performFetch();
|
||||
}
|
||||
switch (notification) {
|
||||
case "CONFIG":
|
||||
this.config = payload;
|
||||
break;
|
||||
case "MODULES":
|
||||
// if this is the 1st time thru the update check process
|
||||
if (!this.updateProcessStarted) {
|
||||
this.updateProcessStarted = true;
|
||||
await this.configureModules(payload);
|
||||
await this.performFetch();
|
||||
}
|
||||
break;
|
||||
case "SCAN_UPDATES":
|
||||
// 1st time of check allows to force new scan
|
||||
if (this.updateProcessStarted) {
|
||||
clearTimeout(this.updateTimer);
|
||||
await this.performFetch();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -42,15 +54,23 @@ module.exports = NodeHelper.create({
|
||||
this.sendSocketNotification("STATUS", repo);
|
||||
}
|
||||
|
||||
if (this.config.sendUpdatesNotifications) {
|
||||
const updates = await this.gitHelper.checkUpdates();
|
||||
if (updates.length) this.sendSocketNotification("UPDATES", updates);
|
||||
}
|
||||
|
||||
this.scheduleNextFetch(this.config.updateInterval);
|
||||
},
|
||||
|
||||
scheduleNextFetch(delay) {
|
||||
clearTimeout(this.updateTimer);
|
||||
|
||||
this.updateTimer = setTimeout(() => {
|
||||
this.performFetch();
|
||||
}, Math.max(delay, ONE_MINUTE));
|
||||
this.updateTimer = setTimeout(
|
||||
() => {
|
||||
this.performFetch();
|
||||
},
|
||||
Math.max(delay, ONE_MINUTE)
|
||||
);
|
||||
},
|
||||
|
||||
ignoreUpdateChecking(moduleName) {
|
||||
|
@@ -8,7 +8,8 @@ Module.register("updatenotification", {
|
||||
defaults: {
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
refreshInterval: 24 * 60 * 60 * 1000, // one day
|
||||
ignoreModules: []
|
||||
ignoreModules: [],
|
||||
sendUpdatesNotifications: false
|
||||
},
|
||||
|
||||
suspended: false,
|
||||
@@ -33,15 +34,25 @@ Module.register("updatenotification", {
|
||||
},
|
||||
|
||||
notificationReceived(notification) {
|
||||
if (notification === "DOM_OBJECTS_CREATED") {
|
||||
this.sendSocketNotification("CONFIG", this.config);
|
||||
this.sendSocketNotification("MODULES", Object.keys(Module.definitions));
|
||||
switch (notification) {
|
||||
case "DOM_OBJECTS_CREATED":
|
||||
this.sendSocketNotification("CONFIG", this.config);
|
||||
this.sendSocketNotification("MODULES", Object.keys(Module.definitions));
|
||||
break;
|
||||
case "SCAN_UPDATES":
|
||||
this.sendSocketNotification("SCAN_UPDATES");
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
socketNotificationReceived(notification, payload) {
|
||||
if (notification === "STATUS") {
|
||||
this.updateUI(payload);
|
||||
switch (notification) {
|
||||
case "STATUS":
|
||||
this.updateUI(payload);
|
||||
break;
|
||||
case "UPDATES":
|
||||
this.sendNotification("UPDATES", payload);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -77,7 +88,7 @@ Module.register("updatenotification", {
|
||||
|
||||
addFilters() {
|
||||
this.nunjucksEnvironment().addFilter("diffLink", (text, status) => {
|
||||
if (status.module !== "default") {
|
||||
if (status.module !== "MagicMirror") {
|
||||
return text;
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,15 @@
|
||||
{% if not suspended %}
|
||||
{% for name, status in moduleList %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>
|
||||
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "default" else "UPDATE_NOTIFICATION_MODULE" %}
|
||||
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="xsmall dimmed">
|
||||
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
|
||||
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for name, status in moduleList %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>
|
||||
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "MagicMirror" else "UPDATE_NOTIFICATION_MODULE" %}
|
||||
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="xsmall dimmed">
|
||||
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
|
||||
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
@@ -1,21 +1,22 @@
|
||||
/**
|
||||
* A function to make HTTP requests via the server to avoid CORS-errors.
|
||||
*
|
||||
* @param {string} url the url to fetch from
|
||||
* @param {string} type what contenttype to expect in the response, can be "json" or "xml"
|
||||
* @param {boolean} useCorsProxy A flag to indicate
|
||||
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||
* @returns {Promise} resolved when the fetch is done. The response headers is placed in a headers-property (provided the response does not allready contain a headers-property).
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
|
||||
* @returns {Promise} resolved when the fetch is done. The response headers is placed in a headers-property (provided the response does not already contain a headers-property).
|
||||
*/
|
||||
async function performWebRequest(url, type = "json", useCorsProxy = false, requestHeaders = undefined, expectedResponseHeaders = undefined) {
|
||||
const request = {};
|
||||
let requestUrl;
|
||||
if (useCorsProxy) {
|
||||
url = getCorsUrl(url, requestHeaders, expectedResponseHeaders);
|
||||
requestUrl = getCorsUrl(url, requestHeaders, expectedResponseHeaders);
|
||||
} else {
|
||||
requestUrl = url;
|
||||
request.headers = getHeadersToSend(requestHeaders);
|
||||
}
|
||||
const response = await fetch(url, request);
|
||||
const response = await fetch(requestUrl, request);
|
||||
const data = await response.text();
|
||||
|
||||
if (type === "xml") {
|
||||
@@ -33,10 +34,9 @@ async function performWebRequest(url, type = "json", useCorsProxy = false, reque
|
||||
|
||||
/**
|
||||
* Gets a URL that will be used when calling the CORS-method on the server.
|
||||
*
|
||||
* @param {string} url the url to fetch from
|
||||
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
|
||||
* @returns {string} to be used as URL when calling CORS-method on server.
|
||||
*/
|
||||
const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders) {
|
||||
@@ -64,7 +64,6 @@ const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders) {
|
||||
|
||||
/**
|
||||
* Gets the part of the CORS URL that represents the HTTP headers to send.
|
||||
*
|
||||
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||
* @returns {string} to be used as request-headers component in CORS URL.
|
||||
*/
|
||||
@@ -84,8 +83,7 @@ const getRequestHeaderString = function (requestHeaders) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets headers and values to attatch to the web request.
|
||||
*
|
||||
* Gets headers and values to attach to the web request.
|
||||
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||
* @returns {object} An object specifying name and value of the headers.
|
||||
*/
|
||||
@@ -101,9 +99,8 @@ const getHeadersToSend = (requestHeaders) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the part of the CORS URL that represents the expected HTTP headers to recieve.
|
||||
*
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||
* Gets the part of the CORS URL that represents the expected HTTP headers to receive.
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
|
||||
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
|
||||
*/
|
||||
const getExpectedResponseHeadersString = function (expectedResponseHeaders) {
|
||||
@@ -123,8 +120,7 @@ const getExpectedResponseHeadersString = function (expectedResponseHeaders) {
|
||||
|
||||
/**
|
||||
* Gets the values for the expected headers from the response.
|
||||
*
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
|
||||
* @param {Response} response the HTTP response
|
||||
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
|
||||
*/
|
||||
@@ -141,7 +137,36 @@ const getHeadersFromResponse = (expectedResponseHeaders, response) => {
|
||||
return responseHeaders;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format the time according to the config
|
||||
* @param {object} config The config of the module
|
||||
* @param {object} time time to format
|
||||
* @returns {string} The formatted time string
|
||||
*/
|
||||
const formatTime = (config, time) => {
|
||||
let date = moment(time);
|
||||
|
||||
if (config.timezone) {
|
||||
date = date.tz(config.timezone);
|
||||
}
|
||||
|
||||
if (config.timeFormat !== 24) {
|
||||
if (config.showPeriod) {
|
||||
if (config.showPeriodUpper) {
|
||||
return date.format("h:mm A");
|
||||
} else {
|
||||
return date.format("h:mm a");
|
||||
}
|
||||
} else {
|
||||
return date.format("h:mm");
|
||||
}
|
||||
}
|
||||
|
||||
return date.format("HH:mm");
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined")
|
||||
module.exports = {
|
||||
performWebRequest
|
||||
performWebRequest,
|
||||
formatTime
|
||||
};
|
||||
|
@@ -7,7 +7,8 @@
|
||||
{% if config.showWindDirection %}
|
||||
<sup>
|
||||
{% if config.showWindDirectionAsArrow %}
|
||||
<i class="fas fa-long-arrow-alt-up" style="transform:rotate({{ current.windDirection }}deg);"></i>
|
||||
<i class="fas fa-long-arrow-alt-down"
|
||||
style="transform:rotate({{ current.windFromDirection }}deg)"></i>
|
||||
{% else %}
|
||||
{{ current.cardinalWindDirection() | translate }}
|
||||
{% endif %}
|
||||
@@ -16,7 +17,7 @@
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if config.showHumidity and current.humidity %}
|
||||
<span>{{ current.humidity | decimalSymbol }}</span><sup> <i class="wi wi-humidity humidityIcon"></i></sup>
|
||||
<span>{{ current.humidity | decimalSymbol }}</span><sup> <i class="wi wi-humidity humidity-icon"></i></sup>
|
||||
{% endif %}
|
||||
{% if config.showSun %}
|
||||
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
|
||||
@@ -28,51 +29,55 @@
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if config.showUVIndex %}
|
||||
<td class="align-right bright uv-index">
|
||||
<div class="wi dimmed wi-hot"></div>
|
||||
{{ current.uv_index }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="large light">
|
||||
<span class="wi weathericon wi-{{current.weatherType}}"></span>
|
||||
<span class="bright">
|
||||
{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</span>
|
||||
<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>
|
||||
{% if (config.showFeelsLike or config.showPrecipitationAmount) and not config.onlyTemp %}
|
||||
{% if (config.showFeelsLike or config.showPrecipitationAmount or config.showPrecipitationProbability) and not config.onlyTemp %}
|
||||
<div class="normal medium feelslike">
|
||||
{% if config.showFeelsLike %}
|
||||
<span class="dimmed">
|
||||
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
|
||||
</span>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationAmount %}
|
||||
{% if config.showPrecipitationAmount and current.precipitationAmount %}
|
||||
<span class="dimmed">
|
||||
{{ "PRECIP" | translate }} {{ current.precipitation | unit("precip") }}
|
||||
<span class="precipitationLeadText">{{ "PRECIP_AMOUNT" | translate }}</span> {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }}
|
||||
</span>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability and current.precipitationProbability %}
|
||||
<span class="dimmed">
|
||||
<span class="precipitationLeadText">{{ "PRECIP_POP" | translate }}</span> {{ current.precipitationProbability }}%
|
||||
</span>
|
||||
{% endif %}
|
||||
</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> -->
|
||||
|
@@ -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>
|
||||
@@ -23,25 +26,27 @@
|
||||
{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</td>
|
||||
{% if config.showPrecipitationAmount %}
|
||||
{% if f.precipitationUnits %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ f.precipitation }}{{ f.precipitationUnits }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ f.precipitation | unit("precip") }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="align-right bright precipitation-amount">
|
||||
{{ f.precipitationAmount | unit("precip", f.precipitationUnits) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability %}
|
||||
<td class="align-right bright precipitation-prob">
|
||||
{{ f.precipitationProbability | unit('precip', '%') }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showUVIndex %}
|
||||
<td class="align-right dimmed uv-index">
|
||||
{{ f.uv_index }}
|
||||
<span class="wi dimmed weathericon wi-hot"></span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% set currentStep = currentStep + 1 %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate }}
|
||||
</div>
|
||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Uncomment the line below to see the contents of the `forecast` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{forecast | dump}}</div> -->
|
||||
|
@@ -4,32 +4,39 @@
|
||||
<table class="{{ config.tableClass }}">
|
||||
{% set hours = hourly.slice(0, numSteps) %}
|
||||
{% for hour in hours %}
|
||||
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||
<tr {% if config.colored %}class="colored"{% endif %}
|
||||
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||
<td class="day">{{ hour.date | formatTime }}</td>
|
||||
<td class="bright weather-icon"><span class="wi weathericon wi-{{ hour.weatherType }}"></span></td>
|
||||
<td class="bright weather-icon">
|
||||
<span class="wi weathericon wi-{{ hour.weatherType }}"></span>
|
||||
</td>
|
||||
<td class="align-right bright">
|
||||
{{ hour.temperature | roundValue | unit("temperature") }}
|
||||
</td>
|
||||
{% if config.showUVIndex %}
|
||||
<td class="align-right bright uv-index">
|
||||
{% if hour.uv_index!=0 %}
|
||||
{{ hour.uv_index }}
|
||||
<span class="wi weathericon wi-hot"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationAmount %}
|
||||
{% if hour.precipitationUnits %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ hour.precipitation }}{{ hour.precipitationUnits }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ hour.precipitation | unit("precip") }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="align-right bright precipitation-amount">
|
||||
{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability %}
|
||||
<td class="align-right bright precipitation-prob">
|
||||
{{ hour.precipitationProbability | unit('precip', '%') }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% set currentStep = currentStep + 1 %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate }}
|
||||
</div>
|
||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{hourly | dump}}</div> -->
|
||||
|
@@ -138,7 +138,7 @@ WeatherProvider.register("envcanada", {
|
||||
// being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
|
||||
//
|
||||
getUrl() {
|
||||
return "https://dd.weather.gc.ca/citypage_weather/xml/" + this.config.provCode + "/" + this.config.siteCode + "_e.xml";
|
||||
return `https://dd.weather.gc.ca/citypage_weather/xml/${this.config.provCode}/${this.config.siteCode}_e.xml`;
|
||||
},
|
||||
|
||||
//
|
||||
@@ -165,7 +165,7 @@ WeatherProvider.register("envcanada", {
|
||||
|
||||
currentWeather.windSpeed = WeatherUtils.convertWindToMs(ECdoc.querySelector("siteData currentConditions wind speed").textContent);
|
||||
|
||||
currentWeather.windDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
|
||||
currentWeather.windFromDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
|
||||
|
||||
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
|
||||
|
||||
@@ -230,12 +230,7 @@ WeatherProvider.register("envcanada", {
|
||||
|
||||
const foreGroup = ECdoc.querySelectorAll("siteData forecastGroup forecast");
|
||||
|
||||
// For simplicity, we will only accumulate precipitation and will not try to break out
|
||||
// rain vs snow accumulations
|
||||
|
||||
weather.rain = null;
|
||||
weather.snow = null;
|
||||
weather.precipitation = null;
|
||||
weather.precipitationAmount = null;
|
||||
|
||||
//
|
||||
// The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
|
||||
@@ -336,16 +331,14 @@ WeatherProvider.register("envcanada", {
|
||||
// Add 1 to the date to reflect the current forecast day we are building
|
||||
|
||||
lastDate = lastDate.add(1, "day");
|
||||
weather.date = moment.unix(lastDate);
|
||||
weather.date = moment(lastDate);
|
||||
|
||||
// Capture the temperatures for the current Element and the next Element in order to set
|
||||
// the Min and Max temperatures for the forecast
|
||||
|
||||
this.setMinMaxTemps(weather, foreGroup, stepDay, true, currentTemp);
|
||||
|
||||
weather.rain = null;
|
||||
weather.snow = null;
|
||||
weather.precipitation = null;
|
||||
weather.precipitationAmount = null;
|
||||
|
||||
this.setPrecipitation(weather, foreGroup, stepDay);
|
||||
|
||||
@@ -391,7 +384,7 @@ WeatherProvider.register("envcanada", {
|
||||
|
||||
const foreTime = moment(hourGroup[stepHour].getAttribute("dateTimeUTC"), "YYYYMMDDhhmmss");
|
||||
const currTime = foreTime.add(hourOffset, "hours");
|
||||
weather.date = moment.unix(currTime);
|
||||
weather.date = moment(currTime);
|
||||
|
||||
// Capture the temperature
|
||||
|
||||
@@ -402,8 +395,7 @@ WeatherProvider.register("envcanada", {
|
||||
const precipLOP = hourGroup[stepHour].querySelector("lop").textContent * 1.0;
|
||||
|
||||
if (precipLOP > 0) {
|
||||
weather.precipitation = precipLOP;
|
||||
weather.precipitationUnits = hourGroup[stepHour].querySelector("lop").getAttribute("units");
|
||||
weather.precipitationProbability = precipLOP;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -508,27 +500,14 @@ WeatherProvider.register("envcanada", {
|
||||
|
||||
setPrecipitation(weather, foreGroup, today) {
|
||||
if (foreGroup[today].querySelector("precipitation accumulation")) {
|
||||
weather.precipitation = foreGroup[today].querySelector("precipitation accumulation amount").textContent * 1.0;
|
||||
|
||||
weather.precipitationUnits = " " + foreGroup[today].querySelector("precipitation accumulation amount").getAttribute("units");
|
||||
|
||||
if (this.config.units === "imperial") {
|
||||
if (weather.precipitationUnits === " cm") {
|
||||
weather.precipitation = (weather.precipitation * 0.394).toFixed(2);
|
||||
weather.precipitationUnits = " in";
|
||||
}
|
||||
if (weather.precipitationUnits === " mm") {
|
||||
weather.precipitation = (weather.precipitation * 0.0394).toFixed(2);
|
||||
weather.precipitationUnits = " in";
|
||||
}
|
||||
}
|
||||
weather.precipitationAmount = foreGroup[today].querySelector("precipitation accumulation amount").textContent * 1.0;
|
||||
weather.precipitationUnits = foreGroup[today].querySelector("precipitation accumulation amount").getAttribute("units");
|
||||
}
|
||||
|
||||
// Check Today element for POP
|
||||
|
||||
if (foreGroup[today].querySelector("abbreviatedForecast pop").textContent > 0) {
|
||||
weather.precipitation = foreGroup[today].querySelector("abbreviatedForecast pop").textContent;
|
||||
weather.precipitationUnits = foreGroup[today].querySelector("abbreviatedForecast pop").getAttribute("units");
|
||||
const precipPOP = foreGroup[today].querySelector("abbreviatedForecast pop").textContent * 1.0;
|
||||
if (precipPOP > 0) {
|
||||
weather.precipitationProbability = precipPOP;
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -24,7 +24,7 @@ WeatherProvider.register("openmeteo", {
|
||||
apiBase: OPEN_METEO_BASE,
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
past_days: 0,
|
||||
pastDays: 0,
|
||||
type: "current"
|
||||
},
|
||||
|
||||
@@ -76,6 +76,10 @@ WeatherProvider.register("openmeteo", {
|
||||
"et0_fao_evapotranspiration",
|
||||
// Total precipitation (rain, showers, snow) sum of the preceding hour
|
||||
"precipitation",
|
||||
// Precipitation Probability
|
||||
"precipitation_probability",
|
||||
// UV index
|
||||
"uv_index",
|
||||
// Snowfall amount of the preceding hour in centimeters. For the water equivalent in millimeter, divide by 7. E.g. 7 cm snow = 10 mm precipitation water equivalent
|
||||
"snowfall",
|
||||
// Rain from large scale weather systems of the preceding hour in millimeter
|
||||
@@ -130,6 +134,8 @@ WeatherProvider.register("openmeteo", {
|
||||
"winddirection_10m_dominant",
|
||||
// The sum of solar radiation on a given day in Megajoules
|
||||
"shortwave_radiation_sum",
|
||||
//UV Index
|
||||
"uv_index_max",
|
||||
// Daily sum of ET₀ Reference Evapotranspiration of a well watered grass field
|
||||
"et0_fao_evapotranspiration"
|
||||
],
|
||||
@@ -194,7 +200,6 @@ WeatherProvider.register("openmeteo", {
|
||||
|
||||
/**
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
*
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
@@ -227,12 +232,12 @@ WeatherProvider.register("openmeteo", {
|
||||
longitude: this.config.lon,
|
||||
timeformat: "unixtime",
|
||||
timezone: "auto",
|
||||
past_days: this.config.past_days ?? 0,
|
||||
past_days: this.config.pastDays ?? 0,
|
||||
daily: this.dailyParams,
|
||||
hourly: this.hourlyParams,
|
||||
// Fixed units as metric
|
||||
temperature_unit: "celsius",
|
||||
windspeed_unit: "kmh",
|
||||
windspeed_unit: "ms",
|
||||
precipitation_unit: "mm"
|
||||
};
|
||||
|
||||
@@ -264,9 +269,9 @@ WeatherProvider.register("openmeteo", {
|
||||
switch (key) {
|
||||
case "hourly":
|
||||
case "daily":
|
||||
return encodeURIComponent(key) + "=" + params[key].join(",");
|
||||
return `${encodeURIComponent(key)}=${params[key].join(",")}`;
|
||||
default:
|
||||
return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`;
|
||||
}
|
||||
})
|
||||
.join("&");
|
||||
@@ -367,11 +372,11 @@ WeatherProvider.register("openmeteo", {
|
||||
* `current_weather` object.
|
||||
*/
|
||||
const h = moment().hour();
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = weather.current_weather.time;
|
||||
currentWeather.windSpeed = weather.current_weather.windspeed;
|
||||
currentWeather.windDirection = weather.current_weather.winddirection;
|
||||
currentWeather.windFromDirection = weather.current_weather.winddirection;
|
||||
currentWeather.sunrise = weather.daily[0].sunrise;
|
||||
currentWeather.sunset = weather.daily[0].sunset;
|
||||
currentWeather.temperature = parseFloat(weather.current_weather.temperature);
|
||||
@@ -381,7 +386,9 @@ WeatherProvider.register("openmeteo", {
|
||||
currentWeather.humidity = parseFloat(weather.hourly[h].relativehumidity_2m);
|
||||
currentWeather.rain = parseFloat(weather.hourly[h].rain);
|
||||
currentWeather.snow = parseFloat(weather.hourly[h].snowfall * 10);
|
||||
currentWeather.precipitation = parseFloat(weather.hourly[h].precipitation);
|
||||
currentWeather.precipitationAmount = parseFloat(weather.hourly[h].precipitation);
|
||||
currentWeather.precipitationProbability = parseFloat(weather.hourly[h].precipitation_probability);
|
||||
currentWeather.uv_index = parseFloat(weather.hourly[h].uv_index);
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
@@ -391,11 +398,11 @@ WeatherProvider.register("openmeteo", {
|
||||
const days = [];
|
||||
|
||||
weathers.daily.forEach((weather, i) => {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = weather.time;
|
||||
currentWeather.windSpeed = weather.windspeed_10m_max;
|
||||
currentWeather.windDirection = weather.winddirection_10m_dominant;
|
||||
currentWeather.windFromDirection = weather.winddirection_10m_dominant;
|
||||
currentWeather.sunrise = weather.sunrise;
|
||||
currentWeather.sunset = weather.sunset;
|
||||
currentWeather.temperature = parseFloat((weather.apparent_temperature_max + weather.apparent_temperature_min) / 2);
|
||||
@@ -404,7 +411,9 @@ WeatherProvider.register("openmeteo", {
|
||||
currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime());
|
||||
currentWeather.rain = parseFloat(weather.rain_sum);
|
||||
currentWeather.snow = parseFloat(weather.snowfall_sum * 10);
|
||||
currentWeather.precipitation = parseFloat(weather.precipitation_sum);
|
||||
currentWeather.precipitationAmount = parseFloat(weather.precipitation_sum);
|
||||
currentWeather.precipitationProbability = parseFloat(weather.precipitation_probability);
|
||||
currentWeather.uv_index = parseFloat(weather.uv_index_max);
|
||||
|
||||
days.push(currentWeather);
|
||||
});
|
||||
@@ -422,12 +431,12 @@ WeatherProvider.register("openmeteo", {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject();
|
||||
const h = Math.ceil((i + 1) / 24) - 1;
|
||||
|
||||
currentWeather.date = weather.time;
|
||||
currentWeather.windSpeed = weather.windspeed_10m;
|
||||
currentWeather.windDirection = weather.winddirection_10m;
|
||||
currentWeather.windFromDirection = weather.winddirection_10m;
|
||||
currentWeather.sunrise = weathers.daily[h].sunrise;
|
||||
currentWeather.sunset = weathers.daily[h].sunset;
|
||||
currentWeather.temperature = parseFloat(weather.apparent_temperature);
|
||||
@@ -437,7 +446,9 @@ WeatherProvider.register("openmeteo", {
|
||||
currentWeather.humidity = parseFloat(weather.relativehumidity_2m);
|
||||
currentWeather.rain = parseFloat(weather.rain);
|
||||
currentWeather.snow = parseFloat(weather.snowfall * 10);
|
||||
currentWeather.precipitation = parseFloat(weather.precipitation);
|
||||
currentWeather.precipitationAmount = parseFloat(weather.precipitation);
|
||||
currentWeather.precipitationProbability = parseFloat(weather.precipitation_probability);
|
||||
currentWeather.uv_index = parseFloat(weather.uv_index);
|
||||
|
||||
hours.push(currentWeather);
|
||||
});
|
||||
|
@@ -90,7 +90,6 @@ WeatherProvider.register("openweathermap", {
|
||||
|
||||
/**
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
*
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
@@ -132,7 +131,7 @@ WeatherProvider.register("openweathermap", {
|
||||
currentWeather.temperature = currentWeatherData.main.temp;
|
||||
currentWeather.feelsLikeTemp = currentWeatherData.main.feels_like;
|
||||
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
||||
currentWeather.windDirection = currentWeatherData.wind.deg;
|
||||
currentWeather.windFromDirection = currentWeatherData.wind.deg;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
||||
currentWeather.sunrise = moment.unix(currentWeatherData.sys.sunrise);
|
||||
currentWeather.sunset = moment.unix(currentWeatherData.sys.sunset);
|
||||
@@ -145,9 +144,9 @@ WeatherProvider.register("openweathermap", {
|
||||
*/
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
if (this.config.weatherEndpoint === "/forecast") {
|
||||
return this.fetchForecastHourly(forecasts);
|
||||
return this.generateForecastHourly(forecasts);
|
||||
} else if (this.config.weatherEndpoint === "/forecast/daily") {
|
||||
return this.fetchForecastDaily(forecasts);
|
||||
return this.generateForecastDaily(forecasts);
|
||||
}
|
||||
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
|
||||
return [new WeatherObject()];
|
||||
@@ -165,9 +164,10 @@ WeatherProvider.register("openweathermap", {
|
||||
},
|
||||
|
||||
/*
|
||||
* fetch forecast information for 3-hourly forecast (available for free subscription).
|
||||
* Generate forecast information for 3-hourly forecast (available for free
|
||||
* subscription).
|
||||
*/
|
||||
fetchForecastHourly(forecasts) {
|
||||
generateForecastHourly(forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
// variables for temperature range and rain
|
||||
@@ -186,7 +186,7 @@ WeatherProvider.register("openweathermap", {
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
weather.rain = rain;
|
||||
weather.snow = snow;
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
// create new weather-object
|
||||
@@ -216,20 +216,12 @@ WeatherProvider.register("openweathermap", {
|
||||
minTemp.push(forecast.main.temp_min);
|
||||
maxTemp.push(forecast.main.temp_max);
|
||||
|
||||
if (forecast.hasOwnProperty("rain")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.rain["3h"])) {
|
||||
rain += forecast.rain["3h"] / 25.4;
|
||||
} else if (!isNaN(forecast.rain["3h"])) {
|
||||
rain += forecast.rain["3h"];
|
||||
}
|
||||
if (forecast.hasOwnProperty("rain") && !isNaN(forecast.rain["3h"])) {
|
||||
rain += forecast.rain["3h"];
|
||||
}
|
||||
|
||||
if (forecast.hasOwnProperty("snow")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.snow["3h"])) {
|
||||
snow += forecast.snow["3h"] / 25.4;
|
||||
} else if (!isNaN(forecast.snow["3h"])) {
|
||||
snow += forecast.snow["3h"];
|
||||
}
|
||||
if (forecast.hasOwnProperty("snow") && !isNaN(forecast.snow["3h"])) {
|
||||
snow += forecast.snow["3h"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,16 +231,17 @@ WeatherProvider.register("openweathermap", {
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
weather.rain = rain;
|
||||
weather.snow = snow;
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
return days.slice(1);
|
||||
},
|
||||
|
||||
/*
|
||||
* fetch forecast information for daily forecast (available for paid subscription or old apiKey).
|
||||
* Generate forecast information for daily forecast (available for paid
|
||||
* subscription or old apiKey).
|
||||
*/
|
||||
fetchForecastDaily(forecasts) {
|
||||
generateForecastDaily(forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
|
||||
@@ -264,25 +257,18 @@ WeatherProvider.register("openweathermap", {
|
||||
|
||||
// forecast.rain not available if amount is zero
|
||||
// The API always returns in millimeters
|
||||
if (forecast.hasOwnProperty("rain")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.rain)) {
|
||||
weather.rain = forecast.rain / 25.4;
|
||||
} else if (!isNaN(forecast.rain)) {
|
||||
weather.rain = forecast.rain;
|
||||
}
|
||||
if (forecast.hasOwnProperty("rain") && !isNaN(forecast.rain)) {
|
||||
weather.rain = forecast.rain;
|
||||
}
|
||||
|
||||
// forecast.snow not available if amount is zero
|
||||
// The API always returns in millimeters
|
||||
if (forecast.hasOwnProperty("snow")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.snow)) {
|
||||
weather.snow = forecast.snow / 25.4;
|
||||
} else if (!isNaN(forecast.snow)) {
|
||||
weather.snow = forecast.snow;
|
||||
}
|
||||
if (forecast.hasOwnProperty("snow") && !isNaN(forecast.snow)) {
|
||||
weather.snow = forecast.snow;
|
||||
}
|
||||
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
weather.precipitationAmount = weather.rain + weather.snow;
|
||||
weather.precipitationProbability = forecast.pop ? forecast.pop * 100 : undefined;
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
@@ -303,30 +289,23 @@ WeatherProvider.register("openweathermap", {
|
||||
if (data.hasOwnProperty("current")) {
|
||||
current.date = moment.unix(data.current.dt).utcOffset(data.timezone_offset / 60);
|
||||
current.windSpeed = data.current.wind_speed;
|
||||
current.windDirection = data.current.wind_deg;
|
||||
current.windFromDirection = data.current.wind_deg;
|
||||
current.sunrise = moment.unix(data.current.sunrise).utcOffset(data.timezone_offset / 60);
|
||||
current.sunset = moment.unix(data.current.sunset).utcOffset(data.timezone_offset / 60);
|
||||
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"])) {
|
||||
if (this.config.units === "imperial") {
|
||||
current.rain = data.current["rain"]["1h"] / 25.4;
|
||||
} else {
|
||||
current.rain = data.current["rain"]["1h"];
|
||||
}
|
||||
current.rain = data.current["rain"]["1h"];
|
||||
precip = true;
|
||||
}
|
||||
if (data.current.hasOwnProperty("snow") && !isNaN(data.current["snow"]["1h"])) {
|
||||
if (this.config.units === "imperial") {
|
||||
current.snow = data.current["snow"]["1h"] / 25.4;
|
||||
} else {
|
||||
current.snow = data.current["snow"]["1h"];
|
||||
}
|
||||
current.snow = data.current["snow"]["1h"];
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
current.precipitation = current.rain + current.snow;
|
||||
current.precipitationAmount = (current.rain ?? 0) + (current.snow ?? 0);
|
||||
}
|
||||
current.feelsLikeTemp = data.current.feels_like;
|
||||
}
|
||||
@@ -342,27 +321,21 @@ WeatherProvider.register("openweathermap", {
|
||||
weather.feelsLikeTemp = hour.feels_like;
|
||||
weather.humidity = hour.humidity;
|
||||
weather.windSpeed = hour.wind_speed;
|
||||
weather.windDirection = hour.wind_deg;
|
||||
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"])) {
|
||||
if (this.config.units === "imperial") {
|
||||
weather.rain = hour.rain["1h"] / 25.4;
|
||||
} else {
|
||||
weather.rain = hour.rain["1h"];
|
||||
}
|
||||
weather.rain = hour.rain["1h"];
|
||||
precip = true;
|
||||
}
|
||||
if (hour.hasOwnProperty("snow") && !isNaN(hour.snow["1h"])) {
|
||||
if (this.config.units === "imperial") {
|
||||
weather.snow = hour.snow["1h"] / 25.4;
|
||||
} else {
|
||||
weather.snow = hour.snow["1h"];
|
||||
}
|
||||
weather.snow = hour.snow["1h"];
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
||||
}
|
||||
|
||||
hours.push(weather);
|
||||
@@ -381,27 +354,21 @@ WeatherProvider.register("openweathermap", {
|
||||
weather.maxTemperature = day.temp.max;
|
||||
weather.humidity = day.humidity;
|
||||
weather.windSpeed = day.wind_speed;
|
||||
weather.windDirection = day.wind_deg;
|
||||
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)) {
|
||||
if (this.config.units === "imperial") {
|
||||
weather.rain = day.rain / 25.4;
|
||||
} else {
|
||||
weather.rain = day.rain;
|
||||
}
|
||||
weather.rain = day.rain;
|
||||
precip = true;
|
||||
}
|
||||
if (!isNaN(day.snow)) {
|
||||
if (this.config.units === "imperial") {
|
||||
weather.snow = day.snow / 25.4;
|
||||
} else {
|
||||
weather.snow = day.snow;
|
||||
}
|
||||
weather.snow = day.snow;
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
||||
}
|
||||
|
||||
days.push(weather);
|
||||
@@ -448,8 +415,8 @@ WeatherProvider.register("openweathermap", {
|
||||
getParams() {
|
||||
let params = "?";
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
params += "lat=" + this.config.lat;
|
||||
params += "&lon=" + this.config.lon;
|
||||
params += `lat=${this.config.lat}`;
|
||||
params += `&lon=${this.config.lon}`;
|
||||
if (this.config.type === "current") {
|
||||
params += "&exclude=minutely,hourly,daily";
|
||||
} else if (this.config.type === "hourly") {
|
||||
@@ -460,23 +427,24 @@ WeatherProvider.register("openweathermap", {
|
||||
params += "&exclude=minutely";
|
||||
}
|
||||
} else if (this.config.lat && this.config.lon) {
|
||||
params += "lat=" + this.config.lat + "&lon=" + this.config.lon;
|
||||
params += `lat=${this.config.lat}&lon=${this.config.lon}`;
|
||||
} else if (this.config.locationID) {
|
||||
params += "id=" + this.config.locationID;
|
||||
params += `id=${this.config.locationID}`;
|
||||
} else if (this.config.location) {
|
||||
params += "q=" + this.config.location;
|
||||
params += `q=${this.config.location}`;
|
||||
} else if (this.firstEvent && this.firstEvent.geo) {
|
||||
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon;
|
||||
params += `lat=${this.firstEvent.geo.lat}&lon=${this.firstEvent.geo.lon}`;
|
||||
} else if (this.firstEvent && this.firstEvent.location) {
|
||||
params += "q=" + this.firstEvent.location;
|
||||
params += `q=${this.firstEvent.location}`;
|
||||
} else {
|
||||
// TODO hide doesnt exist!
|
||||
this.hide(this.config.animationSpeed, { lockString: this.identifier });
|
||||
return;
|
||||
}
|
||||
|
||||
params += "&units=metric"; // WeatherProviders should use metric internally and use the units only for when displaying data
|
||||
params += "&lang=" + this.config.lang;
|
||||
params += "&APPID=" + this.config.apiKey;
|
||||
params += `&lang=${this.config.lang}`;
|
||||
params += `&APPID=${this.config.apiKey}`;
|
||||
|
||||
return params;
|
||||
}
|
||||
|
112
modules/default/weather/providers/overrideWrapper.js
Normal file
112
modules/default/weather/providers/overrideWrapper.js
Normal 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();
|
||||
}
|
||||
});
|
@@ -2,24 +2,23 @@
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Dark Sky
|
||||
* Provider: Pirate Weather
|
||||
*
|
||||
* By Nicholas Hubbard https://github.com/nhubbard
|
||||
* Written by Nicholas Hubbard https://github.com/nhubbard for formerly Dark Sky Provider
|
||||
* Modified by Karsten Hassel for Pirate Weather
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Dark Sky.
|
||||
* Note that the Dark Sky API does not provide rainfall. Instead it provides
|
||||
* snowfall and precipitation probability
|
||||
* This class is a provider for Pirate Weather, it is a replacement for Dark Sky (same api).
|
||||
*/
|
||||
WeatherProvider.register("darksky", {
|
||||
WeatherProvider.register("pirateweather", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
providerName: "Dark Sky",
|
||||
providerName: "pirateweather",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
useCorsProxy: true,
|
||||
apiBase: "https://api.darksky.net",
|
||||
apiBase: "https://api.pirateweather.net",
|
||||
weatherEndpoint: "/forecast",
|
||||
apiKey: "",
|
||||
lat: 0,
|
||||
@@ -73,7 +72,7 @@ WeatherProvider.register("darksky", {
|
||||
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
||||
currentWeather.temperature = parseFloat(currentWeatherData.currently.temperature);
|
||||
currentWeather.windSpeed = parseFloat(currentWeatherData.currently.windSpeed);
|
||||
currentWeather.windDirection = currentWeatherData.currently.windBearing;
|
||||
currentWeather.windFromDirection = currentWeatherData.currently.windBearing;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.currently.icon);
|
||||
currentWeather.sunrise = moment.unix(currentWeatherData.daily.data[0].sunriseTime);
|
||||
currentWeather.sunset = moment.unix(currentWeatherData.daily.data[0].sunsetTime);
|
||||
@@ -92,19 +91,21 @@ WeatherProvider.register("darksky", {
|
||||
weather.maxTemperature = forecast.temperatureMax;
|
||||
weather.weatherType = this.convertWeatherType(forecast.icon);
|
||||
weather.snow = 0;
|
||||
weather.rain = 0;
|
||||
|
||||
// The API will return centimeters if units is 'si' and will return inches for 'us'
|
||||
// Note that the Dark Sky API does not provide rainfall.
|
||||
// Instead it provides snowfall and precipitation probability
|
||||
let precip = 0;
|
||||
if (forecast.hasOwnProperty("precipAccumulation")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.precipAccumulation)) {
|
||||
weather.snow = forecast.precipAccumulation;
|
||||
} else if (!isNaN(forecast.precipAccumulation)) {
|
||||
weather.snow = forecast.precipAccumulation * 10;
|
||||
}
|
||||
precip = forecast.precipAccumulation * 10;
|
||||
}
|
||||
|
||||
weather.precipitation = weather.snow;
|
||||
weather.precipitationAmount = precip;
|
||||
if (forecast.hasOwnProperty("precipType")) {
|
||||
if (forecast.precipType === "snow") {
|
||||
weather.snow = precip;
|
||||
} else {
|
||||
weather.rain = precip;
|
||||
}
|
||||
}
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
@@ -112,7 +113,7 @@ WeatherProvider.register("darksky", {
|
||||
return days;
|
||||
},
|
||||
|
||||
// Map icons from Dark Sky to our icons.
|
||||
// Map icons from Pirate Weather to our icons.
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
"clear-day": "day-sunny",
|
@@ -33,7 +33,7 @@ WeatherProvider.register("smhi", {
|
||||
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setCurrentWeather(weatherObject);
|
||||
})
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ WeatherProvider.register("smhi", {
|
||||
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setWeatherForecast(weatherObjects);
|
||||
})
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
@@ -63,26 +63,24 @@ WeatherProvider.register("smhi", {
|
||||
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setWeatherHourly(weatherObjects);
|
||||
})
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
|
||||
*
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = config;
|
||||
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) === -1) {
|
||||
Log.log("invalid or not set: " + config.precipitationValue);
|
||||
Log.log(`invalid or not set: ${config.precipitationValue}`);
|
||||
config.precipitationValue = this.defaults.precipitationValue;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Of all the times returned find out which one is closest to the current time, should be the first if the data isn't old.
|
||||
*
|
||||
* @param {object[]} times Array of time objects
|
||||
* @returns {object} The weatherdata closest to the current time
|
||||
*/
|
||||
@@ -100,7 +98,6 @@ WeatherProvider.register("smhi", {
|
||||
|
||||
/**
|
||||
* Get the forecast url for the configured coordinates
|
||||
*
|
||||
* @returns {string} the url for the specified coordinates
|
||||
*/
|
||||
getURL() {
|
||||
@@ -115,7 +112,6 @@ WeatherProvider.register("smhi", {
|
||||
|
||||
/**
|
||||
* Calculates the apparent temperature based on known atmospheric data.
|
||||
*
|
||||
* @param {object} weatherData Weatherdata to use for the calculation
|
||||
* @returns {number} The apparent temperature
|
||||
*/
|
||||
@@ -132,7 +128,6 @@ WeatherProvider.register("smhi", {
|
||||
* Converts the returned data into a WeatherObject with required properties set for both current weather and forecast.
|
||||
* The returned units is always in metric system.
|
||||
* Requires coordinates to determine if its daytime or nighttime to know which icon to use and also to set sunrise and sunset.
|
||||
*
|
||||
* @param {object} weatherData Weatherdata to convert
|
||||
* @param {object} coordinates Coordinates of the locations of the weather
|
||||
* @returns {WeatherObject} The converted weatherdata at the specified location
|
||||
@@ -145,7 +140,7 @@ WeatherProvider.register("smhi", {
|
||||
currentWeather.humidity = this.paramValue(weatherData, "r");
|
||||
currentWeather.temperature = this.paramValue(weatherData, "t");
|
||||
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
|
||||
currentWeather.windDirection = this.paramValue(weatherData, "wd");
|
||||
currentWeather.windFromDirection = this.paramValue(weatherData, "wd");
|
||||
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), currentWeather.isDayTime());
|
||||
currentWeather.feelsLikeTemp = this.calculateApparentTemperature(weatherData);
|
||||
|
||||
@@ -157,19 +152,19 @@ WeatherProvider.register("smhi", {
|
||||
// 0 = No precipitation
|
||||
case 1: // Snow
|
||||
currentWeather.snow += precipitationValue;
|
||||
currentWeather.precipitation += precipitationValue;
|
||||
currentWeather.precipitationAmount += precipitationValue;
|
||||
break;
|
||||
case 2: // Snow and rain, treat it as 50/50 snow and rain
|
||||
currentWeather.snow += precipitationValue / 2;
|
||||
currentWeather.rain += precipitationValue / 2;
|
||||
currentWeather.precipitation += precipitationValue;
|
||||
currentWeather.precipitationAmount += precipitationValue;
|
||||
break;
|
||||
case 3: // Rain
|
||||
case 4: // Drizzle
|
||||
case 5: // Freezing rain
|
||||
case 6: // Freezing drizzle
|
||||
currentWeather.rain += precipitationValue;
|
||||
currentWeather.precipitation += precipitationValue;
|
||||
currentWeather.precipitationAmount += precipitationValue;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -178,7 +173,6 @@ WeatherProvider.register("smhi", {
|
||||
|
||||
/**
|
||||
* Takes all the data points and converts it to one WeatherObject per day.
|
||||
*
|
||||
* @param {object[]} allWeatherData Array of weatherdata
|
||||
* @param {object} coordinates Coordinates of the locations of the weather
|
||||
* @param {string} groupBy The interval to use for grouping the data (day, hour)
|
||||
@@ -202,7 +196,7 @@ WeatherProvider.register("smhi", {
|
||||
currentWeather.maxTemperature = -Infinity;
|
||||
currentWeather.snow = 0;
|
||||
currentWeather.rain = 0;
|
||||
currentWeather.precipitation = 0;
|
||||
currentWeather.precipitationAmount = 0;
|
||||
result.push(currentWeather);
|
||||
}
|
||||
|
||||
@@ -221,7 +215,7 @@ WeatherProvider.register("smhi", {
|
||||
currentWeather.maxTemperature = Math.max(currentWeather.maxTemperature, weatherObject.temperature);
|
||||
currentWeather.snow += weatherObject.snow;
|
||||
currentWeather.rain += weatherObject.rain;
|
||||
currentWeather.precipitation += weatherObject.precipitation;
|
||||
currentWeather.precipitationAmount += weatherObject.precipitationAmount;
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -230,7 +224,6 @@ WeatherProvider.register("smhi", {
|
||||
/**
|
||||
* Resolve coordinates from the response data (probably preferably to use
|
||||
* this if it's not matching the config values exactly)
|
||||
*
|
||||
* @param {object} data Response data from the weather service
|
||||
* @returns {{lon, lat}} the lat/long coordinates of the data
|
||||
*/
|
||||
@@ -241,7 +234,6 @@ WeatherProvider.register("smhi", {
|
||||
/**
|
||||
* The distance between the data points is increasing in the data the more distant the prediction is.
|
||||
* Find these gaps and fill them with the previous hours data to make the data returned a complete set.
|
||||
*
|
||||
* @param {object[]} data Response data from the weather service
|
||||
* @returns {object[]} Given data with filled gaps
|
||||
*/
|
||||
@@ -263,7 +255,6 @@ WeatherProvider.register("smhi", {
|
||||
|
||||
/**
|
||||
* Helper method to get a property from the returned data set.
|
||||
*
|
||||
* @param {object} currentWeatherData Weatherdata to get from
|
||||
* @param {string} name The name of the property
|
||||
* @returns {*} The value of the property in the weatherdata
|
||||
@@ -276,7 +267,6 @@ WeatherProvider.register("smhi", {
|
||||
* Map the icon value from SMHI to an icon that MagicMirror² understands.
|
||||
* Uses different icons depending on if its daytime or nighttime.
|
||||
* SMHI's description of what the numeric value means is the comment after the case.
|
||||
*
|
||||
* @param {number} input The SMHI icon value
|
||||
* @param {boolean} isDayTime True if the icon should be for daytime, false for nighttime
|
||||
* @returns {string} The icon name for the MagicMirror
|
||||
|
@@ -100,9 +100,9 @@ WeatherProvider.register("ukmetoffice", {
|
||||
currentWeather.humidity = rep.H;
|
||||
currentWeather.temperature = rep.T;
|
||||
currentWeather.feelsLikeTemp = rep.F;
|
||||
currentWeather.precipitation = parseInt(rep.Pp);
|
||||
currentWeather.precipitationProbability = parseInt(rep.Pp);
|
||||
currentWeather.windSpeed = WeatherUtils.convertWindToMetric(rep.S);
|
||||
currentWeather.windDirection = WeatherUtils.convertWindDirection(rep.D);
|
||||
currentWeather.windFromDirection = WeatherUtils.convertWindDirection(rep.D);
|
||||
currentWeather.weatherType = this.convertWeatherType(rep.W);
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
weather.minTemperature = period.Rep[1].Nm;
|
||||
weather.maxTemperature = period.Rep[0].Dm;
|
||||
weather.weatherType = this.convertWeatherType(period.Rep[0].W);
|
||||
weather.precipitation = parseInt(period.Rep[0].PPd);
|
||||
weather.precipitationProbability = parseInt(period.Rep[0].PPd);
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
@@ -189,14 +189,13 @@ WeatherProvider.register("ukmetoffice", {
|
||||
|
||||
/**
|
||||
* Generates an url with api parameters based on the config.
|
||||
*
|
||||
* @param {string} forecastType daily or 3hourly forecast
|
||||
* @returns {string} url
|
||||
*/
|
||||
getParams(forecastType) {
|
||||
let params = "?";
|
||||
params += "res=" + forecastType;
|
||||
params += "&key=" + this.config.apiKey;
|
||||
params += `res=${forecastType}`;
|
||||
params += `&key=${this.config.apiKey}`;
|
||||
return params;
|
||||
}
|
||||
});
|
||||
|
@@ -55,9 +55,9 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
// Build URL with query strings according to DataHub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
getUrl(forecastType) {
|
||||
let queryStrings = "?";
|
||||
queryStrings += "latitude=" + this.config.lat;
|
||||
queryStrings += "&longitude=" + this.config.lon;
|
||||
queryStrings += "&includeLocationName=" + true;
|
||||
queryStrings += `latitude=${this.config.lat}`;
|
||||
queryStrings += `&longitude=${this.config.lon}`;
|
||||
queryStrings += `&includeLocationName=${true}`;
|
||||
|
||||
// Return URL, making sure there is a trailing "/" in the base URL.
|
||||
return this.config.apiBase + (this.config.apiBase.endsWith("/") ? "" : "/") + forecastType + queryStrings;
|
||||
@@ -104,7 +104,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
})
|
||||
|
||||
// Catch any error(s)
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
||||
|
||||
// Let the module know there is data available
|
||||
.finally(() => this.updateAvailable());
|
||||
@@ -126,7 +126,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
if (nowUtc.isSameOrAfter(forecastTime) && nowUtc.isBefore(moment(forecastTime.add(1, "h")))) {
|
||||
currentWeather.date = forecastTime;
|
||||
currentWeather.windSpeed = forecastDataHours[hour].windSpeed10m;
|
||||
currentWeather.windDirection = forecastDataHours[hour].windDirectionFrom10m;
|
||||
currentWeather.windFromDirection = forecastDataHours[hour].windDirectionFrom10m;
|
||||
currentWeather.temperature = forecastDataHours[hour].screenTemperature;
|
||||
currentWeather.minTemperature = forecastDataHours[hour].minScreenAirTemp;
|
||||
currentWeather.maxTemperature = forecastDataHours[hour].maxScreenAirTemp;
|
||||
@@ -134,7 +134,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
currentWeather.humidity = forecastDataHours[hour].screenRelativeHumidity;
|
||||
currentWeather.rain = forecastDataHours[hour].totalPrecipAmount;
|
||||
currentWeather.snow = forecastDataHours[hour].totalSnowAmount;
|
||||
currentWeather.precipitation = forecastDataHours[hour].probOfPrecipitation;
|
||||
currentWeather.precipitationProbability = forecastDataHours[hour].probOfPrecipitation;
|
||||
currentWeather.feelsLikeTemp = forecastDataHours[hour].feelsLikeTemperature;
|
||||
|
||||
// Pass on full details, so they can be used in custom templates
|
||||
@@ -173,7 +173,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
})
|
||||
|
||||
// Catch any error(s)
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
||||
|
||||
// Let the module know there is new data available
|
||||
.finally(() => this.updateAvailable());
|
||||
@@ -204,9 +204,9 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
|
||||
// Using daytime forecast values
|
||||
forecastWeather.windSpeed = forecastDataDays[day].midday10MWindSpeed;
|
||||
forecastWeather.windDirection = forecastDataDays[day].midday10MWindDirection;
|
||||
forecastWeather.windFromDirection = forecastDataDays[day].midday10MWindDirection;
|
||||
forecastWeather.weatherType = this.convertWeatherType(forecastDataDays[day].daySignificantWeatherCode);
|
||||
forecastWeather.precipitation = forecastDataDays[day].dayProbabilityOfPrecipitation;
|
||||
forecastWeather.precipitationProbability = forecastDataDays[day].dayProbabilityOfPrecipitation;
|
||||
forecastWeather.temperature = forecastDataDays[day].dayMaxScreenTemperature;
|
||||
forecastWeather.humidity = forecastDataDays[day].middayRelativeHumidity;
|
||||
forecastWeather.rain = forecastDataDays[day].dayProbabilityOfRain;
|
||||
|
@@ -55,7 +55,7 @@ WeatherProvider.register("weatherbit", {
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.data);
|
||||
this.setWeatherForecast(forecast);
|
||||
|
||||
this.fetchedLocationName = data.city_name + ", " + data.state_code;
|
||||
this.fetchedLocationName = `${data.city_name}, ${data.state_code}`;
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
@@ -65,7 +65,6 @@ WeatherProvider.register("weatherbit", {
|
||||
|
||||
/**
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
*
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
@@ -106,12 +105,12 @@ WeatherProvider.register("weatherbit", {
|
||||
currentWeather.humidity = parseFloat(currentWeatherData.data[0].rh);
|
||||
currentWeather.temperature = parseFloat(currentWeatherData.data[0].temp);
|
||||
currentWeather.windSpeed = parseFloat(currentWeatherData.data[0].wind_spd);
|
||||
currentWeather.windDirection = currentWeatherData.data[0].wind_dir;
|
||||
currentWeather.windFromDirection = currentWeatherData.data[0].wind_dir;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.data[0].weather.icon);
|
||||
currentWeather.sunrise = moment(currentWeatherData.data[0].sunrise, "HH:mm").add(tzOffset, "m");
|
||||
currentWeather.sunset = moment(currentWeatherData.data[0].sunset, "HH:mm").add(tzOffset, "m");
|
||||
|
||||
this.fetchedLocationName = currentWeatherData.data[0].city_name + ", " + currentWeatherData.data[0].state_code;
|
||||
this.fetchedLocationName = `${currentWeatherData.data[0].city_name}, ${currentWeatherData.data[0].state_code}`;
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
@@ -125,7 +124,8 @@ WeatherProvider.register("weatherbit", {
|
||||
weather.date = moment(forecast.datetime, "YYYY-MM-DD");
|
||||
weather.minTemperature = forecast.min_temp;
|
||||
weather.maxTemperature = forecast.max_temp;
|
||||
weather.precipitation = forecast.precip;
|
||||
weather.precipitationAmount = forecast.precip;
|
||||
weather.precipitationProbability = forecast.pop;
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather.icon);
|
||||
|
||||
days.push(weather);
|
||||
|
@@ -32,7 +32,7 @@ WeatherProvider.register("weatherflow", {
|
||||
currentWeather.humidity = data.current_conditions.relative_humidity;
|
||||
currentWeather.temperature = data.current_conditions.air_temperature;
|
||||
currentWeather.windSpeed = WeatherUtils.convertWindToMs(data.current_conditions.wind_avg);
|
||||
currentWeather.windDirection = data.current_conditions.wind_direction;
|
||||
currentWeather.windFromDirection = data.current_conditions.wind_direction;
|
||||
currentWeather.weatherType = data.forecast.daily[0].icon;
|
||||
currentWeather.sunrise = moment.unix(data.forecast.daily[0].sunrise);
|
||||
currentWeather.sunset = moment.unix(data.forecast.daily[0].sunset);
|
||||
@@ -55,6 +55,7 @@ WeatherProvider.register("weatherflow", {
|
||||
weather.date = moment.unix(forecast.day_start_local);
|
||||
weather.minTemperature = forecast.air_temp_low;
|
||||
weather.maxTemperature = forecast.air_temp_high;
|
||||
weather.precipitationProbability = forecast.precip_probability;
|
||||
weather.weatherType = forecast.icon;
|
||||
weather.snow = 0;
|
||||
|
||||
|
@@ -129,10 +129,10 @@ WeatherProvider.register("weathergov", {
|
||||
// points URL did not respond with usable data.
|
||||
return;
|
||||
}
|
||||
this.fetchedLocationName = data.properties.relativeLocation.properties.city + ", " + data.properties.relativeLocation.properties.state;
|
||||
Log.log("Forecast location is " + this.fetchedLocationName);
|
||||
this.forecastURL = data.properties.forecast + "?units=si";
|
||||
this.forecastHourlyURL = data.properties.forecastHourly + "?units=si";
|
||||
this.fetchedLocationName = `${data.properties.relativeLocation.properties.city}, ${data.properties.relativeLocation.properties.state}`;
|
||||
Log.log(`Forecast location is ${this.fetchedLocationName}`);
|
||||
this.forecastURL = `${data.properties.forecast}?units=si`;
|
||||
this.forecastHourlyURL = `${data.properties.forecastHourly}?units=si`;
|
||||
this.forecastGridDataURL = data.properties.forecastGridData;
|
||||
this.observationStationsURL = data.properties.observationStations;
|
||||
// with this URL, we chain another promise for the station obs URL
|
||||
@@ -143,7 +143,7 @@ WeatherProvider.register("weathergov", {
|
||||
// obs station URL did not respond with usable data.
|
||||
return;
|
||||
}
|
||||
this.stationObsURL = obsData.features[0].id + "/observations/latest";
|
||||
this.stationObsURL = `${obsData.features[0].id}/observations/latest`;
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error(err);
|
||||
@@ -179,9 +179,15 @@ WeatherProvider.register("weathergov", {
|
||||
} else {
|
||||
weather.windSpeed = forecast.windSpeed.slice(0, forecast.windSpeed.search(" "));
|
||||
}
|
||||
weather.windDirection = this.convertWindDirection(forecast.windDirection);
|
||||
weather.windSpeed = WeatherUtils.convertWindToMs(weather.windSpeed);
|
||||
weather.windFromDirection = forecast.windDirection;
|
||||
weather.temperature = forecast.temperature;
|
||||
weather.tempUnits = forecast.temperatureUnit;
|
||||
//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);
|
||||
|
||||
@@ -206,13 +212,11 @@ WeatherProvider.register("weathergov", {
|
||||
currentWeather.date = moment(currentWeatherData.timestamp);
|
||||
currentWeather.temperature = currentWeatherData.temperature.value;
|
||||
currentWeather.windSpeed = WeatherUtils.convertWindToMs(currentWeatherData.windSpeed.value);
|
||||
currentWeather.windDirection = currentWeatherData.windDirection.value;
|
||||
currentWeather.windFromDirection = currentWeatherData.windDirection.value;
|
||||
currentWeather.minTemperature = currentWeatherData.minTemperatureLast24Hours.value;
|
||||
currentWeather.maxTemperature = currentWeatherData.maxTemperatureLast24Hours.value;
|
||||
currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value);
|
||||
currentWeather.rain = null;
|
||||
currentWeather.snow = null;
|
||||
currentWeather.precipitation = this.convertLength(currentWeatherData.precipitationLastHour.value);
|
||||
currentWeather.precipitationAmount = currentWeatherData.precipitationLastHour.value;
|
||||
if (currentWeatherData.heatIndex.value !== null) {
|
||||
currentWeather.feelsLikeTemp = currentWeatherData.heatIndex.value;
|
||||
} else if (currentWeatherData.windChill.value !== null) {
|
||||
@@ -248,7 +252,6 @@ WeatherProvider.register("weathergov", {
|
||||
// variable for date
|
||||
let date = "";
|
||||
let weather = new WeatherObject();
|
||||
weather.precipitation = 0;
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
if (date !== moment(forecast.startTime).format("YYYY-MM-DD")) {
|
||||
@@ -263,7 +266,12 @@ WeatherProvider.register("weathergov", {
|
||||
|
||||
minTemp = [];
|
||||
maxTemp = [];
|
||||
weather.precipitation = 0;
|
||||
//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");
|
||||
@@ -295,18 +303,6 @@ WeatherProvider.register("weathergov", {
|
||||
return days.slice(1);
|
||||
},
|
||||
|
||||
/*
|
||||
* Unit conversions
|
||||
*/
|
||||
// conversion to inches
|
||||
convertLength(meters) {
|
||||
if (this.config.units === "imperial") {
|
||||
return meters * 39.3701;
|
||||
} else {
|
||||
return meters;
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the icons to a more usable name.
|
||||
*/
|
||||
|
@@ -7,7 +7,7 @@
|
||||
* By Magnus Marthinsen
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Yr.no, a norwegian sweather service.
|
||||
* This class is a provider for Yr.no, a norwegian weather service.
|
||||
*
|
||||
* Terms of service: https://developer.yr.no/doc/TermsOfService/
|
||||
*/
|
||||
@@ -47,7 +47,7 @@ WeatherProvider.register("yr", {
|
||||
const getRequests = [this.getWeatherData(), this.getStellarData()];
|
||||
const [weatherData, stellarData] = await Promise.all(getRequests);
|
||||
if (!stellarData) {
|
||||
Log.warn("No stelar data available.");
|
||||
Log.warn("No stellar data available.");
|
||||
}
|
||||
if (!weatherData.properties.timeseries || !weatherData.properties.timeseries[0]) {
|
||||
Log.error("No weather data available.");
|
||||
@@ -65,7 +65,8 @@ WeatherProvider.register("yr", {
|
||||
}
|
||||
const forecastXHours = this.getForecastForXHoursFrom(forecast.data);
|
||||
forecast.weatherType = this.convertWeatherType(forecastXHours.summary.symbol_code, forecast.time);
|
||||
forecast.precipitation = forecastXHours.details?.precipitation_amount;
|
||||
forecast.precipitationAmount = forecastXHours.details?.precipitation_amount;
|
||||
forecast.precipitationProbability = forecastXHours.details?.probability_of_precipitation;
|
||||
forecast.minTemperature = forecastXHours.details?.air_temperature_min;
|
||||
forecast.maxTemperature = forecastXHours.details?.air_temperature_max;
|
||||
return this.getWeatherDataFrom(forecast, stellarData, weatherData.properties.meta.units);
|
||||
@@ -109,13 +110,15 @@ WeatherProvider.register("yr", {
|
||||
this.getWeatherDataFromYr(weatherData?.downloadedAt)
|
||||
.then((weatherData) => {
|
||||
Log.debug("Got weather data from yr.");
|
||||
let data;
|
||||
if (weatherData) {
|
||||
this.cacheWeatherData(weatherData);
|
||||
data = weatherData;
|
||||
} else {
|
||||
//Undefined if unchanged
|
||||
weatherData = this.getWeatherDataFromCache();
|
||||
data = this.getWeatherDataFromCache();
|
||||
}
|
||||
resolve(weatherData);
|
||||
resolve(data);
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error(err);
|
||||
@@ -251,12 +254,12 @@ WeatherProvider.register("yr", {
|
||||
this.cacheStellarData(stellarData);
|
||||
resolve(stellarData);
|
||||
} else {
|
||||
reject("No stellar data returned from Yr for " + tomorrow);
|
||||
reject(`No stellar data returned from Yr for ${tomorrow}`);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error(err);
|
||||
reject("Unable to get stellar data from Yr for " + tomorrow);
|
||||
reject(`Unable to get stellar data from Yr for ${tomorrow}`);
|
||||
})
|
||||
.finally(() => {
|
||||
localStorage.removeItem("yrIsFetchingStellarData");
|
||||
@@ -265,16 +268,16 @@ WeatherProvider.register("yr", {
|
||||
this.getStellarDataFromYr(today, 2)
|
||||
.then((stellarData) => {
|
||||
if (stellarData) {
|
||||
stellarData = {
|
||||
const data = {
|
||||
today: stellarData
|
||||
};
|
||||
stellarData.tomorrow = Object.assign({}, stellarData.today);
|
||||
stellarData.today.date = today;
|
||||
stellarData.tomorrow.date = tomorrow;
|
||||
this.cacheStellarData(stellarData);
|
||||
resolve(stellarData);
|
||||
data.tomorrow = Object.assign({}, data.today);
|
||||
data.today.date = today;
|
||||
data.tomorrow.date = tomorrow;
|
||||
this.cacheStellarData(data);
|
||||
resolve(data);
|
||||
} else {
|
||||
Log.error("Something went wrong when fetching stellar data. Responses: " + stellarData);
|
||||
Log.error(`Something went wrong when fetching stellar data. Responses: ${stellarData}`);
|
||||
reject(stellarData);
|
||||
}
|
||||
})
|
||||
@@ -349,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) {
|
||||
@@ -358,25 +360,22 @@ WeatherProvider.register("yr", {
|
||||
},
|
||||
|
||||
getWeatherDataFrom(forecast, stellarData, units) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
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;
|
||||
const weather = new WeatherObject();
|
||||
|
||||
weather.date = moment(forecast.time);
|
||||
weather.windSpeed = forecast.data.instant.details.wind_speed;
|
||||
weather.windDirection = (forecast.data.instant.details.wind_from_direction + 180) % 360;
|
||||
weather.windFromDirection = forecast.data.instant.details.wind_from_direction;
|
||||
weather.temperature = forecast.data.instant.details.air_temperature;
|
||||
weather.minTemperature = forecast.minTemperature;
|
||||
weather.maxTemperature = forecast.maxTemperature;
|
||||
weather.weatherType = forecast.weatherType;
|
||||
weather.humidity = forecast.data.instant.details.relative_humidity;
|
||||
weather.precipitation = forecast.precipitation;
|
||||
weather.precipitationAmount = forecast.precipitationAmount;
|
||||
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;
|
||||
},
|
||||
@@ -530,7 +529,7 @@ WeatherProvider.register("yr", {
|
||||
return;
|
||||
}
|
||||
if (!stellarData) {
|
||||
Log.warn("No stelar data available.");
|
||||
Log.warn("No stellar data available.");
|
||||
}
|
||||
let forecasts;
|
||||
switch (type) {
|
||||
@@ -554,7 +553,8 @@ WeatherProvider.register("yr", {
|
||||
|
||||
for (const forecast of weatherData.properties.timeseries) {
|
||||
forecast.symbol = forecast.data.next_1_hours?.summary?.symbol_code;
|
||||
forecast.precipitation = forecast.data.next_1_hours?.details?.precipitation_amount;
|
||||
forecast.precipitationAmount = forecast.data.next_1_hours?.details?.precipitation_amount;
|
||||
forecast.precipitationProbability = forecast.data.next_1_hours?.details?.probability_of_precipitation;
|
||||
forecast.minTemperature = forecast.data.next_1_hours?.details?.air_temperature_min;
|
||||
forecast.maxTemperature = forecast.data.next_1_hours?.details?.air_temperature_max;
|
||||
forecast.weatherType = this.convertWeatherType(forecast.symbol, forecast.time);
|
||||
@@ -599,7 +599,8 @@ WeatherProvider.register("yr", {
|
||||
const forecastXHours = forecast.data.next_12_hours ?? forecast.data.next_6_hours ?? forecast.data.next_1_hours;
|
||||
if (forecastXHours) {
|
||||
forecast.symbol = forecastXHours.summary?.symbol_code;
|
||||
forecast.precipitation = forecastXHours.details?.precipitation_amount;
|
||||
forecast.precipitationAmount = forecastXHours.details?.precipitation_amount ?? forecast.data.next_6_hours?.details?.precipitation_amount; // 6 hours is likely to have precipitation amount even if 12 hours does not
|
||||
forecast.precipitationProbability = forecastXHours.details?.probability_of_precipitation;
|
||||
forecast.minTemperature = minTemperature;
|
||||
forecast.maxTemperature = maxTemperature;
|
||||
|
||||
|
@@ -6,7 +6,7 @@
|
||||
transform: translate(0, -3px);
|
||||
}
|
||||
|
||||
.weather .humidityIcon {
|
||||
.weather .humidity-icon {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.weather .precipitation {
|
||||
.weather .precipitation-amount,
|
||||
.weather .precipitation-prob,
|
||||
.weather .uv-index {
|
||||
padding-left: 20px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* global WeatherProvider, WeatherUtils */
|
||||
/* global WeatherProvider, WeatherUtils, formatTime */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
@@ -12,23 +12,28 @@ Module.register("weather", {
|
||||
weatherProvider: "openweathermap",
|
||||
roundTemp: false,
|
||||
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
|
||||
lang: config.language,
|
||||
units: config.units,
|
||||
tempUnits: config.units,
|
||||
windUnits: config.units,
|
||||
timeFormat: config.timeFormat,
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
animationSpeed: 1000,
|
||||
timeFormat: config.timeFormat,
|
||||
showFeelsLike: true,
|
||||
showHumidity: false,
|
||||
showIndoorHumidity: false,
|
||||
showIndoorTemperature: false,
|
||||
allowOverrideNotification: false,
|
||||
showPeriod: true,
|
||||
showPeriodUpper: false,
|
||||
showPrecipitationAmount: false,
|
||||
showPrecipitationProbability: false,
|
||||
showUVIndex: false,
|
||||
showSun: true,
|
||||
showWindDirection: true,
|
||||
showWindDirectionAsArrow: false,
|
||||
lang: config.language,
|
||||
showHumidity: false,
|
||||
showSun: true,
|
||||
degreeLabel: false,
|
||||
decimalSymbol: ".",
|
||||
showIndoorTemperature: false,
|
||||
showIndoorHumidity: false,
|
||||
maxNumberOfDays: 5,
|
||||
maxEntries: 5,
|
||||
ignoreToday: false,
|
||||
@@ -39,10 +44,9 @@ Module.register("weather", {
|
||||
calendarClass: "calendar",
|
||||
tableClass: "small",
|
||||
onlyTemp: false,
|
||||
showPrecipitationAmount: false,
|
||||
colored: false,
|
||||
showFeelsLike: true,
|
||||
absoluteDates: false
|
||||
absoluteDates: false,
|
||||
hourlyForecastIncrements: 1
|
||||
},
|
||||
|
||||
// Module properties.
|
||||
@@ -58,13 +62,13 @@ 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.
|
||||
getHeader: function () {
|
||||
if (this.config.appendLocationNameToHeader && this.weatherProvider) {
|
||||
if (this.data.header) return this.data.header + " " + this.weatherProvider.fetchedLocation();
|
||||
if (this.data.header) return `${this.data.header} ${this.weatherProvider.fetchedLocation()}`;
|
||||
else return this.weatherProvider.fetchedLocation();
|
||||
}
|
||||
|
||||
@@ -116,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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -137,13 +143,17 @@ Module.register("weather", {
|
||||
|
||||
// Add all the data to the template.
|
||||
getTemplateData: function () {
|
||||
const forecast = this.weatherProvider.weatherForecast();
|
||||
const currentData = this.weatherProvider.currentWeather();
|
||||
const forecastData = this.weatherProvider.weatherForecast();
|
||||
|
||||
// Skip some hourly forecast entries if configured
|
||||
const hourlyData = this.weatherProvider.weatherHourly()?.filter((e, i) => (i + 1) % this.config.hourlyForecastIncrements === this.config.hourlyForecastIncrements - 1);
|
||||
|
||||
return {
|
||||
config: this.config,
|
||||
current: this.weatherProvider.currentWeather(),
|
||||
forecast: forecast,
|
||||
hourly: this.weatherProvider.weatherHourly(),
|
||||
current: currentData,
|
||||
forecast: forecastData,
|
||||
hourly: hourlyData,
|
||||
indoor: {
|
||||
humidity: this.indoorHumidity,
|
||||
temperature: this.indoorTemperature
|
||||
@@ -205,54 +215,37 @@ Module.register("weather", {
|
||||
this.nunjucksEnvironment().addFilter(
|
||||
"formatTime",
|
||||
function (date) {
|
||||
date = moment(date);
|
||||
|
||||
if (this.config.timeFormat !== 24) {
|
||||
if (this.config.showPeriod) {
|
||||
if (this.config.showPeriodUpper) {
|
||||
return date.format("h:mm A");
|
||||
} else {
|
||||
return date.format("h:mm a");
|
||||
}
|
||||
} else {
|
||||
return date.format("h:mm");
|
||||
}
|
||||
}
|
||||
|
||||
return date.format("HH:mm");
|
||||
return formatTime(this.config, date);
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
this.nunjucksEnvironment().addFilter(
|
||||
"unit",
|
||||
function (value, type) {
|
||||
function (value, type, valueUnit) {
|
||||
let formattedValue;
|
||||
if (type === "temperature") {
|
||||
value = this.roundValue(WeatherUtils.convertTemp(value, this.config.tempUnits)) + "°";
|
||||
formattedValue = `${this.roundValue(WeatherUtils.convertTemp(value, this.config.tempUnits))}°`;
|
||||
if (this.config.degreeLabel) {
|
||||
if (this.config.tempUnits === "metric") {
|
||||
value += "C";
|
||||
formattedValue += "C";
|
||||
} else if (this.config.tempUnits === "imperial") {
|
||||
value += "F";
|
||||
formattedValue += "F";
|
||||
} else {
|
||||
value += "K";
|
||||
formattedValue += "K";
|
||||
}
|
||||
}
|
||||
} else if (type === "precip") {
|
||||
if (value === null || isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
|
||||
value = "";
|
||||
formattedValue = "";
|
||||
} else {
|
||||
if (this.config.weatherProvider === "ukmetoffice" || this.config.weatherProvider === "ukmetofficedatahub") {
|
||||
value += "%";
|
||||
} else {
|
||||
value = `${value.toFixed(2)} ${this.config.units === "imperial" ? "in" : "mm"}`;
|
||||
}
|
||||
formattedValue = WeatherUtils.convertPrecipitationUnit(value, valueUnit, this.config.units);
|
||||
}
|
||||
} else if (type === "humidity") {
|
||||
value += "%";
|
||||
formattedValue = `${value}%`;
|
||||
} else if (type === "wind") {
|
||||
value = WeatherUtils.convertWind(value, this.config.windUnits);
|
||||
formattedValue = WeatherUtils.convertWind(value, this.config.windUnits);
|
||||
}
|
||||
return value;
|
||||
return formattedValue;
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
|
@@ -11,6 +11,10 @@
|
||||
* Currently this is focused on the information which is necessary for the current weather.
|
||||
* As soon as we start implementing the forecast, mode properties will be added.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external Moment
|
||||
*/
|
||||
class WeatherObject {
|
||||
/**
|
||||
* Constructor for a WeatherObject
|
||||
@@ -18,7 +22,7 @@ class WeatherObject {
|
||||
constructor() {
|
||||
this.date = null;
|
||||
this.windSpeed = null;
|
||||
this.windDirection = null;
|
||||
this.windFromDirection = null;
|
||||
this.sunrise = null;
|
||||
this.sunset = null;
|
||||
this.temperature = null;
|
||||
@@ -26,93 +30,79 @@ class WeatherObject {
|
||||
this.maxTemperature = null;
|
||||
this.weatherType = null;
|
||||
this.humidity = null;
|
||||
this.rain = null;
|
||||
this.snow = null;
|
||||
this.precipitation = null;
|
||||
this.precipitationAmount = null;
|
||||
this.precipitationUnits = null;
|
||||
this.precipitationProbability = null;
|
||||
this.feelsLikeTemp = null;
|
||||
}
|
||||
|
||||
cardinalWindDirection() {
|
||||
if (this.windDirection > 11.25 && this.windDirection <= 33.75) {
|
||||
if (this.windFromDirection > 11.25 && this.windFromDirection <= 33.75) {
|
||||
return "NNE";
|
||||
} else if (this.windDirection > 33.75 && this.windDirection <= 56.25) {
|
||||
} else if (this.windFromDirection > 33.75 && this.windFromDirection <= 56.25) {
|
||||
return "NE";
|
||||
} else if (this.windDirection > 56.25 && this.windDirection <= 78.75) {
|
||||
} else if (this.windFromDirection > 56.25 && this.windFromDirection <= 78.75) {
|
||||
return "ENE";
|
||||
} else if (this.windDirection > 78.75 && this.windDirection <= 101.25) {
|
||||
} else if (this.windFromDirection > 78.75 && this.windFromDirection <= 101.25) {
|
||||
return "E";
|
||||
} else if (this.windDirection > 101.25 && this.windDirection <= 123.75) {
|
||||
} else if (this.windFromDirection > 101.25 && this.windFromDirection <= 123.75) {
|
||||
return "ESE";
|
||||
} else if (this.windDirection > 123.75 && this.windDirection <= 146.25) {
|
||||
} else if (this.windFromDirection > 123.75 && this.windFromDirection <= 146.25) {
|
||||
return "SE";
|
||||
} else if (this.windDirection > 146.25 && this.windDirection <= 168.75) {
|
||||
} else if (this.windFromDirection > 146.25 && this.windFromDirection <= 168.75) {
|
||||
return "SSE";
|
||||
} else if (this.windDirection > 168.75 && this.windDirection <= 191.25) {
|
||||
} else if (this.windFromDirection > 168.75 && this.windFromDirection <= 191.25) {
|
||||
return "S";
|
||||
} else if (this.windDirection > 191.25 && this.windDirection <= 213.75) {
|
||||
} else if (this.windFromDirection > 191.25 && this.windFromDirection <= 213.75) {
|
||||
return "SSW";
|
||||
} else if (this.windDirection > 213.75 && this.windDirection <= 236.25) {
|
||||
} else if (this.windFromDirection > 213.75 && this.windFromDirection <= 236.25) {
|
||||
return "SW";
|
||||
} else if (this.windDirection > 236.25 && this.windDirection <= 258.75) {
|
||||
} else if (this.windFromDirection > 236.25 && this.windFromDirection <= 258.75) {
|
||||
return "WSW";
|
||||
} else if (this.windDirection > 258.75 && this.windDirection <= 281.25) {
|
||||
} else if (this.windFromDirection > 258.75 && this.windFromDirection <= 281.25) {
|
||||
return "W";
|
||||
} else if (this.windDirection > 281.25 && this.windDirection <= 303.75) {
|
||||
} else if (this.windFromDirection > 281.25 && this.windFromDirection <= 303.75) {
|
||||
return "WNW";
|
||||
} else if (this.windDirection > 303.75 && this.windDirection <= 326.25) {
|
||||
} else if (this.windFromDirection > 303.75 && this.windFromDirection <= 326.25) {
|
||||
return "NW";
|
||||
} else if (this.windDirection > 326.25 && this.windDirection <= 348.75) {
|
||||
} else if (this.windFromDirection > 326.25 && this.windFromDirection <= 348.75) {
|
||||
return "NNW";
|
||||
} else {
|
||||
return "N";
|
||||
}
|
||||
}
|
||||
|
||||
nextSunAction() {
|
||||
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
||||
/**
|
||||
* Determines if the sun sets or rises next. Uses the current time and not
|
||||
* the date from the weather-forecast.
|
||||
* @param {Moment} date an optional date where you want to get the next
|
||||
* action for. Useful only in tests, defaults to the current time.
|
||||
* @returns {string} "sunset" or "sunrise"
|
||||
*/
|
||||
nextSunAction(date = moment()) {
|
||||
return date.isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
||||
}
|
||||
|
||||
feelsLike() {
|
||||
if (this.feelsLikeTemp) {
|
||||
return this.feelsLikeTemp;
|
||||
}
|
||||
const windInMph = WeatherUtils.convertWind(this.windSpeed, "imperial");
|
||||
const tempInF = WeatherUtils.convertTemp(this.temperature, "imperial");
|
||||
let feelsLike = tempInF;
|
||||
|
||||
if (windInMph > 3 && tempInF < 50) {
|
||||
feelsLike = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16));
|
||||
} else if (tempInF > 80 && this.humidity > 40) {
|
||||
feelsLike =
|
||||
-42.379 +
|
||||
2.04901523 * tempInF +
|
||||
10.14333127 * this.humidity -
|
||||
0.22475541 * tempInF * this.humidity -
|
||||
6.83783 * Math.pow(10, -3) * tempInF * tempInF -
|
||||
5.481717 * Math.pow(10, -2) * this.humidity * this.humidity +
|
||||
1.22874 * Math.pow(10, -3) * tempInF * tempInF * this.humidity +
|
||||
8.5282 * Math.pow(10, -4) * tempInF * this.humidity * this.humidity -
|
||||
1.99 * Math.pow(10, -6) * tempInF * tempInF * this.humidity * this.humidity;
|
||||
}
|
||||
|
||||
return ((feelsLike - 32) * 5) / 9;
|
||||
return WeatherUtils.calculateFeelsLike(this.temperature, this.windSpeed, this.humidity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the weatherObject is at dayTime.
|
||||
*
|
||||
* @returns {boolean} true if it is at dayTime
|
||||
*/
|
||||
isDayTime() {
|
||||
return this.date.isBetween(this.sunrise, this.sunset, undefined, "[]");
|
||||
const now = !this.date ? moment() : this.date;
|
||||
return now.isBetween(this.sunrise, this.sunset, undefined, "[]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sunrise / sunset time depending on the location. This can be
|
||||
* used if your provider doesn't provide that data by itself. Then SunCalc
|
||||
* is used here to calculate them according to the location.
|
||||
*
|
||||
* @param {number} lat latitude
|
||||
* @param {number} lon longitude
|
||||
*/
|
||||
@@ -128,7 +118,6 @@ class WeatherObject {
|
||||
*
|
||||
* Before being handed to other modules, mutable values must be cloned safely.
|
||||
* Especially 'moment' object is not immutable, so original 'date', 'sunrise', 'sunset' could be corrupted or changed by other modules.
|
||||
*
|
||||
* @returns {object} plained object clone of original weatherObject
|
||||
*/
|
||||
simpleClone() {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* global Class, performWebRequest */
|
||||
/* global Class, performWebRequest, OverrideWrapper */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
@@ -113,7 +113,6 @@ const WeatherProvider = Class.extend({
|
||||
|
||||
/**
|
||||
* A convenience function to make requests.
|
||||
*
|
||||
* @param {string} url the url to fetch from
|
||||
* @param {string} type what contenttype to expect in the response, can be "json" or "xml"
|
||||
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||
@@ -138,7 +137,6 @@ WeatherProvider.providers = [];
|
||||
|
||||
/**
|
||||
* Static method to register a new weather provider.
|
||||
*
|
||||
* @param {string} providerIdentifier The name of the weather provider
|
||||
* @param {object} providerDetails The details of the weather provider
|
||||
*/
|
||||
@@ -148,23 +146,26 @@ WeatherProvider.register = function (providerIdentifier, providerDetails) {
|
||||
|
||||
/**
|
||||
* Static method to initialize a new weather provider.
|
||||
*
|
||||
* @param {string} providerIdentifier The name of the weather provider
|
||||
* @param {object} delegate The weather module
|
||||
* @returns {object} The new weather provider
|
||||
*/
|
||||
WeatherProvider.initialize = function (providerIdentifier, delegate) {
|
||||
providerIdentifier = providerIdentifier.toLowerCase();
|
||||
const pi = providerIdentifier.toLowerCase();
|
||||
|
||||
const provider = new WeatherProvider.providers[providerIdentifier]();
|
||||
const provider = new WeatherProvider.providers[pi]();
|
||||
const config = Object.assign({}, provider.defaults, delegate.config);
|
||||
|
||||
provider.delegate = delegate;
|
||||
provider.setConfig(config);
|
||||
|
||||
provider.providerIdentifier = providerIdentifier;
|
||||
provider.providerIdentifier = pi;
|
||||
if (!provider.providerName) {
|
||||
provider.providerName = providerIdentifier;
|
||||
provider.providerName = pi;
|
||||
}
|
||||
|
||||
if (config.allowOverrideNotification) {
|
||||
return new OverrideWrapper(provider);
|
||||
}
|
||||
|
||||
return provider;
|
||||
|
@@ -7,12 +7,11 @@
|
||||
const WeatherUtils = {
|
||||
/**
|
||||
* Convert wind (from m/s) to beaufort scale
|
||||
*
|
||||
* @param {number} speedInMS the windspeed you want to convert
|
||||
* @returns {number} the speed in beaufort
|
||||
*/
|
||||
beaufortWindSpeed(speedInMS) {
|
||||
const windInKmh = (speedInMS * 3600) / 1000;
|
||||
const windInKmh = this.convertWind(speedInMS, "kmh");
|
||||
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
||||
for (const [index, speed] of speeds.entries()) {
|
||||
if (speed > windInKmh) {
|
||||
@@ -22,10 +21,33 @@ const WeatherUtils = {
|
||||
return 12;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a value in a given unit to a string with a converted
|
||||
* value and a postfix matching the output unit system.
|
||||
* @param {number} value - The value to convert.
|
||||
* @param {string} valueUnit - The unit the values has. Default is mm.
|
||||
* @param {string} outputUnit - The unit system (imperial/metric) the return value should have.
|
||||
* @returns {string} - A string with tha value and a unit postfix.
|
||||
*/
|
||||
convertPrecipitationUnit(value, valueUnit, outputUnit) {
|
||||
if (valueUnit === "%") return `${value.toFixed(0)} ${valueUnit}`;
|
||||
|
||||
let convertedValue = value;
|
||||
let conversionUnit = valueUnit;
|
||||
if (outputUnit === "imperial") {
|
||||
if (valueUnit && valueUnit.toLowerCase() === "cm") convertedValue = convertedValue * 0.3937007874;
|
||||
else convertedValue = convertedValue * 0.03937007874;
|
||||
conversionUnit = "in";
|
||||
} else {
|
||||
conversionUnit = valueUnit ? valueUnit : "mm";
|
||||
}
|
||||
|
||||
return `${convertedValue.toFixed(2)} ${conversionUnit}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert temp (from degrees C) into imperial or metric unit depending on
|
||||
* your config
|
||||
*
|
||||
* @param {number} tempInC the temperature in celsius you want to convert
|
||||
* @param {string} unit can be 'imperial' or 'metric'
|
||||
* @returns {number} the converted temperature
|
||||
@@ -36,7 +58,6 @@ const WeatherUtils = {
|
||||
|
||||
/**
|
||||
* Convert wind speed into another unit.
|
||||
*
|
||||
* @param {number} windInMS the windspeed in meter/sec you want to convert
|
||||
* @param {string} unit can be 'beaufort', 'kmh', 'knots, 'imperial' (mph)
|
||||
* or 'metric' (mps)
|
||||
@@ -90,6 +111,29 @@ const WeatherUtils = {
|
||||
|
||||
convertWindToMs(kmh) {
|
||||
return kmh * 0.27777777777778;
|
||||
},
|
||||
|
||||
calculateFeelsLike(temperature, windSpeed, humidity) {
|
||||
const windInMph = this.convertWind(windSpeed, "imperial");
|
||||
const tempInF = this.convertTemp(temperature, "imperial");
|
||||
let feelsLike = tempInF;
|
||||
|
||||
if (windInMph > 3 && tempInF < 50) {
|
||||
feelsLike = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16));
|
||||
} else if (tempInF > 80 && humidity > 40) {
|
||||
feelsLike =
|
||||
-42.379 +
|
||||
2.04901523 * tempInF +
|
||||
10.14333127 * humidity -
|
||||
0.22475541 * tempInF * humidity -
|
||||
6.83783 * Math.pow(10, -3) * tempInF * tempInF -
|
||||
5.481717 * Math.pow(10, -2) * humidity * humidity +
|
||||
1.22874 * Math.pow(10, -3) * tempInF * tempInF * humidity +
|
||||
8.5282 * Math.pow(10, -4) * tempInF * humidity * humidity -
|
||||
1.99 * Math.pow(10, -6) * tempInF * tempInF * humidity * humidity;
|
||||
}
|
||||
|
||||
return ((feelsLike - 32) * 5) / 9;
|
||||
}
|
||||
};
|
||||
|
||||
|
11839
package-lock.json
generated
11839
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
68
package.json
68
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "magicmirror",
|
||||
"version": "2.22.0",
|
||||
"version": "2.25.0",
|
||||
"description": "The open source modular smart mirror platform.",
|
||||
"main": "js/electron.js",
|
||||
"scripts": {
|
||||
@@ -13,10 +13,10 @@
|
||||
"install-fonts": "echo \"Installing fonts ...\n\" && cd fonts && npm install --loglevel=error --no-audit --no-fund --no-update-notifier",
|
||||
"postinstall": "npm run install-vendor && npm run install-fonts && echo \"MagicMirror² installation finished successfully! \n\"",
|
||||
"test": "NODE_ENV=test jest -i --forceExit",
|
||||
"test:coverage": "NODE_ENV=test jest --coverage -i --forceExit",
|
||||
"test:coverage": "NODE_ENV=test jest --coverage -i --verbose false --forceExit",
|
||||
"test:electron": "NODE_ENV=test jest --selectProjects electron -i --forceExit",
|
||||
"test:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit",
|
||||
"test:unit": "NODE_ENV=test jest --selectProjects unit -i --forceExit",
|
||||
"test:unit": "NODE_ENV=test jest --selectProjects unit",
|
||||
"test:prettier": "prettier . --check",
|
||||
"test:js": "eslint 'js/**/*.js' 'modules/default/**/*.js' 'clientonly/*.js' 'serveronly/*.js' 'translations/*.js' 'vendor/*.js' 'tests/**/*.js' 'config/*' --config .eslintrc.json",
|
||||
"test:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json",
|
||||
@@ -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,51 +49,55 @@
|
||||
},
|
||||
"homepage": "https://magicmirror.builders",
|
||||
"devDependencies": {
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-jest": "^27.1.7",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
"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.2",
|
||||
"jest": "^29.3.1",
|
||||
"jsdom": "^20.0.3",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"lint-staged": "^14.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"playwright": "^1.29.1",
|
||||
"prettier": "^2.8.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"sinon": "^15.0.1",
|
||||
"stylelint": "^14.16.0",
|
||||
"stylelint-config-prettier": "^9.0.4",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-prettier": "^2.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": "^22.0.0"
|
||||
"electron": "^26.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"colors": "^1.4.0",
|
||||
"console-stamp": "^3.1.0",
|
||||
"digest-fetch": "^2.0.1",
|
||||
"eslint": "^8.30.0",
|
||||
"console-stamp": "^3.1.2",
|
||||
"envsub": "^4.1.0",
|
||||
"eslint": "^8.50.0",
|
||||
"express": "^4.18.2",
|
||||
"express-ipfilter": "^1.3.1",
|
||||
"feedme": "^2.0.2",
|
||||
"helmet": "^6.0.1",
|
||||
"helmet": "^7.0.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"luxon": "^1.28.0",
|
||||
"module-alias": "^2.2.2",
|
||||
"luxon": "^1.28.1",
|
||||
"module-alias": "^2.2.3",
|
||||
"moment": "^2.29.4",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-ical": "^0.15.3",
|
||||
"socket.io": "^4.5.4"
|
||||
"node-ical": "^0.16.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": ">=14"
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
const app = require("../js/app.js");
|
||||
const Log = require("logger");
|
||||
const app = require("../js/app");
|
||||
const Log = require("../js/logger");
|
||||
|
||||
app.start((config) => {
|
||||
app.start().then((config) => {
|
||||
const bindAddress = config.address ? config.address : "localhost";
|
||||
const httpType = config.useHttps ? "https" : "http";
|
||||
Log.log("\nReady to go! Please point your browser to: " + httpType + "://" + bindAddress + ":" + config.port);
|
||||
Log.log(`\nReady to go! Please point your browser to: ${httpType}://${bindAddress}:${config.port}`);
|
||||
});
|
||||
|
@@ -3,8 +3,9 @@
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
|
||||
ipWhitelist: []
|
||||
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
|
||||
ipWhitelist: [],
|
||||
port: 8282
|
||||
});
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
|
@@ -11,9 +11,10 @@ let config = {
|
||||
module: "calendar",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
customEvents: [{ keyword: "CustomEvent", symbol: "dice", eventClass: "undo" }],
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 4,
|
||||
maximumEntries: 5,
|
||||
maximumNumberOfDays: 10000,
|
||||
symbol: "birthday-cake",
|
||||
fullDaySymbol: "calendar-day",
|
||||
|
@@ -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;
|
||||
}
|
@@ -9,7 +9,8 @@ let config = {
|
||||
position: "middle_center",
|
||||
config: {
|
||||
displayType: "analog",
|
||||
analogFace: "face-006"
|
||||
analogFace: "face-006",
|
||||
showDate: false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
25
tests/configs/modules/clock/clock_showDateAnalog.js
Normal file
25
tests/configs/modules/clock/clock_showDateAnalog.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/* MagicMirror² Test config for default clock module
|
||||
*
|
||||
* By Johan Hammar
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
timeFormat: 12,
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "clock",
|
||||
position: "middle_center",
|
||||
config: {
|
||||
showTime: true,
|
||||
showDate: true,
|
||||
displayType: "analog"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
24
tests/configs/modules/clock/clock_showSunMoon.js
Normal file
24
tests/configs/modules/clock/clock_showSunMoon.js
Normal 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;
|
||||
}
|
28
tests/configs/modules/compliments/compliments_animateCSS.js
Normal file
28
tests/configs/modules/compliments/compliments_animateCSS.js
Normal 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;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user