mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-08-21 21:00:57 +00:00
5
.github/CONTRIBUTING.md
vendored
5
.github/CONTRIBUTING.md
vendored
@@ -25,13 +25,14 @@ To run StyleLint, use `npm run lint:style`.
|
||||
Please only submit reproducible issues.
|
||||
|
||||
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
|
||||
|
||||
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
|
||||
|
||||
When submitting a new issue, please supply the following information:
|
||||
|
||||
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3, Windows, Mac, Linux, System V UNIX).
|
||||
**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 0.12.13 or later.
|
||||
**Node Version**: Make sure it's version 10 or later.
|
||||
|
||||
**MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2).
|
||||
|
||||
|
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: MichMich
|
||||
custom: ['https://magicmirror.builders/#donate']
|
6
.github/ISSUE_TEMPLATE.md
vendored
6
.github/ISSUE_TEMPLATE.md
vendored
@@ -6,6 +6,8 @@ If you're not sure if it's a real bug or if it's just you, please open a topic o
|
||||
|
||||
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
|
||||
|
||||
A common problem is that your config file could be invalid. Please run in your MagicMirror directory: `npm run config:check` and see if it reports an error.
|
||||
|
||||
## I found a bug in the MagicMirror installer
|
||||
|
||||
If you are facing an issue or found a bug while trying to install MagicMirror via the installer please report it in the respective GitHub repository:
|
||||
@@ -23,9 +25,9 @@ If you are facing an issue or found a bug while running MagicMirror inside a Doc
|
||||
Please make sure to only submit reproducible issues. You can safely remove everything above the dividing line.
|
||||
When submitting a new issue, please supply the following information:
|
||||
|
||||
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3, Windows, Mac, Linux, System V UNIX).
|
||||
**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 8 or later.
|
||||
**Node Version**: Make sure it's version 10 or later.
|
||||
|
||||
**MagicMirror Version**: Please let us now which version of MagicMirror you are running. It can be found in the `package.log` file.
|
||||
|
||||
|
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,13 +1,28 @@
|
||||
> Please send your pull requests the develop branch.
|
||||
> Don't forget to add the change to CHANGELOG.md.
|
||||
Hello and thank you for wanting to contribute to the MagicMirror project
|
||||
|
||||
**Please make sure that you have followed these 4 rules before submitting your Pull Request:**
|
||||
|
||||
> 1) Base your pull requests against the `develop` branch.
|
||||
>
|
||||
>
|
||||
> 2) Include these infos in the description:
|
||||
> * Does the pull request solve a **related** issue?
|
||||
> * If so, can you reference the issue like this `Fixes #<issue_number>`?
|
||||
> * What does the pull request accomplish? Use a list if needed.
|
||||
> * If it includes major visual changes please add screenshots.
|
||||
>
|
||||
>
|
||||
> 3) Please run `npm run lint:prettier` before submitting so that
|
||||
> style issues are fixed.
|
||||
>
|
||||
>
|
||||
> 4) Don't forget to add an entry about your changes to
|
||||
> the CHANGELOG.md file.
|
||||
|
||||
|
||||
**Note**: Sometimes the development moves very fast. It is highly
|
||||
recommended that you update your branch of `develop` before creating a
|
||||
pull request to send us your changes. This makes everyone's lives
|
||||
easier (including yours) and helps us out on the development team.
|
||||
Thanks!
|
||||
|
||||
- Does the pull request solve a **related** issue?
|
||||
- If so, can you reference the issue?
|
||||
- What does the pull request accomplish? Use a list if needed.
|
||||
- If it includes major visual changes please add screenshots.
|
||||
Thanks again and have a nice day!
|
||||
|
24
.github/workflows/codecov-test-suites.yml
vendored
Normal file
24
.github/workflows/codecov-test-suites.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# This workflow runs the automated test and uploads the coverage results to codecov.io
|
||||
|
||||
name: "Run Codecov Tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, develop ]
|
||||
pull_request:
|
||||
branches: [ master, develop ]
|
||||
|
||||
jobs:
|
||||
run-and-upload-coverage-report:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm ci
|
||||
npm run test:coverage
|
||||
- uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
fail_ci_if_error: true
|
4
.github/workflows/enforce-changelog.yml
vendored
4
.github/workflows/enforce-changelog.yml
vendored
@@ -1,10 +1,12 @@
|
||||
# This workflow enforces the update of a changelog file on every pull request
|
||||
|
||||
name: "Enforce Changelog"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
# Enforces the update of a changelog file on every pull request
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
5
.github/workflows/node-ci.js.yml
vendored
5
.github/workflows/node-ci.js.yml
vendored
@@ -1,7 +1,7 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Automated Tests
|
||||
name: "Run Automated Tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,13 +11,10 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
|
82
CHANGELOG.md
82
CHANGELOG.md
@@ -5,6 +5,70 @@ 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.15.0] - 2021-04-01
|
||||
|
||||
Special thanks to the following contributors: @EdgardosReis, @MystaraTheGreat, @TheDuffman85, @ashishtank, @buxxi, @codac, @fewieden, @khassel, @klaernie, @qu1que, @rejas, @sdetweil & @thomasrockhu.
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||
|
||||
### Added
|
||||
|
||||
- Added Galician language.
|
||||
- Added GitHub workflows for automated testing and changelog enforcement.
|
||||
- Added CodeCov badge to Readme.
|
||||
- Added CURRENTWEATHER_TYPE notification to currentweather and weather module, use it in compliments module.
|
||||
- Added `start:dev` command to the npm scripts for starting electron with devTools open.
|
||||
- Added logging when using deprecated modules weatherforecast or currentweather.
|
||||
- Added Portuguese translations for "MODULE_CONFIG_CHANGED" and "PRECIP".
|
||||
- Respect parameter ColoredSymbolOnly also for custom events.
|
||||
- Added a new parameter to hide time portion on relative times.
|
||||
- `module.show` has now the option for a callback on error.
|
||||
- Added locale to sample config file.
|
||||
- Added support for self-signed certificates for the default calendar module (#466).
|
||||
- Added hiddenOnStartup flag to module config (#2475).
|
||||
|
||||
### Updated
|
||||
|
||||
- Updated markdown files for github.
|
||||
- Cleaned up old code on server side.
|
||||
- Convert `-0` to `0` when displaying temperature.
|
||||
- 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.
|
||||
- Bump electron to v11 and enable contextIsolation.
|
||||
- Don't update the DOM when a module is not displayed.
|
||||
- Cleaned up jsdoc and tests.
|
||||
- Exposed logger as node module for easier access for 3rd party modules.
|
||||
- Replaced deprecated `request` package with `node-fetch` and `digest-fetch`.
|
||||
- Refactored calendar fetcher.
|
||||
- Cleaned up newsfeed module.
|
||||
- Cleaned up translations and translator code.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed danger.js library.
|
||||
- Removed `ical` which was substituted by `node-ical` in release `v2.13.0`. Module developers must install this dependency themselves in the module folder if needed.
|
||||
- Removed valid-url library.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Added default log levels to stop calendar log spamming.
|
||||
- Fix socket.io cors errors, see [breaking change since socket.io v3](https://socket.io/docs/v3/handling-cors/).
|
||||
- Fix Issue with weather forecast icons due to fixed day start and end time (#2221).
|
||||
- Fix empty directory for each module's main javascript file in the inspector.
|
||||
- Fix Issue with weather forecast icons unit tests with different timezones (#2221).
|
||||
- Fix issue with unencoded characters in translated strings when using nunjuck template (`Loading …` as an example).
|
||||
- Fix socket.io backward compatibility with socket v2 clients.
|
||||
- Fix 3rd party module language loading if language is English.
|
||||
- Fix e2e tests after spectron update.
|
||||
- Fix updatenotification creating zombie processes by setting a timeout for the git process.
|
||||
- Fix weather module openweathermap not loading if lat and lon set without onecall.
|
||||
- Fix calendar daylight savings offset calculation if recurring start date before 2007.
|
||||
- Fix calendar time/date adjustment when time with GMT offset is different day (#2488).
|
||||
- Fix calendar daylight savings offset calculation if recurring FULL DAY start date before 2007 (#2483).
|
||||
- Fix newsreaders template, for wrong test for nowrap in 2 places (should be if not).
|
||||
|
||||
## [2.14.0] - 2021-01-01
|
||||
|
||||
Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank, @bluemanos, @flopp999, @jakemulley, @jakobsarwary1, @marvai-vgtu, @mirontoli, @rejas, @sdetweil, @Snille & @Sub028.
|
||||
@@ -15,17 +79,15 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
|
||||
|
||||
- Added new log level "debug" to the logger.
|
||||
- Added new parameter "useKmh" to weather module for displaying wind speed as kmh.
|
||||
- Chuvash translation.
|
||||
- Added Chuvash translation.
|
||||
- Added Weatherbit as a provider to Weather module.
|
||||
- Added SMHI as a provider to Weather module.
|
||||
- Added Hindi & Gujarati translation.
|
||||
- Added optional support for DEGREE position in Feels like translation.
|
||||
- Added support for variables in nunjucks templates for translate filter.
|
||||
- Added Chuvash translation.
|
||||
- Calendar: new options "limitDays" and "coloredEvents".
|
||||
- Added new option "limitDays" - limit the number of discreet days displayed.
|
||||
- Added new option "customEvents" - use custom symbol/color based on keyword in event title.
|
||||
- Added GitHub workflows for automated testing and changelog enforcement.
|
||||
|
||||
### Updated
|
||||
|
||||
@@ -44,7 +106,7 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
|
||||
|
||||
### Deleted
|
||||
|
||||
- Removed Travis CI intergration.
|
||||
- Removed Travis CI integration.
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -61,8 +123,8 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
|
||||
- Fix non-fullday recurring rule processing. (#2216)
|
||||
- Catch errors when parsing calendar data with ical. (#2022)
|
||||
- Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228)
|
||||
- Weather module - Always displays night icons when local is other then English. (#2221)
|
||||
- Update Node-ical 0.12.4 , fix invalid RRULE format in cal entries
|
||||
- Weather module - Always displays night icons when local is other than English. (#2221)
|
||||
- Update node-ical 0.12.4, fix invalid RRULE format in cal entries
|
||||
- Fix package.json for optional electron dependency (2378)
|
||||
- Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379)
|
||||
- Remove undefined objects from modules array (#2382)
|
||||
@@ -77,11 +139,11 @@ Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura
|
||||
|
||||
### Added
|
||||
|
||||
- `--dry-run` option adde in fetch call within updatenotification node_helper. This is to prevent
|
||||
- `--dry-run` Added option in fetch call within updatenotification node_helper. This is to prevent
|
||||
MagicMirror from consuming any fetch result. Causes conflict with MMPM when attempting to check
|
||||
for updates to MagicMirror and/or MagicMirror modules.
|
||||
- Test coverage with Istanbul, run it with `npm run test:coverage`.
|
||||
- Add lithuanian language.
|
||||
- Added lithuanian language.
|
||||
- Added support in weatherforecast for OpenWeather onecall API.
|
||||
- Added config option to calendar-icons for recurring- and fullday-events.
|
||||
- Added current, hourly (max 48), and daily (max 7) weather forecasts to weather module via OpenWeatherMap One Call API.
|
||||
@@ -145,7 +207,7 @@ Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryan
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- Throw error when check_config fails. [#1928](https://github.com/MichMich/MagicMirror/issues/1928)
|
||||
- Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MichMich/MagicMirror/issues/2050)
|
||||
- Updated ical library to latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926)
|
||||
- Updated ical library to the latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926)
|
||||
- Fix config check after merge of prettier [#2109](https://github.com/MichMich/MagicMirror/issues/2109)
|
||||
|
||||
## [2.11.0] - 2020-04-01
|
||||
@@ -439,7 +501,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
### Fixed
|
||||
|
||||
- Fixed gzip encoded calendar loading issue #1400.
|
||||
- Mixup between german and spanish translation for newsfeed.
|
||||
- Fixed mixup between german and spanish translation for newsfeed.
|
||||
- Fixed close dates to be absolute, if no configured in the config.js - module Calendar
|
||||
- Fixed the updatenotification module message about new commits in the repository, so they can be correctly localized in singular and plural form.
|
||||
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MichMich/MagicMirror/issues/1374)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright © 2016-2020 Michael Teeuw
|
||||
Copyright © 2016-2021 Michael Teeuw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
|
19
README.md
19
README.md
@@ -1,11 +1,13 @@
|
||||

|
||||
|
||||
<p align="center">
|
||||
<p style="text-align: center">
|
||||
<a href="https://david-dm.org/MichMich/MagicMirror"><img src="https://david-dm.org/MichMich/MagicMirror.svg" alt="Dependency Status"></a>
|
||||
<a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
|
||||
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge"></a>
|
||||
<a href="https://david-dm.org/MichMich/MagicMirror?type=dev"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
|
||||
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge" alt="CLI Best Practices"></a>
|
||||
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/></a>
|
||||
<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
||||
<a href="https://github.com/MichMich/MagicMirror/actions?query=workflow%3A%22Automated+Tests%22"><img src="https://github.com/MichMich/MagicMirror/workflows/Automated%20Tests/badge.svg" alt="Tests"></a>
|
||||
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg" /></a>
|
||||
</p>
|
||||
|
||||
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
|
||||
@@ -27,7 +29,13 @@ For the full documentation including **[installation instructions](https://docs.
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
Contributions of all kinds are welcome, not only in the form of code but also with regards bug reports and documentation. For the full contribution guidelines, check out: [https://docs.magicmirror.builders/getting-started/contributing.html](https://docs.magicmirror.builders/getting-started/contributing.html)
|
||||
Contributions of all kinds are welcome, not only in the form of code but also with regards to
|
||||
|
||||
- bug reports
|
||||
- documentation
|
||||
- translations
|
||||
|
||||
For the full contribution guidelines, check out: [https://docs.magicmirror.builders/getting-started/contributing.html](https://docs.magicmirror.builders/getting-started/contributing.html)
|
||||
|
||||
## Enjoying MagicMirror? Consider a donation!
|
||||
|
||||
@@ -38,7 +46,6 @@ If we receive enough donations we might even be able to free up some working hou
|
||||
|
||||
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
|
||||
|
||||
<p align="center">
|
||||
<br>
|
||||
<p style="text-align: center">
|
||||
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
||||
</p>
|
||||
|
@@ -14,7 +14,6 @@
|
||||
*
|
||||
* @param {string} key key to look for at the command line
|
||||
* @param {string} defaultValue value if no key is given at the command line
|
||||
*
|
||||
* @returns {string} the value of the parameter
|
||||
*/
|
||||
function getCommandLineParameter(key, defaultValue = undefined) {
|
||||
@@ -36,7 +35,6 @@
|
||||
* Gets the config from the specified server url
|
||||
*
|
||||
* @param {string} url location where the server is running.
|
||||
*
|
||||
* @returns {Promise} the config
|
||||
*/
|
||||
function getServerConfig(url) {
|
||||
@@ -66,7 +64,7 @@
|
||||
/**
|
||||
* Print a message to the console in case of errors
|
||||
*
|
||||
* @param {string} [message] error message to print
|
||||
* @param {string} message error message to print
|
||||
* @param {number} code error code for the exit call
|
||||
*/
|
||||
function fail(message, code = 1) {
|
||||
|
@@ -28,6 +28,7 @@ var config = {
|
||||
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
|
||||
|
||||
language: "en",
|
||||
locale: "en-US",
|
||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
@@ -66,22 +67,26 @@ var config = {
|
||||
position: "lower_third"
|
||||
},
|
||||
{
|
||||
module: "currentweather",
|
||||
module: "weather",
|
||||
position: "top_right",
|
||||
config: {
|
||||
weatherProvider: "openweathermap",
|
||||
type: "current",
|
||||
location: "New York",
|
||||
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||
apiKey: "YOUR_OPENWEATHER_API_KEY"
|
||||
}
|
||||
},
|
||||
{
|
||||
module: "weatherforecast",
|
||||
module: "weather",
|
||||
position: "top_right",
|
||||
header: "Weather Forecast",
|
||||
config: {
|
||||
weatherProvider: "openweathermap",
|
||||
type: "forecast",
|
||||
location: "New York",
|
||||
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||
apiKey: "YOUR_OPENWEATHER_API_KEY"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@@ -1,17 +0,0 @@
|
||||
import { danger, fail, warn } from "danger";
|
||||
|
||||
// Check if the CHANGELOG.md file has been edited
|
||||
// Fail the build and post a comment reminding submitters to do so if it wasn't changed
|
||||
if (!danger.git.modified_files.includes("CHANGELOG.md")) {
|
||||
warn("Please include an updated `CHANGELOG.md` file.<br>This way we can keep track of all the contributions.");
|
||||
}
|
||||
|
||||
// Check if the PR request is send to the master branch.
|
||||
// This should only be done by MichMich.
|
||||
if (danger.github.pr.base.ref === "master" && danger.github.pr.user.login !== "MichMich") {
|
||||
// Check if the PR body or title includes the text: #accepted.
|
||||
// If not, the PR will fail.
|
||||
if ((danger.github.pr.body + danger.github.pr.title).includes("#accepted")) {
|
||||
fail("Please send all your pull requests to the `develop` branch.<br>Pull requests on the `master` branch will not be accepted.");
|
||||
}
|
||||
}
|
132
js/app.js
132
js/app.js
@@ -4,22 +4,23 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var Log = require(__dirname + "/logger.js");
|
||||
var Server = require(__dirname + "/server.js");
|
||||
var Utils = require(__dirname + "/utils.js");
|
||||
var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js");
|
||||
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Log = require("logger");
|
||||
const Server = require(`${__dirname}/server`);
|
||||
const Utils = require(`${__dirname}/utils`);
|
||||
const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`);
|
||||
|
||||
// Get version number.
|
||||
global.version = JSON.parse(fs.readFileSync("package.json", "utf8")).version;
|
||||
global.version = require(`${__dirname}/../package.json`).version;
|
||||
Log.log("Starting MagicMirror: v" + global.version);
|
||||
|
||||
// global absolute root path
|
||||
global.root_path = path.resolve(__dirname + "/../");
|
||||
global.root_path = path.resolve(`${__dirname}/../`);
|
||||
|
||||
if (process.env.MM_CONFIG_FILE) {
|
||||
global.configuration_file = process.env.MM_CONFIG_FILE;
|
||||
@@ -45,43 +46,40 @@ process.on("uncaughtException", function (err) {
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
var App = function () {
|
||||
var nodeHelpers = [];
|
||||
function App() {
|
||||
let nodeHelpers = [];
|
||||
|
||||
/**
|
||||
* Loads the config file. Combines it with the defaults, and runs the
|
||||
* 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
|
||||
*/
|
||||
var loadConfig = function (callback) {
|
||||
function loadConfig(callback) {
|
||||
Log.log("Loading config ...");
|
||||
var defaults = require(__dirname + "/defaults.js");
|
||||
const defaults = require(`${__dirname}/defaults`);
|
||||
|
||||
// For this check proposed to TestSuite
|
||||
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
|
||||
var configFilename = path.resolve(global.root_path + "/config/config.js");
|
||||
if (typeof global.configuration_file !== "undefined") {
|
||||
configFilename = path.resolve(global.configuration_file);
|
||||
}
|
||||
const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`);
|
||||
|
||||
try {
|
||||
fs.accessSync(configFilename, fs.F_OK);
|
||||
var c = require(configFilename);
|
||||
const c = require(configFilename);
|
||||
checkDeprecatedOptions(c);
|
||||
var config = Object.assign(defaults, c);
|
||||
const config = Object.assign(defaults, c);
|
||||
callback(config);
|
||||
} catch (e) {
|
||||
if (e.code === "ENOENT") {
|
||||
Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
|
||||
} else if (e instanceof ReferenceError || e instanceof SyntaxError) {
|
||||
Log.error(Utils.colors.error("WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: " + e.stack));
|
||||
Log.error(Utils.colors.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`));
|
||||
} else {
|
||||
Log.error(Utils.colors.error("WARNING! Could not load config file. Starting with default configuration. Error found: " + e));
|
||||
Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
|
||||
}
|
||||
callback(defaults);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the config for deprecated options and throws a warning in the logs
|
||||
@@ -89,21 +87,15 @@ var App = function () {
|
||||
*
|
||||
* @param {object} userConfig The user config
|
||||
*/
|
||||
var checkDeprecatedOptions = function (userConfig) {
|
||||
var deprecated = require(global.root_path + "/js/deprecated.js");
|
||||
var deprecatedOptions = deprecated.configs;
|
||||
function checkDeprecatedOptions(userConfig) {
|
||||
const deprecated = require(`${global.root_path}/js/deprecated`);
|
||||
const deprecatedOptions = deprecated.configs;
|
||||
|
||||
var usedDeprecated = [];
|
||||
|
||||
deprecatedOptions.forEach(function (option) {
|
||||
if (userConfig.hasOwnProperty(option)) {
|
||||
usedDeprecated.push(option);
|
||||
}
|
||||
});
|
||||
const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option));
|
||||
if (usedDeprecated.length > 0) {
|
||||
Log.warn(Utils.colors.warn("WARNING! Your config is using deprecated options: " + usedDeprecated.join(", ") + ". Check README and CHANGELOG for more up-to-date ways of getting the same functionality."));
|
||||
Log.warn(Utils.colors.warn(`WARNING! Your config is using deprecated options: ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific module.
|
||||
@@ -111,35 +103,35 @@ var App = function () {
|
||||
* @param {string} module The name of the module (including subpath).
|
||||
* @param {Function} callback Function to be called after loading
|
||||
*/
|
||||
var loadModule = function (module, callback) {
|
||||
var elements = module.split("/");
|
||||
var moduleName = elements[elements.length - 1];
|
||||
var moduleFolder = __dirname + "/../modules/" + module;
|
||||
function loadModule(module, callback) {
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${__dirname}/../modules/${module}`;
|
||||
|
||||
if (defaultModules.indexOf(moduleName) !== -1) {
|
||||
moduleFolder = __dirname + "/../modules/default/" + module;
|
||||
if (defaultModules.includes(moduleName)) {
|
||||
moduleFolder = `${__dirname}/../modules/default/${module}`;
|
||||
}
|
||||
|
||||
var helperPath = moduleFolder + "/node_helper.js";
|
||||
const helperPath = `${moduleFolder}/node_helper.js`;
|
||||
|
||||
var loadModule = true;
|
||||
let loadHelper = true;
|
||||
try {
|
||||
fs.accessSync(helperPath, fs.R_OK);
|
||||
} catch (e) {
|
||||
loadModule = false;
|
||||
Log.log("No helper found for module: " + moduleName + ".");
|
||||
loadHelper = false;
|
||||
Log.log(`No helper found for module: ${moduleName}.`);
|
||||
}
|
||||
|
||||
if (loadModule) {
|
||||
var Module = require(helperPath);
|
||||
var m = new Module();
|
||||
if (loadHelper) {
|
||||
const Module = require(helperPath);
|
||||
let m = new Module();
|
||||
|
||||
if (m.requiresVersion) {
|
||||
Log.log("Check MagicMirror version for node helper '" + moduleName + "' - Minimum version: " + m.requiresVersion + " - Current version: " + global.version);
|
||||
Log.log(`Check MagicMirror version for node helper '${moduleName}' - Minimum version: ${m.requiresVersion} - Current version: ${global.version}`);
|
||||
if (cmpVersions(global.version, m.requiresVersion) >= 0) {
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
Log.log("Version is incorrect. Skip module: '" + moduleName + "'");
|
||||
Log.warn(`Version is incorrect. Skip module: '${moduleName}'`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -152,7 +144,7 @@ var App = function () {
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all modules.
|
||||
@@ -160,12 +152,15 @@ var App = function () {
|
||||
* @param {Module[]} modules All modules to be loaded
|
||||
* @param {Function} callback Function to be called after loading
|
||||
*/
|
||||
var loadModules = function (modules, callback) {
|
||||
function loadModules(modules, callback) {
|
||||
Log.log("Loading module helpers ...");
|
||||
|
||||
var loadNextModule = function () {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function loadNextModule() {
|
||||
if (modules.length > 0) {
|
||||
var nextModule = modules[0];
|
||||
const nextModule = modules[0];
|
||||
loadModule(nextModule, function () {
|
||||
modules = modules.slice(1);
|
||||
loadNextModule();
|
||||
@@ -175,10 +170,10 @@ var App = function () {
|
||||
Log.log("All module helpers loaded.");
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
loadNextModule();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
@@ -190,11 +185,11 @@ var App = function () {
|
||||
* number if a is smaller and 0 if they are the same
|
||||
*/
|
||||
function cmpVersions(a, b) {
|
||||
var i, diff;
|
||||
var regExStrip0 = /(\.0+)+$/;
|
||||
var segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
var segmentsB = b.replace(regExStrip0, "").split(".");
|
||||
var l = Math.min(segmentsA.length, segmentsB.length);
|
||||
let i, diff;
|
||||
const regExStrip0 = /(\.0+)+$/;
|
||||
const segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
const segmentsB = b.replace(regExStrip0, "").split(".");
|
||||
const l = Math.min(segmentsA.length, segmentsB.length);
|
||||
|
||||
for (i = 0; i < l; i++) {
|
||||
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
|
||||
@@ -219,21 +214,19 @@ var App = function () {
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
var modules = [];
|
||||
let modules = [];
|
||||
|
||||
for (var m in config.modules) {
|
||||
var module = config.modules[m];
|
||||
if (modules.indexOf(module.module) === -1 && !module.disabled) {
|
||||
for (const module of config.modules) {
|
||||
if (!modules.includes(module.module) && !module.disabled) {
|
||||
modules.push(module.module);
|
||||
}
|
||||
}
|
||||
|
||||
loadModules(modules, function () {
|
||||
var server = new Server(config, function (app, io) {
|
||||
const server = new Server(config, function (app, io) {
|
||||
Log.log("Server started ...");
|
||||
|
||||
for (var h in nodeHelpers) {
|
||||
var nodeHelper = nodeHelpers[h];
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
nodeHelper.setExpressApp(app);
|
||||
nodeHelper.setSocketIO(io);
|
||||
nodeHelper.start();
|
||||
@@ -256,8 +249,7 @@ var App = function () {
|
||||
* Added to fix #1056
|
||||
*/
|
||||
this.stop = function () {
|
||||
for (var h in nodeHelpers) {
|
||||
var nodeHelper = nodeHelpers[h];
|
||||
for (const nodeHelper of nodeHelpers) {
|
||||
if (typeof nodeHelper.stop === "function") {
|
||||
nodeHelper.stop();
|
||||
}
|
||||
@@ -292,6 +284,6 @@ var App = function () {
|
||||
this.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = new App();
|
||||
|
@@ -11,9 +11,9 @@ const linter = new Linter();
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const rootPath = path.resolve(__dirname + "/../");
|
||||
const Log = require(rootPath + "/js/logger.js");
|
||||
const Utils = require(rootPath + "/js/utils.js");
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const Utils = require(`${rootPath}/js/utils.js`);
|
||||
|
||||
/**
|
||||
* Returns a string with path of configuration file.
|
||||
@@ -23,11 +23,7 @@ const Utils = require(rootPath + "/js/utils.js");
|
||||
*/
|
||||
function getConfigFile() {
|
||||
// FIXME: This function should be in core. Do you want refactor me ;) ?, be good!
|
||||
let configFileName = path.resolve(rootPath + "/config/config.js");
|
||||
if (process.env.MM_CONFIG_FILE) {
|
||||
configFileName = path.resolve(process.env.MM_CONFIG_FILE);
|
||||
}
|
||||
return configFileName;
|
||||
return path.resolve(process.env.MM_CONFIG_FILE || `${rootPath}/config/config.js`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,21 +50,18 @@ function checkConfigFile() {
|
||||
Log.info(Utils.colors.info("Checking file... "), configFileName);
|
||||
|
||||
// I'm not sure if all ever is utf-8
|
||||
fs.readFile(configFileName, "utf-8", function (err, data) {
|
||||
if (err) {
|
||||
throw err;
|
||||
const configFile = fs.readFileSync(configFileName, "utf-8");
|
||||
|
||||
const errors = linter.verify(configFile);
|
||||
if (errors.length === 0) {
|
||||
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
||||
} else {
|
||||
Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
|
||||
|
||||
for (const error of errors) {
|
||||
Log.error(`Line ${error.line} column ${error.column}: ${error.message}`);
|
||||
}
|
||||
const messages = linter.verify(data);
|
||||
if (messages.length === 0) {
|
||||
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
||||
} else {
|
||||
Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
|
||||
// In case the there errors show messages and return
|
||||
messages.forEach((error) => {
|
||||
Log.error("Line", error.line, "col", error.column, error.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkConfigFile();
|
||||
|
@@ -20,6 +20,7 @@ var defaults = {
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
zoom: 1,
|
||||
@@ -42,7 +43,7 @@ var defaults = {
|
||||
module: "helloworld",
|
||||
position: "middle_center",
|
||||
config: {
|
||||
text: "Please create a config file."
|
||||
text: "Please create a config file or check the existing one for errors."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58,7 +59,7 @@ var defaults = {
|
||||
position: "middle_center",
|
||||
classes: "xsmall",
|
||||
config: {
|
||||
text: "If you get this message while your config file is already<br>created, your config file probably contains an error.<br>Use a JavaScript linter to validate your file."
|
||||
text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror directory<br>" + "<pre>npm run config:check</pre>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@@ -6,11 +6,6 @@
|
||||
* Olex S. original idea this deprecated option
|
||||
*/
|
||||
|
||||
var deprecated = {
|
||||
module.exports = {
|
||||
configs: ["kioskmode"]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = deprecated;
|
||||
}
|
||||
|
@@ -2,10 +2,10 @@
|
||||
|
||||
const electron = require("electron");
|
||||
const core = require("./app.js");
|
||||
const Log = require("./logger.js");
|
||||
const Log = require("logger");
|
||||
|
||||
// Config
|
||||
var config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
let config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
// Module to control application life.
|
||||
const app = electron.app;
|
||||
// Module to create native browser window.
|
||||
@@ -20,13 +20,14 @@ let mainWindow;
|
||||
*/
|
||||
function createWindow() {
|
||||
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
|
||||
var electronOptionsDefaults = {
|
||||
let electronOptionsDefaults = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
x: 0,
|
||||
y: 0,
|
||||
darkTheme: true,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
zoomFactor: config.zoom
|
||||
},
|
||||
@@ -42,7 +43,7 @@ function createWindow() {
|
||||
electronOptionsDefaults.autoHideMenuBar = true;
|
||||
}
|
||||
|
||||
var electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
|
||||
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
|
||||
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow(electronOptions);
|
||||
@@ -50,14 +51,14 @@ function createWindow() {
|
||||
// and load the index.html of the app.
|
||||
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
|
||||
|
||||
var prefix;
|
||||
let prefix;
|
||||
if (config["tls"] !== null && config["tls"]) {
|
||||
prefix = "https://";
|
||||
} else {
|
||||
prefix = "http://";
|
||||
}
|
||||
|
||||
var address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address;
|
||||
let address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address;
|
||||
mainWindow.loadURL(`${prefix}${address}:${config.port}`);
|
||||
|
||||
// Open the DevTools if run with "npm start dev"
|
||||
@@ -125,7 +126,7 @@ app.on("before-quit", (event) => {
|
||||
|
||||
// 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].indexOf(config.address) > -1) {
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
|
||||
core.start(function (c) {
|
||||
config = c;
|
||||
});
|
||||
|
11
js/loader.js
11
js/loader.js
@@ -54,6 +54,14 @@ var Loader = (function () {
|
||||
|
||||
// Notify core of loaded modules.
|
||||
MM.modulesStarted(moduleObjects);
|
||||
|
||||
// Starting modules also hides any modules that have requested to be initially hidden
|
||||
for (let thisModule of moduleObjects) {
|
||||
if (thisModule.data.hiddenOnStartup) {
|
||||
Log.info("Initially hiding " + thisModule.name);
|
||||
thisModule.hide();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -97,6 +105,7 @@ var Loader = (function () {
|
||||
path: moduleFolder + "/",
|
||||
file: moduleName + ".js",
|
||||
position: moduleData.position,
|
||||
hiddenOnStartup: moduleData.hiddenOnStartup,
|
||||
header: moduleData.header,
|
||||
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,
|
||||
config: moduleData.config,
|
||||
@@ -114,7 +123,7 @@ var Loader = (function () {
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
var loadModule = function (module, callback) {
|
||||
var url = module.path + "/" + module.file;
|
||||
var url = module.path + module.file;
|
||||
|
||||
var afterLoad = function () {
|
||||
var moduleObject = Module.create(module.name);
|
||||
|
@@ -295,6 +295,9 @@ var MM = (function () {
|
||||
// Otherwise cancel show action.
|
||||
if (module.lockStrings.length !== 0 && options.force !== true) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -440,7 +443,6 @@ var 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.
|
||||
*/
|
||||
var exceptModule = function (module) {
|
||||
@@ -547,6 +549,11 @@ var MM = (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!module.data.position) {
|
||||
Log.warn("module tries to update the DOM without being displayed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Further implementation is done in the private method.
|
||||
updateDom(module, speed);
|
||||
},
|
||||
|
53
js/module.js
53
js/module.js
@@ -176,7 +176,7 @@ var Module = Class.extend({
|
||||
});
|
||||
|
||||
this._nunjucksEnvironment.addFilter("translate", function (str, variables) {
|
||||
return self.translate(str, variables);
|
||||
return nunjucks.runtime.markSafe(self.translate(str, variables));
|
||||
});
|
||||
|
||||
return this._nunjucksEnvironment;
|
||||
@@ -311,33 +311,33 @@ var Module = Class.extend({
|
||||
*
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadTranslations: function (callback) {
|
||||
var self = this;
|
||||
var translations = this.getTranslations();
|
||||
var lang = config.language.toLowerCase();
|
||||
loadTranslations(callback) {
|
||||
const translations = this.getTranslations() || {};
|
||||
const language = config.language.toLowerCase();
|
||||
|
||||
// The variable `first` will contain the first
|
||||
// defined translation after the following line.
|
||||
for (var first in translations) {
|
||||
break;
|
||||
}
|
||||
const languages = Object.keys(translations);
|
||||
const fallbackLanguage = languages[0];
|
||||
|
||||
if (translations) {
|
||||
var translationFile = translations[lang] || undefined;
|
||||
var translationsFallbackFile = translations[first];
|
||||
|
||||
// If a translation file is set, load it and then also load the fallback translation file.
|
||||
// Otherwise only load the fallback translation file.
|
||||
if (translationFile !== undefined && translationFile !== translationsFallbackFile) {
|
||||
Translator.load(self, translationFile, false, function () {
|
||||
Translator.load(self, translationsFallbackFile, true, callback);
|
||||
});
|
||||
} else {
|
||||
Translator.load(self, translationsFallbackFile, true, callback);
|
||||
}
|
||||
} else {
|
||||
if (languages.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const translationFile = translations[language];
|
||||
const translationsFallbackFile = translations[fallbackLanguage];
|
||||
|
||||
if (!translationFile) {
|
||||
Translator.load(this, translationsFallbackFile, true, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
Translator.load(this, translationFile, false, () => {
|
||||
if (translationFile !== translationsFallbackFile) {
|
||||
Translator.load(this, translationsFallbackFile, true, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -428,12 +428,11 @@ var Module = Class.extend({
|
||||
callback = callback || function () {};
|
||||
options = options || {};
|
||||
|
||||
var self = this;
|
||||
MM.showModule(
|
||||
this,
|
||||
speed,
|
||||
function () {
|
||||
self.resume();
|
||||
() => {
|
||||
this.resume();
|
||||
callback();
|
||||
},
|
||||
options
|
||||
|
@@ -5,21 +5,21 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Class = require("./class.js");
|
||||
const Log = require("./logger.js");
|
||||
const Log = require("logger");
|
||||
const express = require("express");
|
||||
|
||||
var NodeHelper = Class.extend({
|
||||
init: function () {
|
||||
const NodeHelper = Class.extend({
|
||||
init() {
|
||||
Log.log("Initializing new module helper ...");
|
||||
},
|
||||
|
||||
loaded: function (callback) {
|
||||
Log.log("Module helper loaded: " + this.name);
|
||||
loaded(callback) {
|
||||
Log.log(`Module helper loaded: ${this.name}`);
|
||||
callback();
|
||||
},
|
||||
|
||||
start: function () {
|
||||
Log.log("Starting module helper: " + this.name);
|
||||
start() {
|
||||
Log.log(`Starting module helper: ${this.name}`);
|
||||
},
|
||||
|
||||
/* stop()
|
||||
@@ -28,8 +28,8 @@ var NodeHelper = Class.extend({
|
||||
* gracefully exit the module.
|
||||
*
|
||||
*/
|
||||
stop: function () {
|
||||
Log.log("Stopping module helper: " + this.name);
|
||||
stop() {
|
||||
Log.log(`Stopping module helper: ${this.name}`);
|
||||
},
|
||||
|
||||
/* socketNotificationReceived(notification, payload)
|
||||
@@ -38,8 +38,8 @@ var NodeHelper = Class.extend({
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
|
||||
socketNotificationReceived(notification, payload) {
|
||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||
},
|
||||
|
||||
/* setName(name)
|
||||
@@ -47,7 +47,7 @@ var NodeHelper = Class.extend({
|
||||
*
|
||||
* argument name string - Module name.
|
||||
*/
|
||||
setName: function (name) {
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ var NodeHelper = Class.extend({
|
||||
*
|
||||
* argument path string - Module path.
|
||||
*/
|
||||
setPath: function (path) {
|
||||
setPath(path) {
|
||||
this.path = path;
|
||||
},
|
||||
|
||||
@@ -66,7 +66,7 @@ var NodeHelper = Class.extend({
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
*/
|
||||
sendSocketNotification: function (notification, payload) {
|
||||
sendSocketNotification(notification, payload) {
|
||||
this.io.of(this.name).emit(notification, payload);
|
||||
},
|
||||
|
||||
@@ -76,11 +76,10 @@ var NodeHelper = Class.extend({
|
||||
*
|
||||
* argument app Express app - The Express app object.
|
||||
*/
|
||||
setExpressApp: function (app) {
|
||||
setExpressApp(app) {
|
||||
this.expressApp = app;
|
||||
|
||||
var publicPath = this.path + "/public";
|
||||
app.use("/" + this.name, express.static(publicPath));
|
||||
app.use(`/${this.name}`, express.static(`${this.path}/public`));
|
||||
},
|
||||
|
||||
/* setSocketIO(io)
|
||||
@@ -89,27 +88,25 @@ var NodeHelper = Class.extend({
|
||||
*
|
||||
* argument io Socket.io - The Socket io object.
|
||||
*/
|
||||
setSocketIO: function (io) {
|
||||
var self = this;
|
||||
self.io = io;
|
||||
setSocketIO(io) {
|
||||
this.io = io;
|
||||
|
||||
Log.log("Connecting socket for: " + this.name);
|
||||
var namespace = this.name;
|
||||
io.of(namespace).on("connection", function (socket) {
|
||||
Log.log(`Connecting socket for: ${this.name}`);
|
||||
|
||||
io.of(this.name).on("connection", (socket) => {
|
||||
// add a catch all event.
|
||||
var onevent = socket.onevent;
|
||||
const onevent = socket.onevent;
|
||||
socket.onevent = function (packet) {
|
||||
var args = packet.data || [];
|
||||
const args = packet.data || [];
|
||||
onevent.call(this, packet); // original call
|
||||
packet.data = ["*"].concat(args);
|
||||
onevent.call(this, packet); // additional call to catch-all
|
||||
};
|
||||
|
||||
// register catch all.
|
||||
socket.on("*", function (notification, payload) {
|
||||
socket.on("*", (notification, payload) => {
|
||||
if (notification !== "*") {
|
||||
//Log.log('received message in namespace: ' + namespace);
|
||||
self.socketNotificationReceived(notification, payload);
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -120,7 +117,4 @@ NodeHelper.create = function (moduleDefinition) {
|
||||
return NodeHelper.extend(moduleDefinition);
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = NodeHelper;
|
||||
}
|
||||
module.exports = NodeHelper;
|
||||
|
61
js/server.js
61
js/server.js
@@ -4,25 +4,29 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var express = require("express");
|
||||
var app = require("express")();
|
||||
var path = require("path");
|
||||
var ipfilter = require("express-ipfilter").IpFilter;
|
||||
var fs = require("fs");
|
||||
var helmet = require("helmet");
|
||||
const express = require("express");
|
||||
const app = require("express")();
|
||||
const path = require("path");
|
||||
const ipfilter = require("express-ipfilter").IpFilter;
|
||||
const fs = require("fs");
|
||||
const helmet = require("helmet");
|
||||
|
||||
var Log = require("./logger.js");
|
||||
var Utils = require("./utils.js");
|
||||
const Log = require("logger");
|
||||
const Utils = require("./utils.js");
|
||||
|
||||
var Server = function (config, callback) {
|
||||
var port = config.port;
|
||||
if (process.env.MM_PORT) {
|
||||
port = process.env.MM_PORT;
|
||||
}
|
||||
/**
|
||||
* Server
|
||||
*
|
||||
* @param {object} config The MM config
|
||||
* @param {Function} callback Function called when done.
|
||||
* @class
|
||||
*/
|
||||
function Server(config, callback) {
|
||||
const port = process.env.MM_PORT || config.port;
|
||||
|
||||
var server = null;
|
||||
let server = null;
|
||||
if (config.useHttps) {
|
||||
var options = {
|
||||
const options = {
|
||||
key: fs.readFileSync(config.httpsPrivateKey),
|
||||
cert: fs.readFileSync(config.httpsCertificate)
|
||||
};
|
||||
@@ -30,18 +34,24 @@ var Server = function (config, callback) {
|
||||
} else {
|
||||
server = require("http").Server(app);
|
||||
}
|
||||
var io = require("socket.io")(server);
|
||||
const io = require("socket.io")(server, {
|
||||
cors: {
|
||||
origin: /.*$/,
|
||||
credentials: true
|
||||
},
|
||||
allowEIO3: true
|
||||
});
|
||||
|
||||
Log.log("Starting server on port " + port + " ... ");
|
||||
Log.log(`Starting server on port ${port} ... `);
|
||||
|
||||
server.listen(port, config.address ? config.address : "localhost");
|
||||
server.listen(port, config.address || "localhost");
|
||||
|
||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
}
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
var result = ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) {
|
||||
ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) {
|
||||
if (err === undefined) {
|
||||
return next();
|
||||
}
|
||||
@@ -52,10 +62,9 @@ var Server = function (config, callback) {
|
||||
app.use(helmet({ contentSecurityPolicy: false }));
|
||||
|
||||
app.use("/js", express.static(__dirname));
|
||||
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
||||
var directory;
|
||||
for (var i in directories) {
|
||||
directory = directories[i];
|
||||
|
||||
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
||||
for (const directory of directories) {
|
||||
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
||||
}
|
||||
|
||||
@@ -68,10 +77,10 @@ var Server = function (config, callback) {
|
||||
});
|
||||
|
||||
app.get("/", function (req, res) {
|
||||
var html = fs.readFileSync(path.resolve(global.root_path + "/index.html"), { encoding: "utf8" });
|
||||
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
|
||||
html = html.replace("#VERSION#", global.version);
|
||||
|
||||
var configFile = "config/config.js";
|
||||
let configFile = "config/config.js";
|
||||
if (typeof global.configuration_file !== "undefined") {
|
||||
configFile = global.configuration_file;
|
||||
}
|
||||
@@ -83,6 +92,6 @@ var Server = function (config, callback) {
|
||||
if (typeof callback === "function") {
|
||||
callback(app, io);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Server;
|
||||
|
@@ -14,7 +14,7 @@ var Translator = (function () {
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
function loadJSON(file, callback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.overrideMimeType("application/json");
|
||||
xhr.open("GET", file, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
@@ -103,26 +103,19 @@ var Translator = (function () {
|
||||
* @param {boolean} isFallback Flag to indicate fallback translations.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
load: function (module, file, isFallback, callback) {
|
||||
if (!isFallback) {
|
||||
Log.log(module.name + " - Load translation: " + file);
|
||||
} else {
|
||||
Log.log(module.name + " - Load translation fallback: " + file);
|
||||
load(module, file, isFallback, callback) {
|
||||
Log.log(`${module.name} - Load translation${isFallback && " fallback"}: ${file}`);
|
||||
|
||||
if (this.translationsFallback[module.name]) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
if (!this.translationsFallback[module.name]) {
|
||||
loadJSON(module.file(file), function (json) {
|
||||
if (!isFallback) {
|
||||
self.translations[module.name] = json;
|
||||
} else {
|
||||
self.translationsFallback[module.name] = json;
|
||||
}
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
loadJSON(module.file(file), (json) => {
|
||||
const property = isFallback ? "translationsFallback" : "translations";
|
||||
this[property][module.name] = json;
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -131,18 +124,16 @@ var Translator = (function () {
|
||||
* @param {string} lang The language identifier of the core language.
|
||||
*/
|
||||
loadCoreTranslations: function (lang) {
|
||||
var self = this;
|
||||
|
||||
if (lang in translations) {
|
||||
Log.log("Loading core translation file: " + translations[lang]);
|
||||
loadJSON(translations[lang], function (translations) {
|
||||
self.coreTranslations = translations;
|
||||
loadJSON(translations[lang], (translations) => {
|
||||
this.coreTranslations = translations;
|
||||
});
|
||||
} else {
|
||||
Log.log("Configured language not found in core translations.");
|
||||
}
|
||||
|
||||
self.loadCoreTranslationsFallback();
|
||||
this.loadCoreTranslationsFallback();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -150,8 +141,6 @@ var Translator = (function () {
|
||||
* The first language defined in translations.js will be used.
|
||||
*/
|
||||
loadCoreTranslationsFallback: function () {
|
||||
var self = this;
|
||||
|
||||
// The variable `first` will contain the first
|
||||
// defined translation after the following line.
|
||||
for (var first in translations) {
|
||||
@@ -160,8 +149,8 @@ var Translator = (function () {
|
||||
|
||||
if (first) {
|
||||
Log.log("Loading core translation fallback file: " + translations[first]);
|
||||
loadJSON(translations[first], function (translations) {
|
||||
self.coreTranslationsFallback = translations;
|
||||
loadJSON(translations[first], (translations) => {
|
||||
this.coreTranslationsFallback = translations;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -4,9 +4,9 @@
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var colors = require("colors/safe");
|
||||
const colors = require("colors/safe");
|
||||
|
||||
var Utils = {
|
||||
module.exports = {
|
||||
colors: {
|
||||
warn: colors.yellow,
|
||||
error: colors.red,
|
||||
@@ -14,7 +14,3 @@ var Utils = {
|
||||
pass: colors.green
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = Utils;
|
||||
}
|
||||
|
@@ -36,6 +36,7 @@ Module.register("calendar", {
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
hidePrivate: false,
|
||||
hideOngoing: false,
|
||||
hideTime: 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
|
||||
@@ -57,7 +58,8 @@ Module.register("calendar", {
|
||||
excludedEvents: [],
|
||||
sliceMultiDayEvents: false,
|
||||
broadcastPastEvents: false,
|
||||
nextDaysRelative: false
|
||||
nextDaysRelative: false,
|
||||
selfSignedCert: false
|
||||
},
|
||||
|
||||
requiresVersion: "2.1.0",
|
||||
@@ -75,7 +77,7 @@ Module.register("calendar", {
|
||||
// Define required translations.
|
||||
getTranslations: function () {
|
||||
// The translations for the default modules are defined in the core translation files.
|
||||
// Therefor we can just return false. Otherwise we should have returned a dictionary.
|
||||
// Therefore we can just return false. Otherwise we should have returned a dictionary.
|
||||
// If you're trying to build your own module including translations, check out the documentation.
|
||||
return false;
|
||||
},
|
||||
@@ -93,15 +95,16 @@ Module.register("calendar", {
|
||||
// indicate no data available yet
|
||||
this.loaded = false;
|
||||
|
||||
for (var c in this.config.calendars) {
|
||||
var calendar = this.config.calendars[c];
|
||||
this.config.calendars.forEach((calendar) => {
|
||||
calendar.url = calendar.url.replace("webcal://", "http://");
|
||||
|
||||
var calendarConfig = {
|
||||
const calendarConfig = {
|
||||
maximumEntries: calendar.maximumEntries,
|
||||
maximumNumberOfDays: calendar.maximumNumberOfDays,
|
||||
broadcastPastEvents: calendar.broadcastPastEvents
|
||||
broadcastPastEvents: calendar.broadcastPastEvents,
|
||||
selfSignedCert: calendar.selfSignedCert
|
||||
};
|
||||
|
||||
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
|
||||
calendarConfig.symbolClass = "";
|
||||
}
|
||||
@@ -125,7 +128,7 @@ Module.register("calendar", {
|
||||
// tell helper to start a fetcher for this calendar
|
||||
// fetcher till cycle
|
||||
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Override socket notification handler.
|
||||
@@ -161,8 +164,8 @@ Module.register("calendar", {
|
||||
const oneHour = oneMinute * 60;
|
||||
const oneDay = oneHour * 24;
|
||||
|
||||
var events = this.createEventList();
|
||||
var wrapper = document.createElement("table");
|
||||
const events = this.createEventList();
|
||||
const wrapper = document.createElement("table");
|
||||
wrapper.className = this.config.tableClass;
|
||||
|
||||
if (events.length === 0) {
|
||||
@@ -171,37 +174,37 @@ Module.register("calendar", {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
let currentFadeStep = 0;
|
||||
let startFade;
|
||||
let fadeSteps;
|
||||
|
||||
if (this.config.fade && this.config.fadePoint < 1) {
|
||||
if (this.config.fadePoint < 0) {
|
||||
this.config.fadePoint = 0;
|
||||
}
|
||||
var startFade = events.length * this.config.fadePoint;
|
||||
var fadeSteps = events.length - startFade;
|
||||
startFade = events.length * this.config.fadePoint;
|
||||
fadeSteps = events.length - startFade;
|
||||
}
|
||||
|
||||
var currentFadeStep = 0;
|
||||
var lastSeenDate = "";
|
||||
var ev;
|
||||
var needle;
|
||||
let lastSeenDate = "";
|
||||
|
||||
for (var e in events) {
|
||||
var event = events[e];
|
||||
var dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
|
||||
events.forEach((event, index) => {
|
||||
const dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
|
||||
if (this.config.timeFormat === "dateheaders") {
|
||||
if (lastSeenDate !== dateAsString) {
|
||||
var dateRow = document.createElement("tr");
|
||||
const dateRow = document.createElement("tr");
|
||||
dateRow.className = "normal";
|
||||
var dateCell = document.createElement("td");
|
||||
|
||||
const dateCell = document.createElement("td");
|
||||
dateCell.colSpan = "3";
|
||||
dateCell.innerHTML = dateAsString;
|
||||
dateCell.style.paddingTop = "10px";
|
||||
dateRow.appendChild(dateCell);
|
||||
wrapper.appendChild(dateRow);
|
||||
|
||||
if (e >= startFade) {
|
||||
if (this.config.fade && index >= startFade) {
|
||||
//fading
|
||||
currentFadeStep = e - startFade;
|
||||
currentFadeStep = index - startFade;
|
||||
dateRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
|
||||
}
|
||||
|
||||
@@ -209,7 +212,7 @@ Module.register("calendar", {
|
||||
}
|
||||
}
|
||||
|
||||
var eventWrapper = document.createElement("tr");
|
||||
const eventWrapper = document.createElement("tr");
|
||||
|
||||
if (this.config.colored && !this.config.coloredSymbolOnly) {
|
||||
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||
@@ -217,22 +220,22 @@ Module.register("calendar", {
|
||||
|
||||
eventWrapper.className = "normal event";
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
var symbolWrapper = document.createElement("td");
|
||||
const symbolWrapper = document.createElement("td");
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
if (this.config.colored && this.config.coloredSymbolOnly) {
|
||||
symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||
}
|
||||
|
||||
var symbolClass = this.symbolClassForUrl(event.url);
|
||||
const symbolClass = this.symbolClassForUrl(event.url);
|
||||
symbolWrapper.className = "symbol align-right " + symbolClass;
|
||||
|
||||
var symbols = this.symbolsForEvent(event);
|
||||
const symbols = this.symbolsForEvent(event);
|
||||
// If symbols are displayed and custom symbol is set, replace event symbol
|
||||
if (this.config.displaySymbol && this.config.customEvents.length > 0) {
|
||||
for (ev in this.config.customEvents) {
|
||||
for (let ev in this.config.customEvents) {
|
||||
if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") {
|
||||
needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
symbols[0] = this.config.customEvents[ev].symbol;
|
||||
break;
|
||||
@@ -240,31 +243,29 @@ Module.register("calendar", {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < symbols.length; i++) {
|
||||
var symbol = document.createElement("span");
|
||||
symbol.className = "fa fa-fw fa-" + symbols[i];
|
||||
if (i > 0) {
|
||||
symbols.forEach((s, index) => {
|
||||
const symbol = document.createElement("span");
|
||||
symbol.className = "fa fa-fw fa-" + s;
|
||||
if (index > 0) {
|
||||
symbol.style.paddingLeft = "5px";
|
||||
}
|
||||
symbolWrapper.appendChild(symbol);
|
||||
}
|
||||
|
||||
});
|
||||
eventWrapper.appendChild(symbolWrapper);
|
||||
} else if (this.config.timeFormat === "dateheaders") {
|
||||
var blankCell = document.createElement("td");
|
||||
const blankCell = document.createElement("td");
|
||||
blankCell.innerHTML = " ";
|
||||
eventWrapper.appendChild(blankCell);
|
||||
}
|
||||
|
||||
var titleWrapper = document.createElement("td"),
|
||||
repeatingCountTitle = "";
|
||||
const titleWrapper = document.createElement("td");
|
||||
let repeatingCountTitle = "";
|
||||
|
||||
if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) {
|
||||
repeatingCountTitle = this.countTitleForUrl(event.url);
|
||||
|
||||
if (repeatingCountTitle !== "") {
|
||||
var thisYear = new Date(parseInt(event.startDate)).getFullYear(),
|
||||
const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
|
||||
yearDiff = thisYear - event.firstYear;
|
||||
|
||||
repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
|
||||
@@ -273,12 +274,15 @@ Module.register("calendar", {
|
||||
|
||||
// Color events if custom color is specified
|
||||
if (this.config.customEvents.length > 0) {
|
||||
for (ev in this.config.customEvents) {
|
||||
for (let ev in this.config.customEvents) {
|
||||
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
|
||||
needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
titleWrapper.style.cssText = "color:" + 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.displaySymbol) {
|
||||
symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
}
|
||||
@@ -290,7 +294,7 @@ Module.register("calendar", {
|
||||
|
||||
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
|
||||
|
||||
var titleClass = this.titleClassForUrl(event.url);
|
||||
const titleClass = this.titleClassForUrl(event.url);
|
||||
|
||||
if (!this.config.colored) {
|
||||
titleWrapper.className = "title bright " + titleClass;
|
||||
@@ -298,14 +302,12 @@ Module.register("calendar", {
|
||||
titleWrapper.className = "title " + titleClass;
|
||||
}
|
||||
|
||||
var timeWrapper;
|
||||
|
||||
if (this.config.timeFormat === "dateheaders") {
|
||||
if (event.fullDayEvent) {
|
||||
titleWrapper.colSpan = "2";
|
||||
titleWrapper.align = "left";
|
||||
} else {
|
||||
timeWrapper = document.createElement("td");
|
||||
const timeWrapper = document.createElement("td");
|
||||
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
||||
timeWrapper.align = "left";
|
||||
timeWrapper.style.paddingLeft = "2px";
|
||||
@@ -316,10 +318,10 @@ Module.register("calendar", {
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
} else {
|
||||
timeWrapper = document.createElement("td");
|
||||
const timeWrapper = document.createElement("td");
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
var now = new Date();
|
||||
const now = new Date();
|
||||
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
// Use dateFormat
|
||||
@@ -363,7 +365,17 @@ Module.register("calendar", {
|
||||
// Show relative times
|
||||
if (event.startDate >= now) {
|
||||
// Use relative time
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
||||
if (!this.config.hideTime) {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
moment(event.startDate, "x").calendar(null, {
|
||||
sameDay: "[" + this.translate("TODAY") + "]",
|
||||
nextDay: "[" + this.translate("TOMORROW") + "]",
|
||||
nextWeek: "dddd"
|
||||
})
|
||||
);
|
||||
}
|
||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
@@ -385,22 +397,22 @@ Module.register("calendar", {
|
||||
wrapper.appendChild(eventWrapper);
|
||||
|
||||
// Create fade effect.
|
||||
if (e >= startFade) {
|
||||
currentFadeStep = e - startFade;
|
||||
if (index >= startFade) {
|
||||
currentFadeStep = index - startFade;
|
||||
eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
|
||||
}
|
||||
|
||||
if (this.config.showLocation) {
|
||||
if (event.location !== false) {
|
||||
var locationRow = document.createElement("tr");
|
||||
const locationRow = document.createElement("tr");
|
||||
locationRow.className = "normal xsmall light";
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
var symbolCell = document.createElement("td");
|
||||
const symbolCell = document.createElement("td");
|
||||
locationRow.appendChild(symbolCell);
|
||||
}
|
||||
|
||||
var descCell = document.createElement("td");
|
||||
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);
|
||||
@@ -408,13 +420,13 @@ Module.register("calendar", {
|
||||
|
||||
wrapper.appendChild(locationRow);
|
||||
|
||||
if (e >= startFade) {
|
||||
currentFadeStep = e - startFade;
|
||||
if (index >= startFade) {
|
||||
currentFadeStep = index - startFade;
|
||||
locationRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
@@ -448,8 +460,7 @@ Module.register("calendar", {
|
||||
* @returns {boolean} True if the calendar config contains the url, False otherwise
|
||||
*/
|
||||
hasCalendarURL: function (url) {
|
||||
for (var c in this.config.calendars) {
|
||||
var calendar = this.config.calendars[c];
|
||||
for (const calendar of this.config.calendars) {
|
||||
if (calendar.url === url) {
|
||||
return true;
|
||||
}
|
||||
@@ -464,14 +475,15 @@ Module.register("calendar", {
|
||||
* @returns {object[]} Array with events.
|
||||
*/
|
||||
createEventList: function () {
|
||||
var events = [];
|
||||
var today = moment().startOf("day");
|
||||
var now = new Date();
|
||||
var future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
||||
for (var c in this.calendarData) {
|
||||
var calendar = this.calendarData[c];
|
||||
for (var e in calendar) {
|
||||
var event = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day");
|
||||
const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
||||
let events = [];
|
||||
|
||||
for (const calendarUrl in this.calendarData) {
|
||||
const calendar = this.calendarData[calendarUrl];
|
||||
for (const e in calendar) {
|
||||
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
||||
|
||||
if (event.endDate < now) {
|
||||
continue;
|
||||
@@ -490,19 +502,19 @@ Module.register("calendar", {
|
||||
if (this.listContainsEvent(events, event)) {
|
||||
continue;
|
||||
}
|
||||
event.url = c;
|
||||
event.url = calendarUrl;
|
||||
event.today = event.startDate >= today && event.startDate < today + 24 * 60 * 60 * 1000;
|
||||
|
||||
/* 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.
|
||||
*/
|
||||
var maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / (1000 * 60 * 60 * 24)) + 1;
|
||||
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / (1000 * 60 * 60 * 24)) + 1;
|
||||
if (this.config.sliceMultiDayEvents && maxCount > 1) {
|
||||
var splitEvents = [];
|
||||
var midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
|
||||
var count = 1;
|
||||
const splitEvents = [];
|
||||
let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
|
||||
let count = 1;
|
||||
while (event.endDate > midnight) {
|
||||
var thisEvent = JSON.parse(JSON.stringify(event)); // clone object
|
||||
const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
|
||||
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + 24 * 60 * 60 * 1000;
|
||||
thisEvent.endDate = midnight;
|
||||
thisEvent.title += " (" + count + "/" + maxCount + ")";
|
||||
@@ -516,9 +528,9 @@ Module.register("calendar", {
|
||||
event.title += " (" + count + "/" + maxCount + ")";
|
||||
splitEvents.push(event);
|
||||
|
||||
for (event of splitEvents) {
|
||||
if (event.endDate > now && event.endDate <= future) {
|
||||
events.push(event);
|
||||
for (let splitEvent of splitEvents) {
|
||||
if (splitEvent.endDate > now && splitEvent.endDate <= future) {
|
||||
events.push(splitEvent);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -534,12 +546,11 @@ Module.register("calendar", {
|
||||
// Limit the number of days displayed
|
||||
// If limitDays is set > 0, limit display to that number of days
|
||||
if (this.config.limitDays > 0) {
|
||||
var newEvents = [];
|
||||
var lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
|
||||
var days = 0;
|
||||
var eventDate;
|
||||
for (var ev of events) {
|
||||
eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
|
||||
let newEvents = [];
|
||||
let lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
|
||||
let days = 0;
|
||||
for (const ev of events) {
|
||||
let eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
|
||||
// if date of event is later than lastdate
|
||||
// check if we already are showing max unique days
|
||||
if (eventDate > lastDate) {
|
||||
@@ -563,7 +574,7 @@ Module.register("calendar", {
|
||||
},
|
||||
|
||||
listContainsEvent: function (eventList, event) {
|
||||
for (var evt of eventList) {
|
||||
for (const evt of eventList) {
|
||||
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) {
|
||||
return true;
|
||||
}
|
||||
@@ -579,8 +590,6 @@ Module.register("calendar", {
|
||||
* @param {object} calendarConfig The config of the specific calendar
|
||||
*/
|
||||
addCalendar: function (url, auth, calendarConfig) {
|
||||
var self = this;
|
||||
|
||||
this.sendSocketNotification("ADD_CALENDAR", {
|
||||
id: this.identifier,
|
||||
url: url,
|
||||
@@ -592,7 +601,8 @@ Module.register("calendar", {
|
||||
titleClass: calendarConfig.titleClass,
|
||||
timeClass: calendarConfig.timeClass,
|
||||
auth: auth,
|
||||
broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents
|
||||
broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents,
|
||||
selfSignedCert: calendarConfig.selfSignedCert || this.config.selfSignedCert
|
||||
});
|
||||
},
|
||||
|
||||
@@ -693,8 +703,7 @@ Module.register("calendar", {
|
||||
* @returns {*} The property
|
||||
*/
|
||||
getCalendarProperty: function (url, property, defaultValue) {
|
||||
for (var c in this.config.calendars) {
|
||||
var calendar = this.config.calendars[c];
|
||||
for (const calendar of this.config.calendars) {
|
||||
if (calendar.url === url && calendar.hasOwnProperty(property)) {
|
||||
return calendar[property];
|
||||
}
|
||||
@@ -728,13 +737,13 @@ Module.register("calendar", {
|
||||
}
|
||||
|
||||
if (wrapEvents === true) {
|
||||
var temp = "";
|
||||
var currentLine = "";
|
||||
var words = string.split(" ");
|
||||
var line = 0;
|
||||
const words = string.split(" ");
|
||||
let temp = "";
|
||||
let currentLine = "";
|
||||
let line = 0;
|
||||
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
var word = words[i];
|
||||
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 + " ";
|
||||
@@ -789,10 +798,10 @@ Module.register("calendar", {
|
||||
* @returns {string} The transformed title.
|
||||
*/
|
||||
titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
|
||||
for (var needle in titleReplace) {
|
||||
var replacement = titleReplace[needle];
|
||||
for (let needle in titleReplace) {
|
||||
const replacement = titleReplace[needle];
|
||||
|
||||
var regParts = needle.match(/^\/(.+)\/([gim]*)$/);
|
||||
const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
|
||||
if (regParts) {
|
||||
// the parsed pattern is a regexp.
|
||||
needle = new RegExp(regParts[1], regParts[2]);
|
||||
@@ -810,11 +819,10 @@ Module.register("calendar", {
|
||||
* The all events available in one array, sorted on startdate.
|
||||
*/
|
||||
broadcastEvents: function () {
|
||||
var eventList = [];
|
||||
for (var url in this.calendarData) {
|
||||
var calendar = this.calendarData[url];
|
||||
for (var e in calendar) {
|
||||
var event = cloneObject(calendar[e]);
|
||||
const eventList = [];
|
||||
for (const url in this.calendarData) {
|
||||
for (const ev of this.calendarData[url]) {
|
||||
const event = cloneObject(ev);
|
||||
event.symbol = this.symbolsForEvent(event);
|
||||
event.calendarName = this.calendarNameForUrl(url);
|
||||
event.color = this.colorForUrl(url);
|
||||
|
@@ -4,17 +4,12 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Log = require("../../../js/logger.js");
|
||||
const CalendarUtils = require("./calendarutils");
|
||||
const Log = require("logger");
|
||||
const ical = require("node-ical");
|
||||
const request = require("request");
|
||||
|
||||
/**
|
||||
* Moment date
|
||||
*
|
||||
* @external Moment
|
||||
* @see {@link http://momentjs.com}
|
||||
*/
|
||||
const moment = require("moment");
|
||||
const fetch = require("node-fetch");
|
||||
const digest = require("digest-fetch");
|
||||
const https = require("https");
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -25,11 +20,10 @@ const moment = require("moment");
|
||||
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
|
||||
* @param {object} auth The object containing options for authentication against the calendar.
|
||||
* @param {boolean} includePastEvents If true events from the past maximumNumberOfDays will be fetched too
|
||||
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
|
||||
* @class
|
||||
*/
|
||||
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) {
|
||||
const self = this;
|
||||
|
||||
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents, selfSignedCert) {
|
||||
let reloadTimer = null;
|
||||
let events = [];
|
||||
|
||||
@@ -39,492 +33,67 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
/**
|
||||
* Initiates calendar fetch.
|
||||
*/
|
||||
const fetchCalendar = function () {
|
||||
const fetchCalendar = () => {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
const opts = {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
|
||||
},
|
||||
gzip: true
|
||||
let fetcher = null;
|
||||
let httpsAgent = null;
|
||||
let headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
|
||||
};
|
||||
|
||||
if (selfSignedCert) {
|
||||
httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
}
|
||||
if (auth) {
|
||||
if (auth.method === "bearer") {
|
||||
opts.auth = {
|
||||
bearer: auth.pass
|
||||
};
|
||||
headers.Authorization = "Bearer " + auth.pass;
|
||||
} else if (auth.method === "digest") {
|
||||
fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, httpsAgent: httpsAgent });
|
||||
} else {
|
||||
opts.auth = {
|
||||
user: auth.user,
|
||||
pass: auth.pass,
|
||||
sendImmediately: auth.method !== "digest"
|
||||
};
|
||||
headers.Authorization = "Basic " + Buffer.from(auth.user + ":" + auth.pass).toString("base64");
|
||||
}
|
||||
}
|
||||
if (fetcher === null) {
|
||||
fetcher = fetch(url, { headers: headers, httpsAgent: httpsAgent });
|
||||
}
|
||||
|
||||
request(url, opts, function (err, r, requestData) {
|
||||
if (err) {
|
||||
fetchFailedCallback(self, err);
|
||||
fetcher
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
return;
|
||||
} else if (r.statusCode !== 200) {
|
||||
fetchFailedCallback(self, r.statusCode + ": " + r.statusMessage);
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
fetchFailedCallback(this, response.statusText);
|
||||
scheduleTimer();
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((responseData) => {
|
||||
let data = [];
|
||||
|
||||
try {
|
||||
data = ical.parseICS(responseData);
|
||||
Log.debug("parsed data=" + JSON.stringify(data));
|
||||
events = CalendarUtils.filterEvents(data, {
|
||||
excludedEvents,
|
||||
includePastEvents,
|
||||
maximumEntries,
|
||||
maximumNumberOfDays
|
||||
});
|
||||
} catch (error) {
|
||||
fetchFailedCallback(this, error.message);
|
||||
scheduleTimer();
|
||||
return;
|
||||
}
|
||||
this.broadcastEvents();
|
||||
scheduleTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
let data = [];
|
||||
|
||||
try {
|
||||
data = ical.parseICS(requestData);
|
||||
} catch (error) {
|
||||
fetchFailedCallback(self, error.message);
|
||||
scheduleTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.debug(" parsed data=" + JSON.stringify(data));
|
||||
|
||||
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 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]) => {
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day").toDate();
|
||||
const future = moment().startOf("day").add(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;
|
||||
Log.debug("have entries ");
|
||||
if (includePastEvents) {
|
||||
past = moment().startOf("day").subtract(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") {
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
|
||||
Log.debug("\nevent=" + JSON.stringify(event));
|
||||
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() + " end=" + endDate.toDate());
|
||||
|
||||
// calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
const title = getTitleFromEvent(event);
|
||||
|
||||
let excluded = false,
|
||||
dateFilter = null;
|
||||
|
||||
for (let f in excludedEvents) {
|
||||
let filter = 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 (testTitleByFilter(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 (isFullDayEvent(event)) {
|
||||
// if full day event, only use the date part of the ranges
|
||||
pastLocal = pastMoment.toDate();
|
||||
futureLocal = futureMoment.toDate();
|
||||
} else {
|
||||
// if we want past events
|
||||
if (includePastEvents) {
|
||||
// use the calculated past time for the between from
|
||||
pastLocal = pastMoment.toDate();
|
||||
} else {
|
||||
// otherwise use NOW.. cause we shouldnt use any before now
|
||||
pastLocal = moment().toDate(); //now
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
}
|
||||
Log.debug(" between=" + pastLocal + " to " + futureLocal);
|
||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
Log.debug("title=" + event.summary + " 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.
|
||||
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];
|
||||
// ical.js started returning recurrences and exdates as ISOStrings without time information.
|
||||
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
|
||||
// (see https://github.com/peterbraden/ical.js/pull/84 )
|
||||
const dateKey = date.toISOString().substring(0, 10);
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
|
||||
// for full day events, the time might be off from RRULE/Luxon problem
|
||||
if (isFullDayEvent(event)) {
|
||||
Log.debug("fullday");
|
||||
// if the offset is negative, east of GMT where the problem is
|
||||
if (date.getTimezoneOffset() < 0) {
|
||||
// get the offset of today where we are processing
|
||||
// this will be the correction we need to apply
|
||||
let nowOffset = new Date().getTimezoneOffset();
|
||||
Log.debug("now offset is " + nowOffset);
|
||||
// reduce the time by the offset
|
||||
Log.debug(" recurring date is " + date + " offset is " + date.getTimezoneOffset());
|
||||
// apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(nowOffset) * 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 date is " + date);
|
||||
}
|
||||
}
|
||||
startDate = moment(date);
|
||||
|
||||
let adjustDays = getCorrection(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 = 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 (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: 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 : isFullDayEvent(event);
|
||||
// Log.debug("full day event")
|
||||
|
||||
if (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 (timeFilterApplies(now, endDate, dateFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
|
||||
if (fullDayEvent && startDate <= today) {
|
||||
startDate = moment(today);
|
||||
}
|
||||
// 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 = getCorrection(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;
|
||||
});
|
||||
|
||||
// include up to maximumEntries current or upcoming events
|
||||
// If past events should be included, include all past events
|
||||
const now = moment();
|
||||
var entries = 0;
|
||||
events = [];
|
||||
for (let ne of newEvents) {
|
||||
if (moment(ne.endDate, "x").isBefore(now)) {
|
||||
if (includePastEvents) events.push(ne);
|
||||
continue;
|
||||
}
|
||||
entries++;
|
||||
// If max events has been saved, skip the rest
|
||||
if (entries > maximumEntries) break;
|
||||
events.push(ne);
|
||||
}
|
||||
|
||||
self.broadcastEvents();
|
||||
scheduleTimer();
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
*
|
||||
* get the time correction, either dst/std or full day in cases where utc time is day before plus offset
|
||||
*
|
||||
*/
|
||||
const getCorrection = 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 = getIanaTZFromMS(event.start.tz);
|
||||
Log.debug("corrected TZ=" + tz);
|
||||
// watch out for unregistered windows timezone names
|
||||
// if we had a successfule 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
|
||||
Log.debug("start date/time=" + moment(event.start).toDate());
|
||||
start_offset = moment.tz(moment(event.start), 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;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* lookup iana tz from windows
|
||||
*/
|
||||
let zoneTable = null;
|
||||
const getIanaTZFromMS = function (msTZName) {
|
||||
if (!zoneTable) {
|
||||
const p = require("path");
|
||||
zoneTable = require(p.join(__dirname, "windowsZones.json"));
|
||||
}
|
||||
// Get hash entry
|
||||
const he = zoneTable[msTZName];
|
||||
// If found return iana name, else null
|
||||
return he ? he.iana[0] : null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -537,82 +106,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
}, reloadInterval);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
const 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
|
||||
*/
|
||||
const 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const 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;
|
||||
};
|
||||
|
||||
const testTitleByFilter = 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);
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
}
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
|
||||
/**
|
||||
@@ -627,7 +120,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
*/
|
||||
this.broadcastEvents = function () {
|
||||
Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events.");
|
||||
eventsReceivedCallback(self);
|
||||
eventsReceivedCallback(this);
|
||||
};
|
||||
|
||||
/**
|
||||
|
600
modules/default/calendar/calendarutils.js
Normal file
600
modules/default/calendar/calendarutils.js
Normal file
@@ -0,0 +1,600 @@
|
||||
/* Magic Mirror
|
||||
* Calendar Util Methods
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* 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
|
||||
* @param {Date} date
|
||||
* @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 = 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;
|
||||
},
|
||||
|
||||
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]) => {
|
||||
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;
|
||||
Log.debug("have entries ");
|
||||
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") {
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
|
||||
Log.debug("\nevent=" + JSON.stringify(event));
|
||||
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() + " end=" + endDate.toDate());
|
||||
|
||||
// calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
const title = CalendarUtils.getTitleFromEvent(event);
|
||||
|
||||
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;
|
||||
}
|
||||
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)) {
|
||||
// if full day event, only use the date part of the ranges
|
||||
pastLocal = pastMoment.toDate();
|
||||
futureLocal = futureMoment.toDate();
|
||||
} 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(" between=" + pastLocal + " to " + futureLocal);
|
||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
Log.debug("title=" + event.summary + " 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.
|
||||
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];
|
||||
// ical.js started returning recurrences and exdates as ISOStrings without time information.
|
||||
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
|
||||
// (see https://github.com/peterbraden/ical.js/pull/84 )
|
||||
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 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 the date hour is less than the offset
|
||||
if (dh < Math.abs(dateoffset / 60)) {
|
||||
// reduce the time by the offset
|
||||
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
|
||||
// apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(nowOffset) * 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)) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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)) {
|
||||
// reduce the time by the offset
|
||||
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
|
||||
// apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(nowOffset) * 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)) {
|
||||
// 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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
|
||||
if (fullDayEvent && startDate <= today) {
|
||||
startDate = moment(today);
|
||||
}
|
||||
// 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
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newEvents.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
// include up to maximumEntries current or upcoming events
|
||||
// If past events should be included, include all past events
|
||||
const now = moment();
|
||||
let entries = 0;
|
||||
let events = [];
|
||||
for (let ne of newEvents) {
|
||||
if (moment(ne.endDate, "x").isBefore(now)) {
|
||||
if (config.includePastEvents) events.push(ne);
|
||||
continue;
|
||||
}
|
||||
entries++;
|
||||
// If max events has been saved, skip the rest
|
||||
if (entries > config.maximumEntries) break;
|
||||
events.push(ne);
|
||||
}
|
||||
|
||||
return events;
|
||||
},
|
||||
|
||||
/**
|
||||
* Lookup iana tz from windows
|
||||
*
|
||||
* @param msTZName
|
||||
* @returns {*|null}
|
||||
*/
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param title
|
||||
* @param filter
|
||||
* @param useRegex
|
||||
* @param regexFlags
|
||||
* @returns {boolean|*}
|
||||
*/
|
||||
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);
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = CalendarUtils;
|
||||
}
|
@@ -5,9 +5,8 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const NodeHelper = require("node_helper");
|
||||
const validUrl = require("valid-url");
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
const Log = require("../../../js/logger");
|
||||
const Log = require("logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
@@ -19,7 +18,7 @@ module.exports = NodeHelper.create({
|
||||
// Override socketNotificationReceived method.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "ADD_CALENDAR") {
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.id);
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.selfSignedCert, payload.id);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -34,44 +33,55 @@ module.exports = NodeHelper.create({
|
||||
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
|
||||
* @param {object} auth The object containing options for authentication against the calendar.
|
||||
* @param {boolean} broadcastPastEvents If true events from the past maximumNumberOfDays will be included in event broadcasts
|
||||
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
|
||||
* @param {string} identifier ID of the module
|
||||
*/
|
||||
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, identifier) {
|
||||
var self = this;
|
||||
|
||||
if (!validUrl.isUri(url)) {
|
||||
self.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url });
|
||||
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
this.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url });
|
||||
return;
|
||||
}
|
||||
|
||||
var fetcher;
|
||||
if (typeof self.fetchers[identifier + url] === "undefined") {
|
||||
let fetcher;
|
||||
if (typeof this.fetchers[identifier + url] === "undefined") {
|
||||
Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
|
||||
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
self.sendSocketNotification("CALENDAR_EVENTS", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
events: fetcher.events()
|
||||
});
|
||||
fetcher.onReceive((fetcher) => {
|
||||
this.broadcastEvents(fetcher, identifier);
|
||||
});
|
||||
|
||||
fetcher.onError(function (fetcher, error) {
|
||||
fetcher.onError((fetcher, error) => {
|
||||
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
|
||||
self.sendSocketNotification("FETCH_ERROR", {
|
||||
this.sendSocketNotification("FETCH_ERROR", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
error: error
|
||||
});
|
||||
});
|
||||
|
||||
self.fetchers[identifier + url] = fetcher;
|
||||
this.fetchers[identifier + url] = fetcher;
|
||||
} else {
|
||||
Log.log("Use existing calendar fetcher for url: " + url);
|
||||
fetcher = self.fetchers[identifier + url];
|
||||
fetcher = this.fetchers[identifier + url];
|
||||
fetcher.broadcastEvents();
|
||||
}
|
||||
|
||||
fetcher.startFetch();
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} fetcher the fetcher associated with the calendar
|
||||
* @param {string} identifier the identifier of the calendar
|
||||
*/
|
||||
broadcastEvents: function (fetcher, identifier) {
|
||||
this.sendSocketNotification("CALENDAR_EVENTS", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
events: fetcher.events()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -182,34 +182,14 @@ Module.register("compliments", {
|
||||
},
|
||||
|
||||
// From data currentweather set weather type
|
||||
setCurrentWeatherType: function (data) {
|
||||
var weatherIconTable = {
|
||||
"01d": "day_sunny",
|
||||
"02d": "day_cloudy",
|
||||
"03d": "cloudy",
|
||||
"04d": "cloudy_windy",
|
||||
"09d": "showers",
|
||||
"10d": "rain",
|
||||
"11d": "thunderstorm",
|
||||
"13d": "snow",
|
||||
"50d": "fog",
|
||||
"01n": "night_clear",
|
||||
"02n": "night_cloudy",
|
||||
"03n": "night_cloudy",
|
||||
"04n": "night_cloudy",
|
||||
"09n": "night_showers",
|
||||
"10n": "night_rain",
|
||||
"11n": "night_thunderstorm",
|
||||
"13n": "night_snow",
|
||||
"50n": "night_alt_cloudy_windy"
|
||||
};
|
||||
this.currentWeatherType = weatherIconTable[data.weather[0].icon];
|
||||
setCurrentWeatherType: function (type) {
|
||||
this.currentWeatherType = type;
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "CURRENTWEATHER_DATA") {
|
||||
this.setCurrentWeatherType(payload.data);
|
||||
if (notification === "CURRENTWEATHER_TYPE") {
|
||||
this.setCurrentWeatherType(payload.type);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -1,5 +1,7 @@
|
||||
# Module: Current Weather
|
||||
|
||||
> :warning: **This module is deprecated in favor of the [weather](https://docs.magicmirror.builders/modules/weather.html) module.**
|
||||
|
||||
The `currentweather` module is one of the default modules of the MagicMirror.
|
||||
This module displays the current weather, including the windspeed, the sunset or sunrise time, the temperature and an icon to display the current conditions.
|
||||
|
||||
|
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This module is deprecated. Any additional feature will no longer be merged.
|
||||
*/
|
||||
Module.register("currentweather", {
|
||||
// Default module config.
|
||||
@@ -47,24 +49,24 @@ Module.register("currentweather", {
|
||||
roundTemp: false,
|
||||
|
||||
iconTable: {
|
||||
"01d": "wi-day-sunny",
|
||||
"02d": "wi-day-cloudy",
|
||||
"03d": "wi-cloudy",
|
||||
"04d": "wi-cloudy-windy",
|
||||
"09d": "wi-showers",
|
||||
"10d": "wi-rain",
|
||||
"11d": "wi-thunderstorm",
|
||||
"13d": "wi-snow",
|
||||
"50d": "wi-fog",
|
||||
"01n": "wi-night-clear",
|
||||
"02n": "wi-night-cloudy",
|
||||
"03n": "wi-night-cloudy",
|
||||
"04n": "wi-night-cloudy",
|
||||
"09n": "wi-night-showers",
|
||||
"10n": "wi-night-rain",
|
||||
"11n": "wi-night-thunderstorm",
|
||||
"13n": "wi-night-snow",
|
||||
"50n": "wi-night-alt-cloudy-windy"
|
||||
"01d": "day-sunny",
|
||||
"02d": "day-cloudy",
|
||||
"03d": "cloudy",
|
||||
"04d": "cloudy-windy",
|
||||
"09d": "showers",
|
||||
"10d": "rain",
|
||||
"11d": "thunderstorm",
|
||||
"13d": "snow",
|
||||
"50d": "fog",
|
||||
"01n": "night-clear",
|
||||
"02n": "night-cloudy",
|
||||
"03n": "night-cloudy",
|
||||
"04n": "night-cloudy",
|
||||
"09n": "night-showers",
|
||||
"10n": "night-rain",
|
||||
"11n": "night-thunderstorm",
|
||||
"13n": "night-snow",
|
||||
"50n": "night-alt-cloudy-windy"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -219,7 +221,7 @@ Module.register("currentweather", {
|
||||
|
||||
if (this.config.hideTemp === false) {
|
||||
var weatherIcon = document.createElement("span");
|
||||
weatherIcon.className = "wi weathericon " + this.weatherType;
|
||||
weatherIcon.className = "wi weathericon wi-" + this.weatherType;
|
||||
large.appendChild(weatherIcon);
|
||||
|
||||
var temperature = document.createElement("span");
|
||||
@@ -258,13 +260,9 @@ Module.register("currentweather", {
|
||||
|
||||
var feelsLike = document.createElement("span");
|
||||
feelsLike.className = "dimmed";
|
||||
var feelsLikeHtml = this.translate("FEELS");
|
||||
if (feelsLikeHtml.indexOf("{DEGREE}") > -1) {
|
||||
feelsLikeHtml = this.translate("FEELS", {
|
||||
DEGREE: this.feelsLike + degreeLabel
|
||||
});
|
||||
} else feelsLikeHtml += " " + this.feelsLike + degreeLabel;
|
||||
feelsLike.innerHTML = feelsLikeHtml;
|
||||
feelsLike.innerHTML = this.translate("FEELS", {
|
||||
DEGREE: this.feelsLike + degreeLabel
|
||||
});
|
||||
small.appendChild(feelsLike);
|
||||
|
||||
wrapper.appendChild(small);
|
||||
@@ -506,6 +504,7 @@ Module.register("currentweather", {
|
||||
this.loaded = true;
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
this.sendNotification("CURRENTWEATHER_DATA", { data: data });
|
||||
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.config.iconTable[data.weather[0].icon].replace("-", "_") });
|
||||
},
|
||||
|
||||
/* scheduleUpdate()
|
||||
@@ -593,6 +592,7 @@ Module.register("currentweather", {
|
||||
*/
|
||||
roundValue: function (temperature) {
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
return parseFloat(temperature).toFixed(decimals);
|
||||
var roundValue = parseFloat(temperature).toFixed(decimals);
|
||||
return roundValue === "-0" ? 0 : roundValue;
|
||||
}
|
||||
});
|
||||
|
9
modules/default/currentweather/node_helper.js
Normal file
9
modules/default/currentweather/node_helper.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const NodeHelper = require("node_helper");
|
||||
const Log = require("logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.warn(`The module '${this.name}' is deprecated in favor of the 'weather'-module, please refer to the documentation for a migration path`);
|
||||
}
|
||||
});
|
3
modules/default/newsfeed/fullarticle.njk
Normal file
3
modules/default/newsfeed/fullarticle.njk
Normal file
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
|
||||
</div>
|
14
modules/default/newsfeed/newsfeed.css
Normal file
14
modules/default/newsfeed/newsfeed.css
Normal file
@@ -0,0 +1,14 @@
|
||||
iframe.newsfeed-fullarticle {
|
||||
width: 100vw;
|
||||
/* very large height value to allow scrolling */
|
||||
height: 3000px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.region.bottom.bar.newsfeed-fullarticle {
|
||||
bottom: inherit;
|
||||
top: -90px;
|
||||
}
|
@@ -44,6 +44,11 @@ Module.register("newsfeed", {
|
||||
return ["moment.js"];
|
||||
},
|
||||
|
||||
//Define required styles.
|
||||
getStyles: function () {
|
||||
return ["newsfeed.css"];
|
||||
},
|
||||
|
||||
// Define required translations.
|
||||
getTranslations: function () {
|
||||
// The translations for the default modules are defined in the core translation files.
|
||||
@@ -61,6 +66,7 @@ Module.register("newsfeed", {
|
||||
|
||||
this.newsItems = [];
|
||||
this.loaded = false;
|
||||
this.error = null;
|
||||
this.activeItem = 0;
|
||||
this.scrollPosition = 0;
|
||||
|
||||
@@ -75,130 +81,62 @@ Module.register("newsfeed", {
|
||||
this.generateFeed(payload);
|
||||
|
||||
if (!this.loaded) {
|
||||
if (this.config.hideLoading) {
|
||||
this.show();
|
||||
}
|
||||
this.scheduleUpdateInterval();
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
this.error = null;
|
||||
} else if (notification === "INCORRECT_URL") {
|
||||
this.error = `Incorrect url: ${payload.url}`;
|
||||
this.scheduleUpdateInterval();
|
||||
}
|
||||
},
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
const wrapper = document.createElement("div");
|
||||
|
||||
//Override fetching of template name
|
||||
getTemplate: function () {
|
||||
if (this.config.feedUrl) {
|
||||
wrapper.className = "small bright";
|
||||
wrapper.innerHTML = this.translate("MODULE_CONFIG_CHANGED", { MODULE_NAME: "Newsfeed" });
|
||||
return wrapper;
|
||||
return "oldconfig.njk";
|
||||
} else if (this.config.showFullArticle) {
|
||||
return "fullarticle.njk";
|
||||
}
|
||||
return "newsfeed.njk";
|
||||
},
|
||||
|
||||
//Override template data and return whats used for the current template
|
||||
getTemplateData: function () {
|
||||
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
|
||||
if (this.config.showFullArticle) {
|
||||
return {
|
||||
url: this.getActiveItemURL()
|
||||
};
|
||||
}
|
||||
if (this.error) {
|
||||
return {
|
||||
error: this.error
|
||||
};
|
||||
}
|
||||
if (this.newsItems.length === 0) {
|
||||
return {
|
||||
loaded: false
|
||||
};
|
||||
}
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
this.activeItem = 0;
|
||||
}
|
||||
|
||||
if (this.newsItems.length > 0) {
|
||||
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
|
||||
if (!this.config.showFullArticle && (this.config.showSourceTitle || this.config.showPublishDate)) {
|
||||
const sourceAndTimestamp = document.createElement("div");
|
||||
sourceAndTimestamp.className = "newsfeed-source light small dimmed";
|
||||
const item = this.newsItems[this.activeItem];
|
||||
|
||||
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") {
|
||||
sourceAndTimestamp.innerHTML = this.newsItems[this.activeItem].sourceTitle;
|
||||
}
|
||||
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "" && this.config.showPublishDate) {
|
||||
sourceAndTimestamp.innerHTML += ", ";
|
||||
}
|
||||
if (this.config.showPublishDate) {
|
||||
sourceAndTimestamp.innerHTML += moment(new Date(this.newsItems[this.activeItem].pubdate)).fromNow();
|
||||
}
|
||||
if ((this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") || this.config.showPublishDate) {
|
||||
sourceAndTimestamp.innerHTML += ":";
|
||||
}
|
||||
|
||||
wrapper.appendChild(sourceAndTimestamp);
|
||||
}
|
||||
|
||||
//Remove selected tags from the beginning of rss feed items (title or description)
|
||||
|
||||
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
|
||||
for (let f = 0; f < this.config.startTags.length; f++) {
|
||||
if (this.newsItems[this.activeItem].title.slice(0, this.config.startTags[f].length) === this.config.startTags[f]) {
|
||||
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(this.config.startTags[f].length, this.newsItems[this.activeItem].title.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") {
|
||||
if (this.isShowingDescription) {
|
||||
for (let f = 0; f < this.config.startTags.length; f++) {
|
||||
if (this.newsItems[this.activeItem].description.slice(0, this.config.startTags[f].length) === this.config.startTags[f]) {
|
||||
this.newsItems[this.activeItem].description = this.newsItems[this.activeItem].description.slice(this.config.startTags[f].length, this.newsItems[this.activeItem].description.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Remove selected tags from the end of rss feed items (title or description)
|
||||
|
||||
if (this.config.removeEndTags) {
|
||||
for (let f = 0; f < this.config.endTags.length; f++) {
|
||||
if (this.newsItems[this.activeItem].title.slice(-this.config.endTags[f].length) === this.config.endTags[f]) {
|
||||
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(0, -this.config.endTags[f].length);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isShowingDescription) {
|
||||
for (let f = 0; f < this.config.endTags.length; f++) {
|
||||
if (this.newsItems[this.activeItem].description.slice(-this.config.endTags[f].length) === this.config.endTags[f]) {
|
||||
this.newsItems[this.activeItem].description = this.newsItems[this.activeItem].description.slice(0, -this.config.endTags[f].length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.config.showFullArticle) {
|
||||
const title = document.createElement("div");
|
||||
title.className = "newsfeed-title bright medium light" + (!this.config.wrapTitle ? " no-wrap" : "");
|
||||
title.innerHTML = this.newsItems[this.activeItem].title;
|
||||
wrapper.appendChild(title);
|
||||
}
|
||||
|
||||
if (this.isShowingDescription) {
|
||||
const description = document.createElement("div");
|
||||
description.className = "newsfeed-desc small light" + (!this.config.wrapDescription ? " no-wrap" : "");
|
||||
const txtDesc = this.newsItems[this.activeItem].description;
|
||||
description.innerHTML = this.config.truncDescription ? (txtDesc.length > this.config.lengthDescription ? txtDesc.substring(0, this.config.lengthDescription) + "..." : txtDesc) : txtDesc;
|
||||
wrapper.appendChild(description);
|
||||
}
|
||||
|
||||
if (this.config.showFullArticle) {
|
||||
const fullArticle = document.createElement("iframe");
|
||||
fullArticle.className = "";
|
||||
fullArticle.style.width = "100vw";
|
||||
// very large height value to allow scrolling
|
||||
fullArticle.height = "3000";
|
||||
fullArticle.style.height = "3000";
|
||||
fullArticle.style.top = "0";
|
||||
fullArticle.style.left = "0";
|
||||
fullArticle.style.border = "none";
|
||||
fullArticle.src = this.getActiveItemURL();
|
||||
fullArticle.style.zIndex = 1;
|
||||
wrapper.appendChild(fullArticle);
|
||||
}
|
||||
|
||||
if (this.config.hideLoading) {
|
||||
this.show();
|
||||
}
|
||||
} else {
|
||||
if (this.config.hideLoading) {
|
||||
this.hide();
|
||||
} else {
|
||||
wrapper.innerHTML = this.translate("LOADING");
|
||||
wrapper.className = "small dimmed";
|
||||
}
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
return {
|
||||
loaded: true,
|
||||
config: this.config,
|
||||
sourceTitle: item.sourceTitle,
|
||||
publishDate: moment(new Date(item.pubdate)).fromNow(),
|
||||
title: item.title,
|
||||
description: item.description
|
||||
};
|
||||
},
|
||||
|
||||
getActiveItemURL: function () {
|
||||
@@ -209,8 +147,7 @@ Module.register("newsfeed", {
|
||||
* Registers the feeds to be used by the backend.
|
||||
*/
|
||||
registerFeeds: function () {
|
||||
for (var f in this.config.feeds) {
|
||||
var feed = this.config.feeds[f];
|
||||
for (let feed of this.config.feeds) {
|
||||
this.sendSocketNotification("ADD_FEED", {
|
||||
feed: feed,
|
||||
config: this.config
|
||||
@@ -224,12 +161,11 @@ Module.register("newsfeed", {
|
||||
* @param {object} feeds An object with feeds returned by the node helper.
|
||||
*/
|
||||
generateFeed: function (feeds) {
|
||||
var newsItems = [];
|
||||
for (var feed in feeds) {
|
||||
var feedItems = feeds[feed];
|
||||
let newsItems = [];
|
||||
for (let feed in feeds) {
|
||||
const feedItems = feeds[feed];
|
||||
if (this.subscribedToFeed(feed)) {
|
||||
for (var i in feedItems) {
|
||||
var item = feedItems[i];
|
||||
for (let item of feedItems) {
|
||||
item.sourceTitle = this.titleForFeed(feed);
|
||||
if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) {
|
||||
newsItems.push(item);
|
||||
@@ -238,8 +174,8 @@ Module.register("newsfeed", {
|
||||
}
|
||||
}
|
||||
newsItems.sort(function (a, b) {
|
||||
var dateA = new Date(a.pubdate);
|
||||
var dateB = new Date(b.pubdate);
|
||||
const dateA = new Date(a.pubdate);
|
||||
const dateB = new Date(b.pubdate);
|
||||
return dateB - dateA;
|
||||
});
|
||||
if (this.config.maxNewsItems > 0) {
|
||||
@@ -248,8 +184,8 @@ Module.register("newsfeed", {
|
||||
|
||||
if (this.config.prohibitedWords.length > 0) {
|
||||
newsItems = newsItems.filter(function (value) {
|
||||
for (var i = 0; i < this.config.prohibitedWords.length; i++) {
|
||||
if (value["title"].toLowerCase().indexOf(this.config.prohibitedWords[i].toLowerCase()) > -1) {
|
||||
for (let word of this.config.prohibitedWords) {
|
||||
if (value["title"].toLowerCase().indexOf(word.toLowerCase()) > -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -257,8 +193,47 @@ Module.register("newsfeed", {
|
||||
}, this);
|
||||
}
|
||||
|
||||
newsItems.forEach((item) => {
|
||||
//Remove selected tags from the beginning of rss feed items (title or description)
|
||||
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
|
||||
for (let startTag of this.config.startTags) {
|
||||
if (item.title.slice(0, startTag.length) === startTag) {
|
||||
item.title = item.title.slice(startTag.length, item.title.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") {
|
||||
if (this.isShowingDescription) {
|
||||
for (let startTag of this.config.startTags) {
|
||||
if (item.description.slice(0, startTag.length) === startTag) {
|
||||
item.description = item.description.slice(startTag.length, item.description.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Remove selected tags from the end of rss feed items (title or description)
|
||||
|
||||
if (this.config.removeEndTags) {
|
||||
for (let endTag of this.config.endTags) {
|
||||
if (item.title.slice(-endTag.length) === endTag) {
|
||||
item.title = item.title.slice(0, -endTag.length);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isShowingDescription) {
|
||||
for (let endTag of this.config.endTags) {
|
||||
if (item.description.slice(-endTag.length) === endTag) {
|
||||
item.description = item.description.slice(0, -endTag.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// get updated news items and broadcast them
|
||||
var updatedItems = [];
|
||||
const updatedItems = [];
|
||||
newsItems.forEach((value) => {
|
||||
if (this.newsItems.findIndex((value1) => value1 === value) === -1) {
|
||||
// Add item to updated items list
|
||||
@@ -281,8 +256,7 @@ Module.register("newsfeed", {
|
||||
* @returns {boolean} True if it is subscribed, false otherwise
|
||||
*/
|
||||
subscribedToFeed: function (feedUrl) {
|
||||
for (var f in this.config.feeds) {
|
||||
var feed = this.config.feeds[f];
|
||||
for (let feed of this.config.feeds) {
|
||||
if (feed.url === feedUrl) {
|
||||
return true;
|
||||
}
|
||||
@@ -297,8 +271,7 @@ Module.register("newsfeed", {
|
||||
* @returns {string} The title of the feed
|
||||
*/
|
||||
titleForFeed: function (feedUrl) {
|
||||
for (var f in this.config.feeds) {
|
||||
var feed = this.config.feeds[f];
|
||||
for (let feed of this.config.feeds) {
|
||||
if (feed.url === feedUrl) {
|
||||
return feed.title || "";
|
||||
}
|
||||
@@ -310,22 +283,20 @@ Module.register("newsfeed", {
|
||||
* Schedule visual update.
|
||||
*/
|
||||
scheduleUpdateInterval: function () {
|
||||
var self = this;
|
||||
|
||||
self.updateDom(self.config.animationSpeed);
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
|
||||
// Broadcast NewsFeed if needed
|
||||
if (self.config.broadcastNewsFeeds) {
|
||||
self.sendNotification("NEWS_FEED", { items: self.newsItems });
|
||||
if (this.config.broadcastNewsFeeds) {
|
||||
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
||||
}
|
||||
|
||||
this.timer = setInterval(function () {
|
||||
self.activeItem++;
|
||||
self.updateDom(self.config.animationSpeed);
|
||||
this.timer = setInterval(() => {
|
||||
this.activeItem++;
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
|
||||
// Broadcast NewsFeed if needed
|
||||
if (self.config.broadcastNewsFeeds) {
|
||||
self.sendNotification("NEWS_FEED", { items: self.newsItems });
|
||||
if (this.config.broadcastNewsFeeds) {
|
||||
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
||||
}
|
||||
}, this.config.updateInterval);
|
||||
},
|
||||
@@ -335,8 +306,7 @@ Module.register("newsfeed", {
|
||||
this.config.showFullArticle = false;
|
||||
this.scrollPosition = 0;
|
||||
// reset bottom bar alignment
|
||||
document.getElementsByClassName("region bottom bar")[0].style.bottom = "0";
|
||||
document.getElementsByClassName("region bottom bar")[0].style.top = "inherit";
|
||||
document.getElementsByClassName("region bottom bar")[0].classList.remove("newsfeed-fullarticle");
|
||||
if (!this.timer) {
|
||||
this.scheduleUpdateInterval();
|
||||
}
|
||||
@@ -344,7 +314,9 @@ Module.register("newsfeed", {
|
||||
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
const before = this.activeItem;
|
||||
if (notification === "ARTICLE_NEXT") {
|
||||
if (notification === "MODULE_DOM_CREATED" && this.config.hideLoading) {
|
||||
this.hide();
|
||||
} else if (notification === "ARTICLE_NEXT") {
|
||||
this.activeItem++;
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
this.activeItem = 0;
|
||||
@@ -406,8 +378,7 @@ Module.register("newsfeed", {
|
||||
this.config.showFullArticle = !this.isShowingDescription;
|
||||
// make bottom bar align to top to allow scrolling
|
||||
if (this.config.showFullArticle === true) {
|
||||
document.getElementsByClassName("region bottom bar")[0].style.bottom = "inherit";
|
||||
document.getElementsByClassName("region bottom bar")[0].style.top = "-90px";
|
||||
document.getElementsByClassName("region bottom bar")[0].classList.add("newsfeed-fullarticle");
|
||||
}
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
|
32
modules/default/newsfeed/newsfeed.njk
Normal file
32
modules/default/newsfeed/newsfeed.njk
Normal file
@@ -0,0 +1,32 @@
|
||||
{% if loaded %}
|
||||
<div>
|
||||
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if sourceTitle and config.showSourceTitle %}
|
||||
{{ sourceTitle }}{% 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 }}">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ description | truncate(config.lengthDescription) }}
|
||||
{% else %}
|
||||
{{ description }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</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>
|
||||
{% endif %}
|
@@ -4,9 +4,9 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Log = require("../../../js/logger.js");
|
||||
const Log = require("logger");
|
||||
const FeedMe = require("feedme");
|
||||
const request = require("request");
|
||||
const fetch = require("node-fetch");
|
||||
const iconv = require("iconv-lite");
|
||||
|
||||
/**
|
||||
@@ -19,8 +19,6 @@ const iconv = require("iconv-lite");
|
||||
* @class
|
||||
*/
|
||||
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
const self = this;
|
||||
|
||||
let reloadTimer = null;
|
||||
let items = [];
|
||||
|
||||
@@ -36,14 +34,14 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
/**
|
||||
* Request the new items.
|
||||
*/
|
||||
const fetchNews = function () {
|
||||
const fetchNews = () => {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
items = [];
|
||||
|
||||
const parser = new FeedMe();
|
||||
|
||||
parser.on("item", function (item) {
|
||||
parser.on("item", (item) => {
|
||||
const title = item.title;
|
||||
let description = item.description || item.summary || item.content || "";
|
||||
const pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
|
||||
@@ -68,33 +66,31 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
}
|
||||
});
|
||||
|
||||
parser.on("end", function () {
|
||||
self.broadcastItems();
|
||||
parser.on("end", () => {
|
||||
this.broadcastItems();
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
parser.on("error", function (error) {
|
||||
fetchFailedCallback(self, error);
|
||||
parser.on("error", (error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
const opts = {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)",
|
||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache"
|
||||
},
|
||||
encoding: null
|
||||
const headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)",
|
||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache"
|
||||
};
|
||||
|
||||
request(url, opts)
|
||||
.on("error", function (error) {
|
||||
fetchFailedCallback(self, error);
|
||||
fetch(url, { headers: headers })
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
})
|
||||
.pipe(iconv.decodeStream(encoding))
|
||||
.pipe(parser);
|
||||
.then((res) => {
|
||||
res.body.pipe(iconv.decodeStream(encoding)).pipe(parser);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -136,7 +132,7 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
return;
|
||||
}
|
||||
Log.info("Newsfeed-Fetcher: Broadcasting " + items.length + " items.");
|
||||
itemsReceivedCallback(self);
|
||||
itemsReceivedCallback(this);
|
||||
};
|
||||
|
||||
this.onReceive = function (callback) {
|
||||
|
@@ -6,9 +6,8 @@
|
||||
*/
|
||||
|
||||
const NodeHelper = require("node_helper");
|
||||
const validUrl = require("valid-url");
|
||||
const NewsfeedFetcher = require("./newsfeedfetcher.js");
|
||||
const Log = require("../../../js/logger");
|
||||
const Log = require("logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
@@ -36,8 +35,10 @@ module.exports = NodeHelper.create({
|
||||
const encoding = feed.encoding || "UTF-8";
|
||||
const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
|
||||
|
||||
if (!validUrl.isUri(url)) {
|
||||
this.sendSocketNotification("INCORRECT_URL", url);
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
this.sendSocketNotification("INCORRECT_URL", { url: url });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,8 +74,8 @@ module.exports = NodeHelper.create({
|
||||
* and broadcasts these using sendSocketNotification.
|
||||
*/
|
||||
broadcastFeeds: function () {
|
||||
var feeds = {};
|
||||
for (var f in this.fetchers) {
|
||||
const feeds = {};
|
||||
for (let f in this.fetchers) {
|
||||
feeds[f] = this.fetchers[f].items();
|
||||
}
|
||||
this.sendSocketNotification("NEWS_ITEMS", feeds);
|
||||
|
3
modules/default/newsfeed/oldconfig.njk
Normal file
3
modules/default/newsfeed/oldconfig.njk
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="small bright">
|
||||
{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}
|
||||
</div>
|
@@ -3,7 +3,7 @@ const simpleGits = [];
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const defaultModules = require(__dirname + "/../defaultmodules.js");
|
||||
const Log = require(__dirname + "/../../../js/logger.js");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
@@ -14,32 +14,32 @@ module.exports = NodeHelper.create({
|
||||
|
||||
start: function () {},
|
||||
|
||||
configureModules: function (modules) {
|
||||
configureModules: async function (modules) {
|
||||
// Push MagicMirror itself , biggest chance it'll show up last in UI and isn't overwritten
|
||||
// others will be added in front
|
||||
// this method returns promises so we can't wait for every one to resolve before continuing
|
||||
simpleGits.push({ module: "default", git: SimpleGit(path.normalize(__dirname + "/../../../")) });
|
||||
simpleGits.push({ module: "default", git: this.createGit(path.normalize(__dirname + "/../../../")) });
|
||||
|
||||
var promises = [];
|
||||
|
||||
for (var moduleName in modules) {
|
||||
for (let moduleName in modules) {
|
||||
if (!this.ignoreUpdateChecking(moduleName)) {
|
||||
// Default modules are included in the main MagicMirror repo
|
||||
var moduleFolder = path.normalize(__dirname + "/../../" + moduleName);
|
||||
let moduleFolder = path.normalize(__dirname + "/../../" + moduleName);
|
||||
|
||||
try {
|
||||
Log.info("Checking git for module: " + moduleName);
|
||||
let stat = fs.statSync(path.join(moduleFolder, ".git"));
|
||||
promises.push(this.resolveRemote(moduleName, moduleFolder));
|
||||
// Throws error if file doesn't exist
|
||||
fs.statSync(path.join(moduleFolder, ".git"));
|
||||
// Fetch the git or throw error if no remotes
|
||||
let git = await this.resolveRemote(moduleFolder);
|
||||
// Folder has .git and has at least one git remote, watch this folder
|
||||
simpleGits.unshift({ module: moduleName, git: git });
|
||||
} catch (err) {
|
||||
// Error when directory .git doesn't exist
|
||||
// Error when directory .git doesn't exist or doesn't have any remotes
|
||||
// This module is not managed with git, skip
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
},
|
||||
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
@@ -54,36 +54,36 @@ module.exports = NodeHelper.create({
|
||||
}
|
||||
},
|
||||
|
||||
resolveRemote: function (moduleName, moduleFolder) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var git = SimpleGit(moduleFolder);
|
||||
git.getRemotes(true, (err, remotes) => {
|
||||
if (remotes.length < 1 || remotes[0].name.length < 1) {
|
||||
// No valid remote for folder, skip
|
||||
return resolve();
|
||||
}
|
||||
// Folder has .git and has at least one git remote, watch this folder
|
||||
simpleGits.unshift({ module: moduleName, git: git });
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
resolveRemote: async function (moduleFolder) {
|
||||
let git = this.createGit(moduleFolder);
|
||||
let remotes = await git.getRemotes(true);
|
||||
|
||||
if (remotes.length < 1 || remotes[0].name.length < 1) {
|
||||
throw new Error("No valid remote for folder " + moduleFolder);
|
||||
}
|
||||
|
||||
return git;
|
||||
},
|
||||
|
||||
performFetch: function () {
|
||||
var self = this;
|
||||
simpleGits.forEach((sg) => {
|
||||
sg.git.fetch(["--dry-run"]).status((err, data) => {
|
||||
data.module = sg.module;
|
||||
if (!err) {
|
||||
sg.git.log({ "-1": null }, (err, data2) => {
|
||||
if (!err && data2.latest && "hash" in data2.latest) {
|
||||
data.hash = data2.latest.hash;
|
||||
self.sendSocketNotification("STATUS", data);
|
||||
}
|
||||
performFetch: async function () {
|
||||
for (let sg of simpleGits) {
|
||||
try {
|
||||
let fetchData = await sg.git.fetch(["--dry-run"]).status();
|
||||
let logData = await sg.git.log({ "-1": null });
|
||||
|
||||
if (logData.latest && "hash" in logData.latest) {
|
||||
this.sendSocketNotification("STATUS", {
|
||||
module: sg.module,
|
||||
behind: fetchData.behind,
|
||||
current: fetchData.current,
|
||||
hash: logData.latest.hash,
|
||||
tracking: fetchData.tracking
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
Log.error("Failed to fetch git data for " + sg.module + ": " + err);
|
||||
}
|
||||
}
|
||||
|
||||
this.scheduleNextFetch(this.config.updateInterval);
|
||||
},
|
||||
@@ -93,13 +93,17 @@ module.exports = NodeHelper.create({
|
||||
delay = 60 * 1000;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
let self = this;
|
||||
clearTimeout(this.updateTimer);
|
||||
this.updateTimer = setTimeout(function () {
|
||||
self.performFetch();
|
||||
}, delay);
|
||||
},
|
||||
|
||||
createGit: function (folder) {
|
||||
return SimpleGit({ baseDir: folder, timeout: { block: this.config.timeout } });
|
||||
},
|
||||
|
||||
ignoreUpdateChecking: function (moduleName) {
|
||||
// Should not check for updates for default modules
|
||||
if (defaultModules.indexOf(moduleName) >= 0) {
|
||||
|
@@ -8,7 +8,8 @@ Module.register("updatenotification", {
|
||||
defaults: {
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
refreshInterval: 24 * 60 * 60 * 1000, // one day
|
||||
ignoreModules: []
|
||||
ignoreModules: [],
|
||||
timeout: 1000
|
||||
},
|
||||
|
||||
suspended: false,
|
||||
|
@@ -1,7 +1,4 @@
|
||||
{% if current or weatherData %}
|
||||
{% if weatherData %}
|
||||
{% set current = weatherData.current %}
|
||||
{% endif %}
|
||||
{% if current %}
|
||||
{% if not config.onlyTemp %}
|
||||
<div class="normal medium">
|
||||
<span class="wi wi-strong-wind dimmed"></span>
|
||||
@@ -66,13 +63,10 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if (config.showFeelsLike or config.showPrecipitationAmount) and not config.onlyTemp %}
|
||||
<div class="normal medium">
|
||||
<div class="normal medium feelslike">
|
||||
{% if config.showFeelsLike %}
|
||||
<span class="dimmed">
|
||||
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
|
||||
{% if not config.feelsLikeWithDegree %}
|
||||
{{ current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }}
|
||||
{% endif %}
|
||||
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationAmount %}
|
||||
@@ -84,7 +78,7 @@
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
{{ "LOADING" | translate }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@@ -1,10 +1,5 @@
|
||||
{% if forecast or weatherData %}
|
||||
{% if weatherData %}
|
||||
{% set forecast = weatherData.days %}
|
||||
{% set numSteps = forecast | calcNumEntries %}
|
||||
{% else %}
|
||||
{% set numSteps = forecast | calcNumSteps %}
|
||||
{% endif %}
|
||||
{% if forecast %}
|
||||
{% set numSteps = forecast | calcNumSteps %}
|
||||
{% set currentStep = 0 %}
|
||||
<table class="{{ config.tableClass }}">
|
||||
{% set forecast = forecast.slice(0, numSteps) %}
|
||||
@@ -35,7 +30,7 @@
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
{{ "LOADING" | translate }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@@ -1,7 +1,4 @@
|
||||
{% if hourly or weatherData %}
|
||||
{% if weatherData %}
|
||||
{% set hourly = weatherData.hours %}
|
||||
{% endif %}
|
||||
{% if hourly %}
|
||||
{% set numSteps = hourly | calcNumEntries %}
|
||||
{% set currentStep = 0 %}
|
||||
<table class="{{ config.tableClass }}">
|
||||
@@ -24,9 +21,9 @@
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
{{ "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">{{weatherData | dump}}</div> -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{hourly | dump}}</div> -->
|
||||
|
@@ -29,16 +29,23 @@ WeatherProvider.register("yourprovider", {
|
||||
|
||||
#### `fetchCurrentWeather()`
|
||||
|
||||
This method is called when the weather module tries to fetch the current weather of your provider. The implementation of this method is required.
|
||||
This method is called when the weather module tries to fetch the current weather of your provider. The implementation of this method is required for current weather support.
|
||||
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
|
||||
After the response is processed, the current weather information (as a [WeatherObject](#weatherobject)) needs to be set with `this.setCurrentWeather(currentWeather);`.
|
||||
It will then automatically refresh the module DOM with the new data.
|
||||
|
||||
#### `fetchWeatherForecast()`
|
||||
|
||||
This method is called when the weather module tries to fetch the weather of your provider. The implementation of this method is required.
|
||||
This method is called when the weather module tries to fetch the weather of your provider. The implementation of this method is required for forecast support.
|
||||
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
|
||||
After the response is processed, the weather forecast information (as an array of [WeatherObject](#weatherobject)s) needs to be set with `this.setCurrentWeather(forecast);`.
|
||||
After the response is processed, the weather forecast information (as an array of [WeatherObject](#weatherobject)s) needs to be set with `this.setWeatherForecast(forecast);`.
|
||||
It will then automatically refresh the module DOM with the new data.
|
||||
|
||||
#### `fetchWeatherHourly()`
|
||||
|
||||
This method is called when the weather module tries to fetch the weather of your provider. The implementation of this method is required for hourly support.
|
||||
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
|
||||
After the response is processed, the hourly weather forecast information (as an array of [WeatherObject](#weatherobject)s) needs to be set with `this.setWeatherHourly(forecast);`.
|
||||
It will then automatically refresh the module DOM with the new data.
|
||||
|
||||
### Weather Provider instance methods
|
||||
@@ -63,6 +70,10 @@ This returns a WeatherDay object for the current weather.
|
||||
|
||||
This returns an array of WeatherDay objects for the weather forecast.
|
||||
|
||||
#### `weatherHourly()`
|
||||
|
||||
This returns an array of WeatherDay objects for the hourly weather forecast.
|
||||
|
||||
#### `fetchedLocation()`
|
||||
|
||||
This returns the name of the fetched location or an empty string.
|
||||
@@ -75,6 +86,10 @@ Set the currentWeather and notify the delegate that new information is available
|
||||
|
||||
Set the weatherForecastArray and notify the delegate that new information is available.
|
||||
|
||||
#### `setWeatherHourly(weatherHourlyArray)`
|
||||
|
||||
Set the weatherHourlyArray and notify the delegate that new information is available.
|
||||
|
||||
#### `setFetchedLocation(name)`
|
||||
|
||||
Set the fetched location name.
|
||||
|
@@ -15,6 +15,15 @@ WeatherProvider.register("darksky", {
|
||||
// Not strictly required, but helps for debugging.
|
||||
providerName: "Dark Sky",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://cors-anywhere.herokuapp.com/https://api.darksky.net",
|
||||
weatherEndpoint: "/forecast",
|
||||
apiKey: "",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
},
|
||||
|
||||
units: {
|
||||
imperial: "us",
|
||||
metric: "si"
|
||||
|
@@ -14,6 +14,18 @@ WeatherProvider.register("openweathermap", {
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
providerName: "OpenWeatherMap",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiVersion: "2.5",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
weatherEndpoint: "",
|
||||
locationID: false,
|
||||
location: false,
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
apiKey: ""
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
@@ -56,8 +68,8 @@ WeatherProvider.register("openweathermap", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Overwrite the fetchWeatherData method.
|
||||
fetchWeatherData() {
|
||||
// Overwrite the fetchWeatherHourly method.
|
||||
fetchWeatherHourly() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
@@ -69,7 +81,7 @@ WeatherProvider.register("openweathermap", {
|
||||
this.setFetchedLocation(`(${data.lat},${data.lon})`);
|
||||
|
||||
const weatherData = this.generateWeatherObjectsFromOnecall(data);
|
||||
this.setWeatherData(weatherData);
|
||||
this.setWeatherHourly(weatherData.hours);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
@@ -77,6 +89,31 @@ WeatherProvider.register("openweathermap", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
*
|
||||
* @param config
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = config;
|
||||
if (!this.config.weatherEndpoint) {
|
||||
switch (this.config.type) {
|
||||
case "hourly":
|
||||
this.config.weatherEndpoint = "/onecall";
|
||||
break;
|
||||
case "daily":
|
||||
case "forecast":
|
||||
this.config.weatherEndpoint = "/forecast";
|
||||
break;
|
||||
case "current":
|
||||
this.config.weatherEndpoint = "/weather";
|
||||
break;
|
||||
default:
|
||||
Log.error("weatherEndpoint not configured and could not resolve it based on type");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
@@ -428,6 +465,8 @@ WeatherProvider.register("openweathermap", {
|
||||
} else {
|
||||
params += "&exclude=minutely";
|
||||
}
|
||||
} else if (this.config.lat && this.config.lon) {
|
||||
params += "lat=" + this.config.lat + "&lon=" + this.config.lon;
|
||||
} else if (this.config.locationID) {
|
||||
params += "id=" + this.config.locationID;
|
||||
} else if (this.config.location) {
|
||||
|
@@ -14,6 +14,13 @@
|
||||
WeatherProvider.register("smhi", {
|
||||
providerName: "SMHI",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
precipitationValue: "pmedian"
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements method in interface for fetching current weather
|
||||
*/
|
||||
@@ -55,7 +62,7 @@ WeatherProvider.register("smhi", {
|
||||
this.config = config;
|
||||
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) == -1) {
|
||||
console.log("invalid or not set: " + config.precipitationValue);
|
||||
config.precipitationValue = "pmedian";
|
||||
config.precipitationValue = this.defaults.precipitationValue;
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -14,6 +14,13 @@ WeatherProvider.register("ukmetoffice", {
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
providerName: "UK Met Office",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "http://datapoint.metoffice.gov.uk/public/data/val/wxfcs/all/json/",
|
||||
locationID: false,
|
||||
apiKey: ""
|
||||
},
|
||||
|
||||
units: {
|
||||
imperial: "us",
|
||||
metric: "si"
|
||||
|
@@ -44,6 +44,16 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
// Set the name of the provider.
|
||||
providerName: "UK Met Office (DataHub)",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://api-metoffice.apiconnect.ibmcloud.com/metoffice/production/v0/forecasts/point/",
|
||||
apiKey: "",
|
||||
apiSecret: "",
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
windUnits: "mph"
|
||||
},
|
||||
|
||||
// Build URL with query strings according to DataHub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
getUrl(forecastType) {
|
||||
let queryStrings = "?";
|
||||
|
@@ -14,6 +14,15 @@ WeatherProvider.register("weatherbit", {
|
||||
// Not strictly required, but helps for debugging.
|
||||
providerName: "Weatherbit",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://api.weatherbit.io/v2.0",
|
||||
weatherEndpoint: "/current",
|
||||
apiKey: "",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
},
|
||||
|
||||
units: {
|
||||
imperial: "I",
|
||||
metric: "M"
|
||||
|
@@ -19,6 +19,14 @@ WeatherProvider.register("weathergov", {
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
providerName: "Weather.gov",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://api.weatherbit.io/v2.0",
|
||||
weatherEndpoint: "/forecast",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
},
|
||||
|
||||
// Flag all needed URLs availability
|
||||
configURLs: false,
|
||||
|
||||
|
@@ -12,10 +12,6 @@ Module.register("weather", {
|
||||
weatherProvider: "openweathermap",
|
||||
roundTemp: false,
|
||||
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
location: false,
|
||||
locationID: false,
|
||||
units: config.units,
|
||||
useKmh: false,
|
||||
tempUnits: config.units,
|
||||
@@ -40,20 +36,13 @@ Module.register("weather", {
|
||||
fade: true,
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
initialLoadDelay: 0, // 0 seconds delay
|
||||
retryDelay: 2500,
|
||||
apiKey: "",
|
||||
apiSecret: "",
|
||||
apiVersion: "2.5",
|
||||
apiBase: "https://api.openweathermap.org/data/", // TODO: this should not be part of the weather.js file, but should be contained in the openweatherprovider
|
||||
weatherEndpoint: "/weather",
|
||||
appendLocationNameToHeader: true,
|
||||
calendarClass: "calendar",
|
||||
tableClass: "small",
|
||||
onlyTemp: false,
|
||||
showPrecipitationAmount: false,
|
||||
colored: false,
|
||||
showFeelsLike: true,
|
||||
feelsLikeWithDegree: false
|
||||
showFeelsLike: true
|
||||
},
|
||||
|
||||
// Module properties.
|
||||
@@ -89,8 +78,6 @@ Module.register("weather", {
|
||||
// Let the weather provider know we are starting.
|
||||
this.weatherProvider.start();
|
||||
|
||||
this.config.feelsLikeWithDegree = this.translate("FEELS").indexOf("{DEGREE}") > -1;
|
||||
|
||||
// Add custom filters
|
||||
this.addFilters();
|
||||
|
||||
@@ -133,8 +120,9 @@ Module.register("weather", {
|
||||
case "daily":
|
||||
case "forecast":
|
||||
return `forecast.njk`;
|
||||
//Make the invalid values use the "Loading..." from forecast
|
||||
default:
|
||||
return `${this.config.type.toLowerCase()}.njk`;
|
||||
return `forecast.njk`;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -144,7 +132,7 @@ Module.register("weather", {
|
||||
config: this.config,
|
||||
current: this.weatherProvider.currentWeather(),
|
||||
forecast: this.weatherProvider.weatherForecast(),
|
||||
weatherData: this.weatherProvider.weatherData(),
|
||||
hourly: this.weatherProvider.weatherHourly(),
|
||||
indoor: {
|
||||
humidity: this.indoorHumidity,
|
||||
temperature: this.indoorTemperature
|
||||
@@ -157,6 +145,10 @@ Module.register("weather", {
|
||||
Log.log("New weather information available.");
|
||||
this.updateDom(0);
|
||||
this.scheduleUpdate();
|
||||
|
||||
if (this.weatherProvider.currentWeather()) {
|
||||
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType.replace("-", "_") });
|
||||
}
|
||||
},
|
||||
|
||||
scheduleUpdate: function (delay = null) {
|
||||
@@ -166,19 +158,27 @@ Module.register("weather", {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
this.weatherProvider.fetchWeatherData();
|
||||
} else if (this.config.type === "forecast") {
|
||||
this.weatherProvider.fetchWeatherForecast();
|
||||
} else {
|
||||
this.weatherProvider.fetchCurrentWeather();
|
||||
switch (this.config.type.toLowerCase()) {
|
||||
case "current":
|
||||
this.weatherProvider.fetchCurrentWeather();
|
||||
break;
|
||||
case "hourly":
|
||||
this.weatherProvider.fetchWeatherHourly();
|
||||
break;
|
||||
case "daily":
|
||||
case "forecast":
|
||||
this.weatherProvider.fetchWeatherForecast();
|
||||
break;
|
||||
default:
|
||||
Log.error(`Invalid type ${this.config.type} configured (must be one of 'current', 'hourly', 'daily' or 'forecast')`);
|
||||
}
|
||||
}, nextLoad);
|
||||
},
|
||||
|
||||
roundValue: function (temperature) {
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
return parseFloat(temperature).toFixed(decimals);
|
||||
var roundValue = parseFloat(temperature).toFixed(decimals);
|
||||
return roundValue === "-0" ? 0 : roundValue;
|
||||
},
|
||||
|
||||
addFilters() {
|
||||
|
@@ -11,12 +11,13 @@
|
||||
var WeatherProvider = Class.extend({
|
||||
// Weather Provider Properties
|
||||
providerName: null,
|
||||
defaults: {},
|
||||
|
||||
// The following properties have accessor methods.
|
||||
// Try to not access them directly.
|
||||
currentWeatherObject: null,
|
||||
weatherForecastArray: null,
|
||||
weatherDataObject: null,
|
||||
weatherHourlyArray: null,
|
||||
fetchedLocationName: null,
|
||||
|
||||
// The following properties will be set automatically.
|
||||
@@ -57,10 +58,10 @@ var WeatherProvider = Class.extend({
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherForecast method.`);
|
||||
},
|
||||
|
||||
// This method should start the API request to fetch the weather forecast.
|
||||
// This method should start the API request to fetch the weather hourly.
|
||||
// This method should definitely be overwritten in the provider.
|
||||
fetchWeatherData: function () {
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherData method.`);
|
||||
fetchWeatherHourly: function () {
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherHourly method.`);
|
||||
},
|
||||
|
||||
// This returns a WeatherDay object for the current weather.
|
||||
@@ -74,8 +75,8 @@ var WeatherProvider = Class.extend({
|
||||
},
|
||||
|
||||
// This returns an object containing WeatherDay object(s) depending on the type of call.
|
||||
weatherData: function () {
|
||||
return this.weatherDataObject;
|
||||
weatherHourly: function () {
|
||||
return this.weatherHourlyArray;
|
||||
},
|
||||
|
||||
// This returns the name of the fetched location or an empty string.
|
||||
@@ -95,9 +96,9 @@ var WeatherProvider = Class.extend({
|
||||
this.weatherForecastArray = weatherForecastArray;
|
||||
},
|
||||
|
||||
// Set the weatherDataObject and notify the delegate that new information is available.
|
||||
setWeatherData: function (weatherDataObject) {
|
||||
this.weatherDataObject = weatherDataObject;
|
||||
// Set the weatherHourlyArray and notify the delegate that new information is available.
|
||||
setWeatherHourly: function (weatherHourlyArray) {
|
||||
this.weatherHourlyArray = weatherHourlyArray;
|
||||
},
|
||||
|
||||
// Set the fetched location name.
|
||||
@@ -154,10 +155,11 @@ WeatherProvider.register = function (providerIdentifier, providerDetails) {
|
||||
WeatherProvider.initialize = function (providerIdentifier, delegate) {
|
||||
providerIdentifier = providerIdentifier.toLowerCase();
|
||||
|
||||
var provider = new WeatherProvider.providers[providerIdentifier]();
|
||||
const provider = new WeatherProvider.providers[providerIdentifier]();
|
||||
const config = Object.assign({}, provider.defaults, delegate.config);
|
||||
|
||||
provider.delegate = delegate;
|
||||
provider.setConfig(delegate.config);
|
||||
provider.setConfig(config);
|
||||
|
||||
provider.providerIdentifier = providerIdentifier;
|
||||
if (!provider.providerName) {
|
||||
|
@@ -1,5 +1,7 @@
|
||||
# Module: Weather Forecast
|
||||
|
||||
> :warning: **This module is deprecated in favor of the [weather](https://docs.magicmirror.builders/modules/weather.html) module.**
|
||||
|
||||
The `weatherforecast` module is one of the default modules of the MagicMirror.
|
||||
This module displays the weather forecast for the coming week, including an an icon to display the current conditions, the minimum temperature and the maximum temperature.
|
||||
|
||||
|
9
modules/default/weatherforecast/node_helper.js
Normal file
9
modules/default/weatherforecast/node_helper.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const NodeHelper = require("node_helper");
|
||||
const Log = require("logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.warn(`The module '${this.name}' is deprecated in favor of the 'weather'-module, please refer to the documentation for a migration path`);
|
||||
}
|
||||
});
|
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This module is deprecated. Any additional feature will no longer be merged.
|
||||
*/
|
||||
Module.register("weatherforecast", {
|
||||
// Default module config.
|
||||
@@ -351,6 +353,13 @@ Module.register("weatherforecast", {
|
||||
this.forecast = [];
|
||||
var lastDay = null;
|
||||
var forecastData = {};
|
||||
var dayStarts = 8;
|
||||
var dayEnds = 17;
|
||||
|
||||
if (data.city && data.city.sunrise && data.city.sunset) {
|
||||
dayStarts = new Date(moment.unix(data.city.sunrise).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
|
||||
dayEnds = new Date(moment.unix(data.city.sunset).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
|
||||
}
|
||||
|
||||
// Handle different structs between forecast16 and onecall endpoints
|
||||
var forecastList = null;
|
||||
@@ -371,10 +380,10 @@ Module.register("weatherforecast", {
|
||||
var hour;
|
||||
if (forecast.dt_txt) {
|
||||
day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
|
||||
hour = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").toDate().getHours();
|
||||
hour = new Date(moment(forecast.dt_txt).locale("en").format("YYYY-MM-DD HH:mm:ss")).getHours();
|
||||
} else {
|
||||
day = moment(forecast.dt, "X").format("ddd");
|
||||
hour = moment(forecast.dt, "X").toDate().getHours();
|
||||
hour = new Date(moment(forecast.dt, "X")).getHours();
|
||||
}
|
||||
|
||||
if (day !== lastDay) {
|
||||
@@ -400,7 +409,7 @@ Module.register("weatherforecast", {
|
||||
|
||||
// Since we don't want an icon from the start of the day (in the middle of the night)
|
||||
// we update the icon as long as it's somewhere during the day.
|
||||
if (hour >= 8 && hour <= 17) {
|
||||
if (hour > dayStarts && hour < dayEnds) {
|
||||
forecastData.icon = this.config.iconTable[forecast.weather[0].icon];
|
||||
}
|
||||
}
|
||||
@@ -462,7 +471,8 @@ Module.register("weatherforecast", {
|
||||
*/
|
||||
roundValue: function (temperature) {
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
return parseFloat(temperature).toFixed(decimals);
|
||||
var roundValue = parseFloat(temperature).toFixed(decimals);
|
||||
return roundValue === "-0" ? 0 : roundValue;
|
||||
},
|
||||
|
||||
/* processRain(forecast, allForecasts)
|
||||
|
6961
package-lock.json
generated
6961
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
60
package.json
60
package.json
@@ -1,25 +1,26 @@
|
||||
{
|
||||
"name": "magicmirror",
|
||||
"version": "2.14.0",
|
||||
"version": "2.15.0",
|
||||
"description": "The open source modular smart mirror platform.",
|
||||
"main": "js/electron.js",
|
||||
"scripts": {
|
||||
"start": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js",
|
||||
"start:dev": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js dev",
|
||||
"server": "node ./serveronly",
|
||||
"install": "echo \"Installing vendor files ...\n\" && cd vendor && npm install --loglevel=error",
|
||||
"install-fonts": "echo \"Installing fonts ...\n\" && cd fonts && npm install --loglevel=error",
|
||||
"postinstall": "npm run install-fonts && echo \"MagicMirror installation finished successfully! \n\"",
|
||||
"test": "NODE_ENV=test mocha tests --recursive",
|
||||
"test:coverage": "NODE_ENV=test nyc mocha tests --recursive --timeout=3000",
|
||||
"test:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text mocha tests --recursive --timeout=3000",
|
||||
"test:e2e": "NODE_ENV=test mocha tests/e2e --recursive",
|
||||
"test:unit": "NODE_ENV=test mocha tests/unit --recursive",
|
||||
"test:prettier": "prettier --check **/*.{js,css,json,md,yml}",
|
||||
"test:js": "eslint *.js js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --quiet",
|
||||
"test:js": "eslint js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --quiet",
|
||||
"test:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json",
|
||||
"test:calendar": "node ./modules/default/calendar/debug.js",
|
||||
"config:check": "node js/check_config.js",
|
||||
"lint:prettier": "prettier --write **/*.{js,css,json,md,yml}",
|
||||
"lint:js": "eslint *.js js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --fix",
|
||||
"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 modules/default/**/*.css --config .stylelintrc.json --fix"
|
||||
},
|
||||
"repository": {
|
||||
@@ -42,53 +43,56 @@
|
||||
},
|
||||
"homepage": "https://magicmirror.builders",
|
||||
"devDependencies": {
|
||||
"chai": "^4.2.0",
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"danger": "^10.5.4",
|
||||
"eslint-config-prettier": "^7.0.0",
|
||||
"eslint-plugin-jsdoc": "^30.7.8",
|
||||
"eslint-plugin-prettier": "^3.2.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-jsdoc": "^32.3.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"husky": "^4.3.5",
|
||||
"jsdom": "^16.4.0",
|
||||
"lodash": "^4.17.20",
|
||||
"mocha": "^8.2.1",
|
||||
"husky": "^4.3.8",
|
||||
"jsdom": "^16.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mocha": "^8.3.2",
|
||||
"mocha-each": "^2.0.1",
|
||||
"mocha-logger": "^1.0.7",
|
||||
"nyc": "^15.1.0",
|
||||
"prettier": "^2.2.1",
|
||||
"pretty-quick": "^3.1.0",
|
||||
"spectron": "^10.0.1",
|
||||
"stylelint": "^13.8.0",
|
||||
"sinon": "^10.0.0",
|
||||
"spectron": "^13.0.0",
|
||||
"stylelint": "^13.12.0",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-prettier": "^1.1.2"
|
||||
"stylelint-config-standard": "^21.0.0",
|
||||
"stylelint-prettier": "^1.2.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"electron": "^8.5.3"
|
||||
"electron": "^11.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"colors": "^1.4.0",
|
||||
"console-stamp": "^3.0.0-rc4.2",
|
||||
"eslint": "^7.15.0",
|
||||
"digest-fetch": "^1.1.6",
|
||||
"eslint": "^7.23.0",
|
||||
"express": "^4.17.1",
|
||||
"express-ipfilter": "^1.1.2",
|
||||
"feedme": "^2.0.2",
|
||||
"helmet": "^4.2.0",
|
||||
"ical": "^0.8.0",
|
||||
"helmet": "^4.4.1",
|
||||
"iconv-lite": "^0.6.2",
|
||||
"module-alias": "^2.2.2",
|
||||
"moment": "^2.29.1",
|
||||
"node-ical": "^0.12.7",
|
||||
"request": "^2.88.2",
|
||||
"rrule": "^2.6.6",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-ical": "^0.12.9",
|
||||
"rrule": "^2.6.8",
|
||||
"rrule-alt": "^2.2.8",
|
||||
"simple-git": "^2.31.0",
|
||||
"socket.io": "^3.0.4",
|
||||
"valid-url": "^1.0.9"
|
||||
"simple-git": "^2.37.0",
|
||||
"socket.io": "^4.0.0"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"node_helper": "js/node_helper.js"
|
||||
"node_helper": "js/node_helper.js",
|
||||
"logger": "js/logger.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const app = require("../js/app.js");
|
||||
const Log = require("../js/logger.js");
|
||||
const Log = require("logger");
|
||||
|
||||
app.start(function (config) {
|
||||
var bindAddress = config.address ? config.address : "localhost";
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,7 +26,7 @@ var config = {
|
||||
calendars: [
|
||||
{
|
||||
maximumNumberOfDays: 10000,
|
||||
url: "http://localhost:8011/tests/configs/data/calendar_test.ics",
|
||||
url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
|
||||
auth: {
|
||||
user: "MagicMirror",
|
||||
pass: "CallMeADog"
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,7 +26,7 @@ var config = {
|
||||
calendars: [
|
||||
{
|
||||
maximumNumberOfDays: 10000,
|
||||
url: "http://localhost:8010/tests/configs/data/calendar_test.ics",
|
||||
url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
|
||||
auth: {
|
||||
user: "MagicMirror",
|
||||
pass: "CallMeADog",
|
||||
|
44
tests/configs/modules/calendar/changed-port.js
Normal file
44
tests/configs/modules/calendar/changed-port.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/* Magic Mirror Test config default calendar with auth by default
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "calendar",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
calendars: [
|
||||
{
|
||||
maximumNumberOfDays: 10000,
|
||||
url: "http://localhost:8010/tests/configs/data/calendar_test.ics",
|
||||
auth: {
|
||||
user: "MagicMirror",
|
||||
pass: "CallMeADog"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
@@ -11,7 +11,8 @@ let config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -15,7 +15,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,7 +26,7 @@ var config = {
|
||||
calendars: [
|
||||
{
|
||||
maximumNumberOfDays: 10000,
|
||||
url: "http://localhost:8012/tests/configs/data/calendar_test.ics",
|
||||
url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
|
||||
user: "MagicMirror",
|
||||
pass: "CallMeADog"
|
||||
}
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -11,7 +11,8 @@ let config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -16,7 +16,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -14,7 +14,8 @@ let config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -14,7 +14,8 @@ var config = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -3,8 +3,7 @@
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var config = {
|
||||
let config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
@@ -13,7 +12,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
38
tests/configs/modules/newsfeed/incorrect_url.js
Normal file
38
tests/configs/modules/newsfeed/incorrect_url.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/* Magic Mirror Test config newsfeed module
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "newsfeed",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
feeds: [
|
||||
{
|
||||
title: "Incorrect Url",
|
||||
url: "this is not a valid url"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
39
tests/configs/modules/newsfeed/prohibited_words.js
Normal file
39
tests/configs/modules/newsfeed/prohibited_words.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/* Magic Mirror Test config newsfeed module
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "newsfeed",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
feeds: [
|
||||
{
|
||||
title: "Rodrigo Ramirez Blog",
|
||||
url: "http://localhost:8080/tests/configs/data/feed_test_rodrigoramirez.xml"
|
||||
}
|
||||
],
|
||||
prohibitedWords: ["QPanel"]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
@@ -15,7 +15,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
49
tests/configs/modules/weather/currentweather_compliments.js
Normal file
49
tests/configs/modules/weather/currentweather_compliments.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/* Magic Mirror Test config current weather compliments
|
||||
*
|
||||
* By rejas https://github.com/rejas
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
let config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
fullscreen: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "compliments",
|
||||
position: "top_bar",
|
||||
config: {
|
||||
compliments: {
|
||||
snow: ["snow"]
|
||||
},
|
||||
updateInterval: 4000
|
||||
}
|
||||
},
|
||||
{
|
||||
module: "weather",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
location: "Munich",
|
||||
apiKey: "fake key",
|
||||
initialLoadDelay: 3000
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
@@ -14,7 +14,8 @@ let config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -14,7 +14,8 @@ let config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -14,7 +14,8 @@ let config = {
|
||||
units: "imperial",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -14,7 +14,8 @@ let config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -14,7 +14,8 @@ let config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user