For the mergne

This commit is contained in:
Joris
2020-09-20 20:57:20 +02:00
226 changed files with 6747 additions and 9298 deletions

15
.editorconfig Normal file
View File

@@ -0,0 +1,15 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 250
trim_trailing_whitespace = true
[*.{js,json}]
indent_size = 4
indent_style = tab

View File

@@ -1,5 +1,6 @@
{ {
"extends": "eslint:recommended", "extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
"plugins": ["prettier", "jsdoc"],
"env": { "env": {
"browser": true, "browser": true,
"es6": true, "es6": true,
@@ -21,6 +22,7 @@
} }
}, },
"rules": { "rules": {
"prettier/prettier": "error",
"eqeqeq": "error", "eqeqeq": "error",
"no-prototype-builtins": "off", "no-prototype-builtins": "off",
"no-unused-vars": "off" "no-unused-vars": "off"

View File

@@ -1,5 +1,4 @@
Contribution Policy for MagicMirror² # Contribution Policy for MagicMirror²
====================================
Thanks for contributing to MagicMirror²! Thanks for contributing to MagicMirror²!
@@ -30,7 +29,7 @@ Problems installing or configuring your MagicMirror? Check out: [https://forum.m
When submitting a new issue, please supply the following information: 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, 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 0.12.13 or later.

View File

@@ -1,24 +1,29 @@
## I'm not sure if this is a bug ## I'm not sure if this is a bug
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt) If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
## I'm having troubles installing or configuring MagicMirror ## I'm having troubles installing or configuring MagicMirror
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting) Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
## I found a bug in the MagicMirror installer ## 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: If you are facing an issue or found a bug while trying to install MagicMirror via the installer please report it in the respective GitHub repository:
[https://github.com/sdetweil/MagicMirror_scripts](https://github.com/sdetweil/MagicMirror_scripts) [https://github.com/sdetweil/MagicMirror_scripts](https://github.com/sdetweil/MagicMirror_scripts)
## I found a bug in the MagicMirror Docker image ## I found a bug in the MagicMirror Docker image
If you are facing an issue or found a bug while running MagicMirror inside a Docker container please create an issue in the GitHub repository of the MagicMirror Docker image: If you are facing an issue or found a bug while running MagicMirror inside a Docker container please create an issue in the GitHub repository of the MagicMirror Docker image:
[https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror) [https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror)
--- ---
## I found a bug in MagicMirror ## I found a bug in MagicMirror
Please make sure to only submit reproducible issues. You can safely remove everything above the dividing line. 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: 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, Windows, Mac, Linux, System V UNIX).
**Node Version**: Make sure it's version 8 or later. **Node Version**: Make sure it's version 8 or later.

View File

@@ -7,8 +7,7 @@ pull request to send us your changes. This makes everyone's lives
easier (including yours) and helps us out on the development team. easier (including yours) and helps us out on the development team.
Thanks! Thanks!
- Does the pull request solve a **related** issue?
* Does the pull request solve a **related** issue? - If so, can you reference the issue?
* If so, can you reference the issue? - What does the pull request accomplish? Use a list if needed.
* What does the pull request accomplish? Use a list if needed. - If it includes major visual changes please add screenshots.
* If it includes major visual changes please add screenshots.

6
.gitignore vendored
View File

@@ -1,5 +1,4 @@
# Various Node ignoramuses. # Various Node ignoramuses.
logs logs
*.log *.log
npm-debug.log* npm-debug.log*
@@ -13,9 +12,11 @@ build/Release
/node_modules/**/* /node_modules/**/*
fonts/node_modules/**/* fonts/node_modules/**/*
vendor/node_modules/**/* vendor/node_modules/**/*
!/tests/node_modules/**/*
jspm_modules jspm_modules
.npm .npm
.node_repl_history .node_repl_history
.nyc_output/
# Visual Studio Code ignoramuses. # Visual Studio Code ignoramuses.
.vscode/ .vscode/
@@ -53,7 +54,6 @@ Temporary Items
.apdisk .apdisk
# Various Linux ignoramuses. # Various Linux ignoramuses.
.fuse_hidden* .fuse_hidden*
.directory .directory
.Trash-* .Trash-*
@@ -76,5 +76,3 @@ Temporary Items
*.orig *.orig
*.rej *.rej
*.bak *.bak
!/tests/node_modules/**/*

View File

@@ -1 +0,0 @@
modules/default/calendar/vendor/*

View File

@@ -1,23 +0,0 @@
{
"default": true,
"line-length": false,
"blanks-around-headers": false,
"no-duplicate-header": false,
"no-inline-html": false,
"MD010": false,
"MD001": false,
"MD031": false,
"MD040": false,
"MD002": false,
"MD029": false,
"MD041": false,
"MD032": false,
"MD036": false,
"MD037": false,
"MD009": false,
"MD018": false,
"MD012": false,
"MD026": false,
"MD038": false,
"MD047": false
}

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
package-lock.json
/config/**/*
/vendor/**/*
!/vendor/vendor.js

3
.prettierrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"trailingComma": "none"
}

View File

@@ -1,6 +1,7 @@
{ {
"extends": "stylelint-config-standard", "extends": ["stylelint-prettier/recommended"],
"font-family-name-quotes": "double-where-recommended", "plugins": ["stylelint-prettier"],
"block-no-empty": false, "rules": {
"ignoreFiles": ["./modules/default/alert/ns-default.css"] "prettier/prettier": true
}
} }

View File

@@ -13,7 +13,9 @@ before_script:
- "sh -e /etc/init.d/xvfb start" - "sh -e /etc/init.d/xvfb start"
- sleep 5 - sleep 5
script: script:
- npm run test:lint - npm run test:prettier
- npm run test:js
- npm run test:css
- npm run test:e2e - npm run test:e2e
- npm run test:unit - npm run test:unit
after_script: after_script:

View File

@@ -5,26 +5,84 @@ 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² ❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
## [2.12.0] - Unreleased (Develop Branch) ### fixed
*This release is scheduled to be released on 2020-07-01.* - 2110, 2111, 2118 recurring full day events should not use timezone adjustment. just compare month/day
## [2.13.0] - Unreleased (Develop Branch - Please add your contributions to this release.)
_This release is scheduled to be released on 2020-10-01._
### Added ### Added
- `--dry-run` option adde 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 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
- Added eslint-plugin for jsdoc comments
### Updated ### Updated
- Cleaned up alert module code
- Cleaned up check_config code - Change incorrect weather.js default properties.
- Replaced grunt-based linters with their non-grunt equivalents - Cleaned up newsfeed module.
- Switch to most of the eslint:recommended rules and fix warnings - Cleaned up jsdoc comments.
- Replaced insecure links with https ones - Cleaned up clock tests.
- Cleaned up all "no-undef" warnings from eslint
### Deleted ### Deleted
- Removed truetype (ttf) fonts
### Fixed ### Fixed
- The broken modules due to Socket.io change from last release [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
- Add backward compatibility for old module code in socketclient.js [#1973](https://github.com/MichMich/MagicMirror/issues/1973) - Fix backward compatibility issues for Safari < 11.
- Fix the use of "maxNumberOfDays" in the module "weatherforecast depending on the endpoint (forecast/daily or forecast)". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
- Fix calendar display. Account for current timezone. [#2068](https://github.com/MichMich/MagicMirror/issues/2068)
- Fix logLevel being set before loading config.
- Fix incorrect namespace links in svg clockfaces. [#2072](https://github.com/MichMich/MagicMirror/issues/2072)
- Fix weather/providers/weathergov for API guidelines. [#2045](https://github.com/MichMich/MagicMirror/issues/2045)
- Fix "undefined" in weather modules header. [#1985](https://github.com/MichMich/MagicMirror/issues/1985)
## [2.12.0] - 2020-07-01
Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryanzzhu, @chamakura, @DarthBrento, @Ekristoffe, @khassel, @Legion2, @ndom91, @radokristof, @rejas, @XBCreepinJesus & @ZoneMR.
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
### Added
- Added option to config the level of logging.
- Added prettier for an even cleaner codebase.
- Hide Sunrise/Sunset in Weather module.
- Hide Sunrise/Sunset in Current Weather module.
- Added Met Office DataHub (UK) provider.
### Updated
- Cleaned up alert module code.
- Cleaned up check_config code.
- Replaced grunt-based linters with their non-grunt equivalents.
- Switch to most of the eslint:recommended rules and fix warnings.
- Replaced insecure links with https ones.
- Cleaned up all "no-undef" warnings from eslint.
- Added location title wrapping for calendar module.
- Updated the BG translation.
### Deleted
- Removed truetype (ttf) fonts.
### Fixed
- The broken modules due to Socket.io change from last release. [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
- Add backward compatibility for old module code in socketclient.js. [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
- Support multiple instances of calendar module with different config. [#1109](https://github.com/MichMich/MagicMirror/issues/1109)
- Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
- Throw error when check_config fails. [#1928](https://github.com/MichMich/MagicMirror/issues/1928)
- Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MichMich/MagicMirror/issues/2050)
- Updated ical library to 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 ## [2.11.0] - 2020-04-01
@@ -35,11 +93,13 @@ In the past years the project has grown a lot. This came with a huge downside: p
For more information regarding this major change, please check issue [#1860](https://github.com/MichMich/MagicMirror/issues/1860). For more information regarding this major change, please check issue [#1860](https://github.com/MichMich/MagicMirror/issues/1860).
### Deleted ### Deleted
- Remove installers. - Remove installers.
- Remove externalized scripts. - Remove externalized scripts.
- Remove jshint dependency, instead eslint checks your config file now - Remove jshint dependency, instead eslint checks your config file now
### Added ### Added
- Brazilian translation for "FEELS". - Brazilian translation for "FEELS".
- Ukrainian translation. - Ukrainian translation.
- Finnish translation for "PRECIP", "UPDATE_INFO_MULTIPLE" and "UPDATE_INFO_SINGLE". - Finnish translation for "PRECIP", "UPDATE_INFO_MULTIPLE" and "UPDATE_INFO_SINGLE".
@@ -54,6 +114,7 @@ For more information regarding this major change, please check issue [#1860](htt
- Add HTTPS support for clientonly-mode. - Add HTTPS support for clientonly-mode.
### Fixed ### Fixed
- Force declaration of public ip address in config file (ISSUE #1852) - Force declaration of public ip address in config file (ISSUE #1852)
- Fixes `run-start.sh`: If running in docker-container, don't check the environment, just start electron (ISSUE #1859) - Fixes `run-start.sh`: If running in docker-container, don't check the environment, just start electron (ISSUE #1859)
- Fix calendar time offset for recurring events crossing Daylight Savings Time (ISSUE #1798) - Fix calendar time offset for recurring events crossing Daylight Savings Time (ISSUE #1798)
@@ -64,6 +125,7 @@ For more information regarding this major change, please check issue [#1860](htt
- Fix update checking skipping 3rd party modules the first time - Fix update checking skipping 3rd party modules the first time
### Changed ### Changed
- Remove documentation from core repository and link to new dedicated docs site: [docs.magicmirror.builders](https://docs.magicmirror.builders). - Remove documentation from core repository and link to new dedicated docs site: [docs.magicmirror.builders](https://docs.magicmirror.builders).
- Updated config.js.sample: Corrected some grammar on `config.js.sample` comment section. - Updated config.js.sample: Corrected some grammar on `config.js.sample` comment section.
- Removed `run-start.sh` script and update start commands: - Removed `run-start.sh` script and update start commands:
@@ -78,6 +140,7 @@ For more information regarding this major change, please check issue [#1860](htt
## [2.10.1] - 2020-01-10 ## [2.10.1] - 2020-01-10
### Changed ### Changed
- Updated README.md: Added links to the official documentation website and remove links to broken installer. - Updated README.md: Added links to the official documentation website and remove links to broken installer.
## [2.10.0] - 2020-01-01 ## [2.10.0] - 2020-01-01
@@ -87,12 +150,14 @@ Special thanks to @sdetweil for all his great contributions!
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
### Added ### Added
- Timestamps in log output. - Timestamps in log output.
- Padding in dateheader mode of the calendar module. - Padding in dateheader mode of the calendar module.
- New upgrade script to help users consume regular updates installers/upgrade-script.sh. - New upgrade script to help users consume regular updates installers/upgrade-script.sh.
- New script to help setup pm2, without install installers/fixuppm2.sh. - New script to help setup pm2, without install installers/fixuppm2.sh.
### Updated ### Updated
- Updated lower bound of `lodash` and `helmet` dependencies for security patches. - Updated lower bound of `lodash` and `helmet` dependencies for security patches.
- Updated compliments.js to handle newline in text, as textfields to not interpolate contents. - Updated compliments.js to handle newline in text, as textfields to not interpolate contents.
- Updated raspberry.sh installer script to handle new platform issues, split node/npm, pm2, and screen saver changes. - Updated raspberry.sh installer script to handle new platform issues, split node/npm, pm2, and screen saver changes.
@@ -101,6 +166,7 @@ Special thanks to @sdetweil for all his great contributions!
- Only check for xwindows running if not on macOS. - Only check for xwindows running if not on macOS.
### Fixed ### Fixed
- Fixed issue in weatherforecast module where predicted amount of rain was not using the decimal symbol specified in config.js. - Fixed issue in weatherforecast module where predicted amount of rain was not using the decimal symbol specified in config.js.
- Module header now updates correctly, if a module need to dynamically show/hide its header based on a condition. - Module header now updates correctly, if a module need to dynamically show/hide its header based on a condition.
- Fix handling of config.js for serverOnly mode commented out. - Fix handling of config.js for serverOnly mode commented out.
@@ -113,18 +179,21 @@ Special thanks to @sdetweil for all his great contributions!
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md). **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md).
### Added ### Added
- Spanish translation for "PRECIP". - Spanish translation for "PRECIP".
- Adding a Malay (Malaysian) translation for MagicMirror². - Adding a Malay (Malaysian) translation for MagicMirror².
- Add test check URLs of vendors 200 and 404 HTTP CODE. - Add test check URLs of vendors 200 and 404 HTTP CODE.
- Add tests for new weather module and helper to stub ajax requests. - Add tests for new weather module and helper to stub ajax requests.
### Updated ### Updated
- Updatenotification module: Display update notification for a limited (configurable) time. - Updatenotification module: Display update notification for a limited (configurable) time.
- Enabled e2e/vendor_spec.js tests. - Enabled e2e/vendor_spec.js tests.
- The css/custom.css will be renamed after the next release. We've added into `run-start.sh` an instruction by GIT to ignore with `--skip-worktree` and `rm --cached`. [#1540](https://github.com/MichMich/MagicMirror/issues/1540) - The css/custom.css will be renamed after the next release. We've added into `run-start.sh` an instruction by GIT to ignore with `--skip-worktree` and `rm --cached`. [#1540](https://github.com/MichMich/MagicMirror/issues/1540)
- Disable sending of notification CLOCK_SECOND when displaySeconds is false. - Disable sending of notification CLOCK_SECOND when displaySeconds is false.
### Fixed ### Fixed
- Updatenotification module: Properly handle race conditions, prevent crash. - Updatenotification module: Properly handle race conditions, prevent crash.
- Send `NEWS_FEED` notification also for the first news messages which are shown. - Send `NEWS_FEED` notification also for the first news messages which are shown.
- Fixed issue where weather module would not refresh data after a network or API outage. [#1722](https://github.com/MichMich/MagicMirror/issues/1722) - Fixed issue where weather module would not refresh data after a network or API outage. [#1722](https://github.com/MichMich/MagicMirror/issues/1722)
@@ -136,6 +205,7 @@ Special thanks to @sdetweil for all his great contributions!
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md). **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md).
### Added ### Added
- Option to show event location in calendar - Option to show event location in calendar
- Finnish translation for "Feels" and "Weeks" - Finnish translation for "Feels" and "Weeks"
- Russian translation for Feels - Russian translation for Feels
@@ -155,6 +225,7 @@ Special thanks to @sdetweil for all his great contributions!
- Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc - Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc
### Updated ### Updated
- English translation for "Feels" to "Feels like" - English translation for "Feels" to "Feels like"
- Fixed the example calendar url in `config.js.sample` - Fixed the example calendar url in `config.js.sample`
- Update `ical.js` to solve various calendar issues. - Update `ical.js` to solve various calendar issues.
@@ -162,6 +233,7 @@ Special thanks to @sdetweil for all his great contributions!
- Only update clock once per minute when seconds aren't shown - Only update clock once per minute when seconds aren't shown
### Fixed ### Fixed
- Fixed uncaught exception, race condition on module update - Fixed uncaught exception, race condition on module update
- Fixed issue [#1696](https://github.com/MichMich/MagicMirror/issues/1696), some ical files start date to not parse to date type - Fixed issue [#1696](https://github.com/MichMich/MagicMirror/issues/1696), some ical files start date to not parse to date type
- Allowance HTML5 autoplay-policy (policy is changed from Chrome 66 updates) - Allowance HTML5 autoplay-policy (policy is changed from Chrome 66 updates)
@@ -173,6 +245,7 @@ Special thanks to @sdetweil for all his great contributions!
- Updated the fetchedLocationName variable in currentweather.js so that city shows up in the header - Updated the fetchedLocationName variable in currentweather.js so that city shows up in the header
### Updated installer ### Updated installer
- give non-pi2+ users (pi0, odroid, jetson nano, mac, windows, ...) option to continue install - give non-pi2+ users (pi0, odroid, jetson nano, mac, windows, ...) option to continue install
- use current username vs hardcoded 'pi' to support non-pi install - use current username vs hardcoded 'pi' to support non-pi install
- check for npm installed. node install doesn't do npm anymore - check for npm installed. node install doesn't do npm anymore
@@ -189,6 +262,7 @@ Fixed `package.json` version number.
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md). **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md).
### Added ### Added
- Italian translation for "Feels" - Italian translation for "Feels"
- Basic Klingon (tlhIngan Hol) translations - Basic Klingon (tlhIngan Hol) translations
- Disabled the screensaver on raspbian with installation script - Disabled the screensaver on raspbian with installation script
@@ -202,12 +276,14 @@ Fixed `package.json` version number.
- Add `name` config option for calendars to be sent along with event broadcasts - Add `name` config option for calendars to be sent along with event broadcasts
### Updated ### Updated
- Bumped the Electron dependency to v3.0.13 to support the most recent Raspbian. [#1500](https://github.com/MichMich/MagicMirror/issues/1500) - Bumped the Electron dependency to v3.0.13 to support the most recent Raspbian. [#1500](https://github.com/MichMich/MagicMirror/issues/1500)
- Updated modernizr code in alert module, fixed a small typo there too - Updated modernizr code in alert module, fixed a small typo there too
- More verbose error message on console if the config is malformed - More verbose error message on console if the config is malformed
- Updated installer script to install Node.js version 10.x - Updated installer script to install Node.js version 10.x
### Fixed ### Fixed
- Fixed temperature displays in currentweather and weatherforecast modules [#1503](https://github.com/MichMich/MagicMirror/issues/1503), [#1511](https://github.com/MichMich/MagicMirror/issues/1511). - Fixed temperature displays in currentweather and weatherforecast modules [#1503](https://github.com/MichMich/MagicMirror/issues/1503), [#1511](https://github.com/MichMich/MagicMirror/issues/1511).
- Fixed unhandled error on bad git data in updatenotification module [#1285](https://github.com/MichMich/MagicMirror/issues/1285). - Fixed unhandled error on bad git data in updatenotification module [#1285](https://github.com/MichMich/MagicMirror/issues/1285).
- Weather forecast now works with openweathermap in new weather module. Daily data are displayed, see issue [#1504](https://github.com/MichMich/MagicMirror/issues/1504). - Weather forecast now works with openweathermap in new weather module. Daily data are displayed, see issue [#1504](https://github.com/MichMich/MagicMirror/issues/1504).
@@ -225,6 +301,7 @@ Fixed `package.json` version number.
- Fix documentation of `useKMPHwind` option in currentweather - Fix documentation of `useKMPHwind` option in currentweather
### New weather module ### New weather module
- Fixed weather forecast table display [#1499](https://github.com/MichMich/MagicMirror/issues/1499). - Fixed weather forecast table display [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
- Dimmed loading indicator for weather forecast. - Dimmed loading indicator for weather forecast.
- Implemented config option `decimalSymbol` [#1499](https://github.com/MichMich/MagicMirror/issues/1499). - Implemented config option `decimalSymbol` [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
@@ -242,11 +319,13 @@ Fixed `package.json` version number.
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues updating, make sure you are running the latest version of Node. **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues updating, make sure you are running the latest version of Node.
### ✨ Experimental ✨ ### ✨ Experimental ✨
- New default [module weather](modules/default/weather). This module will eventually replace the current `currentweather` and `weatherforecast` modules. The new module is still pretty experimental, but it's included so you can give it a try and help us improve this module. Please give us you feedback using [this forum post](https://forum.magicmirror.builders/topic/9335/default-weather-module-refactoring). - New default [module weather](modules/default/weather). This module will eventually replace the current `currentweather` and `weatherforecast` modules. The new module is still pretty experimental, but it's included so you can give it a try and help us improve this module. Please give us you feedback using [this forum post](https://forum.magicmirror.builders/topic/9335/default-weather-module-refactoring).
A huge, huge, huge thanks to user @fewieden for all his hard work on the new `weather` module! A huge, huge, huge thanks to user @fewieden for all his hard work on the new `weather` module!
### Added ### Added
- Possibility to add classes to the cell of symbol, title and time of the events of calendar. - Possibility to add classes to the cell of symbol, title and time of the events of calendar.
- Font-awesome 5, still has 4 for backwards compatibility. - Font-awesome 5, still has 4 for backwards compatibility.
- Missing `showEnd` in calendar documentation - Missing `showEnd` in calendar documentation
@@ -261,6 +340,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Documentation for the existing `scale` option in the Weather Forecast module. - Documentation for the existing `scale` option in the Weather Forecast module.
### Fixed ### Fixed
- Allow parsing recurring calendar events where the start date is before 1900 - Allow parsing recurring calendar events where the start date is before 1900
- Fixed Polish translation for Single Update Info - Fixed Polish translation for Single Update Info
- Ignore entries with unparseable details in the calendar module - Ignore entries with unparseable details in the calendar module
@@ -268,15 +348,17 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Bug in newsfeed when `removeStartTags` is used on the description [#1478](https://github.com/MichMich/MagicMirror/issues/1478) - Bug in newsfeed when `removeStartTags` is used on the description [#1478](https://github.com/MichMich/MagicMirror/issues/1478)
### Updated ### Updated
- The default calendar setting `showEnd` is changed to `false`. - The default calendar setting `showEnd` is changed to `false`.
### Changed ### Changed
- The Weather Forecast module by default displays the &deg; symbol after every numeric value to be consistent with the Current Weather module.
- The Weather Forecast module by default displays the &deg; symbol after every numeric value to be consistent with the Current Weather module.
## [2.5.0] - 2018-10-01 ## [2.5.0] - 2018-10-01
### Added ### Added
- Romanian translation for "Feels" - Romanian translation for "Feels"
- Support multi-line compliments - Support multi-line compliments
- Simplified Chinese translation for "Feels" - Simplified Chinese translation for "Feels"
@@ -291,6 +373,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Support for showing end of events through config parameters showEnd and dateEndFormat - Support for showing end of events through config parameters showEnd and dateEndFormat
### Fixed ### Fixed
- Fixed gzip encoded calendar loading issue #1400. - Fixed gzip encoded calendar loading issue #1400.
- Mixup between german and spanish translation for newsfeed. - Mixup between german and spanish translation for newsfeed.
- Fixed close dates to be absolute, if no configured in the config.js - module Calendar - Fixed close dates to be absolute, if no configured in the config.js - module Calendar
@@ -336,11 +419,13 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Add update translations for Português Brasileiro - Add update translations for Português Brasileiro
### Changed ### Changed
- Upgrade to Electron 2.0.0. - Upgrade to Electron 2.0.0.
- Remove yarn-or-npm which breaks production builds. - Remove yarn-or-npm which breaks production builds.
- Invoke module suspend even if no dom content. [#1308](https://github.com/MichMich/MagicMirror/issues/1308) - Invoke module suspend even if no dom content. [#1308](https://github.com/MichMich/MagicMirror/issues/1308)
### Fixed ### Fixed
- Fixed issue where wind chill could not be displayed in Fahrenheit. [#1247](https://github.com/MichMich/MagicMirror/issues/1247) - Fixed issue where wind chill could not be displayed in Fahrenheit. [#1247](https://github.com/MichMich/MagicMirror/issues/1247)
- Fixed issues where a module crashes when it tries to dismiss a non existing alert. [#1240](https://github.com/MichMich/MagicMirror/issues/1240) - Fixed issues where a module crashes when it tries to dismiss a non existing alert. [#1240](https://github.com/MichMich/MagicMirror/issues/1240)
- In default module currentWeather/currentWeather.js line 296, 300, self.config.animationSpeed can not be found because the notificationReceived function does not have "self" variable. - In default module currentWeather/currentWeather.js line 296, 300, self.config.animationSpeed can not be found because the notificationReceived function does not have "self" variable.
@@ -353,6 +438,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Fixed issue where heat index and wind chill were reporting incorrect values in Kelvin. [#1263](https://github.com/MichMich/MagicMirror/issues/1263) - Fixed issue where heat index and wind chill were reporting incorrect values in Kelvin. [#1263](https://github.com/MichMich/MagicMirror/issues/1263)
### Updated ### Updated
- Updated Italian translation - Updated Italian translation
- Updated German translation - Updated German translation
- Updated Dutch translation - Updated Dutch translation
@@ -360,6 +446,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
## [2.3.1] - 2018-04-01 ## [2.3.1] - 2018-04-01
### Fixed ### Fixed
- Downgrade electron to 1.4.15 to solve the black screen issue.[#1243](https://github.com/MichMich/MagicMirror/issues/1243) - Downgrade electron to 1.4.15 to solve the black screen issue.[#1243](https://github.com/MichMich/MagicMirror/issues/1243)
## [2.3.0] - 2018-04-01 ## [2.3.0] - 2018-04-01
@@ -380,6 +467,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Add dc:date to parsing in newsfeed module, which allows parsing of more rss feeds. - Add dc:date to parsing in newsfeed module, which allows parsing of more rss feeds.
### Changed ### Changed
- Add link to GitHub repository which contains the respective Dockerfile. - Add link to GitHub repository which contains the respective Dockerfile.
- Optimized automated unit tests cloneObject, cmpVersions - Optimized automated unit tests cloneObject, cmpVersions
- Update notifications use now translation templates instead of normal strings. - Update notifications use now translation templates instead of normal strings.
@@ -387,6 +475,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Changed Electron dependency to v1.7.13. - Changed Electron dependency to v1.7.13.
### Fixed ### Fixed
- News article in fullscreen (iframe) is now shown in front of modules. - News article in fullscreen (iframe) is now shown in front of modules.
- Forecast respects maxNumberOfDays regardless of endpoint. - Forecast respects maxNumberOfDays regardless of endpoint.
- Fix exception on translation of objects. - Fix exception on translation of objects.
@@ -404,6 +493,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
## [2.2.1] - 2018-01-01 ## [2.2.1] - 2018-01-01
### Fixed ### Fixed
- Fixed linting errors. - Fixed linting errors.
## [2.2.0] - 2018-01-01 ## [2.2.0] - 2018-01-01
@@ -411,10 +501,12 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install` **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
### Changed ### Changed
- Calendar week is now handled with a variable translation in order to move number language specific. - Calendar week is now handled with a variable translation in order to move number language specific.
- Reverted the Electron dependency back to 1.4.15 since newer version don't seem to work on the Raspberry Pi very well. - Reverted the Electron dependency back to 1.4.15 since newer version don't seem to work on the Raspberry Pi very well.
### Added ### Added
- Add option to use [Nunjucks](https://mozilla.github.io/nunjucks/) templates in modules. (See `helloworld` module as an example.) - Add option to use [Nunjucks](https://mozilla.github.io/nunjucks/) templates in modules. (See `helloworld` module as an example.)
- Add Bulgarian translations for MagicMirror² and Alert module. - Add Bulgarian translations for MagicMirror² and Alert module.
- Add graceful shutdown of modules by calling `stop` function of each `node_helper` on SIGINT before exiting. - Add graceful shutdown of modules by calling `stop` function of each `node_helper` on SIGINT before exiting.
@@ -428,6 +520,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Add option for decimal symbols other than the decimal point for temperature values in both default weather modules: WeatherForecast and CurrentWeather. - Add option for decimal symbols other than the decimal point for temperature values in both default weather modules: WeatherForecast and CurrentWeather.
### Fixed ### Fixed
- Fixed issue with calendar module showing more than `maximumEntries` allows - Fixed issue with calendar module showing more than `maximumEntries` allows
- WeatherForecast and CurrentWeather are now using HTTPS instead of HTTP - WeatherForecast and CurrentWeather are now using HTTPS instead of HTTP
- Correcting translation for Indonesian language - Correcting translation for Indonesian language
@@ -438,9 +531,11 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install` **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
### Changed ### Changed
- Remove Roboto fonts files inside `fonts` and these are installed by npm install command. - Remove Roboto fonts files inside `fonts` and these are installed by npm install command.
### Added ### Added
- Add `clientonly` script to start only the electron client for a remote server. - Add `clientonly` script to start only the electron client for a remote server.
- Add symbol and color properties of event when `CALENDAR_EVENTS` notification is broadcasted from `default/calendar` module. - Add symbol and color properties of event when `CALENDAR_EVENTS` notification is broadcasted from `default/calendar` module.
- Add `.vscode/` folder to `.gitignore` to keep custom Visual Studio Code config out of git. - Add `.vscode/` folder to `.gitignore` to keep custom Visual Studio Code config out of git.
@@ -457,6 +552,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Add Slack badge to Readme. - Add Slack badge to Readme.
### Updated ### Updated
- Changed 'default.js' - listen on all attached interfaces by default. - Changed 'default.js' - listen on all attached interfaces by default.
- Add execution of `npm list` after the test are ran in Travis CI. - Add execution of `npm list` after the test are ran in Travis CI.
- Change hooks for the vendors e2e tests. - Change hooks for the vendors e2e tests.
@@ -465,6 +561,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Set version of the `express-ipfilter` on 0.3.1. - Set version of the `express-ipfilter` on 0.3.1.
### Fixed ### Fixed
- Fixed issue with incorrect alignment of analog clock when displayed in the center column of the MM. - Fixed issue with incorrect alignment of analog clock when displayed in the center column of the MM.
- Fixed ipWhitelist behaviour to make empty whitelist ([]) allow any and all hosts access to the MM. - Fixed ipWhitelist behaviour to make empty whitelist ([]) allow any and all hosts access to the MM.
- Fixed issue with calendar module where 'excludedEvents' count towards 'maximumEntries'. - Fixed issue with calendar module where 'excludedEvents' count towards 'maximumEntries'.
@@ -475,11 +572,13 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
## [2.1.2] - 2017-07-01 ## [2.1.2] - 2017-07-01
### Changed ### Changed
- Revert Docker related changes in favor of [docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror). All Docker images are outsourced. ([#856](https://github.com/MichMich/MagicMirror/pull/856)) - Revert Docker related changes in favor of [docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror). All Docker images are outsourced. ([#856](https://github.com/MichMich/MagicMirror/pull/856))
- Change Docker base image (Debian + Node) to an arm based distro (AlpineARM + Node) ([#846](https://github.com/MichMich/MagicMirror/pull/846)) - Change Docker base image (Debian + Node) to an arm based distro (AlpineARM + Node) ([#846](https://github.com/MichMich/MagicMirror/pull/846))
- Fix the dockerfile to have it running from the first time. - Fix the dockerfile to have it running from the first time.
### Added ### Added
- Add in option to wrap long calendar events to multiple lines using `wrapEvents` configuration option. - Add in option to wrap long calendar events to multiple lines using `wrapEvents` configuration option.
- Add test e2e `show title newsfeed` for newsfeed module. - Add test e2e `show title newsfeed` for newsfeed module.
- Add task to check configuration file. - Add task to check configuration file.
@@ -498,11 +597,13 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Added Romanian translation. - Added Romanian translation.
### Updated ### Updated
- Added missing keys to Polish translation. - Added missing keys to Polish translation.
- Added missing key to German translation. - Added missing key to German translation.
- Added better translation with flexible word order to Finnish translation. - Added better translation with flexible word order to Finnish translation.
### Fixed ### Fixed
- Fix instruction in README for using automatically installer script. - Fix instruction in README for using automatically installer script.
- Bug of duplicated compliments as described in [here](https://forum.magicmirror.builders/topic/2381/compliments-module-stops-cycling-compliments). - Bug of duplicated compliments as described in [here](https://forum.magicmirror.builders/topic/2381/compliments-module-stops-cycling-compliments).
- Fix double message about port when server is starting - Fix double message about port when server is starting
@@ -516,6 +617,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install` **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
### Changed ### Changed
- Add `anytime` group for Compliments module. - Add `anytime` group for Compliments module.
- Compliments module can use remoteFile without default daytime arrays defined. - Compliments module can use remoteFile without default daytime arrays defined.
- Installer: Use init config.js from config.js.sample. - Installer: Use init config.js from config.js.sample.
@@ -531,6 +633,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Restructured Test Suite. - Restructured Test Suite.
### Added ### Added
- Added Docker support (Pull Request [#673](https://github.com/MichMich/MagicMirror/pull/673)). - Added Docker support (Pull Request [#673](https://github.com/MichMich/MagicMirror/pull/673)).
- Calendar-specific support for `maximumEntries`, and `maximumNumberOfDays`. - Calendar-specific support for `maximumEntries`, and `maximumNumberOfDays`.
- Add loaded function to modules, providing an async callback. - Add loaded function to modules, providing an async callback.
@@ -573,6 +676,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Added a configurable Week section to the clock module. - Added a configurable Week section to the clock module.
### Fixed ### Fixed
- Update .gitignore to not ignore default modules folder. - Update .gitignore to not ignore default modules folder.
- Remove white flash on boot up. - Remove white flash on boot up.
- Added `update` in Raspberry Pi installation script. - Added `update` in Raspberry Pi installation script.
@@ -589,6 +693,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install` **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
### Added ### Added
- Finnish translation. - Finnish translation.
- Danish translation. - Danish translation.
- Turkish translation. - Turkish translation.
@@ -619,6 +724,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Add root_path for global vars - Add root_path for global vars
### Updated ### Updated
- Modified translations for Frysk. - Modified translations for Frysk.
- Modified core English translations. - Modified core English translations.
- Updated package.json as a result of Snyk security update. - Updated package.json as a result of Snyk security update.
@@ -629,6 +735,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Modules are now secure, and Helmet is now used to prevent abuse of the Mirror's API. - Modules are now secure, and Helmet is now used to prevent abuse of the Mirror's API.
### Fixed ### Fixed
- Solve an issue where module margins would appear when the first module of a section was hidden. - Solve an issue where module margins would appear when the first module of a section was hidden.
- Solved visual display errors on chrome, if all modules in one of the right sections are hidden. - Solved visual display errors on chrome, if all modules in one of the right sections are hidden.
- Global and Module default config values are no longer modified when setting config values. - Global and Module default config values are no longer modified when setting config values.
@@ -639,6 +746,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
## [2.0.5] - 2016-09-20 ## [2.0.5] - 2016-09-20
### Added ### Added
- Added ability to remove tags from the beginning or end of newsfeed items in 'newsfeed.js'. - Added ability to remove tags from the beginning or end of newsfeed items in 'newsfeed.js'.
- Added ability to define "the day after tomorrow" for calendar events (Definition for German and Dutch already included). - Added ability to define "the day after tomorrow" for calendar events (Definition for German and Dutch already included).
- Added CII Badge (we are compliant with the CII Best Practices) - Added CII Badge (we are compliant with the CII Best Practices)
@@ -646,11 +754,13 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Add the ability to turn off and on the date display in the Clock Module - Add the ability to turn off and on the date display in the Clock Module
### Fixed ### Fixed
- Fix typo in installer. - Fix typo in installer.
- Add message to unsupported Pi error to mention that Pi Zeros must use server only mode, as ARMv6 is unsupported. Closes #374. - Add message to unsupported Pi error to mention that Pi Zeros must use server only mode, as ARMv6 is unsupported. Closes #374.
- Fix API url for weather API. - Fix API url for weather API.
### Updated ### Updated
- Force fullscreen when kioskmode is active. - Force fullscreen when kioskmode is active.
- Update the .github templates and information with more modern information. - Update the .github templates and information with more modern information.
- Update the Gruntfile with a more functional StyleLint implementation. - Update the Gruntfile with a more functional StyleLint implementation.
@@ -658,6 +768,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
## [2.0.4] - 2016-08-07 ## [2.0.4] - 2016-08-07
### Added ### Added
- Brazilian Portuguese Translation. - Brazilian Portuguese Translation.
- Option to enable Kiosk mode. - Option to enable Kiosk mode.
- Added ability to start the app with Dev Tools. - Added ability to start the app with Dev Tools.
@@ -665,6 +776,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Greek Translation - Greek Translation
### Fixed ### Fixed
- Prevent `getModules()` selectors from returning duplicate entries. - Prevent `getModules()` selectors from returning duplicate entries.
- Append endpoints of weather modules with `/` to retrieve the correct data. (Issue [#337](https://github.com/MichMich/MagicMirror/issues/337)) - Append endpoints of weather modules with `/` to retrieve the correct data. (Issue [#337](https://github.com/MichMich/MagicMirror/issues/337))
- Corrected grammar in `module.js` from 'suspend' to 'suspended'. - Corrected grammar in `module.js` from 'suspend' to 'suspended'.
@@ -673,55 +785,72 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
- Fix issue where translation loading prevented the UI start-up when the language was set to 'en'. (Issue [#388](https://github.com/MichMich/MagicMirror/issues/388)) - Fix issue where translation loading prevented the UI start-up when the language was set to 'en'. (Issue [#388](https://github.com/MichMich/MagicMirror/issues/388))
### Updated ### Updated
- Updated package.json to fix possible vulnerabilities. (Using Snyk) - Updated package.json to fix possible vulnerabilities. (Using Snyk)
- Updated weathericons - Updated weathericons
- Updated default weatherforecast to work with the new icons. - Updated default weatherforecast to work with the new icons.
- More detailed error message in case config file couldn't be loaded. - More detailed error message in case config file couldn't be loaded.
## [2.0.3] - 2016-07-12 ## [2.0.3] - 2016-07-12
### Added ### Added
- Add max newsitems parameter to the newsfeed module. - Add max newsitems parameter to the newsfeed module.
- Translations for Simplified Chinese, Traditional Chinese and Japanese. - Translations for Simplified Chinese, Traditional Chinese and Japanese.
- Polish Translation - Polish Translation
- Add an analog clock in addition to the digital one. - Add an analog clock in addition to the digital one.
### Fixed ### Fixed
- Edit Alert Module to display title & message if they are provided in the notification (Issue [#300](https://github.com/MichMich/MagicMirror/issues/300)) - Edit Alert Module to display title & message if they are provided in the notification (Issue [#300](https://github.com/MichMich/MagicMirror/issues/300))
- Removed 'null' reference from updateModuleContent(). This fixes recent Edge and Internet Explorer browser displays (Issue [#319](https://github.com/MichMich/MagicMirror/issues/319)) - Removed 'null' reference from updateModuleContent(). This fixes recent Edge and Internet Explorer browser displays (Issue [#319](https://github.com/MichMich/MagicMirror/issues/319))
### Changed ### Changed
- Added default string to calendar titleReplace. - Added default string to calendar titleReplace.
## [2.0.2] - 2016-06-05 ## [2.0.2] - 2016-06-05
### Added ### Added
- Norwegian Translations (nb and nn) - Norwegian Translations (nb and nn)
- Portuguese Translation - Portuguese Translation
- Swedish Translation - Swedish Translation
### Fixed ### Fixed
- Added reference to Italian Translation. - Added reference to Italian Translation.
- Added the missing NE translation to all languages. [#344](https://github.com/MichMich/MagicMirror/issues/344) - Added the missing NE translation to all languages. [#344](https://github.com/MichMich/MagicMirror/issues/344)
- Added proper User-Agent string to calendar call. - Added proper User-Agent string to calendar call.
### Changed ### Changed
- Add option to use locationID in weather modules. - Add option to use locationID in weather modules.
## [2.0.1] - 2016-05-18 ## [2.0.1] - 2016-05-18
### Added ### Added
- Changelog - Changelog
- Italian Translation - Italian Translation
### Changed ### Changed
- Improve the installer by fetching the latest Node.js without any 3rd party interferences. - Improve the installer by fetching the latest Node.js without any 3rd party interferences.
## [2.0.0] - 2016-05-03 ## [2.0.0] - 2016-05-03
### Initial release of MagicMirror² ### Initial release of MagicMirror²
It includes (but is not limited to) the following features: It includes (but is not limited to) the following features:
- Modular system allowing 3rd party plugins. - Modular system allowing 3rd party plugins.
- An Node/Electron based application taking away the need for external servers or browsers. - An Node/Electron based application taking away the need for external servers or browsers.
- A complete development API documentation. - A complete development API documentation.
- Small cute fairies that kiss you while you sleep. - Small cute fairies that kiss you while you sleep.
## [1.0.0] - 2014-02-16 ## [1.0.0] - 2014-02-16
### Initial release of MagicMirror. ### Initial release of MagicMirror.
This was part of the blogpost: [https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the) This was part of the blogpost: [https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the)

View File

@@ -1,5 +1,4 @@
The MIT License (MIT) # The MIT License (MIT)
=====================
Copyright © 2016-2019 Michael Teeuw Copyright © 2016-2019 Michael Teeuw

View File

@@ -14,9 +14,11 @@
MagicMirror² focuses on a modular plugin system and uses [Electron](https://www.electronjs.org/) as an application wrapper. So no more web server or browser installs necessary! MagicMirror² focuses on a modular plugin system and uses [Electron](https://www.electronjs.org/) as an application wrapper. So no more web server or browser installs necessary!
## Documentation ## Documentation
For the full documentation including **[installation instructions](https://docs.magicmirror.builders/getting-started/installation.html)**, please visit our dedicated documentation website: [https://docs.magicmirror.builders](https://docs.magicmirror.builders). For the full documentation including **[installation instructions](https://docs.magicmirror.builders/getting-started/installation.html)**, please visit our dedicated documentation website: [https://docs.magicmirror.builders](https://docs.magicmirror.builders).
## Links ## Links
- Website: [https://magicmirror.builders](https://magicmirror.builders) - Website: [https://magicmirror.builders](https://magicmirror.builders)
- Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders) - Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders)
- Forum: [https://forum.magicmirror.builders](https://forum.magicmirror.builders) - Forum: [https://forum.magicmirror.builders](https://forum.magicmirror.builders)
@@ -28,7 +30,6 @@ For the full documentation including **[installation instructions](https://docs.
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 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)
## Enjoying MagicMirror? Consider a donation! ## Enjoying MagicMirror? Consider a donation!
MagicMirror² is opensource and free. That doesn't mean we don't need any money. MagicMirror² is opensource and free. That doesn't mean we don't need any money.

View File

@@ -4,10 +4,19 @@
(function () { (function () {
var config = {}; var config = {};
// Helper function to get server address/hostname from either the commandline or env /**
* Helper function to get server address/hostname from either the commandline or env
*/
function getServerAddress() { function getServerAddress() {
// Helper function to get command line parameters /**
// Assumes that a cmdline parameter is defined with `--key [value]` * Get command line parameters
* Assumes that a cmdline parameter is defined with `--key [value]`
*
* @param {string} key key to look for at the command line
* @param {string} defaultValue value if no key is given at the command line
*
* @returns {string} the value of the parameter
*/
function getCommandLineParameter(key, defaultValue = undefined) { function getCommandLineParameter(key, defaultValue = undefined) {
var index = process.argv.indexOf(`--${key}`); var index = process.argv.indexOf(`--${key}`);
var value = index > -1 ? process.argv[index + 1] : undefined; var value = index > -1 ? process.argv[index + 1] : undefined;
@@ -23,10 +32,17 @@
config["tls"] = process.argv.indexOf("--use-tls") > 0; config["tls"] = process.argv.indexOf("--use-tls") > 0;
} }
/**
* Gets the config from the specified server url
*
* @param {string} url location where the server is running.
*
* @returns {Promise} the config
*/
function getServerConfig(url) { function getServerConfig(url) {
// Return new pending promise // Return new pending promise
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Select http or https module, depending on reqested url // Select http or https module, depending on requested url
const lib = url.startsWith("https") ? require("https") : require("http"); const lib = url.startsWith("https") ? require("https") : require("http");
const request = lib.get(url, (response) => { const request = lib.get(url, (response) => {
var configData = ""; var configData = "";
@@ -47,6 +63,12 @@
}); });
} }
/**
* Print a message to the console in case of errors
*
* @param {string} [message] error message to print
* @param {number} code error code for the exit call
*/
function fail(message, code = 1) { function fail(message, code = 1) {
if (message !== undefined && typeof message === "string") { if (message !== undefined && typeof message === "string") {
console.log(message); console.log(message);
@@ -96,7 +118,6 @@
console.log(`There something wrong. The clientonly is not running code ${code}`); console.log(`There something wrong. The clientonly is not running code ${code}`);
} }
}); });
}) })
.catch(function (reason) { .catch(function (reason) {
fail(`Unable to connect to server: (${reason})`); fail(`Unable to connect to server: (${reason})`);
@@ -104,4 +125,4 @@
} else { } else {
fail(); fail();
} }
}()); })();

View File

@@ -28,6 +28,7 @@ var config = {
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
language: "en", language: "en",
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
timeFormat: 24, timeFormat: 24,
units: "metric", units: "metric",
// serverOnly: true/false/"local" , // serverOnly: true/false/"local" ,

View File

@@ -2,21 +2,14 @@
font-family: Roboto; font-family: Roboto;
font-style: normal; font-style: normal;
font-weight: 100; font-weight: 100;
src: src: local("Roboto Thin"), local("Roboto-Thin"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff");
local("Roboto Thin"),
local("Roboto-Thin"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff");
} }
@font-face { @font-face {
font-family: "Roboto Condensed"; font-family: "Roboto Condensed";
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: src: local("Roboto Condensed Light"), local("RobotoCondensed-Light"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff2") format("woff2"),
local("Roboto Condensed Light"),
local("RobotoCondensed-Light"),
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff") format("woff"); url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff") format("woff");
} }
@@ -24,10 +17,7 @@
font-family: "Roboto Condensed"; font-family: "Roboto Condensed";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: src: local("Roboto Condensed"), local("RobotoCondensed-Regular"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff2") format("woff2"),
local("Roboto Condensed"),
local("RobotoCondensed-Regular"),
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff") format("woff"); url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff") format("woff");
} }
@@ -35,10 +25,7 @@
font-family: "Roboto Condensed"; font-family: "Roboto Condensed";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: src: local("Roboto Condensed Bold"), local("RobotoCondensed-Bold"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff2") format("woff2"),
local("Roboto Condensed Bold"),
local("RobotoCondensed-Bold"),
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff") format("woff"); url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff") format("woff");
} }
@@ -46,42 +33,26 @@
font-family: Roboto; font-family: Roboto;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: src: local("Roboto"), local("Roboto-Regular"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff");
local("Roboto"),
local("Roboto-Regular"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff");
} }
@font-face { @font-face {
font-family: Roboto; font-family: Roboto;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: src: local("Roboto Medium"), local("Roboto-Medium"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff");
local("Roboto Medium"),
local("Roboto-Medium"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff");
} }
@font-face { @font-face {
font-family: Roboto; font-family: Roboto;
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: src: local("Roboto Bold"), local("Roboto-Bold"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff");
local("Roboto Bold"),
local("Roboto-Bold"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff");
} }
@font-face { @font-face {
font-family: Roboto; font-family: Roboto;
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: src: local("Roboto Light"), local("Roboto-Light"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff");
local("Roboto Light"),
local("Roboto-Light"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff");
} }

127
js/app.js
View File

@@ -5,20 +5,18 @@
* MIT Licensed. * MIT Licensed.
*/ */
var fs = require("fs"); var fs = require("fs");
var path = require("path");
var Log = require(__dirname + "/logger.js");
var Server = require(__dirname + "/server.js"); var Server = require(__dirname + "/server.js");
var Utils = require(__dirname + "/utils.js"); var Utils = require(__dirname + "/utils.js");
var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js"); var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js");
var path = require("path");
// Alias modules mentioned in package.js under _moduleAliases. // Alias modules mentioned in package.js under _moduleAliases.
require("module-alias/register"); require("module-alias/register");
// add timestamps in front of log messages
require("console-stamp")(console, "yyyy-mm-dd HH:MM:ss.l");
// Get version number. // Get version number.
global.version = JSON.parse(fs.readFileSync("package.json", "utf8")).version; global.version = JSON.parse(fs.readFileSync("package.json", "utf8")).version;
console.log("Starting MagicMirror: v" + global.version); Log.log("Starting MagicMirror: v" + global.version);
// global absolute root path // global absolute root path
global.root_path = path.resolve(__dirname + "/../"); global.root_path = path.resolve(__dirname + "/../");
@@ -36,31 +34,34 @@ if (process.env.MM_PORT) {
// The next part is here to prevent a major exception when there // The next part is here to prevent a major exception when there
// is no internet connection. This could probable be solved better. // is no internet connection. This could probable be solved better.
process.on("uncaughtException", function (err) { process.on("uncaughtException", function (err) {
console.log("Whoops! There was an uncaught exception..."); Log.error("Whoops! There was an uncaught exception...");
console.error(err); Log.error(err);
console.log("MagicMirror will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?"); Log.error("MagicMirror will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
console.log("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues"); Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
}); });
/* App - The core app. /**
* The core app.
*
* @class
*/ */
var App = function () { var App = function () {
var nodeHelpers = []; var nodeHelpers = [];
/* loadConfig(callback) /**
* Loads the config file. combines it with the defaults, * Loads the config file. Combines it with the defaults, and runs the
* and runs the callback with the found config as argument. * callback with the found config as argument.
* *
* argument callback function - The callback function. * @param {Function} callback Function to be called after loading the config
*/ */
var loadConfig = function (callback) { var loadConfig = function (callback) {
console.log("Loading config ..."); Log.log("Loading config ...");
var defaults = require(__dirname + "/defaults.js"); var defaults = require(__dirname + "/defaults.js");
// For this check proposed to TestSuite // For this check proposed to TestSuite
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8 // https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
var configFilename = path.resolve(global.root_path + "/config/config.js"); var configFilename = path.resolve(global.root_path + "/config/config.js");
if (typeof(global.configuration_file) !== "undefined") { if (typeof global.configuration_file !== "undefined") {
configFilename = path.resolve(global.configuration_file); configFilename = path.resolve(global.configuration_file);
} }
@@ -72,16 +73,22 @@ var App = function() {
callback(config); callback(config);
} catch (e) { } catch (e) {
if (e.code === "ENOENT") { if (e.code === "ENOENT") {
console.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration.")); 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) { } else if (e instanceof ReferenceError || e instanceof SyntaxError) {
console.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 { } else {
console.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); callback(defaults);
} }
}; };
/**
* Checks the config for deprecated options and throws a warning in the logs
* if it encounters one option from the deprecated.js list
*
* @param {object} userConfig The user config
*/
var checkDeprecatedOptions = function (userConfig) { var checkDeprecatedOptions = function (userConfig) {
var deprecated = require(global.root_path + "/js/deprecated.js"); var deprecated = require(global.root_path + "/js/deprecated.js");
var deprecatedOptions = deprecated.configs; var deprecatedOptions = deprecated.configs;
@@ -94,21 +101,17 @@ var App = function() {
} }
}); });
if (usedDeprecated.length > 0) { if (usedDeprecated.length > 0) {
console.warn(Utils.colors.warn( 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."));
"WARNING! Your config is using deprecated options: " +
usedDeprecated.join(", ") +
". Check README and CHANGELOG for more up-to-date ways of getting the same functionality.")
);
} }
}; };
/* loadModule(module) /**
* Loads a specific module. * Loads a specific module.
* *
* argument module string - The name of the module (including subpath). * @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 loadModule = function (module, callback) {
var elements = module.split("/"); var elements = module.split("/");
var moduleName = elements[elements.length - 1]; var moduleName = elements[elements.length - 1];
var moduleFolder = __dirname + "/../modules/" + module; var moduleFolder = __dirname + "/../modules/" + module;
@@ -124,7 +127,7 @@ var App = function() {
fs.accessSync(helperPath, fs.R_OK); fs.accessSync(helperPath, fs.R_OK);
} catch (e) { } catch (e) {
loadModule = false; loadModule = false;
console.log("No helper found for module: " + moduleName + "."); Log.log("No helper found for module: " + moduleName + ".");
} }
if (loadModule) { if (loadModule) {
@@ -132,11 +135,11 @@ var App = function() {
var m = new Module(); var m = new Module();
if (m.requiresVersion) { if (m.requiresVersion) {
console.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) { if (cmpVersions(global.version, m.requiresVersion) >= 0) {
console.log("Version is ok!"); Log.log("Version is ok!");
} else { } else {
console.log("Version is incorrect. Skip module: '" + moduleName + "'"); Log.log("Version is incorrect. Skip module: '" + moduleName + "'");
return; return;
} }
} }
@@ -151,13 +154,14 @@ var App = function() {
} }
}; };
/* loadModules(modules) /**
* Loads all modules. * Loads all modules.
* *
* argument module string - The name of the module (including subpath). * @param {Module[]} modules All modules to be loaded
* @param {Function} callback Function to be called after loading
*/ */
var loadModules = function (modules, callback) { var loadModules = function (modules, callback) {
console.log("Loading module helpers ..."); Log.log("Loading module helpers ...");
var loadNextModule = function () { var loadNextModule = function () {
if (modules.length > 0) { if (modules.length > 0) {
@@ -168,7 +172,7 @@ var App = function() {
}); });
} else { } else {
// All modules are loaded // All modules are loaded
console.log("All module helpers loaded."); Log.log("All module helpers loaded.");
callback(); callback();
} }
}; };
@@ -176,11 +180,14 @@ var App = function() {
loadNextModule(); loadNextModule();
}; };
/* cmpVersions(a,b) /**
* Compare two semantic version numbers and return the difference. * Compare two semantic version numbers and return the difference.
* *
* argument a string - Version number a. * @param {string} a Version number a.
* argument a string - Version number b. * @param {string} b Version number b.
*
* @returns {number} A positive number if a is larger than b, a negative
* number if a is smaller and 0 if they are the same
*/ */
function cmpVersions(a, b) { function cmpVersions(a, b) {
var i, diff; var i, diff;
@@ -198,18 +205,20 @@ var App = function() {
return segmentsA.length - segmentsB.length; return segmentsA.length - segmentsB.length;
} }
/* start(callback) /**
* This methods starts the core app. * Start the core app.
* It loads the config, then it loads all modules.
* When it's done it executes the callback with the config as argument.
* *
* argument callback function - The callback function. * It loads the config, then it loads all modules. When it's done it
* executes the callback with the config as argument.
*
* @param {Function} callback Function to be called after start
*/ */
this.start = function (callback) { this.start = function (callback) {
loadConfig(function (c) { loadConfig(function (c) {
config = c; config = c;
Log.setLogLevel(config.logLevel);
var modules = []; var modules = [];
for (var m in config.modules) { for (var m in config.modules) {
@@ -221,7 +230,7 @@ var App = function() {
loadModules(modules, function () { loadModules(modules, function () {
var server = new Server(config, function (app, io) { var server = new Server(config, function (app, io) {
console.log("Server started ..."); Log.log("Server started ...");
for (var h in nodeHelpers) { for (var h in nodeHelpers) {
var nodeHelper = nodeHelpers[h]; var nodeHelper = nodeHelpers[h];
@@ -230,7 +239,7 @@ var App = function() {
nodeHelper.start(); nodeHelper.start();
} }
console.log("Sockets connected & modules started ..."); Log.log("Sockets connected & modules started ...");
if (typeof callback === "function") { if (typeof callback === "function") {
callback(config); callback(config);
@@ -240,9 +249,10 @@ var App = function() {
}); });
}; };
/* stop() /**
* This methods stops the core app. * Stops the core app. This calls each node_helper's STOP() function, if it
* This calls each node_helper's STOP() function, if it exists. * exists.
*
* Added to fix #1056 * Added to fix #1056
*/ */
this.stop = function () { this.stop = function () {
@@ -254,24 +264,31 @@ var App = function() {
} }
}; };
/* Listen for SIGINT signal and call stop() function. /**
* Listen for SIGINT signal and call stop() function.
* *
* Added to fix #1056 * Added to fix #1056
* Note: this is only used if running `server-only`. Otherwise * Note: this is only used if running `server-only`. Otherwise
* this.stop() is called by app.on("before-quit"... in `electron.js` * this.stop() is called by app.on("before-quit"... in `electron.js`
*/ */
process.on("SIGINT", () => { process.on("SIGINT", () => {
console.log("[SIGINT] Received. Shutting down server..."); Log.log("[SIGINT] Received. Shutting down server...");
setTimeout(() => { process.exit(0); }, 3000); // Force quit after 3 seconds setTimeout(() => {
process.exit(0);
}, 3000); // Force quit after 3 seconds
this.stop(); this.stop();
process.exit(0); process.exit(0);
}); });
/* We also need to listen to SIGTERM signals so we stop everything when we are asked to stop by the OS. /**
* Listen to SIGTERM signals so we can stop everything when we
* are asked to stop by the OS.
*/ */
process.on("SIGTERM", () => { process.on("SIGTERM", () => {
console.log("[SIGTERM] Received. Shutting down server..."); Log.log("[SIGTERM] Received. Shutting down server...");
setTimeout(() => { process.exit(0); }, 3000); // Force quit after 3 seconds setTimeout(() => {
process.exit(0);
}, 3000); // Force quit after 3 seconds
this.stop(); this.stop();
process.exit(0); process.exit(0);
}); });

View File

@@ -1,14 +1,10 @@
/* Magic Mirror /* Magic Mirror
* *
* Checker configuration file * Check the configuration file for errors
*
* By Rodrigo Ramírez Norambuena
* https://rodrigoramirez.com
* *
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed. * MIT Licensed.
*
*/ */
const Linter = require("eslint").Linter; const Linter = require("eslint").Linter;
const linter = new Linter(); const linter = new Linter();
@@ -16,12 +12,14 @@ const path = require("path");
const fs = require("fs"); const fs = require("fs");
const rootPath = path.resolve(__dirname + "/../"); const rootPath = path.resolve(__dirname + "/../");
const config = require(rootPath + "/.eslintrc.json"); const Log = require(rootPath + "/js/logger.js");
const Utils = require(rootPath + "/js/utils.js"); const Utils = require(rootPath + "/js/utils.js");
/* getConfigFile() /**
* Return string with path of configuration file * Returns a string with path of configuration file.
* Check if set by environment variable MM_CONFIG_FILE * Check if set by environment variable MM_CONFIG_FILE
*
* @returns {string} path and filename of the config file
*/ */
function getConfigFile() { function getConfigFile() {
// FIXME: This function should be in core. Do you want refactor me ;) ?, be good! // FIXME: This function should be in core. Do you want refactor me ;) ?, be good!
@@ -32,35 +30,42 @@ function getConfigFile() {
return configFileName; return configFileName;
} }
/**
* Checks the config file using eslint.
*/
function checkConfigFile() { function checkConfigFile() {
const configFileName = getConfigFile(); const configFileName = getConfigFile();
// Check if file is present // Check if file is present
if (fs.existsSync(configFileName) === false) { if (fs.existsSync(configFileName) === false) {
console.error(Utils.colors.error("File not found: "), configFileName); Log.error(Utils.colors.error("File not found: "), configFileName);
return; throw new Error("No config file present!");
} }
// check permission
// Check permission
try { try {
fs.accessSync(configFileName, fs.F_OK); fs.accessSync(configFileName, fs.F_OK);
} catch (e) { } catch (e) {
console.log(Utils.colors.error(e)); Log.log(Utils.colors.error(e));
return; throw new Error("No permission to access config file!");
} }
// Validate syntax of the configuration file. // Validate syntax of the configuration file.
// In case the there errors show messages and Log.info(Utils.colors.info("Checking file... "), configFileName);
// return
console.info(Utils.colors.info("Checking file... "), configFileName);
// I'm not sure if all ever is utf-8 // I'm not sure if all ever is utf-8
fs.readFile(configFileName, "utf-8", function (err, data) { fs.readFile(configFileName, "utf-8", function (err, data) {
if (err) { throw err; } if (err) {
const messages = linter.verify(data, config); throw err;
}
const messages = linter.verify(data);
if (messages.length === 0) { if (messages.length === 0) {
console.log("Your configuration file doesn't contain syntax errors :)"); Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
return true;
} else { } else {
messages.forEach(error => { Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
console.log("Line", error.line, "col", error.column, error.message); // In case the there errors show messages and return
messages.forEach((error) => {
Log.error("Line", error.line, "col", error.column, error.message);
}); });
} }
}); });

View File

@@ -9,7 +9,11 @@
*/ */
(function () { (function () {
var initializing = false; var initializing = false;
var fnTest = /xyz/.test(function () { xyz; }) ? /\b_super\b/ : /.*/; var fnTest = /xyz/.test(function () {
xyz;
})
? /\b_super\b/
: /.*/;
// The base Class implementation (does nothing) // The base Class implementation (does nothing)
this.Class = function () {}; this.Class = function () {};
@@ -32,8 +36,9 @@
// Copy the properties over onto the new prototype // Copy the properties over onto the new prototype
for (var name in prop) { for (var name in prop) {
// Check if we're overwriting an existing function // Check if we're overwriting an existing function
prototype[name] = typeof prop[name] === "function" && prototype[name] =
typeof _super[name] === "function" && fnTest.test(prop[name]) ? (function (name, fn) { typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name])
? (function (name, fn) {
return function () { return function () {
var tmp = this._super; var tmp = this._super;
@@ -48,10 +53,13 @@
return ret; return ret;
}; };
})(name, prop[name]) : prop[name]; })(name, prop[name])
: prop[name];
} }
// The dummy class constructor /**
* The dummy class constructor
*/
function Class() { function Class() {
// All construction is actually done in the init method // All construction is actually done in the init method
if (!initializing && this.init) { if (!initializing && this.init) {
@@ -72,8 +80,13 @@
}; };
})(); })();
//Define the clone method for later use. /**
//Helper Method * Define the clone method for later use. Helper Method.
*
* @param {object} obj Object to be cloned
*
* @returns {object} the cloned object
*/
function cloneObject(obj) { function cloneObject(obj) {
if (obj === null || typeof obj !== "object") { if (obj === null || typeof obj !== "object") {
return obj; return obj;

View File

@@ -8,7 +8,7 @@
*/ */
var address = "localhost"; var address = "localhost";
var port = 8080; var port = 8080;
if (typeof(mmPort) !== "undefined") { if (typeof mmPort !== "undefined") {
port = mmPort; port = mmPort;
} }
var defaults = { var defaults = {
@@ -68,14 +68,16 @@ var defaults = {
config: { config: {
text: "www.michaelteeuw.nl" text: "www.michaelteeuw.nl"
} }
}, }
], ],
paths: { paths: {
modules: "modules", modules: "modules",
vendor: "vendor" vendor: "vendor"
}, }
}; };
/*************** DO NOT EDIT THE LINE BELOW ***************/ /*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = defaults;} if (typeof module !== "undefined") {
module.exports = defaults;
}

View File

@@ -7,8 +7,10 @@
*/ */
var deprecated = { var deprecated = {
configs: ["kioskmode"], configs: ["kioskmode"]
}; };
/*************** DO NOT EDIT THE LINE BELOW ***************/ /*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = deprecated;} if (typeof module !== "undefined") {
module.exports = deprecated;
}

View File

@@ -1,7 +1,8 @@
"use strict"; "use strict";
const electron = require("electron"); const electron = require("electron");
const core = require(__dirname + "/app.js"); const core = require("./app.js");
const Log = require("./logger.js");
// Config // Config
var config = process.env.config ? JSON.parse(process.env.config) : {}; var config = process.env.config ? JSON.parse(process.env.config) : {};
@@ -14,6 +15,9 @@ const BrowserWindow = electron.BrowserWindow;
// be closed automatically when the JavaScript object is garbage collected. // be closed automatically when the JavaScript object is garbage collected.
let mainWindow; let mainWindow;
/**
*
*/
function createWindow() { function createWindow() {
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
var electronOptionsDefaults = { var electronOptionsDefaults = {
@@ -86,7 +90,7 @@ function createWindow() {
// This method will be called when Electron has finished // This method will be called when Electron has finished
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
app.on("ready", function () { app.on("ready", function () {
console.log("Launching application."); Log.log("Launching application.");
createWindow(); createWindow();
}); });
@@ -110,9 +114,11 @@ app.on("activate", function() {
* core.stop() is called by process.on("SIGINT"... in `app.js` * core.stop() is called by process.on("SIGINT"... in `app.js`
*/ */
app.on("before-quit", (event) => { app.on("before-quit", (event) => {
console.log("Shutting down server..."); Log.log("Shutting down server...");
event.preventDefault(); event.preventDefault();
setTimeout(() => { process.exit(0); }, 3000); // Force-quit after 3 seconds. setTimeout(() => {
process.exit(0);
}, 3000); // Force-quit after 3 seconds.
core.stop(); core.stop();
process.exit(0); process.exit(0);
}); });

View File

@@ -7,7 +7,6 @@
* MIT Licensed. * MIT Licensed.
*/ */
var Loader = (function () { var Loader = (function () {
/* Create helper variables */ /* Create helper variables */
var loadedModuleFiles = []; var loadedModuleFiles = [];
@@ -16,11 +15,10 @@ var Loader = (function() {
/* Private Methods */ /* Private Methods */
/* loadModules() /**
* Loops thru all modules and requests load for every module. * Loops thru all modules and requests load for every module.
*/ */
var loadModules = function () { var loadModules = function () {
var moduleData = getModuleData(); var moduleData = getModuleData();
var loadNextModule = function () { var loadNextModule = function () {
@@ -39,14 +37,13 @@ var Loader = (function() {
// custom.css loaded. Start all modules. // custom.css loaded. Start all modules.
startModules(); startModules();
}); });
} }
}; };
loadNextModule(); loadNextModule();
}; };
/* startModules() /**
* Loops thru all modules and requests start for every module. * Loops thru all modules and requests start for every module.
*/ */
var startModules = function () { var startModules = function () {
@@ -59,19 +56,19 @@ var Loader = (function() {
MM.modulesStarted(moduleObjects); MM.modulesStarted(moduleObjects);
}; };
/* getAllModules() /**
* Retrieve list of all modules. * Retrieve list of all modules.
* *
* return array - module data as configured in config * @returns {object[]} module data as configured in config
*/ */
var getAllModules = function () { var getAllModules = function () {
return config.modules; return config.modules;
}; };
/* getModuleData() /**
* Generate array with module information including module paths. * Generate array with module information including module paths.
* *
* return array - Module information. * @returns {object[]} Module information.
*/ */
var getModuleData = function () { var getModuleData = function () {
var modules = getAllModules(); var modules = getAllModules();
@@ -102,18 +99,18 @@ var Loader = (function() {
position: moduleData.position, position: moduleData.position,
header: moduleData.header, header: moduleData.header,
config: moduleData.config, config: moduleData.config,
classes: (typeof moduleData.classes !== "undefined") ? moduleData.classes + " " + module : module classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module
}); });
} }
return moduleFiles; return moduleFiles;
}; };
/* loadModule(module) /**
* Load modules via ajax request and create module objects. * Load modules via ajax request and create module objects.s
* *
* argument callback function - Function called when done. * @param {object} module Information about the module we want to load.
* argument module object - Information about the module we want to load. * @param {Function} callback Function called when done.
*/ */
var loadModule = function (module, callback) { var loadModule = function (module, callback) {
var url = module.path + "/" + module.file; var url = module.path + "/" + module.file;
@@ -139,12 +136,12 @@ var Loader = (function() {
} }
}; };
/* bootstrapModule(module, mObj) /**
* Bootstrap modules by setting the module data and loading the scripts & styles. * Bootstrap modules by setting the module data and loading the scripts & styles.
* *
* argument module object - Information about the module we want to load. * @param {object} module Information about the module we want to load.
* argument mObj object - Modules instance. * @param {Module} mObj Modules instance.
* argument callback function - Function called when done. * @param {Function} callback Function called when done.
*/ */
var bootstrapModule = function (module, mObj, callback) { var bootstrapModule = function (module, mObj, callback) {
Log.info("Bootstrapping module: " + module.name); Log.info("Bootstrapping module: " + module.name);
@@ -164,14 +161,13 @@ var Loader = (function() {
}); });
}; };
/* loadFile(fileName) /**
* Load a script or stylesheet by adding it to the dom. * Load a script or stylesheet by adding it to the dom.
* *
* argument fileName string - Path of the file we want to load. * @param {string} fileName Path of the file we want to load.
* argument callback function - Function called when done. * @param {Function} callback Function called when done.
*/ */
var loadFile = function (fileName, callback) { var loadFile = function (fileName, callback) {
var extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1); var extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
switch (extension.toLowerCase()) { switch (extension.toLowerCase()) {
@@ -181,11 +177,15 @@ var Loader = (function() {
script.type = "text/javascript"; script.type = "text/javascript";
script.src = fileName; script.src = fileName;
script.onload = function () { script.onload = function () {
if (typeof callback === "function") {callback();} if (typeof callback === "function") {
callback();
}
}; };
script.onerror = function () { script.onerror = function () {
console.error("Error on loading script:", fileName); Log.error("Error on loading script:", fileName);
if (typeof callback === "function") {callback();} if (typeof callback === "function") {
callback();
}
}; };
document.getElementsByTagName("body")[0].appendChild(script); document.getElementsByTagName("body")[0].appendChild(script);
@@ -197,11 +197,15 @@ var Loader = (function() {
stylesheet.type = "text/css"; stylesheet.type = "text/css";
stylesheet.href = fileName; stylesheet.href = fileName;
stylesheet.onload = function () { stylesheet.onload = function () {
if (typeof callback === "function") {callback();} if (typeof callback === "function") {
callback();
}
}; };
stylesheet.onerror = function () { stylesheet.onerror = function () {
console.error("Error on loading stylesheet:", fileName); Log.error("Error on loading stylesheet:", fileName);
if (typeof callback === "function") {callback();} if (typeof callback === "function") {
callback();
}
}; };
document.getElementsByTagName("head")[0].appendChild(stylesheet); document.getElementsByTagName("head")[0].appendChild(stylesheet);
@@ -211,24 +215,22 @@ var Loader = (function() {
/* Public Methods */ /* Public Methods */
return { return {
/**
/* loadModules()
* Load all modules as defined in the config. * Load all modules as defined in the config.
*/ */
loadModules: function () { loadModules: function () {
loadModules(); loadModules();
}, },
/* loadFile() /**
* Load a file (script or stylesheet). * Load a file (script or stylesheet).
* Prevent double loading and search for files in the vendor folder. * Prevent double loading and search for files in the vendor folder.
* *
* argument fileName string - Path of the file we want to load. * @param {string} fileName Path of the file we want to load.
* argument module Module Object - the module that calls the loadFile function. * @param {Module} module The module that calls the loadFile function.
* argument callback function - Function called when done. * @param {Function} callback Function called when done.
*/ */
loadFile: function (fileName, module, callback) { loadFile: function (fileName, module, callback) {
if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) { if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) {
Log.log("File already loaded: " + fileName); Log.log("File already loaded: " + fileName);
callback(); callback();

View File

@@ -1,13 +1,25 @@
/* Magic Mirror /* Magic Mirror
* Logger * Log
*
* This logger is very simple, but needs to be extended. * This logger is very simple, but needs to be extended.
* This system can eventually be used to push the log messages to an external target. * This system can eventually be used to push the log messages to an external target.
* *
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
const Log = (function() { (function (root, factory) {
return { if (typeof exports === "object") {
// add timestamps in front of log messages
require("console-stamp")(console, "yyyy-mm-dd HH:MM:ss.l");
// Node, CommonJS-like
module.exports = factory(root.config);
} else {
// Browser globals (root is window)
root.Log = factory(root.config);
}
})(this, function (config) {
const logLevel = {
info: Function.prototype.bind.call(console.info, console), info: Function.prototype.bind.call(console.info, console),
log: Function.prototype.bind.call(console.log, console), log: Function.prototype.bind.call(console.log, console),
error: Function.prototype.bind.call(console.error, console), error: Function.prototype.bind.call(console.error, console),
@@ -19,4 +31,16 @@ const Log = (function() {
timeEnd: Function.prototype.bind.call(console.timeEnd, console), timeEnd: Function.prototype.bind.call(console.timeEnd, console),
timeStamp: Function.prototype.bind.call(console.timeStamp, console) timeStamp: Function.prototype.bind.call(console.timeStamp, console)
}; };
})();
logLevel.setLogLevel = function (newLevel) {
if (newLevel) {
Object.keys(logLevel).forEach(function (key, index) {
if (!newLevel.includes(key.toLocaleUpperCase())) {
logLevel[key] = function () {};
}
});
}
};
return logLevel;
});

View File

@@ -7,14 +7,12 @@
* MIT Licensed. * MIT Licensed.
*/ */
var MM = (function () { var MM = (function () {
var modules = []; var modules = [];
/* Private Methods */ /* Private Methods */
/* createDomObjects() /**
* Create dom objects for all modules that * Create dom objects for all modules that are configured for a specific position.
* are configured for a specific position.
*/ */
var createDomObjects = function () { var createDomObjects = function () {
var domCreationPromises = []; var domCreationPromises = [];
@@ -43,7 +41,9 @@ var MM = (function() {
dom.appendChild(moduleHeader); dom.appendChild(moduleHeader);
if (typeof module.getHeader() === "undefined" || module.getHeader() !== "") { if (typeof module.getHeader() === "undefined" || module.getHeader() !== "") {
moduleHeader.style = "display: none;"; moduleHeader.style.display = "none;";
} else {
moduleHeader.style.display = "block;";
} }
var moduleContent = document.createElement("div"); var moduleContent = document.createElement("div");
@@ -52,9 +52,11 @@ var MM = (function() {
var domCreationPromise = updateDom(module, 0); var domCreationPromise = updateDom(module, 0);
domCreationPromises.push(domCreationPromise); domCreationPromises.push(domCreationPromise);
domCreationPromise.then(function() { domCreationPromise
.then(function () {
sendNotification("MODULE_DOM_CREATED", null, null, module); sendNotification("MODULE_DOM_CREATED", null, null, module);
}).catch(Log.error); })
.catch(Log.error);
}); });
updateWrapperStates(); updateWrapperStates();
@@ -64,10 +66,12 @@ var MM = (function() {
}); });
}; };
/* selectWrapper(position) /**
* Select the wrapper dom object for a specific position. * Select the wrapper dom object for a specific position.
* *
* argument position string - The name of the position. * @param {string} position The name of the position.
*
* @returns {HTMLElement} the wrapper element
*/ */
var selectWrapper = function (position) { var selectWrapper = function (position) {
var classes = position.replace("_", " "); var classes = position.replace("_", " ");
@@ -80,13 +84,13 @@ var MM = (function() {
} }
}; };
/* sendNotification(notification, payload, sender) /**
* Send a notification to all modules. * Send a notification to all modules.
* *
* argument notification string - The identifier of the notification. * @param {string} notification The identifier of the notification.
* argument payload mixed - The payload of the notification. * @param {*} payload The payload of the notification.
* argument sender Module - The module that sent the notification. * @param {Module} sender The module that sent the notification.
* argument sendTo Module - The module to send the notification to. (optional) * @param {Module} [sendTo] The (optional) module to send the notification to.
*/ */
var sendNotification = function (notification, payload, sender, sendTo) { var sendNotification = function (notification, payload, sender, sendTo) {
for (var m in modules) { for (var m in modules) {
@@ -97,13 +101,13 @@ var MM = (function() {
} }
}; };
/* updateDom(module, speed) /**
* Update the dom for a specific module. * Update the dom for a specific module.
* *
* argument module Module - The module that needs an update. * @param {Module} module The module that needs an update.
* argument speed Number - The number of microseconds for the animation. (optional) * @param {number} [speed] The (optional) number of microseconds for the animation.
* *
* return Promise - Resolved when the dom is fully updated. * @returns {Promise} Resolved when the dom is fully updated.
*/ */
var updateDom = function (module, speed) { var updateDom = function (module, speed) {
return new Promise(function (resolve) { return new Promise(function (resolve) {
@@ -115,23 +119,25 @@ var MM = (function() {
newContentPromise = Promise.resolve(newContentPromise); newContentPromise = Promise.resolve(newContentPromise);
} }
newContentPromise.then(function(newContent) { newContentPromise
.then(function (newContent) {
var updatePromise = updateDomWithContent(module, speed, newHeader, newContent); var updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
updatePromise.then(resolve).catch(Log.error); updatePromise.then(resolve).catch(Log.error);
}).catch(Log.error); })
.catch(Log.error);
}); });
}; };
/* updateDomWithContent(module, speed, newHeader, newContent) /**
* Update the dom with the specified content * Update the dom with the specified content
* *
* argument module Module - The module that needs an update. * @param {Module} module The module that needs an update.
* argument speed Number - The number of microseconds for the animation. (optional) * @param {number} [speed] The (optional) number of microseconds for the animation.
* argument newHeader String - The new header that is generated. * @param {string} newHeader The new header that is generated.
* argument newContent Domobject - The new content that is generated. * @param {HTMLElement} newContent The new content that is generated.
* *
* return Promise - Resolved when the module dom has been updated. * @returns {Promise} Resolved when the module dom has been updated.
*/ */
var updateDomWithContent = function (module, speed, newHeader, newContent) { var updateDomWithContent = function (module, speed, newHeader, newContent) {
return new Promise(function (resolve) { return new Promise(function (resolve) {
@@ -162,14 +168,14 @@ var MM = (function() {
}); });
}; };
/* moduleNeedsUpdate(module, newContent) /**
* Check if the content has changed. * Check if the content has changed.
* *
* argument module Module - The module to check. * @param {Module} module The module to check.
* argument newHeader String - The new header that is generated. * @param {string} newHeader The new header that is generated.
* argument newContent Domobject - The new content that is generated. * @param {HTMLElement} newContent The new content that is generated.
* *
* return bool - Does the module need an update? * @returns {boolean} True if the module need an update, false otherwise
*/ */
var moduleNeedsUpdate = function (module, newHeader, newContent) { var moduleNeedsUpdate = function (module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier); var moduleWrapper = document.getElementById(module.identifier);
@@ -194,16 +200,18 @@ var MM = (function() {
return headerNeedsUpdate || contentNeedsUpdate; return headerNeedsUpdate || contentNeedsUpdate;
}; };
/* moduleNeedsUpdate(module, newContent) /**
* Update the content of a module on screen. * Update the content of a module on screen.
* *
* argument module Module - The module to check. * @param {Module} module The module to check.
* argument newHeader String - The new header that is generated. * @param {string} newHeader The new header that is generated.
* argument newContent Domobject - The new content that is generated. * @param {HTMLElement} newContent The new content that is generated.
*/ */
var updateModuleContent = function (module, newHeader, newContent) { var updateModuleContent = function (module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier); var moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper === null) {return;} if (moduleWrapper === null) {
return;
}
var headerWrapper = moduleWrapper.getElementsByClassName("module-header"); var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
var contentWrapper = moduleWrapper.getElementsByClassName("module-content"); var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
@@ -211,15 +219,20 @@ var MM = (function() {
contentWrapper[0].appendChild(newContent); contentWrapper[0].appendChild(newContent);
headerWrapper[0].innerHTML = newHeader; headerWrapper[0].innerHTML = newHeader;
headerWrapper[0].style = headerWrapper.length > 0 && newHeader ? undefined : "display: none;"; if (headerWrapper.length > 0 && newHeader) {
headerWrapper[0].style.display = "block";
} else {
headerWrapper[0].style.display = "none";
}
}; };
/* hideModule(module, speed, callback) /**
* Hide the module. * Hide the module.
* *
* argument module Module - The module to hide. * @param {Module} module The module to hide.
* argument speed Number - The speed of the hide animation. * @param {number} speed The speed of the hide animation.
* argument callback function - Called when the animation is done. * @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the hide method.
*/ */
var hideModule = function (module, speed, callback, options) { var hideModule = function (module, speed, callback, options) {
options = options || {}; options = options || {};
@@ -247,20 +260,25 @@ var MM = (function() {
updateWrapperStates(); updateWrapperStates();
if (typeof callback === "function") { callback(); } if (typeof callback === "function") {
callback();
}
}, speed); }, speed);
} else { } else {
// invoke callback even if no content, issue 1308 // invoke callback even if no content, issue 1308
if (typeof callback === "function") { callback(); } if (typeof callback === "function") {
callback();
}
} }
}; };
/* showModule(module, speed, callback) /**
* Show the module. * Show the module.
* *
* argument module Module - The module to show. * @param {Module} module The module to show.
* argument speed Number - The speed of the show animation. * @param {number} speed The speed of the show animation.
* argument callback function - Called when the animation is done. * @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the show method.
*/ */
var showModule = function (module, speed, callback, options) { var showModule = function (module, speed, callback, options) {
options = options || {}; options = options || {};
@@ -302,15 +320,19 @@ var MM = (function() {
clearTimeout(module.showHideTimer); clearTimeout(module.showHideTimer);
module.showHideTimer = setTimeout(function () { module.showHideTimer = setTimeout(function () {
if (typeof callback === "function") { callback(); } if (typeof callback === "function") {
callback();
}
}, speed); }, speed);
} else { } else {
// invoke callback // invoke callback
if (typeof callback === "function") { callback(); } if (typeof callback === "function") {
callback();
}
} }
}; };
/* updateWrapperStates() /**
* Checks for all positions if it has visible content. * Checks for all positions if it has visible content.
* If not, if will hide the position to prevent unwanted margins. * If not, if will hide the position to prevent unwanted margins.
* This method should be called by the show and hide methods. * This method should be called by the show and hide methods.
@@ -339,8 +361,8 @@ var MM = (function() {
}); });
}; };
/* loadConfig() /**
* Loads the core config and combines it with de system defaults. * Loads the core config and combines it with the system defaults.
*/ */
var loadConfig = function () { var loadConfig = function () {
// FIXME: Think about how to pass config around without breaking tests // FIXME: Think about how to pass config around without breaking tests
@@ -355,42 +377,41 @@ var MM = (function() {
/* eslint-enable */ /* eslint-enable */
}; };
/* setSelectionMethodsForModules() /**
* Adds special selectors on a collection of modules. * Adds special selectors on a collection of modules.
* *
* argument modules array - Array of modules. * @param {Module[]} modules Array of modules.
*/ */
var setSelectionMethodsForModules = function (modules) { var setSelectionMethodsForModules = function (modules) {
/**
/* withClass(className) * Filter modules with the specified classes.
* calls modulesByClass to filter modules with the specified classes.
* *
* argument className string/array - one or multiple classnames. (array or space divided) * @param {string|string[]} className one or multiple classnames (array or space divided).
* *
* return array - Filtered collection of modules. * @returns {Module[]} Filtered collection of modules.
*/ */
var withClass = function (className) { var withClass = function (className) {
return modulesByClass(className, true); return modulesByClass(className, true);
}; };
/* exceptWithClass(className) /**
* calls modulesByClass to filter modules without the specified classes. * Filter modules without the specified classes.
* *
* argument className string/array - one or multiple classnames. (array or space divided) * @param {string|string[]} className one or multiple classnames (array or space divided).
* *
* return array - Filtered collection of modules. * @returns {Module[]} Filtered collection of modules.
*/ */
var exceptWithClass = function (className) { var exceptWithClass = function (className) {
return modulesByClass(className, false); return modulesByClass(className, false);
}; };
/* modulesByClass(className, include) /**
* filters a collection of modules based on classname(s). * Filters a collection of modules based on classname(s).
* *
* argument className string/array - one or multiple classnames. (array or space divided) * @param {string|string[]} className one or multiple classnames (array or space divided).
* argument include boolean - if the filter should include or exclude the modules with the specific classes. * @param {boolean} include if the filter should include or exclude the modules with the specific classes.
* *
* return array - Filtered collection of modules. * @returns {Module[]} Filtered collection of modules.
*/ */
var modulesByClass = function (className, include) { var modulesByClass = function (className, include) {
var searchClasses = className; var searchClasses = className;
@@ -415,12 +436,12 @@ var MM = (function() {
return newModules; return newModules;
}; };
/* exceptModule(module) /**
* Removes a module instance from the collection. * Removes a module instance from the collection.
* *
* argument module Module object - The module instance to remove from the collection. * @param {object} module The module instance to remove from the collection.
* *
* return array - Filtered collection of modules. * @returns {Module[]} Filtered collection of modules.
*/ */
var exceptModule = function (module) { var exceptModule = function (module) {
var newModules = modules.filter(function (mod) { var newModules = modules.filter(function (mod) {
@@ -431,10 +452,10 @@ var MM = (function() {
return newModules; return newModules;
}; };
/* enumerate(callback) /**
* Walks thru a collection of modules and executes the callback with the module as an argument. * Walks thru a collection of modules and executes the callback with the module as an argument.
* *
* argument callback function - The function to execute with the module as an argument. * @param {Function} callback The function to execute with the module as an argument.
*/ */
var enumerate = function (callback) { var enumerate = function (callback) {
modules.map(function (module) { modules.map(function (module) {
@@ -442,29 +463,40 @@ var MM = (function() {
}); });
}; };
if (typeof modules.withClass === "undefined") { Object.defineProperty(modules, "withClass", {value: withClass, enumerable: false}); } if (typeof modules.withClass === "undefined") {
if (typeof modules.exceptWithClass === "undefined") { Object.defineProperty(modules, "exceptWithClass", {value: exceptWithClass, enumerable: false}); } Object.defineProperty(modules, "withClass", { value: withClass, enumerable: false });
if (typeof modules.exceptModule === "undefined") { Object.defineProperty(modules, "exceptModule", {value: exceptModule, enumerable: false}); } }
if (typeof modules.enumerate === "undefined") { Object.defineProperty(modules, "enumerate", {value: enumerate, enumerable: false}); } if (typeof modules.exceptWithClass === "undefined") {
Object.defineProperty(modules, "exceptWithClass", { value: exceptWithClass, enumerable: false });
}
if (typeof modules.exceptModule === "undefined") {
Object.defineProperty(modules, "exceptModule", { value: exceptModule, enumerable: false });
}
if (typeof modules.enumerate === "undefined") {
Object.defineProperty(modules, "enumerate", { value: enumerate, enumerable: false });
}
}; };
return { return {
/* Public Methods */ /* Public Methods */
/* init() /**
* Main init method. * Main init method.
*/ */
init: function () { init: function () {
Log.info("Initializing MagicMirror."); Log.info("Initializing MagicMirror.");
loadConfig(); loadConfig();
Log.setLogLevel(config.logLevel);
Translator.loadCoreTranslations(config.language); Translator.loadCoreTranslations(config.language);
Loader.loadModules(); Loader.loadModules();
}, },
/* modulesStarted(moduleObjects) /**
* Gets called when all modules are started. * Gets called when all modules are started.
* *
* argument moduleObjects array<Module> - All module instances. * @param {Module[]} moduleObjects All module instances.
*/ */
modulesStarted: function (moduleObjects) { modulesStarted: function (moduleObjects) {
modules = []; modules = [];
@@ -479,12 +511,12 @@ var MM = (function() {
createDomObjects(); createDomObjects();
}, },
/* sendNotification(notification, payload, sender) /**
* Send a notification to all modules. * Send a notification to all modules.
* *
* argument notification string - The identifier of the notification. * @param {string} notification The identifier of the notification.
* argument payload mixed - The payload of the notification. * @param {*} payload The payload of the notification.
* argument sender Module - The module that sent the notification. * @param {Module} sender The module that sent the notification.
*/ */
sendNotification: function (notification, payload, sender) { sendNotification: function (notification, payload, sender) {
if (arguments.length < 3) { if (arguments.length < 3) {
@@ -506,11 +538,11 @@ var MM = (function() {
sendNotification(notification, payload, sender); sendNotification(notification, payload, sender);
}, },
/* updateDom(module, speed) /**
* Update the dom for a specific module. * Update the dom for a specific module.
* *
* argument module Module - The module that needs an update. * @param {Module} module The module that needs an update.
* argument speed Number - The number of microseconds for the animation. (optional) * @param {number} [speed] The number of microseconds for the animation.
*/ */
updateDom: function (module, speed) { updateDom: function (module, speed) {
if (!(module instanceof Module)) { if (!(module instanceof Module)) {
@@ -522,43 +554,42 @@ var MM = (function() {
updateDom(module, speed); updateDom(module, speed);
}, },
/* getModules(module, speed) /**
* Returns a collection of all modules currently active. * Returns a collection of all modules currently active.
* *
* return array - A collection of all modules currently active. * @returns {Module[]} A collection of all modules currently active.
*/ */
getModules: function () { getModules: function () {
setSelectionMethodsForModules(modules); setSelectionMethodsForModules(modules);
return modules; return modules;
}, },
/* hideModule(module, speed, callback) /**
* Hide the module. * Hide the module.
* *
* argument module Module - The module hide. * @param {Module} module The module to hide.
* argument speed Number - The speed of the hide animation. * @param {number} speed The speed of the hide animation.
* argument callback function - Called when the animation is done. * @param {Function} callback Called when the animation is done.
* argument options object - Optional settings for the hide method. * @param {object} [options] Optional settings for the hide method.
*/ */
hideModule: function (module, speed, callback, options) { hideModule: function (module, speed, callback, options) {
module.hidden = true; module.hidden = true;
hideModule(module, speed, callback, options); hideModule(module, speed, callback, options);
}, },
/* showModule(module, speed, callback) /**
* Show the module. * Show the module.
* *
* argument module Module - The module show. * @param {Module} module The module to show.
* argument speed Number - The speed of the show animation. * @param {number} speed The speed of the show animation.
* argument callback function - Called when the animation is done. * @param {Function} callback Called when the animation is done.
* argument options object - Optional settings for the hide method. * @param {object} [options] Optional settings for the show method.
*/ */
showModule: function (module, speed, callback, options) { showModule: function (module, speed, callback, options) {
// do not change module.hidden yet, only if we really show it later // do not change module.hidden yet, only if we really show it later
showModule(module, speed, callback, options); showModule(module, speed, callback, options);
} }
}; };
})(); })();
// Add polyfill for Object.assign. // Add polyfill for Object.assign.

View File

@@ -2,12 +2,13 @@
/* Magic Mirror /* Magic Mirror
* Module Blueprint. * Module Blueprint.
* @typedef {Object} Module
* *
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*
*/ */
var Module = Class.extend({ var Module = Class.extend({
/********************************************************* /*********************************************************
* All methods (and properties) below can be subclassed. * * All methods (and properties) below can be subclassed. *
*********************************************************/ *********************************************************/
@@ -30,53 +31,55 @@ var Module = Class.extend({
// Use the nunjucksEnvironment() to get it. // Use the nunjucksEnvironment() to get it.
_nunjucksEnvironment: null, _nunjucksEnvironment: null,
/* init() /**
* Is called when the module is instantiated. * Called when the module is instantiated.
*/ */
init: function () { init: function () {
//Log.log(this.defaults); //Log.log(this.defaults);
}, },
/* start() /**
* Is called when the module is started. * Called when the module is started.
*/ */
start: function () { start: function () {
Log.info("Starting module: " + this.name); Log.info("Starting module: " + this.name);
}, },
/* getScripts() /**
* Returns a list of scripts the module requires to be loaded. * Returns a list of scripts the module requires to be loaded.
* *
* return Array<String> - An array with filenames. * @returns {string[]} An array with filenames.
*/ */
getScripts: function () { getScripts: function () {
return []; return [];
}, },
/* getStyles() /**
* Returns a list of stylesheets the module requires to be loaded. * Returns a list of stylesheets the module requires to be loaded.
* *
* return Array<String> - An array with filenames. * @returns {string[]} An array with filenames.
*/ */
getStyles: function () { getStyles: function () {
return []; return [];
}, },
/* getTranslations() /**
* Returns a map of translation files the module requires to be loaded. * Returns a map of translation files the module requires to be loaded.
* *
* return Map<String, String> - A map with langKeys and filenames. * return Map<String, String> -
*
* @returns {*} A map with langKeys and filenames.
*/ */
getTranslations: function () { getTranslations: function () {
return false; return false;
}, },
/* getDom() /**
* This method generates the dom which needs to be displayed. This method is called by the Magic Mirror core. * Generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
* This method can to be subclassed if the module wants to display info on the mirror. * This method can to be subclassed if the module wants to display info on the mirror.
* Alternatively, the getTemplate method could be subclassed. * Alternatively, the getTemplate method could be subclassed.
* *
* return DomObject | Promise - The dom or a promise with the dom to display. * @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
*/ */
getDom: function () { getDom: function () {
var self = this; var self = this;
@@ -106,46 +109,45 @@ var Module = Class.extend({
}); });
}, },
/* getHeader() /**
* This method generates the header string which needs to be displayed if a user has a header configured for this module. * Generates the header string which needs to be displayed if a user has a header configured for this module.
* This method is called by the Magic Mirror core, but only if the user has configured a default header for the module. * This method is called by the Magic Mirror core, but only if the user has configured a default header for the module.
* This method needs to be subclassed if the module wants to display modified headers on the mirror. * This method needs to be subclassed if the module wants to display modified headers on the mirror.
* *
* return string - The header to display above the header. * @returns {string} The header to display above the header.
*/ */
getHeader: function () { getHeader: function () {
return this.data.header; return this.data.header;
}, },
/* getTemplate() /**
* This method returns the template for the module which is used by the default getDom implementation. * Returns the template for the module which is used by the default getDom implementation.
* This method needs to be subclassed if the module wants to use a template. * This method needs to be subclassed if the module wants to use a template.
* It can either return a template sting, or a template filename. * It can either return a template sting, or a template filename.
* If the string ends with '.html' it's considered a file from within the module's folder. * If the string ends with '.html' it's considered a file from within the module's folder.
* *
* return string - The template string of filename. * @returns {string} The template string of filename.
*/ */
getTemplate: function () { getTemplate: function () {
return "<div class=\"normal\">" + this.name + "</div><div class=\"small dimmed\">" + this.identifier + "</div>"; return '<div class="normal">' + this.name + '</div><div class="small dimmed">' + this.identifier + "</div>";
}, },
/* getTemplateData() /**
* This method returns the data to be used in the template. * Returns the data to be used in the template.
* This method needs to be subclassed if the module wants to use a custom data. * This method needs to be subclassed if the module wants to use a custom data.
* *
* return Object * @returns {object} The data for the template
*/ */
getTemplateData: function () { getTemplateData: function () {
return {}; return {};
}, },
/* notificationReceived(notification, payload, sender) /**
* This method is called when a notification arrives. * Called by the Magic Mirror core when a notification arrives.
* This method is called by the Magic Mirror core.
* *
* argument notification string - The identifier of the notification. * @param {string} notification The identifier of the notification.
* argument payload mixed - The payload of the notification. * @param {*} payload The payload of the notification.
* argument sender Module - The module that sent the notification. * @param {Module} sender The module that sent the notification.
*/ */
notificationReceived: function (notification, payload, sender) { notificationReceived: function (notification, payload, sender) {
if (sender) { if (sender) {
@@ -155,11 +157,11 @@ var Module = Class.extend({
} }
}, },
/** nunjucksEnvironment() /**
* Returns the nunjucks environment for the current module. * Returns the nunjucks environment for the current module.
* The environment is checked in the _nunjucksEnvironment instance variable. * The environment is checked in the _nunjucksEnvironment instance variable.
*
* @returns Nunjucks Environment * @returns {object} The Nunjucks Environment
*/ */
nunjucksEnvironment: function () { nunjucksEnvironment: function () {
if (this._nunjucksEnvironment !== null) { if (this._nunjucksEnvironment !== null) {
@@ -172,6 +174,7 @@ var Module = Class.extend({
trimBlocks: true, trimBlocks: true,
lstripBlocks: true lstripBlocks: true
}); });
this._nunjucksEnvironment.addFilter("translate", function (str) { this._nunjucksEnvironment.addFilter("translate", function (str) {
return self.translate(str); return self.translate(str);
}); });
@@ -179,25 +182,25 @@ var Module = Class.extend({
return this._nunjucksEnvironment; return this._nunjucksEnvironment;
}, },
/* socketNotificationReceived(notification, payload) /**
* This method is called when a socket notification arrives. * Called when a socket notification arrives.
* *
* argument notification string - The identifier of the notification. * @param {string} notification The identifier of the notification.
* argument payload mixed - The payload of the notification. * @param {*} payload The payload of the notification.
*/ */
socketNotificationReceived: function (notification, payload) { socketNotificationReceived: function (notification, payload) {
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload); Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
}, },
/* suspend() /*
* This method is called when a module is hidden. * Called when the module is hidden.
*/ */
suspend: function () { suspend: function () {
Log.log(this.name + " is suspended."); Log.log(this.name + " is suspended.");
}, },
/* resume() /*
* This method is called when a module is shown. * Called when the module is shown.
*/ */
resume: function () { resume: function () {
Log.log(this.name + " is resumed."); Log.log(this.name + " is resumed.");
@@ -207,10 +210,10 @@ var Module = Class.extend({
* The methods below don"t need subclassing. * * The methods below don"t need subclassing. *
*********************************************/ *********************************************/
/* setData(data) /**
* Set the module data. * Set the module data.
* *
* argument data object - Module data. * @param {Module} data The module data
*/ */
setData: function (data) { setData: function (data) {
this.data = data; this.data = data;
@@ -221,18 +224,20 @@ var Module = Class.extend({
this.setConfig(data.config); this.setConfig(data.config);
}, },
/* setConfig(config) /**
* Set the module config and combine it with the module defaults. * Set the module config and combine it with the module defaults.
* *
* argument config object - Module config. * @param {object} config The combined module config.
*/ */
setConfig: function (config) { setConfig: function (config) {
this.config = Object.assign({}, this.defaults, config); this.config = Object.assign({}, this.defaults, config);
}, },
/* socket() /**
* Returns a socket object. If it doesn't exist, it"s created. * Returns a socket object. If it doesn't exist, it's created.
* It also registers the notification callback. * It also registers the notification callback.
*
* @returns {MMSocket} a socket object
*/ */
socket: function () { socket: function () {
if (typeof this._socket === "undefined") { if (typeof this._socket === "undefined") {
@@ -247,40 +252,39 @@ var Module = Class.extend({
return this._socket; return this._socket;
}, },
/* file(file) /**
* Retrieve the path to a module file. * Retrieve the path to a module file.
* *
* argument file string - Filename. * @param {string} file Filename
* * @returns {string} the file path
* return string - File path.
*/ */
file: function (file) { file: function (file) {
return (this.data.path + "/" + file).replace("//", "/"); return (this.data.path + "/" + file).replace("//", "/");
}, },
/* loadStyles() /**
* Load all required stylesheets by requesting the MM object to load the files. * Load all required stylesheets by requesting the MM object to load the files.
* *
* argument callback function - Function called when done. * @param {Function} callback Function called when done.
*/ */
loadStyles: function (callback) { loadStyles: function (callback) {
this.loadDependencies("getStyles", callback); this.loadDependencies("getStyles", callback);
}, },
/* loadScripts() /**
* Load all required scripts by requesting the MM object to load the files. * Load all required scripts by requesting the MM object to load the files.
* *
* argument callback function - Function called when done. * @param {Function} callback Function called when done.
*/ */
loadScripts: function (callback) { loadScripts: function (callback) {
this.loadDependencies("getScripts", callback); this.loadDependencies("getScripts", callback);
}, },
/* loadDependencies(funcName, callback) /**
* Helper method to load all dependencies. * Helper method to load all dependencies.
* *
* argument funcName string - Function name to call to get scripts or styles. * @param {string} funcName Function name to call to get scripts or styles.
* argument callback function - Function called when done. * @param {Function} callback Function called when done.
*/ */
loadDependencies: function (funcName, callback) { loadDependencies: function (funcName, callback) {
var self = this; var self = this;
@@ -301,10 +305,10 @@ var Module = Class.extend({
loadNextDependency(); loadNextDependency();
}, },
/* loadScripts() /**
* Load all required scripts by requesting the MM object to load the files. * Load all translations.
* *
* argument callback function - Function called when done. * @param {Function} callback Function called when done.
*/ */
loadTranslations: function (callback) { loadTranslations: function (callback) {
var self = this; var self = this;
@@ -313,7 +317,9 @@ var Module = Class.extend({
// The variable `first` will contain the first // The variable `first` will contain the first
// defined translation after the following line. // defined translation after the following line.
for (var first in translations) { break; } for (var first in translations) {
break;
}
if (translations) { if (translations) {
var translationFile = translations[lang] || undefined; var translationFile = translations[lang] || undefined;
@@ -333,12 +339,13 @@ var Module = Class.extend({
} }
}, },
/* translate(key, defaultValueOrVariables, defaultValue) /**
* Request the translation for a given key with optional variables and default value. * Request the translation for a given key with optional variables and default value.
* *
* argument key string - The key of the string to translate * @param {string} key The key of the string to translate
* argument defaultValueOrVariables string/object - The default value or variables for translating. (Optional) * @param {string|object} [defaultValueOrVariables] The default value or variables for translating.
* argument defaultValue string - The default value with variables. (Optional) * @param {string} [defaultValue] The default value with variables.
* @returns {string} the translated key
*/ */
translate: function (key, defaultValueOrVariables, defaultValue) { translate: function (key, defaultValueOrVariables, defaultValue) {
if (typeof defaultValueOrVariables === "object") { if (typeof defaultValueOrVariables === "object") {
@@ -347,41 +354,41 @@ var Module = Class.extend({
return Translator.translate(this, key) || defaultValueOrVariables || ""; return Translator.translate(this, key) || defaultValueOrVariables || "";
}, },
/* updateDom(speed) /**
* Request an (animated) update of the module. * Request an (animated) update of the module.
* *
* argument speed Number - The speed of the animation. (Optional) * @param {number} [speed] The speed of the animation.
*/ */
updateDom: function (speed) { updateDom: function (speed) {
MM.updateDom(this, speed); MM.updateDom(this, speed);
}, },
/* sendNotification(notification, payload) /**
* Send a notification to all modules. * Send a notification to all modules.
* *
* argument notification string - The identifier of the notification. * @param {string} notification The identifier of the notification.
* argument payload mixed - The payload of the notification. * @param {*} payload The payload of the notification.
*/ */
sendNotification: function (notification, payload) { sendNotification: function (notification, payload) {
MM.sendNotification(notification, payload, this); MM.sendNotification(notification, payload, this);
}, },
/* sendSocketNotification(notification, payload) /**
* Send a socket notification to the node helper. * Send a socket notification to the node helper.
* *
* argument notification string - The identifier of the notification. * @param {string} notification The identifier of the notification.
* argument payload mixed - The payload of the notification. * @param {*} payload The payload of the notification.
*/ */
sendSocketNotification: function (notification, payload) { sendSocketNotification: function (notification, payload) {
this.socket().sendNotification(notification, payload); this.socket().sendNotification(notification, payload);
}, },
/* hideModule(module, speed, callback) /**
* Hide this module. * Hide this module.
* *
* argument speed Number - The speed of the hide animation. * @param {number} speed The speed of the hide animation.
* argument callback function - Called when the animation is done. * @param {Function} callback Called when the animation is done.
* argument options object - Optional settings for the hide method. * @param {object} [options] Optional settings for the hide method.
*/ */
hide: function (speed, callback, options) { hide: function (speed, callback, options) {
if (typeof callback === "object") { if (typeof callback === "object") {
@@ -393,18 +400,23 @@ var Module = Class.extend({
options = options || {}; options = options || {};
var self = this; var self = this;
MM.hideModule(self, speed, function () { MM.hideModule(
self,
speed,
function () {
self.suspend(); self.suspend();
callback(); callback();
}, options); },
options
);
}, },
/* showModule(module, speed, callback) /**
* Show this module. * Show this module.
* *
* argument speed Number - The speed of the show animation. * @param {number} speed The speed of the show animation.
* argument callback function - Called when the animation is done. * @param {Function} callback Called when the animation is done.
* argument options object - Optional settings for the hide method. * @param {object} [options] Optional settings for the show method.
*/ */
show: function (speed, callback, options) { show: function (speed, callback, options) {
if (typeof callback === "object") { if (typeof callback === "object") {
@@ -416,17 +428,21 @@ var Module = Class.extend({
options = options || {}; options = options || {};
var self = this; var self = this;
MM.showModule(this, speed, function () { MM.showModule(
this,
speed,
function () {
self.resume(); self.resume();
callback; callback;
}, options); },
options
);
} }
}); });
Module.definitions = {}; Module.definitions = {};
Module.create = function (name) { Module.create = function (name) {
// Make sure module definition is available. // Make sure module definition is available.
if (!Module.definitions[name]) { if (!Module.definitions[name]) {
return; return;
@@ -441,11 +457,27 @@ Module.create = function (name) {
return new ModuleClass(); return new ModuleClass();
}; };
/* cmpVersions(a,b) Module.register = function (name, moduleDefinition) {
if (moduleDefinition.requiresVersion) {
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.version);
if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
Log.log("Version is ok!");
} else {
Log.log("Version is incorrect. Skip module: '" + name + "'");
return;
}
}
Log.log("Module registered: " + name);
Module.definitions[name] = moduleDefinition;
};
/**
* Compare two semantic version numbers and return the difference. * Compare two semantic version numbers and return the difference.
* *
* argument a string - Version number a. * @param {string} a Version number a.
* argument a string - Version number b. * @param {string} b Version number b.
* @returns {number} A positive number if a is larger than b, a negative
* number if a is smaller and 0 if they are the same
*/ */
function cmpVersions(a, b) { function cmpVersions(a, b) {
var i, diff; var i, diff;
@@ -462,18 +494,3 @@ function cmpVersions(a, b) {
} }
return segmentsA.length - segmentsB.length; return segmentsA.length - segmentsB.length;
} }
Module.register = function (name, moduleDefinition) {
if (moduleDefinition.requiresVersion) {
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.version);
if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
Log.log("Version is ok!");
} else {
Log.log("Version is incorrect. Skip module: '" + name + "'");
return;
}
}
Log.log("Module registered: " + name);
Module.definitions[name] = moduleDefinition;
};

View File

@@ -5,20 +5,21 @@
* MIT Licensed. * MIT Licensed.
*/ */
const Class = require("./class.js"); const Class = require("./class.js");
const Log = require("./logger.js");
const express = require("express"); const express = require("express");
var NodeHelper = Class.extend({ var NodeHelper = Class.extend({
init: function () { init: function () {
console.log("Initializing new module helper ..."); Log.log("Initializing new module helper ...");
}, },
loaded: function (callback) { loaded: function (callback) {
console.log("Module helper loaded: " + this.name); Log.log("Module helper loaded: " + this.name);
callback(); callback();
}, },
start: function () { start: function () {
console.log("Starting module helper: " + this.name); Log.log("Starting module helper: " + this.name);
}, },
/* stop() /* stop()
@@ -28,7 +29,7 @@ var NodeHelper = Class.extend({
* *
*/ */
stop: function () { stop: function () {
console.log("Stopping module helper: " + this.name); Log.log("Stopping module helper: " + this.name);
}, },
/* socketNotificationReceived(notification, payload) /* socketNotificationReceived(notification, payload)
@@ -38,7 +39,7 @@ var NodeHelper = Class.extend({
* argument payload mixed - The payload of the notification. * argument payload mixed - The payload of the notification.
*/ */
socketNotificationReceived: function (notification, payload) { socketNotificationReceived: function (notification, payload) {
console.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload); Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
}, },
/* setName(name) /* setName(name)
@@ -92,7 +93,7 @@ var NodeHelper = Class.extend({
var self = this; var self = this;
self.io = io; self.io = io;
console.log("Connecting socket for: " + this.name); Log.log("Connecting socket for: " + this.name);
var namespace = this.name; var namespace = this.name;
io.of(namespace).on("connection", function (socket) { io.of(namespace).on("connection", function (socket) {
// add a catch all event. // add a catch all event.
@@ -107,7 +108,7 @@ var NodeHelper = Class.extend({
// register catch all. // register catch all.
socket.on("*", function (notification, payload) { socket.on("*", function (notification, payload) {
if (notification !== "*") { if (notification !== "*") {
//console.log('received message in namespace: ' + namespace); //Log.log('received message in namespace: ' + namespace);
self.socketNotificationReceived(notification, payload); self.socketNotificationReceived(notification, payload);
} }
}); });
@@ -120,4 +121,6 @@ NodeHelper.create = function(moduleDefinition) {
}; };
/*************** DO NOT EDIT THE LINE BELOW ***************/ /*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = NodeHelper;} if (typeof module !== "undefined") {
module.exports = NodeHelper;
}

View File

@@ -4,17 +4,17 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
var express = require("express"); var express = require("express");
var app = require("express")(); var app = require("express")();
var path = require("path"); var path = require("path");
var ipfilter = require("express-ipfilter").IpFilter; var ipfilter = require("express-ipfilter").IpFilter;
var fs = require("fs"); var fs = require("fs");
var helmet = require("helmet"); var helmet = require("helmet");
var Utils = require(__dirname + "/utils.js");
var Log = require("./logger.js");
var Utils = require("./utils.js");
var Server = function (config, callback) { var Server = function (config, callback) {
var port = config.port; var port = config.port;
if (process.env.MM_PORT) { if (process.env.MM_PORT) {
port = process.env.MM_PORT; port = process.env.MM_PORT;
@@ -32,12 +32,12 @@ var Server = function(config, callback) {
} }
var io = require("socket.io")(server); var io = require("socket.io")(server);
console.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 ? config.address : "localhost");
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) { if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
console.info(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs")); Log.info(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
} }
app.use(function (req, res, next) { app.use(function (req, res, next) {
@@ -45,7 +45,7 @@ var Server = function(config, callback) {
if (err === undefined) { if (err === undefined) {
return next(); return next();
} }
console.log(err.message); Log.log(err.message);
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this."); res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
}); });
}); });
@@ -72,7 +72,7 @@ var Server = function(config, callback) {
html = html.replace("#VERSION#", global.version); html = html.replace("#VERSION#", global.version);
var configFile = "config/config.js"; var configFile = "config/config.js";
if (typeof(global.configuration_file) !== "undefined") { if (typeof global.configuration_file !== "undefined") {
configFile = global.configuration_file; configFile = global.configuration_file;
} }
html = html.replace("#CONFIG_FILE#", configFile); html = html.replace("#CONFIG_FILE#", configFile);

View File

@@ -17,7 +17,7 @@ var MMSocket = function(moduleName) {
// Private Methods // Private Methods
var base = "/"; var base = "/";
if ((typeof config !== "undefined") && (typeof config.basePath !== "undefined")) { if (typeof config !== "undefined" && typeof config.basePath !== "undefined") {
base = config.basePath; base = config.basePath;
} }
self.socket = io("/" + self.moduleName, { self.socket = io("/" + self.moduleName, {

View File

@@ -7,12 +7,11 @@
* MIT Licensed. * MIT Licensed.
*/ */
var Translator = (function () { var Translator = (function () {
/**
/* loadJSON(file, callback)
* Load a JSON file via XHR. * Load a JSON file via XHR.
* *
* argument file string - Path of the file we want to load. * @param {string} file Path of the file we want to load.
* argument callback function - Function called when done. * @param {Function} callback Function called when done.
*/ */
function loadJSON(file, callback) { function loadJSON(file, callback) {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
@@ -20,112 +19,39 @@ var Translator = (function() {
xhr.open("GET", file, true); xhr.open("GET", file, true);
xhr.onreadystatechange = function () { xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) { if (xhr.readyState === 4 && xhr.status === 200) {
callback(JSON.parse(stripComments(xhr.responseText))); callback(JSON.parse(xhr.responseText));
} }
}; };
xhr.send(null); xhr.send(null);
} }
/* loadJSON(str, options)
* Remove any commenting from a json file so it can be parsed.
*
* argument str string - The string that contains json with comments.
* argument opts function - Strip options.
*
* return the stripped string.
*/
function stripComments(str, opts) {
// strip comments copied from: https://github.com/sindresorhus/strip-json-comments
var singleComment = 1;
var multiComment = 2;
function stripWithoutWhitespace() {
return "";
}
function stripWithWhitespace(str, start, end) {
return str.slice(start, end).replace(/\S/g, " ");
}
opts = opts || {};
var currentChar;
var nextChar;
var insideString = false;
var insideComment = false;
var offset = 0;
var ret = "";
var strip = opts.whitespace === false ? stripWithoutWhitespace : stripWithWhitespace;
for (var i = 0; i < str.length; i++) {
currentChar = str[i];
nextChar = str[i + 1];
if (!insideComment && currentChar === "\"") {
var escaped = str[i - 1] === "\\" && str[i - 2] !== "\\";
if (!escaped) {
insideString = !insideString;
}
}
if (insideString) {
continue;
}
if (!insideComment && currentChar + nextChar === "//") {
ret += str.slice(offset, i);
offset = i;
insideComment = singleComment;
i++;
} else if (insideComment === singleComment && currentChar + nextChar === "\r\n") {
i++;
insideComment = false;
ret += strip(str, offset, i);
offset = i;
continue;
} else if (insideComment === singleComment && currentChar === "\n") {
insideComment = false;
ret += strip(str, offset, i);
offset = i;
} else if (!insideComment && currentChar + nextChar === "/*") {
ret += str.slice(offset, i);
offset = i;
insideComment = multiComment;
i++;
continue;
} else if (insideComment === multiComment && currentChar + nextChar === "*/") {
i++;
insideComment = false;
ret += strip(str, offset, i + 1);
offset = i + 1;
continue;
}
}
return ret + (insideComment ? strip(str.substr(offset)) : str.substr(offset));
}
return { return {
coreTranslations: {}, coreTranslations: {},
coreTranslationsFallback: {}, coreTranslationsFallback: {},
translations: {}, translations: {},
translationsFallback: {}, translationsFallback: {},
/* translate(module, key, variables) /**
* Load a translation for a given key for a given module. * Load a translation for a given key for a given module.
* *
* argument module Module - The module to load the translation for. * @param {Module} module The module to load the translation for.
* argument key string - The key of the text to translate. * @param {string} key The key of the text to translate.
* argument variables - The variables to use within the translation template (optional) * @param {object} variables The variables to use within the translation template (optional)
* @returns {string} the translated key
*/ */
translate: function (module, key, variables) { translate: function (module, key, variables) {
variables = variables || {}; //Empty object by default variables = variables || {}; //Empty object by default
// Combines template and variables like: /**
// template: "Please wait for {timeToWait} before continuing with {work}." * Combines template and variables like:
// variables: {timeToWait: "2 hours", work: "painting"} * template: "Please wait for {timeToWait} before continuing with {work}."
// to: "Please wait for 2 hours before continuing with painting." * variables: {timeToWait: "2 hours", work: "painting"}
* to: "Please wait for 2 hours before continuing with painting."
*
* @param {string} template Text with placeholder
* @param {object} variables Variables for the placeholder
* @returns {string} the template filled with the variables
*/
function createStringFromTemplate(template, variables) { function createStringFromTemplate(template, variables) {
if (Object.prototype.toString.call(template) !== "[object String]") { if (Object.prototype.toString.call(template) !== "[object String]") {
return template; return template;
@@ -161,13 +87,13 @@ var Translator = (function() {
return key; return key;
}, },
/* load(module, file, isFallback, callback) /**
* Load a translation file (json) and remember the data. * Load a translation file (json) and remember the data.
* *
* argument module Module - The module to load the translation file for. * @param {Module} module The module to load the translation file for.
* argument file string - Path of the file we want to load. * @param {string} file Path of the file we want to load.
* argument isFallback boolean - Flag to indicate fallback translations. * @param {boolean} isFallback Flag to indicate fallback translations.
* argument callback function - Function called when done. * @param {Function} callback Function called when done.
*/ */
load: function (module, file, isFallback, callback) { load: function (module, file, isFallback, callback) {
if (!isFallback) { if (!isFallback) {
@@ -191,10 +117,10 @@ var Translator = (function() {
} }
}, },
/* loadCoreTranslations(lang) /**
* Load the core translations. * Load the core translations.
* *
* argument lang String - The language identifier of the core language. * @param {string} lang The language identifier of the core language.
*/ */
loadCoreTranslations: function (lang) { loadCoreTranslations: function (lang) {
var self = this; var self = this;
@@ -211,7 +137,7 @@ var Translator = (function() {
self.loadCoreTranslationsFallback(); self.loadCoreTranslationsFallback();
}, },
/* loadCoreTranslationsFallback() /**
* Load the core translations fallback. * Load the core translations fallback.
* The first language defined in translations.js will be used. * The first language defined in translations.js will be used.
*/ */
@@ -220,7 +146,9 @@ var Translator = (function() {
// The variable `first` will contain the first // The variable `first` will contain the first
// defined translation after the following line. // defined translation after the following line.
for (var first in translations) {break;} for (var first in translations) {
break;
}
if (first) { if (first) {
Log.log("Loading core translation fallback file: " + translations[first]); Log.log("Loading core translation fallback file: " + translations[first]);
@@ -228,6 +156,6 @@ var Translator = (function() {
self.coreTranslationsFallback = translations; self.coreTranslationsFallback = translations;
}); });
} }
}, }
}; };
})(); })();

View File

@@ -10,8 +10,11 @@ var Utils = {
colors: { colors: {
warn: colors.yellow, warn: colors.yellow,
error: colors.red, error: colors.red,
info: colors.blue info: colors.blue,
pass: colors.green
} }
}; };
if (typeof module !== "undefined") {module.exports = Utils;} if (typeof module !== "undefined") {
module.exports = Utils;
}

View File

@@ -6,8 +6,5 @@
"module": "commonjs", "module": "commonjs",
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },
"exclude": [ "exclude": ["modules", "node_modules"]
"modules",
"node_modules"
]
} }

View File

@@ -1,4 +1,5 @@
# Module: Alert # Module: Alert
The alert module is one of the default modules of the MagicMirror. This module displays notifications from other modules. The alert module is one of the default modules of the MagicMirror. This module displays notifications from other modules.
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/alert.html). For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/alert.html).

View File

@@ -17,7 +17,7 @@ Module.register("alert",{
//Position //Position
position: "center", position: "center",
//shown at startup //shown at startup
welcome_message: false, welcome_message: false
}, },
getScripts: function () { getScripts: function () {
return ["notificationFx.js"]; return ["notificationFx.js"];
@@ -30,11 +30,13 @@ Module.register("alert",{
return { return {
en: "translations/en.json", en: "translations/en.json",
de: "translations/de.json", de: "translations/de.json",
nl: "translations/nl.json", nl: "translations/nl.json"
}; };
}, },
show_notification: function (message) { show_notification: function (message) {
if (this.config.effect === "slide") {this.config.effect = this.config.effect + "-" + this.config.position;} if (this.config.effect === "slide") {
this.config.effect = this.config.effect + "-" + this.config.position;
}
let msg = ""; let msg = "";
if (message.title) { if (message.title) {
msg += "<span class='thin dimmed medium'>" + message.title + "</span>"; msg += "<span class='thin dimmed medium'>" + message.title + "</span>";
@@ -56,19 +58,23 @@ Module.register("alert",{
show_alert: function (params, sender) { show_alert: function (params, sender) {
let image = ""; let image = "";
//Set standard params if not provided by module //Set standard params if not provided by module
if (typeof params.timer === "undefined") { params.timer = null; } if (typeof params.timer === "undefined") {
if (typeof params.imageHeight === "undefined") { params.imageHeight = "80px"; } params.timer = null;
}
if (typeof params.imageHeight === "undefined") {
params.imageHeight = "80px";
}
if (typeof params.imageUrl === "undefined" && typeof params.imageFA === "undefined") { if (typeof params.imageUrl === "undefined" && typeof params.imageFA === "undefined") {
params.imageUrl = null; params.imageUrl = null;
} else if (typeof params.imageFA === "undefined") { } else if (typeof params.imageFA === "undefined") {
image = "<img src='" + (params.imageUrl).toString() + "' height='" + (params.imageHeight).toString() + "' style='margin-bottom: 10px;'/><br />"; image = "<img src='" + params.imageUrl.toString() + "' height='" + params.imageHeight.toString() + "' style='margin-bottom: 10px;'/><br />";
} else if (typeof params.imageUrl === "undefined") { } else if (typeof params.imageUrl === "undefined") {
image = "<span class='bright " + "fa fa-" + params.imageFA + "' style='margin-bottom: 10px;font-size:" + (params.imageHeight).toString() + ";'/></span><br />"; image = "<span class='bright " + "fa fa-" + params.imageFA + "' style='margin-bottom: 10px;font-size:" + params.imageHeight.toString() + ";'/></span><br />";
} }
//Create overlay //Create overlay
const overlay = document.createElement("div"); const overlay = document.createElement("div");
overlay.id = "overlay"; overlay.id = "overlay";
overlay.innerHTML += "<div class=\"black_overlay\"></div>"; overlay.innerHTML += '<div class="black_overlay"></div>';
document.body.insertBefore(overlay, document.body.firstChild); document.body.insertBefore(overlay, document.body.firstChild);
//If module already has an open alert close it //If module already has an open alert close it
@@ -104,7 +110,6 @@ Module.register("alert",{
this.hide_alert(sender); this.hide_alert(sender);
}, params.timer); }, params.timer);
} }
}, },
hide_alert: function (sender) { hide_alert: function (sender) {
//Dismiss alert and remove from this.alerts //Dismiss alert and remove from this.alerts
@@ -119,15 +124,22 @@ Module.register("alert",{
setPosition: function (pos) { setPosition: function (pos) {
//Add css to body depending on the set position for notifications //Add css to body depending on the set position for notifications
const sheet = document.createElement("style"); const sheet = document.createElement("style");
if (pos === "center") {sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}";} if (pos === "center") {
if (pos === "right") {sheet.innerHTML = ".ns-box {margin-left: auto;text-align: right;}";} sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}";
if (pos === "left") {sheet.innerHTML = ".ns-box {margin-right: auto;text-align: left;}";} }
if (pos === "right") {
sheet.innerHTML = ".ns-box {margin-left: auto;text-align: right;}";
}
if (pos === "left") {
sheet.innerHTML = ".ns-box {margin-right: auto;text-align: left;}";
}
document.body.appendChild(sheet); document.body.appendChild(sheet);
}, },
notificationReceived: function (notification, payload, sender) { notificationReceived: function (notification, payload, sender) {
if (notification === "SHOW_ALERT") { if (notification === "SHOW_ALERT") {
if (typeof payload.type === "undefined") { payload.type = "alert"; } if (typeof payload.type === "undefined") {
payload.type = "alert";
}
if (payload.type === "alert") { if (payload.type === "alert") {
this.show_alert(payload, sender); this.show_alert(payload, sender);
} else if (payload.type === "notification") { } else if (payload.type === "notification") {
@@ -143,8 +155,7 @@ Module.register("alert",{
if (this.config.welcome_message) { if (this.config.welcome_message) {
if (this.config.welcome_message === true) { if (this.config.welcome_message === true) {
this.show_notification({ title: this.translate("sysTitle"), message: this.translate("welcome") }); this.show_notification({ title: this.translate("sysTitle"), message: this.translate("welcome") });
} } else {
else{
this.show_notification({ title: this.translate("sysTitle"), message: this.config.welcome_message }); this.show_notification({ title: this.translate("sysTitle"), message: this.config.welcome_message });
} }
} }

View File

@@ -235,8 +235,12 @@
} }
@keyframes animFade { @keyframes animFade {
0% { opacity: 0; } 0% {
100% { opacity: 1; } opacity: 0;
}
100% {
opacity: 1;
}
} }
@keyframes animJelly { @keyframes animJelly {

View File

@@ -11,9 +11,12 @@
* https://tympanus.net/codrops/ * https://tympanus.net/codrops/
*/ */
(function (window) { (function (window) {
/** /**
* extend obj function * Extend one object with another one
*
* @param {object} a The object to extend
* @param {object} b The object which extends the other, overwrites existing keys
* @returns {object} The merged object
*/ */
function extend(a, b) { function extend(a, b) {
for (let key in b) { for (let key in b) {
@@ -25,7 +28,10 @@
} }
/** /**
* NotificationFx function * NotificationFx constructor
*
* @param {object} options The configuration options
* @class
*/ */
function NotificationFx(options) { function NotificationFx(options) {
this.options = extend({}, this.options); this.options = extend({}, this.options);
@@ -58,19 +64,22 @@
ttl: 6000, ttl: 6000,
al_no: "ns-box", al_no: "ns-box",
// callbacks // callbacks
onClose: function() { return false; }, onClose: function () {
onOpen: function() { return false; } return false;
},
onOpen: function () {
return false;
}
}; };
/** /**
* init function * Initialize and cache some vars
* initialize and cache some vars
*/ */
NotificationFx.prototype._init = function () { NotificationFx.prototype._init = function () {
// create HTML structure // create HTML structure
this.ntf = document.createElement("div"); this.ntf = document.createElement("div");
this.ntf.className = this.options.al_no + " ns-" + this.options.layout + " ns-effect-" + this.options.effect + " ns-type-" + this.options.type; this.ntf.className = this.options.al_no + " ns-" + this.options.layout + " ns-effect-" + this.options.effect + " ns-type-" + this.options.type;
let strinner = "<div class=\"ns-box-inner\">"; let strinner = '<div class="ns-box-inner">';
strinner += this.options.message; strinner += this.options.message;
strinner += "</div>"; strinner += "</div>";
this.ntf.innerHTML = strinner; this.ntf.innerHTML = strinner;
@@ -92,15 +101,17 @@
}; };
/** /**
* init events * Init events
*/ */
NotificationFx.prototype._initEvents = function () { NotificationFx.prototype._initEvents = function () {
// dismiss notification by tapping on it if someone has a touchscreen // dismiss notification by tapping on it if someone has a touchscreen
this.ntf.querySelector(".ns-box-inner").addEventListener("click", () => { this.dismiss(); }); this.ntf.querySelector(".ns-box-inner").addEventListener("click", () => {
this.dismiss();
});
}; };
/** /**
* show the notification * Show the notification
*/ */
NotificationFx.prototype.show = function () { NotificationFx.prototype.show = function () {
this.active = true; this.active = true;
@@ -110,7 +121,7 @@
}; };
/** /**
* dismiss the notification * Dismiss the notification
*/ */
NotificationFx.prototype.dismiss = function () { NotificationFx.prototype.dismiss = function () {
this.active = false; this.active = false;
@@ -125,7 +136,9 @@
// after animation ends remove ntf from the DOM // after animation ends remove ntf from the DOM
const onEndAnimationFn = (ev) => { const onEndAnimationFn = (ev) => {
if (ev.target !== this.ntf) {return false;} if (ev.target !== this.ntf) {
return false;
}
this.ntf.removeEventListener("animationend", onEndAnimationFn); this.ntf.removeEventListener("animationend", onEndAnimationFn);
if (ev.target.parentNode === this.options.wrapper) { if (ev.target.parentNode === this.options.wrapper) {
@@ -137,8 +150,7 @@
}; };
/** /**
* add to global namespace * Add to global namespace
*/ */
window.NotificationFx = NotificationFx; window.NotificationFx = NotificationFx;
})(window); })(window);

View File

@@ -1,4 +1,5 @@
# Module: Calendar # Module: Calendar
The `calendar` module is one of the default modules of the MagicMirror. The `calendar` module is one of the default modules of the MagicMirror.
This module displays events from a public .ical calendar. It can combine multiple calendars. This module displays events from a public .ical calendar. It can combine multiple calendars.

View File

@@ -7,7 +7,6 @@
* MIT Licensed. * MIT Licensed.
*/ */
Module.register("calendar", { Module.register("calendar", {
// Define module defaults // Define module defaults
defaults: { defaults: {
maximumEntries: 10, // Total Maximum Entries maximumEntries: 10, // Total Maximum Entries
@@ -18,8 +17,11 @@ Module.register("calendar", {
displayRepeatingCountTitle: false, displayRepeatingCountTitle: false,
defaultRepeatingCountTitle: "", defaultRepeatingCountTitle: "",
maxTitleLength: 25, maxTitleLength: 25,
maxLocationTitleLength: 25,
wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
wrapLocationEvents: false,
maxTitleLines: 3, maxTitleLines: 3,
maxEventTitleLines: 3,
fetchInterval: 5 * 60 * 1000, // Update every 5 minutes. fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
animationSpeed: 2000, animationSpeed: 2000,
fade: true, fade: true,
@@ -39,13 +41,16 @@ Module.register("calendar", {
calendars: [ calendars: [
{ {
symbol: "calendar", symbol: "calendar",
url: "https://www.calendarlabs.com/templates/ical/US-Holidays.ics", url: "https://www.calendarlabs.com/templates/ical/US-Holidays.ics"
}, }
], ],
titleReplace: { titleReplace: {
"De verjaardag van ": "", "De verjaardag van ": "",
"'s birthday": "" "'s birthday": ""
}, },
locationTitleReplace: {
"street ": ""
},
broadcastEvents: true, broadcastEvents: true,
excludedEvents: [], excludedEvents: [],
sliceMultiDayEvents: false, sliceMultiDayEvents: false,
@@ -85,7 +90,7 @@ Module.register("calendar", {
var calendarConfig = { var calendarConfig = {
maximumEntries: calendar.maximumEntries, maximumEntries: calendar.maximumEntries,
maximumNumberOfDays: calendar.maximumNumberOfDays, maximumNumberOfDays: calendar.maximumNumberOfDays,
broadcastPastEvents: calendar.broadcastPastEvents, broadcastPastEvents: calendar.broadcastPastEvents
}; };
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) { if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
calendarConfig.symbolClass = ""; calendarConfig.symbolClass = "";
@@ -123,6 +128,10 @@ Module.register("calendar", {
// Override socket notification handler. // Override socket notification handler.
socketNotificationReceived: function (notification, payload) { socketNotificationReceived: function (notification, payload) {
if (this.identifier !== payload.id) {
return;
}
if (notification === "CALENDAR_EVENTS") { if (notification === "CALENDAR_EVENTS") {
if (this.hasCalendarURL(payload.url)) { if (this.hasCalendarURL(payload.url)) {
this.calendarData[payload.url] = payload.events; this.calendarData[payload.url] = payload.events;
@@ -144,13 +153,12 @@ Module.register("calendar", {
// Override dom generator. // Override dom generator.
getDom: function () { getDom: function () {
var events = this.createEventList(); var events = this.createEventList();
var wrapper = document.createElement("table"); var wrapper = document.createElement("table");
wrapper.className = this.config.tableClass; wrapper.className = this.config.tableClass;
if (events.length === 0) { if (events.length === 0) {
wrapper.innerHTML = (this.loaded) ? this.translate("EMPTY") : this.translate("LOADING"); wrapper.innerHTML = this.loaded ? this.translate("EMPTY") : this.translate("LOADING");
wrapper.className = this.config.tableClass + " dimmed"; wrapper.className = this.config.tableClass + " dimmed";
return wrapper; return wrapper;
} }
@@ -181,9 +189,10 @@ Module.register("calendar", {
dateRow.appendChild(dateCell); dateRow.appendChild(dateCell);
wrapper.appendChild(dateRow); wrapper.appendChild(dateRow);
if (e >= startFade) { //fading if (e >= startFade) {
//fading
currentFadeStep = e - startFade; currentFadeStep = e - startFade;
dateRow.style.opacity = 1 - (1 / fadeSteps * currentFadeStep); dateRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
} }
lastSeenDate = dateAsString; lastSeenDate = dateAsString;
@@ -196,7 +205,7 @@ Module.register("calendar", {
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url); eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
} }
eventWrapper.className = "normal"; eventWrapper.className = "normal event";
if (this.config.displaySymbol) { if (this.config.displaySymbol) {
var symbolWrapper = document.createElement("td"); var symbolWrapper = document.createElement("td");
@@ -208,11 +217,7 @@ Module.register("calendar", {
var symbolClass = this.symbolClassForUrl(event.url); var symbolClass = this.symbolClassForUrl(event.url);
symbolWrapper.className = "symbol align-right " + symbolClass; symbolWrapper.className = "symbol align-right " + symbolClass;
var symbols = this.symbolsForUrl(event.url); var symbols = this.symbolsForEvent(event);
if(typeof symbols === "string") {
symbols = [symbols];
}
for (var i = 0; i < symbols.length; i++) { for (var i = 0; i < symbols.length; i++) {
var symbol = document.createElement("span"); var symbol = document.createElement("span");
symbol.className = "fa fa-fw fa-" + symbols[i]; symbol.className = "fa fa-fw fa-" + symbols[i];
@@ -221,6 +226,7 @@ Module.register("calendar", {
} }
symbolWrapper.appendChild(symbol); symbolWrapper.appendChild(symbol);
} }
eventWrapper.appendChild(symbolWrapper); eventWrapper.appendChild(symbolWrapper);
} else if (this.config.timeFormat === "dateheaders") { } else if (this.config.timeFormat === "dateheaders") {
var blankCell = document.createElement("td"); var blankCell = document.createElement("td");
@@ -232,7 +238,6 @@ Module.register("calendar", {
repeatingCountTitle = ""; repeatingCountTitle = "";
if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) { if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) {
repeatingCountTitle = this.countTitleForUrl(event.url); repeatingCountTitle = this.countTitleForUrl(event.url);
if (repeatingCountTitle !== "") { if (repeatingCountTitle !== "") {
@@ -243,7 +248,7 @@ Module.register("calendar", {
} }
} }
titleWrapper.innerHTML = this.titleTransform(event.title) + repeatingCountTitle; 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); var titleClass = this.titleClassForUrl(event.url);
@@ -256,11 +261,9 @@ Module.register("calendar", {
var timeWrapper; var timeWrapper;
if (this.config.timeFormat === "dateheaders") { if (this.config.timeFormat === "dateheaders") {
if (event.fullDayEvent) { if (event.fullDayEvent) {
titleWrapper.colSpan = "2"; titleWrapper.colSpan = "2";
titleWrapper.align = "left"; titleWrapper.align = "left";
} else { } else {
timeWrapper = document.createElement("td"); timeWrapper = document.createElement("td");
timeWrapper.className = "time light " + this.timeClassForUrl(event.url); timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
@@ -276,7 +279,6 @@ Module.register("calendar", {
timeWrapper = document.createElement("td"); timeWrapper = document.createElement("td");
eventWrapper.appendChild(titleWrapper); eventWrapper.appendChild(titleWrapper);
//console.log(event.today);
var now = new Date(); var now = new Date();
// Define second, minute, hour, and day variables // Define second, minute, hour, and day variables
var oneSecond = 1000; // 1,000 milliseconds var oneSecond = 1000; // 1,000 milliseconds
@@ -305,7 +307,7 @@ Module.register("calendar", {
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim * Note: this needs to be put in its own function, as the whole thing repeats again verbatim
*/ */
if (this.config.timeFormat === "absolute") { if (this.config.timeFormat === "absolute") {
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) { if (this.config.urgency > 1 && event.startDate - now < this.config.urgency * oneDay) {
// This event falls within the config.urgency period that the user has set // This event falls within the config.urgency period that the user has set
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD"))); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
} else { } else {
@@ -343,7 +345,7 @@ Module.register("calendar", {
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim * Note: this needs to be put in its own function, as the whole thing repeats again verbatim
*/ */
if (this.config.timeFormat === "absolute") { if (this.config.timeFormat === "absolute") {
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) { if (this.config.urgency > 1 && event.startDate - now < this.config.urgency * oneDay) {
// This event falls within the config.urgency period that the user has set // This event falls within the config.urgency period that the user has set
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow()); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} else { } else {
@@ -364,11 +366,9 @@ Module.register("calendar", {
if (this.config.showEnd) { if (this.config.showEnd) {
timeWrapper.innerHTML += "-"; timeWrapper.innerHTML += "-";
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat)); timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
} }
} }
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll'); //timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
//console.log(event);
timeWrapper.className = "time light " + this.timeClassForUrl(event.url); timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
eventWrapper.appendChild(timeWrapper); eventWrapper.appendChild(timeWrapper);
} }
@@ -378,7 +378,7 @@ Module.register("calendar", {
// Create fade effect. // Create fade effect.
if (e >= startFade) { if (e >= startFade) {
currentFadeStep = e - startFade; currentFadeStep = e - startFade;
eventWrapper.style.opacity = 1 - (1 / fadeSteps * currentFadeStep); eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
} }
if (this.config.showLocation) { if (this.config.showLocation) {
@@ -394,14 +394,14 @@ Module.register("calendar", {
var descCell = document.createElement("td"); var descCell = document.createElement("td");
descCell.className = "location"; descCell.className = "location";
descCell.colSpan = "2"; descCell.colSpan = "2";
descCell.innerHTML = event.location; descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines);
locationRow.appendChild(descCell); locationRow.appendChild(descCell);
wrapper.appendChild(locationRow); wrapper.appendChild(locationRow);
if (e >= startFade) { if (e >= startFade) {
currentFadeStep = e - startFade; currentFadeStep = e - startFade;
locationRow.style.opacity = 1 - (1 / fadeSteps * currentFadeStep); locationRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
} }
} }
} }
@@ -416,7 +416,7 @@ Module.register("calendar", {
* it will a localeSpecification object with the system locale time format. * it will a localeSpecification object with the system locale time format.
* *
* @param {number} timeFormat Specifies either 12 or 24 hour time format * @param {number} timeFormat Specifies either 12 or 24 hour time format
* @returns {moment.LocaleSpecification} * @returns {moment.LocaleSpecification} formatted time
*/ */
getLocaleSpecification: function (timeFormat) { getLocaleSpecification: function (timeFormat) {
switch (timeFormat) { switch (timeFormat) {
@@ -432,12 +432,11 @@ Module.register("calendar", {
} }
}, },
/* hasCalendarURL(url) /**
* Check if this config contains the calendar url. * Checks if this config contains the calendar url.
* *
* argument url string - Url to look for. * @param {string} url The calendar url
* * @returns {boolean} True if the calendar config contains the url, False otherwise
* return bool - Has calendar url
*/ */
hasCalendarURL: function (url) { hasCalendarURL: function (url) {
for (var c in this.config.calendars) { for (var c in this.config.calendars) {
@@ -450,10 +449,10 @@ Module.register("calendar", {
return false; return false;
}, },
/* createEventList() /**
* Creates the sorted list of all events. * Creates the sorted list of all events.
* *
* return array - Array with events. * @returns {object[]} Array with events.
*/ */
createEventList: function () { createEventList: function () {
var events = []; var events = [];
@@ -464,6 +463,7 @@ Module.register("calendar", {
var calendar = this.calendarData[c]; var calendar = this.calendarData[c];
for (var e in calendar) { for (var e in calendar) {
var event = JSON.parse(JSON.stringify(calendar[e])); // clone object var event = JSON.parse(JSON.stringify(calendar[e])); // clone object
if (event.endDate < now) { if (event.endDate < now) {
continue; continue;
} }
@@ -482,19 +482,19 @@ Module.register("calendar", {
continue; continue;
} }
event.url = c; event.url = c;
event.today = event.startDate >= today && event.startDate < (today + 24 * 60 * 60 * 1000); 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, /* 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. * 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; var 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) { if (this.config.sliceMultiDayEvents && maxCount > 1) {
var splitEvents = []; var splitEvents = [];
var midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x"); var midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
var count = 1; var count = 1;
while (event.endDate > midnight) { while (event.endDate > midnight) {
var thisEvent = JSON.parse(JSON.stringify(event)); // clone object var thisEvent = JSON.parse(JSON.stringify(event)); // clone object
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < (today + 24 * 60 * 60 * 1000); thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + 24 * 60 * 60 * 1000;
thisEvent.endDate = midnight; thisEvent.endDate = midnight;
thisEvent.title += " (" + count + "/" + maxCount + ")"; thisEvent.title += " (" + count + "/" + maxCount + ")";
splitEvents.push(thisEvent); splitEvents.push(thisEvent);
@@ -508,7 +508,7 @@ Module.register("calendar", {
splitEvents.push(event); splitEvents.push(event);
for (event of splitEvents) { for (event of splitEvents) {
if ((event.endDate > now) && (event.endDate <= future)) { if (event.endDate > now && event.endDate <= future) {
events.push(event); events.push(event);
} }
} }
@@ -533,13 +533,16 @@ Module.register("calendar", {
return false; return false;
}, },
/* createEventList(url) /**
* Requests node helper to add calendar url. * Requests node helper to add calendar url.
* *
* argument url string - Url to add. * @param {string} url The calendar url to add
* @param {object} auth The authentication method and credentials
* @param {object} calendarConfig The config of the specific calendar
*/ */
addCalendar: function (url, auth, calendarConfig) { addCalendar: function (url, auth, calendarConfig) {
this.sendSocketNotification("ADD_CALENDAR", { this.sendSocketNotification("ADD_CALENDAR", {
id: this.identifier,
url: url, url: url,
excludedEvents: calendarConfig.excludedEvents || this.config.excludedEvents, excludedEvents: calendarConfig.excludedEvents || this.config.excludedEvents,
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries, maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
@@ -549,99 +552,105 @@ Module.register("calendar", {
titleClass: calendarConfig.titleClass, titleClass: calendarConfig.titleClass,
timeClass: calendarConfig.timeClass, timeClass: calendarConfig.timeClass,
auth: auth, auth: auth,
broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents, broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents
}); });
}, },
/** /**
* symbolsForUrl(url) * Retrieves the symbols for a specific event.
* Retrieves the symbols for a specific url.
* *
* argument url string - Url to look for. * @param {object} event Event to look for.
* * @returns {string[]} The symbols
* return string/array - The Symbols
*/ */
symbolsForUrl: function (url) { symbolsForEvent: function (event) {
return this.getCalendarProperty(url, "symbol", this.config.defaultSymbol); let symbols = this.getCalendarPropertyAsArray(event.url, "symbol", this.config.defaultSymbol);
if (event.recurringEvent === true && this.hasCalendarProperty(event.url, "recurringSymbol")) {
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "recurringSymbol", this.config.defaultSymbol), symbols);
}
if (event.fullDayEvent === true && this.hasCalendarProperty(event.url, "fullDaySymbol")) {
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "fullDaySymbol", this.config.defaultSymbol), symbols);
}
return symbols;
},
mergeUnique: function (arr1, arr2) {
return arr1.concat(
arr2.filter(function (item) {
return arr1.indexOf(item) === -1;
})
);
}, },
/** /**
* symbolClassForUrl(url) * Retrieves the symbolClass for a specific calendar url.
* Retrieves the symbolClass for a specific url.
* *
* @param url string - Url to look for. * @param {string} url The calendar url
* * @returns {string} The class to be used for the symbols of the calendar
* @returns string
*/ */
symbolClassForUrl: function (url) { symbolClassForUrl: function (url) {
return this.getCalendarProperty(url, "symbolClass", ""); return this.getCalendarProperty(url, "symbolClass", "");
}, },
/** /**
* titleClassForUrl(url) * Retrieves the titleClass for a specific calendar url.
* Retrieves the titleClass for a specific url.
* *
* @param url string - Url to look for. * @param {string} url The calendar url
* * @returns {string} The class to be used for the title of the calendar
* @returns string
*/ */
titleClassForUrl: function (url) { titleClassForUrl: function (url) {
return this.getCalendarProperty(url, "titleClass", ""); return this.getCalendarProperty(url, "titleClass", "");
}, },
/** /**
* timeClassForUrl(url) * Retrieves the timeClass for a specific calendar url.
* Retrieves the timeClass for a specific url.
* *
* @param url string - Url to look for. * @param {string} url The calendar url
* * @returns {string} The class to be used for the time of the calendar
* @returns string
*/ */
timeClassForUrl: function (url) { timeClassForUrl: function (url) {
return this.getCalendarProperty(url, "timeClass", ""); return this.getCalendarProperty(url, "timeClass", "");
}, },
/* calendarNameForUrl(url) /**
* Retrieves the calendar name for a specific url. * Retrieves the calendar name for a specific calendar url.
* *
* argument url string - Url to look for. * @param {string} url The calendar url
* * @returns {string} The name of the calendar
* return string - The name of the calendar
*/ */
calendarNameForUrl: function (url) { calendarNameForUrl: function (url) {
return this.getCalendarProperty(url, "name", ""); return this.getCalendarProperty(url, "name", "");
}, },
/* colorForUrl(url) /**
* Retrieves the color for a specific url. * Retrieves the color for a specific calendar url.
* *
* argument url string - Url to look for. * @param {string} url The calendar url
* * @returns {string} The color
* return string - The Color
*/ */
colorForUrl: function (url) { colorForUrl: function (url) {
return this.getCalendarProperty(url, "color", "#fff"); return this.getCalendarProperty(url, "color", "#fff");
}, },
/* countTitleForUrl(url) /**
* Retrieves the name for a specific url. * Retrieves the count title for a specific calendar url.
* *
* argument url string - Url to look for. * @param {string} url The calendar url
* * @returns {string} The title
* return string - The Symbol
*/ */
countTitleForUrl: function (url) { countTitleForUrl: function (url) {
return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle); return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
}, },
/* getCalendarProperty(url, property, defaultValue) /**
* Helper method to retrieve the property for a specific url. * Helper method to retrieve the property for a specific calendar url.
* *
* argument url string - Url to look for. * @param {string} url The calendar url
* argument property string - Property to look for. * @param {string} property The property to look for
* argument defaultValue string - Value if property is not found. * @param {string} defaultValue The value if the property is not found
* * @returns {*} The property
* return string - The Property
*/ */
getCalendarProperty: function (url, property, defaultValue) { getCalendarProperty: function (url, property, defaultValue) {
for (var c in this.config.calendars) { for (var c in this.config.calendars) {
@@ -654,6 +663,16 @@ Module.register("calendar", {
return defaultValue; return defaultValue;
}, },
getCalendarPropertyAsArray: function (url, property, defaultValue) {
let p = this.getCalendarProperty(url, property, defaultValue);
if (!(p instanceof Array)) p = [p];
return p;
},
hasCalendarProperty: function (url, property) {
return !!this.getCalendarProperty(url, property, undefined);
},
/** /**
* Shortens a string if it's longer than maxLength and add a ellipsis to the end * Shortens a string if it's longer than maxLength and add a ellipsis to the end
* *
@@ -676,8 +695,9 @@ Module.register("calendar", {
for (var i = 0; i < words.length; i++) { for (var i = 0; i < words.length; i++) {
var word = words[i]; var word = words[i];
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { // max - 1 to account for a space if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
currentLine += (word + " "); // max - 1 to account for a space
currentLine += word + " ";
} else { } else {
line++; line++;
if (line > maxTitleLines - 1) { if (line > maxTitleLines - 1) {
@@ -688,9 +708,9 @@ Module.register("calendar", {
} }
if (currentLine.length > 0) { if (currentLine.length > 0) {
temp += (currentLine + "<br>" + word + " "); temp += currentLine + "<br>" + word + " ";
} else { } else {
temp += (word + "<br>"); temp += word + "<br>";
} }
currentLine = ""; currentLine = "";
} }
@@ -706,26 +726,31 @@ Module.register("calendar", {
} }
}, },
/* capFirst(string) /**
* Capitalize the first letter of a string * Capitalize the first letter of a string
* Return capitalized string *
* @param {string} string The string to capitalize
* @returns {string} The capitalized string
*/ */
capFirst: function (string) { capFirst: function (string) {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
}, },
/* titleTransform(title) /**
* Transforms the title of an event for usage. * Transforms the title of an event for usage.
* Replaces parts of the text as defined in config.titleReplace. * Replaces parts of the text as defined in config.titleReplace.
* Shortens title based on config.maxTitleLength and config.wrapEvents * Shortens title based on config.maxTitleLength and config.wrapEvents
* *
* argument title string - The title to transform. * @param {string} title The title to transform.
* * @param {object} titleReplace Pairs of strings to be replaced in the title
* return string - The transformed title. * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
* @param {number} maxTitleLength The max length of the string
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
* @returns {string} The transformed title.
*/ */
titleTransform: function (title) { titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
for (var needle in this.config.titleReplace) { for (var needle in titleReplace) {
var replacement = this.config.titleReplace[needle]; var replacement = titleReplace[needle];
var regParts = needle.match(/^\/(.+)\/([gim]*)$/); var regParts = needle.match(/^\/(.+)\/([gim]*)$/);
if (regParts) { if (regParts) {
@@ -736,11 +761,11 @@ Module.register("calendar", {
title = title.replace(needle, replacement); title = title.replace(needle, replacement);
} }
title = this.shorten(title, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines); title = this.shorten(title, maxTitleLength, wrapEvents, maxTitleLines);
return title; return title;
}, },
/* broadcastEvents() /**
* Broadcasts the events to all other modules for reuse. * Broadcasts the events to all other modules for reuse.
* The all events available in one array, sorted on startdate. * The all events available in one array, sorted on startdate.
*/ */
@@ -750,7 +775,7 @@ Module.register("calendar", {
var calendar = this.calendarData[url]; var calendar = this.calendarData[url];
for (var e in calendar) { for (var e in calendar) {
var event = cloneObject(calendar[e]); var event = cloneObject(calendar[e]);
event.symbol = this.symbolsForUrl(url); event.symbol = this.symbolsForEvent(event);
event.calendarName = this.calendarNameForUrl(url); event.calendarName = this.calendarNameForUrl(url);
event.color = this.colorForUrl(url); event.color = this.colorForUrl(url);
delete event.url; delete event.url;
@@ -763,6 +788,5 @@ Module.register("calendar", {
}); });
this.sendNotification("CALENDAR_EVENTS", eventList); this.sendNotification("CALENDAR_EVENTS", eventList);
} }
}); });

View File

@@ -4,29 +4,47 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
const Log = require("../../../js/logger.js");
const ical = require("ical");
const request = require("request");
const ical = require("./vendor/ical.js"); /**
* Moment date
*
* @external Moment
* @see {@link http://momentjs.com}
*/
const moment = require("moment"); const moment = require("moment");
var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) { /**
var self = this; *
* @param {string} url The url of the calendar to fetch
* @param {number} reloadInterval Time in ms the calendar is fetched again
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
* @param {number} maximumEntries The maximum number of events fetched.
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
* @param {object} auth The object containing options for authentication against the calendar.
* @param {boolean} includePastEvents If true events from the past maximumNumberOfDays will be fetched too
* @class
*/
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) {
const self = this;
var reloadTimer = null; let reloadTimer = null;
var events = []; let events = [];
var fetchFailedCallback = function() {}; let fetchFailedCallback = function () {};
var eventsReceivedCallback = function() {}; let eventsReceivedCallback = function () {};
/* fetchCalendar() /**
* Initiates calendar fetch. * Initiates calendar fetch.
*/ */
var fetchCalendar = function() { const fetchCalendar = function () {
clearTimeout(reloadTimer); clearTimeout(reloadTimer);
reloadTimer = null; reloadTimer = null;
var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
var opts = { const opts = {
headers: { headers: {
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)" "User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
}, },
@@ -38,44 +56,43 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
opts.auth = { opts.auth = {
bearer: auth.pass bearer: auth.pass
}; };
} else { } else {
opts.auth = { opts.auth = {
user: auth.user, user: auth.user,
pass: auth.pass pass: auth.pass,
sendImmediately: auth.method !== "digest"
}; };
if(auth.method === "digest"){
opts.auth.sendImmediately = false;
} else {
opts.auth.sendImmediately = true;
}
} }
} }
ical.fromURL(url, opts, function(err, data) { request(url, opts, function (err, r, requestData) {
if (err) { if (err) {
fetchFailedCallback(self, err); fetchFailedCallback(self, err);
scheduleTimer(); scheduleTimer();
return; return;
} else if (r.statusCode !== 200) {
fetchFailedCallback(self, r.statusCode + ": " + r.statusMessage);
scheduleTimer();
return;
} }
// console.log(data); const data = ical.parseICS(requestData);
var newEvents = []; 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 // 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
var limitFunction = function(date, i) {return true;}; const limitFunction = function (date, i) {
return true;
var eventDate = function(event, time) {
return (event[time].length === 8) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
}; };
for (var e in data) { const eventDate = function (event, time) {
var event = data[e]; return event[time].length === 8 ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
var now = new Date(); };
var today = moment().startOf("day").toDate();
var 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. Object.entries(data).forEach(([key, event]) => {
var past = today; 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;
if (includePastEvents) { if (includePastEvents) {
past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate(); past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate();
@@ -83,7 +100,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
// FIXME: Ugly fix to solve the facebook birthday issue. // FIXME: Ugly fix to solve the facebook birthday issue.
// Otherwise, the recurring events only show the birthday for next year. // Otherwise, the recurring events only show the birthday for next year.
var isFacebookBirthday = false; let isFacebookBirthday = false;
if (typeof event.uid !== "undefined") { if (typeof event.uid !== "undefined") {
if (event.uid.indexOf("@facebook.com") !== -1) { if (event.uid.indexOf("@facebook.com") !== -1) {
isFacebookBirthday = true; isFacebookBirthday = true;
@@ -91,14 +108,13 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
} }
if (event.type === "VEVENT") { if (event.type === "VEVENT") {
let startDate = eventDate(event, "start");
let endDate;
var startDate = eventDate(event, "start");
var endDate;
if (typeof event.end !== "undefined") { if (typeof event.end !== "undefined") {
endDate = eventDate(event, "end"); endDate = eventDate(event, "end");
} else if (typeof event.duration !== "undefined") { } else if (typeof event.duration !== "undefined") {
var dur=moment.duration(event.duration); endDate = startDate.clone().add(moment.duration(event.duration));
endDate = startDate.clone().add(dur);
} else { } else {
if (!isFacebookBirthday) { if (!isFacebookBirthday) {
endDate = startDate; endDate = startDate;
@@ -107,20 +123,20 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
} }
} }
// calculate the duration f the event for use with recurring events. // calculate the duration of the event for use with recurring events.
var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x")); let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
if (event.start.length === 8) { if (event.start.length === 8) {
startDate = startDate.startOf("day"); startDate = startDate.startOf("day");
} }
var title = getTitleFromEvent(event); const title = getTitleFromEvent(event);
var excluded = false, let excluded = false,
dateFilter = null; dateFilter = null;
for (var f in excludedEvents) { for (let f in excludedEvents) {
var filter = excludedEvents[f], let filter = excludedEvents[f],
testTitle = title.toLowerCase(), testTitle = title.toLowerCase(),
until = null, until = null,
useRegex = false, useRegex = false,
@@ -163,87 +179,80 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
} }
if (excluded) { if (excluded) {
continue; return;
} }
var location = event.location || false; const location = event.location || false;
var geo = event.geo || false; const geo = event.geo || false;
var description = event.description || false; const description = event.description || false;
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) { if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
var rule = event.rrule; const rule = event.rrule;
var addedEvents = 0; let addedEvents = 0;
const pastMoment = moment(past);
const futureMoment = moment(future);
// can cause problems with e.g. birthdays before 1900 // can cause problems with e.g. birthdays before 1900
if(rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 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.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900){
rule.origOptions.dtstart.setYear(1900); rule.origOptions.dtstart.setYear(1900);
rule.options.dtstart.setYear(1900); rule.options.dtstart.setYear(1900);
} }
// For recurring events, get the set of start dates that fall within the range // For recurring events, get the set of start dates that fall within the range
// of dates we"re looking for. // of dates we're looking for.
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time // kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
var pastLocal = moment(past).subtract(past.getTimezoneOffset(), "minutes").toDate(); let pastLocal = 0;
var futureLocal = moment(future).subtract(future.getTimezoneOffset(), "minutes").toDate(); let futureLocal = 0;
var datesLocal = rule.between(pastLocal, futureLocal, true, limitFunction); if (isFullDayEvent(event)) {
var dates = datesLocal.map(function(dateLocal) { // if full day event, only use the date part of the ranges
var date = moment(dateLocal).add(dateLocal.getTimezoneOffset(), "minutes").toDate(); pastLocal = pastMoment.toDate();
return date; futureLocal = futureMoment.toDate();
}); } else {
pastLocal = pastMoment.subtract(past.getTimezoneOffset(), "minutes").toDate();
futureLocal = futureMoment.subtract(future.getTimezoneOffset(), "minutes").toDate();
}
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
// The "dates" array contains the set of dates within our desired date range range that are valid // 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 // 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, // 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, // 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 // because the logic below will filter out any recurrences that don't actually belong within
// our display range. // our display range.
// Would be great if there was a better way to handle this. // Would be great if there was a better way to handle this.
if (event.recurrences !== undefined) if (event.recurrences !== undefined) {
{ for (let r in event.recurrences) {
var pastMoment = moment(past);
var futureMoment = moment(future);
for (var r in event.recurrences)
{
// Only add dates that weren't already in the range we added from the rrule so that // Only add dates that weren't already in the range we added from the rrule so that
// we don"t double-add those events. // we don"t double-add those events.
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
{
dates.push(new Date(r)); dates.push(new Date(r));
} }
} }
} }
// Loop through the set of date entries to see which recurrences should be added to our event list. // Loop through the set of date entries to see which recurrences should be added to our event list.
for (var d in dates) { for (let d in dates) {
var date = dates[d]; const date = dates[d];
// ical.js started returning recurrences and exdates as ISOStrings without time information. // 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 // .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 ) // (see https://github.com/peterbraden/ical.js/pull/84 )
var dateKey = date.toISOString().substring(0,10); const dateKey = date.toISOString().substring(0, 10);
var curEvent = event; let curEvent = event;
var showRecurrence = true; let showRecurrence = true;
let duration = 0;
// Stop parsing this event's recurrences if we've already found maximumEntries worth of recurrences.
// (The logic below would still filter the extras, but the check is simple since we're already tracking the count)
if (addedEvents >= maximumEntries) {
break;
}
startDate = moment(date); startDate = moment(date);
// For each date that we"re checking, it"s possible that there is a recurrence override for that one day. // 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)) 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. // We found an override, so for this recurrence, use a potentially different title, start date, and duration.
curEvent = curEvent.recurrences[dateKey]; curEvent = curEvent.recurrences[dateKey];
startDate = moment(curEvent.start); startDate = moment(curEvent.start);
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x")); 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. // 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)) 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. // This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false; showRecurrence = false;
} }
@@ -253,7 +262,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
endDate = endDate.endOf("day"); endDate = endDate.endOf("day");
} }
var recurrenceTitle = getTitleFromEvent(curEvent); 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 // 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. // it to the event list.
@@ -265,13 +274,14 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
showRecurrence = false; showRecurrence = false;
} }
if ((showRecurrence === true) && (addedEvents < maximumEntries)) { if (showRecurrence === true) {
addedEvents++; addedEvents++;
newEvents.push({ newEvents.push({
title: recurrenceTitle, title: recurrenceTitle,
startDate: startDate.format("x"), startDate: startDate.format("x"),
endDate: endDate.format("x"), endDate: endDate.format("x"),
fullDayEvent: isFullDayEvent(event), fullDayEvent: isFullDayEvent(event),
recurringEvent: true,
class: event.class, class: event.class,
firstYear: event.start.getFullYear(), firstYear: event.start.getFullYear(),
location: location, location: location,
@@ -282,43 +292,41 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
} }
// end recurring event parsing // end recurring event parsing
} else { } else {
// console.log("Single event ...");
// Single event. // Single event.
var fullDayEvent = (isFacebookBirthday) ? true : isFullDayEvent(event); const fullDayEvent = isFacebookBirthday ? true : isFullDayEvent(event);
if (includePastEvents) { if (includePastEvents) {
// Past event is too far in the past, so skip.
if (endDate < past) { if (endDate < past) {
//console.log("Past event is too far in the past. So skip: " + title); return;
continue;
} }
} else { } else {
// It's not a fullday event, and it is in the past, so skip.
if (!fullDayEvent && endDate < new Date()) { if (!fullDayEvent && endDate < new Date()) {
//console.log("It's not a fullday event, and it is in the past. So skip: " + title); return;
continue;
} }
// It's a fullday event, and it is before today, So skip.
if (fullDayEvent && endDate <= today) { if (fullDayEvent && endDate <= today) {
//console.log("It's a fullday event, and it is before today. So skip: " + title); return;
continue;
} }
} }
// It exceeds the maximumNumberOfDays limit, so skip.
if (startDate > future) { if (startDate > future) {
//console.log("It exceeds the maximumNumberOfDays limit. So skip: " + title); return;
continue;
} }
if (timeFilterApplies(now, endDate, dateFilter)) { if (timeFilterApplies(now, endDate, dateFilter)) {
continue; return;
} }
// adjust start date so multiple day events will be displayed as happening today even though they started some days ago already // 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) { if (fullDayEvent && startDate <= today) {
startDate = moment(today); startDate = moment(today);
} }
// Every thing is good. Add it to the list. // Every thing is good. Add it to the list.
newEvents.push({ newEvents.push({
title: title, title: title,
startDate: startDate.format("x"), startDate: startDate.format("x"),
@@ -329,17 +337,14 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
geo: geo, geo: geo,
description: description description: description
}); });
}
} }
} }
});
newEvents.sort(function (a, b) { newEvents.sort(function (a, b) {
return a.startDate - b.startDate; return a.startDate - b.startDate;
}); });
//console.log(newEvents);
events = newEvents.slice(0, maximumEntries); events = newEvents.slice(0, maximumEntries);
self.broadcastEvents(); self.broadcastEvents();
@@ -347,33 +352,31 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
}); });
}; };
/* scheduleTimer() /**
* Schedule the timer for the next update. * Schedule the timer for the next update.
*/ */
var scheduleTimer = function() { const scheduleTimer = function () {
//console.log('Schedule update timer.');
clearTimeout(reloadTimer); clearTimeout(reloadTimer);
reloadTimer = setTimeout(function () { reloadTimer = setTimeout(function () {
fetchCalendar(); fetchCalendar();
}, reloadInterval); }, reloadInterval);
}; };
/* isFullDayEvent(event) /**
* Checks if an event is a fullday event. * Checks if an event is a fullday event.
* *
* argument event object - The event object to check. * @param {object} event The event object to check.
* * @returns {boolean} True if the event is a fullday event, false otherwise
* return bool - The event is a fullday event.
*/ */
var isFullDayEvent = function(event) { const isFullDayEvent = function (event) {
if (event.start.length === 8 || event.start.dateOnly) { if (event.start.length === 8 || event.start.dateOnly) {
return true; return true;
} }
var start = event.start || 0; const start = event.start || 0;
var startDate = new Date(start); const startDate = new Date(start);
var end = event.end || 0; const end = event.end || 0;
if (((end - start) % (24 * 60 * 60 * 1000)) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 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. // Is 24 hours, and starts on the middle of the night.
return true; return true;
} }
@@ -381,20 +384,19 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
return false; return false;
}; };
/* timeFilterApplies() /**
* Determines if the user defined time filter should apply * Determines if the user defined time filter should apply
* *
* argument now Date - Date object using previously created object for consistency * @param {Date} now Date object using previously created object for consistency
* argument endDate Moment - Moment object representing the event end date * @param {Moment} endDate Moment object representing the event end date
* argument filter string - The time to subtract from the end date to determine if an event should be shown * @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
* return bool - The event should be filtered out
*/ */
var timeFilterApplies = function(now, endDate, filter) { const timeFilterApplies = function (now, endDate, filter) {
if (filter) { if (filter) {
var until = filter.split(" "), const until = filter.split(" "),
value = parseInt(until[0]), value = parseInt(until[0]),
increment = until[1].slice("-1") === "s" ? until[1] : until[1] + "s", // Massage the data for moment js increment = until[1].slice(-1) === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
filterUntil = moment(endDate.format()).subtract(value, increment); filterUntil = moment(endDate.format()).subtract(value, increment);
return now < filterUntil.format("x"); return now < filterUntil.format("x");
@@ -403,17 +405,16 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
return false; return false;
}; };
/* getTitleFromEvent(event) /**
* Gets the title from the event. * Gets the title from the event.
* *
* argument event object - The event object to check. * @param {object} event The event object to check.
* * @returns {string} The title of the event, or "Event" if no title is found.
* return string - The title of the event, or "Event" if no title is found.
*/ */
var getTitleFromEvent = function (event) { const getTitleFromEvent = function (event) {
var title = "Event"; let title = "Event";
if (event.summary) { if (event.summary) {
title = (typeof event.summary.val !== "undefined") ? event.summary.val : event.summary; title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
} else if (event.description) { } else if (event.description) {
title = event.description; title = event.description;
} }
@@ -421,7 +422,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
return title; return title;
}; };
var testTitleByFilter = function (title, filter, useRegex, regexFlags) { const testTitleByFilter = function (title, filter, useRegex, regexFlags) {
if (useRegex) { if (useRegex) {
// Assume if leading slash, there is also trailing slash // Assume if leading slash, there is also trailing slash
if (filter[0] === "/") { if (filter[0] === "/") {
@@ -439,52 +440,52 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
/* public methods */ /* public methods */
/* startFetch() /**
* Initiate fetchCalendar(); * Initiate fetchCalendar();
*/ */
this.startFetch = function () { this.startFetch = function () {
fetchCalendar(); fetchCalendar();
}; };
/* broadcastItems() /**
* Broadcast the existing events. * Broadcast the existing events.
*/ */
this.broadcastEvents = function () { this.broadcastEvents = function () {
//console.log('Broadcasting ' + events.length + ' events.'); Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events.");
eventsReceivedCallback(self); eventsReceivedCallback(self);
}; };
/* onReceive(callback) /**
* Sets the on success callback * Sets the on success callback
* *
* argument callback function - The on success callback. * @param {Function} callback The on success callback.
*/ */
this.onReceive = function (callback) { this.onReceive = function (callback) {
eventsReceivedCallback = callback; eventsReceivedCallback = callback;
}; };
/* onError(callback) /**
* Sets the on error callback * Sets the on error callback
* *
* argument callback function - The on error callback. * @param {Function} callback The on error callback.
*/ */
this.onError = function (callback) { this.onError = function (callback) {
fetchFailedCallback = callback; fetchFailedCallback = callback;
}; };
/* url() /**
* Returns the url of this fetcher. * Returns the url of this fetcher.
* *
* return string - The url of this fetcher. * @returns {string} The url of this fetcher.
*/ */
this.url = function () { this.url = function () {
return url; return url;
}; };
/* events() /**
* Returns current available events for this fetcher. * Returns current available events for this fetcher.
* *
* return array - The current available events for this fetcher. * @returns {object[]} The current available events for this fetcher.
*/ */
this.events = function () { this.events = function () {
return events; return events;

View File

@@ -5,24 +5,23 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
const CalendarFetcher = require("./calendarfetcher.js");
var CalendarFetcher = require("./calendarfetcher.js"); const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
//const url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first)
var url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL const fetchInterval = 60 * 60 * 1000;
// var url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first) const maximumEntries = 10;
var fetchInterval = 60 * 60 * 1000; const maximumNumberOfDays = 365;
var maximumEntries = 10; const user = "magicmirror";
var maximumNumberOfDays = 365; const pass = "MyStrongPass";
var user = "magicmirror"; const auth = {
var pass = "MyStrongPass";
var auth = {
user: user, user: user,
pass: pass pass: pass
}; };
console.log("Create fetcher ..."); console.log("Create fetcher ...");
var fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maximumNumberOfDays, auth); const fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maximumNumberOfDays, auth);
fetcher.onReceive(function (fetcher) { fetcher.onReceive(function (fetcher) {
console.log(fetcher.events()); console.log(fetcher.events());

View File

@@ -4,73 +4,72 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
const NodeHelper = require("node_helper");
var NodeHelper = require("node_helper"); const validUrl = require("valid-url");
var validUrl = require("valid-url"); const CalendarFetcher = require("./calendarfetcher.js");
var CalendarFetcher = require("./calendarfetcher.js"); const Log = require("../../../js/logger");
module.exports = NodeHelper.create({ module.exports = NodeHelper.create({
// Override start method. // Override start method.
start: function () { start: function () {
var events = []; Log.log("Starting node helper for: " + this.name);
this.fetchers = []; this.fetchers = [];
console.log("Starting node helper for: " + this.name);
}, },
// Override socketNotificationReceived method. // Override socketNotificationReceived method.
socketNotificationReceived: function (notification, payload) { socketNotificationReceived: function (notification, payload) {
if (notification === "ADD_CALENDAR") { if (notification === "ADD_CALENDAR") {
//console.log('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);
} }
}, },
/* createFetcher(url, reloadInterval) /**
* Creates a fetcher for a new url if it doesn't exist yet. * Creates a fetcher for a new url if it doesn't exist yet.
* Otherwise it reuses the existing one. * Otherwise it reuses the existing one.
* *
* attribute url string - URL of the news feed. * @param {string} url The url of the calendar
* attribute reloadInterval number - Reload interval in milliseconds. * @param {number} fetchInterval How often does the calendar needs to be fetched in ms
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
* @param {number} maximumEntries The maximum number of events fetched.
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
* @param {object} auth The object containing options for authentication against the calendar.
* @param {boolean} broadcastPastEvents If true events from the past maximumNumberOfDays will be included in event broadcasts
* @param {string} identifier ID of the module
*/ */
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, identifier) {
createFetcher: function(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents) {
var self = this; var self = this;
if (!validUrl.isUri(url)) { if (!validUrl.isUri(url)) {
self.sendSocketNotification("INCORRECT_URL", {url: url}); self.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url });
return; return;
} }
var fetcher; var fetcher;
if (typeof self.fetchers[url] === "undefined") { if (typeof self.fetchers[identifier + url] === "undefined") {
console.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval); 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);
fetcher.onReceive(function (fetcher) { fetcher.onReceive(function (fetcher) {
//console.log('Broadcast events.');
//console.log(fetcher.events());
self.sendSocketNotification("CALENDAR_EVENTS", { self.sendSocketNotification("CALENDAR_EVENTS", {
id: identifier,
url: fetcher.url(), url: fetcher.url(),
events: fetcher.events() events: fetcher.events()
}); });
}); });
fetcher.onError(function (fetcher, error) { fetcher.onError(function (fetcher, error) {
console.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error); Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
self.sendSocketNotification("FETCH_ERROR", { self.sendSocketNotification("FETCH_ERROR", {
id: identifier,
url: fetcher.url(), url: fetcher.url(),
error: error error: error
}); });
}); });
self.fetchers[url] = fetcher; self.fetchers[identifier + url] = fetcher;
} else { } else {
//console.log('Use existing news fetcher for url: ' + url); Log.log("Use existing calendar fetcher for url: " + url);
fetcher = self.fetchers[url]; fetcher = self.fetchers[identifier + url];
fetcher.broadcastEvents(); fetcher.broadcastEvents();
} }

View File

@@ -1,4 +0,0 @@
language: node_js
node_js:
- "8.9"
install: npm install

View File

@@ -1,178 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -1,13 +0,0 @@
Copyright 2012 Peter Braden
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,16 +0,0 @@
'use strict';
const ical = require('ical');
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function (err, data) {
for (let k in data) {
if (data.hasOwnProperty(k)) {
var ev = data[k];
if (data[k].type == 'VEVENT') {
console.log(`${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${months[ev.start.getMonth()]} at ${ev.start.toLocaleTimeString('en-GB')}`);
}
}
}
});

View File

@@ -1,118 +0,0 @@
var ical = require('./node-ical')
var moment = require('moment')
var data = ical.parseFile('./examples/example_rrule.ics');
// Complicated example demonstrating how to handle recurrence rules and exceptions.
for (var k in data) {
// When dealing with calendar recurrences, you need a range of dates to query against,
// because otherwise you can get an infinite number of calendar events.
var rangeStart = moment("2017-01-01");
var rangeEnd = moment("2017-12-31");
var event = data[k]
if (event.type === 'VEVENT') {
var title = event.summary;
var startDate = moment(event.start);
var endDate = moment(event.end);
// Calculate the duration of the event for use with recurring events.
var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
// Simple case - no recurrences, just print out the calendar event.
if (typeof event.rrule === 'undefined')
{
console.log('title:' + title);
console.log('startDate:' + startDate.format('MMMM Do YYYY, h:mm:ss a'));
console.log('endDate:' + endDate.format('MMMM Do YYYY, h:mm:ss a'));
console.log('duration:' + moment.duration(duration).humanize());
console.log();
}
// Complicated case - if an RRULE exists, handle multiple recurrences of the event.
else if (typeof event.rrule !== 'undefined')
{
// For recurring events, get the set of event start dates that fall within the range
// of dates we're looking for.
var dates = event.rrule.between(
rangeStart.toDate(),
rangeEnd.toDate(),
true,
function(date, i) {return true;}
)
// The "dates" array contains the set of dates within our desired date range range that are valid
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
// had its date changed from outside the range to inside the range. One way to handle this is
// to add *all* recurrence override entries into the set of dates that we check, and then later
// filter out any recurrences that don't actually belong within our range.
if (event.recurrences != undefined)
{
for (var 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(rangeStart, rangeEnd) != true)
{
dates.push(new Date(r));
}
}
}
// Loop through the set of date entries to see which recurrences should be printed.
for(var i in dates) {
var date = dates[i];
var curEvent = event;
var showRecurrence = true;
var curDuration = duration;
startDate = moment(date);
// Use just the date of the recurrence to look up overrides and exceptions (i.e. chop off time information)
var dateLookupKey = date.toISOString().substring(0, 10);
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
if ((curEvent.recurrences != undefined) && (curEvent.recurrences[dateLookupKey] != undefined))
{
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
curEvent = curEvent.recurrences[dateLookupKey];
startDate = moment(curEvent.start);
curDuration = 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[dateLookupKey] != undefined))
{
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
}
// Set the the title and the end date from either the regular event or the recurrence override.
var recurrenceTitle = curEvent.summary;
endDate = moment(parseInt(startDate.format("x")) + curDuration, 'x');
// If this recurrence ends before the start of the date range, or starts after the end of the date range,
// don't process it.
if (endDate.isBefore(rangeStart) || startDate.isAfter(rangeEnd)) {
showRecurrence = false;
}
if (showRecurrence === true) {
console.log('title:' + recurrenceTitle);
console.log('startDate:' + startDate.format('MMMM Do YYYY, h:mm:ss a'));
console.log('endDate:' + endDate.format('MMMM Do YYYY, h:mm:ss a'));
console.log('duration:' + moment.duration(curDuration).humanize());
console.log();
}
}
}
}
}

View File

@@ -1,40 +0,0 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:ical
X-WR-TIMEZONE:US/Central
X-WR-CALDESC:
BEGIN:VEVENT
UID:98765432-ABCD-DCBB-999A-987765432123
DTSTART;TZID=US/Central:20170601T090000
DTEND;TZID=US/Central:20170601T170000
DTSTAMP:20170727T044436Z
EXDATE;TZID=US/Central:20170706T090000,20170713T090000,20170720T090000,20
170803T090000
LAST-MODIFIED:20170727T044435Z
RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20170814T045959Z;BYDAY=TH
SEQUENCE:0
SUMMARY:Recurring weekly meeting from June 1 - Aug 14 (except July 6, July 13, July 20, Aug 3)
END:VEVENT
BEGIN:VEVENT
UID:98765432-ABCD-DCBB-999A-987765432123
RECURRENCE-ID;TZID=US/Central:20170629T090000
DTSTART;TZID=US/Central:20170703T090000
DTEND;TZID=US/Central:20170703T120000
DTSTAMP:20170727T044436Z
LAST-MODIFIED:20170216T143445Z
SEQUENCE:0
SUMMARY:Last meeting in June moved to Monday July 3 and shortened to half day
END:VEVENT
BEGIN:VEVENT
UID:12354454-ABCD-DCBB-999A-2349872354897
DTSTART;TZID=US/Central:20171201T130000
DTEND;TZID=US/Central:20171201T150000
DTSTAMP:20170727T044436Z
LAST-MODIFIED:20170727T044435Z
SEQUENCE:0
SUMMARY:Single event on Dec 1
END:VEVENT
END:VCALENDAR

View File

@@ -1,452 +0,0 @@
(function(name, definition) {
/****************
* A tolerant, minimal icalendar parser
* (http://tools.ietf.org/html/rfc5545)
*
* <peterbraden@peterbraden.co.uk>
* **************/
if (typeof module !== 'undefined') {
module.exports = definition();
} else if (typeof define === 'function' && typeof define.amd === 'object'){
define(definition);
} else {
this[name] = definition();
}
}('ical', function(){
// Unescape Text re RFC 4.3.11
var text = function(t){
t = t || "";
return (t
.replace(/\\\,/g, ',')
.replace(/\\\;/g, ';')
.replace(/\\[nN]/g, '\n')
.replace(/\\\\/g, '\\')
)
}
var parseParams = function(p){
var out = {}
for (var i = 0; i<p.length; i++){
if (p[i].indexOf('=') > -1){
var segs = p[i].split('=');
out[segs[0]] = parseValue(segs.slice(1).join('='));
}
}
return out || sp
}
var parseValue = function(val){
if ('TRUE' === val)
return true;
if ('FALSE' === val)
return false;
var number = Number(val);
if (!isNaN(number))
return number;
return val;
}
var storeValParam = function (name) {
return function (val, curr) {
var current = curr[name];
if (Array.isArray(current)) {
current.push(val);
return curr;
}
if (current != null) {
curr[name] = [current, val];
return curr;
}
curr[name] = val;
return curr
}
}
var storeParam = function (name) {
return function (val, params, curr) {
var data;
if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) {
data = { params: parseParams(params), val: text(val) }
}
else
data = text(val)
return storeValParam(name)(data, curr);
}
}
var addTZ = function (dt, params) {
var p = parseParams(params);
if (params && p){
dt.tz = p.TZID
}
return dt
}
var dateParam = function(name){
return function (val, params, curr) {
var newDate = text(val);
if (params && params[0] === "VALUE=DATE") {
// Just Date
var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val);
if (comps !== null) {
// No TZ info - assume same timezone as this computer
newDate = new Date(
comps[1],
parseInt(comps[2], 10)-1,
comps[3]
);
newDate = addTZ(newDate, params);
newDate.dateOnly = true;
// Store as string - worst case scenario
return storeValParam(name)(newDate, curr)
}
}
//typical RFC date-time format
var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val);
if (comps !== null) {
if (comps[7] == 'Z'){ // GMT
newDate = new Date(Date.UTC(
parseInt(comps[1], 10),
parseInt(comps[2], 10)-1,
parseInt(comps[3], 10),
parseInt(comps[4], 10),
parseInt(comps[5], 10),
parseInt(comps[6], 10 )
));
// TODO add tz
} else {
newDate = new Date(
parseInt(comps[1], 10),
parseInt(comps[2], 10)-1,
parseInt(comps[3], 10),
parseInt(comps[4], 10),
parseInt(comps[5], 10),
parseInt(comps[6], 10)
);
}
newDate = addTZ(newDate, params);
}
// Store as string - worst case scenario
return storeValParam(name)(newDate, curr)
}
}
var geoParam = function(name){
return function(val, params, curr){
storeParam(val, params, curr)
var parts = val.split(';');
curr[name] = {lat:Number(parts[0]), lon:Number(parts[1])};
return curr
}
}
var categoriesParam = function (name) {
var separatorPattern = /\s*,\s*/g;
return function (val, params, curr) {
storeParam(val, params, curr)
if (curr[name] === undefined)
curr[name] = val ? val.split(separatorPattern) : []
else
if (val)
curr[name] = curr[name].concat(val.split(separatorPattern))
return curr
}
}
// EXDATE is an entry that represents exceptions to a recurrence rule (ex: "repeat every day except on 7/4").
// The EXDATE entry itself can also contain a comma-separated list, so we make sure to parse each date out separately.
// There can also be more than one EXDATE entries in a calendar record.
// Since there can be multiple dates, we create an array of them. The index into the array is the ISO string of the date itself, for ease of use.
// i.e. You can check if ((curr.exdate != undefined) && (curr.exdate[date iso string] != undefined)) to see if a date is an exception.
// NOTE: This specifically uses date only, and not time. This is to avoid a few problems:
// 1. The ISO string with time wouldn't work for "floating dates" (dates without timezones).
// ex: "20171225T060000" - this is supposed to mean 6 AM in whatever timezone you're currently in
// 2. Daylight savings time potentially affects the time you would need to look up
// 3. Some EXDATE entries in the wild seem to have times different from the recurrence rule, but are still excluded by calendar programs. Not sure how or why.
// These would fail any sort of sane time lookup, because the time literally doesn't match the event. So we'll ignore time and just use date.
// ex: DTSTART:20170814T140000Z
// RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU
// EXDATE:20171219T060000
// Even though "T060000" doesn't match or overlap "T1400000Z", it's still supposed to be excluded? Odd. :(
// TODO: See if this causes any problems with events that recur multiple times a day.
var exdateParam = function (name) {
return function (val, params, curr) {
var separatorPattern = /\s*,\s*/g;
curr[name] = curr[name] || [];
var dates = val ? val.split(separatorPattern) : [];
dates.forEach(function (entry) {
var exdate = new Array();
dateParam(name)(entry, params, exdate);
if (exdate[name])
{
if (typeof exdate[name].toISOString === 'function') {
curr[name][exdate[name].toISOString().substring(0, 10)] = exdate[name];
} else {
console.error("No toISOString function in exdate[name]", exdate[name]);
}
}
}
)
return curr;
}
}
// RECURRENCE-ID is the ID of a specific recurrence within a recurrence rule.
// TODO: It's also possible for it to have a range, like "THISANDPRIOR", "THISANDFUTURE". This isn't currently handled.
var recurrenceParam = function (name) {
return dateParam(name);
}
var addFBType = function (fb, params) {
var p = parseParams(params);
if (params && p){
fb.type = p.FBTYPE || "BUSY"
}
return fb;
}
var freebusyParam = function (name) {
return function(val, params, curr){
var fb = addFBType({}, params);
curr[name] = curr[name] || []
curr[name].push(fb);
storeParam(val, params, fb);
var parts = val.split('/');
['start', 'end'].forEach(function (name, index) {
dateParam(name)(parts[index], params, fb);
});
return curr;
}
}
return {
objectHandlers : {
'BEGIN' : function(component, params, curr, stack){
stack.push(curr)
return {type:component, params:params}
}
, 'END' : function(component, params, curr, stack){
// prevents the need to search the root of the tree for the VCALENDAR object
if (component === "VCALENDAR") {
//scan all high level object in curr and drop all strings
var key,
obj;
for (key in curr) {
if(curr.hasOwnProperty(key)) {
obj = curr[key];
if (typeof obj === 'string') {
delete curr[key];
}
}
}
return curr
}
var par = stack.pop()
if (curr.uid)
{
// If this is the first time we run into this UID, just save it.
if (par[curr.uid] === undefined)
{
par[curr.uid] = curr;
}
else
{
// If we have multiple ical entries with the same UID, it's either going to be a
// modification to a recurrence (RECURRENCE-ID), and/or a significant modification
// to the entry (SEQUENCE).
// TODO: Look into proper sequence logic.
if (curr.recurrenceid === undefined)
{
// If we have the same UID as an existing record, and it *isn't* a specific recurrence ID,
// not quite sure what the correct behaviour should be. For now, just take the new information
// and merge it with the old record by overwriting only the fields that appear in the new record.
var key;
for (key in curr) {
par[curr.uid][key] = curr[key];
}
}
}
// If we have recurrence-id entries, list them as an array of recurrences keyed off of recurrence-id.
// To use - as you're running through the dates of an rrule, you can try looking it up in the recurrences
// array. If it exists, then use the data from the calendar object in the recurrence instead of the parent
// for that day.
// NOTE: Sometimes the RECURRENCE-ID record will show up *before* the record with the RRULE entry. In that
// case, what happens is that the RECURRENCE-ID record ends up becoming both the parent record and an entry
// in the recurrences array, and then when we process the RRULE entry later it overwrites the appropriate
// fields in the parent record.
if (curr.recurrenceid != null)
{
// TODO: Is there ever a case where we have to worry about overwriting an existing entry here?
// Create a copy of the current object to save in our recurrences array. (We *could* just do par = curr,
// except for the case that we get the RECURRENCE-ID record before the RRULE record. In that case, we
// would end up with a shared reference that would cause us to overwrite *both* records at the point
// that we try and fix up the parent record.)
var recurrenceObj = new Object();
var key;
for (key in curr) {
recurrenceObj[key] = curr[key];
}
if (recurrenceObj.recurrences != undefined) {
delete recurrenceObj.recurrences;
}
// If we don't have an array to store recurrences in yet, create it.
if (par[curr.uid].recurrences === undefined) {
par[curr.uid].recurrences = new Array();
}
// Save off our cloned recurrence object into the array, keyed by date but not time.
// We key by date only to avoid timezone and "floating time" problems (where the time isn't associated with a timezone).
// TODO: See if this causes a problem with events that have multiple recurrences per day.
if (typeof curr.recurrenceid.toISOString === 'function') {
par[curr.uid].recurrences[curr.recurrenceid.toISOString().substring(0,10)] = recurrenceObj;
} else {
console.error("No toISOString function in curr.recurrenceid", curr.recurrenceid);
}
}
// One more specific fix - in the case that an RRULE entry shows up after a RECURRENCE-ID entry,
// let's make sure to clear the recurrenceid off the parent field.
if ((par[curr.uid].rrule != undefined) && (par[curr.uid].recurrenceid != undefined))
{
delete par[curr.uid].recurrenceid;
}
}
else
par[Math.random()*100000] = curr // Randomly assign ID : TODO - use true GUID
return par
}
, 'SUMMARY' : storeParam('summary')
, 'DESCRIPTION' : storeParam('description')
, 'URL' : storeParam('url')
, 'UID' : storeParam('uid')
, 'LOCATION' : storeParam('location')
, 'DTSTART' : dateParam('start')
, 'DTEND' : dateParam('end')
, 'EXDATE' : exdateParam('exdate')
,' CLASS' : storeParam('class')
, 'TRANSP' : storeParam('transparency')
, 'GEO' : geoParam('geo')
, 'PERCENT-COMPLETE': storeParam('completion')
, 'COMPLETED': dateParam('completed')
, 'CATEGORIES': categoriesParam('categories')
, 'FREEBUSY': freebusyParam('freebusy')
, 'DTSTAMP': dateParam('dtstamp')
, 'CREATED': dateParam('created')
, 'LAST-MODIFIED': dateParam('lastmodified')
, 'RECURRENCE-ID': recurrenceParam('recurrenceid')
},
handleObject : function(name, val, params, ctx, stack, line){
var self = this
if(self.objectHandlers[name])
return self.objectHandlers[name](val, params, ctx, stack, line)
//handling custom properties
if(name.match(/X\-[\w\-]+/) && stack.length > 0) {
//trimming the leading and perform storeParam
name = name.substring(2);
return (storeParam(name))(val, params, ctx, stack, line);
}
return storeParam(name.toLowerCase())(val, params, ctx);
},
parseICS : function(str){
var self = this
var lines = str.split(/\r?\n/)
var ctx = {}
var stack = []
for (var i = 0, ii = lines.length, l = lines[0]; i<ii; i++, l=lines[i]){
//Unfold : RFC#3.1
while (lines[i+1] && /[ \t]/.test(lines[i+1][0])) {
l += lines[i+1].slice(1)
i += 1
}
var kv = l.split(":")
if (kv.length < 2){
// Invalid line - must have k&v
continue;
}
// Although the spec says that vals with colons should be quote wrapped
// in practise nobody does, so we assume further colons are part of the
// val
var value = kv.slice(1).join(":")
, kp = kv[0].split(";")
, name = kp[0]
, params = kp.slice(1)
ctx = self.handleObject(name, value, params, ctx, stack, l) || {}
}
// type and params are added to the list of items, get rid of them.
delete ctx.type
delete ctx.params
return ctx
}
}
}))

View File

@@ -1,8 +0,0 @@
module.exports = require('./ical')
var node = require('./node-ical')
// Copy node functions across to exports
for (var i in node){
module.exports[i] = node[i]
}

View File

@@ -1,77 +0,0 @@
var ical = require('./ical')
, request = require('request')
, fs = require('fs')
exports.fromURL = function(url, opts, cb){
if (!cb)
return;
request(url, opts, function(err, r, data){
if (err)
{
return cb(err, null);
}
else if (r.statusCode != 200)
{
return cb(r.statusCode + ": " + r.statusMessage, null);
}
cb(undefined, ical.parseICS(data));
})
}
exports.parseFile = function(filename){
return ical.parseICS(fs.readFileSync(filename, 'utf8'))
}
var rrule = require('rrule').RRule
function getLocaleISOString(date) {
var year = date.getFullYear().toString(10).padStart(4,'0');
var month = (date.getMonth() + 1).toString(10).padStart(2,'0');
var day = date.getDate().toString(10).padStart(2,'0');
var hour = date.getHours().toString(10).padStart(2,'0');
var minute = date.getMinutes().toString(10).padStart(2,'0');
var second = date.getSeconds().toString(10).padStart(2,'0');
return `${year}${month}${day}T${hour}${minute}${second}Z`;
}
ical.objectHandlers['RRULE'] = function(val, params, curr, stack, line){
curr.rrule = line;
return curr
}
var originalEnd = ical.objectHandlers['END'];
ical.objectHandlers['END'] = function (val, params, curr, stack) {
// Recurrence rules are only valid for VEVENT, VTODO, and VJOURNAL.
// More specifically, we need to filter the VCALENDAR type because we might end up with a defined rrule
// due to the subtypes.
if ((val === "VEVENT") || (val === "VTODO") || (val === "VJOURNAL")) {
if (curr.rrule) {
var rule = curr.rrule.replace('RRULE:', '');
if (rule.indexOf('DTSTART') === -1) {
if (curr.start.length === 8) {
var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(curr.start);
if (comps) {
curr.start = new Date(comps[1], comps[2] - 1, comps[3]);
}
}
if (typeof curr.start.toISOString === 'function') {
try {
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
rule += ';DTSTART=' + getLocaleISOString(curr.start);
} catch (error) {
console.error("ERROR when trying to convert to ISOString", error);
}
} else {
console.error("No toISOString function in curr.start", curr.start);
}
}
curr.rrule = rrule.fromString(rule);
}
}
return originalEnd.call(this, val, params, curr, stack);
}

View File

@@ -1,29 +0,0 @@
{
"name": "ical",
"version": "0.5.0",
"main": "index.js",
"description": "A tolerant, minimal icalendar parser",
"keywords": [
"ical",
"ics",
"calendar"
],
"homepage": "https://github.com/peterbraden/ical.js",
"author": "Peter Braden <peterbraden@peterbraden.co.uk> (peterbraden.co.uk)",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git://github.com/peterbraden/ical.js.git"
},
"dependencies": {
"request": "^2.88.0",
"rrule": "2.4.1"
},
"devDependencies": {
"vows": "0.8.2",
"underscore": "1.9.1"
},
"scripts": {
"test": "./node_modules/vows/bin/vows ./test/test.js"
}
}

View File

@@ -1,62 +0,0 @@
# ical.js #
(Formerly node-ical)
[![Build Status](https://travis-ci.org/peterbraden/ical.js.png)](https://travis-ci.org/peterbraden/ical.js)
A tolerant, minimal icalendar parser for javascript/node
(http://tools.ietf.org/html/rfc5545)
## Install - Node.js ##
ical.js is availble on npm:
npm install ical
## API ##
ical.parseICS(str)
Parses a string with an ICS File
var data = ical.parseFile(filename)
Reads in the specified iCal file, parses it and returns the parsed data
ical.fromURL(url, options, function(err, data) {} )
Use the request library to fetch the specified URL (```opts``` gets passed on to the ```request()``` call), and call the function with the result (either an error or the data).
## Example 1 - Print list of upcoming node conferences (see example.js)
```javascript
'use strict';
const ical = require('ical');
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function (err, data) {
for (let k in data) {
if (data.hasOwnProperty(k)) {
var ev = data[k];
if (data[k].type == 'VEVENT') {
console.log(`${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${months[ev.start.getMonth()]} at ${ev.start.toLocaleTimeString('en-GB')}`);
}
}
}
});
```
## Recurrences and Exceptions ##
Calendar events with recurrence rules can be significantly more complicated to handle correctly. There are three parts to handling them:
1. rrule - the recurrence rule specifying the pattern of recurring dates and times for the event.
2. recurrences - an optional array of event data that can override specific occurrences of the event.
3. exdate - an optional array of dates that should be excluded from the recurrence pattern.
See example_rrule.js for an example of handling recurring calendar events.

View File

@@ -1,500 +0,0 @@
/****
* Tests
*
*
***/
process.env.TZ = 'America/San_Francisco';
var ical = require('../index')
var vows = require('vows')
, assert = require('assert')
, _ = require('underscore')
vows.describe('node-ical').addBatch({
'when parsing test1.ics (node conferences schedule from lanyrd.com, modified)': {
topic: function () {
return ical.parseFile('./test/test1.ics')
}
,'we get 9 events': function (topic) {
var events = _.select(_.values(topic), function(x){ return x.type==='VEVENT'})
assert.equal (events.length, 9);
}
,'event 47f6e' : {
topic: function(events){
return _.select(_.values(events),
function(x){
return x.uid ==='47f6ea3f28af2986a2192fa39a91fa7d60d26b76'})[0]
}
,'is in fort lauderdale' : function(topic){
assert.equal(topic.location, "Fort Lauderdale, United States")
}
,'starts Tue, 29 Nov 2011' : function(topic){
assert.equal(topic.start.toDateString(), new Date(2011,10,29).toDateString())
}
}
, 'event 480a' : {
topic: function(events){
return _.select(_.values(events),
function(x){
return x.uid ==='480a3ad48af5ed8965241f14920f90524f533c18'})[0]
}
, 'has a summary (invalid colon handling tolerance)' : function(topic){
assert.equal(topic.summary, '[Async]: Everything Express')
}
, 'has a date only start datetime' : function(topic){
assert.equal(topic.start.dateOnly, true)
}
, 'has a date only end datetime' : function(topic){
assert.equal(topic.end.dateOnly, true)
}
}
, 'event d4c8' :{
topic : function(events){
return _.select(_.values(events),
function(x){
return x.uid === 'd4c826dfb701f611416d69b4df81caf9ff80b03a'})[0]
}
, 'has a start datetime' : function(topic){
assert.equal(topic.start.toDateString(), new Date(Date.UTC(2011, 2, 12, 20, 0, 0)).toDateString())
}
}
, 'event sdfkf09fsd0 (Invalid Date)' :{
topic : function(events){
return _.select(_.values(events),
function(x){
return x.uid === 'sdfkf09fsd0'})[0]
}
, 'has a start datetime' : function(topic){
assert.equal(topic.start, "Next Year")
}
}
}
, 'with test2.ics (testing ical features)' : {
topic: function () {
return ical.parseFile('./test/test2.ics')
}
, 'todo item uid4@host1.com' : {
topic : function(items){
return items['uid4@host1.com']
}
, 'is a VTODO' : function(topic){
assert.equal(topic.type, 'VTODO')
}
}
, 'vfreebusy' : {
topic: function(events) {
return _.select(_.values(events), function(x) {
return x.type === 'VFREEBUSY';
})[0];
}
, 'has a URL' : function(topic) {
assert.equal(topic.url, 'http://www.host.com/calendar/busytime/jsmith.ifb');
}
}
, 'vfreebusy first freebusy' : {
topic: function(events) {
return _.select(_.values(events), function(x) {
return x.type === 'VFREEBUSY';
})[0].freebusy[0];
}
, 'has undefined type defaulting to busy' : function(topic) {
assert.equal(topic.type, "BUSY");
}
, 'has an start datetime' : function(topic) {
assert.equal(topic.start.getFullYear(), 1998);
assert.equal(topic.start.getUTCMonth(), 2);
assert.equal(topic.start.getUTCDate(), 14);
assert.equal(topic.start.getUTCHours(), 23);
assert.equal(topic.start.getUTCMinutes(), 30);
}
, 'has an end datetime' : function(topic) {
assert.equal(topic.end.getFullYear(), 1998);
assert.equal(topic.end.getUTCMonth(), 2);
assert.equal(topic.end.getUTCDate(), 15);
assert.equal(topic.end.getUTCHours(), 00);
assert.equal(topic.end.getUTCMinutes(), 30);
}
}
}
, 'with test3.ics (testing tvcountdown.com)' : {
topic: function() {
return ical.parseFile('./test/test3.ics');
}
, 'event -83' : {
topic: function(events) {
return _.select(_.values(events), function(x) {
return x.uid === '20110505T220000Z-83@tvcountdown.com';
})[0];
}
, 'has a start datetime' : function(topic) {
assert.equal(topic.start.getFullYear(), 2011);
assert.equal(topic.start.getMonth(), 4);
}
, 'has an end datetime' : function(topic) {
assert.equal(topic.end.getFullYear(), 2011);
assert.equal(topic.end.getMonth(), 4);
}
}
}
, 'with test4.ics (testing tripit.com)' : {
topic: function() {
return ical.parseFile('./test/test4.ics');
}
, 'event c32a5...' : {
topic: function(events) {
return _.select(_.values(events), function(x) {
return x.uid === 'c32a5eaba2354bb29e012ec18da827db90550a3b@tripit.com';
})[0];
}
, 'has a start datetime' : function(topic) {
assert.equal(topic.start.getFullYear(), 2011);
assert.equal(topic.start.getMonth(), 09);
assert.equal(topic.start.getDate(), 11);
}
, 'has a summary' : function(topic){
// escaped commas and semicolons should be replaced
assert.equal(topic.summary, 'South San Francisco, CA, October 2011;')
}
, 'has a description' : function(topic){
var desired = 'John Doe is in South San Francisco, CA from Oct 11 ' +
'to Oct 13, 2011\nView and/or edit details in TripIt : http://www.tripit.c' +
'om/trip/show/id/23710889\nTripIt - organize your travel at http://www.trip' +
'it.com\n'
assert.equal(topic.description, desired)
}
, 'has a geolocation' : function(topic){
assert.ok(topic.geo, 'no geo param')
assert.equal(topic.geo.lat, 37.654656)
assert.equal(topic.geo.lon, -122.40775)
}
, 'has transparency' : function(topic){
assert.equal(topic.transparency, 'TRANSPARENT')
}
}
}
, 'with test5.ics (testing meetup.com)' : {
topic: function () {
return ical.parseFile('./test/test5.ics')
}
, 'event nsmxnyppbfc@meetup.com' : {
topic: function(events) {
return _.select(_.values(events), function(x) {
return x.uid === 'event_nsmxnyppbfc@meetup.com';
})[0];
}
, 'has a start' : function(topic){
assert.equal(topic.start.tz, 'America/Phoenix')
assert.equal(topic.start.toISOString(), new Date(2011, 10, 09, 19, 0,0).toISOString())
}
}
}
, 'with test6.ics (testing assembly.org)': {
topic: function () {
return ical.parseFile('./test/test6.ics')
}
, 'event with no ID' : {
topic: function(events) {
return _.select(_.values(events), function(x) {
return x.summary === 'foobar Summer 2011 starts!';
})[0];
}
, 'has a start' : function(topic){
assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 12, 0,0).toISOString())
}
}
, 'event with rrule' :{
topic: function(events){
return _.select(_.values(events), function(x){
return x.summary === "foobarTV broadcast starts"
})[0];
}
, "Has an RRULE": function(topic){
assert.notEqual(topic.rrule, undefined);
}
, "RRule text": function(topic){
assert.equal(topic.rrule.toText(), "every 5 weeks on Monday, Friday until January 30, 2013")
}
}
}
, 'with test7.ics (testing dtstart of rrule)' :{
topic: function() {
return ical.parseFile('./test/test7.ics');
},
'recurring yearly event (14 july)': {
topic: function(events){
var ev = _.values(events)[0];
return ev.rrule.between(new Date(2013, 0, 1), new Date(2014, 0, 1));
},
'dt start well set': function(topic) {
assert.equal(topic[0].toDateString(), new Date(2013, 6, 14).toDateString());
}
}
}
, "with test 8.ics (VTODO completion)": {
topic: function() {
return ical.parseFile('./test/test8.ics');
},
'grabbing VTODO task': {
topic: function(topic) {
return _.values(topic)[0];
},
'task completed': function(task){
assert.equal(task.completion, 100);
assert.equal(task.completed.toISOString(), new Date(2013, 06, 16, 10, 57, 45).toISOString());
}
}
}
, "with test 9.ics (VEVENT with VALARM)": {
topic: function() {
return ical.parseFile('./test/test9.ics');
},
'grabbing VEVENT task': {
topic: function(topic) {
return _.values(topic)[0];
},
'task completed': function(task){
assert.equal(task.summary, "Event with an alarm");
}
}
}
, 'with test 11.ics (VEVENT with custom properties)': {
topic: function() {
return ical.parseFile('./test10.ics');
},
'grabbing custom properties': {
topic: function(topic) {
}
}
},
'with test10.ics': {
topic: function () {
return ical.parseFile('./test/test10.ics');
},
'when categories present': {
topic: function (t) {return _.values(t)[0]},
'should be a list': function (e) {
assert(e.categories instanceof [].constructor);
},
'should contain individual category values': function (e) {
assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']);
}
},
'when categories present with trailing whitespace': {
topic: function (t) {return _.values(t)[1]},
'should contain individual category values without whitespace': function (e) {
assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']);
}
},
'when categories present but empty': {
topic: function (t) {return _.values(t)[2]},
'should be an empty list': function (e) {
assert.deepEqual(e.categories, []);
}
},
'when categories present but singular': {
topic: function (t) {return _.values(t)[3]},
'should be a list of single item': function (e) {
assert.deepEqual(e.categories, ['lonely-cat']);
}
},
'when categories present on multiple lines': {
topic: function (t) {return _.values(t)[4]},
'should contain the category values in an array': function (e) {
assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']);
}
}
},
'with test11.ics (testing zimbra freebusy)': {
topic: function () {
return ical.parseFile('./test/test11.ics');
},
'freebusy params' : {
topic: function(events) {
return _.values(events)[0];
}
, 'has a URL' : function(topic) {
assert.equal(topic.url, 'http://mail.example.com/yvr-2a@example.com/20140416');
}
, 'has an ORGANIZER' : function(topic) {
assert.equal(topic.organizer, 'mailto:yvr-2a@example.com');
}
, 'has an start datetime' : function(topic) {
assert.equal(topic.start.getFullYear(), 2014);
assert.equal(topic.start.getMonth(), 3);
}
, 'has an end datetime' : function(topic) {
assert.equal(topic.end.getFullYear(), 2014);
assert.equal(topic.end.getMonth(), 6);
}
}
, 'freebusy busy events' : {
topic: function(events) {
return _.select(_.values(events)[0].freebusy, function(x) {
return x.type === 'BUSY';
})[0];
}
, 'has an start datetime' : function(topic) {
assert.equal(topic.start.getFullYear(), 2014);
assert.equal(topic.start.getMonth(), 3);
assert.equal(topic.start.getUTCHours(), 15);
assert.equal(topic.start.getUTCMinutes(), 15);
}
, 'has an end datetime' : function(topic) {
assert.equal(topic.end.getFullYear(), 2014);
assert.equal(topic.end.getMonth(), 3);
assert.equal(topic.end.getUTCHours(), 19);
assert.equal(topic.end.getUTCMinutes(), 00);
}
}
}
, 'with test12.ics (testing recurrences and exdates)': {
topic: function () {
return ical.parseFile('./test/test12.ics')
}
, 'event with rrule': {
topic: function (events) {
return _.select(_.values(events), function (x) {
return x.uid === '0000001';
})[0];
}
, "Has an RRULE": function (topic) {
assert.notEqual(topic.rrule, undefined);
}
, "Has summary Treasure Hunting": function (topic) {
assert.equal(topic.summary, 'Treasure Hunting');
}
, "Has two EXDATES": function (topic) {
assert.notEqual(topic.exdate, undefined);
assert.notEqual(topic.exdate[new Date(2015, 06, 08, 12, 0, 0).toISOString().substring(0, 10)], undefined);
assert.notEqual(topic.exdate[new Date(2015, 06, 10, 12, 0, 0).toISOString().substring(0, 10)], undefined);
}
, "Has a RECURRENCE-ID override": function (topic) {
assert.notEqual(topic.recurrences, undefined);
assert.notEqual(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString().substring(0, 10)], undefined);
assert.equal(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString().substring(0, 10)].summary, 'More Treasure Hunting');
}
}
}
, 'with test13.ics (testing recurrence-id before rrule)': {
topic: function () {
return ical.parseFile('./test/test13.ics')
}
, 'event with rrule': {
topic: function (events) {
return _.select(_.values(events), function (x) {
return x.uid === '6m2q7kb2l02798oagemrcgm6pk@google.com';
})[0];
}
, "Has an RRULE": function (topic) {
assert.notEqual(topic.rrule, undefined);
}
, "Has summary 'repeated'": function (topic) {
assert.equal(topic.summary, 'repeated');
}
, "Has a RECURRENCE-ID override": function (topic) {
assert.notEqual(topic.recurrences, undefined);
assert.notEqual(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString().substring(0, 10)], undefined);
assert.equal(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString().substring(0, 10)].summary, 'bla bla');
}
}
}
, 'with test14.ics (testing comma-separated exdates)': {
topic: function () {
return ical.parseFile('./test/test14.ics')
}
, 'event with comma-separated exdate': {
topic: function (events) {
return _.select(_.values(events), function (x) {
return x.uid === '98765432-ABCD-DCBB-999A-987765432123';
})[0];
}
, "Has summary 'Example of comma-separated exdates'": function (topic) {
assert.equal(topic.summary, 'Example of comma-separated exdates');
}
, "Has four comma-separated EXDATES": function (topic) {
assert.notEqual(topic.exdate, undefined);
// Verify the four comma-separated EXDATES are there
assert.notEqual(topic.exdate[new Date(2017, 6, 6, 12, 0, 0).toISOString().substring(0, 10)], undefined);
assert.notEqual(topic.exdate[new Date(2017, 6, 17, 12, 0, 0).toISOString().substring(0, 10)], undefined);
assert.notEqual(topic.exdate[new Date(2017, 6, 20, 12, 0, 0).toISOString().substring(0, 10)], undefined);
assert.notEqual(topic.exdate[new Date(2017, 7, 3, 12, 0, 0).toISOString().substring(0, 10)], undefined);
// Verify an arbitrary date isn't there
assert.equal(topic.exdate[new Date(2017, 4, 5, 12, 0, 0).toISOString().substring(0, 10)], undefined);
}
}
}
, 'with test14.ics (testing exdates with bad times)': {
topic: function () {
return ical.parseFile('./test/test14.ics')
}
, 'event with exdates with bad times': {
topic: function (events) {
return _.select(_.values(events), function (x) {
return x.uid === '1234567-ABCD-ABCD-ABCD-123456789012';
})[0];
}
, "Has summary 'Example of exdate with bad times'": function (topic) {
assert.equal(topic.summary, 'Example of exdate with bad times');
}
, "Has two EXDATES even though they have bad times": function (topic) {
assert.notEqual(topic.exdate, undefined);
// Verify the two EXDATES are there, even though they have bad times
assert.notEqual(topic.exdate[new Date(2017, 11, 18, 12, 0, 0).toISOString().substring(0, 10)], undefined);
assert.notEqual(topic.exdate[new Date(2017, 11, 19, 12, 0, 0).toISOString().substring(0, 10)], undefined);
}
}
}
, 'url request errors': {
topic : function () {
ical.fromURL('http://255.255.255.255/', {}, this.callback);
}
, 'are passed back to the callback' : function (err, result) {
assert.instanceOf(err, Error);
if (!err){
console.log(">E:", err, result)
}
}
}
}).export(module)
//ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics',
// {},
// function(err, data){
// console.log("OUT:", data)
// })

View File

@@ -1,78 +0,0 @@
BEGIN:VCALENDAR
PRODID:-//lanyrd.com//Lanyrd//EN
X-ORIGINAL-URL:http://lanyrd.com/topics/nodejs/nodejs.ics
X-WR-CALNAME;CHARSET=utf-8:Node.js conferences
VERSION:2.0
METHOD:PUBLISH
BEGIN:VEVENT
SUMMARY;CHARSET=utf-8:Dyncon 2011
LOCATION;CHARSET=utf-8:Stockholm, Sweden
URL:http://lanyrd.com/2011/dyncon/
UID:d4c826dfb701f611416d69b4df81caf9ff80b03a
DTSTART:20110312T200000Z
DTEND;VALUE=DATE:20110314
END:VEVENT
BEGIN:VEVENT
SUMMARY;CHARSET=utf-8:[Async]: Everything Express
LOCATION;CHARSET=utf-8:Brighton, United Kingdom
URL:http://lanyrd.com/2011/asyncjs-express/
UID:480a3ad48af5ed8965241f14920f90524f533c18
DTSTART;VALUE=DATE:20110324
DTEND;VALUE=DATE:20110325
END:VEVENT
BEGIN:VEVENT
SUMMARY;CHARSET=utf-8:JSConf US 2011
LOCATION;CHARSET=utf-8:Portland, United States
URL:http://lanyrd.com/2011/jsconf/
UID:ed334cc85db5ebdff5ff5a630a7a48631a677dbe
DTSTART;VALUE=DATE:20110502
DTEND;VALUE=DATE:20110504
END:VEVENT
BEGIN:VEVENT
SUMMARY;CHARSET=utf-8:NodeConf 2011
LOCATION;CHARSET=utf-8:Portland, United States
URL:http://lanyrd.com/2011/nodeconf/
UID:25169a7b1ba5c248278f47120a40878055dc8c15
DTSTART;VALUE=DATE:20110505
DTEND;VALUE=DATE:20110506
END:VEVENT
BEGIN:VEVENT
SUMMARY;CHARSET=utf-8:BrazilJS
LOCATION;CHARSET=utf-8:Fortaleza, Brazil
URL:http://lanyrd.com/2011/braziljs/
UID:dafee3be83624f3388c5635662229ff11766bb9c
DTSTART;VALUE=DATE:20110513
DTEND;VALUE=DATE:20110515
END:VEVENT
BEGIN:VEVENT
SUMMARY;CHARSET=utf-8:Falsy Values
LOCATION;CHARSET=utf-8:Warsaw, Poland
URL:http://lanyrd.com/2011/falsy-values/
UID:73cad6a09ac4e7310979c6130f871d17d990b5ad
DTSTART;VALUE=DATE:20110518
DTEND;VALUE=DATE:20110521
END:VEVENT
BEGIN:VEVENT
SUMMARY;CHARSET=utf-8:nodecamp.eu
LOCATION;CHARSET=utf-8:Cologne, Germany
URL:http://lanyrd.com/2011/nodecampde/
UID:b728a5fdb5f292b6293e4a2fd97a1ccfc69e9d6f
DTSTART;VALUE=DATE:20110611
DTEND;VALUE=DATE:20110613
END:VEVENT
BEGIN:VEVENT
SUMMARY;CHARSET=utf-8:Rich Web Experience 2011
LOCATION;CHARSET=utf-8:Fort Lauderdale, United States
URL:http://lanyrd.com/2011/rich-web-experience/
UID:47f6ea3f28af2986a2192fa39a91fa7d60d26b76
DTSTART;VALUE=DATE:20111129
DTEND;VALUE=DATE:20111203
END:VEVENT
BEGIN:VEVENT
SUMMARY;CHARSET=utf-8:Foobar
UID:sdfkf09fsd0
DTSTART;VALUE=DATE:Next Year
DTEND;VALUE=DATE:20111203
END:VEVENT
END:VCALENDAR

View File

@@ -1,34 +0,0 @@
BEGIN:VCALENDAR
BEGIN:VEVENT
UID:1
SUMMARY:Event with a category
DESCRIPTION:Details for an event with a category
CATEGORIES:cat1,cat2,cat3
END:VEVENT
BEGIN:VEVENT
UID:2
SUMMARY:Event with a category
DESCRIPTION:Details for an event with a category
CATEGORIES:cat1 , cat2, cat3
END:VEVENT
BEGIN:VEVENT
UID:3
SUMMARY:Event with a category
DESCRIPTION:Details for an event with a category
CATEGORIES:
END:VEVENT
BEGIN:VEVENT
UID:4
SUMMARY:Event with a category
DESCRIPTION:Details for an event with a category
CATEGORIES:lonely-cat
END:VEVENT
BEGIN:VEVENT
UID:5
SUMMARY:Event with a category
DESCRIPTION:Details for an event with a category
CATEGORIES:cat1
CATEGORIES:cat2
CATEGORIES:cat3
END:VEVENT
END:VCALENDAR

View File

@@ -1,41 +0,0 @@
BEGIN:VCALENDAR
PRODID:Zimbra-Calendar-Provider
VERSION:2.0
METHOD:PUBLISH
BEGIN:VFREEBUSY
ORGANIZER:mailto:yvr-2a@example.com
DTSTAMP:20140516T235436Z
DTSTART:20140415T235436Z
DTEND:20140717T235436Z
URL:http://mail.example.com/yvr-2a@example.com/20140416
FREEBUSY;FBTYPE=BUSY:20140416T151500Z/20140416T190000Z
FREEBUSY;FBTYPE=BUSY:20140416T195500Z/20140416T231500Z
FREEBUSY;FBTYPE=BUSY:20140417T193000Z/20140417T203000Z
FREEBUSY;FBTYPE=BUSY:20140421T210000Z/20140421T213000Z
FREEBUSY;FBTYPE=BUSY:20140423T180000Z/20140423T190000Z
FREEBUSY;FBTYPE=BUSY:20140423T200000Z/20140423T210000Z
FREEBUSY;FBTYPE=BUSY:20140423T223500Z/20140423T231500Z
FREEBUSY;FBTYPE=BUSY:20140424T155000Z/20140424T165500Z
FREEBUSY;FBTYPE=BUSY:20140424T170000Z/20140424T183000Z
FREEBUSY;FBTYPE=BUSY:20140424T195000Z/20140424T230000Z
FREEBUSY;FBTYPE=BUSY:20140425T144500Z/20140425T161500Z
FREEBUSY;FBTYPE=BUSY:20140425T180000Z/20140425T194500Z
FREEBUSY;FBTYPE=BUSY:20140425T223000Z/20140425T230000Z
FREEBUSY;FBTYPE=BUSY:20140428T151500Z/20140428T163000Z
FREEBUSY;FBTYPE=BUSY:20140428T170000Z/20140428T173000Z
FREEBUSY;FBTYPE=BUSY:20140428T195500Z/20140428T213000Z
FREEBUSY;FBTYPE=BUSY:20140428T231000Z/20140428T234000Z
FREEBUSY;FBTYPE=BUSY:20140429T152500Z/20140429T170000Z
FREEBUSY;FBTYPE=BUSY:20140429T180000Z/20140429T183000Z
FREEBUSY;FBTYPE=BUSY:20140429T201500Z/20140429T230000Z
FREEBUSY;FBTYPE=BUSY:20140430T162500Z/20140430T165500Z
FREEBUSY;FBTYPE=BUSY:20140430T180000Z/20140430T190000Z
FREEBUSY;FBTYPE=BUSY:20140501T170000Z/20140501T173000Z
FREEBUSY;FBTYPE=BUSY:20140501T175000Z/20140501T190000Z
FREEBUSY;FBTYPE=BUSY:20140501T232000Z/20140501T235000Z
FREEBUSY;FBTYPE=BUSY:20140502T163500Z/20140502T173000Z
FREEBUSY;FBTYPE=BUSY:20140505T165500Z/20140505T173000Z
FREEBUSY;FBTYPE=BUSY:20140505T201500Z/20140505T203000Z
FREEBUSY;FBTYPE=BUSY:20140505T210000Z/20140505T213000Z
END:VFREEBUSY
END:VCALENDAR

View File

@@ -1,19 +0,0 @@
BEGIN:VCALENDAR
BEGIN:VEVENT
UID:0000001
SUMMARY:Treasure Hunting
DTSTART;TZID=America/Los_Angeles:20150706T120000
DTEND;TZID=America/Los_Angeles:20150706T130000
RRULE:FREQ=DAILY;COUNT=10
EXDATE;TZID=America/Los_Angeles:20150708T120000
EXDATE;TZID=America/Los_Angeles:20150710T120000
END:VEVENT
BEGIN:VEVENT
UID:0000001
SUMMARY:More Treasure Hunting
LOCATION:The other island
DTSTART;TZID=America/Los_Angeles:20150709T150000
DTEND;TZID=America/Los_Angeles:20150707T160000
RECURRENCE-ID;TZID=America/Los_Angeles:20150707T120000
END:VEVENT
END:VCALENDAR

View File

@@ -1,57 +0,0 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:ical
X-WR-TIMEZONE:Europe/Kiev
X-WR-CALDESC:
BEGIN:VTIMEZONE
TZID:Europe/Kiev
X-LIC-LOCATION:Europe/Kiev
BEGIN:DAYLIGHT
TZOFFSETFROM:+0200
TZOFFSETTO:+0300
TZNAME:EEST
DTSTART:19700329T030000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0300
TZOFFSETTO:+0200
TZNAME:EET
DTSTART:19701025T040000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Kiev:20160826T140000
DTEND;TZID=Europe/Kiev:20160826T150000
DTSTAMP:20160825T061505Z
UID:6m2q7kb2l02798oagemrcgm6pk@google.com
RECURRENCE-ID;TZID=Europe/Kiev:20160826T140000
CREATED:20160823T125221Z
DESCRIPTION:
LAST-MODIFIED:20160823T130320Z
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:bla bla
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=Europe/Kiev:20160825T140000
DTEND;TZID=Europe/Kiev:20160825T150000
RRULE:FREQ=DAILY;UNTIL=20160828T110000Z
DTSTAMP:20160825T061505Z
UID:6m2q7kb2l02798oagemrcgm6pk@google.com
CREATED:20160823T125221Z
DESCRIPTION:
LAST-MODIFIED:20160823T125221Z
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:repeated
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR

View File

@@ -1,33 +0,0 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:ical
X-WR-TIMEZONE:Europe/Kiev
X-WR-CALDESC:
BEGIN:VEVENT
UID:98765432-ABCD-DCBB-999A-987765432123
DTSTART;TZID=US/Central:20170216T090000
DTEND;TZID=US/Central:20170216T190000
DTSTAMP:20170727T044436Z
EXDATE;TZID=US/Central:20170706T090000,20170717T090000,20170720T090000,20
170803T090000
LAST-MODIFIED:20170727T044435Z
RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20170814T045959Z;INTERVAL=2;BYDAY=MO,TH
SEQUENCE:0
SUMMARY:Example of comma-separated exdates
END:VEVENT
BEGIN:VEVENT
UID:1234567-ABCD-ABCD-ABCD-123456789012
DTSTART:20170814T140000Z
DTEND:20170815T000000Z
DTSTAMP:20171204T134925Z
EXDATE:20171219T060000
EXDATE:20171218T060000
LAST-MODIFIED:20171024T140004Z
RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU
SEQUENCE:0
SUMMARY:Example of exdate with bad times
END:VEVENT
END:VCALENDAR

View File

@@ -1,83 +0,0 @@
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
X-WR-TIMEZONE;VALUE=TEXT:US/Pacific
METHOD:PUBLISH
PRODID:-//Apple Computer\, Inc//iCal 1.0//EN
X-WR-CALNAME;VALUE=TEXT:Example
VERSION:2.0
BEGIN:VEVENT
SEQUENCE:5
DTSTART;TZID=US/Pacific:20021028T140000
DTSTAMP:20021028T011706Z
SUMMARY:Coffee with Jason
UID:EC9439B1-FF65-11D6-9973-003065F99D04
DTEND;TZID=US/Pacific:20021028T150000
END:VEVENT
BEGIN:VALARM
TRIGGER;VALUE=DURATION:-P1D
ACTION:DISPLAY
DESCRIPTION:Event reminder
END:VALARM
BEGIN:VEVENT
SEQUENCE:1
DTSTAMP:20021128T012034Z
SUMMARY:Code Review
UID:EC944331-FF65-11D6-9973-003065F99D04
DTSTART;TZID=US/Pacific:20021127T120000
DURATION:PT1H
END:VEVENT
BEGIN:VEVENT
SEQUENCE:1
DTSTAMP:20021028T012034Z
SUMMARY:Dinner with T
UID:EC944CFA-FF65-11D6-9973-003065F99D04
DTSTART;TZID=US/Pacific:20021216T200000
DURATION:PT1H
END:VEVENT
BEGIN:VTODO
DTSTAMP:19980130T134500Z
SEQUENCE:2
UID:uid4@host1.com
ORGANIZER:MAILTO:unclesam@us.gov
ATTENDEE;PARTSTAT=ACCEPTED:MAILTO:jqpublic@host.com
DUE:19980415T235959
STATUS:NEEDS-ACTION
SUMMARY:Submit Income Taxes
END:VTODO
BEGIN:VALARM
ACTION:AUDIO
TRIGGER:19980403T120000
ATTACH;FMTTYPE=audio/basic:http://host.com/pub/audio-
files/ssbanner.aud
REPEAT:4
DURATION:PT1H
END:VALARM
BEGIN:VJOURNAL
DTSTAMP:19970324T120000Z
UID:uid5@host1.com
ORGANIZER:MAILTO:jsmith@host.com
STATUS:DRAFT
CLASS:PUBLIC
CATEGORY:Project Report, XYZ, Weekly Meeting
DESCRIPTION:Project xyz Review Meeting Minutes\n
Agenda\n1. Review of project version 1.0 requirements.\n2.
Definition
of project processes.\n3. Review of project schedule.\n
Participants: John Smith, Jane Doe, Jim Dandy\n-It was
decided that the requirements need to be signed off by
product marketing.\n-Project processes were accepted.\n
-Project schedule needs to account for scheduled holidays
and employee vacation time. Check with HR for specific
dates.\n-New schedule will be distributed by Friday.\n-
Next weeks meeting is cancelled. No meeting until 3/23.
END:VJOURNAL
BEGIN:VFREEBUSY
ORGANIZER:MAILTO:jsmith@host.com
DTSTART:19980313T141711Z
DTEND:19980410T141711Z
FREEBUSY:19980314T233000Z/19980315T003000Z
FREEBUSY:19980316T153000Z/19980316T163000Z
FREEBUSY:19980318T030000Z/19980318T040000Z
URL:http://www.host.com/calendar/busytime/jsmith.ifb
END:VFREEBUSY
END:VCALENDAR

View File

@@ -1,226 +0,0 @@
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
PRODID:tvcountdown.com
X-WR-CALNAME:tvcountdown.com
VERSION:2.0
METHOD:PUBLISH
X-WR-TIMEZONE:US/Eastern
X-WR-CALNAME;VALUE=TEXT:tvcountdown.com
X-WR-CALDESC:
BEGIN:VEVENT
UID:20110519T200000Z-79@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110519T200000
DTEND;VALUE=DATE-TIME:20110519T203000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Big Bang Theory - S04E24 - The Roomate Transmogrfication
END:VEVENT
BEGIN:VEVENT
UID:20110512T200000Z-79@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110512T200000
DTEND;VALUE=DATE-TIME:20110512T203000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Big Bang Theory - S04E23 - The Engagement Reaction
END:VEVENT
BEGIN:VEVENT
UID:20110505T220000Z-83@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110505T220000
DTEND;VALUE=DATE-TIME:20110505T223000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:30 Rock - S05E23 - Respawn
END:VEVENT
BEGIN:VEVENT
UID:20110505T200000Z-79@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110505T200000
DTEND;VALUE=DATE-TIME:20110505T203000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Big Bang Theory - S04E22 - The Wildebeest Implementation
END:VEVENT
BEGIN:VEVENT
UID:20110504T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110504T230000
DTEND;VALUE=DATE-TIME:20110504T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E59 - David Barton
END:VEVENT
BEGIN:VEVENT
UID:20110503T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110503T230000
DTEND;VALUE=DATE-TIME:20110503T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E58 - Rachel Maddow
END:VEVENT
BEGIN:VEVENT
UID:20110502T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110502T230000
DTEND;VALUE=DATE-TIME:20110502T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E57 - Philip K. Howard
END:VEVENT
BEGIN:VEVENT
UID:20110428T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110428T230000
DTEND;VALUE=DATE-TIME:20110428T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E56 - William Cohan
END:VEVENT
BEGIN:VEVENT
UID:20110428T220000Z-83@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110428T220000
DTEND;VALUE=DATE-TIME:20110428T223000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:30 Rock - S05E22 - Everything Sunny All the Time Always
END:VEVENT
BEGIN:VEVENT
UID:20110428T200000Z-79@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110428T200000
DTEND;VALUE=DATE-TIME:20110428T203000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Big Bang Theory - S04E21 - The Agreement Dissection
END:VEVENT
BEGIN:VEVENT
UID:20110427T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110427T230000
DTEND;VALUE=DATE-TIME:20110427T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E55 - Sen. Bernie Sanders
END:VEVENT
BEGIN:VEVENT
UID:20110426T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110426T230000
DTEND;VALUE=DATE-TIME:20110426T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E54 - Elizabeth Warren
END:VEVENT
BEGIN:VEVENT
UID:20110425T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110425T230000
DTEND;VALUE=DATE-TIME:20110425T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E53 - Gigi Ibrahim
END:VEVENT
BEGIN:VEVENT
UID:20110421T220000Z-83@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110421T220000
DTEND;VALUE=DATE-TIME:20110421T223000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:30 Rock - S05E21 - 100th Episode Part 2 of 2
END:VEVENT
BEGIN:VEVENT
UID:20110421T220000Z-83@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110421T220000
DTEND;VALUE=DATE-TIME:20110421T223000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:30 Rock - S05E20 - 100th Episode Part 1 of 2
END:VEVENT
BEGIN:VEVENT
UID:20110414T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110414T230000
DTEND;VALUE=DATE-TIME:20110414T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E52 - Ricky Gervais
END:VEVENT
BEGIN:VEVENT
UID:20110414T220000Z-83@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110414T220000
DTEND;VALUE=DATE-TIME:20110414T223000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:30 Rock - S05E19 - I Heart Connecticut
END:VEVENT
BEGIN:VEVENT
UID:20110413T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110413T230000
DTEND;VALUE=DATE-TIME:20110413T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E51 - Tracy Morgan
END:VEVENT
BEGIN:VEVENT
UID:20110412T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110412T230000
DTEND;VALUE=DATE-TIME:20110412T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E50 - Gov. Deval Patrick
END:VEVENT
BEGIN:VEVENT
UID:20110411T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110411T230000
DTEND;VALUE=DATE-TIME:20110411T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E49 - Foo Fighters
END:VEVENT
BEGIN:VEVENT
UID:20110407T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110407T230000
DTEND;VALUE=DATE-TIME:20110407T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E48 - Jamie Oliver
END:VEVENT
BEGIN:VEVENT
UID:20110407T200000Z-79@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110407T200000
DTEND;VALUE=DATE-TIME:20110407T203000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Big Bang Theory - S04E20 - The Herb Garden Germination
END:VEVENT
BEGIN:VEVENT
UID:20110406T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110406T230000
DTEND;VALUE=DATE-TIME:20110406T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E47 - Mike Huckabee
END:VEVENT
BEGIN:VEVENT
UID:20110405T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110405T230000
DTEND;VALUE=DATE-TIME:20110405T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E46 - Colin Quinn
END:VEVENT
BEGIN:VEVENT
UID:20110404T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110404T230000
DTEND;VALUE=DATE-TIME:20110404T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E45 - Billy Crystal
END:VEVENT
BEGIN:VEVENT
UID:20110331T230000Z-289@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110331T230000
DTEND;VALUE=DATE-TIME:20110331T233000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Daily Show - S16E44 - Norm MacDonald
END:VEVENT
BEGIN:VEVENT
UID:20110331T200000Z-79@tvcountdown.com
DTSTART;VALUE=DATE-TIME:20110331T200000
DTEND;VALUE=DATE-TIME:20110331T203000
DTSTAMP:20110430T192946Z
URL;VALUE=URI:
SUMMARY:The Big Bang Theory - S04E19 - The Zarnecki Incursion
END:VEVENT
END:VCALENDAR

View File

@@ -1,747 +0,0 @@
BEGIN:VCALENDAR
X-WR-CALNAME:John Doe (TripIt)
X-WR-CALDESC:TripIt Calendar
X-PUBLISHED-TTL:PT15M
PRODID:-//John Doe/NONSGML Bennu 0.1//EN
VERSION:2.0
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
UID:c32a5eaba2354bb29e012ec18da827db90550a3b@tripit.com
DTSTART;VALUE=DATE:20111011
DTEND;VALUE=DATE:20111014
SUMMARY:South San Francisco\, CA\, October 2011\;
LOCATION:South San Francisco\, CA
GEO:37.654656;-122.40775
TRANSP:TRANSPARENT
DESCRIPTION:John Doe is in South San Francisco\, CA from Oct 11
to Oct 13\, 2011\nView and/or edit details in TripIt : http://www.tripit.c
om/trip/show/id/23710889\nTripIt - organize your travel at http://www.trip
it.com\n
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
TRANSP:TRANSPARENT
UID:item-ee275ccffa83f492d9eb63b01953b39f18d4f944@tripit.com
DTSTART:20111011T100500
DTEND:20111011T110500
SUMMARY:Directions from SFO to Embassy Suites San Francisco Airport - Sout
h San Francisco
LOCATION:250 GATEWAY BLVD\, South San Francisco\, CA\, 94080
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/1234\n \n[Directions] 10/11/2011 10:05am - Directions from S
FO to Embassy Suites San Francisco Airport - South San Francisco \nfrom: S
FO \nto: 250 GATEWAY BLVD\, South San Francisco\, CA\, 94080 \nView direct
ions here: http://maps.google.com/maps?output=mobile&saddr=SFO&daddr=250+G
ATEWAY+BLVD%2C+South+San+Francisco%2C+CA%2C+94080 \n \n \n\nTripIt - organ
ize your travel at http://www.tripit.com
GEO:37.655634;-122.401273
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20111011T165500Z
SUMMARY:US403 PHX to SFO
LOCATION:Phoenix (PHX)
UID:item-c576afd397cf1f90578b4ba35e781b61ba8897db@tripit.com
DTSTART:20111011T144500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/1234\n \n[Flight] 10/11/2011 US Airways(US) #403 dep PHX 7:4
5am MST arr SFO 9:55am PDT\; John Doe\; seat(s) 8B\; conf #DXH9K
Z\, BXQ9WH \nBooked on http://www.americanexpress-travel.com/\; Reference
#: 4127 8626 9715\; http://www.americanexpress-travel.com/\; US:1-800-297-
2977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at htt
p://www.tripit.com
GEO:37.618889;-122.375
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Pick-up Rental Car: Dollar Rent A Car
TRANSP:TRANSPARENT
UID:item-e99a90ee1c7e4f5b68a4e551009e5bb6c475940c@tripit.com
DTSTART:20111011T172500Z
DTEND:20111011T182500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/1234\n \n[Car Rental] Dollar Rent A Car\; San Francisco Inte
rnational Airport\; primary driver John Doe\; conf #R9508361 \np
ickup 10/11/2011 10:25am\; dropoff 10/13/2011 6:49pm \nEconomy \nBooked on
http://www.americanexpress-travel.com/\; Reference #: 4127 8626 9715\; ht
tp://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582
-2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Check-in: Embassy Suites San Francisco Airport - South San Francis
co
TRANSP:TRANSPARENT
UID:item-7f3288d418bed063cc82b4512e792fbb5d8ae761@tripit.com
DTSTART:20111011T185500Z
DTEND:20111011T195500Z
LOCATION:250 GATEWAY BLVD\, South San Francisco\, CA\, 94080
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/23710889\n \n[Lodging] Embassy Suites San Francisco Airport - So
uth San Francisco\; primary guest John Doe\; conf #R9508361 \n25
0 GATEWAY BLVD\, South San Francisco\, CA\, 94080\; tel 1.650.589.3400 \na
rrive 10/11/2011\; depart 10/13/2011\; rooms: 1 \nBooked on http://www.ame
ricanexpress-travel.com/\; Reference #: 4127 8626 9715\; http://www.americ
anexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n \n \n\
nTripIt - organize your travel at http://www.tripit.com
GEO:37.655634;-122.401273
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Check-out: Embassy Suites San Francisco Airport - South San Franci
sco
TRANSP:TRANSPARENT
UID:item-5eb4cb5fc25c55b0423921e18336e57f8c34598d@tripit.com
DTSTART:20111014T011900Z
DTEND:20111014T021900Z
LOCATION:250 GATEWAY BLVD\, South San Francisco\, CA\, 94080
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/23710889\n \n[Lodging] Embassy Suites San Francisco Airport - So
uth San Francisco\; primary guest John Doe\; conf #R9508361 \n25
0 GATEWAY BLVD\, South San Francisco\, CA\, 94080\; tel 1.650.589.3400 \na
rrive 10/11/2011\; depart 10/13/2011\; rooms: 1 \nBooked on http://www.ame
ricanexpress-travel.com/\; Reference #: 4127 8626 9715\; http://www.americ
anexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n \n \n\
nTripIt - organize your travel at http://www.tripit.com
GEO:37.655634;-122.401273
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Drop-off Rental Car: Dollar Rent A Car
TRANSP:TRANSPARENT
UID:item-11fdbf5d02e84646025716d9f9c7a4158e1fb025@tripit.com
DTSTART:20111014T014900Z
DTEND:20111014T024900Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/23710889\n \n[Car Rental] Dollar Rent A Car\; San Francisco Inte
rnational Airport\; primary driver John Doe\; conf #R9508361 \np
ickup 10/11/2011 10:25am\; dropoff 10/13/2011 6:49pm \nEconomy \nBooked on
http://www.americanexpress-travel.com/\; Reference #: 4127 8626 9715\; ht
tp://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582
-2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20111014T051900Z
SUMMARY:CO6256 SFO to PHX
LOCATION:San Francisco (SFO)
UID:item-cb485a571a01972d6bdc74c2b829905d6e3786bf@tripit.com
DTSTART:20111014T031900Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/23710889\n \n[Flight] 10/13/2011 Continental Airlines(CO) #6256
dep SFO 8:19pm PDT arr PHX 10:19pm MST\; John Doe\; conf #DXH9KZ
\, BXQ9WH(Operated by United Airlines flight 6256) \nBooked on http://www.
americanexpress-travel.com/\; Reference #: 4127 8626 9715\; http://www.ame
ricanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n \n
\n\nTripIt - organize your travel at http://www.tripit.com
GEO:33.436111;-112.009444
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
UID:c7b133db1e7be2713a4a63b75dcbad209690cab5@tripit.com
DTSTART;VALUE=DATE:20111023
DTEND;VALUE=DATE:20111028
SUMMARY:Santa Barbara\, CA\, October 2011
LOCATION:Santa Barbara\, CA
GEO:34.420831;-119.69819
TRANSP:TRANSPARENT
DESCRIPTION:John Doe is in Santa Barbara\, CA from Oct 23 to Oct
27\, 2011\nView and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24259445\nTripIt - organize your travel at http://www.tripit.com
\n
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20111023T191200Z
SUMMARY:US2719 PHX to SBA
LOCATION:Phoenix (PHX)
UID:item-c4375369e9070fcc04df39ed18c4d93087577591@tripit.com
DTSTART:20111023T173500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24259445\n \n[Flight] 10/23/2011 US Airways(US) #2719 dep PHX 10
:35am MST arr SBA 12:12pm PDT\; John Doe Ticket #0378717202638\;
conf #A44XS5\, PRX98G\, FYYJZ4 \nBooked on http://www.americanexpress-tra
vel.com/\; Reference #: 7128 8086 8504\; http://www.americanexpress-travel
.com/\; US:1-800-297-2977\, Outside:210-582-2716\; Total Cost: $699.99 \n
\n \n\nTripIt - organize your travel at http://www.tripit.com
GEO:34.427778;-119.839444
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
TRANSP:TRANSPARENT
UID:item-962e4f045d12149319d1837ec096bf43770abd6e@tripit.com
DTSTART:20111025T094000
DTEND:20111025T104000
SUMMARY:Directions from Hertz to Sofitel San Francisco Bay
LOCATION:223 Twin Dolphin Drive\, Redwood City\, CA\, 94065
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24259445\n \n[Directions] 10/25/2011 9:40am - Directions from He
rtz to Sofitel San Francisco Bay \nfrom: 780 McDonnell Road\, San Francisc
o\, CA\, 94128 \nto: 223 Twin Dolphin Drive\, Redwood City\, CA\, 94065 \n
View directions here: http://maps.google.com/maps?output=mobile&saddr=780+
McDonnell+Road%2C+San+Francisco%2C+CA%2C+94128&daddr=223+Twin+Dolphin+Driv
e%2C+Redwood+City%2C+CA%2C+94065 \n \n \n\nTripIt - organize your travel a
t http://www.tripit.com
GEO:37.5232475;-122.261296
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20111025T162600Z
SUMMARY:UA5304 SBA to SFO
LOCATION:Santa Barbara (SBA)
UID:item-ae300a6934c3820974dba2c9c5b8fae843c67693@tripit.com
DTSTART:20111025T150900Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24259445\n \n[Flight] 10/25/2011 United Airlines(UA) #5304 dep S
BA 8:09am PDT arr SFO 9:26am PDT\; John Doe Ticket #037871720263
8\; seat(s) 11B\; conf #A44XS5\, PRX98G\, FYYJZ4 \nBooked on http://www.am
ericanexpress-travel.com/\; Reference #: 7128 8086 8504\; http://www.ameri
canexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716\; Total
Cost: $699.99 \n \n \n\nTripIt - organize your travel at http://www.tripit
.com
GEO:37.618889;-122.375
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Pick-up Rental Car: Hertz
TRANSP:TRANSPARENT
UID:item-2a9fd5a57a4cdda4677fc6ce23738e1954fdbe2a@tripit.com
DTSTART:20111025T163000Z
DTEND:20111025T173000Z
LOCATION:780 McDonnell Road\, San Francisco\, CA\, 94128
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24259445\n \n[Car Rental] Hertz\; San Francisco International Ai
rport\; primary driver John Doe\; conf #F2633064194 \n780 McDonn
ell Road\, San Francisco\, CA\, 94128 \npickup 10/25/2011 9:30am\; dropoff
10/27/2011 7:00pm \nToyota Corolla or similar\; 84.57 USD \nBooked on htt
p://www.hertz.com/\; Reference #: F2633064194\; http://www.hertz.com/\; 80
0-654-3131\; Booking Rate: 84.57 USD\; Total Cost: 333.76 USD \n \n \n\nTr
ipIt - organize your travel at http://www.tripit.com
GEO:37.6297569;-122.4000351
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
TRANSP:TRANSPARENT
UID:item-98dfcb0bcfdcffcce9c58a84947212ed67cadda6@tripit.com
DTSTART:20111025T163600Z
DTEND:20111025T173600Z
SUMMARY:Directions from SFO to Sofitel San Francisco Bay
LOCATION:223 Twin Dolphin Drive\, Redwood City\, CA\, 94065
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24259445\n \n[Directions] 10/25/2011 9:36am - Directions from SF
O to Sofitel San Francisco Bay \nfrom: SFO \nto: 223 Twin Dolphin Drive\,
Redwood City\, CA\, 94065 \nView directions here: http://maps.google.com/m
aps?output=mobile&saddr=SFO&daddr=223+Twin+Dolphin+Drive%2C+Redwood+City%2
C+CA%2C+94065 \n \n \n\nTripIt - organize your travel at http://www.tripit
.com
GEO:37.5232475;-122.261296
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Check-in: Sofitel San Francisco Bay
TRANSP:TRANSPARENT
UID:item-8de3937b336c333faf2d55ad0a41c5ca6cc02393@tripit.com
DTSTART:20111025T220000Z
DTEND:20111025T230000Z
LOCATION:223 Twin Dolphin Drive\, Redwood City\, CA\, 94065
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24259445\n \n[Lodging] Sofitel San Francisco Bay\; primary guest
John Doe\; conf #F80-0GMW \n223 Twin Dolphin Drive\, Redwood Ci
ty\, CA\, 94065\; tel (+1)650/598-9000 \narrive 10/25/2011\; depart 10/27/
2011\; rooms: 1 \nBooked on http://www.sofitel.com/\; http://www.sofitel.c
om/\; Total Cost: 564.00 USD \n \n \n\nTripIt - organize your travel at ht
tp://www.tripit.com
GEO:37.5232475;-122.261296
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Check-out: Sofitel San Francisco Bay
TRANSP:TRANSPARENT
UID:item-f3ade58646964bde101616a6d26ea7784a1a81e8@tripit.com
DTSTART:20111027T190000Z
DTEND:20111027T200000Z
LOCATION:223 Twin Dolphin Drive\, Redwood City\, CA\, 94065
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24259445\n \n[Lodging] Sofitel San Francisco Bay\; primary guest
John Doe\; conf #F80-0GMW \n223 Twin Dolphin Drive\, Redwood Ci
ty\, CA\, 94065\; tel (+1)650/598-9000 \narrive 10/25/2011\; depart 10/27/
2011\; rooms: 1 \nBooked on http://www.sofitel.com/\; http://www.sofitel.c
om/\; Total Cost: 564.00 USD \n \n \n\nTripIt - organize your travel at ht
tp://www.tripit.com
GEO:37.5232475;-122.261296
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Drop-off Rental Car: Hertz
TRANSP:TRANSPARENT
UID:item-50620273fea0614d37775649034d5e1de92ae361@tripit.com
DTSTART:20111028T020000Z
DTEND:20111028T030000Z
LOCATION:780 McDonnell Road\, San Francisco\, CA\, 94128
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24259445\n \n[Car Rental] Hertz\; San Francisco International Ai
rport\; primary driver John Doe\; conf #F2633064194 \n780 McDonn
ell Road\, San Francisco\, CA\, 94128 \npickup 10/25/2011 9:30am\; dropoff
10/27/2011 7:00pm \nToyota Corolla or similar\; 84.57 USD \nBooked on htt
p://www.hertz.com/\; Reference #: F2633064194\; http://www.hertz.com/\; 80
0-654-3131\; Booking Rate: 84.57 USD\; Total Cost: 333.76 USD \n \n \n\nTr
ipIt - organize your travel at http://www.tripit.com
GEO:37.6297569;-122.4000351
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20111028T051900Z
SUMMARY:CO6256 SFO to PHX
LOCATION:San Francisco (SFO)
UID:item-71d327f30d8beeaf7bf50c8fa63ce16005b9b0df@tripit.com
DTSTART:20111028T031900Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24259445\n \n[Flight] 10/27/2011 Continental Airlines(CO) #6256
dep SFO 8:19pm PDT arr PHX 10:19pm MST\; John Doe Ticket #037871
7202638\; seat(s) 17D\; conf #A44XS5\, PRX98G\, FYYJZ4(Operated by United
Airlines flight 6256) \nBooked on http://www.americanexpress-travel.com/\;
Reference #: 7128 8086 8504\; http://www.americanexpress-travel.com/\; US
:1-800-297-2977\, Outside:210-582-2716\; Total Cost: $699.99 \n \n \n\nTri
pIt - organize your travel at http://www.tripit.com
GEO:33.436111;-112.009444
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
UID:2d4b446e63a94ade7dab0f0e9546b2d1965f011c@tripit.com
DTSTART;VALUE=DATE:20111108
DTEND;VALUE=DATE:20111111
SUMMARY:Redwood City\, CA\, November 2011
LOCATION:Redwood City\, CA
GEO:37.485215;-122.236355
TRANSP:TRANSPARENT
DESCRIPTION:John Doe is in Redwood City\, CA from Nov 8 to Nov 1
0\, 2011\nView and/or edit details in TripIt : http://www.tripit.com/trip/
show/id/24913749\nTripIt - organize your travel at http://www.tripit.com\n
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20111108T175700Z
SUMMARY:US403 PHX to SFO
LOCATION:Phoenix (PHX)
UID:item-7de7d829b2f95991de6d01c3d68f24b84770168c@tripit.com
DTSTART:20111108T154500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24913749\n \n[Flight] 11/8/2011 US Airways(US) #403 dep PHX 8:45
am MST arr SFO 9:57am PST\; John Doe\; seat(s) 21C\; conf #FJDX0
J\, I2W8HW \nBooked on http://www.americanexpress-travel.com/\; Reference
#: 4129 9623 4732\; http://www.americanexpress-travel.com/\; US:1-800-297-
2977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at htt
p://www.tripit.com
GEO:37.618889;-122.375
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Pick-up Rental Car: Dollar Rent A Car
TRANSP:TRANSPARENT
UID:item-1ac6982fefdd79bc5ea849785f415a6291c450b1@tripit.com
DTSTART:20111108T182700Z
DTEND:20111108T192700Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24913749\n \n[Car Rental] Dollar Rent A Car\; San Francisco Inte
rnational Airport\; primary driver John Doe\; conf #Q0058133 \np
ickup 11/8/2011 10:27am\; dropoff 11/10/2011 6:25pm \nEconomy \nBooked on
http://www.americanexpress-travel.com/\; Reference #: 4129 9623 4732\; htt
p://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-
2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Check-in: Sofitel San Francisco Bay
TRANSP:TRANSPARENT
UID:item-126e584ffbefbec32a15ca503f0bdf8d3f9cc2f4@tripit.com
DTSTART:20111108T195700Z
DTEND:20111108T205700Z
LOCATION:223 TWIN DOLPHIN DR\, Redwood City\, CA\, 94065-1514
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24913749\n \n[Lodging] Sofitel San Francisco Bay\; primary guest
John Doe\; conf #Q0058133 \n223 TWIN DOLPHIN DR\, Redwood City\
, CA\, 94065-1514\; tel 1.650.598.9000 \narrive 11/8/2011\; depart 11/10/2
011\; rooms: 1 \nBooked on http://www.americanexpress-travel.com/\; Refere
nce #: 4129 9623 4732\; http://www.americanexpress-travel.com/\; US:1-800-
297-2977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at
http://www.tripit.com
GEO:37.5232475;-122.261296
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Check-out: Sofitel San Francisco Bay
TRANSP:TRANSPARENT
UID:item-ff48c502022356ccaa862ebb61761a0de08a1ce9@tripit.com
DTSTART:20111111T015500Z
DTEND:20111111T025500Z
LOCATION:223 TWIN DOLPHIN DR\, Redwood City\, CA\, 94065-1514
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24913749\n \n[Lodging] Sofitel San Francisco Bay\; primary guest
John Doe\; conf #Q0058133 \n223 TWIN DOLPHIN DR\, Redwood City\
, CA\, 94065-1514\; tel 1.650.598.9000 \narrive 11/8/2011\; depart 11/10/2
011\; rooms: 1 \nBooked on http://www.americanexpress-travel.com/\; Refere
nce #: 4129 9623 4732\; http://www.americanexpress-travel.com/\; US:1-800-
297-2977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at
http://www.tripit.com
GEO:37.5232475;-122.261296
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Drop-off Rental Car: Dollar Rent A Car
TRANSP:TRANSPARENT
UID:item-c0273c03ddbb68a9b05d5d43a489bc318136ca42@tripit.com
DTSTART:20111111T022500Z
DTEND:20111111T032500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24913749\n \n[Car Rental] Dollar Rent A Car\; San Francisco Inte
rnational Airport\; primary driver John Doe\; conf #Q0058133 \np
ickup 11/8/2011 10:27am\; dropoff 11/10/2011 6:25pm \nEconomy \nBooked on
http://www.americanexpress-travel.com/\; Reference #: 4129 9623 4732\; htt
p://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-
2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20111111T055400Z
SUMMARY:CO496 SFO to PHX
LOCATION:San Francisco (SFO)
UID:item-3473cf9275326ac393b37859df3b04306b4849aa@tripit.com
DTSTART:20111111T035500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/24913749\n \n[Flight] 11/10/2011 Continental Airlines(CO) #496 d
ep SFO 7:55pm PST arr PHX 10:54pm MST\; John Doe\; seat(s) 26B\;
conf #FJDX0J\, I2W8HW(Operated by United Airlines flight 496) \nBooked on
http://www.americanexpress-travel.com/\; Reference #: 4129 9623 4732\; ht
tp://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582
-2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
GEO:33.436111;-112.009444
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
UID:4ee5ded058432990e3d8808f48ca851e04923b6d@tripit.com
DTSTART;VALUE=DATE:20111129
DTEND;VALUE=DATE:20111202
SUMMARY:Milpitas\, CA\, November 2011
LOCATION:Milpitas\, CA
GEO:37.428272;-121.906624
TRANSP:TRANSPARENT
DESCRIPTION:John Doe is in Milpitas\, CA from Nov 29 to Dec 1\,
2011\nView and/or edit details in TripIt : http://www.tripit.com/trip/show
/id/25671681\nTripIt - organize your travel at http://www.tripit.com\n
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20111129T172400Z
SUMMARY:US282 PHX to SJC
LOCATION:Phoenix (PHX)
UID:item-644d5973b50d521d50e475ccf5321605d54bd0d5@tripit.com
DTSTART:20111129T152500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/25671681\n \n[Flight] 11/29/2011 US Airways(US) #282 dep PHX 8:2
5am MST arr SJC 9:24am PST\; John Doe\; seat(s) 17C\; conf #DQKD
GY \nBooked on http://www.americanexpress-travel.com/\; Reference #: 4131
3301 9911\; http://www.americanexpress-travel.com/\; US:1-800-297-2977\, O
utside:210-582-2716 \n \n \n\nTripIt - organize your travel at http://www.
tripit.com
GEO:37.361111;-121.925556
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Pick-up Rental Car: Alamo
TRANSP:TRANSPARENT
UID:item-10368bbdbc9b6f26f83098500633cc4eb604c751@tripit.com
DTSTART:20111129T175400Z
DTEND:20111129T185400Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/25671681\n \n[Car Rental] Alamo\; San Jose International Airport
\; primary driver John Doe\; conf #372828149COUNT \npickup 11/29
/2011 9:54am\; dropoff 12/1/2011 5:45pm \nIntermediate \nBooked on http://
www.americanexpress-travel.com/\; Reference #: 4131 3301 9911\; http://www
.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n
\n \n\nTripIt - organize your travel at http://www.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Check-in: The Beverly Heritage Hotel
TRANSP:TRANSPARENT
UID:item-98d8638d3f1c011d03cb8f58b3a14a0f1203339b@tripit.com
DTSTART:20111129T192400Z
DTEND:20111129T202400Z
LOCATION:1820 Barber Lane\, Milpitas\, CA\, 95035
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/25671681\n \n[Lodging] The Beverly Heritage Hotel\; primary gues
t John Doe\; conf #372828149COUNT \n1820 Barber Lane\, Milpitas\
, CA\, 95035\; tel 1.408.943.9080 \narrive 11/29/2011\; depart 12/1/2011\;
rooms: 1 \nBooked on http://www.americanexpress-travel.com/\; Reference #
: 4131 3301 9911\; http://www.americanexpress-travel.com/\; US:1-800-297-2
977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at http
://www.tripit.com
GEO:37.4010467;-121.9116284
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20111201T194400Z
SUMMARY:US273 SJC to PHX
LOCATION:San Jose (SJC)
UID:item-7b9ee9bb4edfe69743e32b33f9be55753956a883@tripit.com
DTSTART:20111201T175900Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/25671681\n \n[Flight] 12/1/2011 US Airways(US) #273 dep SJC 9:59
am PST arr PHX 12:44pm MST\; John Doe Ticket #0378727451156\; co
nf #EMF71T \nBooked on http://www.americanexpress-travel.com/\; Reference
#: 5133 5264 1627\; http://www.americanexpress-travel.com/\; US:1-800-297-
2977\, Outside:210-582-2716\; Total Cost: $316.69 \n \n \n\nTripIt - organ
ize your travel at http://www.tripit.com
GEO:33.436111;-112.009444
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Check-out: The Beverly Heritage Hotel
TRANSP:TRANSPARENT
UID:item-f79f203072002b8f06598dcb2be0e36af17b625b@tripit.com
DTSTART:20111202T011500Z
DTEND:20111202T021500Z
LOCATION:1820 Barber Lane\, Milpitas\, CA\, 95035
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/25671681\n \n[Lodging] The Beverly Heritage Hotel\; primary gues
t John Doe\; conf #372828149COUNT \n1820 Barber Lane\, Milpitas\
, CA\, 95035\; tel 1.408.943.9080 \narrive 11/29/2011\; depart 12/1/2011\;
rooms: 1 \nBooked on http://www.americanexpress-travel.com/\; Reference #
: 4131 3301 9911\; http://www.americanexpress-travel.com/\; US:1-800-297-2
977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at http
://www.tripit.com
GEO:37.4010467;-121.9116284
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Drop-off Rental Car: Alamo
TRANSP:TRANSPARENT
UID:item-69f526ad49fa8ca0a74486f4fc77cc3f9d23a72f@tripit.com
DTSTART:20111202T014500Z
DTEND:20111202T024500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/25671681\n \n[Car Rental] Alamo\; San Jose International Airport
\; primary driver John Doe\; conf #372828149COUNT \npickup 11/29
/2011 9:54am\; dropoff 12/1/2011 5:45pm \nIntermediate \nBooked on http://
www.americanexpress-travel.com/\; Reference #: 4131 3301 9911\; http://www
.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n
\n \n\nTripIt - organize your travel at http://www.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20111202T045900Z
SUMMARY:US288 SJC to PHX
LOCATION:San Jose (SJC)
UID:item-dab68a87c8dd49064ab0ba1dec5ba75ba46ff1d3@tripit.com
DTSTART:20111202T031500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/25671681\n \n[Flight] 12/1/2011 US Airways(US) #288 dep SJC 7:15
pm PST arr PHX 9:59pm MST\; John Doe\; seat(s) 13C\; conf #DQKDG
Y \nBooked on http://www.americanexpress-travel.com/\; Reference #: 4131 3
301 9911\; http://www.americanexpress-travel.com/\; US:1-800-297-2977\, Ou
tside:210-582-2716 \n \n \n\nTripIt - organize your travel at http://www.t
ripit.com
GEO:33.436111;-112.009444
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
UID:67d48ddde166a2e9bbac2cf7d93fe493b0860008@tripit.com
DTSTART;VALUE=DATE:20111213
DTEND;VALUE=DATE:20111216
SUMMARY:San Jose\, CA\, December 2011
LOCATION:San Jose\, CA
GEO:37.339386;-121.894955
TRANSP:TRANSPARENT
DESCRIPTION:John Doe is in San Jose\, CA from Dec 13 to Dec 15\,
2011\nView and/or edit details in TripIt : http://www.tripit.com/trip/sho
w/id/27037117\nTripIt - organize your travel at http://www.tripit.com\n
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20111213T172400Z
SUMMARY:US282 PHX to SJC
LOCATION:Phoenix (PHX)
UID:item-2b1b9021be548a87dd335f190b60ab78c33b619d@tripit.com
DTSTART:20111213T152500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/27037117\n \n[Flight] 12/13/2011 US Airways(US) #282 dep PHX 8:2
5am MST arr SJC 9:24am PST\; John Doe Ticket #0378728465928\; se
at(s) 15C\; conf #GGNV29 \nBooked on http://www.americanexpress-travel.com
/\; Reference #: 3134 0525 5102\; http://www.americanexpress-travel.com/\;
US:1-800-297-2977\, Outside:210-582-2716\; Total Cost: $406.39 \n \n \n\n
TripIt - organize your travel at http://www.tripit.com
GEO:37.361111;-121.925556
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Pick-up Rental Car: Advantage
TRANSP:TRANSPARENT
UID:item-619d345bb08aaef68e8767b672277243697f5bff@tripit.com
DTSTART:20111213T180000Z
DTEND:20111213T190000Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/27037117\n \n[Car Rental] Advantage\; San Jose International Air
port\; primary driver John Doe\; conf #F31539020E7 \npickup 12/1
3/2011 10:00am\; dropoff 12/15/2011 7:00pm \nStandard Convertible \nRefere
nce #: 3134 0526 3890 \n \n \n\nTripIt - organize your travel at http://ww
w.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Check-in: Crestview Hotel:
TRANSP:TRANSPARENT
UID:item-fbe6c08e7523c82fac69b40ad1d0899f3d8d5982@tripit.com
DTSTART:20111213T192400Z
DTEND:20111213T202400Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/27037117\n \n[Lodging] Crestview Hotel:\; conf #CR31342159 \ntel
650-966-8848 \narrive 12/13/2011\; depart 12/15/2011 \nBooking Rate: 153.
30 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Check-out: Crestview Hotel:
TRANSP:TRANSPARENT
UID:item-7ed8b84628e650a6b37161c7825bac9e72add49f@tripit.com
DTSTART:20111216T011500Z
DTEND:20111216T021500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/27037117\n \n[Lodging] Crestview Hotel:\; conf #CR31342159 \ntel
650-966-8848 \narrive 12/13/2011\; depart 12/15/2011 \nBooking Rate: 153.
30 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Drop-off Rental Car: Advantage
TRANSP:TRANSPARENT
UID:item-623b54ebe07ffd48845f1a120a86940ce79c698b@tripit.com
DTSTART:20111216T030000Z
DTEND:20111216T040000Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/27037117\n \n[Car Rental] Advantage\; San Jose International Air
port\; primary driver John Doe\; conf #F31539020E7 \npickup 12/1
3/2011 10:00am\; dropoff 12/15/2011 7:00pm \nStandard Convertible \nRefere
nce #: 3134 0526 3890 \n \n \n\nTripIt - organize your travel at http://ww
w.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20111216T045900Z
SUMMARY:US288 SJC to PHX
LOCATION:San Jose (SJC)
UID:item-52481e672972d2e88d5eaa5cf49bb801562c6014@tripit.com
DTSTART:20111216T031500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/27037117\n \n[Flight] 12/15/2011 US Airways(US) #288 dep SJC 7:1
5pm PST arr PHX 9:59pm MST\; John Doe Ticket #0378728465928\; se
at(s) 7B\; conf #GGNV29 \nBooked on http://www.americanexpress-travel.com/
\; Reference #: 3134 0525 5102\; http://www.americanexpress-travel.com/\;
US:1-800-297-2977\, Outside:210-582-2716\; Total Cost: $406.39 \n \n \n\nT
ripIt - organize your travel at http://www.tripit.com
GEO:33.436111;-112.009444
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
UID:7299ff29daed7d5c3e2ed4acc74deec5b7942bd5@tripit.com
DTSTART;VALUE=DATE:20120103
DTEND;VALUE=DATE:20120106
SUMMARY:San Francisco\, CA\, January 2012
LOCATION:San Francisco\, CA
GEO:37.774929;-122.419415
TRANSP:TRANSPARENT
DESCRIPTION:John Doe is in San Francisco\, CA from Jan 3 to Jan
5\, 2012\nView and/or edit details in TripIt : http://www.tripit.com/trip/
show/id/27863159\nTripIt - organize your travel at http://www.tripit.com\n
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20120103T175700Z
SUMMARY:US403 PHX to SFO
LOCATION:Phoenix (PHX)
UID:item-f099e76114bf43ef3b122432579d8b40995412a7@tripit.com
DTSTART:20120103T154500Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/27863159\n \n[Flight] 1/3/2012 US Airways(US) #403 dep PHX 8:45a
m MST arr SFO 9:57am PST\; John Doe Ticket #0378731791515\; conf
#FH9B72\, L4F9M5 \nBooked on http://www.americanexpress-travel.com/\; Ref
erence #: 6135 7391 6119\; http://www.americanexpress-travel.com/\; US:1-8
00-297-2977\, Outside:210-582-2716\; Total Cost: $668.39 \n \n \n\nTripIt
- organize your travel at http://www.tripit.com
GEO:37.618889;-122.375
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Pick-up Rental Car: Alamo
TRANSP:TRANSPARENT
UID:item-fae4b4b07b66fc87df125238e0aaf645106cf4f3@tripit.com
DTSTART:20120103T180000Z
DTEND:20120103T190000Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/27863159\n \n[Car Rental] Alamo\; San Francisco International Ai
rport\; primary driver John Doe\; conf #373525981COUNT \npickup
1/3/2012 10:00am\; dropoff 1/5/2012 6:00pm \nCompact \nReference #: 6135 7
391 6898 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Check-in: Grand Hotel Sunnyvale
TRANSP:TRANSPARENT
UID:item-d89a856eb9da9dfdcb4da46f42e49af3a838fcbb@tripit.com
DTSTART:20120103T195700Z
DTEND:20120103T205700Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/27863159\n \n[Lodging] Grand Hotel Sunnyvale\; conf #22084SY0361
18 \ntel 1-408-7208500 \narrive 1/3/2012\; depart 1/5/2012 \nBooking Rate:
USD 169.00 \nPolicies: Guarantee to valid form of payment is required at
time of booking\; Cancel 1 day prior to arrival date to avoid penalty of 1
Nights Room Charge. Change fee may apply for early departures and changes
made to confirmed reservations.\; \n \n \n\nTripIt - organize your travel
at http://www.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Check-out: Grand Hotel Sunnyvale
TRANSP:TRANSPARENT
UID:item-6edc82f6411fd0b66f2f7f6baafa41623a8623a9@tripit.com
DTSTART:20120106T010900Z
DTEND:20120106T020900Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/27863159\n \n[Lodging] Grand Hotel Sunnyvale\; conf #22084SY0361
18 \ntel 1-408-7208500 \narrive 1/3/2012\; depart 1/5/2012 \nBooking Rate:
USD 169.00 \nPolicies: Guarantee to valid form of payment is required at
time of booking\; Cancel 1 day prior to arrival date to avoid penalty of 1
Nights Room Charge. Change fee may apply for early departures and changes
made to confirmed reservations.\; \n \n \n\nTripIt - organize your travel
at http://www.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
SUMMARY:Drop-off Rental Car: Alamo
TRANSP:TRANSPARENT
UID:item-58a31b96066ffd09b800af49de59a84f7b7a3a06@tripit.com
DTSTART:20120106T020000Z
DTEND:20120106T030000Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/27863159\n \n[Car Rental] Alamo\; San Francisco International Ai
rport\; primary driver John Doe\; conf #373525981COUNT \npickup
1/3/2012 10:00am\; dropoff 1/5/2012 6:00pm \nCompact \nReference #: 6135 7
391 6898 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20120101T215311Z
DTEND:20120106T050500Z
SUMMARY:CO496 SFO to PHX
LOCATION:San Francisco (SFO)
UID:item-7884351ce42d503b90ccc48c33c7c30bd4f44767@tripit.com
DTSTART:20120106T030900Z
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
p/show/id/27863159\n \n[Flight] 1/5/2012 Continental Airlines(CO) #496 dep
SFO 7:09pm PST arr PHX 10:05pm MST\; John Doe Ticket #037873179
1515\; conf #FH9B72\, L4F9M5(Operated by United Airlines flight 496) \nBoo
ked on http://www.americanexpress-travel.com/\; Reference #: 6135 7391 611
9\; http://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:2
10-582-2716\; Total Cost: $668.39 \n \n \n\nTripIt - organize your travel
at http://www.tripit.com
GEO:33.436111;-112.009444
END:VEVENT
END:VCALENDAR

View File

@@ -1,41 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Meetup//RemoteApi//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-ORIGINAL-URL:http://www.meetup.com/events/ical/8333638/dfdba2e469216075
3404f737feace78d526ff0ce/going
X-WR-CALNAME:My Meetups
X-MS-OLK-FORCEINSPECTOROPEN:TRUE
BEGIN:VTIMEZONE
TZID:America/Phoenix
TZURL:http://tzurl.org/zoneinfo-outlook/America/Phoenix
X-LIC-LOCATION:America/Phoenix
BEGIN:STANDARD
TZOFFSETFROM:-0700
TZOFFSETTO:-0700
TZNAME:MST
DTSTART:19700101T000000
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20111106T155927Z
DTSTART;TZID=America/Phoenix:20111109T190000
DTEND;TZID=America/Phoenix:20111109T210000
STATUS:CONFIRMED
SUMMARY:Phoenix Drupal User Group Monthly Meetup
DESCRIPTION:Phoenix Drupal User Group\nWednesday\, November 9 at 7:00 PM\
n\nCustomizing node display with template pages in Drupal 6\n\n Jon Shee
han and Matthew Berry of the Office of Knowledge Enterprise Development
(OKED) Knowledge...\n\nDetails: http://www.meetup.com/Phoenix-Drupal-Use
r-Group/events/33627272/
CLASS:PUBLIC
CREATED:20100630T083023Z
GEO:33.56;-111.90
LOCATION:Open Source Project Tempe (1415 E University Dr. #103A\, Tempe\,
AZ 85281)
URL:http://www.meetup.com/Phoenix-Drupal-User-Group/events/33627272/
LAST-MODIFIED:20111102T213309Z
UID:event_nsmxnyppbfc@meetup.com
END:VEVENT
END:VCALENDAR

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:ownCloud Calendar 0.6.3
X-WR-CALNAME:Fête Nationale - Férié
BEGIN:VEVENT
CREATED:20090502T140513Z
DTSTAMP:20111106T124709Z
UID:FA9831E7-C238-4FEC-95E5-CD46BD466421
SUMMARY:Fête Nationale - Férié
RRULE:FREQ=YEARLY
DTSTART;VALUE=DATE:20120714
DTEND;VALUE=DATE:20120715
TRANSP:OPAQUE
SEQUENCE:5
END:VEVENT
END:VCALENDAR

View File

@@ -1,23 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:ownCloud Calendar 0.6.3
X-WR-CALNAME:Default calendar
BEGIN:VTODO
CREATED;VALUE=DATE-TIME:20130714T092804Z
UID:0aa462f13c
LAST-MODIFIED;VALUE=DATE-TIME:20130714T092804Z
DTSTAMP;VALUE=DATE-TIME:20130714T092804Z
CATEGORIES:Projets
SUMMARY:Migrer le blog
PERCENT-COMPLETE:100
COMPLETED;VALUE=DATE-TIME;TZID=Europe/Monaco:20130716T105745
END:VTODO
BEGIN:VTODO
CREATED;VALUE=DATE-TIME:20130714T092912Z
UID:5e05bbcf34
LAST-MODIFIED;VALUE=DATE-TIME:20130714T092912Z
DTSTAMP;VALUE=DATE-TIME:20130714T092912Z
SUMMARY:Créer test unitaire erreur ical
CATEGORIES:Projets
END:VTODO
END:VCALENDAR

View File

@@ -1,21 +0,0 @@
BEGIN:VCALENDAR
BEGIN:VEVENT
UID:eb9e1bd2-ceba-499f-be77-f02773954c72
SUMMARY:Event with an alarm
DESCRIPTION:This is an event with an alarm.
ORGANIZER="mailto:stomlinson@mozilla.com"
DTSTART;TZID="America/Los_Angeles":20130418T110000
DTEND;TZID="America/Los_Angeles":20130418T120000
STATUS:CONFIRMED
CLASS:PUBLIC
TRANSP:OPAQUE
LAST-MODIFIED:20130418T175632Z
DTSTAMP:20130418T175632Z
SEQUENCE:3
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:-PT5M
DESCRIPTION:Reminder
END:VALARM
END:VEVENT
END:VCALENDAR

View File

@@ -1,4 +1,5 @@
# Module: Clock # Module: Clock
The `clock` module is one of the default modules of the MagicMirror. The `clock` module is one of the default modules of the MagicMirror.
This module displays the current date and time. The information will be updated realtime. This module displays the current date and time. The information will be updated realtime.

View File

@@ -31,7 +31,7 @@ Module.register("clock",{
showSunTimes: false, showSunTimes: false,
showMoonTimes: false, showMoonTimes: false,
lat: 47.630539, lat: 47.630539,
lon: -122.344147, lon: -122.344147
}, },
// Define required scripts. // Define required scripts.
getScripts: function () { getScripts: function () {
@@ -52,10 +52,12 @@ Module.register("clock",{
//Calculate how many ms should pass until next update depending on if seconds is displayed or not //Calculate how many ms should pass until next update depending on if seconds is displayed or not
var delayCalculator = function (reducedSeconds) { var delayCalculator = function (reducedSeconds) {
var EXTRA_DELAY = 50; //Deliberate imperceptable delay to prevent off-by-one timekeeping errors
if (self.config.displaySeconds) { if (self.config.displaySeconds) {
return 1000 - moment().milliseconds(); return 1000 - moment().milliseconds() + EXTRA_DELAY;
} else { } else {
return ((60 - reducedSeconds) * 1000) - moment().milliseconds(); return (60 - reducedSeconds) * 1000 - moment().milliseconds() + EXTRA_DELAY;
} }
}; };
@@ -65,7 +67,7 @@ Module.register("clock",{
//If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent) //If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
if (self.config.displaySeconds) { if (self.config.displaySeconds) {
self.second = (self.second + 1) % 60; self.second = moment().second();
if (self.second !== 0) { if (self.second !== 0) {
self.sendNotification("CLOCK_SECOND", self.second); self.sendNotification("CLOCK_SECOND", self.second);
setTimeout(notificationTimer, delayCalculator(0)); setTimeout(notificationTimer, delayCalculator(0));
@@ -74,7 +76,7 @@ Module.register("clock",{
} }
//If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification //If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
self.minute = (self.minute + 1) % 60; self.minute = moment().minute();
self.sendNotification("CLOCK_MINUTE", self.minute); self.sendNotification("CLOCK_MINUTE", self.minute);
setTimeout(notificationTimer, delayCalculator(0)); setTimeout(notificationTimer, delayCalculator(0));
}; };
@@ -84,11 +86,9 @@ Module.register("clock",{
// Set locale. // Set locale.
moment.locale(config.language); moment.locale(config.language);
}, },
// Override dom generator. // Override dom generator.
getDom: function () { getDom: function () {
var wrapper = document.createElement("div"); var wrapper = document.createElement("div");
/************************************ /************************************
@@ -127,7 +127,7 @@ Module.register("clock",{
} }
if (this.config.clockBold === true) { if (this.config.clockBold === true) {
timeString = now.format(hourSymbol + "[<span class=\"bold\">]mm[</span>]"); timeString = now.format(hourSymbol + '[<span class="bold">]mm[</span>]');
} else { } else {
timeString = now.format(hourSymbol + ":mm"); timeString = now.format(hourSymbol + ":mm");
} }
@@ -152,6 +152,13 @@ Module.register("clock",{
timeWrapper.appendChild(periodWrapper); timeWrapper.appendChild(periodWrapper);
} }
/**
* Format the time according to the config
*
* @param {object} config The config of the module
* @param {object} time time to format
* @returns {string} The formatted time string
*/
function formatTime(config, time) { function formatTime(config, time) {
var formatString = hourSymbol + ":mm"; var formatString = hourSymbol + ":mm";
if (config.showPeriod && config.timeFormat !== 24) { if (config.showPeriod && config.timeFormat !== 24) {
@@ -159,6 +166,7 @@ Module.register("clock",{
} }
return moment(time).format(formatString); return moment(time).format(formatString);
} }
if (this.config.showSunTimes) { if (this.config.showSunTimes) {
const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon); const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon);
const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset); const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset);
@@ -173,9 +181,18 @@ Module.register("clock",{
} }
const untilNextEvent = moment.duration(moment(nextEvent).diff(now)); const untilNextEvent = moment.duration(moment(nextEvent).diff(now));
const untilNextEventString = untilNextEvent.hours() + "h " + untilNextEvent.minutes() + "m"; const untilNextEventString = untilNextEvent.hours() + "h " + untilNextEvent.minutes() + "m";
sunWrapper.innerHTML = "<span class=\"" + (isVisible ? "bright" : "") + "\"><i class=\"fa fa-sun-o\" aria-hidden=\"true\"></i> " + untilNextEventString + "</span>" + sunWrapper.innerHTML =
"<span><i class=\"fa fa-arrow-up\" aria-hidden=\"true\"></i>" + formatTime(this.config, sunTimes.sunrise) + "</span>" + '<span class="' +
"<span><i class=\"fa fa-arrow-down\" aria-hidden=\"true\"></i>" + formatTime(this.config, sunTimes.sunset) + "</span>"; (isVisible ? "bright" : "") +
'"><i class="fa fa-sun-o" aria-hidden="true"></i> ' +
untilNextEventString +
"</span>" +
'<span><i class="fa fa-arrow-up" aria-hidden="true"></i>' +
formatTime(this.config, sunTimes.sunrise) +
"</span>" +
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i>' +
formatTime(this.config, sunTimes.sunset) +
"</span>";
} }
if (this.config.showMoonTimes) { if (this.config.showMoonTimes) {
const moonIllumination = SunCalc.getMoonIllumination(now.toDate()); const moonIllumination = SunCalc.getMoonIllumination(now.toDate());
@@ -190,9 +207,18 @@ Module.register("clock",{
} }
const isVisible = now.isBetween(moonRise, moonSet) || moonTimes.alwaysUp === true; const isVisible = now.isBetween(moonRise, moonSet) || moonTimes.alwaysUp === true;
const illuminatedFractionString = Math.round(moonIllumination.fraction * 100) + "%"; const illuminatedFractionString = Math.round(moonIllumination.fraction * 100) + "%";
moonWrapper.innerHTML = "<span class=\"" + (isVisible ? "bright" : "") + "\"><i class=\"fa fa-moon-o\" aria-hidden=\"true\"></i> " + illuminatedFractionString + "</span>" + moonWrapper.innerHTML =
"<span><i class=\"fa fa-arrow-up\" aria-hidden=\"true\"></i> " + (moonRise ? formatTime(this.config, moonRise) : "...") + "</span>"+ '<span class="' +
"<span><i class=\"fa fa-arrow-down\" aria-hidden=\"true\"></i> " + (moonSet ? formatTime(this.config, moonSet) : "...") + "</span>"; (isVisible ? "bright" : "") +
'"><i class="fa fa-moon-o" aria-hidden="true"></i> ' +
illuminatedFractionString +
"</span>" +
'<span><i class="fa fa-arrow-up" aria-hidden="true"></i> ' +
(moonRise ? formatTime(this.config, moonRise) : "...") +
"</span>" +
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
(moonSet ? formatTime(this.config, moonSet) : "...") +
"</span>";
} }
/**************************************************************** /****************************************************************
@@ -223,7 +249,6 @@ Module.register("clock",{
// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611 // The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
// clockCircle.style.border = "1px solid black"; // clockCircle.style.border = "1px solid black";
clockCircle.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used clockCircle.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
} else if (this.config.analogFace !== "none") { } else if (this.config.analogFace !== "none") {
clockCircle.style.border = "2px solid white"; clockCircle.style.border = "2px solid white";
} }
@@ -305,7 +330,7 @@ Module.register("clock",{
var appendClocks = function (condition, pos1, pos2) { var appendClocks = function (condition, pos1, pos2) {
var padding = [0, 0, 0, 0]; var padding = [0, 0, 0, 0];
padding[(placement === condition) ? pos1 : pos2] = "20px"; padding[placement === condition ? pos1 : pos2] = "20px";
analogWrapper.style.padding = padding.join(" "); analogWrapper.style.padding = padding.join(" ");
if (placement === condition) { if (placement === condition) {
wrapper.appendChild(analogWrapper); wrapper.appendChild(analogWrapper);

View File

@@ -1 +1 @@
<svg id="Hour_Markers_-_Singlets" data-name="Hour Markers - Singlets" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1,.cls-2{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;}.cls-2{stroke-width:0.5px;}</style></defs><title>face-001</title><line class="cls-1" x1="125" y1="1.25" x2="125" y2="16.23"/><line class="cls-1" x1="186.87" y1="17.83" x2="179.39" y2="30.8"/><line class="cls-1" x1="232.17" y1="63.12" x2="219.2" y2="70.61"/><line class="cls-1" x1="248.75" y1="125" x2="233.77" y2="125"/><line class="cls-1" x1="232.17" y1="186.87" x2="219.2" y2="179.39"/><line class="cls-1" x1="186.88" y1="232.17" x2="179.39" y2="219.2"/><line class="cls-1" x1="125" y1="248.75" x2="125" y2="233.77"/><line class="cls-1" x1="63.13" y1="232.17" x2="70.61" y2="219.2"/><line class="cls-1" x1="17.83" y1="186.88" x2="30.8" y2="179.39"/><line class="cls-1" x1="1.25" y1="125" x2="16.23" y2="125"/><line class="cls-1" x1="17.83" y1="63.13" x2="30.8" y2="70.61"/><line class="cls-1" x1="63.12" y1="17.83" x2="70.61" y2="30.8"/><line class="cls-2" x1="138.01" y1="1.25" x2="136.96" y2="11.23"/><line class="cls-2" x1="150.87" y1="3.29" x2="148.78" y2="13.11"/><line class="cls-2" x1="163.45" y1="6.66" x2="160.35" y2="16.21"/><line class="cls-2" x1="175.61" y1="11.33" x2="171.53" y2="20.5"/><line class="cls-2" x1="198.14" y1="24.33" x2="192.24" y2="32.45"/><line class="cls-2" x1="208.26" y1="32.53" x2="201.54" y2="39.99"/><line class="cls-2" x1="217.47" y1="41.74" x2="210.01" y2="48.46"/><line class="cls-2" x1="225.67" y1="51.86" x2="217.55" y2="57.76"/><line class="cls-2" x1="238.67" y1="74.39" x2="229.5" y2="78.47"/><line class="cls-2" x1="243.34" y1="86.55" x2="233.79" y2="89.65"/><line class="cls-2" x1="246.71" y1="99.13" x2="236.89" y2="101.22"/><line class="cls-2" x1="248.75" y1="111.99" x2="238.77" y2="113.04"/><line class="cls-2" x1="248.75" y1="138.01" x2="238.77" y2="136.96"/><line class="cls-2" x1="246.71" y1="150.87" x2="236.89" y2="148.78"/><line class="cls-2" x1="243.34" y1="163.45" x2="233.79" y2="160.35"/><line class="cls-2" x1="238.67" y1="175.61" x2="229.5" y2="171.53"/><line class="cls-2" x1="225.67" y1="198.14" x2="217.55" y2="192.24"/><line class="cls-2" x1="217.47" y1="208.26" x2="210.01" y2="201.54"/><line class="cls-2" x1="208.26" y1="217.47" x2="201.54" y2="210.01"/><line class="cls-2" x1="198.14" y1="225.67" x2="192.24" y2="217.55"/><line class="cls-2" x1="175.61" y1="238.67" x2="171.53" y2="229.5"/><line class="cls-2" x1="163.45" y1="243.34" x2="160.35" y2="233.79"/><line class="cls-2" x1="150.87" y1="246.71" x2="148.78" y2="236.89"/><line class="cls-2" x1="138.01" y1="248.75" x2="136.96" y2="238.77"/><line class="cls-2" x1="111.99" y1="248.75" x2="113.04" y2="238.77"/><line class="cls-2" x1="99.13" y1="246.71" x2="101.22" y2="236.89"/><line class="cls-2" x1="86.55" y1="243.34" x2="89.65" y2="233.79"/><line class="cls-2" x1="74.39" y1="238.67" x2="78.47" y2="229.5"/><line class="cls-2" x1="51.86" y1="225.67" x2="57.76" y2="217.55"/><line class="cls-2" x1="41.74" y1="217.47" x2="48.46" y2="210.01"/><line class="cls-2" x1="32.53" y1="208.26" x2="39.99" y2="201.54"/><line class="cls-2" x1="24.33" y1="198.14" x2="32.45" y2="192.24"/><line class="cls-2" x1="11.33" y1="175.61" x2="20.5" y2="171.53"/><line class="cls-2" x1="6.66" y1="163.45" x2="16.21" y2="160.35"/><line class="cls-2" x1="3.29" y1="150.87" x2="13.11" y2="148.78"/><line class="cls-2" x1="1.25" y1="138.01" x2="11.23" y2="136.96"/><line class="cls-2" x1="1.25" y1="111.99" x2="11.23" y2="113.04"/><line class="cls-2" x1="3.29" y1="99.13" x2="13.11" y2="101.22"/><line class="cls-2" x1="6.66" y1="86.55" x2="16.21" y2="89.65"/><line class="cls-2" x1="11.33" y1="74.39" x2="20.5" y2="78.47"/><line class="cls-2" x1="24.33" y1="51.86" x2="32.45" y2="57.76"/><line class="cls-2" x1="32.53" y1="41.74" x2="39.99" y2="48.46"/><line class="cls-2" x1="41.74" y1="32.53" x2="48.46" y2="39.99"/><line class="cls-2" x1="51.86" y1="24.33" x2="57.76" y2="32.45"/><line class="cls-2" x1="74.39" y1="11.33" x2="78.47" y2="20.5"/><line class="cls-2" x1="86.55" y1="6.66" x2="89.65" y2="16.21"/><line class="cls-2" x1="99.13" y1="3.29" x2="101.22" y2="13.11"/><line class="cls-2" x1="111.99" y1="1.25" x2="113.04" y2="11.23"/></svg> <svg id="Hour_Markers_-_Singlets" data-name="Hour Markers - Singlets" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1,.cls-2{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;}.cls-2{stroke-width:0.5px;}</style></defs><title>face-001</title><line class="cls-1" x1="125" y1="1.25" x2="125" y2="16.23"/><line class="cls-1" x1="186.87" y1="17.83" x2="179.39" y2="30.8"/><line class="cls-1" x1="232.17" y1="63.12" x2="219.2" y2="70.61"/><line class="cls-1" x1="248.75" y1="125" x2="233.77" y2="125"/><line class="cls-1" x1="232.17" y1="186.87" x2="219.2" y2="179.39"/><line class="cls-1" x1="186.88" y1="232.17" x2="179.39" y2="219.2"/><line class="cls-1" x1="125" y1="248.75" x2="125" y2="233.77"/><line class="cls-1" x1="63.13" y1="232.17" x2="70.61" y2="219.2"/><line class="cls-1" x1="17.83" y1="186.88" x2="30.8" y2="179.39"/><line class="cls-1" x1="1.25" y1="125" x2="16.23" y2="125"/><line class="cls-1" x1="17.83" y1="63.13" x2="30.8" y2="70.61"/><line class="cls-1" x1="63.12" y1="17.83" x2="70.61" y2="30.8"/><line class="cls-2" x1="138.01" y1="1.25" x2="136.96" y2="11.23"/><line class="cls-2" x1="150.87" y1="3.29" x2="148.78" y2="13.11"/><line class="cls-2" x1="163.45" y1="6.66" x2="160.35" y2="16.21"/><line class="cls-2" x1="175.61" y1="11.33" x2="171.53" y2="20.5"/><line class="cls-2" x1="198.14" y1="24.33" x2="192.24" y2="32.45"/><line class="cls-2" x1="208.26" y1="32.53" x2="201.54" y2="39.99"/><line class="cls-2" x1="217.47" y1="41.74" x2="210.01" y2="48.46"/><line class="cls-2" x1="225.67" y1="51.86" x2="217.55" y2="57.76"/><line class="cls-2" x1="238.67" y1="74.39" x2="229.5" y2="78.47"/><line class="cls-2" x1="243.34" y1="86.55" x2="233.79" y2="89.65"/><line class="cls-2" x1="246.71" y1="99.13" x2="236.89" y2="101.22"/><line class="cls-2" x1="248.75" y1="111.99" x2="238.77" y2="113.04"/><line class="cls-2" x1="248.75" y1="138.01" x2="238.77" y2="136.96"/><line class="cls-2" x1="246.71" y1="150.87" x2="236.89" y2="148.78"/><line class="cls-2" x1="243.34" y1="163.45" x2="233.79" y2="160.35"/><line class="cls-2" x1="238.67" y1="175.61" x2="229.5" y2="171.53"/><line class="cls-2" x1="225.67" y1="198.14" x2="217.55" y2="192.24"/><line class="cls-2" x1="217.47" y1="208.26" x2="210.01" y2="201.54"/><line class="cls-2" x1="208.26" y1="217.47" x2="201.54" y2="210.01"/><line class="cls-2" x1="198.14" y1="225.67" x2="192.24" y2="217.55"/><line class="cls-2" x1="175.61" y1="238.67" x2="171.53" y2="229.5"/><line class="cls-2" x1="163.45" y1="243.34" x2="160.35" y2="233.79"/><line class="cls-2" x1="150.87" y1="246.71" x2="148.78" y2="236.89"/><line class="cls-2" x1="138.01" y1="248.75" x2="136.96" y2="238.77"/><line class="cls-2" x1="111.99" y1="248.75" x2="113.04" y2="238.77"/><line class="cls-2" x1="99.13" y1="246.71" x2="101.22" y2="236.89"/><line class="cls-2" x1="86.55" y1="243.34" x2="89.65" y2="233.79"/><line class="cls-2" x1="74.39" y1="238.67" x2="78.47" y2="229.5"/><line class="cls-2" x1="51.86" y1="225.67" x2="57.76" y2="217.55"/><line class="cls-2" x1="41.74" y1="217.47" x2="48.46" y2="210.01"/><line class="cls-2" x1="32.53" y1="208.26" x2="39.99" y2="201.54"/><line class="cls-2" x1="24.33" y1="198.14" x2="32.45" y2="192.24"/><line class="cls-2" x1="11.33" y1="175.61" x2="20.5" y2="171.53"/><line class="cls-2" x1="6.66" y1="163.45" x2="16.21" y2="160.35"/><line class="cls-2" x1="3.29" y1="150.87" x2="13.11" y2="148.78"/><line class="cls-2" x1="1.25" y1="138.01" x2="11.23" y2="136.96"/><line class="cls-2" x1="1.25" y1="111.99" x2="11.23" y2="113.04"/><line class="cls-2" x1="3.29" y1="99.13" x2="13.11" y2="101.22"/><line class="cls-2" x1="6.66" y1="86.55" x2="16.21" y2="89.65"/><line class="cls-2" x1="11.33" y1="74.39" x2="20.5" y2="78.47"/><line class="cls-2" x1="24.33" y1="51.86" x2="32.45" y2="57.76"/><line class="cls-2" x1="32.53" y1="41.74" x2="39.99" y2="48.46"/><line class="cls-2" x1="41.74" y1="32.53" x2="48.46" y2="39.99"/><line class="cls-2" x1="51.86" y1="24.33" x2="57.76" y2="32.45"/><line class="cls-2" x1="74.39" y1="11.33" x2="78.47" y2="20.5"/><line class="cls-2" x1="86.55" y1="6.66" x2="89.65" y2="16.21"/><line class="cls-2" x1="99.13" y1="3.29" x2="101.22" y2="13.11"/><line class="cls-2" x1="111.99" y1="1.25" x2="113.04" y2="11.23"/></svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1 +1 @@
<svg id="Hour_Markers_-_Doubles" data-name="Hour Markers - Doubles" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2.98px;}</style></defs><title>face-002</title><line class="cls-1" x1="122.01" y1="1.75" x2="122.01" y2="16.67"/><line class="cls-1" x1="186.62" y1="18.26" x2="179.17" y2="31.18"/><line class="cls-1" x1="231.74" y1="63.37" x2="218.82" y2="70.83"/><line class="cls-1" x1="248.25" y1="127.99" x2="233.33" y2="127.99"/><line class="cls-1" x1="231.74" y1="186.62" x2="218.82" y2="179.17"/><line class="cls-1" x1="186.63" y1="231.74" x2="179.17" y2="218.82"/><line class="cls-1" x1="127.99" y1="248.25" x2="127.99" y2="233.33"/><line class="cls-1" x1="63.38" y1="231.74" x2="70.83" y2="218.82"/><line class="cls-1" x1="18.26" y1="186.63" x2="31.18" y2="179.17"/><line class="cls-1" x1="1.75" y1="122.01" x2="16.67" y2="122.01"/><line class="cls-1" x1="18.26" y1="63.38" x2="31.18" y2="70.83"/><line class="cls-1" x1="63.37" y1="18.26" x2="70.83" y2="31.18"/><line class="cls-1" x1="127.99" y1="1.75" x2="127.99" y2="16.67"/><line class="cls-1" x1="248.25" y1="122.01" x2="233.33" y2="122.01"/><line class="cls-1" x1="122.01" y1="248.25" x2="122.01" y2="233.33"/><line class="cls-1" x1="1.75" y1="127.99" x2="16.67" y2="127.99"/></svg> <svg id="Hour_Markers_-_Doubles" data-name="Hour Markers - Doubles" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2.98px;}</style></defs><title>face-002</title><line class="cls-1" x1="122.01" y1="1.75" x2="122.01" y2="16.67"/><line class="cls-1" x1="186.62" y1="18.26" x2="179.17" y2="31.18"/><line class="cls-1" x1="231.74" y1="63.37" x2="218.82" y2="70.83"/><line class="cls-1" x1="248.25" y1="127.99" x2="233.33" y2="127.99"/><line class="cls-1" x1="231.74" y1="186.62" x2="218.82" y2="179.17"/><line class="cls-1" x1="186.63" y1="231.74" x2="179.17" y2="218.82"/><line class="cls-1" x1="127.99" y1="248.25" x2="127.99" y2="233.33"/><line class="cls-1" x1="63.38" y1="231.74" x2="70.83" y2="218.82"/><line class="cls-1" x1="18.26" y1="186.63" x2="31.18" y2="179.17"/><line class="cls-1" x1="1.75" y1="122.01" x2="16.67" y2="122.01"/><line class="cls-1" x1="18.26" y1="63.38" x2="31.18" y2="70.83"/><line class="cls-1" x1="63.37" y1="18.26" x2="70.83" y2="31.18"/><line class="cls-1" x1="127.99" y1="1.75" x2="127.99" y2="16.67"/><line class="cls-1" x1="248.25" y1="122.01" x2="233.33" y2="122.01"/><line class="cls-1" x1="122.01" y1="248.25" x2="122.01" y2="233.33"/><line class="cls-1" x1="1.75" y1="127.99" x2="16.67" y2="127.99"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -1,4 +1,5 @@
# Module: Compliments # Module: Compliments
The `compliments` module is one of the default modules of the MagicMirror. The `compliments` module is one of the default modules of the MagicMirror.
This module displays a random compliment. This module displays a random compliment.

View File

@@ -5,31 +5,14 @@
* MIT Licensed. * MIT Licensed.
*/ */
Module.register("compliments", { Module.register("compliments", {
// Module config defaults. // Module config defaults.
defaults: { defaults: {
compliments: { compliments: {
anytime: [ anytime: ["Hey there sexy!"],
"Hey there sexy!" morning: ["Good morning, handsome!", "Enjoy your day!", "How was your sleep?"],
], afternoon: ["Hello, beauty!", "You look sexy!", "Looking good today!"],
morning: [ evening: ["Wow, you look hot!", "You look nice!", "Hi, sexy!"],
"Good morning, handsome!", "....-01-01": ["Happy new year!"]
"Enjoy your day!",
"How was your sleep?"
],
afternoon: [
"Hello, beauty!",
"You look sexy!",
"Looking good today!"
],
evening: [
"Wow, you look hot!",
"You look nice!",
"Hi, sexy!"
],
"....-01-01": [
"Happy new year!"
]
}, },
updateInterval: 30000, updateInterval: 30000,
remoteFile: null, remoteFile: null,
@@ -165,11 +148,10 @@ Module.register("compliments", {
if (this.config.random) { if (this.config.random) {
// yes // yes
index = this.randomIndex(compliments); index = this.randomIndex(compliments);
} } else {
else{
// no, sequential // no, sequential
// if doing sequential, don't fall off the end // if doing sequential, don't fall off the end
index = (this.lastIndexUsed >= (compliments.length-1))?0: ++this.lastIndexUsed; index = this.lastIndexUsed >= compliments.length - 1 ? 0 : ++this.lastIndexUsed;
} }
return compliments[index] || ""; return compliments[index] || "";
@@ -229,6 +211,5 @@ Module.register("compliments", {
if (notification === "CURRENTWEATHER_DATA") { if (notification === "CURRENTWEATHER_DATA") {
this.setCurrentWeatherType(payload.data); this.setCurrentWeatherType(payload.data);
} }
}, }
}); });

View File

@@ -1,4 +1,5 @@
# Module: Current Weather # Module: Current Weather
The `currentweather` module is one of the default modules of the MagicMirror. 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. This module displays the current weather, including the windspeed, the sunset or sunrise time, the temperature and an icon to display the current conditions.

View File

@@ -5,7 +5,6 @@
* MIT Licensed. * MIT Licensed.
*/ */
Module.register("currentweather", { Module.register("currentweather", {
// Default module config. // Default module config.
defaults: { defaults: {
location: false, location: false,
@@ -24,6 +23,7 @@ Module.register("currentweather",{
lang: config.language, lang: config.language,
decimalSymbol: ".", decimalSymbol: ".",
showHumidity: false, showHumidity: false,
showSun: true,
degreeLabel: false, degreeLabel: false,
showIndoorTemperature: false, showIndoorTemperature: false,
showIndoorHumidity: false, showIndoorHumidity: false,
@@ -37,6 +37,8 @@ Module.register("currentweather",{
weatherEndpoint: "weather", weatherEndpoint: "weather",
appendLocationNameToHeader: true, appendLocationNameToHeader: true,
useLocationAsHeader: false,
calendarClass: "calendar", calendarClass: "calendar",
tableClass: "large", tableClass: "large",
@@ -63,7 +65,7 @@ Module.register("currentweather",{
"11n": "wi-night-thunderstorm", "11n": "wi-night-thunderstorm",
"13n": "wi-night-snow", "13n": "wi-night-snow",
"50n": "wi-night-alt-cloudy-windy" "50n": "wi-night-alt-cloudy-windy"
}, }
}, },
// create a variable for the first upcoming calendar event. Used if no location is specified. // create a variable for the first upcoming calendar event. Used if no location is specified.
@@ -109,13 +111,11 @@ Module.register("currentweather",{
this.feelsLike = null; this.feelsLike = null;
this.loaded = false; this.loaded = false;
this.scheduleUpdate(this.config.initialLoadDelay); this.scheduleUpdate(this.config.initialLoadDelay);
}, },
// add extra information of current weather // add extra information of current weather
// windDirection, humidity, sunrise and sunset // windDirection, humidity, sunrise and sunset
addExtraInfoWeather: function (wrapper) { addExtraInfoWeather: function (wrapper) {
var small = document.createElement("div"); var small = document.createElement("div");
small.className = "normal medium"; small.className = "normal medium";
@@ -131,7 +131,7 @@ Module.register("currentweather",{
var windDirection = document.createElement("sup"); var windDirection = document.createElement("sup");
if (this.config.showWindDirectionAsArrow) { if (this.config.showWindDirectionAsArrow) {
if (this.windDeg !== null) { if (this.windDeg !== null) {
windDirection.innerHTML = " &nbsp;<i class=\"fa fa-long-arrow-down\" style=\"transform:rotate("+this.windDeg+"deg);\"></i>&nbsp;"; windDirection.innerHTML = ' &nbsp;<i class="fa fa-long-arrow-down" style="transform:rotate(' + this.windDeg + 'deg);"></i>&nbsp;';
} }
} else { } else {
windDirection.innerHTML = " " + this.translate(this.windDirection); windDirection.innerHTML = " " + this.translate(this.windDirection);
@@ -158,6 +158,7 @@ Module.register("currentweather",{
small.appendChild(humidityIcon); small.appendChild(humidityIcon);
} }
if (this.config.showSun) {
var sunriseSunsetIcon = document.createElement("span"); var sunriseSunsetIcon = document.createElement("span");
sunriseSunsetIcon.className = "wi dimmed " + this.sunriseSunsetIcon; sunriseSunsetIcon.className = "wi dimmed " + this.sunriseSunsetIcon;
small.appendChild(sunriseSunsetIcon); small.appendChild(sunriseSunsetIcon);
@@ -165,6 +166,7 @@ Module.register("currentweather",{
var sunriseSunsetTime = document.createElement("span"); var sunriseSunsetTime = document.createElement("span");
sunriseSunsetTime.innerHTML = " " + this.sunriseSunsetTime; sunriseSunsetTime.innerHTML = " " + this.sunriseSunsetTime;
small.appendChild(sunriseSunsetTime); small.appendChild(sunriseSunsetTime);
}
wrapper.appendChild(small); wrapper.appendChild(small);
}, },
@@ -267,15 +269,16 @@ Module.register("currentweather",{
// Override getHeader method. // Override getHeader method.
getHeader: function () { getHeader: function () {
if (this.config.appendLocationNameToHeader && this.data.header !== undefined) {
return this.data.header + " " + this.fetchedLocationName;
}
if (this.config.useLocationAsHeader && this.config.location !== false) { if (this.config.useLocationAsHeader && this.config.location !== false) {
return this.config.location; return this.config.location;
} }
return this.data.header; if (this.config.appendLocationNameToHeader) {
if (this.data.header) return this.data.header + " " + this.fetchedLocationName;
else return this.fetchedLocationName;
}
return this.data.header ? this.data.header : "";
}, },
// Override notification handler. // Override notification handler.
@@ -340,7 +343,7 @@ Module.register("currentweather",{
} }
if (retry) { if (retry) {
self.scheduleUpdate((self.loaded) ? -1 : self.config.retryDelay); self.scheduleUpdate(self.loaded ? -1 : self.config.retryDelay);
} }
} }
}; };
@@ -380,7 +383,6 @@ Module.register("currentweather",{
* argument data object - Weather information received form openweather.org. * argument data object - Weather information received form openweather.org.
*/ */
processWeather: function (data) { processWeather: function (data) {
if (!data || !data.main || typeof data.main.temp === "undefined") { if (!data || !data.main || typeof data.main.temp === "undefined") {
// Did not receive usable new data. // Did not receive usable new data.
// Maybe this needs a better check? // Maybe this needs a better check?
@@ -405,9 +407,11 @@ Module.register("currentweather",{
var tempInF = 0; var tempInF = 0;
switch (this.config.units) { switch (this.config.units) {
case "metric": tempInF = 1.8 * this.temperature + 32; case "metric":
tempInF = 1.8 * this.temperature + 32;
break; break;
case "imperial": tempInF = this.temperature; case "imperial":
tempInF = this.temperature;
break; break;
case "default": case "default":
tempInF = 1.8 * (this.temperature - 273.15) + 32; tempInF = 1.8 * (this.temperature - 273.15) + 32;
@@ -421,28 +425,35 @@ Module.register("currentweather",{
// this.feelsLike = windChillInC.toFixed(0); // this.feelsLike = windChillInC.toFixed(0);
switch (this.config.units) { switch (this.config.units) {
case "metric": this.feelsLike = windChillInC.toFixed(0); case "metric":
this.feelsLike = windChillInC.toFixed(0);
break; break;
case "imperial": this.feelsLike = windChillInF.toFixed(0); case "imperial":
this.feelsLike = windChillInF.toFixed(0);
break; break;
case "default": case "default":
this.feelsLike = (windChillInC + 273.15).toFixed(0); this.feelsLike = (windChillInC + 273.15).toFixed(0);
break; break;
} }
} else if (tempInF > 80 && this.humidity > 40) { } else if (tempInF > 80 && this.humidity > 40) {
// heat index // heat index
var Hindex = -42.379 + 2.04901523*tempInF + 10.14333127*this.humidity var Hindex =
- 0.22475541*tempInF*this.humidity - 6.83783*Math.pow(10,-3)*tempInF*tempInF -42.379 +
- 5.481717*Math.pow(10,-2)*this.humidity*this.humidity 2.04901523 * tempInF +
+ 1.22874*Math.pow(10,-3)*tempInF*tempInF*this.humidity 10.14333127 * this.humidity -
+ 8.5282*Math.pow(10,-4)*tempInF*this.humidity*this.humidity 0.22475541 * tempInF * this.humidity -
- 1.99*Math.pow(10,-6)*tempInF*tempInF*this.humidity*this.humidity; 6.83783 * Math.pow(10, -3) * tempInF * tempInF -
5.481717 * Math.pow(10, -2) * this.humidity * this.humidity +
1.22874 * Math.pow(10, -3) * tempInF * tempInF * this.humidity +
8.5282 * Math.pow(10, -4) * tempInF * this.humidity * this.humidity -
1.99 * Math.pow(10, -6) * tempInF * tempInF * this.humidity * this.humidity;
switch (this.config.units) { switch (this.config.units) {
case "metric": this.feelsLike = parseFloat((Hindex - 32) / 1.8).toFixed(0); case "metric":
this.feelsLike = parseFloat((Hindex - 32) / 1.8).toFixed(0);
break; break;
case "imperial": this.feelsLike = Hindex.toFixed(0); case "imperial":
this.feelsLike = Hindex.toFixed(0);
break; break;
case "default": case "default":
var tc = parseFloat((Hindex - 32) / 1.8) + 273.15; var tc = parseFloat((Hindex - 32) / 1.8) + 273.15;
@@ -464,7 +475,7 @@ Module.register("currentweather",{
// The moment().format('h') method has a bug on the Raspberry Pi. // The moment().format('h') method has a bug on the Raspberry Pi.
// So we need to generate the timestring manually. // So we need to generate the timestring manually.
// See issue: https://github.com/MichMich/MagicMirror/issues/181 // See issue: https://github.com/MichMich/MagicMirror/issues/181
var sunriseSunsetDateObject = (sunrise < now && sunset > now) ? sunset : sunrise; var sunriseSunsetDateObject = sunrise < now && sunset > now ? sunset : sunrise;
var timeString = moment(sunriseSunsetDateObject).format("HH:mm"); var timeString = moment(sunriseSunsetDateObject).format("HH:mm");
if (this.config.timeFormat !== 24) { if (this.config.timeFormat !== 24) {
//var hours = sunriseSunsetDateObject.getHours() % 12 || 12; //var hours = sunriseSunsetDateObject.getHours() % 12 || 12;
@@ -483,7 +494,7 @@ Module.register("currentweather",{
} }
this.sunriseSunsetTime = timeString; this.sunriseSunsetTime = timeString;
this.sunriseSunsetIcon = (sunrise < now && sunset > now) ? "wi-sunset" : "wi-sunrise"; this.sunriseSunsetIcon = sunrise < now && sunset > now ? "wi-sunset" : "wi-sunrise";
this.show(this.config.animationSpeed, { lockString: this.identifier }); this.show(this.config.animationSpeed, { lockString: this.identifier });
this.loaded = true; this.loaded = true;
@@ -520,7 +531,7 @@ Module.register("currentweather",{
* return number - Windspeed in beaufort. * return number - Windspeed in beaufort.
*/ */
ms2Beaufort: function (ms) { ms2Beaufort: function (ms) {
var kmh = ms * 60 * 60 / 1000; var kmh = (ms * 60 * 60) / 1000;
var speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000]; var speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
for (var beaufort in speeds) { for (var beaufort in speeds) {
var speed = speeds[beaufort]; var speed = speeds[beaufort];
@@ -578,5 +589,4 @@ Module.register("currentweather",{
var decimals = this.config.roundTemp ? 0 : 1; var decimals = this.config.roundTemp ? 0 : 1;
return parseFloat(temperature).toFixed(decimals); return parseFloat(temperature).toFixed(decimals);
} }
}); });

View File

@@ -7,18 +7,9 @@
// Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name. // Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
var defaultModules = [ var defaultModules = ["alert", "calendar", "clock", "compliments", "currentweather", "helloworld", "newsfeed", "weatherforecast", "updatenotification", "weather"];
"alert",
"calendar",
"clock",
"compliments",
"currentweather",
"helloworld",
"newsfeed",
"weatherforecast",
"updatenotification",
"weather"
];
/*************** DO NOT EDIT THE LINE BELOW ***************/ /*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = defaultModules;} if (typeof module !== "undefined") {
module.exports = defaultModules;
}

View File

@@ -1,4 +1,5 @@
# Module: Hello World # Module: Hello World
The `helloworld` module is one of the default modules of the MagicMirror. It is a simple way to display a static text on the mirror. The `helloworld` module is one of the default modules of the MagicMirror. It is a simple way to display a static text on the mirror.
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/helloworld.html). For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/helloworld.html).

View File

@@ -5,7 +5,6 @@
* MIT Licensed. * MIT Licensed.
*/ */
Module.register("helloworld", { Module.register("helloworld", {
// Default module config. // Default module config.
defaults: { defaults: {
text: "Hello World!" text: "Hello World!"

View File

@@ -1,5 +1,6 @@
# Module: News Feed # Module: News Feed
The `newsfeed` module is one of the default modules of the MagicMirror. The `newsfeed` module is one of the default modules of the MagicMirror.
This module displays news headlines based on an RSS feed. Scrolling through news headlines happens time-based (````updateInterval````), but can also be controlled by sending news feed specific notifications to the module. This module displays news headlines based on an RSS feed. Scrolling through news headlines happens time-based (`updateInterval`), but can also be controlled by sending news feed specific notifications to the module.
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/newsfeed.html). For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/newsfeed.html).

View File

@@ -1,159 +0,0 @@
/* Magic Mirror
* Fetcher
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
var FeedMe = require("feedme");
var request = require("request");
var iconv = require("iconv-lite");
/* Fetcher
* Responsible for requesting an update on the set interval and broadcasting the data.
*
* attribute url string - URL of the news feed.
* attribute reloadInterval number - Reload interval in milliseconds.
* attribute logFeedWarnings boolean - Log warnings when there is an error parsing a news article.
*/
var Fetcher = function(url, reloadInterval, encoding, logFeedWarnings) {
var self = this;
if (reloadInterval < 1000) {
reloadInterval = 1000;
}
var reloadTimer = null;
var items = [];
var fetchFailedCallback = function() {};
var itemsReceivedCallback = function() {};
/* private methods */
/* fetchNews()
* Request the new items.
*/
var fetchNews = function() {
clearTimeout(reloadTimer);
reloadTimer = null;
items = [];
var parser = new FeedMe();
parser.on("item", function(item) {
var title = item.title;
var description = item.description || item.summary || item.content || "";
var pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
var url = item.url || item.link || "";
if (title && pubdate) {
var regex = /(<([^>]+)>)/ig;
description = description.toString().replace(regex, "");
items.push({
title: title,
description: description,
pubdate: pubdate,
url: url,
});
} else if (logFeedWarnings) {
console.log("Can't parse feed item:");
console.log(item);
console.log("Title: " + title);
console.log("Description: " + description);
console.log("Pubdate: " + pubdate);
}
});
parser.on("end", function() {
//console.log("end parsing - " + url);
self.broadcastItems();
scheduleTimer();
});
parser.on("error", function(error) {
fetchFailedCallback(self, error);
scheduleTimer();
});
var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
var 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({uri: url, encoding: null, headers: headers})
.on("error", function(error) {
fetchFailedCallback(self, error);
scheduleTimer();
})
.pipe(iconv.decodeStream(encoding)).pipe(parser);
};
/* scheduleTimer()
* Schedule the timer for the next update.
*/
var scheduleTimer = function() {
//console.log('Schedule update timer.');
clearTimeout(reloadTimer);
reloadTimer = setTimeout(function() {
fetchNews();
}, reloadInterval);
};
/* public methods */
/* setReloadInterval()
* Update the reload interval, but only if we need to increase the speed.
*
* attribute interval number - Interval for the update in milliseconds.
*/
this.setReloadInterval = function(interval) {
if (interval > 1000 && interval < reloadInterval) {
reloadInterval = interval;
}
};
/* startFetch()
* Initiate fetchNews();
*/
this.startFetch = function() {
fetchNews();
};
/* broadcastItems()
* Broadcast the existing items.
*/
this.broadcastItems = function() {
if (items.length <= 0) {
//console.log('No items to broadcast yet.');
return;
}
//console.log('Broadcasting ' + items.length + ' items.');
itemsReceivedCallback(self);
};
this.onReceive = function(callback) {
itemsReceivedCallback = callback;
};
this.onError = function(callback) {
fetchFailedCallback = callback;
};
this.url = function() {
return url;
};
this.items = function() {
return items;
};
};
module.exports = Fetcher;

View File

@@ -5,7 +5,6 @@
* MIT Licensed. * MIT Licensed.
*/ */
Module.register("newsfeed", { Module.register("newsfeed", {
// Default module config. // Default module config.
defaults: { defaults: {
feeds: [ feeds: [
@@ -85,11 +84,11 @@ Module.register("newsfeed",{
// Override dom generator. // Override dom generator.
getDom: function () { getDom: function () {
var wrapper = document.createElement("div"); const wrapper = document.createElement("div");
if (this.config.feedUrl) { if (this.config.feedUrl) {
wrapper.className = "small bright"; wrapper.className = "small bright";
wrapper.innerHTML = this.translate("configuration_changed"); wrapper.innerHTML = this.translate("MODULE_CONFIG_CHANGED", { MODULE_NAME: "Newsfeed" });
return wrapper; return wrapper;
} }
@@ -98,10 +97,9 @@ Module.register("newsfeed",{
} }
if (this.newsItems.length > 0) { if (this.newsItems.length > 0) {
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications // this.config.showFullArticle is a run-time configuration, triggered by optional notifications
if (!this.config.showFullArticle && (this.config.showSourceTitle || this.config.showPublishDate)) { if (!this.config.showFullArticle && (this.config.showSourceTitle || this.config.showPublishDate)) {
var sourceAndTimestamp = document.createElement("div"); const sourceAndTimestamp = document.createElement("div");
sourceAndTimestamp.className = "newsfeed-source light small dimmed"; sourceAndTimestamp.className = "newsfeed-source light small dimmed";
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") { if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") {
@@ -113,7 +111,7 @@ Module.register("newsfeed",{
if (this.config.showPublishDate) { if (this.config.showPublishDate) {
sourceAndTimestamp.innerHTML += moment(new Date(this.newsItems[this.activeItem].pubdate)).fromNow(); sourceAndTimestamp.innerHTML += moment(new Date(this.newsItems[this.activeItem].pubdate)).fromNow();
} }
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "" || this.config.showPublishDate) { if ((this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") || this.config.showPublishDate) {
sourceAndTimestamp.innerHTML += ":"; sourceAndTimestamp.innerHTML += ":";
} }
@@ -123,17 +121,14 @@ Module.register("newsfeed",{
//Remove selected tags from the beginning of rss feed items (title or description) //Remove selected tags from the beginning of rss feed items (title or description)
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") { if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
for (let f = 0; f < this.config.startTags.length; f++) { 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]) { 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); 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.config.removeStartTags === "description" || this.config.removeStartTags === "both") {
if (this.isShowingDescription) { if (this.isShowingDescription) {
for (let f = 0; f < this.config.startTags.length; f++) { 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]) { if (this.newsItems[this.activeItem].description.slice(0, this.config.startTags[f].length) === this.config.startTags[f]) {
@@ -141,7 +136,6 @@ Module.register("newsfeed",{
} }
} }
} }
} }
//Remove selected tags from the end of rss feed items (title or description) //Remove selected tags from the end of rss feed items (title or description)
@@ -160,26 +154,25 @@ Module.register("newsfeed",{
} }
} }
} }
} }
if (!this.config.showFullArticle) { if (!this.config.showFullArticle) {
var title = document.createElement("div"); const title = document.createElement("div");
title.className = "newsfeed-title bright medium light" + (!this.config.wrapTitle ? " no-wrap" : ""); title.className = "newsfeed-title bright medium light" + (!this.config.wrapTitle ? " no-wrap" : "");
title.innerHTML = this.newsItems[this.activeItem].title; title.innerHTML = this.newsItems[this.activeItem].title;
wrapper.appendChild(title); wrapper.appendChild(title);
} }
if (this.isShowingDescription) { if (this.isShowingDescription) {
var description = document.createElement("div"); const description = document.createElement("div");
description.className = "newsfeed-desc small light" + (!this.config.wrapDescription ? " no-wrap" : ""); description.className = "newsfeed-desc small light" + (!this.config.wrapDescription ? " no-wrap" : "");
var txtDesc = this.newsItems[this.activeItem].description; 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); description.innerHTML = this.config.truncDescription ? (txtDesc.length > this.config.lengthDescription ? txtDesc.substring(0, this.config.lengthDescription) + "..." : txtDesc) : txtDesc;
wrapper.appendChild(description); wrapper.appendChild(description);
} }
if (this.config.showFullArticle) { if (this.config.showFullArticle) {
var fullArticle = document.createElement("iframe"); const fullArticle = document.createElement("iframe");
fullArticle.className = ""; fullArticle.className = "";
fullArticle.style.width = "100vw"; fullArticle.style.width = "100vw";
// very large height value to allow scrolling // very large height value to allow scrolling
@@ -196,7 +189,6 @@ Module.register("newsfeed",{
if (this.config.hideLoading) { if (this.config.hideLoading) {
this.show(); this.show();
} }
} else { } else {
if (this.config.hideLoading) { if (this.config.hideLoading) {
this.hide(); this.hide();
@@ -213,8 +205,8 @@ Module.register("newsfeed",{
return typeof this.newsItems[this.activeItem].url === "string" ? this.newsItems[this.activeItem].url : this.newsItems[this.activeItem].url.href; return typeof this.newsItems[this.activeItem].url === "string" ? this.newsItems[this.activeItem].url : this.newsItems[this.activeItem].url.href;
}, },
/* registerFeeds() /**
* registers the feeds to be used by the backend. * Registers the feeds to be used by the backend.
*/ */
registerFeeds: function () { registerFeeds: function () {
for (var f in this.config.feeds) { for (var f in this.config.feeds) {
@@ -226,10 +218,10 @@ Module.register("newsfeed",{
} }
}, },
/* generateFeed() /**
* Generate an ordered list of items for this configured module. * Generate an ordered list of items for this configured module.
* *
* attribute feeds object - An object with feeds returned by the node helper. * @param {object} feeds An object with feeds returned by the node helper.
*/ */
generateFeed: function (feeds) { generateFeed: function (feeds) {
var newsItems = []; var newsItems = [];
@@ -239,7 +231,7 @@ Module.register("newsfeed",{
for (var i in feedItems) { for (var i in feedItems) {
var item = feedItems[i]; var item = feedItems[i];
item.sourceTitle = this.titleForFeed(feed); item.sourceTitle = this.titleForFeed(feed);
if (!(this.config.ignoreOldItems && ((Date.now() - new Date(item.pubdate)) > this.config.ignoreOlderThan))) { if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) {
newsItems.push(item); newsItems.push(item);
} }
} }
@@ -267,8 +259,8 @@ Module.register("newsfeed",{
// get updated news items and broadcast them // get updated news items and broadcast them
var updatedItems = []; var updatedItems = [];
newsItems.forEach(value => { newsItems.forEach((value) => {
if (this.newsItems.findIndex(value1 => value1 === value) === -1) { if (this.newsItems.findIndex((value1) => value1 === value) === -1) {
// Add item to updated items list // Add item to updated items list
updatedItems.push(value); updatedItems.push(value);
} }
@@ -282,12 +274,11 @@ Module.register("newsfeed",{
this.newsItems = newsItems; this.newsItems = newsItems;
}, },
/* subscribedToFeed(feedUrl) /**
* Check if this module is configured to show this feed. * Check if this module is configured to show this feed.
* *
* attribute feedUrl string - Url of the feed to check. * @param {string} feedUrl Url of the feed to check.
* * @returns {boolean} True if it is subscribed, false otherwise
* returns bool
*/ */
subscribedToFeed: function (feedUrl) { subscribedToFeed: function (feedUrl) {
for (var f in this.config.feeds) { for (var f in this.config.feeds) {
@@ -299,12 +290,11 @@ Module.register("newsfeed",{
return false; return false;
}, },
/* titleForFeed(feedUrl) /**
* Returns title for a specific feed Url. * Returns title for the specific feed url.
* *
* attribute feedUrl string - Url of the feed to check. * @param {string} feedUrl Url of the feed
* * @returns {string} The title of the feed
* returns string
*/ */
titleForFeed: function (feedUrl) { titleForFeed: function (feedUrl) {
for (var f in this.config.feeds) { for (var f in this.config.feeds) {
@@ -316,7 +306,7 @@ Module.register("newsfeed",{
return ""; return "";
}, },
/* scheduleUpdateInterval() /**
* Schedule visual update. * Schedule visual update.
*/ */
scheduleUpdateInterval: function () { scheduleUpdateInterval: function () {
@@ -340,17 +330,6 @@ Module.register("newsfeed",{
}, this.config.updateInterval); }, this.config.updateInterval);
}, },
/* capitalizeFirstLetter(string)
* Capitalizes the first character of a string.
*
* argument string string - Input string.
*
* return string - Capitalized output string.
*/
capitalizeFirstLetter: function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
resetDescrOrFullArticleAndTimer: function () { resetDescrOrFullArticleAndTimer: function () {
this.isShowingDescription = this.config.showDescription; this.isShowingDescription = this.config.showDescription;
this.config.showFullArticle = false; this.config.showFullArticle = false;
@@ -364,7 +343,7 @@ Module.register("newsfeed",{
}, },
notificationReceived: function (notification, payload, sender) { notificationReceived: function (notification, payload, sender) {
var before = this.activeItem; const before = this.activeItem;
if (notification === "ARTICLE_NEXT") { if (notification === "ARTICLE_NEXT") {
this.activeItem++; this.activeItem++;
if (this.activeItem >= this.newsItems.length) { if (this.activeItem >= this.newsItems.length) {
@@ -390,8 +369,7 @@ Module.register("newsfeed",{
window.scrollTo(0, this.scrollPosition); window.scrollTo(0, this.scrollPosition);
Log.info(this.name + " - scrolling down"); Log.info(this.name + " - scrolling down");
Log.info(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength); Log.info(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
} } else {
else {
this.showFullArticle(); this.showFullArticle();
} }
} else if (notification === "ARTICLE_SCROLL_UP") { } else if (notification === "ARTICLE_SCROLL_UP") {

View File

@@ -0,0 +1,159 @@
/* Magic Mirror
* Node Helper: Newsfeed - NewsfeedFetcher
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const Log = require("../../../js/logger.js");
const FeedMe = require("feedme");
const request = require("request");
const iconv = require("iconv-lite");
/**
* Responsible for requesting an update on the set interval and broadcasting the data.
*
* @param {string} url URL of the news feed.
* @param {number} reloadInterval Reload interval in milliseconds.
* @param {string} encoding Encoding of the feed.
* @param {boolean} logFeedWarnings If true log warnings when there is an error parsing a news article.
* @class
*/
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
const self = this;
let reloadTimer = null;
let items = [];
let fetchFailedCallback = function () {};
let itemsReceivedCallback = function () {};
if (reloadInterval < 1000) {
reloadInterval = 1000;
}
/* private methods */
/**
* Request the new items.
*/
const fetchNews = function () {
clearTimeout(reloadTimer);
reloadTimer = null;
items = [];
const parser = new FeedMe();
parser.on("item", function (item) {
const title = item.title;
let description = item.description || item.summary || item.content || "";
const pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
const url = item.url || item.link || "";
if (title && pubdate) {
const regex = /(<([^>]+)>)/gi;
description = description.toString().replace(regex, "");
items.push({
title: title,
description: description,
pubdate: pubdate,
url: url
});
} else if (logFeedWarnings) {
Log.warn("Can't parse feed item:");
Log.warn(item);
Log.warn("Title: " + title);
Log.warn("Description: " + description);
Log.warn("Pubdate: " + pubdate);
}
});
parser.on("end", function () {
self.broadcastItems();
scheduleTimer();
});
parser.on("error", function (error) {
fetchFailedCallback(self, 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
};
request(url, opts)
.on("error", function (error) {
fetchFailedCallback(self, error);
scheduleTimer();
})
.pipe(iconv.decodeStream(encoding))
.pipe(parser);
};
/**
* Schedule the timer for the next update.
*/
const scheduleTimer = function () {
clearTimeout(reloadTimer);
reloadTimer = setTimeout(function () {
fetchNews();
}, reloadInterval);
};
/* public methods */
/**
* Update the reload interval, but only if we need to increase the speed.
*
* @param {number} interval Interval for the update in milliseconds.
*/
this.setReloadInterval = function (interval) {
if (interval > 1000 && interval < reloadInterval) {
reloadInterval = interval;
}
};
/**
* Initiate fetchNews();
*/
this.startFetch = function () {
fetchNews();
};
/**
* Broadcast the existing items.
*/
this.broadcastItems = function () {
if (items.length <= 0) {
Log.info("Newsfeed-Fetcher: No items to broadcast yet.");
return;
}
Log.info("Newsfeed-Fetcher: Broadcasting " + items.length + " items.");
itemsReceivedCallback(self);
};
this.onReceive = function (callback) {
itemsReceivedCallback = callback;
};
this.onError = function (callback) {
fetchFailedCallback = callback;
};
this.url = function () {
return url;
};
this.items = function () {
return items;
};
};
module.exports = NewsfeedFetcher;

View File

@@ -5,64 +5,62 @@
* MIT Licensed. * MIT Licensed.
*/ */
var NodeHelper = require("node_helper"); const NodeHelper = require("node_helper");
var validUrl = require("valid-url"); const validUrl = require("valid-url");
var Fetcher = require("./fetcher.js"); const NewsfeedFetcher = require("./newsfeedfetcher.js");
const Log = require("../../../js/logger");
module.exports = NodeHelper.create({ module.exports = NodeHelper.create({
// Subclass start method. // Override start method.
start: function () { start: function () {
console.log("Starting module: " + this.name); Log.log("Starting node helper for: " + this.name);
this.fetchers = []; this.fetchers = [];
}, },
// Subclass socketNotificationReceived received. // Override socketNotificationReceived received.
socketNotificationReceived: function (notification, payload) { socketNotificationReceived: function (notification, payload) {
if (notification === "ADD_FEED") { if (notification === "ADD_FEED") {
this.createFetcher(payload.feed, payload.config); this.createFetcher(payload.feed, payload.config);
return;
} }
}, },
/* createFetcher(feed, config) /**
* Creates a fetcher for a new feed if it doesn't exist yet. * Creates a fetcher for a new feed if it doesn't exist yet.
* Otherwise it reuses the existing one. * Otherwise it reuses the existing one.
* *
* attribute feed object - A feed object. * @param {object} feed The feed object.
* attribute config object - A configuration object containing reload interval in milliseconds. * @param {object} config The configuration object.
*/ */
createFetcher: function (feed, config) { createFetcher: function (feed, config) {
var self = this; const url = feed.url || "";
const encoding = feed.encoding || "UTF-8";
var url = feed.url || ""; const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
var encoding = feed.encoding || "UTF-8";
var reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
if (!validUrl.isUri(url)) { if (!validUrl.isUri(url)) {
self.sendSocketNotification("INCORRECT_URL", url); this.sendSocketNotification("INCORRECT_URL", url);
return; return;
} }
var fetcher; let fetcher;
if (typeof self.fetchers[url] === "undefined") { if (typeof this.fetchers[url] === "undefined") {
console.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval); Log.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval);
fetcher = new Fetcher(url, reloadInterval, encoding, config.logFeedWarnings); fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings);
fetcher.onReceive(function(fetcher) { fetcher.onReceive(() => {
self.broadcastFeeds(); this.broadcastFeeds();
}); });
fetcher.onError(function(fetcher, error) { fetcher.onError((fetcher, error) => {
self.sendSocketNotification("FETCH_ERROR", { this.sendSocketNotification("FETCH_ERROR", {
url: fetcher.url(), url: fetcher.url(),
error: error error: error
}); });
}); });
self.fetchers[url] = fetcher; this.fetchers[url] = fetcher;
} else { } else {
console.log("Use existing news fetcher for url: " + url); Log.log("Use existing news fetcher for url: " + url);
fetcher = self.fetchers[url]; fetcher = this.fetchers[url];
fetcher.setReloadInterval(reloadInterval); fetcher.setReloadInterval(reloadInterval);
fetcher.broadcastItems(); fetcher.broadcastItems();
} }
@@ -70,7 +68,7 @@ module.exports = NodeHelper.create({
fetcher.startFetch(); fetcher.startFetch();
}, },
/* broadcastFeeds() /**
* Creates an object with all feed items of the different registered feeds, * Creates an object with all feed items of the different registered feeds,
* and broadcasts these using sendSocketNotification. * and broadcasts these using sendSocketNotification.
*/ */

View File

@@ -1,3 +0,0 @@
{
"configuration_changed": "Die Konfigurationsoptionen für das Newsfeed-Modul haben sich geändert. \nBitte überprüfen Sie die Dokumentation."
}

View File

@@ -1,3 +0,0 @@
{
"configuration_changed": "The configuration options for the newsfeed module have changed.\nPlease check the documentation."
}

View File

@@ -1,3 +0,0 @@
{
"configuration_changed": "Las opciones de configuración para el módulo de suministro de noticias han cambiado. \nVerifique la documentación."
}

View File

@@ -1,3 +0,0 @@
{
"configuration_changed": "Les options de configuration du module newsfeed ont changé. \nVeuillez consulter la documentation."
}

View File

@@ -1,4 +1,5 @@
# Module: Update Notification # Module: Update Notification
The `updatenotification` module is one of the default modules of the MagicMirror. The `updatenotification` module is one of the default modules of the MagicMirror.
This will display a message whenever a new version of the MagicMirror application is available. This will display a message whenever a new version of the MagicMirror application is available.

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