MediaWiki:Gadget-eventslist.js: Difference between revisions

From [N8]
Jump to navigation Jump to search
Created page with "//<nowiki> * * Interface for events * * @version 1.1 * @author JaydenKieran * @author Elessar2 * @author ThePsionic *: /*global jQuery, mediaWiki, mw, gswiki, rs..."
 
No edit summary
 
(2 intermediate revisions by the same user not shown)
Line 16: Line 16:
// TODO: Cache Vos as well
// TODO: Cache Vos as well


;(function($, mw, rs){
;(function($, mw, gs){
var portletLink,
var portletLink,
$popup,
$popup,
Line 49: Line 49:
{ name:'Wilderness Warbands', id:'wildwar', link:'/w/Wilderness_Warbands', length:10, repeat:(60 * 7), offset:(60 * 2), from:'isoWeek', img:'/images/5/57/Wilderness_Warbands_emblem.png' },
{ name:'Wilderness Warbands', id:'wildwar', link:'/w/Wilderness_Warbands', length:10, repeat:(60 * 7), offset:(60 * 2), from:'isoWeek', img:'/images/5/57/Wilderness_Warbands_emblem.png' },
];
];
 
var self = {
// VoS Locations
var voslocs = { 'Amlodd':['WNW','West-north-west'], 'Crwys':['ENE','East-north-east'], 'Cadarn':['ESE','East-south-east'], 'Hefin':['NNW','North-north-west'],
'Iorwerth':['SSW','South-south-west'], 'Ithell':['WSW','West-south-west'], 'Meilyr':['NNE','North-north-east'], 'Trahaearn':['SSE','South-south-east'] };
// Spotlight lists
var dd_spotlights = ['Shooting Star', 'Penguin Hide and Seek', 'Circus', 'Evil Tree'],
mg_spotlights = ['Pest Control', 'Soul Wars', 'Fist of Guthix', 'Barbarian Assault', 'Conquest', 'Fishing Trawler', 'The Great Orb Project', 'Flash Powder Factory', 'Castle Wars', 'Stealing Creation', 'Cabbage Facepunch Bonanza', 'Heist', 'Soul Wars', 'Barbarian Assault',
'Conquest', 'Fist of Guthix', 'Castle Wars', 'Pest Control', 'Soul Wars', 'Fishing Trawler', 'The Great Orb Project', 'Flash Powder Factory', 'Stealing Creation', 'Cabbage Facepunch Bonanza', 'Heist', 'Trouble Brewing', 'Castle Wars'],
ed_spotlights = ['Temple of Aminishi', 'Dragonkin Laboratory', 'The Shadow Reef'];
var self = {
// Settings
// Settings
settings: {
settings: {
Line 137: Line 147:


// Check browser supports notifications
// Check browser supports notifications
self.supportNotif = rs.canSendBrowserNotifs();
self.supportNotif = gs.canSendBrowserNotifs();
// TODO: Check if using service worker notifications
// TODO: Check if using service worker notifications


Line 216: Line 226:
autoClose: true,
autoClose: true,
classes: ['gsw-events-popup oo-ui-labelElement-invisible'],
classes: ['gsw-events-popup oo-ui-labelElement-invisible'],
hideWhenOutOfView: (rs.isUsingStickyHeader() ? true : false),
hideWhenOutOfView: (gs.isUsingStickyHeader() ? true : false),
invisibleLabel: true,
invisibleLabel: true,
label: 'Events',
label: 'Events',
Line 276: Line 286:
mw.log('Loading settings');
mw.log('Loading settings');
return new Promise( function (resolve, reject) {
return new Promise( function (resolve, reject) {
if (!rs.hasLocalStorage()) {
if (!gs.hasLocalStorage()) {
console.warn('Browser does not support localStorage');
console.warn('Browser does not support localStorage');
reject();
reject();
Line 294: Line 304:
} );
} );
},
},
 
/**
/**
* Loads events team events, travelling merchant stock
* @return {Promise}
*/
loadEvents: function ( force ) {
mw.log('Loading events, travelling merchant stock');
if ( moment().isBefore( moment(self.settings.dcache).utc().add(1, 'days').startOf('day') ) && !force ) {
mw.log('Less than a day since last lookup');
et_events = self.settings.cache.events;
merchstock = {
tod: self.settings.cache.tms,
tmr: self.settings.cache.tmsTom
};
return Promise.resolve();
}
 
// ET events
var etapi = api.get({
action: 'ask',
query: '[[RuneScape:Events Team]]|?Events JSON',
maxage: '7200',
smaxage: '7200'
}).then( function (ret) {
mw.log('Events team api return:');
mw.log(ret);
if (ret.query.results && ret.query.results['RuneScape:Events Team'].printouts['Events JSON']) {
ret.query.results['RuneScape:Events Team'].printouts['Events JSON'].forEach( function (ev) {
ev = JSON.parse(ev);
et_events.push( { name:mw.html.escape(ev.name), date:ev.time, length:Number(ev.length) } );
});
} else {
mw.log('No events team events');
}
self.saveSetting('cache.events', et_events);
}, function (err) {
console.warn('Error getting Events team events');
throw err;
});
 
// Resolve/reject all
return Promise.all([ etapi, tmsapi, tmsTomapi ]).then( function () {
// Success, resolve promise
self.saveSetting('dcache', moment().format());
}, function (err) {
// Error, reject promise
throw err;
});
},
 
/**
* Saves a setting to localStorage
* Saves a setting to localStorage
* @param  {string} name Name (key) of setting to save
* @param  {string} name Name (key) of setting to save
Line 357: Line 318:
}
}


if (!rs.hasLocalStorage()) {
if (!gs.hasLocalStorage()) {
console.warn('Browser does not support localStorage');
console.warn('Browser does not support localStorage');
return false;
return false;
Line 382: Line 343:
self.settings.notiftype[key] = value;
self.settings.notiftype[key] = value;


if (!rs.hasLocalStorage()) {
if (!gs.hasLocalStorage()) {
console.warn('Browser does not support localStorage');
console.warn('Browser does not support localStorage');
return false;
return false;
Line 643: Line 604:
settingsForm.eventsNumLay.toggle(false);
settingsForm.eventsNumLay.toggle(false);
}
}
// Show spotlights (combined)
settingsForm.spots = new OO.ui.ToggleSwitchWidget({
title: 'Show D&D, Minigame and ED spotlights',
value: self.settings.spots
});
settingsForm.spotsLay = new OO.ui.FieldLayout(settingsForm.spots, {
label: 'Spotlights',
help: 'Show Minigame Spotlight, Featured D&D and Elite Dungeon Spotlight'
});
settingsForm.spots.on('change', function (val) {
self.saveSetting('mg_spotlight', val);
self.saveSetting('dd_spotlight', val);
self.saveSetting('ed_spotlight', val);
});
// Show minigame spotlight
// Show minigame spotlight
settingsForm.mg = new OO.ui.ToggleSwitchWidget({
settingsForm.mg = new OO.ui.ToggleSwitchWidget({
Line 650: Line 627:
settingsForm.mgLay = new OO.ui.FieldLayout(settingsForm.mg, { label: 'Minigame Spotlight' });
settingsForm.mgLay = new OO.ui.FieldLayout(settingsForm.mg, { label: 'Minigame Spotlight' });
settingsForm.mg.on('change', self.saveSetting, ['mg_spotlight']);
settingsForm.mg.on('change', self.saveSetting, ['mg_spotlight']);
// Show featured D&D spotlight
settingsForm.dd = new OO.ui.ToggleSwitchWidget({
title: 'Show featured D&D',
value: self.settings.dd_spotlight
});
settingsForm.ddLay = new OO.ui.FieldLayout(settingsForm.dd, { label: 'Featured D&D' });
settingsForm.dd.on('change', self.saveSetting, ['dd_spotlight']);
// Show elite dungeon spotlight
// Show elite dungeon spotlight
settingsForm.ed = new OO.ui.ToggleSwitchWidget({
settingsForm.ed = new OO.ui.ToggleSwitchWidget({
Line 676: Line 662:
settingsForm.pvmLay = new OO.ui.FieldLayout(settingsForm.pvm, { label: 'PvM Rotations' });
settingsForm.pvmLay = new OO.ui.FieldLayout(settingsForm.pvm, { label: 'PvM Rotations' });
settingsForm.pvm.on('change', self.saveSetting, ['pvm']);
settingsForm.pvm.on('change', self.saveSetting, ['pvm']);
 
// Settings form footer
// Force update events and travelling merchant stock
settingsForm.fupdate = new OO.ui.ButtonWidget({
label: 'Refresh',
icon: 'reload',
title: 'Force refresh of travelling merchant stock and events'
});
settingsForm.fupdateLay = new OO.ui.FieldLayout(settingsForm.fupdate, {
label: 'Force TMS Update',
help: 'Force the travelling merchant stock and events team events to update regardless of the last update time.'
});
settingsForm.fupdate.on('click', function () {
self.loadEvents(true).then(self.updateAll(), self.updateAll());
});
 
settingsForm.form =  new OO.ui.FieldsetLayout({ classes:'settings-form' });
settingsForm.form.addItems([
settingsForm.sortLay,
settingsForm.notifsLay,
settingsForm.ravenLay,
settingsForm.eventsLay,
settingsForm.eventsNumLay,
settingsForm.tmsLay,
settingsForm.pofLay,
settingsForm.spotsLay,
settingsForm.nextLay,
settingsForm.pvmLay,
settingsForm.fupdateLay
]);
 
// Settings form footer
settingsForm.$footer = $('<div>')
settingsForm.$footer = $('<div>')
.addClass('footer')
.addClass('footer')
Line 815: Line 830:
label: 'Monthly Reset'
label: 'Monthly Reset'
});
});
 
// Events team events
// All Voice of Seren
notifsForm.etevents = new OO.ui.ToggleSwitchWidget({
notifsForm.vos = new OO.ui.DropdownInputWidget({
title: 'Events team notifications',
options: [
value: self.settings.notiftype.etevents
{ data:'None', label:'None' },
{ data:'All', label:'All' },
{ data:'Some', label:'Some' }
],
title: 'Receive notifications for Voice of Seren',
});
});
notifsForm.etevents.on('change', self.saveNotif, ['etevents']);
notifsForm.vos.setValue(self.settings.notiftype.vos);
notifsForm.eteventsLay = new OO.ui.FieldLayout(notifsForm.etevents, {
notifsForm.vosLay = new OO.ui.FieldLayout(notifsForm.vos, {
label: 'Events Team',
label: 'Voice of Seren',
help: 'Receive notifications for RuneScape Wiki events team events'
help: 'Receive notifications for Voice of Seren'
});
});
var togglevos = function (val) {
self.saveNotif('vos', val);


// PVM
if (val == 'Some') {
notifsForm.vorago = new OO.ui.ToggleSwitchWidget({
notifsForm.vosselLay.toggle(true);
title: 'Vorago attack notifications',
self.saveNotif('allvos', false);
value: self.settings.notiftype.vorago
} else if (val == 'All') {
notifsForm.vosselLay.toggle(false);
self.saveNotif('allvos', true);
} else {
notifsForm.vosselLay.toggle(false);
self.saveNotif('allvos', false);
}
};
notifsForm.vos.on('change', togglevos);
// Individual VoS
notifsForm.vossel = new OO.ui.CheckboxMultiselectWidget({ classes: ['notifs-group'] });
for (var clan in voslocs) {
notifsForm[clan] = new OO.ui.CheckboxMultioptionWidget({
label: clan,
data: clan,
selected: self.settings.notiftype['vos' + clan]
});
 
notifsForm[clan].on('change', self.saveNotif, ['vos' + clan]);
 
notifsForm.vossel.addItems([ notifsForm[clan] ]);
}
notifsForm.vosselLay = new OO.ui.FieldLayout(notifsForm.vossel, {
align: 'top',
label: 'Individual Clan Districts',
help: 'Choose the Prifddinas clan districts you wish to be notified about when a Voice of Seren is active.'
});
});
notifsForm.vorago.on('change', self.saveNotif, ['vorago']);
if (self.settings.notiftype.vos != 'Some') {
notifsForm.voragoLay = new OO.ui.FieldLayout(notifsForm.vorago, {
notifsForm.vosselLay.toggle(false);
label: 'Vorago',
}
help: 'Receive notifications about current Vorago attack'
 
// Events team events
notifsForm.etevents = new OO.ui.ToggleSwitchWidget({
title: 'Events team notifications',
value: self.settings.notiftype.etevents
});
});
notifsForm.araxxor = new OO.ui.ToggleSwitchWidget({
notifsForm.etevents.on('change', self.saveNotif, ['etevents']);
title: 'Araxxor path notifications',
notifsForm.eteventsLay = new OO.ui.FieldLayout(notifsForm.etevents, {
value: self.settings.notiftype.araxxor
label: 'Events Team',
});
help: 'Receive notifications for RuneScape Wiki events team events'
notifsForm.araxxor.on('change', self.saveNotif, ['araxxor']);
notifsForm.araxxorLay = new OO.ui.FieldLayout(notifsForm.araxxor, {
label: 'Araxxor',
help: 'Receive notifications about open Araxxor paths'
});
notifsForm.rots = new OO.ui.ToggleSwitchWidget({
title: 'Rise of the Six notifications',
value: self.settings.notiftype.rots
});
notifsForm.rots.on('change', self.saveNotif, ['rots']);
notifsForm.rotsLay = new OO.ui.FieldLayout(notifsForm.rots, {
label: 'RotS',
title: 'Rise of the Six',
help: 'Receive notifications for Rise of the Six rotations'
});
});


// Spotlights
// All PoF Resets
notifsForm.mg_spotlight = new OO.ui.ToggleSwitchWidget({
notifsForm.pof = new OO.ui.DropdownInputWidget({
title: 'Minigame spotlight notifications',
options: [
value: self.settings.notiftype.mg_spotlight
{ data:'None', label:'None' },
{ data:'All', label:'All' },
{ data:'Some', label:'Some' }
],
title: 'Receive notifications for player-owned farm resets',
});
});
notifsForm.mg_spotlight.on('change', self.saveNotif, ['mg_spotlight']);
notifsForm.pof.setValue(self.settings.notiftype.pof);
notifsForm.mg_spotlightLay = new OO.ui.FieldLayout(notifsForm.mg_spotlight, {
notifsForm.pofLay = new OO.ui.FieldLayout(notifsForm.pof, {
label: 'Minigame Spotlight',
label: 'Player-owned Farm',
help: 'Receive notifications for current Minigame Spotlight'
help: 'Receive notifications for player-owned farm resets'
});
});
notifsForm.ed_spotlight = new OO.ui.ToggleSwitchWidget({
var togglepof = function (val) {
title: 'Elite dungeon spotlight notifications',
self.saveNotif('pof', val);
value: self.settings.notiftype.ed_spotlight
 
if (val == 'Some') {
notifsForm.pofselLay.toggle(true);
self.saveNotif('allpof', false);
} else if (val == 'All') {
notifsForm.pofselLay.toggle(false);
self.saveNotif('allpof', true);
} else {
notifsForm.pofselLay.toggle(false);
self.saveNotif('allpof', false);
}
};
notifsForm.pof.on('change', togglepof);
// Individual PoF resets
notifsForm.pofsel = new OO.ui.CheckboxMultiselectWidget({ classes: ['notifs-group'] });
pofresets.forEach( function (res) {
notifsForm[res] = new OO.ui.CheckboxMultioptionWidget({
label: res,
data: res,
selected: self.settings.notiftype['pof' + res]
});
 
notifsForm[res].on('change', self.saveNotif, ['pof' + res]);
 
notifsForm.pofsel.addItems([ notifsForm[res] ]);
});
});
notifsForm.ed_spotlight.on('change', self.saveNotif, ['ed_spotlight']);
notifsForm.pofselLay = new OO.ui.FieldLayout(notifsForm.pofsel, {
notifsForm.ed_spotlightLay = new OO.ui.FieldLayout(notifsForm.ed_spotlight, {
align: 'top',
label: 'Elite Dungeon Spotlight',
label: 'Individual PoF resets',
help: 'Receive notifications for current Elite dungeon spotlight'
help: 'Choose the player-owned farm resets you wish to be notified about'
});
});
notifsForm.dd_spotlight = new OO.ui.ToggleSwitchWidget({
if (self.settings.notiftype.pof != 'Some') {
title: 'Distraction and diversion of the week notifications',
notifsForm.vosselLay.toggle(false);
value: self.settings.notiftype.dd_spotlight
}
 
// PVM
notifsForm.vorago = new OO.ui.ToggleSwitchWidget({
title: 'Vorago attack notifications',
value: self.settings.notiftype.vorago
});
});
notifsForm.dd_spotlight.on('change', self.saveNotif, ['dd_spotlight']);
notifsForm.vorago.on('change', self.saveNotif, ['vorago']);
notifsForm.dd_spotlightLay = new OO.ui.FieldLayout(notifsForm.dd_spotlight, {
notifsForm.voragoLay = new OO.ui.FieldLayout(notifsForm.vorago, {
label: 'Distraction & Diversions',
label: 'Vorago',
help: 'Receive notifications for Distraction and Diversion of the week'
help: 'Receive notifications about current Vorago attack'
});
});
 
notifsForm.form = new OO.ui.FieldsetLayout({ classes:'notifs-form' });
// Spotlights
notifsForm.form.addItems([
notifsForm.mg_spotlight = new OO.ui.ToggleSwitchWidget({
notifsForm.browsnotifLay,
title: 'Minigame spotlight notifications',
notifsForm.dailiesLay,
value: self.settings.notiftype.mg_spotlight
notifsForm.dailiesselLay,
});
notifsForm.ravenLay,
notifsForm.mg_spotlight.on('change', self.saveNotif, ['mg_spotlight']);
notifsForm.dresetLay,
notifsForm.mg_spotlightLay = new OO.ui.FieldLayout(notifsForm.mg_spotlight, {
notifsForm.wresetLay,
label: 'Minigame Spotlight',
notifsForm.mresetLay,
help: 'Receive notifications for current Minigame Spotlight'
notifsForm.vosLay,
});
notifsForm.vosselLay,
notifsForm.ed_spotlight = new OO.ui.ToggleSwitchWidget({
notifsForm.tmsLay,
title: 'Elite dungeon spotlight notifications',
notifsForm.tmsselLay,
value: self.settings.notiftype.ed_spotlight
notifsForm.pofLay,
});
notifsForm.pofselLay,
notifsForm.ed_spotlight.on('change', self.saveNotif, ['ed_spotlight']);
notifsForm.eteventsLay,
notifsForm.ed_spotlightLay = new OO.ui.FieldLayout(notifsForm.ed_spotlight, {
notifsForm.voragoLay,
label: 'Elite Dungeon Spotlight',
notifsForm.araxxorLay,
help: 'Receive notifications for current Elite dungeon spotlight'
notifsForm.rotsLay,
});
notifsForm.mg_spotlightLay,
notifsForm.dd_spotlight = new OO.ui.ToggleSwitchWidget({
notifsForm.ed_spotlightLay,
title: 'Distraction and diversion of the week notifications',
notifsForm.dd_spotlightLay
value: self.settings.notiftype.dd_spotlight
]);
 
var $notifoverlay = $('<div>').addClass('notifs-overlay');
$notifoverlay.click( function () {
$('.gsw-events-popup .events-notifs').addClass('slideout');
setTimeout( function () {
$('.gsw-events-popup .events-notifs').removeClass('slideout').addClass('oo-ui-element-hidden');
}, 500);
});
});
notifsForm.dd_spotlight.on('change', self.saveNotif, ['dd_spotlight']);
notifsForm.dd_spotlightLay = new OO.ui.FieldLayout(notifsForm.dd_spotlight, {
label: 'Distraction & Diversions',
help: 'Receive notifications for Distraction and Diversion of the week'
});
notifsForm.form = new OO.ui.FieldsetLayout({ classes:'notifs-form' });
notifsForm.form.addItems([
notifsForm.browsnotifLay,
notifsForm.dailiesLay,
notifsForm.dailiesselLay,
notifsForm.ravenLay,
notifsForm.dresetLay,
notifsForm.wresetLay,
notifsForm.mresetLay,
notifsForm.vosLay,
notifsForm.vosselLay,
notifsForm.tmsLay,
notifsForm.tmsselLay,
notifsForm.pofLay,
notifsForm.pofselLay,
notifsForm.eteventsLay,
notifsForm.voragoLay,
notifsForm.araxxorLay,
notifsForm.rotsLay,
notifsForm.mg_spotlightLay,
notifsForm.ed_spotlightLay,
notifsForm.dd_spotlightLay
]);


// Complete form
var $notifoverlay = $('<div>').addClass('notifs-overlay');
notifsForm.$form = $('<div>')
$notifoverlay.click( function () {
.addClass('events-notifs oo-ui-element-hidden')
$('.gsw-events-popup .events-notifs').addClass('slideout');
.append(
setTimeout( function () {
$notifoverlay,
$('.gsw-events-popup .events-notifs').removeClass('slideout').addClass('oo-ui-element-hidden');
$('<div>')
}, 500);
.addClass('notifs-container')
});
.append(
notifsForm.back.$element,
notifsForm.$title,
notifsForm.form.$element
)
);
},
 
/**
* Updates all events dropdown values (used on  opening)
* @return {undefined}
*/
updateAll: function () {
var now = moment.utc();
$content.find('.cur-utc-time')
.attr({ 'title':now.format('HH:mm D/M/Y'), 'datetime':now.format() })
.text(now.format('HH:mm') + ' (UTC)');
self.updateTimes();
self.updateSpots();
if ( now.isAfter(moment(self.nextvos)) ) {
self.updateVos();
} else {
self.updateVostime();
}
},
 
/**
* Continuously updates the values in Events dropdown
* @return {undefined}
*/
update: function () {
self.updateTimes();
 
// Current UTC time
var now = moment.utc();


// Complete form
notifsForm.$form = $('<div>')
.addClass('events-notifs oo-ui-element-hidden')
.append(
$notifoverlay,
$('<div>')
.addClass('notifs-container')
.append(
notifsForm.back.$element,
notifsForm.$title,
notifsForm.form.$element
)
);
},
/**
* Updates all events dropdown values (used on  opening)
* @return {undefined}
*/
updateAll: function () {
var now = moment.utc();
$content.find('.cur-utc-time')
$content.find('.cur-utc-time')
.attr({ 'title':now.format('HH:mm D/M/Y'), 'datetime':now.format() })
.attr({ 'title':now.format('HH:mm D/M/Y'), 'datetime':now.format() })
.text(now.format('HH:mm') + ' (UTC)');
.text(now.format('HH:mm') + ' (UTC)');
 
self.updateTimes();
// Check if it's after a new hour, update VoS
self.updateSpots();
if ( now.isAfter(moment(self.nextvos)) ) {
if ( now.isAfter(moment(self.nextvos)) ) {
self.updateVos();
self.updateVos();
} else {
} else {
self.updateVostime();
self.updateVostime();
}
// Check if it's after a new day, update PVM etc
if ( now.isBetween(moment(now).startOf('day'), moment(now).startOf('day').add(1, 'minutes'), null, '[]') ) {
self.updateSpots();
}
}
},
},


/**
/**
* Update time based events in the Events popup
* Continuously updates the values in Events dropdown
* Dailies, Resets, Events team events
* @return {undefined}
* @return {[type]} [description]
*/
*/
updateTimes: function () {
update: function () {
self.updateTimes();
 
// Current UTC time
// Current UTC time
var now = moment.utc();
var now = moment.utc();


// Dailies
$content.find('.cur-utc-time')
$content.find('.daily-list').empty();
.attr({ 'title':now.format('HH:mm D/M/Y'), 'datetime':now.format() })
var dailies_array = [];
.text(now.format('HH:mm') + ' (UTC)');
simple_dailies.forEach( function (dd) {
 
var next = moment(now).startOf(dd.from).add((dd.offset), 'minutes'),
// Check if it's after a new hour, update VoS
nextEnd = moment(next).add(dd.length, 'minutes');
if ( now.isAfter(moment(self.nextvos)) ) {
while (!now.isBefore(nextEnd)) {
self.updateVos();
next.add(dd.repeat, 'minutes');
} else {
self.updateVostime();
}
 
// Check if it's after a new day, update PVM etc
if ( now.isBetween(moment(now).startOf('day'), moment(now).startOf('day').add(1, 'minutes'), null, '[]') ) {
self.updateSpots();
}
},
 
/**
* Update time based events in the Events popup
* Dailies, Resets, Events team events
* @return {[type]} [description]
*/
updateTimes: function () {
// Current UTC time
var now = moment.utc();
 
// Dailies
$content.find('.daily-list').empty();
var dailies_array = [];
simple_dailies.forEach( function (dd) {
var next = moment(now).startOf(dd.from).add((dd.offset), 'minutes'),
nextEnd = moment(next).add(dd.length, 'minutes');
while (!now.isBefore(nextEnd)) {
next.add(dd.repeat, 'minutes');
nextEnd.add(dd.repeat, 'minutes');
nextEnd.add(dd.repeat, 'minutes');
}
}
Line 1,123: Line 1,221:
var now = moment.utc();
var now = moment.utc();


// Append to content
// Error updating VoS
$content.find('.pvm').append(
var vosError = function (jqXHR, status, error) {
$('<div>')
console.warn('Error loading VoS:\n' + status + ': ' + error);
.addClass('col col-l')
$content.find('.VoS-time').empty().append('There was an error loading the Voice of Seren. Please try again later.<br/>If this issue persists, please <a href="/w/RS:AR">contact an administrator</a>.');
.append(
$content.find('.VoS-container').empty();
$('<h3>').html('<a href="/w/Vorago" title="Vorago">Vorago</a>'),
$content.find('.VoS-last').empty().append('not found');
$vorago
};
),
// Update Voice of Seren (VoS)
$('<div>')
var vosSucc = function (vosjson, status, jqXHR) {
.addClass('col col-r')
mw.log(vosjson);
.append(
if (!(vosjson.data && vosjson.data[0] && vosjson.data[0].districts)) {
$('<div>')
vosError({}, 'Missing districts', 'returned json did not contain a districts array');
.addClass('section araxxor')
return;
.append(
}
$('<h3>').html('<a href="/w/Araxxor" title="Araxxor">Araxxor</a>'),
 
$araxxor
var voschng = moment(now).add(1, 'hours').startOf('hour'),
),
chngtitle = voschng.format('MMM D') + ' at ' + voschng.format('HH:mm') + ' game time',
$('<div>')
currvos = vosjson.data[0].districts,
.addClass('section rots')
lastvos = vosjson.data[1].districts;
.append(
$('<h3>').attr('title', 'Barrows: Rise of the Six').html('<a href="/w/Barrows:_Rise_of_the_Six" title="Barrows: Rise of the Six">Rise of the Six</a>'),
$content.find('.VoS-time').empty().append(
$rots
$('<time>').addClass('time').attr({ 'datetime':voschng.format(), 'title':chngtitle }).text(voschng.fromNow(true)),
)
' until new districts are active.'
);
$content.find('.VoS-container').empty().append(
$('<div>').addClass('event-box VoS-district').append(
$('<div>').addClass('VoS-image VoS-'+currvos[0]),
$('<div>').addClass('VoS-text').append(
$('<span>').addClass('by-line').attr('title', voslocs[currvos[0]][1]).text(voslocs[currvos[0]][0]),
$('<label>').addClass('event-name').html('<a href="/w/' + currvos[0] + '_Clan" title="' + currvos[0] + ' Clan">' + currvos[0] + '</a>')
)
),
$('<div>').addClass('event-box VoS-district').append(
$('<div>').addClass('VoS-image VoS-'+currvos[1]),
$('<div>').addClass('VoS-text').append(
$('<span>').addClass('by-line').attr('title', voslocs[currvos[1]][1]).text(voslocs[currvos[1]][0]),
$('<label>').addClass('event-name').html('<a href="/w/' + currvos[1] + '_Clan" title="' + currvos[1] + ' Clan">' + currvos[1] + '</a>')
)
)
)
);
);
}


// Spotlights container empty?
$content.find('.VoS-last').empty();
if( !self.settings.dd_spotlight && !self.settings.mg_spotlight && !self.settings.ed_spotlight ) {
$content.find('.VoS-last').append(lastvos[0] + ', ' + lastvos[1]);
$content.addClass('spotlights-empty');
} else {
$content.removeClass('spotlights-empty');
}
$content.find('.spotlight-spacer').removeClass('first');


// Minigame Spotlight
// Set time for next check
$content.find('.mg-spotlight').empty().prev().addClass('empty');
self.nextvos = moment(now).startOf('hour').add(61, 'minutes').format();
if (self.settings.mg_spotlight) {
};
var mg_array = spotlights(mg_spotlights, 3, 49);
$content.find('.mg-spotlight').prev().removeClass('empty');
$content.find('.mg-spotlight').append(
$('<label>').text('Minigame Spotlight'),
$('<span>').addClass('spotlight-value').html('<a href="/w/' + mg_array[0] + '" title="' + mg_array[0] + '">' + mg_array[0] + '</a>')
);
if (self.settings.show_next) {
$content.find('.mg-spotlight').append(
$('<label>').addClass('sl-next').text('Next:'),
$('<span>').addClass('sl-next mg-spotlight-next').text(mg_array[2] + ' in '),
$('<time>').addClass('sl-next mg-spotlight-in').attr({ 'datetime':mg_array[3], 'title':mg_array[4] }).text(mg_array[1])
);
}
} else {
$content.find('.mg-spotlight').next().addClass('first');
}


// Elite dungeon spotlight
// Make api Call
$content.find('.ed-spotlight').empty().prev().addClass('empty');
$content.find('.VoS-time').empty().append('Loading current VoS...');
if (self.settings.ed_spotlight) {
$content.find('.VoS-container').empty();
var ed_array = spotlights(ed_spotlights, 1, 0);
$content.find('.VoS-last').empty().append('Loading last VoS...');
$content.find('.ed-spotlight').prev().removeClass('empty');
$.ajax({
$content.find('.ed-spotlight').append(
dataType: 'json',
$('<label>').text('Elite Dungeon Spotlight'),
url: vosurl,
$('<span>').addClass('spotlight-value').html('<a href="/w/' + ed_array[0] + '" title="' + ed_array[0] + '">' + ed_array[0] + '</a>')
error: vosError,
);
success: vosSucc
if (self.settings.show_next) {
});
$content.find('.ed-spotlight').append(
},
$('<label>').addClass('sl-next').text('Next:'),
 
$('<span>').addClass('sl-next ed-spotlight-next').text(ed_array[2] + ' in '),
/**
$('<time>').addClass('sl-next ed-spotlight-in').attr({ 'datetime':ed_array[3], 'title':ed_array[4] }).text(ed_array[1])
* Update the Voice of Seren time display only
);
* @return {undefined}
*/
updateVostime: function () {
// Next change
var voschng = moment.utc().add(1, 'hours').startOf('hour'),
chngtitle = voschng.format('MMM D') + ' at ' + voschng.format('HH:mm') + ' game time';
// Update time display
$content.find('.VoS-time').empty().append(
$('<time>').addClass('time').attr({ 'datetime':voschng.format(), 'title':chngtitle }).text(voschng.fromNow(true)),
' until new districts are active.'
);
},
/**
* Opens the settings form
* @return {undefined}
*/
openSettings: function () {
// Update settings, switch window
var switchWind = function () {
// Update form elements
settingsForm.sort.setValue(self.settings.daily_sort);
settingsForm.notifs.setValue(self.settings.notifs);
// Disable if notifications disabled
if (self.supportNotif) {
settingsForm.notifs.setDisabled(false);
} else {
settingsForm.notifs.setDisabled(true);
}
}
} else if (!self.settings.mg_spotlight) {
settingsForm.raven.setValue(self.settings.raven);
$content.find('.ed-spotlight').prev().removeClass('first');
settingsForm.events.setValue(self.settings.et_events);
$content.find('.ed-spotlight').next().addClass('first');
settingsForm.eventsNum.setValue(self.settings.num_events);
}
settingsForm.eventsNum.setDisabled(!self.settings.et_events);
settingsForm.eventsNumLay.toggle(self.settings.et_events);
/**
settingsForm.tms.setValue(self.settings.merch);
* Sends notifications to the browser based on user prefs
settingsForm.pof.setValue(self.settings.farm);
settingsForm.spots.setValue(self.settings.spots);
//settingsForm.mg.setValue(self.settings.mg_spotlight);
//settingsForm.dd.setValue(self.settings.dd_spotlight);
//settingsForm.ed.setValue(self.settings.ed_spotlight);
settingsForm.next.setValue(self.settings.show_next);
settingsForm.pvm.setValue(self.settings.pvm);
$('.gsw-events-popup .events-settings').removeClass('oo-ui-element-hidden');
$('.gsw-events-popup .events-settings').addClass('slideout');
setTimeout( function () {
$('.gsw-events-popup .events-settings').removeClass('slideout');
}, 5);
};
self.loadSettings().then(switchWind(), switchWind());
},
 
/**
* Opens the notifications form
* @return {undefined}
* @return {undefined}
*/
*/
notifs: function () {
openNotifs: function () {
var switchWind = function () {
// Update form elements
notifsForm.browsnotif.setValue(self.settings.nobrownotifs);
notifsForm.dailies.setValue(self.settings.notiftype.dailies);
simple_dailies.forEach( function (dd) {
notifsForm[dd.id].setSelected(self.settings.notiftype[dd.id]);
});
notifsForm.raven.setValue(self.settings.notiftype.raven);
notifsForm.dreset.setValue(self.settings.notiftype.dreset);
notifsForm.wreset.setValue(self.settings.notiftype.wreset);
notifsForm.mreset.setValue(self.settings.notiftype.mreset);
notifsForm.vos.setValue(self.settings.notiftype.vos);
for (var clan in voslocs) {
notifsForm[clan].setSelected(self.settings.notiftype['vos' + clan]);
}
notifsForm.etevents.setValue(self.settings.notiftype.etevents);
notifsForm.tms.setValue(self.settings.notiftype.tms);
if (self.settings.notiftype.tmsitems) {
var tmsitems = self.settings.notiftype.tmsitems;
notifsForm.tmssel.clearItems();
tmsitems.forEach( function (item) {
notifsForm.tmssel.addTag(item, item);
});
} else {
notifsForm.tmssel.clearItems();
}
notifsForm.pof.setValue(self.settings.notiftype.pof);
pofresets.forEach( function (fres) {
notifsForm[fres].setSelected(self.settings.notiftype['pof' + fres]);
});
notifsForm.vorago.setValue(self.settings.notiftype.vorago);
notifsForm.araxxor.setValue(self.settings.notiftype.araxxor);
notifsForm.rots.setValue(self.settings.notiftype.rots);
notifsForm.mg_spotlight.setValue(self.settings.notiftype.mg_spotlight);
notifsForm.ed_spotlight.setValue(self.settings.notiftype.ed_spotlight);
notifsForm.dd_spotlight.setValue(self.settings.notiftype.dd_spotlight);
 
$('.gsw-events-popup .events-notifs').removeClass('oo-ui-element-hidden');
$('.gsw-events-popup .events-notifs').addClass('slideout');
setTimeout( function () {
$('.gsw-events-popup .events-notifs').removeClass('slideout');
}, 5);
};
self.loadSettings().then(switchWind(), switchWind());
},
 
/**
* Sends notifications to the browser based on user prefs
* @return {undefined}
*/
notifs: function () {
mw.log('Running notifications');
mw.log('Running notifications');
// TODO: Check that still not using service worker notifs
// TODO: Check that still not using service worker notifs
Line 1,216: Line 1,403:


// Latest rotations and settings from localStorage
// Latest rotations and settings from localStorage
if (rs.hasLocalStorage()) {
if (gs.hasLocalStorage()) {
var last = {};
var last = {};
try {
try {
Line 1,273: Line 1,460:
vosjson.districts.forEach( function (clan) {
vosjson.districts.forEach( function (clan) {
if (self.settings.notiftype['vos' + clan] && self.settings.notiftype.vos == 'Some' || self.settings.notiftype.allvos) {
if (self.settings.notiftype['vos' + clan] && self.settings.notiftype.vos == 'Some' || self.settings.notiftype.allvos) {
var img = rs.getFileURL(clan + ' Clan.png');
var img = gs.getFileURL(clan + ' Clan.png');
vosnotifs.push( { title:'VoS ' + clan, opts:{
vosnotifs.push( { title:'VoS ' + clan, opts:{
badge: img,
badge: img,
Line 1,290: Line 1,477:


// Save the last rotations to localstorage if possible
// Save the last rotations to localstorage if possible
if (rs.hasLocalStorage()) {
if (gs.hasLocalStorage()) {
var string = JSON.stringify(self.lastRot);
var string = JSON.stringify(self.lastRot);
try {
try {
Line 1,350: Line 1,537:
});
});


// Resets
// Raven
if (self.settings.notiftype.dreset) {
if (self.settings.notiftype.raven) {
// Daily
var days_into = -( moment('0', 'X').utc().add(6, 'days').diff(now, 'days') ) % 13,
var dailyReset = moment(now).add(1, 'days').startOf('day'),
text = 'Raven is currently spawned in Prifddinas',
img = '/images/thumb/0/08/D%26D_token_%28daily%29_detail.png/200px-D%26D_token_%28daily%29_detail.png';
tag = 'raven',
if (dailyReset.diff(now, 'minutes') <= 10) {
img = '/images/d/d5/Raven_%28Prifddinas%29.png';
if (days_into >= 11) {
var tospawn = 13 - days_into;
if (tospawn > 1) {
text = 'Raven spawns in Prifddinas in ' + tospawn + 'days';
} else {
text = 'Raven spawns in Prifddinas tomorrow';
}
tag = 'raven-' + tospawn;
}
if (days_into < 1 || days_into >= 11) {
if (self.lastRot.raven != tag) {
notifs.push( { title:'Raven (Prif)', opts:{
badge: img,
body: text,
tag: tag,
icon: img,
image: img,
vibrate: true,
renotify: false,
requireInteraction: false
} } );
self.lastRot.raven = tag;
}
}
}
 
// Resets
if (self.settings.notiftype.dreset) {
// Daily
var dailyReset = moment(now).add(1, 'days').startOf('day'),
img = '';
if (dailyReset.diff(now, 'minutes') <= 10) {
notifs.push( { title:'Daily Reset', opts:{
notifs.push( { title:'Daily Reset', opts:{
badge: img,
badge: img,
Line 1,371: Line 1,590:
// Weekly
// Weekly
var weeklyReset = moment(now).day(3).startOf('day'),
var weeklyReset = moment(now).day(3).startOf('day'),
img = '/images/4/47/D%26D_token_%28weekly%29_detail.png';
img = '';
if (now.isAfter(weeklyReset)) {
if (now.isAfter(weeklyReset)) {
weeklyReset.add(1, 'week');
weeklyReset.add(1, 'week');
Line 1,403: Line 1,622:
// Monthly
// Monthly
var monthlyReset = moment(now).add(1, 'months').startOf('month'),
var monthlyReset = moment(now).add(1, 'months').startOf('month'),
img = '/images/thumb/a/a7/D%26D_token_%28monthly%29_detail.png/200px-D%26D_token_%28monthly%29_detail.png';
img = '';
if (monthlyReset.diff(now, 'minutes') <= 10) {
if (monthlyReset.diff(now, 'minutes') <= 10) {
notifs.push( { title:'Monthly Reset', opts:{
notifs.push( { title:'Monthly Reset', opts:{
Line 1,469: Line 1,688:
});
});
}
}
 
 
// For spotlights
self.sendNotifs( dnotifs );
var curspotlight = function(items, duration, offset) {
// Save the last rotations to localstorage if possible
var days_passed = -( moment('0', 'X').utc().add(offset, 'days').diff(now, 'days') ),
if (gs.hasLocalStorage()) {
days_into = days_passed % (duration * items.length),
var string = JSON.stringify(self.lastRot);
rotation = Math.floor(days_into / duration),
try {
days_till = duration - (days_into % duration);
localStorage.setItem(localLast, string);
 
} catch (err) {
return [rotation, items[rotation], days_till];
console.warn('Error saving latest rotations to localStorage');
}
}
}, function (err) {
console.warn('Error loading events and merchant stock');
console.warn(err);
});
}
 
// For spotlights
var curspotlight = function(items, duration, offset) {
var days_passed = -( moment('0', 'X').utc().add(offset, 'days').diff(now, 'days') ),
days_into = days_passed % (duration * items.length),
rotation = Math.floor(days_into / duration),
days_till = duration - (days_into % duration);
 
return [rotation, items[rotation], days_till];
};
 
// Player-owned farm notifications
if (self.settings.notiftype.pof == 'All' || self.settings.notiftype.pof == 'Some') {
var pofnots = [];
// Is less 10 minutes before reset
if ( moment(now).add(1, 'days').startOf('day').diff(now, 'minutes') <= 10 ) {
// Small buyer, easy request
if (self.settings.notiftype.pof == 'All' || self.settings.notiftype['pofSmall Buyer']) {
pofnots.push( 'small buyer' );
}
if (self.settings.notiftype.pof == 'All' || self.settings.notiftype['pofEasy Request']) {
pofnots.push( 'easy request' );
}
// Medium buyer
if (self.settings.notiftype.pof == 'All' || self.settings.notiftype['pofMedium Buyer']) {
if ( curspotlight(['med'], 2, 1)[2] == 1 ) {
pofnots.push( 'medium buyer' );
}
}
// Medium request
if (self.settings.notiftype.pof == 'All' || self.settings.notiftype['pofMedium Request']) {
if ( curspotlight(['med'], 3, 0)[2] == 1 ) {
pofnots.push( 'medium request' );
}
}
// Large buyer
if (self.settings.notiftype.pof == 'All' || self.settings.notiftype['pofLarge Buyer']) {
if ( curspotlight(['large'], 3, 0)[2] == 1 ) {
pofnots.push( 'large buyer' );
}
}
}
// Generate notification
if (pofnots.length > 0) {
var img = '',
nbod = 'Spawning soon:',
len = pofnots.length,
i = 1;
 
pofnots.forEach( function (npc) {
if (i == 1) {
nbod = nbod + ' ' + npc;
} else if (i == len) {
nbod =  nbod + ' and ' + npc;
} else {
nbod = nbod + ', ' + npc;
}
i ++;
});
nbod = nbod + '.';
 
notifs.push( { title:'POF Spawns', opts:{
badge: img,
body: nbod,
tag: 'pofresets',
icon: img,
image: img,
vibrate: true,
renotify: false,
requireInteraction: false
} } );
}
}
// Minigame Spotlight
if (self.settings.notiftype.mg_spotlight) {
var spot = curspotlight(mg_spotlights, 3, 49);
if (spot[0] != self.lastRot.mgspot) {
self.lastRot.mgspot = spot[0];
var img = '';
 
notifs.push( { title:'Minigame Spotlight', opts:{
badge: img,
body: 'The currently spotlighted minigame is ' + spot[1],
tag: 'mgspot',
icon: img,
image: img,
vibrate: true,
renotify: true,
requireInteraction: false
} } );
}
}
 
// Elite dungeon spotlight
if (self.settings.notiftype.ed_spotlight) {
var spot = curspotlight(ed_spotlights, 1, 0);
if (spot[0] != self.lastRot.edspot) {
self.lastRot.edspot = spot[0];
var imgs = {
'Temple of Aminishi':'/images/thumb/f/fd/Relic_of_aminishi_%28rare%29_detail.png/200px-Relic_of_aminishi_%28rare%29_detail.png',
'Dragonkin Laboratory':'/images/thumb/d/d4/Laboratory_relic_%28uncommon%29_detail.png/180px-Laboratory_relic_%28uncommon%29_detail.png',
'The Shadow Reef':'/images/thumb/e/e5/Umbral_urn_detail.png/160px-Umbral_urn_detail.png'
},
img = imgs[spot[1]];
 
notifs.push( { title:'Elite Dungeon Spotlight', opts:{
badge: img,
body: 'The currently spotlighted elite dungeon is ' + spot[1],
tag: 'edspot',
icon: img,
image: img,
vibrate: true,
renotify: true,
requireInteraction: false
} } );
}
}
// Save the last rotations to localstorage if possible
if (gs.hasLocalStorage()) {
var string = JSON.stringify(self.lastRot);
try {
localStorage.setItem(localLast, string);
} catch (err) {
console.warn('Error saving latest rotations to localStorage');
}
}
},
/**
* Sends notifications (on site or browser)
* @param  {array} notifs Array of notifcation objects to be sent
* @param  {boolean} add    (optional) Whether to replace or add to current notifications
* @return {undefined}
*/
sendNotifs: function ( notifs, add ) {
// Send browser notifications
var sendNotifs = function ( nl ) {
mw.log('Sending browser notifications');
// Google analytics tracker
if (typeof ga === 'function') {
ga('gtag_UA_126479006_1.send', 'event', 'Gadget-events', 'Send Notifactions', 'Browser', nl.length);
}
 
var interval = 200;
nl.forEach( function (n,i) {
mw.log('Sending ' + n.title);
// Some browser block them if to many sent in a short time
setTimeout(function() {
var bn = new Notification(n.title, n.opts);
bn.onclick = function () {
popup.toggle(true);
bn.close.bind(bn);
};
}, interval * i);
});
};
};
// Minigame Spotlight
if (self.settings.notiftype.mg_spotlight) {
var spot = curspotlight(mg_spotlights, 3, 49);
if (spot[0] != self.lastRot.mgspot) {
self.lastRot.mgspot = spot[0];
var img = '/images/c/c8/Current_spotlight_icon.png';
notifs.push( { title:'Minigame Spotlight', opts:{
badge: img,
body: 'The currently spotlighted minigame is ' + spot[1],
tag: 'mgspot',
icon: img,
image: img,
vibrate: true,
renotify: true,
requireInteraction: false
} } );
}
}
// Elite dungeon spotlight
if (self.settings.notiftype.ed_spotlight) {
var spot = curspotlight(ed_spotlights, 1, 0);
if (spot[0] != self.lastRot.edspot) {
self.lastRot.edspot = spot[0];
var imgs = {
'Temple of Aminishi':'/images/thumb/f/fd/Relic_of_aminishi_%28rare%29_detail.png/200px-Relic_of_aminishi_%28rare%29_detail.png',
'Dragonkin Laboratory':'/images/thumb/d/d4/Laboratory_relic_%28uncommon%29_detail.png/180px-Laboratory_relic_%28uncommon%29_detail.png',
'The Shadow Reef':'/images/thumb/e/e5/Umbral_urn_detail.png/160px-Umbral_urn_detail.png'
},
img = imgs[spot[1]];


notifs.push( { title:'Elite Dungeon Spotlight', opts:{
// Send popup notifs
badge: img,
body: 'The currently spotlighted elite dungeon is ' + spot[1],
tag: 'edspot',
icon: img,
image: img,
vibrate: true,
renotify: true,
requireInteraction: false
} } );
}
}
// Send popup notifs
var popNotifs = function ( nl, noempty ) {
var popNotifs = function ( nl, noempty ) {
mw.log('Sending popup notifications');
mw.log('Sending popup notifications');

Latest revision as of 15:45, 9 June 2020

//<nowiki>
/**
 * Interface for events
 *
 * @version 1.1
 * @author JaydenKieran
 * @author Elessar2
 * @author ThePsionic
 * 
 */

/*global jQuery, mediaWiki, mw, gswiki, rs, OO, moment, ga */
'use strict';

// TODO: Add castaways?, circus city, Jewels of the Elid,
// TODO: Cache Vos as well

;(function($, mw, gs){
	var portletLink,
		$popup,
		popup,
		$content,
		settingsForm,
		notifsForm,
		api,
		et_events,
		merchstock,
		notiftimer,
		notifpopup,
		$notifs,
		formConst = false,
		formMade = false;

	// localStorage key
	var localKey = 'gsw-events-prefs',
		localLast = 'gsw-events-last',
		vosurl = 'https://chisel.weirdgloop.org/api/runescape/vos/history',
		simplevosurl = 'https://chisel.weirdgloop.org/api/runescape/vos';

	// Dailies
	var simple_dailies = [
		{ name:'Big Chinchompa', id:'bigchin', link:'/w/Big_Chinchompa', length:20, repeat:60, offset:30, from:'hour', img:'/images/6/63/Nyriki%27s_portal.png' },
		{ name:'Guthixian Caches', id:'guthcache', link:'/w/Guthixian_Cache', length:10, repeat:60, offset:0, from:'hour', img:'/images/thumb/2/23/Player_automaton.png/600px-Player_automaton.png' },
		{ name:'Guthixian Caches (boost)', id:'guthcacheb', link:'/w/Guthixian_Cache', length:10, repeat:(60*3), offset:0, from:'day', img:'/images/8/8e/Confused_automaton.png' },
		{ name:'Sinkholes', id:'sinkhole', link:'/w/Sinkholes', length:25, repeat:60, offset:30, from:'hour', img:'/images/5/5c/Sinkhole_entrance.png' },
		{ name:'Demon Flash Mob', id:'demonmob', link:'/w/Demon_Flash_Mob', length:5, repeat:60, offset:0, from:'hour', img:'/images/1/1f/Agrith_Naar_chathead.png' },
		{ name:'Fish Flingers', id:'fishflin', link:'/w/Fish_Flingers', length:10, repeat:20, offset:0, from:'hour', img:'/images/thumb/c/cb/Fish_Flingers_logo.png/200px-Fish_Flingers_logo.png' },
		{ name:'Supply Run', id:'goebiesup', link:'/w/Supply_run', length:25, repeat:(60 * 12), offset:0, from:'day', img:'/images/9/9e/Goebie_chathead.png' },
		{ name:'Wilderness Warbands', id:'wildwar', link:'/w/Wilderness_Warbands', length:10, repeat:(60 * 7), offset:(60 * 2), from:'isoWeek', img:'/images/5/57/Wilderness_Warbands_emblem.png' },
	];

	// VoS Locations
	var voslocs = { 'Amlodd':['WNW','West-north-west'], 'Crwys':['ENE','East-north-east'], 'Cadarn':['ESE','East-south-east'], 'Hefin':['NNW','North-north-west'],
			'Iorwerth':['SSW','South-south-west'], 'Ithell':['WSW','West-south-west'], 'Meilyr':['NNE','North-north-east'], 'Trahaearn':['SSE','South-south-east'] };
			
	// Spotlight lists
	var dd_spotlights = ['Shooting Star', 'Penguin Hide and Seek', 'Circus', 'Evil Tree'],
		mg_spotlights = ['Pest Control', 'Soul Wars', 'Fist of Guthix', 'Barbarian Assault', 'Conquest', 'Fishing Trawler', 'The Great Orb Project', 'Flash Powder Factory', 'Castle Wars', 'Stealing Creation', 'Cabbage Facepunch Bonanza', 'Heist', 'Soul Wars', 'Barbarian Assault',
			'Conquest', 'Fist of Guthix', 'Castle Wars', 'Pest Control', 'Soul Wars', 'Fishing Trawler', 'The Great Orb Project', 'Flash Powder Factory', 'Stealing Creation', 'Cabbage Facepunch Bonanza', 'Heist', 'Trouble Brewing', 'Castle Wars'],
		ed_spotlights = ['Temple of Aminishi', 'Dragonkin Laboratory', 'The Shadow Reef'];
		
	var self = {
		// Settings
		settings: {
			daily_sort: 'by time',
			notifs: false,
			nobrownotifs: false,
			notiftype: {},
			raven: true,
			et_events: false,
			num_events: 3,
			spots: true,
			mg_spotlight: true,
			dd_spotlight: true,
			ed_spotlight: true,
			show_next: false,
			pvm: false,
			//tms: false,
			//pof: false,
			merch: true,
			farm: true,
			//etlookup: '1970-01-01T01:00:00+00:00',
			//events: []
			dcache: '1970-01-01T01:00:00+00:00',
			cache: {
				events: [],
				tms: {},
				tmsTom: {} 
			}
		},

		// Using service worker notifications?
		swnotif: false,

		// Last times (for constant updating)
		updatetimer: 0,
		nextvos: '1970-01-01T01:00:00+00:00',

		// Latest rotations (for notifications)
		lastRot: {
			time: '1970-01-01T01:00:00+00:00',
			wreset: '1970-01-01T01:00:00+00:00',
			mreset: '1970-01-01T01:00:00+00:00',
			vos: '',
			raven: '',
			tms: '',
			vorago: 99,
			araxxor: 99,
			rots: 99,
			mgspot: 99,
			edspot: 99,
			ddspot: 99
		},

		/**
		 * Starts the events gadget
		 * @return {undefined}
		 */
		init: function () {
			mw.log('Starting Events Interface');
			api = new mw.Api();
			
			// setup moment library
			moment.locale('en-events', {
			    relativeTime : {
			    	parentLocale: "en",
			        future: "in %s",
			        past:   "%s ago",
			        s:  "secs",
			        m:  "1m",
			        mm: "%dm",
			        h:  "1h",
			        hh: "%dh",
			        d:  "1d",
			        dd: "%dd",
			        M:  "1mth",
			        MM: "%dmths",
			        y:  "1yr",
			        yy: "%dyr"
			    }
			});

			// Event team events
			et_events = [];
			// Travelling merchant stock
			merchstock = { tod:[], tmr:[] };

			// Check browser supports notifications
			self.supportNotif = gs.canSendBrowserNotifs();
			// TODO: Check if using service worker notifications

			portletLink = mw.util.addPortletLink(
				'p-personal',
				'',
				'',
				'pt-events',
				'Events',
				null,
				$('#pt-userpage, #pt-anonuserpage')
			);

			$(portletLink).find('a').addClass('oo-ui-icon-calendar').click(function(e) {
				e.preventDefault();
				var openPopup = function() {
					mw.log('Open events popup');
					self.loadSettings().then(self.loadEvents).then( self.updateAll(), self.updateAll() );
				
					// Disable browser notifications settings if unavailable
					// Done here to avoid a race condition where we disable it and save to localStorage
					// before we actually load the settings
					if (!self.supportNotif) {
						notifsForm.browsnotif.setValue(true);
						notifsForm.browsnotif.setDisabled(true);
					}

					// Google analytics tracker
					if (typeof ga === 'function') {
						ga('gtag_UA_126479006_1.send', 'event', 'Gadget-events', 'Open', 'Normal');
					}
					
					$('.gsw-events-popup .events-settings').removeClass('slideout').addClass('oo-ui-element-hidden');
					popup.toggle();
				};
				if (!formMade && !formConst) {
					mw.log('Initialise events popup');
					formConst = true;
					mw.loader.using(['oojs-ui-core', 'oojs-ui-windows', 'oojs-ui-widgets']).then(self.initInt).then(openPopup);
				} else if (!formConst) {
					openPopup();
				} else {
					mw.log('Waiting for initialisation to finish');
				}
			});

			// Load settings and events from localStorage or via api on page load to handle notifs
			self.loadSettings().then(self.loadEvents).then( function () {
				if (self.settings.notifs && !self.swnotif) {
					self.notifs();
					notiftimer = setInterval(self.notifs, 300000);
				}
			}, function () {
				console.warn('Error loading events settings!');
			});
		},

		/**
		 * Initialises the interface (popup and forms)
		 * @return {Promise}
		 */
		initInt: function () {
			mw.log('Initialising...');
			return new Promise( function (resolve,reject) {
				self.initPopup();
				self.initSettingsForm();
				self.initNotifsForm();

				$popup = $('<div>').append(
					$content,
					settingsForm.$form
				);
				settingsForm.$form.find('.settings-container').append( notifsForm.$form );

				popup = new OO.ui.PopupWidget({
					$content: $popup,
					$floatableContainer: $('#pt-events'),
					autoClose: true,
					classes: ['gsw-events-popup oo-ui-labelElement-invisible'],
					hideWhenOutOfView: (gs.isUsingStickyHeader() ? true : false),
					invisibleLabel: true,
					label: 'Events',
					width: 700
				});
				$('body').append(popup.$element);
				popup.on('toggle', function (state) {
					// Start constant update
					clearInterval(self.updatetimer);
					if (state) {
						self.updatetimer = setInterval(self.update, 60000);
					}
				});

				// For on wiki notifications
				$('body').append($('<div>').attr('id', 'gsw-notifs-anchor'));
				var notifclose = new OO.ui.ButtonWidget({
					classes: ['oo-ui-labelElement-invisible'],
					icon: 'close',
					id:'gsw-eventsnotifs-close',
					invisibleLabel: true,
					label: 'Close',
					framed: false
				});
				notifclose.on('click', function () {
					notifpopup.toggle(false);
				});
				$notifs = $('<div>').addClass('event-notifs-container').append(
					notifclose.$element,
					$('<h3>').addClass('oo-ui-element-hidden').text('Event Notifications'),
					$('<ul>').addClass('event-notifications')
				);
				notifpopup = new OO.ui.PopupWidget({
					$content: $notifs,
					$floatableContainer: $('#gsw-notifs-anchor'),
					anchor: false,
					autoClose: true,
					classes: ['gsw-events-notifs oo-ui-labelElement-invisible'],
					hideWhenOutOfView: false,
					invisibleLabel: true,
					label: 'Events  Notifications',
					width: 450
				});
				$('body').append(notifpopup.$element);

				// Mark interface as initialised
				formMade = true;
				formConst = false;

				resolve();
			} );
		},

		/**
		 * Loads settings from browser localStorage
		 * @return {Promise}
		 */
		loadSettings: function () {
			mw.log('Loading settings');
			return new Promise( function (resolve, reject) {
				if (!gs.hasLocalStorage()) {
					console.warn('Browser does not support localStorage');
					reject();
				}

				var prefs = {};
				try {
					prefs = JSON.parse(localStorage.getItem(localKey));
				} catch (err) {
					prefs = {};
					console.warn('Error loading settings (events)');
				}
				for (var p in prefs) {
					self.settings[p] = prefs[p];
				}
				resolve();
			} );
		},
		
				/**
		 * Saves a setting to localStorage
		 * @param  {string} name Name (key) of setting to save
		 * @return {boolean}  If save was successfull
		 */
		saveSetting: function (key, value) {
			mw.log('Saving setting: ' + key);
			if ( key.split('.')[0] == 'cache' ) {
				self.settings.cache[key.split('.')[1]] = value;
			} else {
				self.settings[key] = value;
			}

			if (!gs.hasLocalStorage()) {
				console.warn('Browser does not support localStorage');
				return false;
			}

			var string = JSON.stringify(self.settings);
			try {
				localStorage.setItem(localKey, string);
			} catch (err) {
				console.warn('Error saving presets to localStorage');
				return false;
			}
			return true;
		},

		/**
		 * Saves a notification setting to localStorage
		 * @param  {string} key   Name (key) of notification setting to save
		 * @param  {boolean} value Send notifications?
		 * @return {boolean}       If save was successfull
		 */
		saveNotif: function (key, value) {
			mw.log('Saving notification setting: ' + key);
			self.settings.notiftype[key] = value;

			if (!gs.hasLocalStorage()) {
				console.warn('Browser does not support localStorage');
				return false;
			}

			var string = JSON.stringify(self.settings);
			try {
				localStorage.setItem(localKey, string);
			} catch (err) {
				console.warn('Error saving presets to localStorage');
				return false;
			}
			return true;
		},

		/**
		 * Generates the events popup
		 * @return {undefined}
		 */
		initPopup: function () {
			var closeButton = new OO.ui.ButtonWidget({
				classes: ['oo-ui-labelElement-invisible'],
				icon: 'close',
				id:'gsw-events-close',
				invisibleLabel: true,
				label: 'Close',
				framed: false
			});
			closeButton.on('click', function () {
				popup.toggle(false);
			});

			var settingsButton = new OO.ui.ButtonWidget({
				classes: ['oo-ui-labelElement-invisible'],
				icon: 'advanced',
				id:'gsw-events-setbut',
				invisibleLabel: true,
				label: 'Settings',
				framed: false
			});
			settingsButton.on('click', function () {
				self.openSettings();
			});

			// Current UTC time
			var now = moment.utc();

			$content = $('<div>');
			$content
				.addClass('events-popup')
				.append(
					$('<div>')
						.addClass('header')
						.append(
							closeButton.$element,
							$('<time>')
								.addClass('cur-utc-time')
								.attr({ 'title':now.format('HH:mm D/M/Y'), 'datetime':now.format() })
								.text(now.format('HH:mm') + ' (UTC)'),
							settingsButton.$element
						),
					$('<div>')
						.addClass('top')
						.append(
							$('<div>')
								.addClass('col col-l')
								.append(
									$('<div>')
										.addClass('dailies section')
										.append(
											$('<h3>').text('Dailies'),
											$('<ul>').addClass('events-list daily-list')
										),
									$('<div>')
										.addClass('raven section')
								),
							$('<div>')
								.addClass('col col-r')
								.append(
									$('<div>')
										.addClass('resets section')
										.append(
											$('<h3>').text('Resets'),
											$('<div>')
												.addClass('event-boxes')
												.append(
													$('<div>')
														.addClass('day event-box')
														.append(
															$('<label>').addClass('by-line').text('Day'),
															$('<time>').addClass('event-name countdown-day').text('? hrs')
														),
													$('<div>')
														.addClass('week event-box')
														.append(
															$('<label>').addClass('by-line').text('Week'),
															$('<time>').addClass('event-name countdown-week').text('? days')
														),
													$('<div>')
														.addClass('month event-box')
														.append(
															$('<label>').addClass('by-line').text('Month'),
															$('<time>').addClass('event-name countdown-month').text('? days')
														)
												)
										),
									$('<div>')
										.addClass('voice section')
										.append(
											$('<h3>').html('<a href="/w/Voice_of_Seren" title="Voice of Seren">Voice of Seren</a>'),
											$('<div>')
												.addClass('VoS-time by-line')
												.text(
													'There was an error loading the Voice of Seren. Please try again.<br/>If this issue persists, please <a href="/w/RS:AR">contact an administrator</a>.'
												),
											$('<div>')
												.addClass('VoS-container event-boxes'),
											$('<div>')
												.addClass('by-line')
												.append(
													$('<span>').text('Last districts: '),
													$('<span>').addClass('VoS-last').text('not found')
												)
										)
								)
						),
					$('<div>')
						.addClass('middle events-team'),
					$('<div>')
						.addClass('middle tms-pof'),
					$('<div>')
						.addClass('middle pvm'),
					$('<div>')
						.addClass('spotlights')
						.append(
							$('<div>')
								.addClass('spotlight-spacer empty'),
							$('<div>')
								.addClass('spotlight mg-spotlight'),
							$('<div>')
								.addClass('spotlight-spacer empty'),
							$('<div>')
								.addClass('spotlight ed-spotlight'),
							$('<div>')
								.addClass('spotlight-spacer empty'),
							$('<div>')
								.addClass('spotlight dd-spotlight'),
							$('<div>')
								.addClass('spotlight-spacer')
						)
				);
		},

		/**
		 * Generates the settings popup
		 * @return {undefined}
		 */
		initSettingsForm: function () {
			settingsForm = {};

			// Back button
			settingsForm.back = new OO.ui.ButtonWidget({
				classes: ['oo-ui-labelElement-invisible'],
				icon: 'close',
				id: 'gsw-events-backbut',
				invisibleLabel: true,
				label: 'Back',
				framed: false
			});
			var closeset = function () {
				self.updateTimes();
				self.updateVos();
				self.updateSpots();
				$('.gsw-events-popup .events-settings').addClass('slideout');
				setTimeout( function () {
					$('.gsw-events-popup .events-settings').removeClass('slideout').addClass('oo-ui-element-hidden');
				}, 500);
			};
			settingsForm.back.on('click', closeset);

			// Title
			settingsForm.$title = $('<h3>').text('Options');

			// Dailies sort selector
			settingsForm.sort = new OO.ui.DropdownInputWidget({
				classes: 'sort-selector',
				options: [
					{ data:'by time', label:'by time' },
					{ data:'by name', label:'by name' }
				],
			});
			settingsForm.sort.setValue(self.settings.daily_sort);
			settingsForm.sortLay = new OO.ui.FieldLayout(settingsForm.sort, {
				label: 'Sorting'
			});
			settingsForm.sort.on('change', self.saveSetting, ['daily_sort']);

			// Show browser notifications
			settingsForm.notifs = new OO.ui.ToggleSwitchWidget({
				title: 'Recieve browser notifications',
				value: self.settings.notifs
			});
			settingsForm.notifs.on('change', function (val) {
				self.saveSetting('notifs', val);
				if (val && !self.swnotif) {
					window.clearInterval(notiftimer);
					self.notifs();
					notiftimer = setInterval(self.notifs, 300000);
				} else {
					window.clearInterval(notiftimer);
				}
			});
			settingsForm.notifSets = new OO.ui.ButtonWidget({
				classes: ['oo-ui-labelElement-invisible'],
				icon: 'advanced',
				invisibleLabel: true,
				label: 'Notification Settings',
				framed: false
			});
			settingsForm.notifSets.on('click', function () {
				self.openNotifs();
			});
			settingsForm.notifsLay = new OO.ui.ActionFieldLayout(settingsForm.notifs, settingsForm.notifSets, {
				label: 'Notifications',
				help: 'Enable notifications for configured events. Currently in development.'
			});

			// Show events team events
			settingsForm.events = new OO.ui.ToggleSwitchWidget({
				title: 'Show events team events',
				value: self.settings.et_events
			});
			settingsForm.eventsLay = new OO.ui.FieldLayout(settingsForm.events, { label: 'Events Team' });
			settingsForm.events.on('change', function (val) {
				self.saveSetting('et_events', val);
				settingsForm.eventsNum.setDisabled(!val);
				settingsForm.eventsNumLay.toggle(val);
			});

			// Number of events to display
			settingsForm.eventsNum = new OO.ui.NumberInputWidget({
				min: 0,
				required: true,
				step: 1,
				validate: 'integer',
				value: self.settings.num_events
			});
			settingsForm.eventsNumLay = new OO.ui.FieldLayout(settingsForm.eventsNum, {
				label: 'Number of Events',
				help: 'Number of Events Team events to display, use 0 for infinite'
			});
			settingsForm.eventsNum.on('change', function (val) {
				val = parseInt(val, 10);
				if (isNaN(val)) { val = 1; }
				self.saveSetting('num_events', val);
			});
			// Disable if events turned off
			if (!self.settings.et_events) {
				settingsForm.eventsNum.setDisabled(true);
				settingsForm.eventsNumLay.toggle(false);
			}

			// Show spotlights (combined)
			settingsForm.spots = new OO.ui.ToggleSwitchWidget({
				title: 'Show D&D, Minigame and ED spotlights',
				value: self.settings.spots
			});
			settingsForm.spotsLay = new OO.ui.FieldLayout(settingsForm.spots, {
				label: 'Spotlights',
				help: 'Show Minigame Spotlight, Featured D&D and Elite Dungeon Spotlight'
			});
			settingsForm.spots.on('change', function (val) {
				self.saveSetting('mg_spotlight', val);
				self.saveSetting('dd_spotlight', val);
				self.saveSetting('ed_spotlight', val);
			});

			// Show minigame spotlight
			settingsForm.mg = new OO.ui.ToggleSwitchWidget({
				title: 'Show minigame spotlight',
				value: self.settings.mg_spotlight
			});
			settingsForm.mgLay = new OO.ui.FieldLayout(settingsForm.mg, { label: 'Minigame Spotlight' });
			settingsForm.mg.on('change', self.saveSetting, ['mg_spotlight']);

			// Show featured D&D spotlight
			settingsForm.dd = new OO.ui.ToggleSwitchWidget({
				title: 'Show featured D&D',
				value: self.settings.dd_spotlight
			});
			settingsForm.ddLay = new OO.ui.FieldLayout(settingsForm.dd, { label: 'Featured D&D' });
			settingsForm.dd.on('change', self.saveSetting, ['dd_spotlight']);

			// Show elite dungeon spotlight
			settingsForm.ed = new OO.ui.ToggleSwitchWidget({
				title: 'Show Elite Dungeon spotlight',
				value: self.settings.ed_spotlight
			});
			settingsForm.edLay = new OO.ui.FieldLayout(settingsForm.ed, { label: 'Elite Dungeon Spotlight' });
			settingsForm.ed.on('change', self.saveSetting, ['ed_spotlight']);

			// Show next spotlight
			settingsForm.next = new OO.ui.ToggleSwitchWidget({
				title: 'Show next spotlight item',
				value: self.settings.show_next
			});
			settingsForm.nextLay = new OO.ui.FieldLayout(settingsForm.next, {
				label: 'Show next',
				help: 'Show next item in spotlight rotations'
			});
			settingsForm.next.on('change', self.saveSetting, ['show_next']);

			// Show PvM
			settingsForm.pvm = new OO.ui.ToggleSwitchWidget({
				title: 'Show PvM rotations',
				value: self.settings.pvm
			});
			settingsForm.pvmLay = new OO.ui.FieldLayout(settingsForm.pvm, { label: 'PvM Rotations' });
			settingsForm.pvm.on('change', self.saveSetting, ['pvm']);

			// Force update events and travelling merchant stock
			settingsForm.fupdate = new OO.ui.ButtonWidget({
				label: 'Refresh',
				icon: 'reload',
				title: 'Force refresh of travelling merchant stock and events'
			});
			settingsForm.fupdateLay = new OO.ui.FieldLayout(settingsForm.fupdate, {
				label: 'Force TMS Update',
				help: 'Force the travelling merchant stock and events team events to update regardless of the last update time.'
			});
			settingsForm.fupdate.on('click', function () {
				self.loadEvents(true).then(self.updateAll(), self.updateAll());
			});

			settingsForm.form =  new OO.ui.FieldsetLayout({ classes:'settings-form' });
			settingsForm.form.addItems([
				settingsForm.sortLay,
				settingsForm.notifsLay,
				settingsForm.ravenLay,
				settingsForm.eventsLay,
				settingsForm.eventsNumLay,
				settingsForm.tmsLay,
				settingsForm.pofLay,
				settingsForm.spotsLay,
				settingsForm.nextLay,
				settingsForm.pvmLay,
				settingsForm.fupdateLay
			]);

			// Settings form footer
			settingsForm.$footer = $('<div>')
				.addClass('footer')
				.html('<a href="/w/Help_talk:Gadget-events" title="Events gadget discussion">Give us feedback on these timers!</a>');

			var $setoverlay = $('<div>').addClass('settings-overlay');
			$setoverlay.click( closeset );

			// Complete form
			settingsForm.$form = $('<div>')
				.addClass('events-settings oo-ui-element-hidden')
				.append(
					$setoverlay,
					$('<div>')
						.addClass('settings-container')
						.append(
							settingsForm.back.$element,
							settingsForm.$title,
							settingsForm.form.$element,
							settingsForm.$footer
						)
				);
		},

		/**
		 * Generates the notifications settings popup
		 * @return {undefined}
		 */
		initNotifsForm: function () {
			notifsForm = {};

			// Back button
			notifsForm.back = new OO.ui.ButtonWidget({
				classes: ['oo-ui-labelElement-invisible'],
				icon: 'close',
				id: 'gsw-notifs-backbut',
				invisibleLabel: true,
				label: 'Back',
				framed: false
			});
			notifsForm.back.on('click', function () {
				$('.gsw-events-popup .events-notifs').addClass('slideout');
				setTimeout( function () {
					$('.gsw-events-popup .events-notifs').removeClass('slideout').addClass('oo-ui-element-hidden');
				}, 500);
			});

			// Title
			notifsForm.$title = $('<h3>').text('Notifications');

			// Disable browser notifications
			notifsForm.browsnotif = new OO.ui.ToggleSwitchWidget({
				title: 'Disable browser notifications',
				value: self.settings.nobrownotifs
			});
			notifsForm.browsnotif.on('change', self.saveSetting, ['nobrownotifs']);
			notifsForm.browsnotifLay = new OO.ui.FieldLayout(notifsForm.browsnotif, {
				label: 'No Browser Notifs',
				help: 'Disable native browser notifications. While disabled, notifications will instead be displayed in a popup at the top right of the wiki page.'
			});

			// All Dailies
			notifsForm.dailies = new OO.ui.DropdownInputWidget({
				options: [
					{ data:'None', label:'None' },
					{ data:'All', label:'All' },
					{ data:'Some', label:'Selectively' }
				],
				title: 'Recieve notifications for dailies',
			});
			notifsForm.dailies.setValue(self.settings.notiftype.dailies);
			notifsForm.dailiesLay = new OO.ui.FieldLayout(notifsForm.dailies, {
				label: 'Dailies',
				help: 'Recieve notifications for dailies such as Guthixian Caches or Sinkholes'
			});
			var toggledailies = function (val) {
				self.saveNotif('dailies', val);

				if (val == 'Some') {
					notifsForm.dailiesselLay.toggle(true);
					self.saveNotif('alldailies', false);
				} else if (val == 'All') {
					notifsForm.dailiesselLay.toggle(false);
					self.saveNotif('alldailies', true);
				} else {
					notifsForm.dailiesselLay.toggle(false);
					self.saveNotif('alldailies', false);
				}
			};
			notifsForm.dailies.on('change', toggledailies);

			// Individual Dailies
			notifsForm.dailiessel = new OO.ui.CheckboxMultiselectWidget({ classes: ['notifs-group'] });
			simple_dailies.forEach( function (dd) {
				notifsForm[dd.id] = new OO.ui.CheckboxMultioptionWidget({
					label: dd.name,
					data: dd.name,
					selected: self.settings.notiftype[dd.id]
				});

				notifsForm[dd.id].on('change', self.saveNotif, [dd.id]);

				notifsForm.dailiessel.addItems([ notifsForm[dd.id] ]);
			});
			notifsForm.dailiesselLay = new OO.ui.FieldLayout(notifsForm.dailiessel, {
				align: 'top',
				label: 'Individual Dailies',
				help: 'Choose individual dalies to be notified about'
			});
			if (self.settings.notiftype.dailies != 'Some') {
				notifsForm.dailiesselLay.toggle(false);
			}

			// Resets
			notifsForm.dreset = new OO.ui.ToggleSwitchWidget({
				title: 'Daily reset notification',
				value: self.settings.notiftype.dreset
			});
			notifsForm.dreset.on('change', self.saveNotif, ['dreset']);
			notifsForm.dresetLay = new OO.ui.FieldLayout(notifsForm.dreset, {
				label: 'Daily Reset'
			});
			notifsForm.wreset = new OO.ui.ToggleSwitchWidget({
				title: 'Weekly reset notification',
				value: self.settings.notiftype.wreset
			});
			notifsForm.wreset.on('change', self.saveNotif, ['wreset']);
			notifsForm.wresetLay = new OO.ui.FieldLayout(notifsForm.wreset, {
				label: 'Weekly Reset'
			});
			notifsForm.mreset = new OO.ui.ToggleSwitchWidget({
				title: 'Monthly reset notification',
				value: self.settings.notiftype.mreset
			});
			notifsForm.mreset.on('change', self.saveNotif, ['mreset']);
			notifsForm.mresetLay = new OO.ui.FieldLayout(notifsForm.mreset, {
				label: 'Monthly Reset'
			});

			// All Voice of Seren
			notifsForm.vos = new OO.ui.DropdownInputWidget({
				options: [
					{ data:'None', label:'None' },
					{ data:'All', label:'All' },
					{ data:'Some', label:'Some' }
				],
				title: 'Receive notifications for Voice of Seren',
			});
			notifsForm.vos.setValue(self.settings.notiftype.vos);
			notifsForm.vosLay = new OO.ui.FieldLayout(notifsForm.vos, {
				label: 'Voice of Seren',
				help: 'Receive notifications for Voice of Seren'
			});
			var togglevos = function (val) {
				self.saveNotif('vos', val);

				if (val == 'Some') {
					notifsForm.vosselLay.toggle(true);
					self.saveNotif('allvos', false);
				} else if (val == 'All') {
					notifsForm.vosselLay.toggle(false);
					self.saveNotif('allvos', true);
				} else {
					notifsForm.vosselLay.toggle(false);
					self.saveNotif('allvos', false);
				}
			};
			notifsForm.vos.on('change', togglevos);
			// Individual VoS
			notifsForm.vossel = new OO.ui.CheckboxMultiselectWidget({ classes: ['notifs-group'] });
			for (var clan in voslocs) {
				notifsForm[clan] = new OO.ui.CheckboxMultioptionWidget({
					label: clan,
					data: clan,
					selected: self.settings.notiftype['vos' + clan]
				});

				notifsForm[clan].on('change', self.saveNotif, ['vos' + clan]);

				notifsForm.vossel.addItems([ notifsForm[clan] ]);
			}
			notifsForm.vosselLay = new OO.ui.FieldLayout(notifsForm.vossel, {
				align: 'top',
				label: 'Individual Clan Districts',
				help: 'Choose the Prifddinas clan districts you wish to be notified about when a Voice of Seren is active.'
			});
			if (self.settings.notiftype.vos != 'Some') {
				notifsForm.vosselLay.toggle(false);
			}

			// Events team events
			notifsForm.etevents = new OO.ui.ToggleSwitchWidget({
				title: 'Events team notifications',
				value: self.settings.notiftype.etevents
			});
			notifsForm.etevents.on('change', self.saveNotif, ['etevents']);
			notifsForm.eteventsLay = new OO.ui.FieldLayout(notifsForm.etevents, {
				label: 'Events Team',
				help: 'Receive notifications for RuneScape Wiki events team events'
			});

			// All PoF Resets
			notifsForm.pof = new OO.ui.DropdownInputWidget({
				options: [
					{ data:'None', label:'None' },
					{ data:'All', label:'All' },
					{ data:'Some', label:'Some' }
				],
				title: 'Receive notifications for player-owned farm resets',
			});
			notifsForm.pof.setValue(self.settings.notiftype.pof);
			notifsForm.pofLay = new OO.ui.FieldLayout(notifsForm.pof, {
				label: 'Player-owned Farm',
				help: 'Receive notifications for player-owned farm resets'
			});
			var togglepof = function (val) {
				self.saveNotif('pof', val);

				if (val == 'Some') {
					notifsForm.pofselLay.toggle(true);
					self.saveNotif('allpof', false);
				} else if (val == 'All') {
					notifsForm.pofselLay.toggle(false);
					self.saveNotif('allpof', true);
				} else {
					notifsForm.pofselLay.toggle(false);
					self.saveNotif('allpof', false);
				}
			};
			notifsForm.pof.on('change', togglepof);
			// Individual PoF resets
			notifsForm.pofsel = new OO.ui.CheckboxMultiselectWidget({ classes: ['notifs-group'] });
			pofresets.forEach( function (res) {
				notifsForm[res] = new OO.ui.CheckboxMultioptionWidget({
					label: res,
					data: res,
					selected: self.settings.notiftype['pof' + res]
				});

				notifsForm[res].on('change', self.saveNotif, ['pof' + res]);

				notifsForm.pofsel.addItems([ notifsForm[res] ]);
			});
			notifsForm.pofselLay = new OO.ui.FieldLayout(notifsForm.pofsel, {
				align: 'top',
				label: 'Individual PoF resets',
				help: 'Choose the player-owned farm resets you wish to be notified about'
			});
			if (self.settings.notiftype.pof != 'Some') {
				notifsForm.vosselLay.toggle(false);
			}

			// PVM
			notifsForm.vorago = new OO.ui.ToggleSwitchWidget({
				title: 'Vorago attack notifications',
				value: self.settings.notiftype.vorago
			});
			notifsForm.vorago.on('change', self.saveNotif, ['vorago']);
			notifsForm.voragoLay = new OO.ui.FieldLayout(notifsForm.vorago, {
				label: 'Vorago',
				help: 'Receive notifications about current Vorago attack'
			});

			// Spotlights
			notifsForm.mg_spotlight = new OO.ui.ToggleSwitchWidget({
				title: 'Minigame spotlight notifications',
				value: self.settings.notiftype.mg_spotlight
			});
			notifsForm.mg_spotlight.on('change', self.saveNotif, ['mg_spotlight']);
			notifsForm.mg_spotlightLay = new OO.ui.FieldLayout(notifsForm.mg_spotlight, {
				label: 'Minigame Spotlight',
				help: 'Receive notifications for current Minigame Spotlight'
			});
			notifsForm.ed_spotlight = new OO.ui.ToggleSwitchWidget({
				title: 'Elite dungeon spotlight notifications',
				value: self.settings.notiftype.ed_spotlight
			});
			notifsForm.ed_spotlight.on('change', self.saveNotif, ['ed_spotlight']);
			notifsForm.ed_spotlightLay = new OO.ui.FieldLayout(notifsForm.ed_spotlight, {
				label: 'Elite Dungeon Spotlight',
				help: 'Receive notifications for current Elite dungeon spotlight'
			});
			notifsForm.dd_spotlight = new OO.ui.ToggleSwitchWidget({
				title: 'Distraction and diversion of the week notifications',
				value: self.settings.notiftype.dd_spotlight
			});
			notifsForm.dd_spotlight.on('change', self.saveNotif, ['dd_spotlight']);
			notifsForm.dd_spotlightLay = new OO.ui.FieldLayout(notifsForm.dd_spotlight, {
				label: 'Distraction & Diversions',
				help: 'Receive notifications for Distraction and Diversion of the week'
			});
			
			notifsForm.form = new OO.ui.FieldsetLayout({ classes:'notifs-form' });
			notifsForm.form.addItems([
				notifsForm.browsnotifLay,
				notifsForm.dailiesLay,
				notifsForm.dailiesselLay,
				notifsForm.ravenLay,
				notifsForm.dresetLay,
				notifsForm.wresetLay,
				notifsForm.mresetLay,
				notifsForm.vosLay,
				notifsForm.vosselLay,
				notifsForm.tmsLay,
				notifsForm.tmsselLay,
				notifsForm.pofLay,
				notifsForm.pofselLay,
				notifsForm.eteventsLay,
				notifsForm.voragoLay,
				notifsForm.araxxorLay,
				notifsForm.rotsLay,
				notifsForm.mg_spotlightLay,
				notifsForm.ed_spotlightLay,
				notifsForm.dd_spotlightLay
			]);

			var $notifoverlay = $('<div>').addClass('notifs-overlay');
			$notifoverlay.click( function () {
				$('.gsw-events-popup .events-notifs').addClass('slideout');
				setTimeout( function () {
					$('.gsw-events-popup .events-notifs').removeClass('slideout').addClass('oo-ui-element-hidden');
				}, 500);
			});

			// Complete form
			notifsForm.$form = $('<div>')
				.addClass('events-notifs oo-ui-element-hidden')
				.append(
					$notifoverlay,
					$('<div>')
						.addClass('notifs-container')
						.append(
							notifsForm.back.$element,
							notifsForm.$title,
							notifsForm.form.$element
						)
				);
		},

		/**
		 * Updates all events dropdown values (used on  opening)
		 * @return {undefined}
		 */
		updateAll: function () {
			var now = moment.utc();
			$content.find('.cur-utc-time')
				.attr({ 'title':now.format('HH:mm D/M/Y'), 'datetime':now.format() })
				.text(now.format('HH:mm') + ' (UTC)');
			self.updateTimes();
			self.updateSpots();
			if ( now.isAfter(moment(self.nextvos)) ) {
				self.updateVos();
			} else {
				self.updateVostime();
			}
		},

		/**
		 * Continuously updates the values in Events dropdown
		 * @return {undefined}
		 */
		update: function () {
			self.updateTimes();

			// Current UTC time
			var now = moment.utc();

			$content.find('.cur-utc-time')
				.attr({ 'title':now.format('HH:mm D/M/Y'), 'datetime':now.format() })
				.text(now.format('HH:mm') + ' (UTC)');

			// Check if it's after a new hour, update VoS
			if ( now.isAfter(moment(self.nextvos)) ) {
				self.updateVos();
			} else {
				self.updateVostime();
			}

			// Check if it's after a new day, update PVM etc
			if ( now.isBetween(moment(now).startOf('day'), moment(now).startOf('day').add(1, 'minutes'), null, '[]') ) {
				self.updateSpots();
			}
		},

		/**
		 * Update time based events in the Events popup
		 * Dailies, Resets, Events team events
		 * @return {[type]} [description]
		 */
		updateTimes: function () {
			// Current UTC time
			var now = moment.utc();

			// Dailies
			$content.find('.daily-list').empty();
			var dailies_array = [];
			simple_dailies.forEach( function (dd) {
				var next = moment(now).startOf(dd.from).add((dd.offset), 'minutes'),
					nextEnd = moment(next).add(dd.length, 'minutes');
				while (!now.isBefore(nextEnd)) {
					next.add(dd.repeat, 'minutes');
					nextEnd.add(dd.repeat, 'minutes');
				}
				var time_till = 'now!',
					diff_secs = 0,
					title = 'Going on now',
					future = moment(next).add(dd.repeat, 'minutes'),
					ntxt = future.fromNow(true),
					ntitle = future.format('HH:mm') + ' game time, on ' + future.format('MMM D');
				if (now.isBefore(next)) {
					time_till = next.fromNow(true);
					title = next.format('HH:mm') + ' game time, on ' + next.format('MMM D');
					diff_secs = next.diff(now, 'seconds');
				}	
				dailies_array.push( {name:dd.name, link:dd.link, txt:time_till, title:title, sort:diff_secs, time:next.format(), ntxt:ntxt, ntime:future.format(), ntitle:ntitle} );
			});
			// Sort Dailies
			if ( self.settings.daily_sort == 'by time') {
				dailies_array.sort( function (a,b) {
					return a.sort - b.sort;
				});
			} else {
				dailies_array.sort( function (a,b) {
					if (a.name < b.name) {
						return -1;
					} else if (a.name > b.name) {
						return 1;
					} else {
						return 0;
					}
				});
			}
			// Print Dailies
			dailies_array.forEach( function (dd) {
				var ddClass = 'daily';
				if (dd.sort === 0) { ddClass += ' now'; }
				$content.find('.daily-list').append(
					$('<li>')
						.addClass(ddClass)
						.append(
							$('<label>').addClass('daily-title').html('<a href="'+dd.link+'" title="'+dd.name+'">'+dd.name+'</a>'),
							$('<time>').addClass('daily-timer').attr({'datetime':dd.time, 'title':dd.title}).text(dd.txt),
							$('<time>').addClass('daily-next').attr({'datetime':dd.ntime, 'title':dd.ntitle}).text(dd.ntxt)
						)
				);
			});

			// Update resets
			moment.relativeTimeThreshold('d', 31);
			moment.relativeTimeThreshold('h', 24);
			var dailyReset = moment(now).add(1, 'days').startOf('day'),
				dailyTitle = dailyReset.format('MMM D') + ' at ' + dailyReset.format('HH:mm') + ' game time',
				monthlyReset = moment(now).add(1, 'months').startOf('month'),
				monthlyTitle = monthlyReset.format('MMM D') + ' at ' + monthlyReset.format('HH:mm') + ' game time',
				weeklyReset = moment(now).day(3).startOf('day'); // reset is a wednesday
			if (now.isAfter(weeklyReset)) {
				weeklyReset.add(1, 'week');
			}
			var weeklyTitle = weeklyReset.format('MMM D') + ' at ' + weeklyReset.format('HH:mm') + ' game time';
			$content.find('.countdown-day').attr({'datetime':dailyReset.format(), 'title':dailyTitle }).text( dailyReset.fromNow(true) );
			$content.find('.countdown-week').attr({'datetime':weeklyReset.format(), 'title':weeklyTitle }).text( weeklyReset.fromNow(true) );
			$content.find('.countdown-month').attr({'datetime':monthlyReset.format(), 'title':monthlyTitle }).text( monthlyReset.fromNow(true) );
			moment.relativeTimeThreshold('h', 22);
			moment.relativeTimeThreshold('d', 26);

			// Events team events
			$content.find('.events-team').empty();
			if (self.settings.et_events) {
				$content.find('.events-team').append(
					$('<h3>').html('<a href="/w/RuneScape:Events_Team" title="Events Team events">Events Team</a>'),
					$('<ul>')
						.addClass('events-list etevents-list')
						.append(
							$('<li>').text('No upcoming events')
						)
				);
				var events_array = [];
				et_events.forEach( function (e) {
					var emom = moment(e.date).utc(),
						time = emom.fromNow(true),
						sort = emom.diff(now, 'seconds'),
						dtime = emom.format(),
						title = emom.format('MMM D') + ' at ' + emom.format('HH:mm') + ' game time';
					if (sort >= 0) {
						if (sort < (e.length * 60)) {
							sort = 0;
							time = 'now!';
						}
						events_array.push( {name:e.name, txt:time, sort:sort, dtime:dtime, title:title} );
					} else if (sort > -(e.length * 60)) {
						sort = 0;
						time = 'now!';
						events_array.push( {name:e.name, txt:time, sort:sort, dtime:dtime, title:title} );
					}
				});
				// Sort and trim
				events_array.sort( function (a,b) {
					return a.sort - b.sort;
				});
				if (self.settings.num_events > 0) {
					events_array = events_array.slice(0,self.settings.num_events);
				}
				// Print Events
				if (events_array.length > 0) {
					$content.find('.etevents-list').empty();
					events_array.forEach( function (e) {
						var eClass = 'event';
						if (e.sort === 0) { eClass += ' now'; }
						$content.find('.etevents-list').append(
							$('<li>')
								.addClass(eClass)
								.append(
									$('<label>').addClass('event-title').text(e.name),
									$('<time>').addClass('event-timer').attr({ 'datetime':e.dtime, 'title':e.title }).text(e.txt)
								)
						);
					});
				}
			}
		},

		/**
		 * Update the Voice of Seren values in the Events popup
		 * @return {undefined}
		 */
		updateVos: function () {
			// Current UTC time
			var now = moment.utc();

			// Error updating VoS
			var vosError = function (jqXHR, status, error) {
				console.warn('Error loading VoS:\n' + status + ': ' + error);
				$content.find('.VoS-time').empty().append('There was an error loading the Voice of Seren. Please try again later.<br/>If this issue persists, please <a href="/w/RS:AR">contact an administrator</a>.');
				$content.find('.VoS-container').empty();
				$content.find('.VoS-last').empty().append('not found');
			};
			// Update Voice of Seren (VoS)
			var vosSucc = function (vosjson, status, jqXHR) {
				mw.log(vosjson);
				if (!(vosjson.data && vosjson.data[0] && vosjson.data[0].districts)) {
					vosError({}, 'Missing districts', 'returned json did not contain a districts array');
					return;
				}

				var voschng = moment(now).add(1, 'hours').startOf('hour'),
					chngtitle = voschng.format('MMM D') + ' at ' + voschng.format('HH:mm') + ' game time',
					currvos = vosjson.data[0].districts,
					lastvos = vosjson.data[1].districts;
				
				$content.find('.VoS-time').empty().append(
					$('<time>').addClass('time').attr({ 'datetime':voschng.format(), 'title':chngtitle }).text(voschng.fromNow(true)),
					' until new districts are active.'
				);
				$content.find('.VoS-container').empty().append(
					$('<div>').addClass('event-box VoS-district').append(
						$('<div>').addClass('VoS-image VoS-'+currvos[0]),
						$('<div>').addClass('VoS-text').append(
							$('<span>').addClass('by-line').attr('title', voslocs[currvos[0]][1]).text(voslocs[currvos[0]][0]),
							$('<label>').addClass('event-name').html('<a href="/w/' + currvos[0] + '_Clan" title="' + currvos[0] + ' Clan">' + currvos[0] + '</a>')
						)
					),
					$('<div>').addClass('event-box VoS-district').append(
						$('<div>').addClass('VoS-image VoS-'+currvos[1]),
						$('<div>').addClass('VoS-text').append(
							$('<span>').addClass('by-line').attr('title', voslocs[currvos[1]][1]).text(voslocs[currvos[1]][0]),
							$('<label>').addClass('event-name').html('<a href="/w/' + currvos[1] + '_Clan" title="' + currvos[1] + ' Clan">' + currvos[1] + '</a>')
						)
					)
				);

				$content.find('.VoS-last').empty();
				$content.find('.VoS-last').append(lastvos[0] + ', ' + lastvos[1]);

				// Set time for next check
				self.nextvos = moment(now).startOf('hour').add(61, 'minutes').format();
			};

			// Make api Call
			$content.find('.VoS-time').empty().append('Loading current VoS...');
			$content.find('.VoS-container').empty();
			$content.find('.VoS-last').empty().append('Loading last VoS...');
			$.ajax({
				dataType: 'json',
				url: vosurl,
				error: vosError,
				success: vosSucc
			});
		},

		/**
		 * Update the Voice of Seren time display only
		 * @return {undefined}
		 */
		updateVostime: function () {
			// Next change
			var voschng = moment.utc().add(1, 'hours').startOf('hour'),
				chngtitle = voschng.format('MMM D') + ' at ' + voschng.format('HH:mm') + ' game time';
			// Update time display
			$content.find('.VoS-time').empty().append(
				$('<time>').addClass('time').attr({ 'datetime':voschng.format(), 'title':chngtitle }).text(voschng.fromNow(true)),
				' until new districts are active.'
			);
		},
		
		/**
		 * Opens the settings form
		 * @return {undefined}
		 */
		openSettings: function () {
			// Update settings, switch window
			var switchWind = function () {
				// Update form elements
				settingsForm.sort.setValue(self.settings.daily_sort);
				settingsForm.notifs.setValue(self.settings.notifs);
				// Disable if notifications disabled
				if (self.supportNotif) {
					settingsForm.notifs.setDisabled(false);
				} else {
					settingsForm.notifs.setDisabled(true);
				}
				settingsForm.raven.setValue(self.settings.raven);
				settingsForm.events.setValue(self.settings.et_events);
				settingsForm.eventsNum.setValue(self.settings.num_events);
				settingsForm.eventsNum.setDisabled(!self.settings.et_events);
				settingsForm.eventsNumLay.toggle(self.settings.et_events);
				settingsForm.tms.setValue(self.settings.merch);
				settingsForm.pof.setValue(self.settings.farm);
				settingsForm.spots.setValue(self.settings.spots);
				//settingsForm.mg.setValue(self.settings.mg_spotlight);
				//settingsForm.dd.setValue(self.settings.dd_spotlight);
				//settingsForm.ed.setValue(self.settings.ed_spotlight);
				settingsForm.next.setValue(self.settings.show_next);
				settingsForm.pvm.setValue(self.settings.pvm);
				
				$('.gsw-events-popup .events-settings').removeClass('oo-ui-element-hidden');
				$('.gsw-events-popup .events-settings').addClass('slideout');
				setTimeout( function () {
					$('.gsw-events-popup .events-settings').removeClass('slideout');
				}, 5);
			};
			self.loadSettings().then(switchWind(), switchWind());
		},

		/**
		 * Opens the notifications form
		 * @return {undefined}
		 */
		openNotifs: function () {
			var switchWind = function () {
				// Update form elements
				notifsForm.browsnotif.setValue(self.settings.nobrownotifs);
				notifsForm.dailies.setValue(self.settings.notiftype.dailies);
				simple_dailies.forEach( function (dd) {
					notifsForm[dd.id].setSelected(self.settings.notiftype[dd.id]);
				});
				notifsForm.raven.setValue(self.settings.notiftype.raven);
				notifsForm.dreset.setValue(self.settings.notiftype.dreset);
				notifsForm.wreset.setValue(self.settings.notiftype.wreset);
				notifsForm.mreset.setValue(self.settings.notiftype.mreset);
				notifsForm.vos.setValue(self.settings.notiftype.vos);
				for (var clan in voslocs) {
					notifsForm[clan].setSelected(self.settings.notiftype['vos' + clan]);
				}
				notifsForm.etevents.setValue(self.settings.notiftype.etevents);
				notifsForm.tms.setValue(self.settings.notiftype.tms);
				if (self.settings.notiftype.tmsitems) {
					var tmsitems = self.settings.notiftype.tmsitems;
					notifsForm.tmssel.clearItems();
					tmsitems.forEach( function (item) {
						notifsForm.tmssel.addTag(item, item);
					});
				} else {
					notifsForm.tmssel.clearItems();
				}
				notifsForm.pof.setValue(self.settings.notiftype.pof);
				pofresets.forEach( function (fres) {
					notifsForm[fres].setSelected(self.settings.notiftype['pof' + fres]);
				});
				notifsForm.vorago.setValue(self.settings.notiftype.vorago);
				notifsForm.araxxor.setValue(self.settings.notiftype.araxxor);
				notifsForm.rots.setValue(self.settings.notiftype.rots);
				notifsForm.mg_spotlight.setValue(self.settings.notiftype.mg_spotlight);
				notifsForm.ed_spotlight.setValue(self.settings.notiftype.ed_spotlight);
				notifsForm.dd_spotlight.setValue(self.settings.notiftype.dd_spotlight);

				$('.gsw-events-popup .events-notifs').removeClass('oo-ui-element-hidden');
				$('.gsw-events-popup .events-notifs').addClass('slideout');
				setTimeout( function () {
					$('.gsw-events-popup .events-notifs').removeClass('slideout');
				}, 5);
			};
			self.loadSettings().then(switchWind(), switchWind());
		},

		/**
		 * Sends notifications to the browser based on user prefs
		 * @return {undefined}
		 */
		notifs: function () {
			mw.log('Running notifications');
			// TODO: Check that still not using service worker notifs
			if (self.swnotif) {
				mw.log('Exiting notifications, using service worker instead');
				window.clearInterval(notiftimer);
				return;
			}
			
			// Notifications to send
			var notifs = [];

			// Latest rotations and settings from localStorage
			if (gs.hasLocalStorage()) {
				var last = {};
				try {
					last = JSON.parse(localStorage.getItem(localLast));
				} catch (err) {
					last = {};
					console.warn('Error loading latest rotations from localStorage');
				}
				for (var l in last) {
					self.lastRot[l] = last[l];
				}

				var prefs = {};
				try {
					prefs = JSON.parse(localStorage.getItem(localKey));
				} catch (err) {
					prefs = {};
					console.warn('Error loading settings (events)');
				}
				for (var p in prefs) {
					self.settings[p] = prefs[p];
				}
			}

			// Exit if notifs turned off
			if (!self.settings.notifs) {
				mw.log('Exiting notifications, they\'ve been disabled');
				return;
			}
			
			// Current UTC time
			var now = moment.utc();

			// Prevent multiple tabs from updating in short period of time
			mw.log( now.diff(moment(self.lastRot.time), 'seconds') );
			if ( now.diff(moment(self.lastRot.time), 'seconds') <= 255 ) {
				return;
			} else {
				self.lastRot.time = now.format();
			}

			// VoS
			if (now.isBefore( moment(now).startOf('hour').add(10,'minutes') )) {
				var vosError = function (jqXHR, status, error) {
					console.warn('Error loading VoS:\n' + status + ': ' + error);
				};
				var vosSucc = function (vosjson, status, jqXHR) {
					if (!vosjson.districts) {
						vosError({}, 'Missing districts', 'returned json did not contain a districts array');
						return;
					}
					var vostext = vosjson.districts[0] + ',' + vosjson.districts[1];
					if (vostext !== self.lastRot.vos) {
						self.lastRot.vos = vostext;
						var vosnotifs = [];
						vosjson.districts.forEach( function (clan) {
							if (self.settings.notiftype['vos' + clan] && self.settings.notiftype.vos == 'Some' || self.settings.notiftype.allvos) {
								var img = gs.getFileURL(clan + ' Clan.png');
								vosnotifs.push( { title:'VoS ' + clan, opts:{
									badge: img,
									body: 'Voice of Seren is now active in the ' + clan + ' district.',
									tag: 'vos-' + clan,
									icon: img,
									image: img,
									vibrate: true,
									renotify: true,
									requireInteraction: false
								} } );
							}
						});

						self.sendNotifs( vosnotifs, true );

						// Save the last rotations to localstorage if possible
						if (gs.hasLocalStorage()) {
							var string = JSON.stringify(self.lastRot);
							try {
								localStorage.setItem(localLast, string);
							} catch (err) {
								console.warn('Error saving latest rotations to localStorage');
							}
						}
					}
				}

				$.ajax({
					dataType: 'json',
					url: simplevosurl,
					error: vosError,
					success: vosSucc
				});
			}

			// Dailies
			simple_dailies.forEach( function (dd) {
				if (self.settings.notiftype[dd.id] && self.settings.notiftype.dailies == 'Some' || self.settings.notiftype.alldailies) {
					var next = moment(now).startOf(dd.from).add((dd.offset), 'minutes'),
						nextEnd = moment(next).add(dd.length, 'minutes'),
						text = dd.name + ' is going on now!';
					while (!now.isBefore(nextEnd)) {
						next.add(dd.repeat, 'minutes');
						nextEnd.add(dd.repeat, 'minutes');
					}

					if (now.isBefore(next) && next.diff(now, 'seconds') <= 300) {
						// Will happen soon
						text = dd.name + ' is starting in ' + next.fromNow(true);

						notifs.push( { title:dd.name, opts:{
							badge: dd.img,
							body: text,
							tag: 'dd-' + dd.name,
							icon: dd.img,
							image: dd.img,
							vibrate: true,
							renotify: true,
							requireInteraction: false
						} } );
					} else if (now.isAfter(next)) {
						// Happening now
						notifs.push( { title:dd.name, opts:{
							badge: dd.img,
							body: text,
							tag: 'dd-on-' + dd.name,
							icon: dd.img,
							image: dd.img,
							vibrate: true,
							renotify: false,
							requireInteraction: false
						} } ); 
					}
				}
			});

			// Raven
			if (self.settings.notiftype.raven) {
				var days_into = -( moment('0', 'X').utc().add(6, 'days').diff(now, 'days') ) % 13,
					text = 'Raven is currently spawned in Prifddinas',
					tag = 'raven',
					img = '/images/d/d5/Raven_%28Prifddinas%29.png';
				if (days_into >= 11) {
					var tospawn = 13 - days_into;
					if (tospawn > 1) {
						text = 'Raven spawns in Prifddinas in ' + tospawn + 'days';
					} else {
						text = 'Raven spawns in Prifddinas tomorrow';
					}
					tag = 'raven-' + tospawn;
				}
				if (days_into < 1 || days_into >= 11) {
					if (self.lastRot.raven != tag) {
						notifs.push( { title:'Raven (Prif)', opts:{
							badge: img,
							body: text,
							tag: tag,
							icon: img,
							image: img,
							vibrate: true,
							renotify: false,
							requireInteraction: false
						} } );
						self.lastRot.raven = tag;
					}
				}
			}

			// Resets
			if (self.settings.notiftype.dreset) {
				// Daily
				var dailyReset = moment(now).add(1, 'days').startOf('day'),
					img = '';
				if (dailyReset.diff(now, 'minutes') <= 10) {
					notifs.push( { title:'Daily Reset', opts:{
						badge: img,
						body: 'Daily reset in ' + dailyReset.fromNow(true),
						tag: 'daily-reset',
						icon: img,
						image: img,
						vibrate: true,
						renotify: true,
						requireInteraction: false
					} } );
				}
			}
			if (self.settings.notiftype.wreset) {
				// Weekly
				var weeklyReset = moment(now).day(3).startOf('day'),
					img = '';
				if (now.isAfter(weeklyReset)) {
					weeklyReset.add(1, 'week');
				}
				if (weeklyReset.diff(now, 'minutes') <= 10 ) {
					notifs.push( { title:'Weekly Reset', opts:{
						badge: img,
						body: 'Weekly reset in ' + weeklyReset.fromNow(true),
						tag: 'weekly-reset-min',
						icon: img,
						image: img,
						vibrate: true,
						renotify: true,
						requireInteraction: false
					} } );
				} else if (weeklyReset.diff(now, 'days') <= 1 && now.isAfter(moment(self.lastRot.wreset))) {
					self.lastRot.wreset = weeklyReset.format();
					notifs.push( { title:'Weekly Reset', opts:{
						badge: img,
						body: 'Weekly reset is tomorrow',
						tag: 'weekly-reset',
						icon: img,
						image: img,
						vibrate: true,
						renotify: false,
						requireInteraction: false
					} } );
				}
			}
			if (self.settings.notiftype.mreset) {
				// Monthly
				var monthlyReset = moment(now).add(1, 'months').startOf('month'),
					img = '';
				if (monthlyReset.diff(now, 'minutes') <= 10) {
					notifs.push( { title:'Monthly Reset', opts:{
						badge: img,
						body: 'Monthly reset in ' + monthlyReset.fromNow(true),
						tag: 'monthly-reset-min',
						icon: img,
						image: img,
						vibrate: true,
						renotify: true,
						requireInteraction: false
					} } );
				} else if (monthlyReset.diff(now, 'days') <= 1  && now.isAfter(moment(self.lastRot.mreset))) {
					self.lastRot.mreset = monthlyReset.format();
					notifs.push( { title:'Monthly Reset', opts:{
						badge: img,
						body: 'Monthly reset is tomorrow',
						tag: 'monthly-reset',
						icon: img,
						image: img,
						vibrate: true,
						renotify: false,
						requireInteraction: false
					} } );
				}
			}

			// Daily cached
			if (self.settings.notiftype.tms == 'All' || self.settings.notiftype.tms == 'Some' || self.settings.notiftype.etevents) {
				self.loadEvents().then( function () {
					var dnotifs = [];

					// Events team events
					if (self.settings.notiftype.etevents) {
						var img = '/images/4/43/Apple-touch-icon.png';
						et_events.forEach( function (e) {
							var start = moment(e.date),
								end = moment(start).add(e.length, 'minutes');

							if (now.isBefore(start) && start.diff(now, 'seconds') <= 300) {
								// Starting soon
								dnotifs.push( { title:e.name, opts:{
									badge: img,
									body: 'Join the RS Wiki Events Team for ' + e.name + ' in ' + start.fromNow(true),
									tag: 'et-' + e.name,
									icon: img,
									image: img,
									vibrate: true,
									renotify: false,
									requireInteraction: false
								} } );
							} else if (now.isAfter(start) && now.isBefore(end)) {
								// Going on
								dnotifs.push( { title:e.name, opts:{
									badge: img,
									body: 'Join the RS Wiki Events Team for ' + e.name + ' going on now!',
									tag: 'et-on-' + e.name,
									icon: img,
									image: img,
									vibrate: true,
									renotify: false,
									requireInteraction: false
								} } );
							}
						});
					}

					self.sendNotifs( dnotifs );
					// Save the last rotations to localstorage if possible
					if (gs.hasLocalStorage()) {
						var string = JSON.stringify(self.lastRot);
						try {
							localStorage.setItem(localLast, string);
						} catch (err) {
							console.warn('Error saving latest rotations to localStorage');
						}
					}
				}, function (err) {
					console.warn('Error loading events and merchant stock');
					console.warn(err);
				});
			}

			// For spotlights
			var curspotlight = function(items, duration, offset) {
				var days_passed = -( moment('0', 'X').utc().add(offset, 'days').diff(now, 'days') ),
					days_into = days_passed % (duration * items.length),
					rotation = Math.floor(days_into / duration),
					days_till = duration - (days_into % duration);

				return [rotation, items[rotation], days_till];
			};

			// Player-owned farm notifications
			if (self.settings.notiftype.pof == 'All' || self.settings.notiftype.pof == 'Some') {
				var pofnots = [];
				// Is less 10 minutes before reset
				if ( moment(now).add(1, 'days').startOf('day').diff(now, 'minutes') <= 10 ) {
					// Small buyer, easy request
					if (self.settings.notiftype.pof == 'All' || self.settings.notiftype['pofSmall Buyer']) {
						pofnots.push( 'small buyer' );
					}
					if (self.settings.notiftype.pof == 'All' || self.settings.notiftype['pofEasy Request']) {
						pofnots.push( 'easy request' );
					}
					// Medium buyer
					if (self.settings.notiftype.pof == 'All' || self.settings.notiftype['pofMedium Buyer']) {
						if ( curspotlight(['med'], 2, 1)[2] == 1 ) {
							pofnots.push( 'medium buyer' );
						}
					}
					// Medium request
					if (self.settings.notiftype.pof == 'All' || self.settings.notiftype['pofMedium Request']) {
						if ( curspotlight(['med'], 3, 0)[2] == 1 ) {
							pofnots.push( 'medium request' );
						}
					}
					// Large buyer
					if (self.settings.notiftype.pof == 'All' || self.settings.notiftype['pofLarge Buyer']) {
						if ( curspotlight(['large'], 3, 0)[2] == 1 ) {
							pofnots.push( 'large buyer' );
						}
					}
				}
				// Generate notification
				if (pofnots.length > 0) {
					var img = '',
						nbod = 'Spawning soon:',
						len = pofnots.length,
						i = 1;

					pofnots.forEach( function (npc) {
						if (i == 1) {
							nbod = nbod + ' ' + npc;
						} else if (i == len) {
							nbod =  nbod + ' and ' + npc;
						} else {
							nbod = nbod + ', ' + npc;
						}
						i ++;
					});
					nbod = nbod + '.';

					notifs.push( { title:'POF Spawns', opts:{
						badge: img,
						body: nbod,
						tag: 'pofresets',
						icon: img,
						image: img,
						vibrate: true,
						renotify: false,
						requireInteraction: false
					} } );
				}
			}
			
			// Minigame Spotlight
			if (self.settings.notiftype.mg_spotlight) {
				var spot = curspotlight(mg_spotlights, 3, 49);
				if (spot[0] != self.lastRot.mgspot) {
					self.lastRot.mgspot = spot[0];
					var img = '';

					notifs.push( { title:'Minigame Spotlight', opts:{
						badge: img,
						body: 'The currently spotlighted minigame is ' + spot[1],
						tag: 'mgspot',
						icon: img,
						image: img,
						vibrate: true,
						renotify: true,
						requireInteraction: false
					} } );
				}
			}

			// Elite dungeon spotlight
			if (self.settings.notiftype.ed_spotlight) {
				var spot = curspotlight(ed_spotlights, 1, 0);
				if (spot[0] != self.lastRot.edspot) {
					self.lastRot.edspot = spot[0];
					var imgs = {
							'Temple of Aminishi':'/images/thumb/f/fd/Relic_of_aminishi_%28rare%29_detail.png/200px-Relic_of_aminishi_%28rare%29_detail.png',
							'Dragonkin Laboratory':'/images/thumb/d/d4/Laboratory_relic_%28uncommon%29_detail.png/180px-Laboratory_relic_%28uncommon%29_detail.png',
							'The Shadow Reef':'/images/thumb/e/e5/Umbral_urn_detail.png/160px-Umbral_urn_detail.png'
						},
						img = imgs[spot[1]];

					notifs.push( { title:'Elite Dungeon Spotlight', opts:{
						badge: img,
						body: 'The currently spotlighted elite dungeon is ' + spot[1],
						tag: 'edspot',
						icon: img,
						image: img,
						vibrate: true,
						renotify: true,
						requireInteraction: false
					} } );
				}
			}
			
						// Save the last rotations to localstorage if possible
			if (gs.hasLocalStorage()) {
				var string = JSON.stringify(self.lastRot);
				try {
					localStorage.setItem(localLast, string);
				} catch (err) {
					console.warn('Error saving latest rotations to localStorage');
				}
			}
		},
		
				/**
		 * Sends notifications (on site or browser)
		 * @param  {array} notifs Array of notifcation objects to be sent
		 * @param  {boolean} add    (optional) Whether to replace or add to current notifications
		 * @return {undefined}
		 */
		sendNotifs: function ( notifs, add ) {
			// Send browser notifications
			var sendNotifs = function ( nl ) {
				mw.log('Sending browser notifications');
				// Google analytics tracker
				if (typeof ga === 'function') {
					ga('gtag_UA_126479006_1.send', 'event', 'Gadget-events', 'Send Notifactions', 'Browser', nl.length);
				}

				var interval = 200;
				nl.forEach( function (n,i) {
					mw.log('Sending ' + n.title);
					// Some browser block them if to many sent in a short time
					setTimeout(function() {
						var bn = new Notification(n.title, n.opts);
						bn.onclick = function () {
							popup.toggle(true);
							bn.close.bind(bn);
						};
					}, interval * i);
				});
			};

			// Send popup notifs
			var popNotifs = function ( nl, noempty ) {
				mw.log('Sending popup notifications');
				// Google analytics tracker
				if (typeof ga === 'function') {
					ga('gtag_UA_126479006_1.send', 'event', 'Gadget-events', 'Send Notifactions', 'Popup', nl.length);
				}

				var createNotifs = function () {
					var notiflist = $notifs.find('ul.event-notifications');
					if ( !noempty ) {
						notiflist.empty();
					}
					nl.forEach( function (n) {
						mw.log('Sending ' + n.title);
						notiflist.append(
							$('<li>').addClass('event-notif').append(
								$('<div>').addClass('notif-image').append(
									$('<img>', {'src':n.opts.image, 'alt':n.title})
								),
								$('<label>').text(n.title),
								$('<span>').text(n.opts.body)
							)
						);
					});

					notifpopup.toggle(true);
					setTimeout(notifpopup.toggle, 180000, false);
				};
				if (!formMade) {
					mw.log('Initialise interface');
					mw.loader.using(['oojs-ui-core', 'oojs-ui-windows', 'oojs-ui-widgets']).then(self.initInt).then(createNotifs);
				} else {
					createNotifs();
				}
			};

			// Check permissions
			if (self.supportNotif && Notification.permission === 'granted' && !self.settings.nobrownotifs) {
				mw.log('Notification permission granted');
				// Send browser notifications
				sendNotifs( notifs );
			} else if (self.supportNotif && Notification.permission !== 'denied' && !self.settings.nobrownotifs) {
				mw.log('Requesting permission');
				// Request permissions
				Notification.requestPermission().then( function (permission) {
					if (permission === 'granted') {
						mw.log('Notification permission granted');
						// Send browser notifications
						sendNotifs( notifs );
					} else {
						mw.log('Notification permission denied or dismissed');
						// Send popup notifications
						popNotifs( notifs, add );
					}
				}, function () {
					console.warn('Error getting notification permissions');
					// Send popup notifications
					popNotifs( notifs, add );
				});
			} else {
				mw.log('Notificationpermission denied');
				// Send popup notifications
				popNotifs( notifs, add );
			}

		}
	};

	mw.loader.using(['ext.gadget.gsw-util', 'moment', 'mediawiki.api', 'mediawiki.api.messages'], function () {
		$(self.init);
	});

}(jQuery, mediaWiki, gswiki));
//</nowiki>