diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..136532c1 --- /dev/null +++ b/.github/stale.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index e7942d2d..85565c83 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,15 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [2.8.0] - Unreleased -*This release is scheduled to be released on 2019-04-01.* +*This release is scheduled to be released on 2019-07-01.* ### 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 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) @@ -20,8 +26,17 @@ Added UK Met Office Datapoint feed as a provider in the default weather module. - optionally display probability of precipitation (PoP) in current weather (UK Met Office data) ### 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 +- Allowance HTML5 autoplay-policy (policy is changed from Chrome 66 updates) +- Handle SIGTERM messages +- Fixes sliceMultiDayEvents so it respects maximumNumberOfDays +>>>>>>> upstream/develop ## [2.7.1] - 2019-04-02 diff --git a/config/config.js.sample b/config/config.js.sample index 6e419e59..8ec8622f 100644 --- a/config/config.js.sample +++ b/config/config.js.sample @@ -45,8 +45,7 @@ var config = { calendars: [ { 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" } ] } }, @@ -59,7 +58,7 @@ var config = { position: "top_right", config: { location: "New York", - locationID: "", //ID from http://bulk.openweathermap.org/sample/; unzip the gz file and find your city + 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" } }, @@ -69,7 +68,7 @@ var config = { header: "Weather Forecast", config: { location: "New York", - locationID: "5128581", //ID from https://openweathermap.org/city + 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" } }, diff --git a/js/app.js b/js/app.js index 95c9693b..4a10ba6d 100644 --- a/js/app.js +++ b/js/app.js @@ -263,6 +263,15 @@ var App = function() { 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(); diff --git a/js/electron.js b/js/electron.js index fc6ab98e..c5e9c4cb 100644 --- a/js/electron.js +++ b/js/electron.js @@ -17,6 +17,7 @@ const BrowserWindow = electron.BrowserWindow; let mainWindow; function createWindow() { + app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); var electronOptionsDefaults = { width: 800, height: 600, diff --git a/modules/default/calendar/README.md b/modules/default/calendar/README.md index b2bb5c7f..eb9324f2 100755 --- a/modules/default/calendar/README.md +++ b/modules/default/calendar/README.md @@ -30,6 +30,7 @@ The following properties can be configured: | `maximumNumberOfDays` | The maximum number of days in the future.

**Default value:** `365` | `displaySymbol` | Display a symbol in front of an entry.

**Possible values:** `true` or `false`
**Default value:** `true` | `defaultSymbol` | The default symbol.

