mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-28 04:02:12 +00:00
Merge remote-tracking branch 'upstream/develop'
This commit is contained in:
commit
fb7115fc13
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
|
17
CHANGELOG.md
17
CHANGELOG.md
@ -9,9 +9,15 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
## [2.8.0] - Unreleased
|
## [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
|
### 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 UK Met Office Datapoint feed as a provider in the default weather module.
|
||||||
- added new provider class
|
- added new provider class
|
||||||
- added suncalc.js dependency to calculate sun times (not provided in UK Met Office feed)
|
- 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)
|
- optionally display probability of precipitation (PoP) in current weather (UK Met Office data)
|
||||||
|
|
||||||
### Updated
|
### 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
|
||||||
|
- 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
|
## [2.7.1] - 2019-04-02
|
||||||
|
|
||||||
|
@ -45,8 +45,7 @@ var config = {
|
|||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
symbol: "calendar-check",
|
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",
|
position: "top_right",
|
||||||
config: {
|
config: {
|
||||||
location: "New York",
|
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"
|
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -69,7 +68,7 @@ var config = {
|
|||||||
header: "Weather Forecast",
|
header: "Weather Forecast",
|
||||||
config: {
|
config: {
|
||||||
location: "New York",
|
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"
|
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -263,6 +263,15 @@ var App = function() {
|
|||||||
this.stop();
|
this.stop();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* We also need to listen to SIGTERM signals so we stop everything when we are asked to stop by the OS.
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
@ -17,6 +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,6 +30,7 @@ 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`
|
| `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`
|
||||||
@ -53,8 +54,9 @@ The following properties can be configured:
|
|||||||
| `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`
|
||||||
| `hideOngoing` | Hides calendar events that have already started. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
| `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:** `[]`
|
| `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:** `[]`
|
||||||
| `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 <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
|
||||||
|
|
||||||
@ -95,6 +97,7 @@ config: {
|
|||||||
| `symbolClass` | Add a class to the cell of symbol.
|
| `symbolClass` | Add a class to the cell of symbol.
|
||||||
| `titleClass` | Add a class to the title's cell.
|
| `titleClass` | Add a class to the title's cell.
|
||||||
| `timeClass` | Add a class to the time'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:
|
||||||
|
@ -15,6 +15,7 @@ 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,
|
||||||
@ -48,7 +49,9 @@ Module.register("calendar", {
|
|||||||
},
|
},
|
||||||
broadcastEvents: true,
|
broadcastEvents: true,
|
||||||
excludedEvents: [],
|
excludedEvents: [],
|
||||||
sliceMultiDayEvents: false
|
sliceMultiDayEvents: false,
|
||||||
|
broadcastPastEvents: false,
|
||||||
|
nextDaysRelative: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// Define required scripts.
|
// Define required scripts.
|
||||||
@ -82,7 +85,8 @@ 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) {
|
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
|
||||||
calendarConfig.symbolClass = "";
|
calendarConfig.symbolClass = "";
|
||||||
@ -325,7 +329,7 @@ Module.register("calendar", {
|
|||||||
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
|
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||||
} else {
|
} 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));
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||||
} else {
|
} else {
|
||||||
// Otherwise just say 'Today/Tomorrow at such-n-such time'
|
// Otherwise just say 'Today/Tomorrow at such-n-such time'
|
||||||
@ -379,6 +383,31 @@ Module.register("calendar", {
|
|||||||
currentFadeStep = e - startFade;
|
currentFadeStep = e - startFade;
|
||||||
eventWrapper.style.opacity = 1 - (1 / fadeSteps * currentFadeStep);
|
eventWrapper.style.opacity = 1 - (1 / fadeSteps * currentFadeStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.config.showLocation) {
|
||||||
|
if (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;
|
return wrapper;
|
||||||
@ -436,10 +465,14 @@ Module.register("calendar", {
|
|||||||
var events = [];
|
var events = [];
|
||||||
var today = moment().startOf("day");
|
var today = moment().startOf("day");
|
||||||
var now = new Date();
|
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
|
||||||
@ -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,
|
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
|
||||||
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
|
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
|
||||||
*/
|
*/
|
||||||
if (this.config.sliceMultiDayEvents) {
|
var maxCount = Math.ceil(((event.endDate - 1) - moment(event.startDate, "x").endOf("day").format("x"))/(1000*60*60*24)) + 1;
|
||||||
var midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x"); //next midnight
|
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 count = 1;
|
||||||
var maxCount = Math.ceil(((event.endDate - 1) - moment(event.startDate, "x").endOf("day").format("x"))/(1000*60*60*24)) + 1
|
while (event.endDate > midnight) {
|
||||||
if (event.endDate > midnight) {
|
var thisEvent = JSON.parse(JSON.stringify(event)) // clone object
|
||||||
while (event.endDate > midnight) {
|
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < (today + 24 * 60 * 60 * 1000);
|
||||||
var nextEvent = JSON.parse(JSON.stringify(event)); //make a copy without reference to the original event
|
thisEvent.endDate = midnight;
|
||||||
nextEvent.startDate = midnight;
|
thisEvent.title += " (" + count + "/" + maxCount + ")";
|
||||||
event.endDate = midnight;
|
splitEvents.push(thisEvent);
|
||||||
event.title += " (" + count + "/" + maxCount + ")";
|
|
||||||
events.push(event);
|
event.startDate = midnight;
|
||||||
event = nextEvent;
|
count += 1;
|
||||||
count += 1;
|
midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
|
||||||
midnight = moment(midnight, "x").add(1, "day").format("x"); //move further one day for next split
|
}
|
||||||
}
|
// Last day
|
||||||
event.title += " ("+count+"/"+maxCount+")";
|
event.title += " ("+count+"/"+maxCount+")";
|
||||||
}
|
splitEvents.push(event);
|
||||||
events.push(event);
|
|
||||||
|
for (event of splitEvents) {
|
||||||
|
if ((event.endDate > now) && (event.endDate <= future)) {
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
events.push(event);
|
events.push(event);
|
||||||
}
|
}
|
||||||
@ -487,7 +527,6 @@ Module.register("calendar", {
|
|||||||
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);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -517,7 +556,8 @@ Module.register("calendar", {
|
|||||||
symbolClass: calendarConfig.symbolClass,
|
symbolClass: calendarConfig.symbolClass,
|
||||||
titleClass: calendarConfig.titleClass,
|
titleClass: calendarConfig.titleClass,
|
||||||
timeClass: calendarConfig.timeClass,
|
timeClass: calendarConfig.timeClass,
|
||||||
auth: auth
|
auth: auth,
|
||||||
|
broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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, excludedEvents, 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;
|
||||||
@ -74,6 +74,11 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
|||||||
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.
|
||||||
@ -181,7 +186,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
|||||||
rule.options.dtstart.setYear(1900);
|
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) {
|
for (var d in dates) {
|
||||||
startDate = moment(new Date(dates[d]));
|
startDate = moment(new Date(dates[d]));
|
||||||
@ -191,7 +196,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endDate.format("x") > now) {
|
if (includePastEvents || endDate.format("x") > now) {
|
||||||
newEvents.push({
|
newEvents.push({
|
||||||
title: title,
|
title: title,
|
||||||
startDate: startDate.format("x"),
|
startDate: startDate.format("x"),
|
||||||
@ -210,14 +215,21 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
|||||||
// 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) {
|
||||||
|
@ -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,
|
||||||
|
@ -24,7 +24,7 @@ module.exports = NodeHelper.create({
|
|||||||
socketNotificationReceived: function(notification, payload) {
|
socketNotificationReceived: function(notification, payload) {
|
||||||
if (notification === "ADD_CALENDAR") {
|
if (notification === "ADD_CALENDAR") {
|
||||||
//console.log('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.
|
* 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;
|
var self = this;
|
||||||
|
|
||||||
if (!validUrl.isUri(url)) {
|
if (!validUrl.isUri(url)) {
|
||||||
@ -47,7 +47,7 @@ module.exports = NodeHelper.create({
|
|||||||
var fetcher;
|
var fetcher;
|
||||||
if (typeof self.fetchers[url] === "undefined") {
|
if (typeof self.fetchers[url] === "undefined") {
|
||||||
console.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
|
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) {
|
fetcher.onReceive(function(fetcher) {
|
||||||
//console.log('Broadcast events.');
|
//console.log('Broadcast events.');
|
||||||
|
281
modules/default/calendar/vendor/ical.js/ical.js
vendored
281
modules/default/calendar/vendor/ical.js/ical.js
vendored
@ -55,75 +55,52 @@
|
|||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
var storeParam = function(name){
|
var storeValParam = function (name) {
|
||||||
return function(val, params, curr){
|
return function (val, curr) {
|
||||||
var data;
|
var current = curr[name];
|
||||||
if (params && params.length && !(params.length==1 && params[0]==='CHARSET=utf-8')){
|
if (Array.isArray(current)) {
|
||||||
data = {params:parseParams(params), val:text(val)}
|
current.push(val);
|
||||||
}
|
return curr;
|
||||||
else
|
}
|
||||||
data = text(val)
|
|
||||||
|
|
||||||
var current = curr[name];
|
if (current != null) {
|
||||||
if (Array.isArray(current)){
|
curr[name] = [current, val];
|
||||||
current.push(data);
|
return curr;
|
||||||
return curr;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (current != null){
|
curr[name] = val;
|
||||||
curr[name] = [current, data];
|
return curr
|
||||||
return curr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
curr[name] = data;
|
|
||||||
return curr
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var addTZ = function(dt, params){
|
var storeParam = function (name) {
|
||||||
|
return function (val, params, curr) {
|
||||||
|
var data;
|
||||||
|
if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) {
|
||||||
|
data = { params: parseParams(params), val: text(val) }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
data = text(val)
|
||||||
|
|
||||||
|
return storeValParam(name)(data, curr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var addTZ = function (dt, params) {
|
||||||
var p = parseParams(params);
|
var p = parseParams(params);
|
||||||
|
|
||||||
if (params && p && dt){
|
if (params && p){
|
||||||
dt.tz = p.TZID
|
dt.tz = p.TZID
|
||||||
}
|
}
|
||||||
|
|
||||||
return dt
|
return dt
|
||||||
}
|
}
|
||||||
|
|
||||||
var parseTimestamp = function(val){
|
|
||||||
//typical RFC date-time format
|
|
||||||
var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val);
|
|
||||||
if (comps !== null) {
|
|
||||||
if (comps[7] == 'Z'){ // GMT
|
|
||||||
return new Date(Date.UTC(
|
|
||||||
parseInt(comps[1], 10),
|
|
||||||
parseInt(comps[2], 10)-1,
|
|
||||||
parseInt(comps[3], 10),
|
|
||||||
parseInt(comps[4], 10),
|
|
||||||
parseInt(comps[5], 10),
|
|
||||||
parseInt(comps[6], 10 )
|
|
||||||
));
|
|
||||||
// TODO add tz
|
|
||||||
} else {
|
|
||||||
return new Date(
|
|
||||||
parseInt(comps[1], 10),
|
|
||||||
parseInt(comps[2], 10)-1,
|
|
||||||
parseInt(comps[3], 10),
|
|
||||||
parseInt(comps[4], 10),
|
|
||||||
parseInt(comps[5], 10),
|
|
||||||
parseInt(comps[6], 10)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dateParam = function(name){
|
var dateParam = function(name){
|
||||||
return function(val, params, curr){
|
return function (val, params, curr) {
|
||||||
|
|
||||||
|
var newDate = text(val);
|
||||||
|
|
||||||
// Store as string - worst case scenario
|
|
||||||
storeParam(name)(val, undefined, curr)
|
|
||||||
|
|
||||||
if (params && params[0] === "VALUE=DATE") {
|
if (params && params[0] === "VALUE=DATE") {
|
||||||
// Just Date
|
// Just Date
|
||||||
@ -131,47 +108,54 @@
|
|||||||
var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val);
|
var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val);
|
||||||
if (comps !== null) {
|
if (comps !== null) {
|
||||||
// No TZ info - assume same timezone as this computer
|
// No TZ info - assume same timezone as this computer
|
||||||
curr[name] = new Date(
|
newDate = new Date(
|
||||||
comps[1],
|
comps[1],
|
||||||
parseInt(comps[2], 10)-1,
|
parseInt(comps[2], 10)-1,
|
||||||
comps[3]
|
comps[3]
|
||||||
);
|
);
|
||||||
|
|
||||||
curr[name] = addTZ(curr[name], params);
|
newDate = addTZ(newDate, params);
|
||||||
return curr;
|
newDate.dateOnly = true;
|
||||||
|
|
||||||
|
// Store as string - worst case scenario
|
||||||
|
return storeValParam(name)(newDate, curr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
curr[name] = []
|
|
||||||
val.split(',').forEach(function(val){
|
|
||||||
var newDate = parseTimestamp(val);
|
|
||||||
curr[name].push(addTZ(newDate, params));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (curr[name].length === 0){
|
//typical RFC date-time format
|
||||||
delete curr[name];
|
var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val);
|
||||||
} else if (curr[name].length === 1){
|
if (comps !== null) {
|
||||||
curr[name] = curr[name][0];
|
if (comps[7] == 'Z'){ // GMT
|
||||||
}
|
newDate = new Date(Date.UTC(
|
||||||
|
parseInt(comps[1], 10),
|
||||||
|
parseInt(comps[2], 10)-1,
|
||||||
|
parseInt(comps[3], 10),
|
||||||
|
parseInt(comps[4], 10),
|
||||||
|
parseInt(comps[5], 10),
|
||||||
|
parseInt(comps[6], 10 )
|
||||||
|
));
|
||||||
|
// TODO add tz
|
||||||
|
} else {
|
||||||
|
newDate = new Date(
|
||||||
|
parseInt(comps[1], 10),
|
||||||
|
parseInt(comps[2], 10)-1,
|
||||||
|
parseInt(comps[3], 10),
|
||||||
|
parseInt(comps[4], 10),
|
||||||
|
parseInt(comps[5], 10),
|
||||||
|
parseInt(comps[6], 10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return curr;
|
newDate = addTZ(newDate, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Store as string - worst case scenario
|
||||||
|
return storeValParam(name)(newDate, curr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var exdateParam = function(name){
|
|
||||||
return function(val, params, curr){
|
|
||||||
var date = dateParam(name)(val, params, curr);
|
|
||||||
if (date.exdates === undefined) {
|
|
||||||
date.exdates = [];
|
|
||||||
}
|
|
||||||
if (Array.isArray(date.exdate)){
|
|
||||||
date.exdates = date.exdates.concat(date.exdate);
|
|
||||||
} else {
|
|
||||||
date.exdates.push(date.exdate);
|
|
||||||
}
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var geoParam = function(name){
|
var geoParam = function(name){
|
||||||
return function(val, params, curr){
|
return function(val, params, curr){
|
||||||
@ -195,7 +179,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var addFBType = function(fb, params){
|
// EXDATE is an entry that represents exceptions to a recurrence rule (ex: "repeat every day except on 7/4").
|
||||||
|
// The EXDATE entry itself can also contain a comma-separated list, so we make sure to parse each date out separately.
|
||||||
|
// There can also be more than one EXDATE entries in a calendar record.
|
||||||
|
// Since there can be multiple dates, we create an array of them. The index into the array is the ISO string of the date itself, for ease of use.
|
||||||
|
// i.e. You can check if ((curr.exdate != undefined) && (curr.exdate[date iso string] != undefined)) to see if a date is an exception.
|
||||||
|
// NOTE: This specifically uses date only, and not time. This is to avoid a few problems:
|
||||||
|
// 1. The ISO string with time wouldn't work for "floating dates" (dates without timezones).
|
||||||
|
// ex: "20171225T060000" - this is supposed to mean 6 AM in whatever timezone you're currently in
|
||||||
|
// 2. Daylight savings time potentially affects the time you would need to look up
|
||||||
|
// 3. Some EXDATE entries in the wild seem to have times different from the recurrence rule, but are still excluded by calendar programs. Not sure how or why.
|
||||||
|
// These would fail any sort of sane time lookup, because the time literally doesn't match the event. So we'll ignore time and just use date.
|
||||||
|
// ex: DTSTART:20170814T140000Z
|
||||||
|
// RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU
|
||||||
|
// EXDATE:20171219T060000
|
||||||
|
// Even though "T060000" doesn't match or overlap "T1400000Z", it's still supposed to be excluded? Odd. :(
|
||||||
|
// TODO: See if this causes any problems with events that recur multiple times a day.
|
||||||
|
var exdateParam = function (name) {
|
||||||
|
return function (val, params, curr) {
|
||||||
|
var separatorPattern = /\s*,\s*/g;
|
||||||
|
curr[name] = curr[name] || [];
|
||||||
|
var dates = val ? val.split(separatorPattern) : [];
|
||||||
|
dates.forEach(function (entry) {
|
||||||
|
var exdate = new Array();
|
||||||
|
dateParam(name)(entry, params, exdate);
|
||||||
|
|
||||||
|
if (exdate[name])
|
||||||
|
{
|
||||||
|
if (typeof exdate[name].toISOString === 'function') {
|
||||||
|
curr[name][exdate[name].toISOString().substring(0, 10)] = exdate[name];
|
||||||
|
} else {
|
||||||
|
console.error("No toISOString function in exdate[name]", exdate[name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return curr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RECURRENCE-ID is the ID of a specific recurrence within a recurrence rule.
|
||||||
|
// TODO: It's also possible for it to have a range, like "THISANDPRIOR", "THISANDFUTURE". This isn't currently handled.
|
||||||
|
var recurrenceParam = function (name) {
|
||||||
|
return dateParam(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
var addFBType = function (fb, params) {
|
||||||
var p = parseParams(params);
|
var p = parseParams(params);
|
||||||
|
|
||||||
if (params && p){
|
if (params && p){
|
||||||
@ -255,7 +284,86 @@
|
|||||||
var par = stack.pop()
|
var par = stack.pop()
|
||||||
|
|
||||||
if (curr.uid)
|
if (curr.uid)
|
||||||
par[curr.uid] = curr
|
{
|
||||||
|
// If this is the first time we run into this UID, just save it.
|
||||||
|
if (par[curr.uid] === undefined)
|
||||||
|
{
|
||||||
|
par[curr.uid] = curr;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If we have multiple ical entries with the same UID, it's either going to be a
|
||||||
|
// modification to a recurrence (RECURRENCE-ID), and/or a significant modification
|
||||||
|
// to the entry (SEQUENCE).
|
||||||
|
|
||||||
|
// TODO: Look into proper sequence logic.
|
||||||
|
|
||||||
|
if (curr.recurrenceid === undefined)
|
||||||
|
{
|
||||||
|
// If we have the same UID as an existing record, and it *isn't* a specific recurrence ID,
|
||||||
|
// not quite sure what the correct behaviour should be. For now, just take the new information
|
||||||
|
// and merge it with the old record by overwriting only the fields that appear in the new record.
|
||||||
|
var key;
|
||||||
|
for (key in curr) {
|
||||||
|
par[curr.uid][key] = curr[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have recurrence-id entries, list them as an array of recurrences keyed off of recurrence-id.
|
||||||
|
// To use - as you're running through the dates of an rrule, you can try looking it up in the recurrences
|
||||||
|
// array. If it exists, then use the data from the calendar object in the recurrence instead of the parent
|
||||||
|
// for that day.
|
||||||
|
|
||||||
|
// NOTE: Sometimes the RECURRENCE-ID record will show up *before* the record with the RRULE entry. In that
|
||||||
|
// case, what happens is that the RECURRENCE-ID record ends up becoming both the parent record and an entry
|
||||||
|
// in the recurrences array, and then when we process the RRULE entry later it overwrites the appropriate
|
||||||
|
// fields in the parent record.
|
||||||
|
|
||||||
|
if (curr.recurrenceid != null)
|
||||||
|
{
|
||||||
|
|
||||||
|
// TODO: Is there ever a case where we have to worry about overwriting an existing entry here?
|
||||||
|
|
||||||
|
// Create a copy of the current object to save in our recurrences array. (We *could* just do par = curr,
|
||||||
|
// except for the case that we get the RECURRENCE-ID record before the RRULE record. In that case, we
|
||||||
|
// would end up with a shared reference that would cause us to overwrite *both* records at the point
|
||||||
|
// that we try and fix up the parent record.)
|
||||||
|
var recurrenceObj = new Object();
|
||||||
|
var key;
|
||||||
|
for (key in curr) {
|
||||||
|
recurrenceObj[key] = curr[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recurrenceObj.recurrences != undefined) {
|
||||||
|
delete recurrenceObj.recurrences;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// If we don't have an array to store recurrences in yet, create it.
|
||||||
|
if (par[curr.uid].recurrences === undefined) {
|
||||||
|
par[curr.uid].recurrences = new Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save off our cloned recurrence object into the array, keyed by date but not time.
|
||||||
|
// We key by date only to avoid timezone and "floating time" problems (where the time isn't associated with a timezone).
|
||||||
|
// TODO: See if this causes a problem with events that have multiple recurrences per day.
|
||||||
|
if (typeof curr.recurrenceid.toISOString === 'function') {
|
||||||
|
par[curr.uid].recurrences[curr.recurrenceid.toISOString().substring(0,10)] = recurrenceObj;
|
||||||
|
} else {
|
||||||
|
console.error("No toISOString function in curr.recurrenceid", curr.recurrenceid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One more specific fix - in the case that an RRULE entry shows up after a RECURRENCE-ID entry,
|
||||||
|
// let's make sure to clear the recurrenceid off the parent field.
|
||||||
|
if ((par[curr.uid].rrule != undefined) && (par[curr.uid].recurrenceid != undefined))
|
||||||
|
{
|
||||||
|
delete par[curr.uid].recurrenceid;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
else
|
else
|
||||||
par[Math.random()*100000] = curr // Randomly assign ID : TODO - use true GUID
|
par[Math.random()*100000] = curr // Randomly assign ID : TODO - use true GUID
|
||||||
|
|
||||||
@ -277,6 +385,11 @@
|
|||||||
, 'COMPLETED': dateParam('completed')
|
, 'COMPLETED': dateParam('completed')
|
||||||
, 'CATEGORIES': categoriesParam('categories')
|
, 'CATEGORIES': categoriesParam('categories')
|
||||||
, 'FREEBUSY': freebusyParam('freebusy')
|
, 'FREEBUSY': freebusyParam('freebusy')
|
||||||
|
, 'DTSTAMP': dateParam('dtstamp')
|
||||||
|
, 'CREATED': dateParam('created')
|
||||||
|
, 'LAST-MODIFIED': dateParam('lastmodified')
|
||||||
|
, 'RECURRENCE-ID': recurrenceParam('recurrenceid')
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,8 +41,11 @@ Module.register("clock",{
|
|||||||
|
|
||||||
// Schedule update interval.
|
// Schedule update interval.
|
||||||
var self = this;
|
var self = this;
|
||||||
|
self.lastDisplayedMinute = null;
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
self.updateDom();
|
if (self.config.displaySeconds || self.lastDisplayedMinute !== moment().minute()) {
|
||||||
|
self.updateDom();
|
||||||
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// Set locale.
|
// Set locale.
|
||||||
@ -75,6 +78,7 @@ Module.register("clock",{
|
|||||||
// See issue: https://github.com/MichMich/MagicMirror/issues/181
|
// See issue: https://github.com/MichMich/MagicMirror/issues/181
|
||||||
var timeString;
|
var timeString;
|
||||||
var now = moment();
|
var now = moment();
|
||||||
|
this.lastDisplayedMinute = now.minute();
|
||||||
if (this.config.timezone) {
|
if (this.config.timezone) {
|
||||||
now.tz(this.config.timezone);
|
now.tz(this.config.timezone);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ modules: [
|
|||||||
config: {
|
config: {
|
||||||
// See 'Configuration options' for more information.
|
// See 'Configuration options' for more information.
|
||||||
location: "Amsterdam,Netherlands",
|
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.
|
appid: "abcde12345abcde12345abcde12345ab" //openweathermap.org API key.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,5 +28,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Uncomment the line below to see the contents of the `current` object. -->
|
<!-- Uncomment the line below to see the contents of the `forecast` object. -->
|
||||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{forecast | dump}}</div> -->
|
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{forecast | dump}}</div> -->
|
||||||
|
@ -19,7 +19,7 @@ modules: [
|
|||||||
config: {
|
config: {
|
||||||
// See 'Configuration options' for more information.
|
// See 'Configuration options' for more information.
|
||||||
location: "Amsterdam,Netherlands",
|
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.
|
appid: "abcde12345abcde12345abcde12345ab" //openweathermap.org API key.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,6 @@
|
|||||||
"UPDATE_INFO_SINGLE": "The current installation is {COMMIT_COUNT} commit behind on the {BRANCH_NAME} branch.",
|
"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.",
|
"UPDATE_INFO_MULTIPLE": "The current installation is {COMMIT_COUNT} commits behind on the {BRANCH_NAME} branch.",
|
||||||
|
|
||||||
"FEELS": "Feels",
|
"FEELS": "Feels like"
|
||||||
"PRECIP": "PoP"
|
"PRECIP": "PoP"
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
"RUNNING": "Päättyy {timeUntilEnd} päästä",
|
"RUNNING": "Päättyy {timeUntilEnd} päästä",
|
||||||
"EMPTY": "Ei tulevia tapahtumia.",
|
"EMPTY": "Ei tulevia tapahtumia.",
|
||||||
|
|
||||||
|
"WEEK": "Viikko {weekNumber}",
|
||||||
|
|
||||||
"N": "P",
|
"N": "P",
|
||||||
"NNE": "PPI",
|
"NNE": "PPI",
|
||||||
"NE": "PI",
|
"NE": "PI",
|
||||||
@ -25,5 +27,7 @@
|
|||||||
"NNW": "PPL",
|
"NNW": "PPL",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² päivitys saatavilla.",
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
"WNW": "ЗСЗ",
|
"WNW": "ЗСЗ",
|
||||||
"NW": "СЗ",
|
"NW": "СЗ",
|
||||||
"NNW": "ССЗ",
|
"NNW": "ССЗ",
|
||||||
|
"FEELS": "По ощущению",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "Есть обновление для MagicMirror².",
|
"UPDATE_NOTIFICATION": "Есть обновление для MagicMirror².",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Есть обновление для {MODULE_NAME} модуля.",
|
"UPDATE_NOTIFICATION_MODULE": "Есть обновление для {MODULE_NAME} модуля.",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user