mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-08-21 12:55:22 +00:00
Merge remote-tracking branch 'origin/develop' into tests-404-vendors
This commit is contained in:
@@ -1,72 +0,0 @@
|
|||||||
# Various Node ignoramuses.
|
|
||||||
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
lib-cov
|
|
||||||
coverage
|
|
||||||
.grunt
|
|
||||||
.lock-wscript
|
|
||||||
build/Release
|
|
||||||
node_modules
|
|
||||||
jspm_modules
|
|
||||||
.npm
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Various Windows ignoramuses.
|
|
||||||
Thumbs.db
|
|
||||||
ehthumbs.db
|
|
||||||
Desktop.ini
|
|
||||||
$RECYCLE.BIN/
|
|
||||||
*.cab
|
|
||||||
*.msi
|
|
||||||
*.msm
|
|
||||||
*.msp
|
|
||||||
*.lnk
|
|
||||||
|
|
||||||
# Various OSX ignoramuses.
|
|
||||||
.DS_Store
|
|
||||||
.AppleDouble
|
|
||||||
.LSOverride
|
|
||||||
Icon
|
|
||||||
._*
|
|
||||||
.DocumentRevisions-V100
|
|
||||||
.fseventsd
|
|
||||||
.Spotlight-V100
|
|
||||||
.TemporaryItems
|
|
||||||
.Trashes
|
|
||||||
.VolumeIcon.icns
|
|
||||||
.AppleDB
|
|
||||||
.AppleDesktop
|
|
||||||
Network Trash Folder
|
|
||||||
Temporary Items
|
|
||||||
.apdisk
|
|
||||||
|
|
||||||
# Various Linux ignoramuses.
|
|
||||||
|
|
||||||
.fuse_hidden*
|
|
||||||
.directory
|
|
||||||
.Trash-*
|
|
||||||
|
|
||||||
# Various Magic Mirror ignoramuses and anti-ignoramuses.
|
|
||||||
|
|
||||||
# Don't ignore the node_helper core module.
|
|
||||||
!/modules/node_helper
|
|
||||||
!/modules/node_helper/**
|
|
||||||
|
|
||||||
# Ignore all modules except the default modules.
|
|
||||||
/modules/**
|
|
||||||
!/modules/default/**
|
|
||||||
|
|
||||||
# Ignore changes to the custom css files.
|
|
||||||
/css/custom.css
|
|
||||||
|
|
||||||
# Ignore unnecessary files for docker
|
|
||||||
CHANGELOG.md
|
|
||||||
LICENSE.md
|
|
||||||
README.md
|
|
||||||
Gruntfile.js
|
|
||||||
.*
|
|
@@ -3,4 +3,3 @@ vendor/*
|
|||||||
!/modules/default/**
|
!/modules/default/**
|
||||||
!/modules/node_helper
|
!/modules/node_helper
|
||||||
!/modules/node_helper/**
|
!/modules/node_helper/**
|
||||||
!/modules/default/defaultmodules.js
|
|
||||||
|
@@ -2,15 +2,23 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"indent": ["error", "tab"],
|
"indent": ["error", "tab"],
|
||||||
"quotes": ["error", "double"],
|
"quotes": ["error", "double"],
|
||||||
|
"semi": ["error"],
|
||||||
"max-len": ["error", 250],
|
"max-len": ["error", 250],
|
||||||
"curly": "error",
|
"curly": "error",
|
||||||
"camelcase": ["error", {"properties": "never"}],
|
"camelcase": ["error", {"properties": "never"}],
|
||||||
"no-trailing-spaces": ["error"],
|
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1 }],
|
||||||
|
"no-trailing-spaces": ["error", {"ignoreComments": false }],
|
||||||
"no-irregular-whitespace": ["error"]
|
"no-irregular-whitespace": ["error"]
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"node": true,
|
"node": true,
|
||||||
"es6": true
|
"es6": true
|
||||||
}
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"globalReturn": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
19
.github/stale.yml
vendored
Normal file
19
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Number of days of inactivity before an issue becomes stale
|
||||||
|
daysUntilStale: 60
|
||||||
|
# Number of days of inactivity before a stale issue is closed
|
||||||
|
daysUntilClose: 7
|
||||||
|
# Issues with these labels will never be considered stale
|
||||||
|
exemptLabels:
|
||||||
|
- pinned
|
||||||
|
- security
|
||||||
|
- under investigation
|
||||||
|
- pr welcome
|
||||||
|
# Label to use when marking an issue as stale
|
||||||
|
staleLabel: wontfix
|
||||||
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
|
for your contributions.
|
||||||
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
|
closeComment: false
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -16,6 +16,12 @@ jspm_modules
|
|||||||
.npm
|
.npm
|
||||||
.node_repl_history
|
.node_repl_history
|
||||||
|
|
||||||
|
# Visual Studio Code ignoramuses.
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# IDE Code ignoramuses.
|
||||||
|
.idea/
|
||||||
|
|
||||||
# Various Windows ignoramuses.
|
# Various Windows ignoramuses.
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
|
10
.travis.yml
10
.travis.yml
@@ -1,16 +1,18 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- "7"
|
- "8"
|
||||||
- "6"
|
|
||||||
- "5.1"
|
|
||||||
before_script:
|
before_script:
|
||||||
|
- yarn danger ci
|
||||||
- npm install grunt-cli -g
|
- npm install grunt-cli -g
|
||||||
- "export DISPLAY=:99.0"
|
- "export DISPLAY=:99.0"
|
||||||
- "sh -e /etc/init.d/xvfb start"
|
- "sh -e /etc/init.d/xvfb start"
|
||||||
- sleep 5
|
- sleep 5
|
||||||
script:
|
script:
|
||||||
- grunt
|
- grunt
|
||||||
- npm test
|
- npm run test:unit
|
||||||
|
- npm run test:e2e
|
||||||
|
after_script:
|
||||||
|
- npm list
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- node_modules
|
- node_modules
|
||||||
|
414
CHANGELOG.md
Normal file → Executable file
414
CHANGELOG.md
Normal file → Executable file
@@ -1,10 +1,372 @@
|
|||||||
# MagicMirror² Change Log
|
# MagicMirror² Change Log
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
## [2.1.2] - Unreleased
|
---
|
||||||
|
|
||||||
|
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror² core.
|
||||||
|
|
||||||
|
## [2.9.0] - Unreleased (Develop Branch)
|
||||||
|
|
||||||
|
*This release is scheduled to be released on 2019-10-01.*
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add test check URLs of vendors 200 and 404 HTTP CODE.
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
- Updatenotification module: Display update notification for a limited (configurable) time.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Updatenotification module: Properly handle race conditions, prevent crash.
|
||||||
|
- Send `NEWS_FEED` notification also for the first news messages which are shown
|
||||||
|
- Fixed issue where weather module would not refresh data after a network or API outage [#1722](https://github.com/MichMich/MagicMirror/issues/1722)
|
||||||
|
|
||||||
|
## [2.8.0] - 2019-07-01
|
||||||
|
|
||||||
|
ℹ️ **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
|
||||||
|
- Option to show event location in calendar
|
||||||
|
- Finnish translation for "Feels" and "Weeks"
|
||||||
|
- Russian translation for “Feels”
|
||||||
|
- Calendar module: added `nextDaysRelative` config option
|
||||||
|
- Add `broadcastPastEvents` config option for calendars to include events from the past `maximumNumberOfDays` in event broadcasts
|
||||||
|
- Added feature to broadcast news feed items `NEWS_FEED` and updated news items `NEWS_FEED_UPDATED` in default [newsfeed](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/newsfeed) module (when news is updated) with documented default and `config.js` options in [README.md](https://github.com/MichMich/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||||
|
- Added notifications to default `clock` module broadcasting `CLOCK_SECOND` and `CLOCK_MINUTE` for the respective time elapsed.
|
||||||
|
- Added UK Met Office Datapoint feed as a provider in the default weather module.
|
||||||
|
- Added new provider class
|
||||||
|
- Added suncalc.js dependency to calculate sun times (not provided in UK Met Office feed)
|
||||||
|
- Added "tempUnits" and "windUnits" to allow, for example, temp in metric (i.e. celsius) and wind in imperial (i.e. mph). These will override "units" if specified, otherwise the "units" value will be used.
|
||||||
|
- Use Feels Like temp from feed if present
|
||||||
|
- Optionally display probability of precipitation (PoP) in current weather (UK Met Office data)
|
||||||
|
- Automatically try to fix eslint errors by passing `--fix` option to it
|
||||||
|
- Added sunrise and sunset times to weathergov weather provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705)
|
||||||
|
- Added "useLocationAsHeader" to display "location" in `config.js` as header when location name is not returned
|
||||||
|
- Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
- English translation for "Feels" to "Feels like"
|
||||||
|
- Fixed the example calender url in `config.js.sample`
|
||||||
|
- Update `ical.js` to solve various calendar issues.
|
||||||
|
- Update weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
|
||||||
|
- Only update clock once per minute when seconds aren't shown
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed uncaught exception, race condition on module update
|
||||||
|
- Fixed issue [#1696](https://github.com/MichMich/MagicMirror/issues/1696), some ical files start date to not parse to date type
|
||||||
|
- Allowance HTML5 autoplay-policy (policy is changed from Chrome 66 updates)
|
||||||
|
- Handle SIGTERM messages
|
||||||
|
- Fixes sliceMultiDayEvents so it respects maximumNumberOfDays
|
||||||
|
- Minor types in default NewsFeed [README.md](https://github.com/MichMich/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||||
|
- Fix typos and small syntax errors, cleanup dependencies, remove multiple-empty-lines, add semi-rule
|
||||||
|
- Fixed issues with calendar not displaying one-time changes to repeating events
|
||||||
|
- Updated the fetchedLocationName variable in currentweather.js so that city shows up in the header
|
||||||
|
|
||||||
|
### Updated installer
|
||||||
|
- 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
|
||||||
|
- check for npm installed. node install doesn't do npm anymore
|
||||||
|
- check for mac as part of PM2 install, add install option string
|
||||||
|
- update pm2 config with current username instead of hard coded 'pi'
|
||||||
|
- check for screen saver config, "/etc/xdg/lxsession", bypass if not setup
|
||||||
|
|
||||||
|
## [2.7.1] - 2019-04-02
|
||||||
|
|
||||||
|
Fixed `package.json` version number.
|
||||||
|
|
||||||
|
## [2.7.0] - 2019-04-01
|
||||||
|
|
||||||
|
ℹ️ **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
|
||||||
|
- Italian translation for "Feels"
|
||||||
|
- Basic Klingon (tlhIngan Hol) translations
|
||||||
|
- Disabled the screensaver on raspbian with installation script
|
||||||
|
- Added option to truncate the number of vertical lines a calendar item can span if `wrapEvents` is enabled.
|
||||||
|
- Danish translation for "Feels" and "Weeks"
|
||||||
|
- Added option to split multiple day events in calendar to separate numbered events
|
||||||
|
- Slovakian translation
|
||||||
|
- Alerts now can contain Font Awesome icons
|
||||||
|
- Notifications display time can be set in request
|
||||||
|
- Newsfeed: added support for `ARTICLE_INFO_REQUEST` notification
|
||||||
|
- Add `name` config option for calendars to be sent along with event broadcasts
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
- 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
|
||||||
|
- More verbose error message on console if the config is malformed
|
||||||
|
- Updated installer script to install Node.js version 10.x
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed temperature displays in currentweather and weatherforecast modules [#1503](https://github.com/MichMich/MagicMirror/issues/1503), [#1511](https://github.com/MichMich/MagicMirror/issues/1511).
|
||||||
|
- Fixed unhandled error on bad git data in updatenotification module [#1285](https://github.com/MichMich/MagicMirror/issues/1285).
|
||||||
|
- Weather forecast now works with openweathermap in new weather module. Daily data are displayed, see issue [#1504](https://github.com/MichMich/MagicMirror/issues/1504).
|
||||||
|
- Fixed analogue clock border display issue where non-black backgrounds used (previous fix for issue 611)
|
||||||
|
- Fixed compatibility issues caused when modules request different versions of Font Awesome, see issue [#1522](https://github.com/MichMich/MagicMirror/issues/1522). MagicMirror now uses [Font Awesome 5 with v4 shims included for backwards compatibility](https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4#shims).
|
||||||
|
- Installation script problems with raspbian
|
||||||
|
- Calendar: only show repeating count if the event is actually repeating [#1534](https://github.com/MichMich/MagicMirror/pull/1534)
|
||||||
|
- Calendar: Fix exdate handling when multiple values are specified (comma separated)
|
||||||
|
- Calendar: Fix relative date handling for fulldate events, calculate difference always from start of day [#1572](https://github.com/MichMich/MagicMirror/issues/1572)
|
||||||
|
- Fix null dereference in moduleNeedsUpdate when the module isn't visible
|
||||||
|
- Calendar: Fixed event end times by setting default calendarEndTime to "LT" (Local time format). [#1479]
|
||||||
|
- Calendar: Fixed missing calendar fetchers after server process restarts [#1589](https://github.com/MichMich/MagicMirror/issues/1589)
|
||||||
|
- Notification: fixed background color (was white text on white background)
|
||||||
|
- Use getHeader instead of data.header when creating the DOM so overwriting the function also propagates into it
|
||||||
|
- Fix documentation of `useKMPHwind` option in currentweather
|
||||||
|
|
||||||
|
### New weather module
|
||||||
|
- Fixed weather forecast table display [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||||
|
- Dimmed loading indicator for weather forecast.
|
||||||
|
- Implemented config option `decimalSymbol` [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||||
|
- Aligned indoor values in current weather vertical [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||||
|
- Added humidity support to nunjuck unit filter.
|
||||||
|
- Do not display degree symbol for temperature in Kelvin [#1503](https://github.com/MichMich/MagicMirror/issues/1503).
|
||||||
|
- Weather forecast now works with openweathermap for both, `/forecast` and `/forecast/daily`, in new weather module. If you use the `/forecast`-weatherEndpoint, the hourly data are converted to daily data, see issues [#1504](https://github.com/MichMich/MagicMirror/issues/1504), [#1513](https://github.com/MichMich/MagicMirror/issues/1513).
|
||||||
|
- Added fade, fadePoint and maxNumberOfDays properties to the forecast mode [#1516](https://github.com/MichMich/MagicMirror/issues/1516)
|
||||||
|
- Fixed Loading string and decimalSymbol string replace [#1538](https://github.com/MichMich/MagicMirror/issues/1538)
|
||||||
|
- Show Snow amounts in new weather module [#1545](https://github.com/MichMich/MagicMirror/issues/1545)
|
||||||
|
- Added weather.gov as a new weather provider for US locations
|
||||||
|
|
||||||
|
## [2.6.0] - 2019-01-01
|
||||||
|
|
||||||
|
ℹ️ **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 ✨
|
||||||
|
- 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!
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 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.
|
||||||
|
- Missing `showEnd` in calendar documentation
|
||||||
|
- Screenshot for the new feed module
|
||||||
|
- Screenshot for the compliments module
|
||||||
|
- Screenshot for the clock module
|
||||||
|
- Screenshot for the current weather
|
||||||
|
- Screenshot for the weather forecast module
|
||||||
|
- Portuguese translation for "Feels"
|
||||||
|
- Croatian translation
|
||||||
|
- Fading for dateheaders timeFormat in Calendar [#1464](https://github.com/MichMich/MagicMirror/issues/1464)
|
||||||
|
- Documentation for the existing `scale` option in the Weather Forecast module.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Allow to parse recurring calendar events where the start date is before 1900
|
||||||
|
- Fixed Polish translation for Single Update Info
|
||||||
|
- Ignore entries with unparseable details in the calendar module
|
||||||
|
- Bug showing FullDayEvents one day too long in calendar fixed
|
||||||
|
- Bug in newsfeed when `removeStartTags` is used on the description [#1478](https://github.com/MichMich/MagicMirror/issues/1478)
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
- The default calendar setting `showEnd` is changed to `false`.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- The Weather Forecast module by default displays the ° symbol after every numeric value to be consistent with the Current Weather module.
|
||||||
|
|
||||||
|
|
||||||
|
## [2.5.0] - 2018-10-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Romanian translation for "Feels"
|
||||||
|
- Support multi-line compliments
|
||||||
|
- Simplified Chinese translation for "Feels"
|
||||||
|
- Polish translate for "Feels"
|
||||||
|
- French translate for "Feels"
|
||||||
|
- Translations for newsfeed module
|
||||||
|
- Support for toggling news article in fullscreen
|
||||||
|
- Hungarian translation for "Feels" and "Week"
|
||||||
|
- Spanish translation for "Feels"
|
||||||
|
- Add classes instead of inline style to the message from the module Alert
|
||||||
|
- Support for events having a duration instead of an end
|
||||||
|
- Support for showing end of events through config parameters showEnd and dateEndFormat
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed gzip encoded calendar loading issue #1400.
|
||||||
|
- Mixup between german and spanish translation for newsfeed.
|
||||||
|
- Fixed close dates to be absolute, if no configured in the config.js - module Calendar
|
||||||
|
- Fixed the updatenotification module message about new commits in the repository, so they can be correctly localized in singular and plural form.
|
||||||
|
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MichMich/MagicMirror/issues/1374)
|
||||||
|
- Fix calendar parsing issue for Midori on RasperryPi Zero w, related to issue #694.
|
||||||
|
- Fix weather city ID link in sample config
|
||||||
|
- Fixed issue with clientonly not updating with IP address and port provided on command line.
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- Updated Simplified Chinese translation
|
||||||
|
- Swedish translations
|
||||||
|
- Hungarian translations for the updatenotification module
|
||||||
|
- Updated Norsk bokmål translation
|
||||||
|
- Updated Norsk nynorsk translation
|
||||||
|
- Consider multi days event as full day events
|
||||||
|
|
||||||
|
## [2.4.1] - 2018-07-04
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix weather parsing issue #1332.
|
||||||
|
|
||||||
|
## [2.4.0] - 2018-07-01
|
||||||
|
|
||||||
|
⚠️ **Warning:** This release includes an updated version of Electron. This requires a Raspberry Pi configuration change to allow the best performance and prevent the CPU from overheating. Please read the information on the [MagicMirror Wiki](https://github.com/michmich/magicmirror/wiki/configuring-the-raspberry-pi#enable-the-open-gl-driver-to-decrease-electrons-cpu-usage).
|
||||||
|
|
||||||
|
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Enabled translation of feelsLike for module currentweather
|
||||||
|
- Added support for on-going calendar events
|
||||||
|
- Added scroll up in fullscreen newsfeed article view
|
||||||
|
- Changed fullscreen newsfeed width from 100% to 100vw (better results)
|
||||||
|
- Added option to calendar module that colors only the symbol instead of the whole line
|
||||||
|
- Added option for new display format in the calendar module with date headers with times/events below.
|
||||||
|
- Ability to fetch compliments from a remote server
|
||||||
|
- Add regex filtering to calendar module
|
||||||
|
- Customize classes for table
|
||||||
|
- Added option to newsfeed module to only log error parsing a news article if enabled
|
||||||
|
- Add update translations for Português Brasileiro
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Upgrade to Electron 2.0.0.
|
||||||
|
- Remove yarn-or-npm which breaks production builds.
|
||||||
|
- Invoke module suspend even if no dom content. [#1308](https://github.com/MichMich/MagicMirror/issues/1308)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed issue where wind chill could not be displayed in Fahrenheit. [#1247](https://github.com/MichMich/MagicMirror/issues/1247)
|
||||||
|
- Fixed issues where a module crashes when it tries to dismiss a non existing alert. [#1240](https://github.com/MichMich/MagicMirror/issues/1240)
|
||||||
|
- In default module currentWeather/currentWeather.js line 296, 300, self.config.animationSpeed can not be found because the notificationReceived function does not have "self" variable.
|
||||||
|
- Fixed browser-side code to work on the Midori browser.
|
||||||
|
- Fixed issue where heat index was reporting incorrect values in Celsius and Fahrenheit. [#1263](https://github.com/MichMich/MagicMirror/issues/1263)
|
||||||
|
- Fixed weatherforecast to use dt_txt field instead of dt to handle timezones better
|
||||||
|
- Newsfeed now remembers to show the description when `"ARTICLE_LESS_DETAILS"` is called if the user wants to always show the description. [#1282](https://github.com/MichMich/MagicMirror/issues/1282)
|
||||||
|
- `clientonly/*.js` is now linted, and one linting error is fixed
|
||||||
|
- Fix issue #1196 by changing underscore to hyphen in locale id, in align with momentjs.
|
||||||
|
- Fixed issue where heat index and wind chill were reporting incorrect values in Kelvin. [#1263](https://github.com/MichMich/MagicMirror/issues/1263)
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
- Updated Italian translation
|
||||||
|
- Updated German translation
|
||||||
|
- Updated Dutch translation
|
||||||
|
|
||||||
|
## [2.3.1] - 2018-04-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add new settings in compliments module: setting time intervals for morning and afternoon
|
||||||
|
- Add system notification `MODULE_DOM_CREATED` for notifying each module when their Dom has been fully loaded.
|
||||||
|
- Add types for module.
|
||||||
|
- Implement Danger.js to notify contributors when CHANGELOG.md is missing in PR.
|
||||||
|
- Allow to scroll in full page article view of default newsfeed module with gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures)
|
||||||
|
- Changed 'compliments.js' - update DOM if remote compliments are loaded instead of waiting one updateInterval to show custom compliments
|
||||||
|
- Automated unit tests utils, deprecated, translator, cloneObject(lockstrings)
|
||||||
|
- Automated integration tests translations
|
||||||
|
- Add advanced filtering to the excludedEvents configuration of the default calendar module
|
||||||
|
- New currentweather module config option: `showFeelsLike`: Shows how it actually feels like. (wind chill or heat index)
|
||||||
|
- New currentweather module config option: `useKMPHwind`: adds an option to see wind speed in Kmph instead of just m/s or Beaufort.
|
||||||
|
- Add dc:date to parsing in newsfeed module, which allows parsing of more rss feeds.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Add link to GitHub repository which contains the respective Dockerfile.
|
||||||
|
- Optimized automated unit tests cloneObject, cmpVersions
|
||||||
|
- Update notifications use now translation templates instead of normal strings.
|
||||||
|
- Yarn can be used now as an installation tool
|
||||||
|
- Changed Electron dependency to v1.7.13.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- News article in fullscreen (iframe) is now shown in front of modules.
|
||||||
|
- Forecast respects maxNumberOfDays regardless of endpoint.
|
||||||
|
- Fix exception on translation of objects.
|
||||||
|
|
||||||
|
## [2.2.2] - 2018-01-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add missing `package-lock.json`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed Electron dependency to v1.7.10.
|
||||||
|
|
||||||
|
## [2.2.1] - 2018-01-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed linting errors.
|
||||||
|
|
||||||
|
## [2.2.0] - 2018-01-01
|
||||||
|
|
||||||
|
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Calender 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.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 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 graceful shutdown of modules by calling `stop` function of each `node_helper` on SIGINT before exiting.
|
||||||
|
- Link update subtext to Github diff of current version versus tracking branch.
|
||||||
|
- Add Catalan translation.
|
||||||
|
- Add ability to filter out newsfeed items based on prohibited words found in title (resolves #1071)
|
||||||
|
- Add options to truncate description support of a feed in newsfeed module
|
||||||
|
- Add reloadInterval option for particular feed in newsfeed module
|
||||||
|
- Add no-cache entries of HTTP headers in newsfeed module (fetcher)
|
||||||
|
- Add Czech translation.
|
||||||
|
- Add option for decimal symbols other than the decimal point for temperature values in both default weather modules: WeatherForecast and CurrentWeather.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed issue with calendar module showing more than `maximumEntries` allows
|
||||||
|
- WeatherForecast and CurrentWeather are now using HTTPS instead of HTTP
|
||||||
|
- Correcting translation for Indonesian language
|
||||||
|
- Fix issue where calendar icons wouldn't align correctly
|
||||||
|
|
||||||
|
## [2.1.3] - 2017-10-01
|
||||||
|
|
||||||
|
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Remove Roboto fonts files inside `fonts` and these are installed by npm install command.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 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 `.vscode/` folder to `.gitignore` to keep custom Visual Studio Code config out of git.
|
||||||
|
- Add unit test the capitalizeFirstLetter function of newsfeed module.
|
||||||
|
- Add new unit tests for function `shorten` in calendar module.
|
||||||
|
- Add new unit tests for function `getLocaleSpecification` in calendar module.
|
||||||
|
- Add unit test for js/class.js.
|
||||||
|
- Add unit tests for function `roundValue` in currentweather module.
|
||||||
|
- Add test e2e showWeek feature in spanish language.
|
||||||
|
- Add warning Log when is used old authentication method in the calendar module.
|
||||||
|
- Add test e2e for helloworld module with default config text.
|
||||||
|
- Add ability for `currentweather` module to display indoor humidity via INDOOR_HUMIDITY notification.
|
||||||
|
- Add Welsh (Cymraeg) translation.
|
||||||
|
- Add Slack badge to Readme.
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
- Changed 'default.js' - listen on all attached interfaces by default.
|
||||||
|
- Add execution of `npm list` after the test are ran in Travis CI.
|
||||||
|
- Change hooks for the vendors e2e tests.
|
||||||
|
- Add log when clientonly failed on starting.
|
||||||
|
- Add warning color when are using full ip whitelist.
|
||||||
|
- Set version of the `express-ipfilter` on 0.3.1.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed issue with incorrect alignment of analog clock when displayed in the center column of the MM.
|
||||||
|
- Fixed ipWhitelist behaviour to make empty whitelist ([]) allow any and all hosts access to the MM.
|
||||||
|
- Fixed issue with calendar module where 'excludedEvents' count towards 'maximumEntries'.
|
||||||
|
- Fixed issue with calendar module where global configuration of maximumEntries was not overridden by calendar specific config (see module doc).
|
||||||
|
- Fixed issue where `this.file(filename)` returns a path with two hashes.
|
||||||
|
- Workaround for the WeatherForecast API limitation.
|
||||||
|
|
||||||
|
## [2.1.2] - 2017-07-01
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Revert Docker related changes in favor of [docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror). All Docker images are outsourced. ([#856](https://github.com/MichMich/MagicMirror/pull/856))
|
||||||
- Change Docker base image (Debian + Node) to an arm based distro (AlpineARM + Node) ([#846](https://github.com/MichMich/MagicMirror/pull/846))
|
- 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.
|
||||||
|
|
||||||
@@ -12,15 +374,33 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- 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.
|
||||||
- Add test check URLs of vendors 200 and 404 HTTP CODE.
|
- Add test check URLs of vendors.
|
||||||
|
- Add test of match current week number on clock module with showWeek configuration.
|
||||||
- Add test default modules present modules/default/defaultmodules.js.
|
- Add test default modules present modules/default/defaultmodules.js.
|
||||||
|
- Add unit test calendar_modules function capFirst.
|
||||||
|
- Add test for check if exists the directories present in defaults modules.
|
||||||
|
- Add support for showing wind direction as an arrow instead of abbreviation in currentWeather module.
|
||||||
|
- Add support for writing translation functions to support flexible word order
|
||||||
|
- Add test for check if exits the directories present in defaults modules.
|
||||||
|
- Add calendar option to set a separate date format for full day events.
|
||||||
|
- Add ability for `currentweather` module to display indoor temperature via INDOOR_TEMPERATURE notification
|
||||||
|
- Add ability to change the path of the `custom.css`.
|
||||||
|
- Add translation Dutch to Alert module.
|
||||||
|
- 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.
|
||||||
|
|
||||||
### 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).
|
||||||
|
- Fix double message about port when server is starting
|
||||||
|
- Corrected Swedish translations for TODAY/TOMORROW/DAYAFTERTOMORROW.
|
||||||
|
- Removed unused import from js/electron.js
|
||||||
|
- Made calendar.js respect config.timeFormat irrespective of locale setting.
|
||||||
|
- Fixed alignment of analog clock when a large calendar is displayed in the same side bar.
|
||||||
|
|
||||||
## [2.1.1] - 2017-04-01
|
## [2.1.1] - 2017-04-01
|
||||||
|
|
||||||
@@ -28,18 +408,18 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
### 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.
|
||||||
- Switched out `rrule` package for `rrule-alt` and fixes in `ical.js` in order to fix calendar issues. ([#565](https://github.com/MichMich/MagicMirror/issues/565))
|
- Switched out `rrule` package for `rrule-alt` and fixes in `ical.js` in order to fix calendar issues. ([#565](https://github.com/MichMich/MagicMirror/issues/565))
|
||||||
- Make mouse events pass through the region fullscreen_above to modules below.
|
- Make mouse events pass through the region fullscreen_above to modules below.
|
||||||
- Scaled the splash screen down to make it a bit more subtle.
|
- Scaled the splash screen down to make it a bit more subtle.
|
||||||
- Replace HTML tables with markdown tables in README files.
|
- Replace HTML tables with markdown tables in README files.
|
||||||
- Added `DAYAFTERTOMORROW`, `UPDATE_NOTIFICATION` and `UPDATE_NOTIFICATION_MODULE` to Finnish translations.
|
- Added `DAYAFTERTOMORROW`, `UPDATE_NOTIFICATION` and `UPDATE_NOTIFICATION_MODULE` to Finnish translations.
|
||||||
- Run `npm test` on Travis automatically
|
- Run `npm test` on Travis automatically.
|
||||||
- Show the splash screen image even when is reboot or halted.
|
- Show the splash screen image even when is reboot or halted.
|
||||||
- Added some missing translaton strings in the sv.json file.
|
- Added some missing translation strings in the sv.json file.
|
||||||
- Run task jsonlint to check translation files.
|
- Run task jsonlint to check translation files.
|
||||||
- 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)).
|
||||||
@@ -54,12 +434,12 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Option to use RegExp in Calendar's titleReplace.
|
- Option to use RegExp in Calendar's titleReplace.
|
||||||
- Hungarian Translation.
|
- Hungarian Translation.
|
||||||
- Icelandic Translation.
|
- Icelandic Translation.
|
||||||
- Add use a script to prevent when is run by SSH session set DISPLAY enviroment.
|
- Add use a script to prevent when is run by SSH session set DISPLAY environment.
|
||||||
- Enable ability to set configuration file by the enviroment variable called MM_CONFIG_FILE.
|
- Enable ability to set configuration file by the environment variable called MM_CONFIG_FILE.
|
||||||
- Option to give each calendar a different color.
|
- Option to give each calendar a different color.
|
||||||
- Option for colored min-temp and max-temp.
|
- Option for colored min-temp and max-temp.
|
||||||
- Add test e2e helloworld.
|
- Add test e2e helloworld.
|
||||||
- Add test e2e enviroment.
|
- Add test e2e environment.
|
||||||
- Add `chai-as-promised` npm module to devDependencies.
|
- Add `chai-as-promised` npm module to devDependencies.
|
||||||
- Basic set of tests for clock module.
|
- Basic set of tests for clock module.
|
||||||
- Run e2e test in Travis.
|
- Run e2e test in Travis.
|
||||||
@@ -77,10 +457,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Added tests for Translations, dev argument, version, dev console.
|
- Added tests for Translations, dev argument, version, dev console.
|
||||||
- Added test anytime feature compliments module.
|
- Added test anytime feature compliments module.
|
||||||
- Added test ipwhitelist configuration directive.
|
- Added test ipwhitelist configuration directive.
|
||||||
- Added test for calendar module: default, basic-auth, backward compability, fail-basic-auth.
|
- Added test for calendar module: default, basic-auth, backward compatibility, fail-basic-auth.
|
||||||
- Added meta tags to support fullscreen mode on iOS (for server mode)
|
- Added meta tags to support fullscreen mode on iOS (for server mode)
|
||||||
- Added `ignoreOldItems` and `ignoreOlderThan` options to the News Feed module
|
- Added `ignoreOldItems` and `ignoreOlderThan` options to the News Feed module
|
||||||
- Added test for MM_PORT enviroment variable.
|
- Added test for MM_PORT environment variable.
|
||||||
- Added a configurable Week section to the clock module.
|
- Added a configurable Week section to the clock module.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -92,7 +472,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Module currentWeather: check if temperature received from api is defined.
|
- Module currentWeather: check if temperature received from api is defined.
|
||||||
- Fix an issue with module hidden status changing to `true` although lock string prevented showing it.
|
- Fix an issue with module hidden status changing to `true` although lock string prevented showing it.
|
||||||
- Fix newsfeed module bug (removeStartTags)
|
- Fix newsfeed module bug (removeStartTags)
|
||||||
- Fix when is set MM_PORT enviroment variable.
|
- Fix when is set MM_PORT environment variable.
|
||||||
- Fixed missing animation on `this.show(speed)` when module is alone in a region.
|
- Fixed missing animation on `this.show(speed)` when module is alone in a region.
|
||||||
|
|
||||||
## [2.1.0] - 2016-12-31
|
## [2.1.0] - 2016-12-31
|
||||||
@@ -114,8 +494,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Calendar module now broadcasts the event list to all other modules using the notification system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/calendar) for more information.
|
- Calendar module now broadcasts the event list to all other modules using the notification system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/calendar) for more information.
|
||||||
- Possibility to use the the calendar feed as the source for the weather (currentweather & weatherforecast) location data. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/weatherforecast) for more information.
|
- Possibility to use the the calendar feed as the source for the weather (currentweather & weatherforecast) location data. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/weatherforecast) for more information.
|
||||||
- Added option to show rain amount in the weatherforecast default module
|
- Added option to show rain amount in the weatherforecast default module
|
||||||
- Add module `updatenotification` to get an update whenever a new version is availabe. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
|
- Add module `updatenotification` to get an update whenever a new version is available. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
|
||||||
- Add the abilty to set timezone on the date display in the Clock Module
|
- Add the ability to set timezone on the date display in the Clock Module
|
||||||
- Ability to set date format in calendar module
|
- Ability to set date format in calendar module
|
||||||
- Possibility to use currentweather for the compliments
|
- Possibility to use currentweather for the compliments
|
||||||
- Added option `disabled` for modules.
|
- Added option `disabled` for modules.
|
||||||
@@ -154,7 +534,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- 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)
|
||||||
- Add support for doing http basic auth when loading calendars
|
- Add support for doing http basic auth when loading calendars
|
||||||
- Add the abilty 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.
|
||||||
@@ -177,8 +557,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Prevent `getModules()` selectors from returning duplicate entries.
|
- Prevent `getModules()` selectors from returning duplicate entries.
|
||||||
- Append endpoints of weather modules with `/` to retreive 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 grammer in `module.js` from 'suspend' to 'suspended'.
|
- Corrected grammar in `module.js` from 'suspend' to 'suspended'.
|
||||||
- Fixed openweathermap.org URL in config sample.
|
- Fixed openweathermap.org URL in config sample.
|
||||||
- Prevent currentweather module from crashing when received data object is incorrect.
|
- Prevent currentweather module from crashing when received data object is incorrect.
|
||||||
- Fix issue where translation loading prevented the UI start-up when the language was set to 'en'. (Issue [#388](https://github.com/MichMich/MagicMirror/issues/388))
|
- Fix issue where translation loading prevented the UI start-up when the language was set to 'en'. (Issue [#388](https://github.com/MichMich/MagicMirror/issues/388))
|
||||||
|
27
Dockerfile
27
Dockerfile
@@ -1,27 +0,0 @@
|
|||||||
FROM izone/arm:node
|
|
||||||
|
|
||||||
# Set env variables
|
|
||||||
ENV NODE_ENV production
|
|
||||||
ENV MM_PORT 8080
|
|
||||||
|
|
||||||
WORKDIR /opt/magic_mirror
|
|
||||||
|
|
||||||
# Cache node_modules
|
|
||||||
COPY package.json /opt/magic_mirror
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Copy all needed files
|
|
||||||
COPY . /opt/magic_mirror
|
|
||||||
|
|
||||||
# Save/Cache config and modules folder for docker-entrypoint
|
|
||||||
COPY /modules /opt/magic_mirror/unmount_modules
|
|
||||||
COPY /config /opt/magic_mirror/unmount_config
|
|
||||||
|
|
||||||
# Convert docker-entrypoint.sh to unix format and grant execution privileges
|
|
||||||
RUN apk update \
|
|
||||||
&& apk add dos2unix --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ --allow-untrusted \
|
|
||||||
&& dos2unix docker-entrypoint.sh \
|
|
||||||
&& chmod +x docker-entrypoint.sh
|
|
||||||
|
|
||||||
EXPOSE $MM_PORT
|
|
||||||
ENTRYPOINT ["/opt/magic_mirror/docker-entrypoint.sh"]
|
|
10
Gruntfile.js
10
Gruntfile.js
@@ -4,6 +4,7 @@ module.exports = function(grunt) {
|
|||||||
pkg: grunt.file.readJSON("package.json"),
|
pkg: grunt.file.readJSON("package.json"),
|
||||||
eslint: {
|
eslint: {
|
||||||
options: {
|
options: {
|
||||||
|
fix: "true",
|
||||||
configFile: ".eslintrc.json"
|
configFile: ".eslintrc.json"
|
||||||
},
|
},
|
||||||
target: [
|
target: [
|
||||||
@@ -11,6 +12,7 @@ module.exports = function(grunt) {
|
|||||||
"modules/default/*.js",
|
"modules/default/*.js",
|
||||||
"modules/default/*/*.js",
|
"modules/default/*/*.js",
|
||||||
"serveronly/*.js",
|
"serveronly/*.js",
|
||||||
|
"clientonly/*.js",
|
||||||
"*.js",
|
"*.js",
|
||||||
"tests/**/*.js",
|
"tests/**/*.js",
|
||||||
"!modules/default/alert/notificationFx.js",
|
"!modules/default/alert/notificationFx.js",
|
||||||
@@ -25,7 +27,7 @@ module.exports = function(grunt) {
|
|||||||
stylelint: {
|
stylelint: {
|
||||||
simple: {
|
simple: {
|
||||||
options: {
|
options: {
|
||||||
configFile: ".stylelintrc"
|
configFile: ".stylelintrc.json"
|
||||||
},
|
},
|
||||||
src: [
|
src: [
|
||||||
"css/main.css",
|
"css/main.css",
|
||||||
@@ -41,11 +43,11 @@ module.exports = function(grunt) {
|
|||||||
src: [
|
src: [
|
||||||
"package.json",
|
"package.json",
|
||||||
".eslintrc.json",
|
".eslintrc.json",
|
||||||
".stylelintrc",
|
".stylelintrc.json",
|
||||||
|
"installers/pm2_MagicMirror.json",
|
||||||
"translations/*.json",
|
"translations/*.json",
|
||||||
"modules/default/*/translations/*.json",
|
"modules/default/*/translations/*.json",
|
||||||
"installers/pm2_MagicMirror.json",
|
"vendor/package.json"
|
||||||
"vendor/package.js"
|
|
||||||
],
|
],
|
||||||
options: {
|
options: {
|
||||||
reporter: "jshint"
|
reporter: "jshint"
|
||||||
|
202
README.md
202
README.md
@@ -5,7 +5,7 @@
|
|||||||
<a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
|
<a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
|
||||||
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge"></a>
|
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge"></a>
|
||||||
<a href="http://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
<a href="http://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
||||||
<a href="https://travis-ci.org/MichMich/MagicMirror"><img src="https://travis-ci.org/MichMich/MagicMirror.svg" alt="Travis"></a>
|
<a href="https://travis-ci.com/MichMich/MagicMirror"><img src="https://travis-ci.com/MichMich/MagicMirror.svg" alt="Travis"></a>
|
||||||
<a href="https://snyk.io/test/github/MichMich/MagicMirror"><img src="https://snyk.io/test/github/MichMich/MagicMirror/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/MichMich/MagicMirror" style="max-width:100%;"></a>
|
<a href="https://snyk.io/test/github/MichMich/MagicMirror"><img src="https://snyk.io/test/github/MichMich/MagicMirror/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/MichMich/MagicMirror" style="max-width:100%;"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -15,121 +15,143 @@ MagicMirror² focuses on a modular plugin system and uses [Electron](http://elec
|
|||||||
|
|
||||||
## Table Of Contents
|
## Table Of Contents
|
||||||
|
|
||||||
- [Usage](#usage)
|
- [Table Of Contents](#table-of-contents)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Raspberry Pi](#raspberry-pi)
|
||||||
|
- [Automatic Installation (Raspberry Pi only!)](#automatic-installation-raspberry-pi-only)
|
||||||
|
- [Manual Installation](#manual-installation)
|
||||||
|
- [Server Only](#server-only)
|
||||||
|
- [Client Only](#client-only)
|
||||||
|
- [Docker](#docker)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
|
- [Raspberry Specific](#raspberry-specific)
|
||||||
|
- [General](#general)
|
||||||
- [Modules](#modules)
|
- [Modules](#modules)
|
||||||
- [Known Issues](#known-issues)
|
- [Updating](#updating)
|
||||||
- [Community](#community)
|
- [Community](#community)
|
||||||
- [Contributing Guidelines](#contributing-guidelines)
|
- [Contributing Guidelines](#contributing-guidelines)
|
||||||
|
- [Enjoying MagicMirror? Consider a donation!](#enjoying-magicmirror-consider-a-donation)
|
||||||
|
- [Manifesto](#manifesto)
|
||||||
|
|
||||||
## Usage
|
## Installation
|
||||||
|
|
||||||
### Raspberry Pi Support
|
### Raspberry Pi
|
||||||
Electron, the app wrapper around MagicMirror², only supports the Raspberry Pi 2 & 3. The Raspberry Pi 1 is currently **not** supported. If you want to run this on a Raspberry Pi 1, use the [server only](#server-only) feature and setup a fullscreen browser yourself.
|
|
||||||
|
|
||||||
### Automatic Installer (Raspberry Pi Only!)
|
#### Automatic Installation (Raspberry Pi only!)
|
||||||
|
|
||||||
|
*Electron*, the app wrapper around MagicMirror², only supports the Raspberry Pi 2/3. The Raspberry Pi 0/1 is currently **not** supported. If you want to run this on a Raspberry Pi 1, use the [server only](#server-only) feature and setup a fullscreen browser yourself. (Yes, people have managed to run MM² also on a Pi0, so if you insist, search in the forums.)
|
||||||
|
|
||||||
|
Note that you will need to install the latest full version of Raspbian, **don't use the Lite version**.
|
||||||
|
|
||||||
Execute the following command on your Raspberry Pi to install MagicMirror²:
|
Execute the following command on your Raspberry Pi to install MagicMirror²:
|
||||||
````
|
|
||||||
|
```bash
|
||||||
bash -c "$(curl -sL https://raw.githubusercontent.com/MichMich/MagicMirror/master/installers/raspberry.sh)"
|
bash -c "$(curl -sL https://raw.githubusercontent.com/MichMich/MagicMirror/master/installers/raspberry.sh)"
|
||||||
````
|
```
|
||||||
|
|
||||||
### Manual Installation
|
#### Manual Installation
|
||||||
|
|
||||||
1. Download and install the latest Node.js version.
|
1. Download and install the latest *Node.js* version:
|
||||||
|
- `curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -`
|
||||||
|
- `sudo apt install -y nodejs`
|
||||||
2. Clone the repository and check out the master branch: `git clone https://github.com/MichMich/MagicMirror`
|
2. Clone the repository and check out the master branch: `git clone https://github.com/MichMich/MagicMirror`
|
||||||
3. Enter the repository: `cd ~/MagicMirror`
|
3. Enter the repository: `cd MagicMirror/`
|
||||||
4. Install and run the app: `npm install && npm start`
|
4. Install and run the app with: `npm install && npm start` \
|
||||||
|
For **Server Only** use: `npm install && node serveronly` .
|
||||||
|
|
||||||
**Important:** `npm start` does **not** work via SSH, use `DISPLAY=:0 nohup npm start &` instead. This starts the mirror on the remote display.
|
|
||||||
|
|
||||||
**Note:** if you want to debug on Raspberry Pi you can use `npm start dev` which will start the MagicMirror app with Dev Tools enabled.
|
**:warning: Important!**
|
||||||
|
|
||||||
|
- **The installation step for `npm install` will take a very long time**, often with little or no terminal response! \
|
||||||
|
For the RPi3 this is **~10** minutes and for the Rpi2 **~25** minutes. \
|
||||||
|
Do not interrupt or you risk getting a :broken_heart: by Raspberry Jam.
|
||||||
|
|
||||||
|
|
||||||
|
Also note that:
|
||||||
|
|
||||||
|
- `npm start` does **not** work via SSH. But you can use `DISPLAY=:0 nohup npm start &` instead. \
|
||||||
|
This starts the mirror on the remote display.
|
||||||
|
- If you want to debug on Raspberry Pi you can use `npm start dev` which will start MM with *Dev Tools* enabled.
|
||||||
|
- To access toolbar menu when in mirror mode, hit `ALT` key.
|
||||||
|
- To toggle the (web) `Developer Tools` from mirror mode, use `CTRL-SHIFT-I` or `ALT` and select `View`.
|
||||||
|
|
||||||
|
|
||||||
### Server Only
|
### Server Only
|
||||||
|
|
||||||
In some cases, you want to start the application without an actual app window. In this case, you can start MagicMirror² in server only mode by manually running `node serveronly` or using Docker. This will start the server, after which you can open the application in your browser of choice. Detailed description below.
|
In some cases, you want to start the application without an actual app window. In this case, you can start MagicMirror² in server only mode by manually running `node serveronly` or using Docker. This will start the server, after which you can open the application in your browser of choice. Detailed description below.
|
||||||
|
|
||||||
#### Docker
|
**Important:** Make sure that you whitelist the interface/ip (`ipWhitelist`) in the server config where you want the client to connect to, otherwise it will not be allowed to connect to the server. You also need to set the local host `address` field to `0.0.0.0` in order for the RPi to listen on all interfaces and not only `localhost` (default).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var config = {
|
||||||
|
address: "0.0.0.0", // default is "localhost"
|
||||||
|
port: 8080, // default
|
||||||
|
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:172.17.0.1"], // default -- need to add your IP here
|
||||||
|
...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Client Only
|
||||||
|
|
||||||
|
This is when you already have a server running remotely and want your RPi to connect as a standalone client to this instance, to show the MM from the server. Then from your RPi, you run it with: `node clientonly --address 192.168.1.5 --port 8080`. (Specify the ip address and port number of the server)
|
||||||
|
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
MagicMirror² in server only mode can be deployed using [Docker](https://docker.com). After a successful [Docker installation](https://docs.docker.com/engine/installation/) you just need to execute the following command in the shell:
|
MagicMirror² in server only mode can be deployed using [Docker](https://docker.com). After a successful [Docker installation](https://docs.docker.com/engine/installation/) you just need to execute the following command in the shell:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--publish 80:8080 \
|
--publish 80:8080 \
|
||||||
--restart always \
|
--restart always \
|
||||||
--volume ~/magic_mirror/config:/opt/magic_mirror/config \
|
--volume ~/magic_mirror/config:/opt/magic_mirror/config \
|
||||||
--volume ~/magic_mirror/modules:/opt/magic_mirror/modules \
|
--volume ~/magic_mirror/modules:/opt/magic_mirror/modules \
|
||||||
--name magic_mirror \
|
--name magic_mirror \
|
||||||
michmich/magicmirror
|
bastilimbach/docker-magicmirror
|
||||||
```
|
```
|
||||||
|
To get more information about the available Dockerfile versions and configurations head over to the respective [GitHub repository](https://github.com/bastilimbach/docker-MagicMirror).
|
||||||
|
|
||||||
| **Volumes** | **Description** |
|
|
||||||
| --- | --- |
|
|
||||||
| `/opt/magic_mirror/config` | Mount this volume to insert your own config into the docker container. |
|
|
||||||
| `/opt/magic_mirror/modules` | Mount this volume to add your own custom modules into the docker container. |
|
|
||||||
|
|
||||||
You may need to add your Docker Host IP to your `ipWhitelist` option. If you have some issues setting up this configuration, check [this forum post](https://forum.magicmirror.builders/topic/1326/ipwhitelist-howto).
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var config = {
|
|
||||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:172.17.0.1"]
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Manual
|
|
||||||
|
|
||||||
1. Download and install the latest Node.js version.
|
|
||||||
2. Clone the repository and check out the master branch: `git clone https://github.com/MichMich/MagicMirror`
|
|
||||||
3. Enter the repository: `cd ~/MagicMirror`
|
|
||||||
4. Install and run the app: `npm install && node serveronly`
|
|
||||||
|
|
||||||
### Raspberry Configuration & Auto Start.
|
|
||||||
|
|
||||||
The following wiki links are helpful in the configuration of your MagicMirror² operating system:
|
|
||||||
- [Configuring the Raspberry Pi](https://github.com/MichMich/MagicMirror/wiki/Configuring-the-Raspberry-Pi)
|
|
||||||
- [Auto Starting MagicMirror](https://github.com/MichMich/MagicMirror/wiki/Auto-Starting-MagicMirror)
|
|
||||||
|
|
||||||
### Updating your MagicMirror²
|
|
||||||
|
|
||||||
If you want to update your MagicMirror² to the latest version, use your terminal to go to your Magic Mirror folder and type the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git pull && npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
If you changed nothing more than the config or the modules, this should work without any problems.
|
|
||||||
Type `git status` to see your changes, if there are any, you can reset them with `git reset --hard`. After that, git pull should be possible.
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
1. Duplicate `config/config.js.sample` to `config/config.js`. **Note:** If you used the installer script. This step is already done for you.
|
### Raspberry Specific
|
||||||
2. Modify your required settings.
|
|
||||||
|
The following wiki links are helpful for the initial configuration of your MagicMirror² operating system:
|
||||||
|
- [Configuring the Raspberry Pi](https://github.com/MichMich/MagicMirror/wiki/Configuring-the-Raspberry-Pi)
|
||||||
|
- [Auto Starting MagicMirror](https://github.com/MichMich/MagicMirror/wiki/Auto-Starting-MagicMirror)
|
||||||
|
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
1. Copy `/home/pi/MagicMirror/config/config.js.sample` to `/home/pi/MagicMirror/config/config.js`. \
|
||||||
|
**Note:** If you used the installer script. This step is already done for you.
|
||||||
|
|
||||||
|
2. Modify your required settings. \
|
||||||
|
Note: You can check your configuration running `npm run config:check` in `/home/pi/MagicMirror`.
|
||||||
|
|
||||||
Note: You'll can check your configuration running the follow command:
|
|
||||||
```bash
|
|
||||||
npm run config:check
|
|
||||||
```
|
|
||||||
|
|
||||||
The following properties can be configured:
|
The following properties can be configured:
|
||||||
|
|
||||||
| **Option** | **Description** |
|
| **Option** | **Description** |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `port` | The port on which the MagicMirror² server will run on. The default value is `8080`. |
|
| `port` | The port on which the MagicMirror² server will run on. The default value is `8080`. |
|
||||||
| `address` | The ip address the accept connections. The default open bind `::` is IPv6 is available or `0.0.0.0` IPv4 run on. Example config: `192.168.10.100`. |
|
| `address` | The *interface* ip address on which to accept connections. The default is `localhost`, which would prevent exposing the built-in webserver to machines on the local network. To expose it to other machines, use: `0.0.0.0`. |
|
||||||
| `ipWhitelist` | The list of IPs from which you are allowed to access the MagicMirror². The default value is `["127.0.0.1", "::ffff:127.0.0.1", "::1"]`. It is possible to specify IPs with subnet masks (`["127.0.0.1", "127.0.0.1/24"]`) or define ip ranges (`["127.0.0.1", ["192.168.0.1", "192.168.0.100"]]`). Set `[]` to allow all IP addresses. For more information about how configure this directive see the [follow post ipWhitelist HowTo](https://forum.magicmirror.builders/topic/1326/ipwhitelist-howto) |
|
| `ipWhitelist` | The list of IPs from which you are allowed to access the MagicMirror². The default value is `["127.0.0.1", "::ffff:127.0.0.1", "::1"]`, which is from `localhost` only. Add your IP when needed. You can also specify IP ranges with subnet masks (`["127.0.0.1", "127.0.0.1/24"]`) or directly with (`["127.0.0.1", ["192.168.0.1", "192.168.0.100"]]`). Set `[]` to allow all IP addresses. For more information see: [follow post ipWhitelist HowTo](https://forum.magicmirror.builders/topic/1326/ipwhitelist-howto) |
|
||||||
| `zoom` | This allows to scale the mirror contents with a given zoom factor. The default value is `1.0`|
|
| `zoom` | This allows to scale the mirror contents with a given zoom factor. The default value is `1.0`|
|
||||||
| `language` | The language of the interface. (Note: Not all elements will be localized.) Possible values are `en`, `nl`, `ru`, `fr`, etc., but the default value is `en`. |
|
| `language` | The language of the interface. (Note: Not all elements will be localized.) Possible values are `en`, `nl`, `ru`, `fr`, etc., but the default value is `en`. |
|
||||||
| `timeFormat` | The form of time notation that will be used. Possible values are `12` or `24`. The default is `24`. |
|
| `timeFormat` | The form of time notation that will be used. Possible values are `12` or `24`. The default is `24`. |
|
||||||
| `units` | The units that will be used in the default weather modules. Possible values are `metric` or `imperial`. The default is `metric`. |
|
| `units` | The units that will be used in the default weather modules. Possible values are `metric` or `imperial`. The default is `metric`. |
|
||||||
| `modules` | An array of active modules. **The array must contain objects. See the next table below for more information.** |
|
| `modules` | An array of active modules. **The array must contain objects. See the next table below for more information.** |
|
||||||
| `electronOptions` | An optional array of Electron (browser) options. This allows configuration of e.g. the browser screen size and position (example: `electronOptions: { fullscreen: false, width: 800, height: 600 }`). Kiosk mode can be enabled by setting `kiosk = true`, `autoHideMenuBar = false` and `fullscreen = false`. More options can be found [here](https://github.com/electron/electron/blob/master/docs/api/browser-window.md). |
|
| `electronOptions` | An optional array of Electron (browser) options. This allows configuration of e.g. the browser screen size and position (example: `electronOptions: { fullscreen: false, width: 800, height: 600 }`). Kiosk mode can be enabled by setting `kiosk: true`, `autoHideMenuBar: false` and `fullscreen: false`. More options can be found [here](https://github.com/electron/electron/blob/master/docs/api/browser-window.md). |
|
||||||
|
| `customCss` | The path of the `custom.css` stylesheet. The default is `css/custom.css`. |
|
||||||
|
|
||||||
Module configuration:
|
Module configuration:
|
||||||
|
|
||||||
| **Option** | **Description** |
|
| **Option** | **Description** |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `module` | The name of the module. This can also contain the subfolder. Valid examples include `clock`, `default/calendar` and `custommodules/mymodule`. |
|
| `module` | The name of the module. This can also contain the subfolder. Valid examples include `clock`, `default/calendar` and `custommodules/mymodule`. |
|
||||||
| `position` | The location of the module in which the module will be loaded. Possible values are `top_ bar`, `top_left`, `top_center`, `top_right`, `upper_third`, `middle_center`, `lower_third`, `bottom_left`, `bottom_center`, `bottom_right`, `bottom_bar`, `fullscreen_above`, and `fullscreen_below`. This field is optional but most modules require this field to set. Check the documentation of the module for more information. Multiple modules with the same position will be ordered based on the order in the configuration file. |
|
| `position` | The location of the module in which the module will be loaded. Possible values are `top_bar`, `top_left`, `top_center`, `top_right`, `upper_third`, `middle_center`, `lower_third`, `bottom_left`, `bottom_center`, `bottom_right`, `bottom_bar`, `fullscreen_above`, and `fullscreen_below`. This field is optional but most modules require this field to set. Check the documentation of the module for more information. Multiple modules with the same position will be ordered based on the order in the configuration file. |
|
||||||
| `classes` | Additional classes which are passed to the module. The field is optional. |
|
| `classes` | Additional classes which are passed to the module. The field is optional. |
|
||||||
| `header` | To display a header text above the module, add the header property. This field is optional. |
|
| `header` | To display a header text above the module, add the header property. This field is optional. |
|
||||||
| `disabled` | Set disabled to `true` to skip creating the module. This field is optional. |
|
| `disabled` | Set disabled to `true` to skip creating the module. This field is optional. |
|
||||||
@@ -148,12 +170,20 @@ The following modules are installed by default.
|
|||||||
- [**Hello World**](modules/default/helloworld)
|
- [**Hello World**](modules/default/helloworld)
|
||||||
- [**Alert**](modules/default/alert)
|
- [**Alert**](modules/default/alert)
|
||||||
|
|
||||||
For more available modules, check out out the wiki page: [MagicMirror² Modules](https://github.com/MichMich/MagicMirror/wiki/MagicMirror²-Modules). If you want to build your own modules, check out the [MagicMirror² Module Development Documentation](modules) and don't forget to add it to the wiki and the [forum](https://forum.magicmirror.builders/category/7/showcase)!
|
For more available modules, check out out the wiki page [MagicMirror² 3rd Party Modules](https://github.com/MichMich/MagicMirror/wiki/3rd-party-modules). If you want to build your own modules, check out the [MagicMirror² Module Development Documentation](modules) and don't forget to add it to the wiki and the [forum](https://forum.magicmirror.builders/category/7/showcase)!
|
||||||
|
|
||||||
## Known issues
|
|
||||||
|
|
||||||
- Electron seems to have some issues on certain Raspberry Pi 2's. See [#145](https://github.com/MichMich/MagicMirror/issues/145).
|
## Updating
|
||||||
- MagicMirror² (Electron) sometimes quits without an error after an extended period of use. See [#150](https://github.com/MichMich/MagicMirror/issues/150).
|
|
||||||
|
If you want to update your MagicMirror² to the latest version, use your terminal to go to your Magic Mirror folder and type the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull && npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
If you changed nothing more than the config or the modules, this should work without any problems.
|
||||||
|
Type `git status` to see your changes, if there are any, you can reset them with `git reset --hard`. After that, git pull should be possible.
|
||||||
|
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
@@ -172,6 +202,32 @@ Please keep the following in mind:
|
|||||||
|
|
||||||
Thanks for your help in making MagicMirror² better!
|
Thanks for your help in making MagicMirror² better!
|
||||||
|
|
||||||
|
|
||||||
|
## Enjoying MagicMirror? Consider a donation!
|
||||||
|
|
||||||
|
MagicMirror² is opensource and free. That doesn't mean we don't need any money.
|
||||||
|
|
||||||
|
Please consider a donation to help us cover the ongoing costs like webservers and email services.
|
||||||
|
If we receive enough donations we might even be able to free up some working hours and spend some extra time improving the MagicMirror² core.
|
||||||
|
|
||||||
|
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
|
||||||
|
|
||||||
|
## Manifesto
|
||||||
|
|
||||||
|
A real Manifesto is still to be written. Till then, Michael's response on [one of the repository issues](https://github.com/MichMich/MagicMirror/issues/1174) gives a great summary:
|
||||||
|
|
||||||
|
> "... I started this project as an ultimate starter project for Raspberry Pi enthusiasts. As a matter of fact, for most of the contributors, the MagicMirror project is the first open source project they ever contributed to. This is one of the reasons why the MagicMirror project is featured in several RasPi magazines.
|
||||||
|
>
|
||||||
|
>The project has a lot of opportunities for improvement. We could use a powerful framework like Vue to ramp up the development speed. We could use SASS for better/easier css implementations. We could make it an NPM installable package. And as you say, we could bundle it up. The big downside of of of these changes is that it over complicates things: a user no longer will be able to open just one file and make a small modification and see how it works out.
|
||||||
|
>
|
||||||
|
>Of course, a bundled version can be complimentary to the regular un-bundled version. And I'm sure a lot of (new) users will opt for the bundled version. But this means those users won't be motivated to take a peek under the hood. They will just remain 'users'. They won't become contributors, and worse: they won't be motivated to take their first steps in software development.
|
||||||
|
>
|
||||||
|
>And to be honest: motivating curious users to step out of their comfort zone and take those first steps is what drives me in this project. Therefor my ultimate goal is this project is to keep it as accessible as possible."
|
||||||
|
>
|
||||||
|
> ~ Michael Teeuw
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<br>
|
<br>
|
||||||
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
||||||
|
104
clientonly/index.js
Normal file
104
clientonly/index.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/* jshint esversion: 6 */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Use separate scope to prevent global scope pollution
|
||||||
|
(function () {
|
||||||
|
var config = {};
|
||||||
|
|
||||||
|
// Helper function to get server address/hostname from either the commandline or env
|
||||||
|
function getServerAddress() {
|
||||||
|
// Helper function to get command line parameters
|
||||||
|
// Assumes that a cmdline parameter is defined with `--key [value]`
|
||||||
|
function getCommandLineParameter(key, defaultValue = undefined) {
|
||||||
|
var index = process.argv.indexOf(`--${key}`);
|
||||||
|
var value = index > -1 ? process.argv[index + 1] : undefined;
|
||||||
|
return value !== undefined ? String(value) : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer command line arguments over environment variables
|
||||||
|
["address", "port"].forEach((key) => {
|
||||||
|
config[key] = getCommandLineParameter(key, process.env[key.toUpperCase()]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServerConfig(url) {
|
||||||
|
// Return new pending promise
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Select http or https module, depending on reqested url
|
||||||
|
const lib = url.startsWith("https") ? require("https") : require("http");
|
||||||
|
const request = lib.get(url, (response) => {
|
||||||
|
var configData = "";
|
||||||
|
|
||||||
|
// Gather incoming data
|
||||||
|
response.on("data", function(chunk) {
|
||||||
|
configData += chunk;
|
||||||
|
});
|
||||||
|
// Resolve promise at the end of the HTTP/HTTPS stream
|
||||||
|
response.on("end", function() {
|
||||||
|
resolve(JSON.parse(configData));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("error", function(error) {
|
||||||
|
reject(new Error(`Unable to read config from server (${url} (${error.message}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(message, code = 1) {
|
||||||
|
if (message !== undefined && typeof message === "string") {
|
||||||
|
console.log(message);
|
||||||
|
} else {
|
||||||
|
console.log("Usage: 'node clientonly --address 192.168.1.10 --port 8080'");
|
||||||
|
}
|
||||||
|
process.exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
getServerAddress();
|
||||||
|
|
||||||
|
(config.address && config.port) || fail();
|
||||||
|
|
||||||
|
// Only start the client if a non-local server was provided
|
||||||
|
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) {
|
||||||
|
getServerConfig(`http://${config.address}:${config.port}/config/`)
|
||||||
|
.then(function (configReturn) {
|
||||||
|
// Pass along the server config via an environment variable
|
||||||
|
var env = Object.create(process.env);
|
||||||
|
var options = { env: env };
|
||||||
|
configReturn.address = config.address;
|
||||||
|
configReturn.port = config.port;
|
||||||
|
env.config = JSON.stringify(configReturn);
|
||||||
|
|
||||||
|
// Spawn electron application
|
||||||
|
const electron = require("electron");
|
||||||
|
const child = require("child_process").spawn(electron, ["js/electron.js"], options);
|
||||||
|
|
||||||
|
// Pipe all child process output to current stdout
|
||||||
|
child.stdout.on("data", function (buf) {
|
||||||
|
process.stdout.write(`Client: ${buf}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pipe all child process errors to current stderr
|
||||||
|
child.stderr.on("data", function (buf) {
|
||||||
|
process.stderr.write(`Client: ${buf}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", function (err) {
|
||||||
|
process.stdout.write(`Client: ${err}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
console.log(`There something wrong. The clientonly is not running code ${code}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(function (reason) {
|
||||||
|
fail(`Unable to connect to server: (${reason})`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
}());
|
@@ -9,6 +9,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var config = {
|
var config = {
|
||||||
|
address: "localhost", // Address to listen on, can be:
|
||||||
|
// - "localhost", "127.0.0.1", "::1" to listen on loopback interface
|
||||||
|
// - another specific IPv4/6 to listen on a specific interface
|
||||||
|
// - "", "0.0.0.0", "::" to listen on any interface
|
||||||
|
// Default, when address config is left out, is "localhost"
|
||||||
port: 8080,
|
port: 8080,
|
||||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses
|
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses
|
||||||
// or add a specific IPv4 of 192.168.1.5 :
|
// or add a specific IPv4 of 192.168.1.5 :
|
||||||
@@ -39,9 +44,8 @@ var config = {
|
|||||||
config: {
|
config: {
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
symbol: "calendar-check-o ",
|
symbol: "calendar-check",
|
||||||
url: "webcal://www.calendarlabs.com/templates/ical/US-Holidays.ics"
|
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics" }
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -54,7 +58,7 @@ var config = {
|
|||||||
position: "top_right",
|
position: "top_right",
|
||||||
config: {
|
config: {
|
||||||
location: "New York",
|
location: "New York",
|
||||||
locationID: "", //ID from http://www.openweathermap.org/help/city_list.txt
|
locationID: "", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||||
appid: "YOUR_OPENWEATHER_API_KEY"
|
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -64,7 +68,7 @@ var config = {
|
|||||||
header: "Weather Forecast",
|
header: "Weather Forecast",
|
||||||
config: {
|
config: {
|
||||||
location: "New York",
|
location: "New York",
|
||||||
locationID: "5128581", //ID from http://www.openweathermap.org/help/city_list.txt
|
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||||
appid: "YOUR_OPENWEATHER_API_KEY"
|
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -79,7 +83,9 @@ var config = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
showSourceTitle: true,
|
showSourceTitle: true,
|
||||||
showPublishDate: true
|
showPublishDate: true,
|
||||||
|
broadcastNewsFeeds: true,
|
||||||
|
broadcastNewsUpdates: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
19
css/main.css
19
css/main.css
@@ -95,7 +95,7 @@ body {
|
|||||||
header {
|
header {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-family: "Roboto Condensed";
|
font-family: "Roboto Condensed", Arial, Helvetica, sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
border-bottom: 1px solid #666;
|
border-bottom: 1px solid #666;
|
||||||
line-height: 15px;
|
line-height: 15px;
|
||||||
@@ -128,6 +128,10 @@ sup {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pre-line {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Region Definitions.
|
* Region Definitions.
|
||||||
*/
|
*/
|
||||||
@@ -151,6 +155,7 @@ sup {
|
|||||||
|
|
||||||
.region.right {
|
.region.right {
|
||||||
right: 0;
|
right: 0;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.region.top {
|
.region.top {
|
||||||
@@ -161,6 +166,10 @@ sup {
|
|||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.region.bottom .container {
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
.region.top .container:empty {
|
.region.top .container:empty {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
@@ -185,10 +194,6 @@ sup {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.region.bottom .container {
|
|
||||||
margin-top: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.region.bottom .container:empty {
|
.region.bottom .container:empty {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
@@ -231,10 +236,6 @@ sup {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.region.right {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.region table {
|
.region table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
|
17
dangerfile.js
Normal file
17
dangerfile.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { danger, fail, warn } from "danger";
|
||||||
|
|
||||||
|
// Check if the CHANGELOG.md file has been edited
|
||||||
|
// Fail the build and post a comment reminding submitters to do so if it wasn't changed
|
||||||
|
if (!danger.git.modified_files.includes("CHANGELOG.md")) {
|
||||||
|
warn("Please include an updated `CHANGELOG.md` file.<br>This way we can keep track of all the contributions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the PR request is send to the master branch.
|
||||||
|
// This should only be done by MichMich.
|
||||||
|
if (danger.github.pr.base.ref === "master" && danger.github.pr.user.login !== "MichMich") {
|
||||||
|
// Check if the PR body or title includes the text: #accepted.
|
||||||
|
// If not, the PR will fail.
|
||||||
|
if ((danger.github.pr.body + danger.github.pr.title).includes("#accepted")) {
|
||||||
|
fail("Please send all your pull requests to the `develop` branch.<br>Pull requests on the `master` branch will not be accepted.");
|
||||||
|
}
|
||||||
|
}
|
@@ -1,11 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
if [ ! -f /opt/magic_mirror/modules ]; then
|
|
||||||
cp -Rn /opt/magic_mirror/unmount_modules/. /opt/magic_mirror/modules
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f /opt/magic_mirror/config ]; then
|
|
||||||
cp -Rn /opt/magic_mirror/unmount_config/. /opt/magic_mirror/config
|
|
||||||
fi
|
|
||||||
|
|
||||||
node serveronly
|
|
@@ -1,202 +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
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
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.
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
12
fonts/package-lock.json
generated
Normal file
12
fonts/package-lock.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "magicmirror-fonts",
|
||||||
|
"requires": true,
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"dependencies": {
|
||||||
|
"roboto-fontface": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-ZYzRkETgBrdEGzL5JSKimvjI2CX7ioyZCkX2BpcfyjqI+079W0wHAyj5W4rIZMcDSOHgLZtgz1IdDi/vU77KEQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
fonts/package.json
Normal file
15
fonts/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "magicmirror-fonts",
|
||||||
|
"description": "Package for fonts use by MagicMirror Core.",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/MichMich/MagicMirror.git"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/MichMich/MagicMirror/issues"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"roboto-fontface": "^0.8.0"
|
||||||
|
}
|
||||||
|
}
|
@@ -5,9 +5,9 @@
|
|||||||
src:
|
src:
|
||||||
local("Roboto Thin"),
|
local("Roboto Thin"),
|
||||||
local("Roboto-Thin"),
|
local("Roboto-Thin"),
|
||||||
url("Roboto-Thin/Roboto-Thin.woff2") format("woff2"),
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"),
|
||||||
url("Roboto-Thin/Roboto-Thin.woff") format("woff"),
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff"),
|
||||||
url("Roboto-Thin/Roboto-Thin.ttf") format("truetype");
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.ttf") format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
src:
|
src:
|
||||||
local("Roboto Condensed Light"),
|
local("Roboto Condensed Light"),
|
||||||
local("RobotoCondensed-Light"),
|
local("RobotoCondensed-Light"),
|
||||||
url("RobotoCondensed-Light/RobotoCondensed-Light.woff2") format("woff2"),
|
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff2") format("woff2"),
|
||||||
url("RobotoCondensed-Light/RobotoCondensed-Light.woff") format("woff"),
|
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff") format("woff"),
|
||||||
url("RobotoCondensed-Light/RobotoCondensed-Light.ttf") format("truetype");
|
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.ttf") format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -29,9 +29,9 @@
|
|||||||
src:
|
src:
|
||||||
local("Roboto Condensed"),
|
local("Roboto Condensed"),
|
||||||
local("RobotoCondensed-Regular"),
|
local("RobotoCondensed-Regular"),
|
||||||
url("RobotoCondensed-Regular/RobotoCondensed-Regular.woff2") format("woff2"),
|
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff2") format("woff2"),
|
||||||
url("RobotoCondensed-Regular/RobotoCondensed-Regular.woff") format("woff"),
|
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff") format("woff"),
|
||||||
url("RobotoCondensed-Regular/RobotoCondensed-Regular.ttf") format("truetype");
|
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.ttf") format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -41,9 +41,9 @@
|
|||||||
src:
|
src:
|
||||||
local("Roboto Condensed Bold"),
|
local("Roboto Condensed Bold"),
|
||||||
local("RobotoCondensed-Bold"),
|
local("RobotoCondensed-Bold"),
|
||||||
url("RobotoCondensed-Bold/RobotoCondensed-Bold.woff2") format("woff2"),
|
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff2") format("woff2"),
|
||||||
url("RobotoCondensed-Bold/RobotoCondensed-Bold.woff") format("woff"),
|
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff") format("woff"),
|
||||||
url("RobotoCondensed-Bold/RobotoCondensed-Bold.ttf") format("truetype");
|
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.ttf") format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -53,9 +53,9 @@
|
|||||||
src:
|
src:
|
||||||
local("Roboto"),
|
local("Roboto"),
|
||||||
local("Roboto-Regular"),
|
local("Roboto-Regular"),
|
||||||
url("Roboto-Regular/Roboto-Regular.woff2") format("woff2"),
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"),
|
||||||
url("Roboto-Regular/Roboto-Regular.woff") format("woff"),
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff"),
|
||||||
url("Roboto-Regular/Roboto-Regular.ttf") format("truetype");
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.ttf") format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -65,9 +65,9 @@
|
|||||||
src:
|
src:
|
||||||
local("Roboto Medium"),
|
local("Roboto Medium"),
|
||||||
local("Roboto-Medium"),
|
local("Roboto-Medium"),
|
||||||
url("Roboto-Medium/Roboto-Medium.woff2") format("woff2"),
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"),
|
||||||
url("Roboto-Medium/Roboto-Medium.woff") format("woff"),
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff"),
|
||||||
url("Roboto-Medium/Roboto-Medium.ttf") format("truetype");
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.ttf") format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -77,9 +77,9 @@
|
|||||||
src:
|
src:
|
||||||
local("Roboto Bold"),
|
local("Roboto Bold"),
|
||||||
local("Roboto-Bold"),
|
local("Roboto-Bold"),
|
||||||
url("Roboto-Bold/Roboto-Bold.woff2") format("woff2"),
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"),
|
||||||
url("Roboto-Bold/Roboto-Bold.woff") format("woff"),
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff"),
|
||||||
url("Roboto-Bold/Roboto-Bold.ttf") format("truetype");
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.ttf") format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
src:
|
src:
|
||||||
local("Roboto Light"),
|
local("Roboto Light"),
|
||||||
local("Roboto-Light"),
|
local("Roboto-Light"),
|
||||||
url("Roboto-Light/Roboto-Light.woff2") format("woff2"),
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"),
|
||||||
url("Roboto-Light/Roboto-Light.woff") format("woff"),
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff"),
|
||||||
url("Roboto-Light/Roboto-Light.ttf") format("truetype");
|
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.ttf") format("truetype");
|
||||||
}
|
}
|
||||||
|
7
fonts/yarn.lock
Normal file
7
fonts/yarn.lock
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
roboto-fontface@^0.8.0:
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/roboto-fontface/-/roboto-fontface-0.8.0.tgz#031a83c8f79932801a57d83bf743f37250163499"
|
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Magic Mirror</title>
|
<title>MagicMirror²</title>
|
||||||
<meta name="google" content="notranslate" />
|
<meta name="google" content="notranslate" />
|
||||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="region fullscreen above"><div class="container"></div></div>
|
<div class="region fullscreen above"><div class="container"></div></div>
|
||||||
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
|
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
|
||||||
|
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
|
||||||
<script type="text/javascript" src="js/defaults.js"></script>
|
<script type="text/javascript" src="js/defaults.js"></script>
|
||||||
<script type="text/javascript" src="#CONFIG_FILE#"></script>
|
<script type="text/javascript" src="#CONFIG_FILE#"></script>
|
||||||
<script type="text/javascript" src="vendor/vendor.js"></script>
|
<script type="text/javascript" src="vendor/vendor.js"></script>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
|
|
||||||
# This is an installer script for MagicMirror2. It works well enough
|
# This is an installer script for MagicMirror2. It works well enough
|
||||||
# that it can detect if you have Node installed, run a binary script
|
# that it can detect if you have Node installed, run a binary script
|
||||||
@@ -20,21 +20,29 @@ echo -e "\e[0m"
|
|||||||
|
|
||||||
# Define the tested version of Node.js.
|
# Define the tested version of Node.js.
|
||||||
NODE_TESTED="v5.1.0"
|
NODE_TESTED="v5.1.0"
|
||||||
|
NPM_TESTED="V6.0.0"
|
||||||
|
USER=`whoami`
|
||||||
|
PM2_FILE=~/MagicMirror/installers/pm2_MagicMirror.json
|
||||||
|
|
||||||
# Determine which Pi is running.
|
# Determine which Pi is running.
|
||||||
ARM=$(uname -m)
|
ARM=$(uname -m)
|
||||||
|
|
||||||
# Check the Raspberry Pi version.
|
# Check the Raspberry Pi version.
|
||||||
if [ "$ARM" != "armv7l" ]; then
|
if [ "$ARM" != "armv7l" ]; then
|
||||||
echo -e "\e[91mSorry, your Raspberry Pi is not supported."
|
read -p "this appears not to be a Raspberry Pi 2 or 3, do you want to continue installtion (y/N)?" choice
|
||||||
echo -e "\e[91mPlease run MagicMirror on a Raspberry Pi 2 or 3."
|
if [[ $choice =~ ^[Nn]$ ]]; then
|
||||||
echo -e "\e[91mIf this is a Pi Zero, you are in the same boat as the original Raspberry Pi. You must run in server only mode."
|
echo -e "\e[91mSorry, your Raspberry Pi is not supported."
|
||||||
|
echo -e "\e[91mPlease run MagicMirror on a Raspberry Pi 2 or 3."
|
||||||
|
echo -e "\e[91mIf this is a Pi Zero, you are in the same boat as the original Raspberry Pi. You must run in server only mode."
|
||||||
exit;
|
exit;
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# Define helper methods.
|
# Define helper methods.
|
||||||
function version_gt() { test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1"; }
|
|
||||||
function command_exists () { type "$1" &> /dev/null ;}
|
function command_exists () { type "$1" &> /dev/null ;}
|
||||||
|
function verlte() { [ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ];}
|
||||||
|
function verlt() { [ "$1" = "$2" ] && return 1 || verlte $1 $2 ;}
|
||||||
|
|
||||||
# Update before first apt-get
|
# Update before first apt-get
|
||||||
echo -e "\e[96mUpdating packages ...\e[90m"
|
echo -e "\e[96mUpdating packages ...\e[90m"
|
||||||
@@ -42,7 +50,7 @@ sudo apt-get update || echo -e "\e[91mUpdate failed, carrying on installation ..
|
|||||||
|
|
||||||
# Installing helper tools
|
# Installing helper tools
|
||||||
echo -e "\e[96mInstalling helper tools ...\e[90m"
|
echo -e "\e[96mInstalling helper tools ...\e[90m"
|
||||||
sudo apt-get install curl wget git build-essential unzip || exit
|
sudo apt-get --assume-yes install curl wget git build-essential unzip || exit
|
||||||
|
|
||||||
# Check if we need to install or upgrade Node.js.
|
# Check if we need to install or upgrade Node.js.
|
||||||
echo -e "\e[96mCheck current Node installation ...\e[0m"
|
echo -e "\e[96mCheck current Node installation ...\e[0m"
|
||||||
@@ -52,7 +60,7 @@ if command_exists node; then
|
|||||||
NODE_CURRENT=$(node -v)
|
NODE_CURRENT=$(node -v)
|
||||||
echo -e "\e[0mMinimum Node version: \e[1m$NODE_TESTED\e[0m"
|
echo -e "\e[0mMinimum Node version: \e[1m$NODE_TESTED\e[0m"
|
||||||
echo -e "\e[0mInstalled Node version: \e[1m$NODE_CURRENT\e[0m"
|
echo -e "\e[0mInstalled Node version: \e[1m$NODE_CURRENT\e[0m"
|
||||||
if version_gt $NODE_TESTED $NODE_CURRENT; then
|
if verlte $NODE_CURRENT $NODE_TESTED; then
|
||||||
echo -e "\e[96mNode should be upgraded.\e[0m"
|
echo -e "\e[96mNode should be upgraded.\e[0m"
|
||||||
NODE_INSTALL=true
|
NODE_INSTALL=true
|
||||||
|
|
||||||
@@ -82,12 +90,57 @@ if $NODE_INSTALL; then
|
|||||||
# The NODE_STABLE_BRANCH variable will need to be manually adjusted when a new branch is released. (e.g. 7.x)
|
# The NODE_STABLE_BRANCH variable will need to be manually adjusted when a new branch is released. (e.g. 7.x)
|
||||||
# Only tested (stable) versions are recommended as newer versions could break MagicMirror.
|
# Only tested (stable) versions are recommended as newer versions could break MagicMirror.
|
||||||
|
|
||||||
NODE_STABLE_BRANCH="6.x"
|
NODE_STABLE_BRANCH="10.x"
|
||||||
curl -sL https://deb.nodesource.com/setup_$NODE_STABLE_BRANCH | sudo -E bash -
|
curl -sL https://deb.nodesource.com/setup_$NODE_STABLE_BRANCH | sudo -E bash -
|
||||||
sudo apt-get install -y nodejs
|
sudo apt-get install -y nodejs
|
||||||
echo -e "\e[92mNode.js installation Done!\e[0m"
|
echo -e "\e[92mNode.js installation Done!\e[0m"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check if we need to install or upgrade npm.
|
||||||
|
echo -e "\e[96mCheck current NPM installation ...\e[0m"
|
||||||
|
NPM_INSTALL=false
|
||||||
|
if command_exists npm; then
|
||||||
|
echo -e "\e[0mNPM currently installed. Checking version number.";
|
||||||
|
NPM_CURRENT='V'$(npm -v)
|
||||||
|
echo -e "\e[0mMinimum npm version: \e[1m$NPM_TESTED\e[0m"
|
||||||
|
echo -e "\e[0mInstalled npm version: \e[1m$NPM_CURRENT\e[0m"
|
||||||
|
if verlte $NPM_CURRENT $NPM_TESTED; then
|
||||||
|
echo -e "\e[96mnpm should be upgraded.\e[0m"
|
||||||
|
NPM_INSTALL=true
|
||||||
|
|
||||||
|
# Check if a node process is currently running.
|
||||||
|
# If so abort installation.
|
||||||
|
if pgrep "npm" > /dev/null; then
|
||||||
|
echo -e "\e[91mA npm process is currently running. Can't upgrade."
|
||||||
|
echo "Please quit all npm processes and restart the installer."
|
||||||
|
exit;
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
echo -e "\e[92mNo npm upgrade necessary.\e[0m"
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
echo -e "\e[93mnpm is not installed.\e[0m";
|
||||||
|
NPM_INSTALL=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install or upgrade node if necessary.
|
||||||
|
if $NPM_INSTALL; then
|
||||||
|
|
||||||
|
echo -e "\e[96mInstalling npm ...\e[90m"
|
||||||
|
|
||||||
|
# Fetch the latest version of npm from the selected branch
|
||||||
|
# The NODE_STABLE_BRANCH variable will need to be manually adjusted when a new branch is released. (e.g. 7.x)
|
||||||
|
# Only tested (stable) versions are recommended as newer versions could break MagicMirror.
|
||||||
|
|
||||||
|
#NODE_STABLE_BRANCH="9.x"
|
||||||
|
#curl -sL https://deb.nodesource.com/setup_$NODE_STABLE_BRANCH | sudo -E bash -
|
||||||
|
#
|
||||||
|
sudo apt-get install -y npm
|
||||||
|
echo -e "\e[92mnpm installation Done!\e[0m"
|
||||||
|
fi
|
||||||
|
|
||||||
# Install MagicMirror
|
# Install MagicMirror
|
||||||
cd ~
|
cd ~
|
||||||
if [ -d "$HOME/MagicMirror" ] ; then
|
if [ -d "$HOME/MagicMirror" ] ; then
|
||||||
@@ -101,7 +154,7 @@ if [ -d "$HOME/MagicMirror" ] ; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "\e[96mCloning MagicMirror ...\e[90m"
|
echo -e "\e[96mCloning MagicMirror ...\e[90m"
|
||||||
if git clone https://github.com/MichMich/MagicMirror.git; then
|
if git clone --depth=1 https://github.com/MichMich/MagicMirror.git; then
|
||||||
echo -e "\e[92mCloning MagicMirror Done!\e[0m"
|
echo -e "\e[92mCloning MagicMirror Done!\e[0m"
|
||||||
else
|
else
|
||||||
echo -e "\e[91mUnable to clone MagicMirror."
|
echo -e "\e[91mUnable to clone MagicMirror."
|
||||||
@@ -149,13 +202,40 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Use pm2 control like a service MagicMirror
|
# Use pm2 control like a service MagicMirror
|
||||||
read -p "Do you want use pm2 for auto starting of your MagicMirror (y/n)?" choice
|
read -p "Do you want use pm2 for auto starting of your MagicMirror (y/N)?" choice
|
||||||
if [[ $choice =~ ^[Yy]$ ]]
|
if [[ $choice =~ ^[Yy]$ ]]; then
|
||||||
then
|
#
|
||||||
sudo npm install -g pm2
|
# check if this is a mac
|
||||||
sudo su -c "env PATH=$PATH:/usr/bin pm2 startup linux -u pi --hp /home/pi"
|
#
|
||||||
pm2 start ~/MagicMirror/installers/pm2_MagicMirror.json
|
mac=$(uname -s)
|
||||||
pm2 save
|
up=""
|
||||||
|
if [ $mac == 'Darwin' ]; then
|
||||||
|
up="--unsafe-perm"
|
||||||
|
fi
|
||||||
|
sudo npm install $up -g pm2
|
||||||
|
if [[ "$(ps --no-headers -o comm 1)" =~ systemd ]]; then #Checking for systemd
|
||||||
|
pm2 startup systemd -u $USER --hp /home/$USER
|
||||||
|
else
|
||||||
|
sudo su -c "env PATH=$PATH:/usr/bin pm2 startup linux -u $USER --hp /home/$USER"
|
||||||
|
fi
|
||||||
|
if [ "USER" != "pi" ]; then
|
||||||
|
sed 's/pi/'$USER'/g' mm.sh >mm.sh
|
||||||
|
sed 's/pi/'$USER'/g' $PM2_FILE > ~/MagicMirror/installers/pm2_MagicMirror_new.json
|
||||||
|
PM2_FILE=~/MagicMirror/installers/pm2_MagicMirror_new.json
|
||||||
|
fi
|
||||||
|
pm2 start $PM2_FILE
|
||||||
|
pm2 save
|
||||||
|
fi
|
||||||
|
# Disable Screensaver
|
||||||
|
if [ -d "/etc/xdg/lxsession" ]; then
|
||||||
|
read -p "Do you want to disable the screen saver? (y/N)?" choice
|
||||||
|
if [[ $choice =~ ^[Yy]$ ]]; then
|
||||||
|
sudo su -c "echo -e '@xset s noblank\n@xset s off\n@xset -dpms' >> /etc/xdg/lxsession/LXDE-pi/autostart"
|
||||||
|
export DISPLAY=:0; xset s noblank;xset s off;xset -dpms
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " "
|
||||||
|
echo -e "unable to disable screen saver, /etc/xdg/lxsession does not exist"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo " "
|
echo " "
|
||||||
|
48
js/app.js
48
js/app.js
@@ -48,7 +48,6 @@ var App = function() {
|
|||||||
*
|
*
|
||||||
* argument callback function - The callback function.
|
* argument callback function - The callback function.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var loadConfig = function(callback) {
|
var loadConfig = function(callback) {
|
||||||
console.log("Loading config ...");
|
console.log("Loading config ...");
|
||||||
var defaults = require(__dirname + "/defaults.js");
|
var defaults = require(__dirname + "/defaults.js");
|
||||||
@@ -67,10 +66,10 @@ var App = function() {
|
|||||||
var config = Object.assign(defaults, c);
|
var config = Object.assign(defaults, c);
|
||||||
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."));
|
console.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. Please correct syntax errors. Starting with default configuration."));
|
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));
|
||||||
} else {
|
} else {
|
||||||
console.error(Utils.colors.error("WARNING! Could not load config file. Starting with default configuration. Error found: " + e));
|
console.error(Utils.colors.error("WARNING! Could not load config file. Starting with default configuration. Error found: " + e));
|
||||||
}
|
}
|
||||||
@@ -96,7 +95,7 @@ var App = function() {
|
|||||||
". Check README and CHANGELOG for more up-to-date ways of getting the same functionality.")
|
". Check README and CHANGELOG for more up-to-date ways of getting the same functionality.")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/* loadModule(module)
|
/* loadModule(module)
|
||||||
* Loads a specific module.
|
* Loads a specific module.
|
||||||
@@ -173,7 +172,7 @@ var App = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* cmpVersions(a,b)
|
/* cmpVersions(a,b)
|
||||||
* Compare two symantic version numbers and return the difference.
|
* Compare two semantic version numbers and return the difference.
|
||||||
*
|
*
|
||||||
* argument a string - Version number a.
|
* argument a string - Version number a.
|
||||||
* argument a string - Version number b.
|
* argument a string - Version number b.
|
||||||
@@ -197,7 +196,7 @@ var App = function() {
|
|||||||
/* start(callback)
|
/* start(callback)
|
||||||
* This methods starts the core app.
|
* This methods starts the core app.
|
||||||
* It loads the config, then it loads all modules.
|
* It loads the config, then it loads all modules.
|
||||||
* When it"s done it executs the callback with the config as argument.
|
* When it's done it executes the callback with the config as argument.
|
||||||
*
|
*
|
||||||
* argument callback function - The callback function.
|
* argument callback function - The callback function.
|
||||||
*/
|
*/
|
||||||
@@ -231,11 +230,46 @@ var App = function() {
|
|||||||
if (typeof callback === "function") {
|
if (typeof callback === "function") {
|
||||||
callback(config);
|
callback(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* stop()
|
||||||
|
* This methods stops the core app.
|
||||||
|
* This calls each node_helper's STOP() function, if it exists.
|
||||||
|
* Added to fix #1056
|
||||||
|
*/
|
||||||
|
this.stop = function() {
|
||||||
|
for (var h in nodeHelpers) {
|
||||||
|
var nodeHelper = nodeHelpers[h];
|
||||||
|
if (typeof nodeHelper.stop === "function") {
|
||||||
|
nodeHelper.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Listen for SIGINT signal and call stop() function.
|
||||||
|
*
|
||||||
|
* Added to fix #1056
|
||||||
|
* Note: this is only used if running `server-only`. Otherwise
|
||||||
|
* this.stop() is called by app.on("before-quit"... in `electron.js`
|
||||||
|
*/
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
console.log("[SIGINT] Received. Shutting down server...");
|
||||||
|
setTimeout(() => { process.exit(0); }, 3000); // Force quit after 3 seconds
|
||||||
|
this.stop();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* We also need to listen to SIGTERM signals so we stop everything when we are asked to stop by the OS.
|
||||||
|
*/
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
console.log("[SIGTERM] Received. Shutting down server...");
|
||||||
|
setTimeout(() => { process.exit(0); }, 3000); // Force quit after 3 seconds
|
||||||
|
this.stop();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = new App();
|
module.exports = new App();
|
||||||
|
43
js/class.js
43
js/class.js
@@ -4,15 +4,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Inspired by base2 and Prototype
|
// Inspired by base2 and Prototype
|
||||||
(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 () { };
|
||||||
|
|
||||||
// Create a new Class that inherits from this class
|
// Create a new Class that inherits from this class
|
||||||
Class.extend = function(prop) {
|
Class.extend = function (prop) {
|
||||||
var _super = this.prototype;
|
var _super = this.prototype;
|
||||||
|
|
||||||
// Instantiate a base class (but only create the instance,
|
// Instantiate a base class (but only create the instance,
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
var prototype = new this();
|
var prototype = new this();
|
||||||
initializing = false;
|
initializing = false;
|
||||||
|
|
||||||
// Make a copy of all prototype properies, to prevent reference issues.
|
// Make a copy of all prototype properties, to prevent reference issues.
|
||||||
for (var name in prototype) {
|
for (var name in prototype) {
|
||||||
prototype[name] = cloneObject(prototype[name]);
|
prototype[name] = cloneObject(prototype[name]);
|
||||||
}
|
}
|
||||||
@@ -29,24 +29,23 @@
|
|||||||
// 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 prop[name] === "function" &&
|
||||||
typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn) {
|
typeof _super[name] === "function" && fnTest.test(prop[name]) ? (function (name, fn) {
|
||||||
return function() {
|
return function () {
|
||||||
var tmp = this._super;
|
var tmp = this._super;
|
||||||
|
|
||||||
// Add a new ._super() method that is the same method
|
// Add a new ._super() method that is the same method
|
||||||
// but on the super-class
|
// but on the super-class
|
||||||
this._super = _super[name];
|
this._super = _super[name];
|
||||||
|
|
||||||
// The method only need to be bound temporarily, so we
|
// The method only need to be bound temporarily, so we
|
||||||
// remove it when we're done executing
|
// remove it when we're done executing
|
||||||
var ret = fn.apply(this, arguments);
|
var ret = fn.apply(this, arguments);
|
||||||
this._super = tmp;
|
this._super = tmp;
|
||||||
|
|
||||||
|
return ret;
|
||||||
return ret;
|
};
|
||||||
};
|
})(name, prop[name]) : prop[name];
|
||||||
})(name, prop[name]) : prop[name];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The dummy class constructor
|
// The dummy class constructor
|
||||||
@@ -90,4 +89,6 @@ function cloneObject(obj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||||
if (typeof module !== "undefined") {module.exports = Class;}
|
if (typeof module !== "undefined") {
|
||||||
|
module.exports = Class;
|
||||||
|
}
|
||||||
|
@@ -8,10 +8,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var port = 8080;
|
var port = 8080;
|
||||||
|
var address = "localhost";
|
||||||
if (typeof(mmPort) !== "undefined") {
|
if (typeof(mmPort) !== "undefined") {
|
||||||
port = mmPort;
|
port = mmPort;
|
||||||
}
|
}
|
||||||
var defaults = {
|
var defaults = {
|
||||||
|
address: address,
|
||||||
port: port,
|
port: port,
|
||||||
kioskmode: false,
|
kioskmode: false,
|
||||||
electronOptions: {},
|
electronOptions: {},
|
||||||
@@ -21,6 +23,7 @@ var defaults = {
|
|||||||
timeFormat: 24,
|
timeFormat: 24,
|
||||||
units: "metric",
|
units: "metric",
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
|
customCss: "css/custom.css",
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
{
|
{
|
||||||
|
@@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const Server = require(__dirname + "/server.js");
|
|
||||||
const electron = require("electron");
|
const electron = require("electron");
|
||||||
const core = require(__dirname + "/app.js");
|
const core = require(__dirname + "/app.js");
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
var config = {};
|
var config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||||
// Module to control application life.
|
// Module to control application life.
|
||||||
const app = electron.app;
|
const app = electron.app;
|
||||||
// Module to create native browser window.
|
// Module to create native browser window.
|
||||||
@@ -18,7 +17,7 @@ const BrowserWindow = electron.BrowserWindow;
|
|||||||
let mainWindow;
|
let mainWindow;
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
|
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
|
||||||
var electronOptionsDefaults = {
|
var electronOptionsDefaults = {
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 600,
|
height: 600,
|
||||||
@@ -30,7 +29,7 @@ function createWindow() {
|
|||||||
zoomFactor: config.zoom
|
zoomFactor: config.zoom
|
||||||
},
|
},
|
||||||
backgroundColor: "#000000"
|
backgroundColor: "#000000"
|
||||||
}
|
};
|
||||||
|
|
||||||
// DEPRECATED: "kioskmode" backwards compatibility, to be removed
|
// DEPRECATED: "kioskmode" backwards compatibility, to be removed
|
||||||
// settings these options directly instead provides cleaner interface
|
// settings these options directly instead provides cleaner interface
|
||||||
@@ -47,11 +46,12 @@ function createWindow() {
|
|||||||
mainWindow = new BrowserWindow(electronOptions);
|
mainWindow = new BrowserWindow(electronOptions);
|
||||||
|
|
||||||
// and load the index.html of the app.
|
// and load the index.html of the app.
|
||||||
//mainWindow.loadURL('file://' + __dirname + '../../index.html');
|
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
|
||||||
mainWindow.loadURL("http://localhost:" + config.port);
|
var address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address;
|
||||||
|
mainWindow.loadURL(`http://${address}:${config.port}`);
|
||||||
|
|
||||||
// Open the DevTools if run with "npm start dev"
|
// Open the DevTools if run with "npm start dev"
|
||||||
if(process.argv[2] == "dev") {
|
if (process.argv.includes("dev")) {
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,8 +97,24 @@ app.on("activate", function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the core application.
|
/* This method will be called when SIGINT is received and will call
|
||||||
// This starts all node helpers and starts the webserver.
|
* each node_helper's stop function if it exists. Added to fix #1056
|
||||||
core.start(function(c) {
|
*
|
||||||
config = c;
|
* Note: this is only used if running Electron. Otherwise
|
||||||
|
* core.stop() is called by process.on("SIGINT"... in `app.js`
|
||||||
|
*/
|
||||||
|
app.on("before-quit", (event) => {
|
||||||
|
console.log("Shutting down server...");
|
||||||
|
event.preventDefault();
|
||||||
|
setTimeout(() => { process.exit(0); }, 3000); // Force-quit after 3 seconds.
|
||||||
|
core.stop();
|
||||||
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Start the core application if server is run on localhost
|
||||||
|
// This starts all node helpers and starts the webserver.
|
||||||
|
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) > -1) {
|
||||||
|
core.start(function(c) {
|
||||||
|
config = c;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
15
js/loader.js
15
js/loader.js
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
var Loader = (function() {
|
var Loader = (function() {
|
||||||
|
|
||||||
/* Create helper valiables */
|
/* Create helper variables */
|
||||||
|
|
||||||
var loadedModuleFiles = [];
|
var loadedModuleFiles = [];
|
||||||
var loadedFiles = [];
|
var loadedFiles = [];
|
||||||
@@ -32,10 +32,10 @@ var Loader = (function() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// All modules loaded. Load custom.css
|
// All modules loaded. Load custom.css
|
||||||
// This is done after all the moduels so we can
|
// This is done after all the modules so we can
|
||||||
// overwrite all the defined styls.
|
// overwrite all the defined styles.
|
||||||
|
|
||||||
loadFile("css/custom.css", function() {
|
loadFile(config.customCss, function() {
|
||||||
// custom.css loaded. Start all modules.
|
// custom.css loaded. Start all modules.
|
||||||
startModules();
|
startModules();
|
||||||
});
|
});
|
||||||
@@ -55,7 +55,7 @@ var Loader = (function() {
|
|||||||
module.start();
|
module.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifiy core of loded modules.
|
// Notify core of loaded modules.
|
||||||
MM.modulesStarted(moduleObjects);
|
MM.modulesStarted(moduleObjects);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,7 +104,6 @@ var Loader = (function() {
|
|||||||
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;
|
||||||
@@ -138,7 +137,6 @@ var Loader = (function() {
|
|||||||
afterLoad();
|
afterLoad();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* bootstrapModule(module, mObj)
|
/* bootstrapModule(module, mObj)
|
||||||
@@ -164,7 +162,6 @@ var Loader = (function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* loadFile(fileName)
|
/* loadFile(fileName)
|
||||||
@@ -210,7 +207,6 @@ var Loader = (function() {
|
|||||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Public Methods */
|
/* Public Methods */
|
||||||
@@ -261,5 +257,4 @@ var Loader = (function() {
|
|||||||
loadFile(module.file(fileName), callback);
|
loadFile(module.file(fileName), callback);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
146
js/main.js
146
js/main.js
@@ -1,5 +1,5 @@
|
|||||||
/* global Log, Loader, Module, config, defaults */
|
/* global Log, Loader, Module, config, defaults */
|
||||||
/* jshint -W020 */
|
/* jshint -W020, esversion: 6 */
|
||||||
|
|
||||||
/* Magic Mirror
|
/* Magic Mirror
|
||||||
* Main System
|
* Main System
|
||||||
@@ -19,42 +19,49 @@ var MM = (function() {
|
|||||||
* are configured for a specific position.
|
* are configured for a specific position.
|
||||||
*/
|
*/
|
||||||
var createDomObjects = function() {
|
var createDomObjects = function() {
|
||||||
for (var m in modules) {
|
var domCreationPromises = [];
|
||||||
var module = modules[m];
|
|
||||||
|
|
||||||
if (typeof module.data.position === "string") {
|
modules.forEach(function(module) {
|
||||||
|
if (typeof module.data.position !== "string") {
|
||||||
var wrapper = selectWrapper(module.data.position);
|
return;
|
||||||
|
|
||||||
var dom = document.createElement("div");
|
|
||||||
dom.id = module.identifier;
|
|
||||||
dom.className = module.name;
|
|
||||||
|
|
||||||
if (typeof module.data.classes === "string") {
|
|
||||||
dom.className = "module " + dom.className + " " + module.data.classes;
|
|
||||||
}
|
|
||||||
|
|
||||||
dom.opacity = 0;
|
|
||||||
wrapper.appendChild(dom);
|
|
||||||
|
|
||||||
if (typeof module.data.header !== "undefined" && module.data.header !== "") {
|
|
||||||
var moduleHeader = document.createElement("header");
|
|
||||||
moduleHeader.innerHTML = module.data.header;
|
|
||||||
moduleHeader.className = "module-header";
|
|
||||||
dom.appendChild(moduleHeader);
|
|
||||||
}
|
|
||||||
|
|
||||||
var moduleContent = document.createElement("div");
|
|
||||||
moduleContent.className = "module-content";
|
|
||||||
dom.appendChild(moduleContent);
|
|
||||||
|
|
||||||
updateDom(module, 0);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
var wrapper = selectWrapper(module.data.position);
|
||||||
|
|
||||||
|
var dom = document.createElement("div");
|
||||||
|
dom.id = module.identifier;
|
||||||
|
dom.className = module.name;
|
||||||
|
|
||||||
|
if (typeof module.data.classes === "string") {
|
||||||
|
dom.className = "module " + dom.className + " " + module.data.classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
dom.opacity = 0;
|
||||||
|
wrapper.appendChild(dom);
|
||||||
|
|
||||||
|
if (typeof module.getHeader() !== "undefined" && module.getHeader() !== "") {
|
||||||
|
var moduleHeader = document.createElement("header");
|
||||||
|
moduleHeader.innerHTML = module.getHeader();
|
||||||
|
moduleHeader.className = "module-header";
|
||||||
|
dom.appendChild(moduleHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
var moduleContent = document.createElement("div");
|
||||||
|
moduleContent.className = "module-content";
|
||||||
|
dom.appendChild(moduleContent);
|
||||||
|
|
||||||
|
var domCreationPromise = updateDom(module, 0);
|
||||||
|
domCreationPromises.push(domCreationPromise);
|
||||||
|
domCreationPromise.then(function() {
|
||||||
|
sendNotification("MODULE_DOM_CREATED", null, null, module);
|
||||||
|
}).catch(Log.error);
|
||||||
|
});
|
||||||
|
|
||||||
updateWrapperStates();
|
updateWrapperStates();
|
||||||
|
|
||||||
sendNotification("DOM_OBJECTS_CREATED");
|
Promise.all(domCreationPromises).then(function() {
|
||||||
|
sendNotification("DOM_OBJECTS_CREATED");
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/* selectWrapper(position)
|
/* selectWrapper(position)
|
||||||
@@ -79,11 +86,12 @@ var MM = (function() {
|
|||||||
* argument notification string - The identifier of the notification.
|
* argument notification string - The identifier of the notification.
|
||||||
* argument payload mixed - The payload of the notification.
|
* argument payload mixed - The payload of the notification.
|
||||||
* argument sender Module - The module that sent the notification.
|
* argument sender Module - The module that sent the notification.
|
||||||
|
* argument sendTo Module - The module to send the notification to. (optional)
|
||||||
*/
|
*/
|
||||||
var sendNotification = function(notification, payload, sender) {
|
var sendNotification = function(notification, payload, sender, sendTo) {
|
||||||
for (var m in modules) {
|
for (var m in modules) {
|
||||||
var module = modules[m];
|
var module = modules[m];
|
||||||
if (module !== sender) {
|
if (module !== sender && (!sendTo || module === sendTo)) {
|
||||||
module.notificationReceived(notification, payload, sender);
|
module.notificationReceived(notification, payload, sender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,19 +102,53 @@ var MM = (function() {
|
|||||||
*
|
*
|
||||||
* argument module Module - The module that needs an update.
|
* argument module Module - The module that needs an update.
|
||||||
* argument speed Number - The number of microseconds for the animation. (optional)
|
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||||
|
*
|
||||||
|
* return Promise - Resolved when the dom is fully updated.
|
||||||
*/
|
*/
|
||||||
var updateDom = function(module, speed) {
|
var updateDom = function(module, speed) {
|
||||||
var newContent = module.getDom();
|
return new Promise(function(resolve) {
|
||||||
var newHeader = module.getHeader();
|
var newContentPromise = module.getDom();
|
||||||
|
var newHeader = module.getHeader();
|
||||||
|
|
||||||
if (!module.hidden) {
|
if (!(newContentPromise instanceof Promise)) {
|
||||||
|
// convert to a promise if not already one to avoid if/else's everywhere
|
||||||
|
newContentPromise = Promise.resolve(newContentPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
newContentPromise.then(function(newContent) {
|
||||||
|
var updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
|
||||||
|
|
||||||
|
updatePromise.then(resolve).catch(Log.error);
|
||||||
|
}).catch(Log.error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* updateDomWithContent(module, speed, newHeader, newContent)
|
||||||
|
* Update the dom with the specified content
|
||||||
|
*
|
||||||
|
* argument module Module - The module that needs an update.
|
||||||
|
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||||
|
* argument newHeader String - The new header that is generated.
|
||||||
|
* argument newContent Domobject - The new content that is generated.
|
||||||
|
*
|
||||||
|
* return Promise - Resolved when the module dom has been updated.
|
||||||
|
*/
|
||||||
|
var updateDomWithContent = function(module, speed, newHeader, newContent) {
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
if (module.hidden || !speed) {
|
||||||
|
updateModuleContent(module, newHeader, newContent);
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!moduleNeedsUpdate(module, newHeader, newContent)) {
|
if (!moduleNeedsUpdate(module, newHeader, newContent)) {
|
||||||
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!speed) {
|
if (!speed) {
|
||||||
updateModuleContent(module, newHeader, newContent);
|
updateModuleContent(module, newHeader, newContent);
|
||||||
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,22 +157,26 @@ var MM = (function() {
|
|||||||
if (!module.hidden) {
|
if (!module.hidden) {
|
||||||
showModule(module, speed / 2);
|
showModule(module, speed / 2);
|
||||||
}
|
}
|
||||||
|
resolve();
|
||||||
});
|
});
|
||||||
} else {
|
});
|
||||||
updateModuleContent(module, newHeader, newContent);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* moduleNeedsUpdate(module, newContent)
|
/* moduleNeedsUpdate(module, newContent)
|
||||||
* Check if the content has changed.
|
* Check if the content has changed.
|
||||||
*
|
*
|
||||||
* argument module Module - The module to check.
|
* argument module Module - The module to check.
|
||||||
|
* argument newHeader String - The new header that is generated.
|
||||||
* argument newContent Domobject - The new content that is generated.
|
* argument newContent Domobject - The new content that is generated.
|
||||||
*
|
*
|
||||||
* return bool - Does the module need an update?
|
* return bool - Does the module need an update?
|
||||||
*/
|
*/
|
||||||
var moduleNeedsUpdate = function(module, newHeader, newContent) {
|
var moduleNeedsUpdate = function(module, newHeader, newContent) {
|
||||||
var moduleWrapper = document.getElementById(module.identifier);
|
var moduleWrapper = document.getElementById(module.identifier);
|
||||||
|
if (moduleWrapper === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||||
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||||
|
|
||||||
@@ -152,10 +198,12 @@ var MM = (function() {
|
|||||||
* Update the content of a module on screen.
|
* Update the content of a module on screen.
|
||||||
*
|
*
|
||||||
* argument module Module - The module to check.
|
* argument module Module - The module to check.
|
||||||
|
* argument newHeader String - The new header that is generated.
|
||||||
* argument newContent Domobject - The new content that is generated.
|
* argument newContent Domobject - 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;
|
||||||
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||||
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||||
|
|
||||||
@@ -165,8 +213,6 @@ var MM = (function() {
|
|||||||
if( headerWrapper.length > 0 && newHeader) {
|
if( headerWrapper.length > 0 && newHeader) {
|
||||||
headerWrapper[0].innerHTML = newHeader;
|
headerWrapper[0].innerHTML = newHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* hideModule(module, speed, callback)
|
/* hideModule(module, speed, callback)
|
||||||
@@ -204,6 +250,9 @@ var MM = (function() {
|
|||||||
|
|
||||||
if (typeof callback === "function") { callback(); }
|
if (typeof callback === "function") { callback(); }
|
||||||
}, speed);
|
}, speed);
|
||||||
|
} else {
|
||||||
|
// invoke callback even if no content, issue 1308
|
||||||
|
if (typeof callback === "function") { callback(); }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -219,7 +268,7 @@ var MM = (function() {
|
|||||||
|
|
||||||
// remove lockString if set in options.
|
// remove lockString if set in options.
|
||||||
if (options.lockString) {
|
if (options.lockString) {
|
||||||
var index = module.lockStrings.indexOf(options.lockString)
|
var index = module.lockStrings.indexOf(options.lockString);
|
||||||
if ( index !== -1) {
|
if ( index !== -1) {
|
||||||
module.lockStrings.splice(index, 1);
|
module.lockStrings.splice(index, 1);
|
||||||
}
|
}
|
||||||
@@ -243,7 +292,7 @@ var MM = (function() {
|
|||||||
var moduleWrapper = document.getElementById(module.identifier);
|
var moduleWrapper = document.getElementById(module.identifier);
|
||||||
if (moduleWrapper !== null) {
|
if (moduleWrapper !== null) {
|
||||||
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
||||||
// Restore the postition. See hideModule() for more info.
|
// Restore the position. See hideModule() for more info.
|
||||||
moduleWrapper.style.position = "static";
|
moduleWrapper.style.position = "static";
|
||||||
|
|
||||||
updateWrapperStates();
|
updateWrapperStates();
|
||||||
@@ -263,7 +312,7 @@ var MM = (function() {
|
|||||||
/* updateWrapperStates()
|
/* 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 schould be called by the show and hide methods.
|
* This method should be called by the show and hide methods.
|
||||||
*
|
*
|
||||||
* Example:
|
* Example:
|
||||||
* If the top_bar only contains the update notification. And no update is available,
|
* If the top_bar only contains the update notification. And no update is available,
|
||||||
@@ -271,7 +320,6 @@ var MM = (function() {
|
|||||||
* an ugly top margin. By using this function, the top bar will be hidden if the
|
* an ugly top margin. By using this function, the top bar will be hidden if the
|
||||||
* update notification is not visible.
|
* update notification is not visible.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var updateWrapperStates = function() {
|
var updateWrapperStates = function() {
|
||||||
var positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
var positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||||
|
|
||||||
@@ -281,7 +329,7 @@ var MM = (function() {
|
|||||||
|
|
||||||
var showWrapper = false;
|
var showWrapper = false;
|
||||||
Array.prototype.forEach.call(moduleWrappers, function(moduleWrapper) {
|
Array.prototype.forEach.call(moduleWrappers, function(moduleWrapper) {
|
||||||
if (moduleWrapper.style.position == "" || moduleWrapper.style.position == "static") {
|
if (moduleWrapper.style.position === "" || moduleWrapper.style.position === "static") {
|
||||||
showWrapper = true;
|
showWrapper = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -430,7 +478,7 @@ var MM = (function() {
|
|||||||
/* sendNotification(notification, payload, sender)
|
/* sendNotification(notification, payload, sender)
|
||||||
* Send a notification to all modules.
|
* Send a notification to all modules.
|
||||||
*
|
*
|
||||||
* argument notification string - The identifier of the noitication.
|
* argument notification string - The identifier of the notification.
|
||||||
* argument payload mixed - The payload of the notification.
|
* argument payload mixed - The payload of the notification.
|
||||||
* argument sender Module - The module that sent the notification.
|
* argument sender Module - The module that sent the notification.
|
||||||
*/
|
*/
|
||||||
@@ -510,7 +558,7 @@ var MM = (function() {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
// Add polyfill for Object.assign.
|
// Add polyfill for Object.assign.
|
||||||
if (typeof Object.assign != "function") {
|
if (typeof Object.assign !== "function") {
|
||||||
(function() {
|
(function() {
|
||||||
Object.assign = function(target) {
|
Object.assign = function(target) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
124
js/module.js
124
js/module.js
@@ -27,6 +27,11 @@ var Module = Class.extend({
|
|||||||
// visibility when hiding and showing module.
|
// visibility when hiding and showing module.
|
||||||
lockStrings: [],
|
lockStrings: [],
|
||||||
|
|
||||||
|
// Storage of the nunjuck Environment,
|
||||||
|
// This should not be referenced directly.
|
||||||
|
// Use the nunjucksEnvironment() to get it.
|
||||||
|
_nunjucksEnvironment: null,
|
||||||
|
|
||||||
/* init()
|
/* init()
|
||||||
* Is called when the module is instantiated.
|
* Is called when the module is instantiated.
|
||||||
*/
|
*/
|
||||||
@@ -70,25 +75,37 @@ var Module = Class.extend({
|
|||||||
|
|
||||||
/* getDom()
|
/* getDom()
|
||||||
* This method generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
|
* This method generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
|
||||||
* This method needs 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.
|
||||||
*
|
*
|
||||||
* return domobject - The dom to display.
|
* return DomObject | Promise - The dom or a promise with the dom to display.
|
||||||
*/
|
*/
|
||||||
getDom: function () {
|
getDom: function () {
|
||||||
var nameWrapper = document.createElement("div");
|
var self = this;
|
||||||
var name = document.createTextNode(this.name);
|
return new Promise(function(resolve) {
|
||||||
nameWrapper.appendChild(name);
|
var div = document.createElement("div");
|
||||||
|
var template = self.getTemplate();
|
||||||
|
var templateData = self.getTemplateData();
|
||||||
|
|
||||||
var identifierWrapper = document.createElement("div");
|
// Check to see if we need to render a template string or a file.
|
||||||
var identifier = document.createTextNode(this.identifier);
|
if (/^.*((\.html)|(\.njk))$/.test(template)) {
|
||||||
identifierWrapper.appendChild(identifier);
|
// the template is a filename
|
||||||
identifierWrapper.className = "small dimmed";
|
self.nunjucksEnvironment().render(template, templateData, function (err, res) {
|
||||||
|
if (err) {
|
||||||
|
Log.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
var div = document.createElement("div");
|
div.innerHTML = res;
|
||||||
div.appendChild(nameWrapper);
|
|
||||||
div.appendChild(identifierWrapper);
|
|
||||||
|
|
||||||
return div;
|
resolve(div);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// the template is a template string.
|
||||||
|
div.innerHTML = self.nunjucksEnvironment().renderString(template, templateData);
|
||||||
|
|
||||||
|
resolve(div);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/* getHeader()
|
/* getHeader()
|
||||||
@@ -102,6 +119,28 @@ var Module = Class.extend({
|
|||||||
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.
|
||||||
|
* This method needs to be subclassed if the module wants to use a template.
|
||||||
|
* It can either return a template sting, or a template filename.
|
||||||
|
* If the string ends with '.html' it's considered a file from within the module's folder.
|
||||||
|
*
|
||||||
|
* return string - The template string of filename.
|
||||||
|
*/
|
||||||
|
getTemplate: function () {
|
||||||
|
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.
|
||||||
|
* This method needs to be subclassed if the module wants to use a custom data.
|
||||||
|
*
|
||||||
|
* return Object
|
||||||
|
*/
|
||||||
|
getTemplateData: function () {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
/* notificationReceived(notification, payload, sender)
|
/* notificationReceived(notification, payload, sender)
|
||||||
* This method is called when a notification arrives.
|
* This method is called when a notification arrives.
|
||||||
* This method is called by the Magic Mirror core.
|
* This method is called by the Magic Mirror core.
|
||||||
@@ -118,6 +157,30 @@ var Module = Class.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** nunjucksEnvironment()
|
||||||
|
* Returns the nunjucks environment for the current module.
|
||||||
|
* The environment is checked in the _nunjucksEnvironment instance variable.
|
||||||
|
|
||||||
|
* @returns Nunjucks Environment
|
||||||
|
*/
|
||||||
|
nunjucksEnvironment: function() {
|
||||||
|
if (this._nunjucksEnvironment !== null) {
|
||||||
|
return this._nunjucksEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this._nunjucksEnvironment = new nunjucks.Environment(new nunjucks.WebLoader(this.file(""), {async: true}), {
|
||||||
|
trimBlocks: true,
|
||||||
|
lstripBlocks: true
|
||||||
|
});
|
||||||
|
this._nunjucksEnvironment.addFilter("translate", function(str) {
|
||||||
|
return self.translate(str);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this._nunjucksEnvironment;
|
||||||
|
},
|
||||||
|
|
||||||
/* socketNotificationReceived(notification, payload)
|
/* socketNotificationReceived(notification, payload)
|
||||||
* This method is called when a socket notification arrives.
|
* This method is called when a socket notification arrives.
|
||||||
*
|
*
|
||||||
@@ -149,7 +212,7 @@ var Module = Class.extend({
|
|||||||
/* setData(data)
|
/* setData(data)
|
||||||
* Set the module data.
|
* Set the module data.
|
||||||
*
|
*
|
||||||
* argument data obejct - Module data.
|
* argument data object - Module data.
|
||||||
*/
|
*/
|
||||||
setData: function (data) {
|
setData: function (data) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
@@ -163,14 +226,14 @@ var Module = Class.extend({
|
|||||||
/* setConfig(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 obejct - Module config.
|
* argument config object - Module config.
|
||||||
*/
|
*/
|
||||||
setConfig: function (config) {
|
setConfig: function (config) {
|
||||||
this.config = Object.assign({}, this.defaults, config);
|
this.config = Object.assign({}, this.defaults, config);
|
||||||
},
|
},
|
||||||
|
|
||||||
/* socket()
|
/* 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.
|
||||||
*/
|
*/
|
||||||
socket: function () {
|
socket: function () {
|
||||||
@@ -194,7 +257,7 @@ var Module = Class.extend({
|
|||||||
* return string - File path.
|
* return string - File path.
|
||||||
*/
|
*/
|
||||||
file: function (file) {
|
file: function (file) {
|
||||||
return this.data.path + "/" + file;
|
return (this.data.path + "/" + file).replace("//", "/");
|
||||||
},
|
},
|
||||||
|
|
||||||
/* loadStyles()
|
/* loadStyles()
|
||||||
@@ -272,14 +335,18 @@ var Module = Class.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/* translate(key, defaultValue)
|
/* translate(key, defaultValueOrVariables, defaultValue)
|
||||||
* Request the translation for a given key.
|
* Request the translation for a given key with optional variables and default value.
|
||||||
*
|
*
|
||||||
* argument key string - The key of the string to translage
|
* argument key string - The key of the string to translate
|
||||||
* argument defaultValue string - The default value if no translation was found. (Optional)
|
* argument defaultValueOrVariables string/object - The default value or variables for translating. (Optional)
|
||||||
|
* argument defaultValue string - The default value with variables. (Optional)
|
||||||
*/
|
*/
|
||||||
translate: function (key, defaultValue) {
|
translate: function (key, defaultValueOrVariables, defaultValue) {
|
||||||
return Translator.translate(this, key) || defaultValue || "";
|
if(typeof defaultValueOrVariables === "object") {
|
||||||
|
return Translator.translate(this, key, defaultValueOrVariables) || defaultValue || "";
|
||||||
|
}
|
||||||
|
return Translator.translate(this, key) || defaultValueOrVariables || "";
|
||||||
},
|
},
|
||||||
|
|
||||||
/* updateDom(speed)
|
/* updateDom(speed)
|
||||||
@@ -371,11 +438,10 @@ Module.create = function (name) {
|
|||||||
var ModuleClass = Module.extend(clonedDefinition);
|
var ModuleClass = Module.extend(clonedDefinition);
|
||||||
|
|
||||||
return new ModuleClass();
|
return new ModuleClass();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* cmpVersions(a,b)
|
/* cmpVersions(a,b)
|
||||||
* Compare two symantic version numbers and return the difference.
|
* Compare two semantic version numbers and return the difference.
|
||||||
*
|
*
|
||||||
* argument a string - Version number a.
|
* argument a string - Version number a.
|
||||||
* argument a string - Version number b.
|
* argument a string - Version number b.
|
||||||
@@ -410,11 +476,3 @@ Module.register = function (name, moduleDefinition) {
|
|||||||
Log.log("Module registered: " + name);
|
Log.log("Module registered: " + name);
|
||||||
Module.definitions[name] = moduleDefinition;
|
Module.definitions[name] = moduleDefinition;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof exports != "undefined") { // For testing purpose only
|
|
||||||
// A good a idea move the function cmpversions a helper file.
|
|
||||||
// It's used into other side.
|
|
||||||
exports._test = {
|
|
||||||
cmpVersions: cmpVersions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
16
js/server.js
16
js/server.js
@@ -13,25 +13,25 @@ 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 Server = function(config, callback) {
|
var Server = function(config, callback) {
|
||||||
console.log("Starting server on port " + config.port + " ... ");
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Starting server op port " + port + " ... ");
|
console.log("Starting server on port " + port + " ... ");
|
||||||
|
|
||||||
server.listen(port, config.address ? config.address : null);
|
server.listen(port, config.address ? config.address : null);
|
||||||
|
|
||||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length == 0) {
|
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||||
console.info("You're using a full whitelist configuration to allow for all IPs")
|
console.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) {
|
||||||
var result = ipfilter(config.ipWhitelist, {mode: "allow", log: false})(req, res, function(err) {
|
var result = ipfilter(config.ipWhitelist, {mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false})(req, res, function(err) {
|
||||||
if (err === undefined) {
|
if (err === undefined) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ var Server = function(config, callback) {
|
|||||||
app.use("/js", express.static(__dirname));
|
app.use("/js", express.static(__dirname));
|
||||||
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
||||||
var directory;
|
var directory;
|
||||||
for (i in directories) {
|
for (var i in directories) {
|
||||||
directory = directories[i];
|
directory = directories[i];
|
||||||
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
||||||
}
|
}
|
||||||
@@ -53,6 +53,10 @@ var Server = function(config, callback) {
|
|||||||
res.send(global.version);
|
res.send(global.version);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/config", function(req,res) {
|
||||||
|
res.send(config);
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/", function(req, res) {
|
app.get("/", function(req, res) {
|
||||||
var html = fs.readFileSync(path.resolve(global.root_path + "/index.html"), {encoding: "utf8"});
|
var html = fs.readFileSync(path.resolve(global.root_path + "/index.html"), {encoding: "utf8"});
|
||||||
html = html.replace("#VERSION#", global.version);
|
html = html.replace("#VERSION#", global.version);
|
||||||
|
@@ -22,7 +22,6 @@ var MMSocket = function(moduleName) {
|
|||||||
// register catch all.
|
// register catch all.
|
||||||
self.socket.on("*", function(notification, payload) {
|
self.socket.on("*", function(notification, payload) {
|
||||||
if (notification !== "*") {
|
if (notification !== "*") {
|
||||||
//console.log('Received notification: ' + notification +', payload: ' + payload);
|
|
||||||
notificationCallback(notification, payload);
|
notificationCallback(notification, payload);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -18,7 +18,7 @@ var Translator = (function() {
|
|||||||
xhr.overrideMimeType("application/json");
|
xhr.overrideMimeType("application/json");
|
||||||
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(stripComments(xhr.responseText)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -111,41 +111,61 @@ var Translator = (function() {
|
|||||||
translations: {},
|
translations: {},
|
||||||
translationsFallback: {},
|
translationsFallback: {},
|
||||||
|
|
||||||
/* translate(module, key)
|
/* 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.
|
* argument module Module - The module to load the translation for.
|
||||||
* argument key string - The key of the text to translate.
|
* argument key string - The key of the text to translate.
|
||||||
|
* argument variables - The variables to use within the translation template (optional)
|
||||||
*/
|
*/
|
||||||
translate: function(module, key) {
|
translate: function(module, key, variables) {
|
||||||
|
variables = variables || {}; //Empty object by default
|
||||||
|
|
||||||
|
// Combines template and variables like:
|
||||||
|
// template: "Please wait for {timeToWait} before continuing with {work}."
|
||||||
|
// variables: {timeToWait: "2 hours", work: "painting"}
|
||||||
|
// to: "Please wait for 2 hours before continuing with painting."
|
||||||
|
function createStringFromTemplate(template, variables) {
|
||||||
|
if(Object.prototype.toString.call(template) !== "[object String]") {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
if(variables.fallback && !template.match(new RegExp("\{.+\}"))) {
|
||||||
|
template = variables.fallback;
|
||||||
|
}
|
||||||
|
return template.replace(new RegExp("\{([^\}]+)\}", "g"), function(_unused, varName){
|
||||||
|
return variables[varName] || "{"+varName+"}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if(this.translations[module.name] && key in this.translations[module.name]) {
|
if(this.translations[module.name] && key in this.translations[module.name]) {
|
||||||
// Log.log("Got translation for " + key + " from module translation: ");
|
// Log.log("Got translation for " + key + " from module translation: ");
|
||||||
return this.translations[module.name][key];
|
return createStringFromTemplate(this.translations[module.name][key], variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key in this.coreTranslations) {
|
if (key in this.coreTranslations) {
|
||||||
// Log.log("Got translation for " + key + " from core translation.");
|
// Log.log("Got translation for " + key + " from core translation.");
|
||||||
return this.coreTranslations[key];
|
return createStringFromTemplate(this.coreTranslations[key], variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.translationsFallback[module.name] && key in this.translationsFallback[module.name]) {
|
if (this.translationsFallback[module.name] && key in this.translationsFallback[module.name]) {
|
||||||
// Log.log("Got translation for " + key + " from module translation fallback.");
|
// Log.log("Got translation for " + key + " from module translation fallback.");
|
||||||
return this.translationsFallback[module.name][key];
|
return createStringFromTemplate(this.translationsFallback[module.name][key], variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key in this.coreTranslationsFallback) {
|
if (key in this.coreTranslationsFallback) {
|
||||||
// Log.log("Got translation for " + key + " from core translation fallback.");
|
// Log.log("Got translation for " + key + " from core translation fallback.");
|
||||||
return this.coreTranslationsFallback[key];
|
return createStringFromTemplate(this.coreTranslationsFallback[key], variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
return key;
|
return key;
|
||||||
},
|
},
|
||||||
/* load(module, file, callback)
|
|
||||||
|
/* 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.
|
* argument module Module - The module to load the translation file for.
|
||||||
* argument file string - Path of the file we want to load.
|
* argument file string - Path of the file we want to load.
|
||||||
|
* argument isFallback boolean - Flag to indicate fallback translations.
|
||||||
* argument callback function - Function called when done.
|
* argument callback function - Function called when done.
|
||||||
*/
|
*/
|
||||||
load: function(module, file, isFallback, callback) {
|
load: function(module, file, isFallback, callback) {
|
||||||
@@ -201,10 +221,12 @@ var Translator = (function() {
|
|||||||
// defined translation after the following line.
|
// defined translation after the following line.
|
||||||
for (var first in translations) {break;}
|
for (var first in translations) {break;}
|
||||||
|
|
||||||
Log.log("Loading core translation fallback file: " + translations[first]);
|
if (first) {
|
||||||
loadJSON(translations[first], function(translations) {
|
Log.log("Loading core translation fallback file: " + translations[first]);
|
||||||
self.coreTranslationsFallback = translations;
|
loadJSON(translations[first], function(translations) {
|
||||||
});
|
self.coreTranslationsFallback = translations;
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
31
module-types.ts
Normal file
31
module-types.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
type ModuleProperties = {
|
||||||
|
defaults?: object,
|
||||||
|
start?(): void,
|
||||||
|
getHeader?(): string,
|
||||||
|
getTemplate?(): string,
|
||||||
|
getTemplateData?(): object,
|
||||||
|
notificationReceived?(notification: string, payload: any, sender: object): void,
|
||||||
|
socketNotificationReceived?(notification: string, payload: any): void,
|
||||||
|
suspend?(): void,
|
||||||
|
resume?(): void,
|
||||||
|
getDom?(): HTMLElement,
|
||||||
|
getStyles?(): string[],
|
||||||
|
[key: string]: any,
|
||||||
|
};
|
||||||
|
|
||||||
|
export declare const Module: {
|
||||||
|
register(moduleName: string, moduleProperties: ModuleProperties): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export declare const Log: {
|
||||||
|
info(message?: any, ...optionalParams: any[]): void,
|
||||||
|
log(message?: any, ...optionalParams: any[]): void,
|
||||||
|
error(message?: any, ...optionalParams: any[]): void,
|
||||||
|
warn(message?: any, ...optionalParams: any[]): void,
|
||||||
|
group(groupTitle?: string, ...optionalParams: any[]): void,
|
||||||
|
groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void,
|
||||||
|
groupEnd(): void,
|
||||||
|
time(timerName?: string): void,
|
||||||
|
timeEnd(timerName?: string): void,
|
||||||
|
timeStamp(timerName?: string): void,
|
||||||
|
};
|
@@ -2,6 +2,42 @@
|
|||||||
|
|
||||||
This document describes the way to develop your own MagicMirror² modules.
|
This document describes the way to develop your own MagicMirror² modules.
|
||||||
|
|
||||||
|
Table of Contents:
|
||||||
|
|
||||||
|
- Module structure
|
||||||
|
- Files
|
||||||
|
|
||||||
|
- The Core module file: modulename.js
|
||||||
|
- Available module instance properties
|
||||||
|
- Subclassable module methods
|
||||||
|
- Module instance methods
|
||||||
|
- Visibility locking
|
||||||
|
|
||||||
|
- The Node Helper: node_helper.js
|
||||||
|
- Available module instance properties
|
||||||
|
- Subclassable module methods
|
||||||
|
- Module instance methods
|
||||||
|
|
||||||
|
- MagicMirror Helper Methods
|
||||||
|
- Module Selection
|
||||||
|
|
||||||
|
- MagicMirror Logger
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## General Advice
|
||||||
|
|
||||||
|
As MagicMirror has gained huge popularity, so has the number of available modules. For new users and developers alike, it is very time consuming to navigate around the various repositories in order to find out what exactly a certain modules does, how it looks and what it depends on. Unfortunately, this information is rarely available, nor easily obtained without having to install it first.
|
||||||
|
Therefore **we highly recommend you to include the following information in your README file.**
|
||||||
|
|
||||||
|
- A high quality screenshot of your working module
|
||||||
|
- A short, one sentence, clear description what it does (duh!)
|
||||||
|
- What external API's it depends upon, including web links to those
|
||||||
|
- Whether the API/request require a key and the user limitations of those. (Is it free?)
|
||||||
|
|
||||||
|
Surely this also help you get better recognition and feedback for your work.
|
||||||
|
|
||||||
## Module structure
|
## Module structure
|
||||||
|
|
||||||
All modules are loaded in the `modules` folder. The default modules are grouped together in the `modules/default` folder. Your module should be placed in a subfolder of `modules`. Note that any file or folder your create in the `modules` folder will be ignored by git, allowing you to upgrade the MagicMirror² without the loss of your files.
|
All modules are loaded in the `modules` folder. The default modules are grouped together in the `modules/default` folder. Your module should be placed in a subfolder of `modules`. Note that any file or folder your create in the `modules` folder will be ignored by git, allowing you to upgrade the MagicMirror² without the loss of your files.
|
||||||
@@ -10,11 +46,11 @@ A module can be placed in one single folder. Or multiple modules can be grouped
|
|||||||
|
|
||||||
### Files
|
### Files
|
||||||
- **modulename/modulename.js** - This is your core module script.
|
- **modulename/modulename.js** - This is your core module script.
|
||||||
- **modulename/node_helper.js** - This is an optional helper that will be loaded by the node script. The node helper and module script can communicate with each other using an intergrated socket system.
|
- **modulename/node_helper.js** - This is an optional helper that will be loaded by the node script. The node helper and module script can communicate with each other using an integrated socket system.
|
||||||
- **modulename/public** - Any files in this folder can be accesed via the browser on `/modulename/filename.ext`.
|
- **modulename/public** - Any files in this folder can be accessed via the browser on `/modulename/filename.ext`.
|
||||||
- **modulename/anyfileorfolder** Any other file or folder in the module folder can be used by the core module script. For example: *modulename/css/modulename.css* would be a good path for your additional module styles.
|
- **modulename/anyfileorfolder** Any other file or folder in the module folder can be used by the core module script. For example: *modulename/css/modulename.css* would be a good path for your additional module styles.
|
||||||
|
|
||||||
## Core module file: modulename.js
|
## The Core module file: modulename.js
|
||||||
This is the script in which the module will be defined. This script is required in order for the module to be used. In it's most simple form, the core module file must contain:
|
This is the script in which the module will be defined. This script is required in order for the module to be used. In it's most simple form, the core module file must contain:
|
||||||
````javascript
|
````javascript
|
||||||
Module.register("modulename",{});
|
Module.register("modulename",{});
|
||||||
@@ -44,30 +80,16 @@ As you can see, the `Module.register()` method takes two arguments: the name of
|
|||||||
### Available module instance properties
|
### Available module instance properties
|
||||||
After the module is initialized, the module instance has a few available module properties:
|
After the module is initialized, the module instance has a few available module properties:
|
||||||
|
|
||||||
####`this.name`
|
| Instance Property | Type | Description |
|
||||||
**String**
|
|:----------------- |:---- |:----------- |
|
||||||
|
| `this.name` | String | The name of the module. |
|
||||||
|
| `this.identifier` | String | This is a unique identifier for the module instance. |
|
||||||
|
| `this.hidden` | Boolean | This represents if the module is currently hidden (faded away). |
|
||||||
|
| `this.config` | Boolean | The configuration of the module instance as set in the user's `config.js` file. This config will also contain the module's defaults if these properties are not over-written by the user config. |
|
||||||
|
| `this.data` | Object | The data object contain additional metadata about the module instance. (See below) |
|
||||||
|
|
||||||
The name of the module.
|
|
||||||
|
|
||||||
####`this.identifier`
|
The `this.data` data object contain the following metadata:
|
||||||
**String**
|
|
||||||
|
|
||||||
This is a unique identifier for the module instance.
|
|
||||||
|
|
||||||
####`this.hidden`
|
|
||||||
**Boolean**
|
|
||||||
|
|
||||||
This represents if the module is currently hidden (faded away).
|
|
||||||
|
|
||||||
####`this.config`
|
|
||||||
**Boolean**
|
|
||||||
|
|
||||||
The configuration of the module instance as set in the user's config.js file. This config will also contain the module's defaults if these properties are not over written by the user config.
|
|
||||||
|
|
||||||
####`this.data`
|
|
||||||
**Object**
|
|
||||||
|
|
||||||
The data object contains additional metadata about the module instance:
|
|
||||||
- `data.classes` - The classes which are added to the module dom wrapper.
|
- `data.classes` - The classes which are added to the module dom wrapper.
|
||||||
- `data.file` - The filename of the core module file.
|
- `data.file` - The filename of the core module file.
|
||||||
- `data.path` - The path of the module folder.
|
- `data.path` - The path of the module folder.
|
||||||
@@ -75,10 +97,10 @@ The data object contains additional metadata about the module instance:
|
|||||||
- `data.position` - The position in which the instance will be shown.
|
- `data.position` - The position in which the instance will be shown.
|
||||||
|
|
||||||
|
|
||||||
####`defaults: {}`
|
#### `defaults: {}`
|
||||||
Any properties defined in the defaults object, will be merged with the module config as defined in the user's config.js file. This is the best place to set your modules's configuration defaults. Any of the module configuration properties can be accessed using `this.config.propertyName`, but more about that later.
|
Any properties defined in the defaults object, will be merged with the module config as defined in the user's config.js file. This is the best place to set your modules' configuration defaults. Any of the module configuration properties can be accessed using `this.config.propertyName`, but more about that later.
|
||||||
|
|
||||||
####'requiresVersion:'
|
#### `requiresVersion:`
|
||||||
|
|
||||||
*Introduced in version: 2.1.0.*
|
*Introduced in version: 2.1.0.*
|
||||||
|
|
||||||
@@ -93,10 +115,10 @@ requiresVersion: "2.1.0",
|
|||||||
|
|
||||||
### Subclassable module methods
|
### Subclassable module methods
|
||||||
|
|
||||||
####`init()`
|
#### `init()`
|
||||||
This method is called when a module gets instantiated. In most cases you do not need to subclass this method.
|
This method is called when a module gets instantiated. In most cases you do not need to subclass this method.
|
||||||
|
|
||||||
####`loaded(callback)`
|
#### `loaded(callback)`
|
||||||
|
|
||||||
*Introduced in version: 2.1.1.*
|
*Introduced in version: 2.1.1.*
|
||||||
|
|
||||||
@@ -111,8 +133,8 @@ loaded: function(callback) {
|
|||||||
}
|
}
|
||||||
````
|
````
|
||||||
|
|
||||||
####`start()`
|
#### `start()`
|
||||||
This method is called when all modules are loaded an the system is ready to boot up. Keep in mind that the dom object for the module is not yet created. The start method is a perfect place to define any additional module properties:
|
This method is called when all modules are loaded and the system is ready to boot up. Keep in mind that the dom object for the module is not yet created. The start method is a perfect place to define any additional module properties:
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
````javascript
|
````javascript
|
||||||
@@ -122,7 +144,7 @@ start: function() {
|
|||||||
}
|
}
|
||||||
````
|
````
|
||||||
|
|
||||||
####`getScripts()`
|
#### `getScripts()`
|
||||||
**Should return: Array**
|
**Should return: Array**
|
||||||
|
|
||||||
The getScripts method is called to request any additional scripts that need to be loaded. This method should therefore return an array with strings. If you want to return a full path to a file in the module folder, use the `this.file('filename.js')` method. In all cases the loader will only load a file once. It even checks if the file is available in the default vendor folder.
|
The getScripts method is called to request any additional scripts that need to be loaded. This method should therefore return an array with strings. If you want to return a full path to a file in the module folder, use the `this.file('filename.js')` method. In all cases the loader will only load a file once. It even checks if the file is available in the default vendor folder.
|
||||||
@@ -139,10 +161,10 @@ getScripts: function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
````
|
````
|
||||||
**Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore it's advised not to use any external urls.
|
**Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore, it's advised not to use any external urls.
|
||||||
|
|
||||||
|
|
||||||
####`getStyles()`
|
#### `getStyles()`
|
||||||
**Should return: Array**
|
**Should return: Array**
|
||||||
|
|
||||||
The getStyles method is called to request any additional stylesheets that need to be loaded. This method should therefore return an array with strings. If you want to return a full path to a file in the module folder, use the `this.file('filename.css')` method. In all cases the loader will only load a file once. It even checks if the file is available in the default vendor folder.
|
The getStyles method is called to request any additional stylesheets that need to be loaded. This method should therefore return an array with strings. If you want to return a full path to a file in the module folder, use the `this.file('filename.css')` method. In all cases the loader will only load a file once. It even checks if the file is available in the default vendor folder.
|
||||||
@@ -152,16 +174,16 @@ The getStyles method is called to request any additional stylesheets that need t
|
|||||||
getStyles: function() {
|
getStyles: function() {
|
||||||
return [
|
return [
|
||||||
'script.css', // will try to load it from the vendor folder, otherwise it will load is from the module folder.
|
'script.css', // will try to load it from the vendor folder, otherwise it will load is from the module folder.
|
||||||
'font-awesome.css', // this file is available in the vendor folder, so it doesn't need to be avialable in the module folder.
|
'font-awesome.css', // this file is available in the vendor folder, so it doesn't need to be available in the module folder.
|
||||||
this.file('anotherfile.css'), // this file will be loaded straight from the module folder.
|
this.file('anotherfile.css'), // this file will be loaded straight from the module folder.
|
||||||
'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css', // this file will be loaded from the bootstrapcdn servers.
|
'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css', // this file will be loaded from the bootstrapcdn servers.
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
````
|
````
|
||||||
**Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore it's advised not to use any external urls.
|
**Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore, it's advised not to use any external URLs.
|
||||||
|
|
||||||
####`getTranslations()`
|
#### `getTranslations()`
|
||||||
**Should return: Dictionary**
|
**Should return: Dictionary**
|
||||||
|
|
||||||
The getTranslations method is called to request translation files that need to be loaded. This method should therefore return a dictionary with the files to load, identified by the country's short name.
|
The getTranslations method is called to request translation files that need to be loaded. This method should therefore return a dictionary with the files to load, identified by the country's short name.
|
||||||
@@ -179,7 +201,7 @@ getTranslations: function() {
|
|||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
####`getDom()`
|
#### `getDom()`
|
||||||
**Should return:** Dom Object
|
**Should return:** Dom Object
|
||||||
|
|
||||||
Whenever the MagicMirror needs to update the information on screen (because it starts, or because your module asked a refresh using `this.updateDom()`), the system calls the getDom method. This method should therefore return a dom object.
|
Whenever the MagicMirror needs to update the information on screen (because it starts, or because your module asked a refresh using `this.updateDom()`), the system calls the getDom method. This method should therefore return a dom object.
|
||||||
@@ -194,7 +216,7 @@ getDom: function() {
|
|||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
####`getHeader()`
|
#### `getHeader()`
|
||||||
**Should return:** String
|
**Should return:** String
|
||||||
|
|
||||||
Whenever the MagicMirror needs to update the information on screen (because it starts, or because your module asked a refresh using `this.updateDom()`), the system calls the getHeader method to retrieve the module's header. This method should therefor return a string. If this method is not subclassed, this function will return the user's configured header.
|
Whenever the MagicMirror needs to update the information on screen (because it starts, or because your module asked a refresh using `this.updateDom()`), the system calls the getHeader method to retrieve the module's header. This method should therefor return a string. If this method is not subclassed, this function will return the user's configured header.
|
||||||
@@ -211,13 +233,13 @@ getHeader: function() {
|
|||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
####`notificationReceived(notification, payload, sender)`
|
#### `notificationReceived(notification, payload, sender)`
|
||||||
|
|
||||||
That MagicMirror core has the ability to send notifications to modules. Or even better: the modules have the possibility to send notifications to other modules. When this module is called, it has 3 arguments:
|
That MagicMirror core has the ability to send notifications to modules. Or even better: the modules have the possibility to send notifications to other modules. When this module is called, it has 3 arguments:
|
||||||
|
|
||||||
- `notification` - String - The notification identifier.
|
- `notification` - String - The notification identifier.
|
||||||
- `payload` - AnyType - The payload of a notification.
|
- `payload` - AnyType - The payload of a notification.
|
||||||
- `sender` - Module - The sender of the notification. If this argument is `undefined`, the sender of the notififiction is the core system.
|
- `sender` - Module - The sender of the notification. If this argument is `undefined`, the sender of the notification is the core system.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
````javascript
|
````javascript
|
||||||
@@ -230,21 +252,22 @@ notificationReceived: function(notification, payload, sender) {
|
|||||||
}
|
}
|
||||||
````
|
````
|
||||||
|
|
||||||
**Note:** the system sends two notifications when starting up. These notifications could come in handy!
|
**Note:** the system sends three notifications when starting up. These notifications could come in handy!
|
||||||
|
|
||||||
|
|
||||||
- `ALL_MODULES_STARTED` - All modules are started. You can now send notifications to other modules.
|
- `ALL_MODULES_STARTED` - All modules are started. You can now send notifications to other modules.
|
||||||
- `DOM_OBJECTS_CREATED` - All dom objects are created. The system is now ready to perform visual changes.
|
- `DOM_OBJECTS_CREATED` - All dom objects are created. The system is now ready to perform visual changes.
|
||||||
|
- `MODULE_DOM_CREATED` - This module's dom has been fully loaded. You can now access your module's dom objects.
|
||||||
|
|
||||||
|
|
||||||
####`socketNotificationReceived: function(notification, payload)`
|
#### `socketNotificationReceived: function(notification, payload)`
|
||||||
When using a node_helper, the node helper can send your module notifications. When this module is called, it has 2 arguments:
|
When using a node_helper, the node helper can send your module notifications. When this module is called, it has 2 arguments:
|
||||||
|
|
||||||
- `notification` - String - The notification identifier.
|
- `notification` - String - The notification identifier.
|
||||||
- `payload` - AnyType - The payload of a notification.
|
- `payload` - AnyType - The payload of a notification.
|
||||||
|
|
||||||
**Note 1:** When a node helper sends a notification, all modules of that module type receive the same notifications. <br>
|
**Note 1:** When a node helper sends a notification, all modules of that module type receive the same notifications. <br>
|
||||||
**Note 2:** The socket connection is established as soon as the module sends its first message using [sendSocketNotification](thissendsocketnotificationnotification-payload).
|
**Note 2:** The socket connection is established as soon as the module sends its first message using [sendSocketNotification](#thissendsocketnotificationnotification-payload).
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
````javascript
|
````javascript
|
||||||
@@ -253,11 +276,11 @@ socketNotificationReceived: function(notification, payload) {
|
|||||||
},
|
},
|
||||||
````
|
````
|
||||||
|
|
||||||
####`suspend()`
|
#### `suspend()`
|
||||||
When a module is hidden (using the `module.hide()` method), the `suspend()` method will be called. By subclassing this method you can perform tasks like halting the update timers.
|
When a module is hidden (using the `module.hide()` method), the `suspend()` method will be called. By subclassing this method you can perform tasks like halting the update timers.
|
||||||
|
|
||||||
####`resume()`
|
#### `resume()`
|
||||||
When a module will be shown after it was previously hidden (using the `module.show()` method), the `resume()` method will be called. By subclassing this method you can perform tasks restarting the update timers.
|
When a module is requested to be shown (using the `module.show()` method), the `resume()` method will be called. By subclassing this method you can perform tasks restarting the update timers.
|
||||||
|
|
||||||
|
|
||||||
### Module instance methods
|
### Module instance methods
|
||||||
@@ -265,13 +288,13 @@ When a module will be shown after it was previously hidden (using the `module.sh
|
|||||||
Each module instance has some handy methods which can be helpful building your module.
|
Each module instance has some handy methods which can be helpful building your module.
|
||||||
|
|
||||||
|
|
||||||
####`this.file(filename)`
|
#### `this.file(filename)`
|
||||||
***filename* String** - The name of the file you want to create the path for.<br>
|
***filename* String** - The name of the file you want to create the path for.<br>
|
||||||
**Returns String**
|
**Returns String**
|
||||||
|
|
||||||
If you want to create a path to a file in your module folder, use the `file()` method. It returns the path to the filename given as the attribute. Is method comes in handy when configuring the [getScripts](#getscripts) and [getStyles](#getstyles) methods.
|
If you want to create a path to a file in your module folder, use the `file()` method. It returns the path to the filename given as the attribute. Is method comes in handy when configuring the [getScripts](#getscripts) and [getStyles](#getstyles) methods.
|
||||||
|
|
||||||
####`this.updateDom(speed)`
|
#### `this.updateDom(speed)`
|
||||||
***speed* Number** - Optional. Animation speed in milliseconds.<br>
|
***speed* Number** - Optional. Animation speed in milliseconds.<br>
|
||||||
|
|
||||||
Whenever your module need to be updated, call the `updateDom(speed)` method. It requests the MagicMirror core to update its dom object. If you define the speed, the content update will be animated, but only if the content will really change.
|
Whenever your module need to be updated, call the `updateDom(speed)` method. It requests the MagicMirror core to update its dom object. If you define the speed, the content update will be animated, but only if the content will really change.
|
||||||
@@ -289,7 +312,7 @@ start: function() {
|
|||||||
...
|
...
|
||||||
````
|
````
|
||||||
|
|
||||||
####`this.sendNotification(notification, payload)`
|
#### `this.sendNotification(notification, payload)`
|
||||||
***notification* String** - The notification identifier.<br>
|
***notification* String** - The notification identifier.<br>
|
||||||
***payload* AnyType** - Optional. A notification payload.<br>
|
***payload* AnyType** - Optional. A notification payload.<br>
|
||||||
|
|
||||||
@@ -300,7 +323,7 @@ If you want to send a notification to all other modules, use the `sendNotificati
|
|||||||
this.sendNotification('MYMODULE_READY_FOR_ACTION', {foo:bar});
|
this.sendNotification('MYMODULE_READY_FOR_ACTION', {foo:bar});
|
||||||
````
|
````
|
||||||
|
|
||||||
####`this.sendSocketNotification(notification, payload)`
|
#### `this.sendSocketNotification(notification, payload)`
|
||||||
***notification* String** - The notification identifier.<br>
|
***notification* String** - The notification identifier.<br>
|
||||||
***payload* AnyType** - Optional. A notification payload.<br>
|
***payload* AnyType** - Optional. A notification payload.<br>
|
||||||
|
|
||||||
@@ -311,7 +334,7 @@ If you want to send a notification to the node_helper, use the `sendSocketNotifi
|
|||||||
this.sendSocketNotification('SET_CONFIG', this.config);
|
this.sendSocketNotification('SET_CONFIG', this.config);
|
||||||
````
|
````
|
||||||
|
|
||||||
####`this.hide(speed, callback, options)`
|
#### `this.hide(speed, callback, options)`
|
||||||
***speed* Number** - Optional (Required when setting callback or options), The speed of the hide animation in milliseconds.
|
***speed* Number** - Optional (Required when setting callback or options), The speed of the hide animation in milliseconds.
|
||||||
***callback* Function** - Optional, The callback after the hide animation is finished.
|
***callback* Function** - Optional, The callback after the hide animation is finished.
|
||||||
***options* Function** - Optional, Object with additional options for the hide action (see below). (*Introduced in version: 2.1.0.*)
|
***options* Function** - Optional, Object with additional options for the hide action (see below). (*Introduced in version: 2.1.0.*)
|
||||||
@@ -323,12 +346,12 @@ Possible configurable options:
|
|||||||
- `lockString` - String - When setting lock string, the module can not be shown without passing the correct lockstring. This way (multiple) modules can prevent a module from showing. It's considered best practice to use your modules identifier as the locksString: `this.identifier`. See *visibility locking* below.
|
- `lockString` - String - When setting lock string, the module can not be shown without passing the correct lockstring. This way (multiple) modules can prevent a module from showing. It's considered best practice to use your modules identifier as the locksString: `this.identifier`. See *visibility locking* below.
|
||||||
|
|
||||||
|
|
||||||
**Note 1:** If the hide animation is canceled, for instance because the show method is called before the hide animation was finished, the callback will not be called.<br>
|
**Note 1:** If the hide animation is cancelled, for instance because the show method is called before the hide animation was finished, the callback will not be called.<br>
|
||||||
**Note 2:** If the hide animation is hijacked (an other method calls hide on the same module), the callback will not be called.<br>
|
**Note 2:** If the hide animation is hijacked (an other method calls hide on the same module), the callback will not be called.<br>
|
||||||
**Note 3:** If the dom is not yet created, the hide method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender).
|
**Note 3:** If the dom is not yet created, the hide method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender).
|
||||||
|
|
||||||
|
|
||||||
####`this.show(speed, callback, options)`
|
#### `this.show(speed, callback, options)`
|
||||||
***speed* Number** - Optional (Required when setting callback or options), The speed of the show animation in milliseconds.
|
***speed* Number** - Optional (Required when setting callback or options), The speed of the show animation in milliseconds.
|
||||||
***callback* Function** - Optional, The callback after the show animation is finished.
|
***callback* Function** - Optional, The callback after the show animation is finished.
|
||||||
***options* Function** - Optional, Object with additional options for the show action (see below). (*Introduced in version: 2.1.0.*)
|
***options* Function** - Optional, Object with additional options for the show action (see below). (*Introduced in version: 2.1.0.*)
|
||||||
@@ -344,11 +367,11 @@ Possible configurable options:
|
|||||||
**Note 2:** If the show animation is hijacked (an other method calls show on the same module), the callback will not be called.<br>
|
**Note 2:** If the show animation is hijacked (an other method calls show on the same module), the callback will not be called.<br>
|
||||||
**Note 3:** If the dom is not yet created, the show method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender).
|
**Note 3:** If the dom is not yet created, the show method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender).
|
||||||
|
|
||||||
####Visibility locking
|
#### Visibility locking
|
||||||
|
|
||||||
(*Introduced in version: 2.1.0.*)
|
(*Introduced in version: 2.1.0.*)
|
||||||
|
|
||||||
Visiblity locking helps the module system to prevent unwanted hide/show actions. The following scenario explains the concept:
|
Visibility locking helps the module system to prevent unwanted hide/show actions. The following scenario explains the concept:
|
||||||
|
|
||||||
**Module B asks module A to hide:**
|
**Module B asks module A to hide:**
|
||||||
````javascript
|
````javascript
|
||||||
@@ -401,7 +424,7 @@ Use this `force` method with caution. See `show()` method for more information.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
####`this.translate(identifier)`
|
#### `this.translate(identifier)`
|
||||||
***identifier* String** - Identifier of the string that should be translated.
|
***identifier* String** - Identifier of the string that should be translated.
|
||||||
|
|
||||||
The Magic Mirror contains a convenience wrapper for `l18n`. You can use this to automatically serve different translations for your modules based on the user's `language` configuration.
|
The Magic Mirror contains a convenience wrapper for `l18n`. You can use this to automatically serve different translations for your modules based on the user's `language` configuration.
|
||||||
@@ -413,7 +436,7 @@ If no translation is found, a fallback will be used. The fallback sequence is as
|
|||||||
- 4. Translation as defined in core translation file of the fallback language (the first defined core translation file).
|
- 4. Translation as defined in core translation file of the fallback language (the first defined core translation file).
|
||||||
- 5. The key (identifier) of the translation.
|
- 5. The key (identifier) of the translation.
|
||||||
|
|
||||||
When adding translations to your module, it's a good idea to see if an apropriate translation is already available in the [core translation files](https://github.com/MichMich/MagicMirror/tree/master/translations). This way, your module can benefit from the existing translations.
|
When adding translations to your module, it's a good idea to see if an appropriate translation is already available in the [core translation files](https://github.com/MichMich/MagicMirror/tree/master/translations). This way, your module can benefit from the existing translations.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
````javascript
|
````javascript
|
||||||
@@ -429,6 +452,51 @@ this.translate("INFO") //Will return a translated string for the identifier INFO
|
|||||||
|
|
||||||
**Note:** although comments are officially not supported in JSON files, MagicMirror allows it by stripping the comments before parsing the JSON file. Comments in translation files could help other translators.
|
**Note:** although comments are officially not supported in JSON files, MagicMirror allows it by stripping the comments before parsing the JSON file. Comments in translation files could help other translators.
|
||||||
|
|
||||||
|
##### `this.translate(identifier, variables)`
|
||||||
|
***identifier* String** - Identifier of the string that should be translated.
|
||||||
|
***variables* Object** - Object of variables to be used in translation.
|
||||||
|
|
||||||
|
This improved and backwards compatible way to handle translations behaves like the normal translation function and follows the rules described above. It's recommended to use this new format for translating everywhere. It allows translator to change the word order in the sentence to be translated.
|
||||||
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
````javascript
|
||||||
|
var timeUntilEnd = moment(event.endDate, "x").fromNow(true);
|
||||||
|
this.translate("RUNNING", { "timeUntilEnd": timeUntilEnd) }); // Will return a translated string for the identifier RUNNING, replacing `{timeUntilEnd}` with the contents of the variable `timeUntilEnd` in the order that translator intended.
|
||||||
|
````
|
||||||
|
|
||||||
|
**Example English .json file:**
|
||||||
|
````javascript
|
||||||
|
{
|
||||||
|
"RUNNING": "Ends in {timeUntilEnd}",
|
||||||
|
}
|
||||||
|
````
|
||||||
|
|
||||||
|
**Example Finnish .json file:**
|
||||||
|
````javascript
|
||||||
|
{
|
||||||
|
"RUNNING": "Päättyy {timeUntilEnd} päästä",
|
||||||
|
}
|
||||||
|
````
|
||||||
|
|
||||||
|
**Note:** The *variables* Object has an special case called `fallback`. It's used to support old translations in translation files that do not have the variables in them. If you are upgrading an old module that had translations that did not support the word order, it is recommended to have the fallback layout.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
````javascript
|
||||||
|
var timeUntilEnd = moment(event.endDate, "x").fromNow(true);
|
||||||
|
this.translate("RUNNING", {
|
||||||
|
"fallback": this.translate("RUNNING") + " {timeUntilEnd}"
|
||||||
|
"timeUntilEnd": timeUntilEnd
|
||||||
|
)}); // Will return a translated string for the identifier RUNNING, replacing `{timeUntilEnd}` with the contents of the variable `timeUntilEnd` in the order that translator intended. (has a fallback)
|
||||||
|
````
|
||||||
|
|
||||||
|
**Example Swedish .json file that does not have the variable in it:**
|
||||||
|
````javascript
|
||||||
|
{
|
||||||
|
"RUNNING": "Slutar",
|
||||||
|
}
|
||||||
|
````
|
||||||
|
In this case the `translate`-function will not find any variables in the translation, will look for `fallback` variable and use that if possible to create the translation.
|
||||||
|
|
||||||
## The Node Helper: node_helper.js
|
## The Node Helper: node_helper.js
|
||||||
|
|
||||||
@@ -447,17 +515,17 @@ Of course, the above helper would not do anything useful. So with the informatio
|
|||||||
|
|
||||||
### Available module instance properties
|
### Available module instance properties
|
||||||
|
|
||||||
####`this.name`
|
#### `this.name`
|
||||||
**String**
|
**String**
|
||||||
|
|
||||||
The name of the module
|
The name of the module
|
||||||
|
|
||||||
####`this.path`
|
#### `this.path`
|
||||||
**String**
|
**String**
|
||||||
|
|
||||||
The path of the module
|
The path of the module
|
||||||
|
|
||||||
####`this.expressApp`
|
#### `this.expressApp`
|
||||||
**Express App Instance**
|
**Express App Instance**
|
||||||
|
|
||||||
This is a link to the express instance. It will allow you to define extra routes.
|
This is a link to the express instance. It will allow you to define extra routes.
|
||||||
@@ -476,13 +544,13 @@ start: function() {
|
|||||||
this.expressApp.use("/" + this.name, express.static(this.path + "/public"));
|
this.expressApp.use("/" + this.name, express.static(this.path + "/public"));
|
||||||
````
|
````
|
||||||
|
|
||||||
####`this.io`
|
#### `this.io`
|
||||||
**Socket IO Instance**
|
**Socket IO Instance**
|
||||||
|
|
||||||
This is a link to the IO instance. It will allow you to do some Socket.IO magic. In most cases you won't need this, since the Node Helper has a few convenience methods to make this simple.
|
This is a link to the IO instance. It will allow you to do some Socket.IO magic. In most cases you won't need this, since the Node Helper has a few convenience methods to make this simple.
|
||||||
|
|
||||||
|
|
||||||
####'requiresVersion:'
|
#### `requiresVersion:`
|
||||||
*Introduced in version: 2.1.0.*
|
*Introduced in version: 2.1.0.*
|
||||||
|
|
||||||
A string that defines the minimum version of the MagicMirror framework. If it is set, the system compares the required version with the users version. If the version of the user is out of date, it won't run the module.
|
A string that defines the minimum version of the MagicMirror framework. If it is set, the system compares the required version with the users version. If the version of the user is out of date, it won't run the module.
|
||||||
@@ -496,10 +564,10 @@ requiresVersion: "2.1.0",
|
|||||||
|
|
||||||
### Subclassable module methods
|
### Subclassable module methods
|
||||||
|
|
||||||
####`init()`
|
#### `init()`
|
||||||
This method is called when a node helper gets instantiated. In most cases you do not need to subclass this method.
|
This method is called when a node helper gets instantiated. In most cases you do not need to subclass this method.
|
||||||
|
|
||||||
####`start()`
|
#### `start()`
|
||||||
This method is called when all node helpers are loaded and the system is ready to boot up. The start method is a perfect place to define any additional module properties:
|
This method is called when all node helpers are loaded and the system is ready to boot up. The start method is a perfect place to define any additional module properties:
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
@@ -510,7 +578,18 @@ start: function() {
|
|||||||
}
|
}
|
||||||
````
|
````
|
||||||
|
|
||||||
####`socketNotificationReceived: function(notification, payload)`
|
#### `stop()`
|
||||||
|
This method is called when the MagicMirror server receives a `SIGINT` command and is shutting down. This method should include any commands needed to close any open connections, stop any sub-processes and gracefully exit the module.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
````javascript
|
||||||
|
stop: function() {
|
||||||
|
console.log("Shutting down MyModule");
|
||||||
|
this.connection.close();
|
||||||
|
}
|
||||||
|
````
|
||||||
|
|
||||||
|
#### `socketNotificationReceived: function(notification, payload)`
|
||||||
With this method, your node helper can receive notifications from your modules. When this method is called, it has 2 arguments:
|
With this method, your node helper can receive notifications from your modules. When this method is called, it has 2 arguments:
|
||||||
|
|
||||||
- `notification` - String - The notification identifier.
|
- `notification` - String - The notification identifier.
|
||||||
@@ -529,7 +608,7 @@ socketNotificationReceived: function(notification, payload) {
|
|||||||
|
|
||||||
Each node helper has some handy methods which can be helpful building your module.
|
Each node helper has some handy methods which can be helpful building your module.
|
||||||
|
|
||||||
####`this.sendSocketNotification(notification, payload)`
|
#### `this.sendSocketNotification(notification, payload)`
|
||||||
***notification* String** - The notification identifier.<br>
|
***notification* String** - The notification identifier.<br>
|
||||||
***payload* AnyType** - Optional. A notification payload.<br>
|
***payload* AnyType** - Optional. A notification payload.<br>
|
||||||
|
|
||||||
@@ -549,7 +628,7 @@ The core Magic Mirror object: `MM` has some handy method that will help you in c
|
|||||||
### Module selection
|
### Module selection
|
||||||
The only additional method available for your module, is the feature to retrieve references to other modules. This can be used to hide and show other modules.
|
The only additional method available for your module, is the feature to retrieve references to other modules. This can be used to hide and show other modules.
|
||||||
|
|
||||||
####`MM.getModules()`
|
#### `MM.getModules()`
|
||||||
**Returns Array** - An array with module instances.<br>
|
**Returns Array** - An array with module instances.<br>
|
||||||
|
|
||||||
To make a selection of all currently loaded module instances, run the `MM.getModules()` method. It will return an array with all currently loaded module instances. The returned array has a lot of filtering methods. See below for more info.
|
To make a selection of all currently loaded module instances, run the `MM.getModules()` method. It will return an array with all currently loaded module instances. The returned array has a lot of filtering methods. See below for more info.
|
||||||
@@ -557,7 +636,7 @@ To make a selection of all currently loaded module instances, run the `MM.getMod
|
|||||||
**Note:** This method returns an empty array if not all modules are started yet. Wait for the `ALL_MODULES_STARTED` [notification](#notificationreceivednotification-payload-sender).
|
**Note:** This method returns an empty array if not all modules are started yet. Wait for the `ALL_MODULES_STARTED` [notification](#notificationreceivednotification-payload-sender).
|
||||||
|
|
||||||
|
|
||||||
#####`.withClass(classnames)`
|
##### `.withClass(classnames)`
|
||||||
***classnames* String or Array** - The class names on which you want to filter.
|
***classnames* String or Array** - The class names on which you want to filter.
|
||||||
**Returns Array** - An array with module instances.<br>
|
**Returns Array** - An array with module instances.<br>
|
||||||
|
|
||||||
@@ -570,7 +649,7 @@ var modules = MM.getModules().withClass('classname1 classname2');
|
|||||||
var modules = MM.getModules().withClass(['classname1','classname2']);
|
var modules = MM.getModules().withClass(['classname1','classname2']);
|
||||||
````
|
````
|
||||||
|
|
||||||
#####`.exceptWithClass(classnames)`
|
##### `.exceptWithClass(classnames)`
|
||||||
***classnames* String or Array** - The class names of the modules you want to remove from the results.
|
***classnames* String or Array** - The class names of the modules you want to remove from the results.
|
||||||
**Returns Array** - An array with module instances.<br>
|
**Returns Array** - An array with module instances.<br>
|
||||||
|
|
||||||
@@ -583,7 +662,7 @@ var modules = MM.getModules().exceptWithClass('classname1 classname2');
|
|||||||
var modules = MM.getModules().exceptWithClass(['classname1','classname2']);
|
var modules = MM.getModules().exceptWithClass(['classname1','classname2']);
|
||||||
````
|
````
|
||||||
|
|
||||||
#####`.exceptModule(module)`
|
##### `.exceptModule(module)`
|
||||||
***module* Module Object** - The reference to a module you want to remove from the results.
|
***module* Module Object** - The reference to a module you want to remove from the results.
|
||||||
**Returns Array** - An array with module instances.<br>
|
**Returns Array** - An array with module instances.<br>
|
||||||
|
|
||||||
@@ -601,7 +680,7 @@ Of course, you can combine all of the above filters:
|
|||||||
var modules = MM.getModules().withClass('classname1').exceptwithClass('classname2').exceptModule(aModule);
|
var modules = MM.getModules().withClass('classname1').exceptwithClass('classname2').exceptModule(aModule);
|
||||||
````
|
````
|
||||||
|
|
||||||
#####`.enumerate(callback)`
|
##### `.enumerate(callback)`
|
||||||
***callback* Function(module)** - The callback run on every instance.
|
***callback* Function(module)** - The callback run on every instance.
|
||||||
|
|
||||||
If you want to perform an action on all selected modules, you can use the `enumerate` function:
|
If you want to perform an action on all selected modules, you can use the `enumerate` function:
|
||||||
|
@@ -43,10 +43,11 @@ self.sendNotification("SHOW_ALERT", {});
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Notification params
|
### Notification params
|
||||||
| Option | Description
|
| Option | Description
|
||||||
| --------- | -----------
|
| ------------------ | -----------
|
||||||
| `title` | The title of the notification. <br><br> **Possible values:** `text` or `html`
|
| `title` | The title of the notification. <br><br> **Possible values:** `text` or `html`
|
||||||
| `message` | The message of the notification. <br><br> **Possible values:** `text` or `html`
|
| `message` | The message of the notification. <br><br> **Possible values:** `text` or `html`
|
||||||
|
| `timer` (optional) | How long the notification should stay visible in ms. <br> If absent, the default `display_time` is used. <br> **Possible values:** `int` `float`
|
||||||
|
|
||||||
|
|
||||||
### Alert params
|
### Alert params
|
||||||
@@ -60,5 +61,5 @@ self.sendNotification("SHOW_ALERT", {});
|
|||||||
| `timer` (optional) | How long the alert should stay visible in ms. <br> **Important:** If you do not use the `timer`, it is your duty to hide the alert by using `self.sendNotification("HIDE_ALERT");`! <br><br>**Possible values:** `int` `float` <br> **Default value:** `none`
|
| `timer` (optional) | How long the alert should stay visible in ms. <br> **Important:** If you do not use the `timer`, it is your duty to hide the alert by using `self.sendNotification("HIDE_ALERT");`! <br><br>**Possible values:** `int` `float` <br> **Default value:** `none`
|
||||||
|
|
||||||
## Open Source Licenses
|
## Open Source Licenses
|
||||||
###[NotificationStyles](https://github.com/codrops/NotificationStyles)
|
### [NotificationStyles](https://github.com/codrops/NotificationStyles)
|
||||||
See [ympanus.net](http://tympanus.net/codrops/licensing/) for license.
|
See [tympanus.net](http://tympanus.net/codrops/licensing/) for license.
|
||||||
|
@@ -24,33 +24,34 @@ Module.register("alert",{
|
|||||||
return ["classie.js", "modernizr.custom.js", "notificationFx.js"];
|
return ["classie.js", "modernizr.custom.js", "notificationFx.js"];
|
||||||
},
|
},
|
||||||
getStyles: function() {
|
getStyles: function() {
|
||||||
return ["ns-default.css"];
|
return ["ns-default.css", "font-awesome.css"];
|
||||||
},
|
},
|
||||||
// Define required translations.
|
// Define required translations.
|
||||||
getTranslations: function() {
|
getTranslations: function() {
|
||||||
return {
|
return {
|
||||||
en: "translations/en.json",
|
en: "translations/en.json",
|
||||||
de: "translations/de.json"
|
de: "translations/de.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;}
|
||||||
msg = "";
|
msg = "";
|
||||||
if (message.title) {
|
if (message.title) {
|
||||||
msg += "<span class='thin' style='line-height: 35px; font-size:24px' color='#4A4A4A'>" + message.title + "</span>";
|
msg += "<span class='thin dimmed medium'>" + message.title + "</span>";
|
||||||
}
|
}
|
||||||
if (message.message){
|
if (message.message){
|
||||||
if (msg != ""){
|
if (msg !== ""){
|
||||||
msg+= "<br />";
|
msg+= "<br />";
|
||||||
}
|
}
|
||||||
msg += "<span class='light' style='font-size:28px;line-height: 30px;'>" + message.message + "</span>";
|
msg += "<span class='light bright small'>" + message.message + "</span>";
|
||||||
}
|
}
|
||||||
|
|
||||||
new NotificationFx({
|
new NotificationFx({
|
||||||
message: msg,
|
message: msg,
|
||||||
layout: "growl",
|
layout: "growl",
|
||||||
effect: this.config.effect,
|
effect: this.config.effect,
|
||||||
ttl: this.config.display_time
|
ttl: message.timer !== undefined ? message.timer : this.config.display_time
|
||||||
}).show();
|
}).show();
|
||||||
},
|
},
|
||||||
show_alert: function(params, sender) {
|
show_alert: function(params, sender) {
|
||||||
@@ -62,9 +63,9 @@ Module.register("alert",{
|
|||||||
params.imageUrl = null;
|
params.imageUrl = null;
|
||||||
image = "";
|
image = "";
|
||||||
} 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='" + "fa fa-" + params.imageFA + "' style='margin-bottom: 10px;color: #fff;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
|
||||||
var overlay = document.createElement("div");
|
var overlay = document.createElement("div");
|
||||||
@@ -78,16 +79,16 @@ Module.register("alert",{
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Display title and message only if they are provided in notification parameters
|
//Display title and message only if they are provided in notification parameters
|
||||||
message ="";
|
var message = "";
|
||||||
if (params.title) {
|
if (params.title) {
|
||||||
message += "<span class='light' style='line-height: 35px; font-size:30px' color='#4A4A4A'>" + params.title + "</span>"
|
message += "<span class='light dimmed medium'>" + params.title + "</span>";
|
||||||
}
|
}
|
||||||
if (params.message) {
|
if (params.message) {
|
||||||
if (message != ""){
|
if (message !== ""){
|
||||||
message += "<br />";
|
message += "<br />";
|
||||||
}
|
}
|
||||||
|
|
||||||
message += "<span class='thin' style='font-size:22px;line-height: 30px;'>" + params.message + "</span>";
|
message += "<span class='thin bright small'>" + params.message + "</span>";
|
||||||
}
|
}
|
||||||
|
|
||||||
//Store alert in this.alerts
|
//Store alert in this.alerts
|
||||||
@@ -109,27 +110,29 @@ Module.register("alert",{
|
|||||||
},
|
},
|
||||||
hide_alert: function(sender) {
|
hide_alert: function(sender) {
|
||||||
//Dismiss alert and remove from this.alerts
|
//Dismiss alert and remove from this.alerts
|
||||||
this.alerts[sender.name].dismiss();
|
if (this.alerts[sender.name]) {
|
||||||
this.alerts[sender.name] = null;
|
this.alerts[sender.name].dismiss();
|
||||||
//Remove overlay
|
this.alerts[sender.name] = null;
|
||||||
var overlay = document.getElementById("overlay");
|
//Remove overlay
|
||||||
overlay.parentNode.removeChild(overlay);
|
var overlay = document.getElementById("overlay");
|
||||||
|
overlay.parentNode.removeChild(overlay);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
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
|
||||||
var sheet = document.createElement("style");
|
var sheet = document.createElement("style");
|
||||||
if (pos == "center") {sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}";}
|
if (pos === "center") {sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}";}
|
||||||
if (pos == "right") {sheet.innerHTML = ".ns-box {margin-left: auto;text-align: right;}";}
|
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;}";}
|
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") {
|
||||||
this.show_notification(payload);
|
this.show_notification(payload);
|
||||||
}
|
}
|
||||||
} else if (notification === "HIDE_ALERT") {
|
} else if (notification === "HIDE_ALERT") {
|
||||||
@@ -140,7 +143,7 @@ Module.register("alert",{
|
|||||||
this.alerts = {};
|
this.alerts = {};
|
||||||
this.setPosition(this.config.position);
|
this.setPosition(this.config.position);
|
||||||
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{
|
||||||
@@ -149,5 +152,4 @@ Module.register("alert",{
|
|||||||
}
|
}
|
||||||
Log.info("Starting module: " + this.name);
|
Log.info("Starting module: " + this.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
|||||||
/* Based on work by http://tympanus.net/codrops/licensing/ */
|
/* Based on work by http://tympanus.net/codrops/licensing/ */
|
||||||
|
|
||||||
.ns-box {
|
.ns-box {
|
||||||
background: #fff;
|
background-color: rgba(0, 0, 0, 0.93);
|
||||||
padding: 17px;
|
padding: 17px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -12,7 +12,10 @@
|
|||||||
display: table;
|
display: table;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
border-width: 1px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ns-alert {
|
.ns-alert {
|
||||||
|
4
modules/default/alert/translations/bg.json
Normal file
4
modules/default/alert/translations/bg.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"sysTitle": "MagicMirror нотификация",
|
||||||
|
"welcome": "Добре дошли, стартирането беше успешно"
|
||||||
|
}
|
4
modules/default/alert/translations/fr.json
Normal file
4
modules/default/alert/translations/fr.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"sysTitle": "MagicMirror Notification",
|
||||||
|
"welcome": "Bienvenue, le démarrage a été un succès!"
|
||||||
|
}
|
4
modules/default/alert/translations/nl.json
Normal file
4
modules/default/alert/translations/nl.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"sysTitle": "MagicMirror Notificatie",
|
||||||
|
"welcome": "Welkom, Succesvol gestart!"
|
||||||
|
}
|
24
modules/default/calendar/README.md
Normal file → Executable file
24
modules/default/calendar/README.md
Normal file → Executable file
@@ -30,32 +30,45 @@ The following properties can be configured:
|
|||||||
| `maximumNumberOfDays` | The maximum number of days in the future. <br><br> **Default value:** `365`
|
| `maximumNumberOfDays` | The maximum number of days in the future. <br><br> **Default value:** `365`
|
||||||
| `displaySymbol` | Display a symbol in front of an entry. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
| `displaySymbol` | Display a symbol in front of an entry. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||||
| `defaultSymbol` | The default symbol. <br><br> **Possible values:** See [Font Awsome](http://fontawesome.io/icons/) website. <br> **Default value:** `calendar`
|
| `defaultSymbol` | The default symbol. <br><br> **Possible values:** See [Font Awsome](http://fontawesome.io/icons/) website. <br> **Default value:** `calendar`
|
||||||
|
| `showLocation` | Whether to show event locations. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||||
| `maxTitleLength` | The maximum title length. <br><br> **Possible values:** `10` - `50` <br> **Default value:** `25`
|
| `maxTitleLength` | The maximum title length. <br><br> **Possible values:** `10` - `50` <br> **Default value:** `25`
|
||||||
| `wrapEvents` | Wrap event titles to multiple lines. Breaks lines at the length defined by `maxTitleLength`. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
| `wrapEvents` | Wrap event titles to multiple lines. Breaks lines at the length defined by `maxTitleLength`. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||||
|
| `maxTitleLines` | The maximum number of lines a title will wrap vertically before being cut (Only enabled if `wrapEvents` is also enabled). <br><br> **Possible values:** `0` - `10` <br> **Default value:** `3`
|
||||||
| `fetchInterval` | How often does the content needs to be fetched? (Milliseconds) <br><br> **Possible values:** `1000` - `86400000` <br> **Default value:** `300000` (5 minutes)
|
| `fetchInterval` | How often does the content needs to be fetched? (Milliseconds) <br><br> **Possible values:** `1000` - `86400000` <br> **Default value:** `300000` (5 minutes)
|
||||||
| `animationSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:**`0` - `5000` <br> **Default value:** `2000` (2 seconds)
|
| `animationSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:** `0` - `5000` <br> **Default value:** `2000` (2 seconds)
|
||||||
| `fade` | Fade the future events to black. (Gradient) <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
| `fade` | Fade the future events to black. (Gradient) <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||||
| `fadePoint` | Where to start fade? <br><br> **Possible values:** `0` (top of the list) - `1` (bottom of list) <br> **Default value:** `0.25`
|
| `fadePoint` | Where to start fade? <br><br> **Possible values:** `0` (top of the list) - `1` (bottom of list) <br> **Default value:** `0.25`
|
||||||
|
| `tableClass` | Name of the classes issued from `main.css`. <br><br> **Possible values:** xsmall, small, medium, large, xlarge. <br> **Default value:** _small._
|
||||||
| `calendars` | The list of calendars. <br><br> **Possible values:** An array, see _calendar configuration_ below. <br> **Default value:** _An example calendar._
|
| `calendars` | The list of calendars. <br><br> **Possible values:** An array, see _calendar configuration_ below. <br> **Default value:** _An example calendar._
|
||||||
| `titleReplace` | An object of textual replacements applied to the tile of the event. This allow to remove or replace certains words in the title. <br><br> **Example:** `{'Birthday of ' : '', 'foo':'bar'}` <br> **Default value:** `{ "De verjaardag van ": "", "'s birthday": "" }`
|
| `titleReplace` | An object of textual replacements applied to the tile of the event. This allow to remove or replace certains words in the title. <br><br> **Example:** `{'Birthday of ' : '', 'foo':'bar'}` <br> **Default value:** `{ "De verjaardag van ": "", "'s birthday": "" }`
|
||||||
| `displayRepeatingCountTitle` | Show count title for yearly repeating events (e.g. "X. Birthday", "X. Anniversary") <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
| `displayRepeatingCountTitle` | Show count title for yearly repeating events (e.g. "X. Birthday", "X. Anniversary") <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||||
| `dateFormat` | Format to use for the date of events (when using absolute dates) <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `MMM Do` (e.g. Jan 18th)
|
| `dateFormat` | Format to use for the date of events (when using absolute dates) <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `MMM Do` (e.g. Jan 18th)
|
||||||
| `timeFormat` | Display event times as absolute dates, or relative time <br><br> **Possible values:** `absolute` or `relative` <br> **Default value:** `relative`
|
| `dateEndFormat` | Format to use for the end time of events <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `HH:mm` (e.g. 16:30)
|
||||||
|
| `showEnd` | Show end time of events <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||||
|
| `fullDayEventDateFormat` | Format to use for the date of full day events (when using absolute dates) <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `MMM Do` (e.g. Jan 18th)
|
||||||
|
| `timeFormat` | Display event times as absolute dates, or relative time, or using absolute date headers with times for each event next to it <br><br> **Possible values:** `absolute` or `relative` or `dateheaders` <br> **Default value:** `relative`
|
||||||
|
| `showEnd` | Display the end of a date as well <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||||
| `getRelative` | How much time (in hours) should be left until calendar events start getting relative? <br><br> **Possible values:** `0` (events stay absolute) - `48` (48 hours before the event starts) <br> **Default value:** `6`
|
| `getRelative` | How much time (in hours) should be left until calendar events start getting relative? <br><br> **Possible values:** `0` (events stay absolute) - `48` (48 hours before the event starts) <br> **Default value:** `6`
|
||||||
| `urgency` | When using a timeFormat of `absolute`, the `urgency` setting allows you to display events within a specific time frame as `relative`. This allows events within a certain time frame to be displayed as relative (in xx days) while others are displayed as absolute dates <br><br> **Possible values:** a positive integer representing the number of days for which you want a relative date, for example `7` (for 7 days) <br><br> **Default value:** `7`
|
| `urgency` | When using a timeFormat of `absolute`, the `urgency` setting allows you to display events within a specific time frame as `relative`. This allows events within a certain time frame to be displayed as relative (in xx days) while others are displayed as absolute dates <br><br> **Possible values:** a positive integer representing the number of days for which you want a relative date, for example `7` (for 7 days) <br><br> **Default value:** `7`
|
||||||
| `broadcastEvents` | If this property is set to true, the calendar will broadcast all the events to all other modules with the notification message: `CALENDAR_EVENTS`. The event objects are stored in an array and contain the following fields: `title`, `startDate`, `endDate`, `fullDayEvent`, `location` and `geo`. <br><br> **Possible values:** `true`, `false` <br><br> **Default value:** `true`
|
| `broadcastEvents` | If this property is set to true, the calendar will broadcast all the events to all other modules with the notification message: `CALENDAR_EVENTS`. The event objects are stored in an array and contain the following fields: `title`, `startDate`, `endDate`, `fullDayEvent`, `location` and `geo`. <br><br> **Possible values:** `true`, `false` <br><br> **Default value:** `true`
|
||||||
| `hidePrivate` | Hides private calendar events. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
| `hidePrivate` | Hides private calendar events. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||||
| `excludedEvents` | An array of words / phrases from event titles that will be excluded from being shown. <br><br> **Example:** `['Birthday', 'Hide This Event']` <br> **Default value:** `[]`
|
| `hideOngoing` | Hides calendar events that have already started. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||||
|
| `excludedEvents` | An array of words / phrases from event titles that will be excluded from being shown. <br><br>Additionally advanced filter objects can be passed in. Below is the configuration for the advance filtering object.<br>**Required**<br>`filterBy` - string used to determine if filter is applied.<br>**Optional**<br>`until` - Time before an event to display it Ex: [`'3 days'`, `'2 months'`, `'1 week'`]<br>`caseSensitive` - By default, excludedEvents are case insensitive, set this to true to enforce case sensitivity<br>`regex` - set to `true` if filterBy is a regex. For those not familiar with regex it is used for pattern matching, please see [here](https://regexr.com/) for more info.<br><br> **Example:** `['Birthday', 'Hide This Event', {filterBy: 'Payment', until: '6 days', caseSensitive: true}, {filterBy: '^[0-9]{1,}.*', regex: true}]` <br> **Default value:** `[]`
|
||||||
|
| `broadcastPastEvents` | If this is set to true, events from the past `maximumNumberOfDays` will be included in event broadcasts <br> **Default value:** `false`
|
||||||
|
| `sliceMultiDayEvents` | If this is set to true, events exceeding at least one midnight will be sliced into separate events including a counter like (1/2). This is especially helpful in "dateheaders" mode. Events will be sliced at midnight, end time for all events but the last will be 23:59 <br> **Default value:** `true`
|
||||||
|
| `nextDaysRelative ` | If this is set to true, the appointments of today and tomorrow are displayed relatively, even if the timeformat is set to absolute. <br> **Default value:** `false`
|
||||||
|
|
||||||
### Calendar configuration
|
### Calendar configuration
|
||||||
|
|
||||||
The `calendars` property contains an array of the configured calendars.
|
The `calendars` property contains an array of the configured calendars.
|
||||||
The `colored` property gives the option for an individual color for each calendar.
|
The `colored` property gives the option for an individual color for each calendar.
|
||||||
|
The `coloredSymbolOnly` property will apply color to the symbol only, not the whole line. This is only applicable when `colored` is also enabled.
|
||||||
|
|
||||||
#### Default value:
|
#### Default value:
|
||||||
````javascript
|
````javascript
|
||||||
config: {
|
config: {
|
||||||
colored: false,
|
colored: false,
|
||||||
|
coloredSymbolOnly: false,
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
url: 'http://www.calendarlabs.com/templates/ical/US-Holidays.ics',
|
url: 'http://www.calendarlabs.com/templates/ical/US-Holidays.ics',
|
||||||
@@ -79,7 +92,12 @@ config: {
|
|||||||
| `repeatingCountTitle` | The count title for yearly repating events in this calendar. <br><br> **Example:** `'Birthday'`
|
| `repeatingCountTitle` | The count title for yearly repating events in this calendar. <br><br> **Example:** `'Birthday'`
|
||||||
| `maximumEntries` | The maximum number of events shown. Overrides global setting. **Possible values:** `0` - `100`
|
| `maximumEntries` | The maximum number of events shown. Overrides global setting. **Possible values:** `0` - `100`
|
||||||
| `maximumNumberOfDays` | The maximum number of days in the future. Overrides global setting
|
| `maximumNumberOfDays` | The maximum number of days in the future. Overrides global setting
|
||||||
|
| `name` | The name of the calendar. Included in event broadcasts as `calendarName`.
|
||||||
| `auth` | The object containing options for authentication against the calendar.
|
| `auth` | The object containing options for authentication against the calendar.
|
||||||
|
| `symbolClass` | Add a class to the cell of symbol.
|
||||||
|
| `titleClass` | Add a class to the title's cell.
|
||||||
|
| `timeClass` | Add a class to the time's cell.
|
||||||
|
| `broadcastPastEvents` | Whether to include past events from this calendar. Overrides global setting
|
||||||
|
|
||||||
|
|
||||||
#### Calendar authentication options:
|
#### Calendar authentication options:
|
||||||
|
497
modules/default/calendar/calendar.js
Normal file → Executable file
497
modules/default/calendar/calendar.js
Normal file → Executable file
@@ -15,20 +15,28 @@ Module.register("calendar", {
|
|||||||
maximumNumberOfDays: 365,
|
maximumNumberOfDays: 365,
|
||||||
displaySymbol: true,
|
displaySymbol: true,
|
||||||
defaultSymbol: "calendar", // Fontawesome Symbol see http://fontawesome.io/cheatsheet/
|
defaultSymbol: "calendar", // Fontawesome Symbol see http://fontawesome.io/cheatsheet/
|
||||||
|
showLocation: false,
|
||||||
displayRepeatingCountTitle: false,
|
displayRepeatingCountTitle: false,
|
||||||
defaultRepeatingCountTitle: "",
|
defaultRepeatingCountTitle: "",
|
||||||
maxTitleLength: 25,
|
maxTitleLength: 25,
|
||||||
wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
|
wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
|
||||||
|
maxTitleLines: 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,
|
||||||
urgency: 7,
|
urgency: 7,
|
||||||
timeFormat: "relative",
|
timeFormat: "relative",
|
||||||
dateFormat: "MMM Do",
|
dateFormat: "MMM Do",
|
||||||
|
dateEndFormat: "LT",
|
||||||
|
fullDayEventDateFormat: "MMM Do",
|
||||||
|
showEnd: false,
|
||||||
getRelative: 6,
|
getRelative: 6,
|
||||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||||
hidePrivate: false,
|
hidePrivate: false,
|
||||||
|
hideOngoing: false,
|
||||||
colored: false,
|
colored: false,
|
||||||
|
coloredSymbolOnly: false,
|
||||||
|
tableClass: "small",
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
symbol: "calendar",
|
symbol: "calendar",
|
||||||
@@ -40,7 +48,10 @@ Module.register("calendar", {
|
|||||||
"'s birthday": ""
|
"'s birthday": ""
|
||||||
},
|
},
|
||||||
broadcastEvents: true,
|
broadcastEvents: true,
|
||||||
excludedEvents: []
|
excludedEvents: [],
|
||||||
|
sliceMultiDayEvents: false,
|
||||||
|
broadcastPastEvents: false,
|
||||||
|
nextDaysRelative: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// Define required scripts.
|
// Define required scripts.
|
||||||
@@ -66,7 +77,7 @@ Module.register("calendar", {
|
|||||||
Log.log("Starting module: " + this.name);
|
Log.log("Starting module: " + this.name);
|
||||||
|
|
||||||
// Set locale.
|
// Set locale.
|
||||||
moment.locale(config.language);
|
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
|
||||||
|
|
||||||
for (var c in this.config.calendars) {
|
for (var c in this.config.calendars) {
|
||||||
var calendar = this.config.calendars[c];
|
var calendar = this.config.calendars[c];
|
||||||
@@ -74,18 +85,37 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
var calendarConfig = {
|
var calendarConfig = {
|
||||||
maximumEntries: calendar.maximumEntries,
|
maximumEntries: calendar.maximumEntries,
|
||||||
maximumNumberOfDays: calendar.maximumNumberOfDays
|
maximumNumberOfDays: calendar.maximumNumberOfDays,
|
||||||
|
broadcastPastEvents: calendar.broadcastPastEvents,
|
||||||
};
|
};
|
||||||
|
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
|
||||||
|
calendarConfig.symbolClass = "";
|
||||||
|
}
|
||||||
|
if (calendar.titleClass === "undefined" || calendar.titleClass === null) {
|
||||||
|
calendarConfig.titleClass = "";
|
||||||
|
}
|
||||||
|
if (calendar.timeClass === "undefined" || calendar.timeClass === null) {
|
||||||
|
calendarConfig.timeClass = "";
|
||||||
|
}
|
||||||
|
|
||||||
// we check user and password here for backwards compatibility with old configs
|
// we check user and password here for backwards compatibility with old configs
|
||||||
if(calendar.user && calendar.pass){
|
if(calendar.user && calendar.pass) {
|
||||||
|
Log.warn("Deprecation warning: Please update your calendar authentication configuration.");
|
||||||
|
Log.warn("https://github.com/MichMich/MagicMirror/tree/v2.1.2/modules/default/calendar#calendar-authentication-options");
|
||||||
calendar.auth = {
|
calendar.auth = {
|
||||||
user: calendar.user,
|
user: calendar.user,
|
||||||
pass: calendar.pass
|
pass: calendar.pass
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||||
|
|
||||||
|
// Trigger ADD_CALENDAR every fetchInterval to make sure there is always a calendar
|
||||||
|
// fetcher running on the server side.
|
||||||
|
var self = this;
|
||||||
|
setInterval(function() {
|
||||||
|
self.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||||
|
}, self.config.fetchInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.calendarData = {};
|
this.calendarData = {};
|
||||||
@@ -105,6 +135,7 @@ Module.register("calendar", {
|
|||||||
}
|
}
|
||||||
} else if (notification === "FETCH_ERROR") {
|
} else if (notification === "FETCH_ERROR") {
|
||||||
Log.error("Calendar Error. Could not fetch calendar: " + payload.url);
|
Log.error("Calendar Error. Could not fetch calendar: " + payload.url);
|
||||||
|
this.loaded = true;
|
||||||
} else if (notification === "INCORRECT_URL") {
|
} else if (notification === "INCORRECT_URL") {
|
||||||
Log.error("Calendar Error. Incorrect url: " + payload.url);
|
Log.error("Calendar Error. Incorrect url: " + payload.url);
|
||||||
} else {
|
} else {
|
||||||
@@ -119,33 +150,51 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
var events = this.createEventList();
|
var events = this.createEventList();
|
||||||
var wrapper = document.createElement("table");
|
var wrapper = document.createElement("table");
|
||||||
wrapper.className = "small";
|
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 = "small dimmed";
|
wrapper.className = this.config.tableClass + " dimmed";
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.config.fade && this.config.fadePoint < 1) {
|
||||||
|
if (this.config.fadePoint < 0) {
|
||||||
|
this.config.fadePoint = 0;
|
||||||
|
}
|
||||||
|
var startFade = events.length * this.config.fadePoint;
|
||||||
|
var fadeSteps = events.length - startFade;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentFadeStep = 0;
|
||||||
|
var lastSeenDate = "";
|
||||||
|
|
||||||
for (var e in events) {
|
for (var e in events) {
|
||||||
var event = events[e];
|
var event = events[e];
|
||||||
|
var dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
|
||||||
|
if(this.config.timeFormat === "dateheaders"){
|
||||||
|
if(lastSeenDate !== dateAsString){
|
||||||
|
var dateRow = document.createElement("tr");
|
||||||
|
dateRow.className = "normal";
|
||||||
|
var dateCell = document.createElement("td");
|
||||||
|
|
||||||
var excluded = false;
|
dateCell.colSpan = "3";
|
||||||
for (var f in this.config.excludedEvents) {
|
dateCell.innerHTML = dateAsString;
|
||||||
var filter = this.config.excludedEvents[f];
|
dateRow.appendChild(dateCell);
|
||||||
if (event.title.toLowerCase().includes(filter.toLowerCase())) {
|
wrapper.appendChild(dateRow);
|
||||||
excluded = true;
|
|
||||||
break;
|
if (e >= startFade) { //fading
|
||||||
|
currentFadeStep = e - startFade;
|
||||||
|
dateRow.style.opacity = 1 - (1 / fadeSteps * currentFadeStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSeenDate = dateAsString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (excluded) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var eventWrapper = document.createElement("tr");
|
var eventWrapper = document.createElement("tr");
|
||||||
|
|
||||||
if (this.config.colored) {
|
if (this.config.colored && !this.config.coloredSymbolOnly) {
|
||||||
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +202,14 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
if (this.config.displaySymbol) {
|
if (this.config.displaySymbol) {
|
||||||
var symbolWrapper = document.createElement("td");
|
var symbolWrapper = document.createElement("td");
|
||||||
symbolWrapper.className = "symbol align-right";
|
|
||||||
|
if (this.config.colored && this.config.coloredSymbolOnly) {
|
||||||
|
symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
var symbolClass = this.symbolClassForUrl(event.url);
|
||||||
|
symbolWrapper.className = "symbol align-right " + symbolClass;
|
||||||
|
|
||||||
var symbols = this.symbolsForUrl(event.url);
|
var symbols = this.symbolsForUrl(event.url);
|
||||||
if(typeof symbols === "string") {
|
if(typeof symbols === "string") {
|
||||||
symbols = [symbols];
|
symbols = [symbols];
|
||||||
@@ -161,20 +217,23 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
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-" + symbols[i];
|
symbol.className = "fa fa-fw fa-" + symbols[i];
|
||||||
if(i > 0){
|
if(i > 0){
|
||||||
symbol.style.paddingLeft = "5px";
|
symbol.style.paddingLeft = "5px";
|
||||||
}
|
}
|
||||||
symbolWrapper.appendChild(symbol);
|
symbolWrapper.appendChild(symbol);
|
||||||
}
|
}
|
||||||
eventWrapper.appendChild(symbolWrapper);
|
eventWrapper.appendChild(symbolWrapper);
|
||||||
|
} else if(this.config.timeFormat === "dateheaders"){
|
||||||
|
var blankCell = document.createElement("td");
|
||||||
|
blankCell.innerHTML = " ";
|
||||||
|
eventWrapper.appendChild(blankCell);
|
||||||
}
|
}
|
||||||
|
|
||||||
var titleWrapper = document.createElement("td"),
|
var titleWrapper = document.createElement("td"),
|
||||||
repeatingCountTitle = "";
|
repeatingCountTitle = "";
|
||||||
|
|
||||||
|
if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) {
|
||||||
if (this.config.displayRepeatingCountTitle) {
|
|
||||||
|
|
||||||
repeatingCountTitle = this.countTitleForUrl(event.url);
|
repeatingCountTitle = this.countTitleForUrl(event.url);
|
||||||
|
|
||||||
@@ -188,103 +247,165 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
titleWrapper.innerHTML = this.titleTransform(event.title) + repeatingCountTitle;
|
titleWrapper.innerHTML = this.titleTransform(event.title) + repeatingCountTitle;
|
||||||
|
|
||||||
|
var titleClass = this.titleClassForUrl(event.url);
|
||||||
|
|
||||||
if (!this.config.colored) {
|
if (!this.config.colored) {
|
||||||
titleWrapper.className = "title bright";
|
titleWrapper.className = "title bright " + titleClass;
|
||||||
} else {
|
} else {
|
||||||
titleWrapper.className = "title";
|
titleWrapper.className = "title " + titleClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
eventWrapper.appendChild(titleWrapper);
|
if(this.config.timeFormat === "dateheaders"){
|
||||||
|
|
||||||
|
if (event.fullDayEvent) {
|
||||||
|
titleWrapper.colSpan = "2";
|
||||||
|
titleWrapper.align = "left";
|
||||||
|
|
||||||
var timeWrapper = document.createElement("td");
|
|
||||||
//console.log(event.today);
|
|
||||||
var now = new Date();
|
|
||||||
// Define second, minute, hour, and day variables
|
|
||||||
var oneSecond = 1000; // 1,000 milliseconds
|
|
||||||
var oneMinute = oneSecond * 60;
|
|
||||||
var oneHour = oneMinute * 60;
|
|
||||||
var oneDay = oneHour * 24;
|
|
||||||
if (event.fullDayEvent) {
|
|
||||||
if (event.today) {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
|
||||||
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
|
||||||
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
|
|
||||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
/* Check to see if the user displays absolute or relative dates with their events
|
|
||||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
var timeClass = this.timeClassForUrl(event.url);
|
||||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
var timeWrapper = document.createElement("td");
|
||||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
timeWrapper.className = "time light " + timeClass;
|
||||||
*
|
timeWrapper.align = "left";
|
||||||
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
|
timeWrapper.style.paddingLeft = "2px";
|
||||||
*/
|
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
|
||||||
if (this.config.timeFormat === "absolute") {
|
eventWrapper.appendChild(timeWrapper);
|
||||||
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) {
|
titleWrapper.align = "right";
|
||||||
// This event falls within the config.urgency period that the user has set
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventWrapper.appendChild(titleWrapper);
|
||||||
} else {
|
} else {
|
||||||
if (event.startDate >= new Date()) {
|
var timeWrapper = document.createElement("td");
|
||||||
if (event.startDate - now < 2 * oneDay) {
|
|
||||||
// This event is within the next 48 hours (2 days)
|
eventWrapper.appendChild(titleWrapper);
|
||||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
//console.log(event.today);
|
||||||
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
|
var now = new Date();
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
// Define second, minute, hour, and day variables
|
||||||
|
var oneSecond = 1000; // 1,000 milliseconds
|
||||||
|
var oneMinute = oneSecond * 60;
|
||||||
|
var oneHour = oneMinute * 60;
|
||||||
|
var oneDay = oneHour * 24;
|
||||||
|
if (event.fullDayEvent) {
|
||||||
|
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||||
|
event.endDate -= oneSecond;
|
||||||
|
if (event.today) {
|
||||||
|
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
||||||
|
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
|
||||||
|
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
||||||
|
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
|
||||||
|
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||||
|
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||||
} else {
|
} else {
|
||||||
// Otherwise just say 'Today/Tomorrow at such-n-such time'
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* Check to see if the user displays absolute or relative dates with their events
|
/* Check to see if the user displays absolute or relative dates with their events
|
||||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
* Also check to see if an event is happening within an 'urgency' time frameElement
|
||||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
||||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
||||||
*
|
*
|
||||||
* 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").from(moment().format("YYYYMMDD")));
|
||||||
} else {
|
} else {
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(this.config.showEnd){
|
||||||
|
timeWrapper.innerHTML += "-" ;
|
||||||
|
timeWrapper.innerHTML += this.capFirst(moment(event.endDate , "x").format(this.config.fullDayEventDateFormat));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
timeWrapper.innerHTML = this.capFirst(this.translate("RUNNING")) + " " + moment(event.endDate, "x").fromNow(true);
|
if (event.startDate >= new Date()) {
|
||||||
|
if (event.startDate - now < 2 * oneDay) {
|
||||||
|
// This event is within the next 48 hours (2 days)
|
||||||
|
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||||
|
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
|
||||||
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||||
|
} else {
|
||||||
|
if(this.config.timeFormat === "absolute" && !this.config.nextDaysRelative) {
|
||||||
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||||
|
} else {
|
||||||
|
// Otherwise just say 'Today/Tomorrow at such-n-such time'
|
||||||
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Check to see if the user displays absolute or relative dates with their events
|
||||||
|
* Also check to see if an event is happening within an 'urgency' time frameElement
|
||||||
|
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
||||||
|
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
||||||
|
*
|
||||||
|
* 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.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) {
|
||||||
|
// This event falls within the config.urgency period that the user has set
|
||||||
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||||
|
} else {
|
||||||
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
timeWrapper.innerHTML = this.capFirst(
|
||||||
|
this.translate("RUNNING", {
|
||||||
|
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||||
|
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.config.showEnd) {
|
||||||
|
timeWrapper.innerHTML += "-";
|
||||||
|
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
|
||||||
|
//console.log(event);
|
||||||
|
var timeClass = this.timeClassForUrl(event.url);
|
||||||
|
timeWrapper.className = "time light " + timeClass;
|
||||||
|
eventWrapper.appendChild(timeWrapper);
|
||||||
}
|
}
|
||||||
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
|
|
||||||
//console.log(event);
|
|
||||||
timeWrapper.className = "time light";
|
|
||||||
eventWrapper.appendChild(timeWrapper);
|
|
||||||
|
|
||||||
wrapper.appendChild(eventWrapper);
|
wrapper.appendChild(eventWrapper);
|
||||||
|
|
||||||
// Create fade effect.
|
// Create fade effect.
|
||||||
if (this.config.fade && this.config.fadePoint < 1) {
|
if (e >= startFade) {
|
||||||
if (this.config.fadePoint < 0) {
|
currentFadeStep = e - startFade;
|
||||||
this.config.fadePoint = 0;
|
eventWrapper.style.opacity = 1 - (1 / fadeSteps * currentFadeStep);
|
||||||
}
|
}
|
||||||
var startingPoint = events.length * this.config.fadePoint;
|
|
||||||
var steps = events.length - startingPoint;
|
if (this.config.showLocation) {
|
||||||
if (e >= startingPoint) {
|
if (event.location !== false) {
|
||||||
var currentStep = e - startingPoint;
|
var locationRow = document.createElement("tr");
|
||||||
eventWrapper.style.opacity = 1 - (1 / steps * currentStep);
|
locationRow.className = "normal xsmall light";
|
||||||
|
|
||||||
|
if (this.config.displaySymbol) {
|
||||||
|
var symbolCell = document.createElement("td");
|
||||||
|
locationRow.appendChild(symbolCell);
|
||||||
|
}
|
||||||
|
|
||||||
|
var descCell = document.createElement("td");
|
||||||
|
descCell.className = "location";
|
||||||
|
descCell.colSpan = "2";
|
||||||
|
descCell.innerHTML = event.location;
|
||||||
|
locationRow.appendChild(descCell);
|
||||||
|
|
||||||
|
wrapper.appendChild(locationRow);
|
||||||
|
|
||||||
|
if (e >= startFade) {
|
||||||
|
currentFadeStep = e - startFade;
|
||||||
|
locationRow.style.opacity = 1 - (1 / fadeSteps * currentFadeStep);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,6 +413,31 @@ Module.register("calendar", {
|
|||||||
return wrapper;
|
return wrapper;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
|
||||||
|
* corresponding timeformat to be used in the calendar display. If no number is given (or otherwise invalid input)
|
||||||
|
* it will a localeSpecification object with the system locale time format.
|
||||||
|
*
|
||||||
|
* @param {number} timeFormat Specifies either 12 or 24 hour time format
|
||||||
|
* @returns {moment.LocaleSpecification}
|
||||||
|
*/
|
||||||
|
getLocaleSpecification: function(timeFormat) {
|
||||||
|
switch (timeFormat) {
|
||||||
|
case 12: {
|
||||||
|
return { longDateFormat: {LT: "h:mm A"} };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 24: {
|
||||||
|
return { longDateFormat: {LT: "HH:mm"} };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return { longDateFormat: {LT: moment.localeData().longDateFormat("LT")} };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/* hasCalendarURL(url)
|
/* hasCalendarURL(url)
|
||||||
* Check if this config contains the calendar url.
|
* Check if this config contains the calendar url.
|
||||||
*
|
*
|
||||||
@@ -318,29 +464,81 @@ Module.register("calendar", {
|
|||||||
createEventList: function () {
|
createEventList: function () {
|
||||||
var events = [];
|
var events = [];
|
||||||
var today = moment().startOf("day");
|
var today = moment().startOf("day");
|
||||||
|
var now = new Date();
|
||||||
|
var future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
||||||
for (var c in this.calendarData) {
|
for (var c in this.calendarData) {
|
||||||
var calendar = this.calendarData[c];
|
var calendar = this.calendarData[c];
|
||||||
for (var e in calendar) {
|
for (var e in calendar) {
|
||||||
var event = calendar[e];
|
var event = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
||||||
|
if(event.endDate < now) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if(this.config.hidePrivate) {
|
if(this.config.hidePrivate) {
|
||||||
if(event.class === "PRIVATE") {
|
if(event.class === "PRIVATE") {
|
||||||
// do not add the current event, skip it
|
// do not add the current event, skip it
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(this.config.hideOngoing) {
|
||||||
|
if(event.startDate < now) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(this.listContainsEvent(events,event)){
|
||||||
|
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);
|
||||||
events.push(event);
|
|
||||||
|
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
|
||||||
|
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
|
||||||
|
*/
|
||||||
|
var maxCount = Math.ceil(((event.endDate - 1) - moment(event.startDate, "x").endOf("day").format("x"))/(1000*60*60*24)) + 1;
|
||||||
|
if (this.config.sliceMultiDayEvents && maxCount > 1) {
|
||||||
|
var splitEvents = [];
|
||||||
|
var midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
|
||||||
|
var count = 1;
|
||||||
|
while (event.endDate > midnight) {
|
||||||
|
var thisEvent = JSON.parse(JSON.stringify(event)); // clone object
|
||||||
|
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < (today + 24 * 60 * 60 * 1000);
|
||||||
|
thisEvent.endDate = midnight;
|
||||||
|
thisEvent.title += " (" + count + "/" + maxCount + ")";
|
||||||
|
splitEvents.push(thisEvent);
|
||||||
|
|
||||||
|
event.startDate = midnight;
|
||||||
|
count += 1;
|
||||||
|
midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
|
||||||
|
}
|
||||||
|
// Last day
|
||||||
|
event.title += " ("+count+"/"+maxCount+")";
|
||||||
|
splitEvents.push(event);
|
||||||
|
|
||||||
|
for (event of splitEvents) {
|
||||||
|
if ((event.endDate > now) && (event.endDate <= future)) {
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events.sort(function (a, b) {
|
events.sort(function (a, b) {
|
||||||
return a.startDate - b.startDate;
|
return a.startDate - b.startDate;
|
||||||
});
|
});
|
||||||
|
|
||||||
return events.slice(0, this.config.maximumEntries);
|
return events.slice(0, this.config.maximumEntries);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
listContainsEvent: function(eventList, event){
|
||||||
|
for(var evt of eventList){
|
||||||
|
if(evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
/* createEventList(url)
|
/* createEventList(url)
|
||||||
* Requests node helper to add calendar url.
|
* Requests node helper to add calendar url.
|
||||||
*
|
*
|
||||||
@@ -349,14 +547,20 @@ Module.register("calendar", {
|
|||||||
addCalendar: function (url, auth, calendarConfig) {
|
addCalendar: function (url, auth, calendarConfig) {
|
||||||
this.sendSocketNotification("ADD_CALENDAR", {
|
this.sendSocketNotification("ADD_CALENDAR", {
|
||||||
url: url,
|
url: url,
|
||||||
|
excludedEvents: calendarConfig.excludedEvents || this.config.excludedEvents,
|
||||||
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
|
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
|
||||||
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
|
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
|
||||||
fetchInterval: this.config.fetchInterval,
|
fetchInterval: this.config.fetchInterval,
|
||||||
auth: auth
|
symbolClass: calendarConfig.symbolClass,
|
||||||
|
titleClass: calendarConfig.titleClass,
|
||||||
|
timeClass: calendarConfig.timeClass,
|
||||||
|
auth: auth,
|
||||||
|
broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/* symbolsForUrl(url)
|
/**
|
||||||
|
* symbolsForUrl(url)
|
||||||
* Retrieves the symbols for a specific url.
|
* Retrieves the symbols for a specific url.
|
||||||
*
|
*
|
||||||
* argument url string - Url to look for.
|
* argument url string - Url to look for.
|
||||||
@@ -367,6 +571,53 @@ Module.register("calendar", {
|
|||||||
return this.getCalendarProperty(url, "symbol", this.config.defaultSymbol);
|
return this.getCalendarProperty(url, "symbol", this.config.defaultSymbol);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* symbolClassForUrl(url)
|
||||||
|
* Retrieves the symbolClass for a specific url.
|
||||||
|
*
|
||||||
|
* @param url string - Url to look for.
|
||||||
|
*
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
symbolClassForUrl: function (url) {
|
||||||
|
return this.getCalendarProperty(url, "symbolClass", "");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* titleClassForUrl(url)
|
||||||
|
* Retrieves the titleClass for a specific url.
|
||||||
|
*
|
||||||
|
* @param url string - Url to look for.
|
||||||
|
*
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
titleClassForUrl: function (url) {
|
||||||
|
return this.getCalendarProperty(url, "titleClass", "");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* timeClassForUrl(url)
|
||||||
|
* Retrieves the timeClass for a specific url.
|
||||||
|
*
|
||||||
|
* @param url string - Url to look for.
|
||||||
|
*
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
timeClassForUrl: function (url) {
|
||||||
|
return this.getCalendarProperty(url, "timeClass", "");
|
||||||
|
},
|
||||||
|
|
||||||
|
/* calendarNameForUrl(url)
|
||||||
|
* Retrieves the calendar name for a specific url.
|
||||||
|
*
|
||||||
|
* argument url string - Url to look for.
|
||||||
|
*
|
||||||
|
* return string - The name of the calendar
|
||||||
|
*/
|
||||||
|
calendarNameForUrl: function (url) {
|
||||||
|
return this.getCalendarProperty(url, "name", "");
|
||||||
|
},
|
||||||
|
|
||||||
/* colorForUrl(url)
|
/* colorForUrl(url)
|
||||||
* Retrieves the color for a specific url.
|
* Retrieves the color for a specific url.
|
||||||
*
|
*
|
||||||
@@ -409,27 +660,39 @@ Module.register("calendar", {
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
},
|
},
|
||||||
|
|
||||||
/* shorten(string, maxLength)
|
/**
|
||||||
* Shortens a string if it's longer than maxLength.
|
* Shortens a string if it's longer than maxLength and add a ellipsis to the end
|
||||||
* Adds an ellipsis to the end.
|
|
||||||
*
|
*
|
||||||
* argument string string - The string to shorten.
|
* @param {string} string Text string to shorten
|
||||||
* argument maxLength number - The max length of the string.
|
* @param {number} maxLength The max length of the string
|
||||||
* argument wrapEvents - Wrap the text after the line has reached maxLength
|
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
|
||||||
*
|
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
|
||||||
* return string - The shortened string.
|
* @returns {string} The shortened string
|
||||||
*/
|
*/
|
||||||
shorten: function (string, maxLength, wrapEvents) {
|
shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
|
||||||
if (wrapEvents) {
|
if (typeof string !== "string") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wrapEvents === true) {
|
||||||
var temp = "";
|
var temp = "";
|
||||||
var currentLine = "";
|
var currentLine = "";
|
||||||
var words = string.split(" ");
|
var words = string.split(" ");
|
||||||
|
var line = 0;
|
||||||
|
|
||||||
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 < 25 - 1) { // max - 1 to account for a space
|
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { // max - 1 to account for a space
|
||||||
currentLine += (word + " ");
|
currentLine += (word + " ");
|
||||||
} else {
|
} else {
|
||||||
|
line++;
|
||||||
|
if (line > maxTitleLines - 1) {
|
||||||
|
if (i < words.length) {
|
||||||
|
currentLine += "…";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentLine.length > 0) {
|
if (currentLine.length > 0) {
|
||||||
temp += (currentLine + "<br>" + word + " ");
|
temp += (currentLine + "<br>" + word + " ");
|
||||||
} else {
|
} else {
|
||||||
@@ -439,12 +702,12 @@ Module.register("calendar", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return temp + currentLine;
|
return (temp + currentLine).trim();
|
||||||
} else {
|
} else {
|
||||||
if (string.length > maxLength) {
|
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
|
||||||
return string.slice(0, maxLength) + "…";
|
return string.trim().slice(0, maxLength) + "…";
|
||||||
} else {
|
} else {
|
||||||
return string;
|
return string.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -453,7 +716,6 @@ Module.register("calendar", {
|
|||||||
* Capitalize the first letter of a string
|
* Capitalize the first letter of a string
|
||||||
* Return capitalized string
|
* Return capitalized string
|
||||||
*/
|
*/
|
||||||
|
|
||||||
capFirst: function (string) {
|
capFirst: function (string) {
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
},
|
},
|
||||||
@@ -480,7 +742,7 @@ Module.register("calendar", {
|
|||||||
title = title.replace(needle, replacement);
|
title = title.replace(needle, replacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
title = this.shorten(title, this.config.maxTitleLength, this.config.wrapEvents);
|
title = this.shorten(title, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines);
|
||||||
return title;
|
return title;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -490,10 +752,13 @@ Module.register("calendar", {
|
|||||||
*/
|
*/
|
||||||
broadcastEvents: function () {
|
broadcastEvents: function () {
|
||||||
var eventList = [];
|
var eventList = [];
|
||||||
for (url in this.calendarData) {
|
for (var url in this.calendarData) {
|
||||||
var calendar = this.calendarData[url];
|
var calendar = this.calendarData[url];
|
||||||
for (e in calendar) {
|
for (var e in calendar) {
|
||||||
var event = cloneObject(calendar[e]);
|
var event = cloneObject(calendar[e]);
|
||||||
|
event.symbol = this.symbolsForUrl(url);
|
||||||
|
event.calendarName = this.calendarNameForUrl(url);
|
||||||
|
event.color = this.colorForUrl(url);
|
||||||
delete event.url;
|
delete event.url;
|
||||||
eventList.push(event);
|
eventList.push(event);
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
var ical = require("./vendor/ical.js");
|
var ical = require("./vendor/ical.js");
|
||||||
var moment = require("moment");
|
var moment = require("moment");
|
||||||
|
|
||||||
var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumberOfDays, auth) {
|
var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var reloadTimer = null;
|
var reloadTimer = null;
|
||||||
@@ -29,16 +29,17 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
var opts = {
|
var 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/)"
|
||||||
}
|
},
|
||||||
|
gzip: true
|
||||||
};
|
};
|
||||||
|
|
||||||
if (auth) {
|
if (auth) {
|
||||||
if(auth.method === "bearer"){
|
if(auth.method === "bearer"){
|
||||||
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
|
||||||
@@ -46,7 +47,7 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
|
|
||||||
if(auth.method === "digest"){
|
if(auth.method === "digest"){
|
||||||
opts.auth.sendImmediately = false;
|
opts.auth.sendImmediately = false;
|
||||||
}else{
|
} else {
|
||||||
opts.auth.sendImmediately = true;
|
opts.auth.sendImmediately = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +63,8 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
// console.log(data);
|
// console.log(data);
|
||||||
newEvents = [];
|
newEvents = [];
|
||||||
|
|
||||||
var limitFunction = function(date, i) {return i < maximumEntries;};
|
// 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;};
|
||||||
|
|
||||||
var eventDate = function(event, time) {
|
var eventDate = function(event, time) {
|
||||||
return (event[time].length === 8) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
return (event[time].length === 8) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||||
@@ -73,6 +75,11 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
var now = new Date();
|
var now = new Date();
|
||||||
var today = moment().startOf("day").toDate();
|
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.
|
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.
|
||||||
|
var past = today;
|
||||||
|
|
||||||
|
if (includePastEvents) {
|
||||||
|
past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate();
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME:
|
// FIXME:
|
||||||
// Ugly fix to solve the facebook birthday issue.
|
// Ugly fix to solve the facebook birthday issue.
|
||||||
@@ -90,6 +97,9 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
var endDate;
|
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") {
|
||||||
|
dur=moment.duration(event.duration);
|
||||||
|
endDate = startDate.clone().add(dur);
|
||||||
} else {
|
} else {
|
||||||
if (!isFacebookBirthday) {
|
if (!isFacebookBirthday) {
|
||||||
endDate = startDate;
|
endDate = startDate;
|
||||||
@@ -98,7 +108,6 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// calculate the duration f the event for use with recurring events.
|
// calculate the duration f the event for use with recurring events.
|
||||||
var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||||
|
|
||||||
@@ -106,27 +115,154 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
startDate = startDate.startOf("day");
|
startDate = startDate.startOf("day");
|
||||||
}
|
}
|
||||||
|
|
||||||
var title = "Event";
|
var title = getTitleFromEvent(event);
|
||||||
if (event.summary) {
|
|
||||||
title = (typeof event.summary.val !== "undefined") ? event.summary.val : event.summary;
|
var excluded = false,
|
||||||
} else if(event.description) {
|
dateFilter = null;
|
||||||
title = event.description;
|
|
||||||
|
for (var f in excludedEvents) {
|
||||||
|
var filter = excludedEvents[f],
|
||||||
|
testTitle = title.toLowerCase(),
|
||||||
|
until = null,
|
||||||
|
useRegex = false,
|
||||||
|
regexFlags = "g";
|
||||||
|
|
||||||
|
if (filter instanceof Object) {
|
||||||
|
if (typeof filter.until !== "undefined") {
|
||||||
|
until = filter.until;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof filter.regex !== "undefined") {
|
||||||
|
useRegex = filter.regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If additional advanced filtering is added in, this section
|
||||||
|
// must remain last as we overwrite the filter object with the
|
||||||
|
// filterBy string
|
||||||
|
if (filter.caseSensitive) {
|
||||||
|
filter = filter.filterBy;
|
||||||
|
testTitle = title;
|
||||||
|
} else if (useRegex) {
|
||||||
|
filter = filter.filterBy;
|
||||||
|
testTitle = title;
|
||||||
|
regexFlags += "i";
|
||||||
|
} else {
|
||||||
|
filter = filter.filterBy.toLowerCase();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filter = filter.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testTitleByFilter(testTitle, filter, useRegex, regexFlags)) {
|
||||||
|
if (until) {
|
||||||
|
dateFilter = until;
|
||||||
|
} else {
|
||||||
|
excluded = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excluded) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var location = event.location || false;
|
var location = event.location || false;
|
||||||
var geo = event.geo || false;
|
var geo = event.geo || false;
|
||||||
var description = event.description || false;
|
var description = event.description || false;
|
||||||
|
|
||||||
if (typeof event.rrule != "undefined" && !isFacebookBirthday) {
|
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||||
var rule = event.rrule;
|
var rule = event.rrule;
|
||||||
var dates = rule.between(today, future, true, limitFunction);
|
var addedEvents = 0;
|
||||||
|
|
||||||
|
// can cause problems with e.g. birthdays before 1900
|
||||||
|
if(rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900 ||
|
||||||
|
rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900){
|
||||||
|
rule.origOptions.dtstart.setYear(1900);
|
||||||
|
rule.options.dtstart.setYear(1900);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For recurring events, get the set of start dates that fall within the range
|
||||||
|
// of dates we"re looking for.
|
||||||
|
var dates = rule.between(past, future, true, limitFunction);
|
||||||
|
|
||||||
|
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||||
|
// for the recurrence rule. *However*, it"s possible for us to have a specific recurrence that
|
||||||
|
// had its date changed from outside the range to inside the range. For the time being,
|
||||||
|
// we"ll handle this by adding *all* recurrence entries into the set of dates that we check,
|
||||||
|
// because the logic below will filter out any recurrences that don"t actually belong within
|
||||||
|
// our display range.
|
||||||
|
// Would be great if there was a better way to handle this.
|
||||||
|
if (event.recurrences != undefined)
|
||||||
|
{
|
||||||
|
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
|
||||||
|
// we don"t double-add those events.
|
||||||
|
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) != true)
|
||||||
|
{
|
||||||
|
dates.push(new Date(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||||
for (var d in dates) {
|
for (var d in dates) {
|
||||||
startDate = moment(new Date(dates[d]));
|
var date = dates[d];
|
||||||
|
// ical.js started returning recurrences and exdates as ISOStrings without time information.
|
||||||
|
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
|
||||||
|
// (see https://github.com/peterbraden/ical.js/pull/84 )
|
||||||
|
var dateKey = date.toISOString().substring(0,10);
|
||||||
|
var curEvent = event;
|
||||||
|
var showRecurrence = true;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// For each date that we"re checking, it"s possible that there is a recurrence override for that one day.
|
||||||
|
if ((curEvent.recurrences != undefined) && (curEvent.recurrences[dateKey] != undefined))
|
||||||
|
{
|
||||||
|
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||||
|
curEvent = curEvent.recurrences[dateKey];
|
||||||
|
startDate = moment(curEvent.start);
|
||||||
|
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
||||||
|
}
|
||||||
|
// If there"s no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||||
|
else if ((curEvent.exdate != undefined) && (curEvent.exdate[dateKey] != undefined))
|
||||||
|
{
|
||||||
|
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||||
|
showRecurrence = false;
|
||||||
|
}
|
||||||
|
|
||||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||||
if (endDate.format("x") > now) {
|
if (startDate.format("x") == endDate.format("x")) {
|
||||||
|
endDate = endDate.endOf("day");
|
||||||
|
}
|
||||||
|
|
||||||
|
var recurrenceTitle = getTitleFromEvent(curEvent);
|
||||||
|
|
||||||
|
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
|
||||||
|
// it to the event list.
|
||||||
|
if (endDate.isBefore(past) || startDate.isAfter(future)) {
|
||||||
|
showRecurrence = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeFilterApplies(now, endDate, dateFilter)) {
|
||||||
|
showRecurrence = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((showRecurrence === true) && (addedEvents < maximumEntries)) {
|
||||||
|
addedEvents++;
|
||||||
newEvents.push({
|
newEvents.push({
|
||||||
title: title,
|
title: recurrenceTitle,
|
||||||
startDate: startDate.format("x"),
|
startDate: startDate.format("x"),
|
||||||
endDate: endDate.format("x"),
|
endDate: endDate.format("x"),
|
||||||
fullDayEvent: isFullDayEvent(event),
|
fullDayEvent: isFullDayEvent(event),
|
||||||
@@ -138,19 +274,27 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// end recurring event parsing
|
||||||
} else {
|
} else {
|
||||||
// console.log("Single event ...");
|
// console.log("Single event ...");
|
||||||
// Single event.
|
// Single event.
|
||||||
var fullDayEvent = (isFacebookBirthday) ? true : isFullDayEvent(event);
|
var fullDayEvent = (isFacebookBirthday) ? true : isFullDayEvent(event);
|
||||||
|
|
||||||
if (!fullDayEvent && endDate < new Date()) {
|
if (includePastEvents) {
|
||||||
//console.log("It's not a fullday event, and it is in the past. So skip: " + title);
|
if (endDate < past) {
|
||||||
continue;
|
//console.log("Past event is too far in the past. So skip: " + title);
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!fullDayEvent && endDate < new Date()) {
|
||||||
|
//console.log("It's not a fullday event, and it is in the past. So skip: " + title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (fullDayEvent && endDate <= today) {
|
if (fullDayEvent && endDate <= today) {
|
||||||
//console.log("It's a fullday event, and it is before today. So skip: " + title);
|
//console.log("It's a fullday event, and it is before today. So skip: " + title);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startDate > future) {
|
if (startDate > future) {
|
||||||
@@ -158,6 +302,10 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timeFilterApplies(now, endDate, dateFilter)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Every thing is good. Add it to the list.
|
// Every thing is good. Add it to the list.
|
||||||
|
|
||||||
newEvents.push({
|
newEvents.push({
|
||||||
@@ -202,7 +350,7 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
/* isFullDayEvent(event)
|
/* isFullDayEvent(event)
|
||||||
* Checks if an event is a fullday event.
|
* Checks if an event is a fullday event.
|
||||||
*
|
*
|
||||||
* argument event obejct - The event object to check.
|
* argument event object - The event object to check.
|
||||||
*
|
*
|
||||||
* return bool - The event is a fullday event.
|
* return bool - The event is a fullday event.
|
||||||
*/
|
*/
|
||||||
@@ -214,8 +362,7 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
var start = event.start || 0;
|
var start = event.start || 0;
|
||||||
var startDate = new Date(start);
|
var startDate = new Date(start);
|
||||||
var end = event.end || 0;
|
var 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 && 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;
|
||||||
}
|
}
|
||||||
@@ -223,6 +370,62 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* timeFilterApplies()
|
||||||
|
* Determines if the user defined time filter should apply
|
||||||
|
*
|
||||||
|
* argument now Date - Date object using previously created object for consistency
|
||||||
|
* argument endDate Moment - 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
|
||||||
|
*
|
||||||
|
* return bool - The event should be filtered out
|
||||||
|
*/
|
||||||
|
var timeFilterApplies = function(now, endDate, filter) {
|
||||||
|
if (filter) {
|
||||||
|
var until = filter.split(" "),
|
||||||
|
value = parseInt(until[0]),
|
||||||
|
increment = until[1].slice("-1") === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
|
||||||
|
filterUntil = moment(endDate.format()).subtract(value, increment);
|
||||||
|
|
||||||
|
return now < filterUntil.format("x");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* getTitleFromEvent(event)
|
||||||
|
* Gets the title from the event.
|
||||||
|
*
|
||||||
|
* argument event object - The event object to check.
|
||||||
|
*
|
||||||
|
* return string - The title of the event, or "Event" if no title is found.
|
||||||
|
*/
|
||||||
|
var getTitleFromEvent = function (event) {
|
||||||
|
var title = "Event";
|
||||||
|
if (event.summary) {
|
||||||
|
title = (typeof event.summary.val !== "undefined") ? event.summary.val : event.summary;
|
||||||
|
} else if (event.description) {
|
||||||
|
title = event.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
return title;
|
||||||
|
};
|
||||||
|
|
||||||
|
var testTitleByFilter = function (title, filter, useRegex, regexFlags) {
|
||||||
|
if (useRegex) {
|
||||||
|
// Assume if leading slash, there is also trailing slash
|
||||||
|
if (filter[0] === "/") {
|
||||||
|
// Strip leading and trailing slashes
|
||||||
|
filter = filter.substr(1).slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
filter = new RegExp(filter, regexFlags);
|
||||||
|
|
||||||
|
return filter.test(title);
|
||||||
|
} else {
|
||||||
|
return title.includes(filter);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/* public methods */
|
/* public methods */
|
||||||
|
|
||||||
/* startFetch()
|
/* startFetch()
|
||||||
@@ -275,8 +478,6 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
|
|||||||
this.events = function() {
|
this.events = function() {
|
||||||
return events;
|
return events;
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = CalendarFetcher;
|
module.exports = CalendarFetcher;
|
||||||
|
@@ -15,6 +15,7 @@ var maximumEntries = 10;
|
|||||||
var maximumNumberOfDays = 365;
|
var maximumNumberOfDays = 365;
|
||||||
var user = "magicmirror";
|
var user = "magicmirror";
|
||||||
var pass = "MyStrongPass";
|
var pass = "MyStrongPass";
|
||||||
|
var broadcastPastEvents = false;
|
||||||
|
|
||||||
var auth = {
|
var auth = {
|
||||||
user: user,
|
user: user,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user