**Possible values:** See [Font Awsome](http://fontawesome.io/icons/) website.
**Default value:** `calendar` +| `showLocation` | Whether to show event locations.

**Possible values:** `true` or `false`
**Default value:** `false` | `maxTitleLength` | The maximum title length.

**Possible values:** `10` - `50`
**Default value:** `25` | `wrapEvents` | Wrap event titles to multiple lines. Breaks lines at the length defined by `maxTitleLength`.

**Possible values:** `true` or `false`
**Default value:** `false` | `maxTitleLines` | The maximum number of lines a title will wrap vertically before being cut (Only enabled if `wrapEvents` is also enabled).

**Possible values:** `0` - `10`
**Default value:** `3` @@ -53,8 +54,9 @@ The following properties can be configured: | `hidePrivate` | Hides private calendar events.

**Possible values:** `true` or `false`
**Default value:** `false` | `hideOngoing` | Hides calendar events that have already started.

**Possible values:** `true` or `false`
**Default value:** `false` | `excludedEvents` | An array of words / phrases from event titles that will be excluded from being shown.

Additionally advanced filter objects can be passed in. Below is the configuration for the advance filtering object.
**Required**
`filterBy` - string used to determine if filter is applied.
**Optional**
`until` - Time before an event to display it Ex: [`'3 days'`, `'2 months'`, `'1 week'`]
`caseSensitive` - By default, excludedEvents are case insensitive, set this to true to enforce case sensitivity
`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.

**Example:** `['Birthday', 'Hide This Event', {filterBy: 'Payment', until: '6 days', caseSensitive: true}, {filterBy: '^[0-9]{1,}.*', regex: true}]`
**Default value:** `[]` -| `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 **Default value:** `true` - +| `broadcastPastEvents` | If this is set to true, events from the past `maximumNumberOfDays` will be included in event broadcasts
**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
**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.
**Default value:** `false` ### Calendar configuration @@ -95,6 +97,7 @@ config: { | `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: diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index d584a9ed..2eac086e 100755 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -15,6 +15,7 @@ Module.register("calendar", { maximumNumberOfDays: 365, displaySymbol: true, defaultSymbol: "calendar", // Fontawesome Symbol see http://fontawesome.io/cheatsheet/ + showLocation: false, displayRepeatingCountTitle: false, defaultRepeatingCountTitle: "", maxTitleLength: 25, @@ -48,7 +49,9 @@ Module.register("calendar", { }, broadcastEvents: true, excludedEvents: [], - sliceMultiDayEvents: false + sliceMultiDayEvents: false, + broadcastPastEvents: false, + nextDaysRelative: false }, // Define required scripts. @@ -82,7 +85,8 @@ Module.register("calendar", { var calendarConfig = { maximumEntries: calendar.maximumEntries, - maximumNumberOfDays: calendar.maximumNumberOfDays + maximumNumberOfDays: calendar.maximumNumberOfDays, + broadcastPastEvents: calendar.broadcastPastEvents, }; if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) { calendarConfig.symbolClass = ""; @@ -325,7 +329,7 @@ Module.register("calendar", { // 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") { + 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' @@ -379,6 +383,31 @@ Module.register("calendar", { currentFadeStep = e - startFade; eventWrapper.style.opacity = 1 - (1 / fadeSteps * currentFadeStep); } + + if (this.config.showLocation) { + if (event.location !== false) { + var locationRow = document.createElement("tr"); + 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); + } + } + } } return wrapper; @@ -436,10 +465,14 @@ Module.register("calendar", { var events = []; var today = moment().startOf("day"); var now = new Date(); + var future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate(); for (var c in this.calendarData) { var calendar = this.calendarData[c]; for (var e in calendar) { - var event = calendar[e]; + var event = JSON.parse(JSON.stringify(calendar[e])); // clone object + if(event.endDate < now) { + continue; + } if(this.config.hidePrivate) { if(event.class === "PRIVATE") { // do not add the current event, skip it @@ -460,24 +493,31 @@ Module.register("calendar", { /* 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. */ - if (this.config.sliceMultiDayEvents) { - var midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x"); //next midnight + 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; - var maxCount = Math.ceil(((event.endDate - 1) - moment(event.startDate, "x").endOf("day").format("x"))/(1000*60*60*24)) + 1 - if (event.endDate > midnight) { - while (event.endDate > midnight) { - var nextEvent = JSON.parse(JSON.stringify(event)); //make a copy without reference to the original event - nextEvent.startDate = midnight; - event.endDate = midnight; - event.title += " (" + count + "/" + maxCount + ")"; - events.push(event); - event = nextEvent; - count += 1; - midnight = moment(midnight, "x").add(1, "day").format("x"); //move further one day for next split - } - event.title += " ("+count+"/"+maxCount+")"; - } - events.push(event); + 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); } @@ -487,7 +527,6 @@ Module.register("calendar", { events.sort(function (a, b) { return a.startDate - b.startDate; }); - return events.slice(0, this.config.maximumEntries); }, @@ -517,7 +556,8 @@ Module.register("calendar", { symbolClass: calendarConfig.symbolClass, titleClass: calendarConfig.titleClass, timeClass: calendarConfig.timeClass, - auth: auth + auth: auth, + broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents, }); }, diff --git a/modules/default/calendar/calendarfetcher.js b/modules/default/calendar/calendarfetcher.js index 0076a605..3a30aea2 100644 --- a/modules/default/calendar/calendarfetcher.js +++ b/modules/default/calendar/calendarfetcher.js @@ -8,7 +8,7 @@ var ical = require("./vendor/ical.js"); var moment = require("moment"); -var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth) { +var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) { var self = this; var reloadTimer = null; @@ -74,6 +74,11 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri var now = new Date(); var today = moment().startOf("day").toDate(); var future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1,"seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat. + var past = today; + + if (includePastEvents) { + past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate(); + } // FIXME: // Ugly fix to solve the facebook birthday issue. @@ -181,7 +186,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri rule.options.dtstart.setYear(1900); } - var dates = rule.between(today, future, true, limitFunction); + var dates = rule.between(past, future, true, limitFunction); for (var d in dates) { startDate = moment(new Date(dates[d])); @@ -191,7 +196,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri continue; } - if (endDate.format("x") > now) { + if (includePastEvents || endDate.format("x") > now) { newEvents.push({ title: title, startDate: startDate.format("x"), @@ -210,14 +215,21 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri // Single event. var fullDayEvent = (isFacebookBirthday) ? true : isFullDayEvent(event); - if (!fullDayEvent && endDate < new Date()) { - //console.log("It's not a fullday event, and it is in the past. So skip: " + title); - continue; - } + if (includePastEvents) { + if (endDate < past) { + //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) { - //console.log("It's a fullday event, and it is before today. So skip: " + title); - continue; + if (fullDayEvent && endDate <= today) { + //console.log("It's a fullday event, and it is before today. So skip: " + title); + continue; + } } if (startDate > future) { diff --git a/modules/default/calendar/debug.js b/modules/default/calendar/debug.js index ddf0fb42..a568e1cd 100644 --- a/modules/default/calendar/debug.js +++ b/modules/default/calendar/debug.js @@ -15,6 +15,7 @@ var maximumEntries = 10; var maximumNumberOfDays = 365; var user = "magicmirror"; var pass = "MyStrongPass"; +var broadcastPastEvents = false; var auth = { user: user, diff --git a/modules/default/calendar/node_helper.js b/modules/default/calendar/node_helper.js index 25e7f1f7..9c6e4979 100644 --- a/modules/default/calendar/node_helper.js +++ b/modules/default/calendar/node_helper.js @@ -24,7 +24,7 @@ module.exports = NodeHelper.create({ socketNotificationReceived: function(notification, payload) { if (notification === "ADD_CALENDAR") { //console.log('ADD_CALENDAR: '); - this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth); + this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents); } }, @@ -36,7 +36,7 @@ module.exports = NodeHelper.create({ * attribute reloadInterval number - Reload interval in milliseconds. */ - createFetcher: function(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth) { + createFetcher: function(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents) { var self = this; if (!validUrl.isUri(url)) { @@ -47,7 +47,7 @@ module.exports = NodeHelper.create({ var fetcher; if (typeof self.fetchers[url] === "undefined") { console.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval); - fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth); + fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents); fetcher.onReceive(function(fetcher) { //console.log('Broadcast events.'); diff --git a/modules/default/calendar/vendor/ical.js/ical.js b/modules/default/calendar/vendor/ical.js/ical.js index 8f0c532b..024625b7 100644 --- a/modules/default/calendar/vendor/ical.js/ical.js +++ b/modules/default/calendar/vendor/ical.js/ical.js @@ -33,9 +33,9 @@ for (var i = 0; i {% endif %} - + diff --git a/modules/default/weatherforecast/README.md b/modules/default/weatherforecast/README.md index 4be3d9bf..a75cb984 100644 --- a/modules/default/weatherforecast/README.md +++ b/modules/default/weatherforecast/README.md @@ -19,7 +19,7 @@ modules: [ config: { // See 'Configuration options' for more information. location: "Amsterdam,Netherlands", - locationID: "", //Location ID from http://openweathermap.org/help/city_list.txt + locationID: "", //Location ID from http://bulk.openweathermap.org/sample/city.list.json.gz appid: "abcde12345abcde12345abcde12345ab" //openweathermap.org API key. } } diff --git a/translations/en.json b/translations/en.json index f873fd0e..18a2ca1e 100755 --- a/translations/en.json +++ b/translations/en.json @@ -30,7 +30,7 @@ "UPDATE_NOTIFICATION_MODULE": "Update available for {MODULE_NAME} module.", "UPDATE_INFO_SINGLE": "The current installation is {COMMIT_COUNT} commit behind on the {BRANCH_NAME} branch.", "UPDATE_INFO_MULTIPLE": "The current installation is {COMMIT_COUNT} commits behind on the {BRANCH_NAME} branch.", - - "FEELS": "Feels", + + "FEELS": "Feels like" "PRECIP": "PoP" } diff --git a/translations/fi.json b/translations/fi.json index aa20759d..495439c1 100644 --- a/translations/fi.json +++ b/translations/fi.json @@ -7,6 +7,8 @@ "RUNNING": "Päättyy {timeUntilEnd} päästä", "EMPTY": "Ei tulevia tapahtumia.", + "WEEK": "Viikko {weekNumber}", + "N": "P", "NNE": "PPI", "NE": "PI", @@ -25,5 +27,7 @@ "NNW": "PPL", "UPDATE_NOTIFICATION": "MagicMirror² päivitys saatavilla.", - "UPDATE_NOTIFICATION_MODULE": "Päivitys saatavilla moduulille {MODULE_NAME}." + "UPDATE_NOTIFICATION_MODULE": "Päivitys saatavilla moduulille {MODULE_NAME}.", + + "FEELS": "Tuntuu kuin" } diff --git a/translations/ru.json b/translations/ru.json index cf2e5c5f..0512e14b 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -25,6 +25,7 @@ "WNW": "ЗСЗ", "NW": "СЗ", "NNW": "ССЗ", + "FEELS": "По ощущению", "UPDATE_NOTIFICATION": "Есть обновление для MagicMirror².", "UPDATE_NOTIFICATION_MODULE": "Есть обновление для {MODULE_NAME} модуля.",