VulpesOS/assets/webapps/clock.gaiamobile.org/js/startup.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

3825 lines
108 KiB
JavaScript

requirejs.config({
// waitSeconds is set to the default here; the build step rewrites
// it to 0 in build/require_config.jslike so that we never timeout
// waiting for modules in production. This is important when the
// device is under super-low-memory stress, as it may take a while
// for the device to get around to loading things like Clock's alarm
// ringing screen, and we absolutely do not want that to time out.
waitSeconds: 0,
paths: {
shared: '../shared'
},
shim: {
'shared/js/template': {
exports: 'Template'
},
'shared/js/gesture_detector': {
exports: 'GestureDetector'
},
'shared/js/async_storage': {
exports: 'asyncStorage'
},
'shared/js/accessibility_helper': {
exports: 'AccessibilityHelper'
}
}
});
define("require_config", function(){});
(function(exports) {
var AccessibilityHelper = {
/**
* For a set of tab elements, set aria-selected attribute in accordance with
* the current selection.
* @param {Object} selectedTab a tab to select object.
* @param {Array} tabs an array of tabs.
*/
setAriaSelected: function ah_setAriaSelected(selectedTab, tabs) {
// In case tabs is a NodeList, that does not have forEach.
Array.prototype.forEach.call(tabs, function setAriaSelectedAttr(tab) {
tab.setAttribute('aria-selected',
tab === selectedTab ? 'true' : 'false');
});
}
};
exports.AccessibilityHelper = AccessibilityHelper;
})(window);
define("shared/js/accessibility_helper", (function (global) {
return function () {
var ret, fn;
return ret || global.AccessibilityHelper;
};
}(this)));
define('tabs',['require','shared/js/accessibility_helper'],function(require) {
'use strict';
var AccessibilityHelper = require('shared/js/accessibility_helper');
/**
* Abstraction for handling the Tabs links at the bottom of the UI.
* @param {HTMLElement} element The containing element for the Tabs UI.
*/
function Tabs(element) {
this.element = element;
this.links = element.querySelectorAll('a');
this.element.addEventListener('click', this);
}
/**
* Update selected attributes for the selected tab.
* Also emit a 'selected' event with the relevant data.
*/
Tabs.prototype.handleEvent = function tabsHandleEvent(event) {
AccessibilityHelper.setAriaSelected(event.target, this.links);
};
return Tabs;
});
define('view',['require'],function(require) {
'use strict';
var priv = new WeakMap();
var elementMap = new WeakMap();
/**
* A View is simply a wrapper around an element.
*
* @constructor
* @param {HTMLElement} element The element that will be wrapped by this view.
*/
function View(element) {
if (!(this instanceof View)) {
throw new Error('View must be called as a constructor');
}
elementMap.set(element, this);
Object.defineProperties(this, {
id: { value: element.id },
element: { value: element }
});
priv.set(this, {
visible: !element.classList.contains('hidden'),
pendingVisible: false
});
}
/**
* Find or create a view instance for an element.
*
* @param {HTMLElement} element The element that will be wrapped by the view.
* @param {Function} ctor The constructor method for the view, defaults to View.
*/
View.instance = function(element, ctor = View) {
if (elementMap.has(element)) {
return elementMap.get(element);
}
return new ctor(element);
};
Object.defineProperties(View.prototype, {
/**
* View.prototype.visible - set to true or false to toggle the "hidden" class
* on the element.
*
* Also emits a 'visibilitychange' event passing either true or false to show
* the new visible state. The event happens before the class is changed to
* allow time to modify the DOM before something becomes visible.
*/
visible: {
get: function() {
return priv.get(this).visible;
},
set: function(value) {
var state = priv.get(this);
value = !!value;
if (state.visible !== value || state.pendingVisible) {
state.pendingVisible = false;
state.visible = value;
var event = new CustomEvent('panel-visibilitychange', {
detail: {
isVisible: value
}
});
this.element.dispatchEvent(event);
if (!value) {
this.element.classList.add('hidden');
} else {
this.element.classList.remove('hidden');
}
}
return value;
}
},
pendingVisible: {
get: function() {
return priv.get(this).pendingVisible;
},
set: function(value) {
return (priv.get(this).pendingVisible = !!value);
}
}
});
return View;
});
define('sounds',['require','exports','module'],function(require, exports) {
'use strict';
// Sadly, this is needed because when sound l10n ids change, they no
// longer match up with the sound filename.
var DEFAULT_SOUND = 'ac_awake.opus';
var SOUND_FILE_TO_L10N_ID = {
'0': 'noSound',
'ac_awake.opus': 'ac_awake_opus',
'ac_crystalize.opus': 'ac_crystalize_opus',
'ac_cycle.opus': 'ac_cycle_opus',
'ac_digicloud.opus': 'ac_digicloud_opus',
'ac_humming_waves.opus': 'ac_humming_waves_opus',
'ac_into_the_void.opus': 'ac_into_the_void_opus',
'ac_lightly.opus': 'ac_lightly_opus',
'ac_mobile.opus': 'ac_mobile_opus',
'ac_pinger.opus': 'ac_pinger_opus',
'ac_skip.opus': 'ac_skip_opus',
'ac_tri.opus': 'ac_tri_opus',
'ac_universal.opus': 'ac_universal_opus'
};
exports.normalizeSound = function(sound) {
// Since ringtones are stored on the system, they may be
// version-dependent. Ensure the sound exists (based upon our
// understanding of the available sounds); if not, default to
// something else.
if (sound && !SOUND_FILE_TO_L10N_ID.hasOwnProperty(sound)) {
return DEFAULT_SOUND;
} else {
return sound;
}
};
/**
* Given a sound ID, return the label to be displayed, for instance,
* on a FormButton.
*/
exports.formatLabel = function(sound) {
return (sound === null || sound === '0') ?
'noSound' : SOUND_FILE_TO_L10N_ID[sound];
};
});
define('alarm_database',['require','./sounds','alarm','alarm'],function(require) {
var sounds = require('./sounds');
/**
* The AlarmDatabase stores a list of alarms in IndexedDB. All
* mutation operations return Promises, for easy chaining and state
* management. This module returns the one-and-only instance of
* AlarmDatabase.
*/
function AlarmDatabase(dbName, storeName, version) {
this.dbName = dbName;
this.storeName = storeName;
this.version = version;
this.withDatabase = new Promise((resolve, reject) => {
var request = indexedDB.open(this.dbName, this.version);
request.onupgradeneeded = (event) => {
var db = event.target.result;
// Ensure the object store exists.
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, {
keyPath: 'id',
autoIncrement: true
});
}
};
request.onerror = (() => reject(request.errorCode));
request.onsuccess = ((event) => resolve(event.target.result));
}).then((db) => {
// Only return when all of the alarms have been upgraded.
return new Promise((resolve, reject) => {
// Go through existing alarms here, and make sure they conform
// to the latest spec (upgrade old versions, etc.).
var transaction = db.transaction(this.storeName, 'readwrite');
var store = transaction.objectStore(this.storeName);
var cursor = store.openCursor();
cursor.onsuccess = (event) => {
var cursor = event.target.result;
if (cursor) {
store.put(this.normalizeAlarmRecord(cursor.value));
cursor.continue();
}
};
transaction.oncomplete = (() => resolve(db));
transaction.onerror = ((evt) => reject(evt.target.errorCode));
});
}).catch(function(err) {
// Explicit err.toString() coercion needed to see a message.
console.error('AlarmDatabase Fatal Error:', err.toString());
});
}
AlarmDatabase.prototype = {
/**
* Given an Alarm's JSON data (as returned by IndexedDB),
* normalize any properties to ensure it conforms to the most
* current Alarm specification.
*/
normalizeAlarmRecord: function(alarm) {
if (!alarm.registeredAlarms) {
alarm.registeredAlarms = {};
}
if (typeof alarm.enabled !== 'undefined') {
delete alarm.enabled;
}
if (typeof alarm.normalAlarmId !== 'undefined') {
alarm.registeredAlarms.normal = alarm.normalAlarmId;
delete alarm.normalAlarmId;
}
if (typeof alarm.snoozeAlarmId !== 'undefined') {
alarm.registeredAlarms.snooze = alarm.snoozeAlarmId;
delete alarm.snoozeAlarmId;
}
var newRepeat = {};
var i;
// Map '1111100' string bitmap to a repeat object with day properties.
if (typeof alarm.repeat === 'string') {
for (i = 0; i < alarm.repeat.length; i++) {
if (alarm.repeat[i.toString()] === '1') {
newRepeat[i.toString()] = true;
}
}
alarm.repeat = newRepeat;
} else if (typeof alarm.repeat === 'object') {
var keys = Object.keys(alarm.repeat);
if (keys.length !== 0 && Number.isNaN(parseInt(keys[0]))) {
var oldKeys = ['sunday', 'monday', 'tuesday', 'wednesday',
'thursday', 'friday', 'saturday'];
for (i = 0; i < keys.length; i++) {
var index = oldKeys.indexOf(keys[i]);
if (index !== -1) {
newRepeat[index] = alarm.repeat[keys[i]];
}
}
alarm.repeat = newRepeat;
}
} else {
alarm.repeat = newRepeat;
}
// Pre-April-2014 code may have stored 'vibrate' and 'sound' as
// the string "0", and hour/minute as strings.
alarm.sound = sounds.normalizeSound(
alarm.sound !== '0' ? alarm.sound : null);
alarm.vibrate = (alarm.vibrate && alarm.vibrate !== '0') || false;
alarm.hour = parseInt(alarm.hour, 10);
alarm.minute = parseInt(alarm.minute, 10);
return alarm;
},
/**
* Execute a database store request with the given method and
* arguments, returning a Promise that will be fulfilled with the
* Store's result.
*/
withStoreRequest: function(method /*, args... */) {
var args = Array.slice(arguments, 1);
var readmode = (/get/.test(method) ? 'readonly' : 'readwrite');
return this.withDatabase.then((database) => {
var store = database
.transaction(this.storeName, readmode)
.objectStore(this.storeName);
if (method === 'getAll') {
return objectStoreGetAll(store);
} else {
return new Promise((resolve, reject) => {
var request = store[method].apply(store, args);
request.onsuccess = (() => resolve(request.result));
request.onerror = (() => reject(request.errorCode));
});
}
});
},
put: function(alarm) {
var data = alarm.toJSON();
if (!data.id) {
delete data.id; // IndexedDB requires _no_ ID key, not null/undefined.
}
return this.withStoreRequest('put', data).then((id) => {
alarm.id = id;
});
},
getAll: function() {
var Alarm = require('alarm'); // Circular dependency.
return this.withStoreRequest('getAll').then((alarms) => {
return alarms.map((data) => new Alarm(data));
});
},
get: function(id) {
var Alarm = require('alarm'); // Circular dependency.
return this.withStoreRequest('get', id).then((data) => {
return new Alarm(data);
});
},
delete: function(id) {
return this.withStoreRequest('delete', id);
}
};
/**
* Return all records from an ObjectStore. This function is
* non-standard, but is such a common pattern that it has actually
* been included in certain implementations of IndexedDB. It is
* extracted here for clarity.
*/
function objectStoreGetAll(objectStore) {
return new Promise((resolve, reject) => {
var items = [];
var cursor = objectStore.openCursor();
cursor.onerror = reject;
cursor.onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
items.push(cursor.value);
cursor.continue();
}
else {
resolve(items);
}
};
});
}
// For Clock, we only use one database and store, both named 'alarms'.
// Right now, we're on version 7.
return new AlarmDatabase('alarms', 'alarms', 7);
});
define('alarm',['require','exports','module','alarm_database','alarm_database','alarm_database','alarm_database'],function(require, exports, module) {
'use strict';
/**
* Alarm represents one alarm instance. It tracks any mozAlarms it
* has registered, its IndexedDB ID, and any other properties
* relating to the alarm's schedule and firing options.
*/
function Alarm(opts) {
opts = opts || {};
var now = new Date();
var defaults = {
id: null,
registeredAlarms: {}, // keys: ('normal' or 'snooze') => mozAlarmID
// Map of weekdays { "0": true, "1": false, ... }
// "0" is for Sunday in Gregorian calendar
repeat: {},
hour: now.getHours(),
minute: now.getMinutes(),
label: '',
sound: 'ac_awake.opus',
vibrate: true,
snooze: 10 // Number of minutes to snooze
};
for (var key in defaults) {
this[key] = (key in opts ? opts[key] : defaults[key]);
}
}
Alarm.prototype = {
toJSON: function() {
return {
id: this.id,
registeredAlarms: this.registeredAlarms,
repeat: this.repeat,
hour: this.hour,
minute: this.minute,
label: this.label,
sound: this.sound,
vibrate: this.vibrate,
snooze: this.snooze
};
},
/**
* An alarm is enabled if and only if it has a registeredAlarm set
* with a type of 'normal'. To disable an alarm, any
* registeredAlarms are unregistered with mozAlarms and removed
* from this.registeredAlarms.
*/
isEnabled: function() {
for (var i in this.registeredAlarms) {
// Both 'normal' and 'snooze' registered alarms should be
// treated as enabled, because the alarm will imminently fire.
if (i === 'normal' || i === 'snooze') {
return true;
}
}
return false;
},
isRepeating: function() {
for (var key in this.repeat) {
if (this.repeat[key]) {
return true;
}
}
return false;
},
getNextAlarmFireTime: function(relativeTo) {
var now = relativeTo || new Date();
var nextFire = new Date(now.getTime());
nextFire.setHours(this.hour, this.minute, 0, 0);
while (nextFire <= now ||
(this.isRepeating() &&
!this.repeat[nextFire.getDay().toString()])) {
nextFire.setDate(nextFire.getDate() + 1);
}
return nextFire;
},
getNextSnoozeFireTime: function(relativeTo) {
var now = relativeTo || new Date();
return new Date(now.getTime() + this.snooze * 60 * 1000);
},
/**
* Schedule an alarm to ring in the future.
*
* @return {Promise}
* @param {'normal'|'snooze'} type
*/
schedule: function(type) {
var alarmDatabase = require('alarm_database'); // circular dependency
var firedate, promise;
if (type === 'normal') {
promise = this.cancel(); // Cancel both snooze and regular mozAlarms.
firedate = this.getNextAlarmFireTime();
} else if (type === 'snooze') {
promise = this.cancel('snooze'); // Cancel any snooze mozAlarms.
firedate = this.getNextSnoozeFireTime();
} else {
return Promise.reject('Invalid type for Alarm.schedule().');
}
// Save the alarm to the database first. This ensures we have a
// valid ID, and that we've saved any modified properties before
// attempting to schedule the alarm.
return promise.then(() => alarmDatabase.put(this)).then(() => {
return new Promise((resolve, reject) => {
// Then, schedule the alarm.
var req = navigator.mozAlarms.add(firedate, 'ignoreTimezone',
{ id: this.id, type: type });
req.onerror = reject;
req.onsuccess = (evt) => {
this.registeredAlarms[type] = evt.target.result;
resolve();
};
});
// After scheduling the alarm, this.registeredAlarms has
// changed, so we must save that too.
}).then(() => alarmDatabase.put(this))
.then(() => {
this._notifyChanged();
}).catch((e) => {
console.log('Alarm scheduling error: ' + e.toString());
throw e;
});
},
/**
* Cancel an alarm. If `type` is provided, cancel only that type
* ('normal' or 'snooze'). Returns a Promise.
*/
cancel: function(/* optional */ type) {
var types = (type ? [type] : Object.keys(this.registeredAlarms));
var alarmDatabase = require('alarm_database'); // circular dependency
types.forEach((type) => {
var id = this.registeredAlarms[type];
navigator.mozAlarms.remove(id);
delete this.registeredAlarms[type];
});
return alarmDatabase.put(this).then(() => {
this._notifyChanged();
}).catch((e) => {
console.log('Alarm cancel error: ' + e.toString());
throw e;
});
},
_notifyChanged: function(removed) {
// Only update the application if this alarm was actually saved
// (i.e. it has an ID).
if (this.id) {
var alarmDatabase = require('alarm_database');
// Updating Alarm info from the DataStore
alarmDatabase.getAll()
.then((alarms) => {
var storeAlarms = [];
alarms.forEach(function(a) {
// Grabbing alarms that are 'normal'
if(a.registeredAlarms.normal) {
storeAlarms.push(a.getNextAlarmFireTime());
}
});
return this.getNextAlarm(storeAlarms);
}).then((nextAlarm) => {
navigator.getDataStores('alarms')
.then((stores) => {
return [stores[0].getLength(), stores[0]];
}).then(([len, store]) => {
if(0 === len) {
return store.add(nextAlarm);
} else {
return store.put(nextAlarm, 1);
}
}).then((id) => {
if(id) {
console.log('[Clock] ==== Updated Alarm on LockScreen ====');
}
});
});
window.dispatchEvent(
new CustomEvent(removed ? 'alarm-removed' : 'alarm-changed', {
detail: { alarm: this }
})
);
}
},
/**
* Delete an alarm completely from the database, canceling any
* pending scheduled mozAlarms.
*/
delete: function() {
var alarmDatabase = require('alarm_database'); // circular dependency
return this.cancel().then(() => {
return alarmDatabase.delete(this.id).then(() => {
this._notifyChanged(/* removed = */ true);
});
});
},
/**
* Returns the next scheduled alarm within the next 24 hours
* @param {Array} type 'Date'
* @returns {Object} This is either an empty obj or hour and minute
*/
getNextAlarm: function(storeAlarms) {
var alarmIndex = [],
nextAlarm = {};
// remove alarms that have more than 24 hrs
storeAlarms.forEach( function(val,index) {
if((val.getTime() - new Date().getTime()) / 36e5 > 24 ) {
alarmIndex.push(index);
}
});
// remove last index first
alarmIndex.sort(function(a,b) {
return b - a;
});
alarmIndex.forEach( function(val) {
storeAlarms.splice(val, 1);
});
// sort alarms
storeAlarms.sort(function(a,b){
return new Date(a.getTime()) - new Date(b.getTime());
});
if(storeAlarms.length > 0) {
var mins = storeAlarms[0].getMinutes();
nextAlarm = {
hour : '' + storeAlarms[0].getHours(),
minute : mins < 10 ? '0' + mins : '' + mins
};
}
return nextAlarm;
}
};
module.exports = Alarm;
});
/**
* parse-loose-time v1.0.0
* https://github.com/millermedeiros/parse-loose-time/
* released under the MIT License
*/
define('ext/parse_loose_time',['require','exports','module'],function(require, exports, module) {
'use strict';
// Release the ZALGO!
var numeric = /\d{1,2}[^\d]*(\d{2})?/i;
var oclock = /\d+.*o.*clock/i;
var british = /(half|quarter|\d+).*(to|past)[^\d]*\d{1,2}/i;
var knownWords = [
'noon',
'noontime',
'midday',
'half-day',
'midnight'
];
// match "p" after digit for "pm"
var rPm = /\d\s*(o.*clock\s*)?p/i;
var checkKnowWords = {
test: function(str) {
return knownWords.indexOf(str) !== -1;
}
};
var map = [
[checkKnowWords, parseWords],
[british, parseBritish],
[oclock, parseOClock],
[numeric, parseNumeric]
];
module.exports = parseTime;
function parseTime(time) {
time = time.trim().toLowerCase();
// node 0.12 don't support Array#find :/
var fn;
map.some(function(a) {
if (a[0].test(time)) {
fn = a[1];
return true;
}
});
return fn ? fn(time) : null;
}
function parseNumeric(str) {
var hour;
var minute;
var num = extractNumbers(str);
switch (num.length) {
case 4:
hour = num.slice(0, 2);
minute = num.slice(2, 4);
break;
case 3:
hour = num.slice(0, 1);
minute = num.slice(1, 3);
break;
case 2:
case 1:
hour = num.slice(0, 2);
minute = 0;
break;
default:
return '';
}
hour = Number(hour);
minute = Number(minute);
if (num.length === 2 && hour > 24) {
return null;
}
if (hour < 12 && str.match(rPm)) {
hour += 12;
}
return {
hour: Math.min(hour, 24),
minute: Math.min(minute, 59)
};
}
function extractNumbers(str) {
return str.replace(/[^0-9]/g, '');
}
function parseOClock(str) {
// yes, we will consider "1:23 o'clock" the same as "1 o'clock"
var time = parseNumeric(str);
if (!time) {
return null;
}
time.minute = 0;
return time;
}
function parseBritish(str) {
// need to handle case where it is a custom numeric value
var parts = str.match(/(.+)(to|past)(.+)/i);
var time = parseNumeric(parts[3]);
if (!time) {
return null;
}
var diff;
if (str.indexOf('quarter') !== -1) {
diff = 15;
} else if (str.indexOf('half') !== -1) {
diff = 30;
} else {
diff = Number(extractNumbers(parts[1]));
}
var isTo = str.indexOf('to') !== -1;
if (isTo) {
time.hour -= 1;
time.minute = 60 - diff;
} else {
time.minute = diff;
}
return time;
}
function parseWords(str) {
if (str === 'midnight') {
return { hour: 0, minute: 0 };
}
return { hour: 12, minute: 0 };
}
});
define('connection/alarm',['require','exports','module','alarm','ext/parse_loose_time'],function(require, exports) {
'use strict';
var Alarm = require('alarm');
var parseTime = require('ext/parse_loose_time');
exports.onmessage = function(event, port) {
if (event.data.type !== 'create') {
port.postMessage({
error: `Error: Invalid gaia_alarm message type "${event.data.type}"`
});
return;
}
var time = event.data.time;
var timeObject = typeof time === 'object' ? time : parseTime(time);
if (!isValidTime(timeObject)) {
port.postMessage({
error: `Invalid alarm time "${time}"`
});
return;
}
var alarm = new Alarm(timeObject);
alarm.schedule('normal').then(() => {
window.dispatchEvent(new CustomEvent('alarm-changed', {
detail: { alarm: alarm, showBanner: false }
}));
port.postMessage({
hour: timeObject.hour,
minute: timeObject.minute,
time: alarm.getNextAlarmFireTime().getTime()
});
}).catch(err => {
port.postMessage({
error: `${err.message} ${err.stack}`
});
});
};
function isValidTime(time) {
// time can be "null" and "typeof null === 'object'"
return time && typeof time === 'object' && 'hour' in time &&
time.hour >= 0 && time.hour <= 24 && 'minute' in time &&
time.minute >= 0 && time.minute <= 59;
}
});
/* globals indexedDB */
/**
* This file defines an asynchronous version of the localStorage API, backed by
* an IndexedDB database. It creates a global asyncStorage object that has
* methods like the localStorage object.
*
* To store a value use setItem:
*
* asyncStorage.setItem('key', 'value');
*
* If you want confirmation that the value has been stored, pass a callback
* function as the third argument:
*
* asyncStorage.setItem('key', 'newvalue', function() {
* console.log('new value stored');
* });
*
* To read a value, call getItem(), but note that you must supply a callback
* function that the value will be passed to asynchronously:
*
* asyncStorage.getItem('key', function(value) {
* console.log('The value of key is:', value);
* });
*
* Note that unlike localStorage, asyncStorage does not allow you to store and
* retrieve values by setting and querying properties directly. You cannot just
* write asyncStorage.key; you have to explicitly call setItem() or getItem().
*
* removeItem(), clear(), length(), and key() are like the same-named methods of
* localStorage, but, like getItem() and setItem() they take a callback
* argument.
*
* The asynchronous nature of getItem() makes it tricky to retrieve multiple
* values. But unlike localStorage, asyncStorage does not require the values you
* store to be strings. So if you need to save multiple values and want to
* retrieve them together, in a single asynchronous operation, just group the
* values into a single object. The properties of this object may not include
* DOM elements, but they may include things like Blobs and typed arrays.
*
* Unit tests are in apps/gallery/test/unit/asyncStorage_test.js
*/
this.asyncStorage = (function() {
'use strict';
var DBNAME = 'asyncStorage';
var DBVERSION = 1;
var STORENAME = 'keyvaluepairs';
var db = null;
function withDatabase(f) {
if (db) {
f();
} else {
var openreq = indexedDB.open(DBNAME, DBVERSION);
openreq.onerror = function withStoreOnError() {
console.error('asyncStorage: can\'t open database:',
openreq.error.name);
};
openreq.onupgradeneeded = function withStoreOnUpgradeNeeded() {
// First time setup: create an empty object store
openreq.result.createObjectStore(STORENAME);
};
openreq.onsuccess = function withStoreOnSuccess() {
db = openreq.result;
f();
};
}
}
function withStore(type, callback, oncomplete) {
withDatabase(function() {
var transaction = db.transaction(STORENAME, type);
if (oncomplete) {
transaction.oncomplete = oncomplete;
}
callback(transaction.objectStore(STORENAME));
});
}
function getItem(key, callback) {
var req;
withStore('readonly', function getItemBody(store) {
req = store.get(key);
req.onerror = function getItemOnError() {
console.error('Error in asyncStorage.getItem(): ', req.error.name);
};
}, function onComplete() {
var value = req.result;
if (value === undefined) {
value = null;
}
callback(value);
});
}
function setItem(key, value, callback) {
withStore('readwrite', function setItemBody(store) {
var req = store.put(value, key);
req.onerror = function setItemOnError() {
console.error('Error in asyncStorage.setItem(): ', req.error.name);
};
}, callback);
}
function removeItem(key, callback) {
withStore('readwrite', function removeItemBody(store) {
var req = store.delete(key);
req.onerror = function removeItemOnError() {
console.error('Error in asyncStorage.removeItem(): ', req.error.name);
};
}, callback);
}
function clear(callback) {
withStore('readwrite', function clearBody(store) {
var req = store.clear();
req.onerror = function clearOnError() {
console.error('Error in asyncStorage.clear(): ', req.error.name);
};
}, callback);
}
function length(callback) {
var req;
withStore('readonly', function lengthBody(store) {
req = store.count();
req.onerror = function lengthOnError() {
console.error('Error in asyncStorage.length(): ', req.error.name);
};
}, function onComplete() {
callback(req.result);
});
}
function key(n, callback) {
if (n < 0) {
callback(null);
return;
}
var req;
withStore('readonly', function keyBody(store) {
var advanced = false;
req = store.openCursor();
req.onsuccess = function keyOnSuccess() {
var cursor = req.result;
if (!cursor) {
// this means there weren't enough keys
return;
}
if (n === 0 || advanced) {
// Either 1) we have the first key, return it if that's what they
// wanted, or 2) we've got the nth key.
return;
}
// Otherwise, ask the cursor to skip ahead n records
advanced = true;
cursor.advance(n);
};
req.onerror = function keyOnError() {
console.error('Error in asyncStorage.key(): ', req.error.name);
};
}, function onComplete() {
var cursor = req.result;
callback(cursor ? cursor.key : null);
});
}
return {
getItem: getItem,
setItem: setItem,
removeItem: removeItem,
clear: clear,
length: length,
key: key
};
}());
define("shared/js/async_storage", (function (global) {
return function () {
var ret, fn;
return ret || global.asyncStorage;
};
}(this)));
define('constants',['require','exports','module'],function(require, exports) {
'use strict';
// ---------------------------------------------------------
// Constants
// Random day that happens to be Sunday
exports.KNOWN_SUNDAY = new Date(2015, 8, 20);
});
define('utils',['require','constants'],function(require) {
'use strict';
/* global mozIntl, IntlHelper */
var constants = require('constants');
var Utils = {};
// Maintain references to millisecond multipliers
var dateMultipliers = {
days: 1000 * 60 * 60 * 24,
hours: 1000 * 60 * 60,
minutes: 1000 * 60,
seconds: 1000,
milliseconds: 1
};
var units = Object.keys(dateMultipliers);
IntlHelper.define('weekday-short', 'datetime', {
weekday: 'short'
});
IntlHelper.define('time-html', 'mozdatetime', {
dayperiod: true,
hour: 'numeric',
minute: 'numeric',
});
IntlHelper.define('time-text', 'datetime', {
hour: 'numeric',
minute: 'numeric',
});
/**
* Define a singleton method that returns a unified instance
* based on arguments.
*
* @param {function} constructor - A constructor function used
* to create a new instance.
* @param {function} [getKey] - A function called with (arguments),
* and returns a lookup key for this singleton.
* @return {object} - returns the instance either created or retrieved
* from the singleton-map by the key.
*/
Utils.singleton = function(constructor, getKey) {
var singletonMap = new Map();
return function() {
var arglist = Array.prototype.slice.call(arguments);
var key = (typeof getKey === 'function') ? getKey(arglist) : constructor;
var instance = singletonMap.get(key);
if (!instance) {
instance = Object.create(constructor.prototype);
constructor.apply(instance, arglist);
singletonMap.set(key, instance);
}
return instance;
};
};
Utils.memoizedDomPropertyDescriptor = function(selector) {
var memoizedValue = null;
return {
get: function() {
if (memoizedValue === null) {
memoizedValue = document.querySelector(selector);
}
return memoizedValue;
},
set: function(value) {
memoizedValue = value;
}
};
};
/**
* Extend the given prototype object with lazy getters.
* selectorMap is a mapping of { propertyName: selector }.
*/
Utils.extendWithDomGetters = function(proto, selectorMap) {
for (var property in selectorMap) {
Object.defineProperty(proto, property,
Utils.memoizedDomPropertyDescriptor(selectorMap[property]));
}
return proto;
};
Utils.dateMath = {
/**
* Convert object literals containing interval length to milliseconds
*
* @param {Object|Date|Number} interval An object literal containing days,
* hours, minutes etc.
* Optionally a number or date object.
* @param {Object} opts Options object with a unitsPartial property containing
* (if desired) a restriction on which properties will be
* searched on the interval.
* @return {Number} Millisecond value for interval length.
*/
toMS: function(interval, opts) {
var converted, sign, unitsPartial;
// if a millisecond interval or a Date is passed in, return that
if (interval instanceof Date || typeof interval === 'number') {
return +interval;
}
opts = opts || {};
unitsPartial = opts.unitsPartial || units;
// Allow for 'hours' or 'hour'
unitsPartial = unitsPartial.map(function(unit) {
// String.prototype.endsWith is available in FF17+
return unit.endsWith('s') ? unit : unit.concat('s');
});
// some will exit early when it returns a truthy value
sign = unitsPartial.some(function(unit) {
return interval[unit] < 0;
});
// Using as a multiplier later
sign = sign ? -1 : 1;
// collect passed in units and multiply by their millisecond/unit count
converted = unitsPartial.map(function(unit) {
var partial;
// we're storing the sign out of the iterator
partial = Math.abs(interval[unit]);
// A missing property and 0 should be treated the same
return partial ? partial * dateMultipliers[unit] : 0;
});
// add up each millisecond-converted term and multiply total by sign
return sign * converted.reduce(function(a, b) { return a + b; });
},
/**
* Convert millisecond values to object literals conformable to toMS()
*
* @param {Number} interval A millisecond value.
* @param {Object} [opts] Options object with a unitsPartial property
* containing (if desired) a restriction on which
* properties will be reated for the return value.
* @return {Object} Object literal with properties as deliniated by opts.
*/
fromMS: function(interval, opts) {
var times, sign, unitsPartial;
opts = opts || {};
unitsPartial = opts.unitsPartial || units;
// Allow for 'hours' or 'hour'
unitsPartial = unitsPartial.map(function(unit) {
// String.prototype.endsWith is available in FF17+
return unit.endsWith('s') ? unit : unit.concat('s');
});
// For negative intervals (time previous to now)
// update interval to absolute value and store the sign
// to apply to all units
if (interval < 0) {
sign = -1;
interval = Math.abs(interval);
} else {
sign = 1;
}
// divide the time interval by the highest millisecond multiplier
// store the truncated result and subtract that from the interval
// update the interval to be the remainder
times = unitsPartial.map(function(unit, index) {
var truncated, mult;
mult = dateMultipliers[unit];
truncated = Math.floor(interval / mult);
interval = interval - (truncated * mult);
// units are either all positive or negative
// only iterate to needed specificity
return sign * truncated;
});
// Populate the returned object using units as property names
// and times for values
return times.reduce(function(out, unitTime, index) {
out[unitsPartial[index]] = unitTime;
return out;
}, {});
}
};
Utils.extend = function(initialObject, extensions) {
// extend({}, a, b, c ... d) -> {...}
// rightmost properties (on 'd') take precedence
extensions = Array.prototype.slice.call(arguments, 1);
for (var i = 0; i < extensions.length; i++) {
var extender = extensions[i];
for (var prop in extender) {
var descriptor = Object.getOwnPropertyDescriptor(extender, prop);
if (descriptor && descriptor.value !== undefined) {
initialObject[prop] = extender[prop];
}
}
}
return initialObject;
};
Utils.escapeHTML = function(str, escapeQuotes) {
var span = document.createElement('span');
span.textContent = str;
if (escapeQuotes) {
return span.innerHTML.replace(/"/g, '&quot;').replace(/'/g, '&#x27;');
}
return span.innerHTML;
};
Utils.getLocalizedTimeHtml = function(date) {
var f = IntlHelper.get('time-html');
return f.format(date, {
dayperiod: '<small>$&</small>'
});
};
Utils.getLocalizedTimeText = function(date) {
var f = IntlHelper.get('time-text');
return f.format(date);
};
Utils.changeSelectByValue = function(selectElement, value) {
var options = selectElement.options;
for (var i = 0; i < options.length; i++) {
if (options[i].value == value) {
if (selectElement.selectedIndex != i) {
selectElement.selectedIndex = i;
}
break;
}
}
};
Utils.getSelectedValueByIndex = function(selectElement) {
return selectElement.options[selectElement.selectedIndex].value;
};
var wakeTarget = {
requests: {
cpu: new Map(), screen: new Map(), wifi: new Map()
},
locks: {
cpu: null, screen: null, wifi: null
},
timeouts: {
cpu: null, screen: null, wifi: null
}
};
function getLongestLock(type) {
var max = 0;
for (var i of wakeTarget.requests[type]) {
var request = i[1];
if (request.time > max) {
max = request.time;
}
}
return {
time: max,
lock: wakeTarget.locks[type],
timeout: wakeTarget.timeouts[type]
};
}
Utils.safeWakeLock = function(opts, fn) {
/*
* safeWakeLock
*
* Create a Wake lock that is automatically released after
* timeoutMs. Locks are reentrant, and have no meaningful mutual
* exclusion behavior.
*
* @param {Object} options - an object containing
* [type] {string} a string passed to requestWakeLock
* default = 'cpu'. This string can be any
* resource exposed by the environment that
* this application was designed to run in.
* Gaia exposes three of them: 'cpu',
* 'screen', and 'wifi'. Certified apps may
* expose more.
* timeoutMs {number} number of milliseconds to hold
* the lock.
* @param {Function} callback - a function to be called after all other
* generated callbacks have been called.
* function ([err]) -> undefined.
*/
opts = opts || {};
var type = opts.type || 'cpu';
var timeoutMs = opts.timeoutMs | 0;
var now = Date.now();
var myKey = {};
wakeTarget.requests[type].set(myKey, {
time: now + timeoutMs
});
var max = getLongestLock(type);
var unlockFn = function() {
if (!myKey) {
return;
}
wakeTarget.requests[type]. delete(myKey);
var now = Date.now();
var max = getLongestLock(type);
if (max.time > now) {
clearTimeout(wakeTarget.timeouts[type]);
wakeTarget.timeouts[type] = setTimeout(unlockFn, max.time - now);
} else {
if (wakeTarget.locks[type]) {
wakeTarget.locks[type].unlock();
}
wakeTarget.locks[type] = null;
clearTimeout(wakeTarget.timeouts[type]);
wakeTarget.timeouts[type] = null;
}
myKey = null;
};
clearTimeout(wakeTarget.timeouts[type]);
wakeTarget.timeouts[type] = setTimeout(unlockFn, max.time - now);
try {
if (!wakeTarget.locks[type] && max.time > now) {
wakeTarget.locks[type] = navigator.requestWakeLock(type);
}
fn(unlockFn);
} catch (err) {
unlockFn();
throw err;
}
};
Utils.async = {
generator: function(latchCallback) {
/*
* Generator
*
* Create an async generator. Each time the generator is
* called, it will return a new callback. When all issued
* callbacks have been called, the latchCallback is called.
*
* If any of the callbacks are called with and error as
* the first argument, the latchCallback will be called
* immediately with that error.
*
* @latchCallback {Function} a function to be called after
* all other generated callbacks have been
* called
* function ([err]) -> undefined
*/
var tracker = new Map();
var issuedCallbackCount = 0;
var disabled = false;
var testFn = function(err) {
var trackerSize;
if (!disabled) {
// FF18 defines size to be a method, so we need to test here:
// Remove with FF18 support
if (typeof tracker.size === 'function') {
trackerSize = tracker.size();
} else {
trackerSize = tracker.size;
}
if (err || trackerSize === issuedCallbackCount) {
disabled = true;
latchCallback && latchCallback(err);
}
}
};
return function() {
return (function() {
var i = issuedCallbackCount++;
return function(err) {
tracker.set(i, true);
testFn(err);
};
})();
};
},
namedParallel: function(names, latchCallback) {
/*
* namedParallel
*
* Create an async namedParallel.
*
* The return value is an object containing the parameters
* specified in the names array. Each parameter is set to
* a callback. When all callbacks have been called, latchCallback
* is called.
*
* If any named callback is called with an error as the first
* parameter, latchCallback is immediately called with that
* error. Future calls to callbacks are then no-ops.
*
* @names {List<String>} - A list of strings to be used as
* parameter names for callbacks on the returned object.
*/
var generator = Utils.async.generator(latchCallback);
var done = generator();
var ret = {};
for (var i = 0; i < names.length; i++) {
ret[names[i]] = generator();
}
done();
return ret;
}
};
Utils.data = {
defaultCompare: function ud_defaultCompare(a, b) {
if (typeof a === 'number' && typeof b === 'number') {
var diff = a - b;
return diff !== 0 ? diff / Math.abs(diff) : diff;
} else if ((typeof a === 'string' || a instanceof String) &&
(typeof b === 'string' || b instanceof String)) {
return (a < b) ? -1 : ((a > b) ? 1 : 0);
} else if (Array.isArray(a) && Array.isArray(b)) {
var commonLength = Math.min(a.length, b.length);
for (var i = 0; i < commonLength; i++) {
var compareResult = Utils.data.defaultCompare(a[i], b[i]);
if (compareResult !== 0) {
return compareResult;
}
}
return b.length - a.length;
} else {
throw new Error('Cannot compare ' + JSON.stringify([a, b]));
}
},
keyedCompare: function ud_keyedCompare(key) {
return function internal_keyedCompare(a, b) {
return Utils.data.defaultCompare(a[key], b[key]);
};
},
binarySearch: function ud_binarySearch(key, list, compare) {
compare = compare || Utils.data.defaultCompare;
var botI = 0;
var topI = list.length;
var midI, comp, value;
if (list.length > 0) {
do {
midI = botI + ((topI - botI) / 2) | 0;
value = list[midI];
comp = compare(value, key);
if (comp < 0) {
botI = midI + 1;
} else if (comp > 0) {
topI = midI - 1;
}
} while (comp !== 0 && botI < topI);
}
midI = comp === 0 ? midI : topI;
value = list[midI];
if (comp === 0 || value && compare(value, key) === 0) {
return { match: true, index: midI, value: value };
} else {
var index;
if (0 > midI) {
index = 0;
} else if (midI >= list.length) {
index = list.length;
} else {
index = midI + (compare(value, key) < 0 ? 1 : 0);
}
return {
match: false,
index: index
};
}
},
sortedInsert: function ud_sortedInsert(item, list, compare, unique) {
compare = compare || Utils.data.defaultCompare;
var bs = Utils.data.binarySearch(item, list, compare);
var inserted = !bs.match || !unique;
if (inserted) {
list.splice(bs.index, 0, item);
}
return (inserted) ? bs.index : null;
},
sortedRemove: function ud_sortedRemove(item, list, compare, multiple) {
compare = compare || Utils.data.defaultCompare;
var removed = false;
if (!multiple) {
var bs = Utils.data.binarySearch(item, list, compare);
if (bs.match) {
list.splice(bs.index, 1);
removed = true;
}
} else {
var biasedCompare = function(target, result, ic) {
return function(a, b) {
if (ic(a, target) === 0) {
return result;
} else {
return ic(a, b);
}
};
};
var leftBound = Utils.data.binarySearch(item, list,
biasedCompare(item, 1, compare));
var rightBound = Utils.data.binarySearch(item, list,
biasedCompare(item, -1, compare));
if (leftBound.index < rightBound.index) {
list.splice(leftBound.index, rightBound.index - leftBound.index);
removed = true;
}
}
return removed;
}
};
Utils.addEventListenerOnce = function(element, type, fn, useCapture) {
var handler = function(evt) {
element.removeEventListener(type, handler, useCapture);
fn(evt);
};
element.addEventListener(type, handler, useCapture);
};
Utils.summarizeDaysOfWeek = function(repeat) {
var days = [];
if (repeat) {
for (var day in repeat) {
if (repeat[day]) {
days.push(day);
}
}
}
if (days.length === 7) {
return Promise.resolve('everyday');
} else if (days.length === 5 &&
days.indexOf('6') === -1 && // Saturday
days.indexOf('0') === -1) { // Sunday
return Promise.resolve('weekdays');
} else if (days.length === 2 &&
days.indexOf('6') !== -1 && // Saturday
days.indexOf('0') !== -1) { // Sunday
return Promise.resolve('weekends');
} else if (days.length === 0) {
return Promise.resolve('never');
} else {
var formatter = IntlHelper.get('weekday-short');
var repeatStrings = [];
for (var i = 0; i < 7; i++) {
if (days.indexOf(i.toString()) !== -1) {
var dayDate = new Date(constants.KNOWN_SUNDAY);
dayDate.setDate(constants.KNOWN_SUNDAY.getDate() + i);
repeatStrings.push(formatter.format(dayDate));
}
}
return mozIntl.formatList(repeatStrings).then(listString => {
return { raw: listString };
});
}
};
return Utils;
});
define('timer',['require','shared/js/async_storage','utils'],function(require) {
'use strict';
var asyncStorage = require('shared/js/async_storage');
var Utils = require('utils');
var timerPrivate = new WeakMap();
/**
* Timer
*
* Create new or revive existing timer objects.
*
* @param {Object} opts Optional timer object to create or revive
* a new or existing timer object.
* - startTime, number time in ms.
* - duration, time to count from `start`.
* - configuredDuration, time requested by user.
* - sound, string sound name.
* - vibrate, boolean, vibrate or not.
* - id, integer, mozAlarm API id number.
*/
function Timer(opts) {
opts = opts || {};
var now = Date.now();
if (opts.id !== undefined) {
delete opts.id;
}
// private properties
timerPrivate.set(this, Utils.extend({
state: Timer.INITIAL
}, extractProtected(opts)));
// public properties
Utils.extend(this, {
startTime: now,
duration: null,
configuredDuration: null,
sound: 'ac_awake.opus',
vibrate: true
}, opts);
}
Timer.prototype.constructor = Timer;
/**
* request - get the persisted Timer object.
*
* @param {function} [callback] - called with (err, timer_raw).
*/
Timer.getFromStorage = function(callback) {
asyncStorage.getItem('active_timer', function(timer) {
if (timer) {
// Normalize the timer data. Pre-April-2014 code may have stored
// 'vibrate' and 'sound' as the string "0".
timer.sound = (timer.sound !== '0' ? timer.sound : null);
timer.vibrate = (timer.vibrate && timer.vibrate !== '0');
}
callback && callback(timer || null);
});
};
/**
* singleton - get the unique persisted Timer object.
*
* @param {function} [callback] - called with (err, timer).
*/
var timerSingleton = Utils.singleton(Timer);
Timer.singleton = function tm_singleton(callback) {
Timer.getFromStorage(function(err, obj) {
var ts = timerSingleton(obj);
callback && callback(null, ts);
});
};
function extractProtected(config) {
var ret = {};
var protectedProperties = new Set(['state']);
for (var i in config) {
if (protectedProperties.has(i)) {
ret[i] = config[i];
delete config[i];
}
}
return ret;
}
/**
* toSerializable - convert `this` to a serialized format.
*
* @return {object} - object representation of this Timer.
*/
Timer.prototype.toSerializable = function timerToSerializable() {
var timer = Utils.extend({}, this, timerPrivate.get(this));
// Normalize the data. TODO: Perform this normalization immediately
// at the getter/setter level when this class is refactored.
return {
startTime: timer.startTime,
duration: timer.duration,
configuredDuration: timer.configuredDuration,
sound: (timer.sound !== '0' ? timer.sound : null),
vibrate: (timer.vibrate !== '0' ? timer.vibrate : null),
state: timer.state
};
};
/**
* save - Save the timer to the database.
*
* @param {function} [callback] - callback to call after the timer
* has been saved.
*/
Timer.prototype.save = function timerSave(callback) {
asyncStorage.setItem('active_timer', this.toSerializable(), function() {
callback && callback(null, this);
}.bind(this));
};
/**
* register - Register the timer with mozAlarm API.
*
* @param {function} [callback] - callback to call after the timer
* has been registered.
*/
Timer.prototype.register = function timerRegister(callback) {
var data = {
type: 'timer'
};
var request;
// Remove previously-created mozAlarm for this alarm, if necessary.
this.unregister();
request = navigator.mozAlarms.add(
new Date(Date.now() + this.remaining), 'ignoreTimezone', data
);
request.onsuccess = (function(ev) {
this.id = ev.target.result;
callback && callback(null, this);
}.bind(this));
request.onerror = function(ev) {
callback && callback(ev.target.error);
};
};
/**
* commit - save and register the timer as necessary.
*
* @param {function} [callback] - callback to call after the timer
* has been registered.
*/
Timer.prototype.commit = function timerCommit(callback) {
var saveSelf = this.save.bind(this, callback);
if (this.state === Timer.STARTED) {
this.register(saveSelf);
} else {
this.unregister();
saveSelf();
}
};
Timer.prototype.unregister = function timerUnregister() {
if (typeof this.id === 'number') {
navigator.mozAlarms.remove(this.id);
}
};
Object.defineProperty(Timer.prototype, 'remaining', {
get: function() {
if (this.state === Timer.INITIAL) {
return this.configuredDuration;
} else if (this.state === Timer.PAUSED) {
return this.duration;
} else if (this.state === Timer.STARTED) {
if (typeof this.startTime === 'undefined' ||
typeof this.duration === 'undefined') {
return 0;
}
var r = (this.startTime + this.duration) - Date.now();
return r >= 0 ? r : 0;
}
}
});
Object.defineProperty(Timer.prototype, 'state', {
get: function() {
var priv = timerPrivate.get(this);
return priv.state;
}
});
Timer.prototype.start = function timerStart() {
if (this.state !== Timer.STARTED) {
var priv = timerPrivate.get(this);
priv.state = Timer.STARTED;
this.startTime = Date.now();
this.duration = (typeof this.duration === 'number') ? this.duration :
this.configuredDuration;
window.dispatchEvent(new CustomEvent('timer-start', {
detail: { remaining: this.remaining }
}));
this._tick();
}
};
Timer.prototype.pause = function timerPause() {
if (this.state === Timer.STARTED) {
this.duration = this.remaining; // remaining getter observes private state
var priv = timerPrivate.get(this);
priv.state = Timer.PAUSED;
this.startTime = null;
this._cancelTick();
window.dispatchEvent(new CustomEvent('timer-pause', {
detail: { remaining: this.remaining }
}));
}
};
Timer.prototype.cancel = function timerReset() {
if (this.state !== Timer.INITIAL) {
var priv = timerPrivate.get(this);
priv.state = Timer.INITIAL;
this.startTime = null;
this.duration = this.configuredDuration;
this._cancelTick();
this._dispatchEnd();
}
};
/**
* plus Increase the duration and extend the endAt time
*
* @param {Number} seconds The time in seconds to add.
*
* @return {Timer} Timer instance.
*/
Timer.prototype.plus = function timerPlus(seconds) {
// Convert to ms
var ms = seconds * 1000;
this.duration += ms;
return this;
};
Timer.prototype._dispatchEnd = function timerDispatchEnd() {
// cancel and end are basically the same thing
window.dispatchEvent(new CustomEvent('timer-end', {
detail: { remaining: 0 }
}));
};
Timer.prototype._tick = function timerTick() {
if (this.state !== Timer.STARTED) {
return;
}
// we cache the previous value posted to avoid triggering multiple "tick"
// messages for the same second
var remaining = this.remaining;
var current = Math.round(remaining / 1000);
if (current !== this._previousTick) {
this._previousTick = current;
window.dispatchEvent(new CustomEvent('timer-tick', {
detail: { remaining: remaining }
}));
}
if (remaining > 0) {
// use setTimeout instead of requestAnimationFrame because window will
// probably be hidden when handling connections
this._tickTimeout = window.setTimeout(() => this._tick(), 100);
} else {
this._dispatchEnd();
}
};
Timer.prototype._cancelTick = function timerCancelTick() {
window.clearTimeout(this._tickTimeout);
};
/**
* Static "const" Timer states.
*/
Object.defineProperties(Timer, {
INITIAL: { value: 0 },
STARTED: { value: 1 },
PAUSED: { value: 2 }
});
return Timer;
});
/**
* parse-duration v0.1.1
* Released under the MIT License
* https://github.com/jkroso/parse-duration
*/
define('ext/parse_duration',['require','exports','module'],function(require, exports, module) {
var duration = /(-?\d*\.?\d+(?:e[-+]?\d+)?)\s*([a-zμ]*)/ig
module.exports = parse
/**
* conversion ratios
*/
parse.nanosecond =
parse.ns = 1 / 1e6
parse.μs =
parse.microsecond = 1 / 1e3
parse.millisecond =
parse.ms = 1
parse.second =
parse.sec =
parse.s = parse.ms * 1000
parse.minute =
parse.min =
parse.m = parse.s * 60
parse.hour =
parse.hr =
parse.h = parse.m * 60
parse.day =
parse.d = parse.h * 24
parse.week =
parse.wk =
parse.w = parse.d * 7
parse.month = parse.d * (365.25 / 12)
parse.year =
parse.yr =
parse.y = parse.d * 365.25
/**
* convert `str` to ms
*
* @param {String} str
* @return {Number}
*/
function parse(str){
var result = 0
// ignore commas
str = str.replace(/(\d),(\d)/g, '$1$2')
str.replace(duration, function(_, n, units){
units = parse[units]
|| parse[units.toLowerCase().replace(/s$/, '')]
|| 1
result += parseFloat(n, 10) * units
})
return result
}
});
define('connection/timer',['require','exports','module','timer','ext/parse_duration'],function(require, exports) {
'use strict';
var Timer = require('timer');
var parseDuration = require('ext/parse_duration');
var port;
var timer;
exports.onstart = function(request) {
port = request.port;
window.addEventListener('timer-start', onTimerEvent);
window.addEventListener('timer-pause', onTimerEvent);
window.addEventListener('timer-end', onTimerEvent);
window.addEventListener('timer-tick', onTimerEvent);
};
exports.onclose = function() {
port = null;
timer = null;
window.removeEventListener('timer-start', onTimerEvent);
window.removeEventListener('timer-pause', onTimerEvent);
window.removeEventListener('timer-end', onTimerEvent);
window.removeEventListener('timer-tick', onTimerEvent);
};
function onTimerEvent(evt) {
if (!port) {
return;
}
port.postMessage({
type: evt.type,
remaining: evt.detail.remaining
});
}
exports.onmessage = function(event) {
if (timer) {
exec(event);
} else {
Timer.singleton((err, t) => {
timer = t;
exec(event);
});
}
};
function exec(event) {
var { type, duration } = event.data;
if (type === 'create') {
create(duration);
return;
}
// type: [start, pause, cancel]
timer[type]();
timer.commit();
}
function create(inputDuration) {
var duration = parseDuration(String(inputDuration));
if (!duration) {
port.postMessage({
error: `Invalid timer duration "${inputDuration}"`
});
return;
}
// we don't check the timer state since we assume the user wants to override
// any running timer and start it immediately
timer.duration = duration;
timer.start();
timer.commit();
}
});
define('connection/handler',['require','exports','module','./alarm','./timer'],function(require, exports) {
'use strict';
var handlers = {
'gaia_alarm': require('./alarm'),
'gaia_timer': require('./timer')
};
exports.init = function() {
navigator.mozSetMessageHandler('connection', req => {
var port = req.port;
var handler = handlers[req.keyword];
if (!handler) {
console.error(`can't find handler for connection "${req.keyword}"`);
return;
}
// IACMessagePort don't support onstart & onclose events as of 2015-07-21
// but for timer we do need to emulate this behavior
if (handler.onstart) {
handler.onstart(req);
}
if (handler.onmessage || handler.onclose) {
port.onmessage = event => {
if (event.data.type === 'close' && handler.onclose) {
handler.onclose(req);
return;
}
handler.onmessage && handler.onmessage(event, port);
};
}
});
};
});
define('app',['require','tabs','view','connection/handler'],function(require) {
'use strict';
/* global IntlHelper */
var Tabs = require('tabs');
var View = require('view');
var connectionHandler = require('connection/handler');
/**
* Global Application event handling and paging
*/
var App = {
/**
* Load the Tabs and Panels, attach events and navigate to the default view.
*/
init: function() {
connectionHandler.init();
IntlHelper.define('digit-nopadding', 'number', {
style: 'decimal',
useGrouping: false
});
this.tabs = new Tabs(document.getElementById('clock-tabs'));
window.addEventListener('hashchange', this);
window.addEventListener('visibilitychange', this);
// Tell audio channel manager that we want to adjust the alarm channel
// if the user press the volumeup/volumedown buttons in Clock.
if (navigator.mozAudioChannelManager) {
navigator.mozAudioChannelManager.volumeControlChannel = 'alarm';
}
this.visible = !document.hidden;
this.panels = Array.prototype.map.call(
document.querySelectorAll('[data-panel-id]'),
function(element) {
var panel = {
el: element,
fragment: element.dataset.panelId.replace('_', '-') + '-panel',
instance: null
};
return panel;
}.bind(this)
);
window.performance.mark('navigationLoaded');
this.navigate({ hash: '#alarm-panel' }, function() {
// Dispatch an event to mark when we've finished loading.
// At this point, the navigation is usable, and the primary
// alarm list tab has begun loading.
window.performance.mark('navigationInteractive');
}.bind(this));
return this;
},
/**
* Load and instantiate the specified panel (when necessary).
*
* @param {Object} panel - An object describing the panel. It must contain
* either an `el` attribute (defining the panel's
* containing element) or an `instance` attribute
* (defining the instantiated Panel itself).
* @param {Function} [callback] - A function that will be invoked with the
* instantiated panel once it is loaded.
*/
loadPanel: function(panel, callback) {
if (panel.instance) {
callback && setTimeout(callback, 0, panel);
return;
}
var moduleId = 'panels/' + panel.el.dataset.panelId + '/main';
require([moduleId], function(PanelModule) {
panel.instance = View.instance(panel.el, PanelModule);
callback && callback(panel);
});
},
alarmListLoaded: function() {
// Performance testing markers. See <https://bugzil.la/996038>.
// At this point, the alarm list has been loaded, and all facets
// of Clock are now interactive. The other panels are lazily
// loaded when the user switches tabs.
window.performance.mark('visuallyLoaded');
window.performance.mark('contentInteractive');
window.performance.mark('fullyLoaded');
},
/**
* split each event handler into it's own method
*/
handleEvent: function(event) {
var handler = this['on' + event.type];
if (handler) {
return handler.apply(this, arguments);
}
},
/**
* navigate between pages.
*
* @param {object} data Options for navigation.
* @param {string} data.hash The hash of the panel id. I.E. '#alarm-panel'.
* @param {function} callback Callback to invoke when done.
*/
navigate: function(data, callback) {
var currentIndex = this.panels.indexOf(this.currentPanel);
this.panels.forEach(function(panel, panelIndex) {
if ('#' + panel.fragment === data.hash) {
this.loadPanel(panel, function() {
var instance = panel.instance;
instance.navData = data.data || null;
instance.active = true;
instance.visible = true;
if (currentIndex !== -1 && currentIndex !== panelIndex) {
var direction = currentIndex < panelIndex;
requestAnimationFrame(function startAnimation(oldPanel) {
instance.transition =
direction ? 'slide-in-right' : 'slide-in-left';
oldPanel.instance.transition =
direction ? 'slide-out-left' : 'slide-out-right';
}.bind(null, this.currentPanel));
}
this.currentPanel = panel;
callback && callback();
}.bind(this));
} else {
if (panel.instance) {
panel.instance.active = false;
}
}
}, this);
this.currentHash = data.hash;
},
/**
* Navigate to the new hash.
*/
onhashchange: function(event) {
if (this.currentHash === location.hash) {
return;
}
this.navigate({ hash: location.hash });
},
/**
* Whenever the application gains/loses focus, inform the current panel of
* its visibility loss.
*/
onvisibilitychange: function(event) {
this.visible = !document.hidden;
if (this.currentPanel) {
this.currentPanel.visible = this.visible;
}
}
};
return App;
});
define('startup_init', ['require','app'],function(require) {
var App = require('app');
App.init();
});
require(['require_config'], function() {
requirejs(['startup_init']);
});
define("startup", function(){});
define('panel',['require','view'],function(require) {
'use strict';
var View = require('view');
var priv = new WeakMap();
/**
* A Panel is a "full screen" style tab/dialog. Panels have an active state
* which can be true or false, and can transition in and out using CSS3
* classes and animation events.
*
* @constructor
* @param {HTMLElement} element The element to wrap.
*/
function Panel(element) {
View.apply(this, arguments);
priv.set(this, {
active: element.classList.contains('active'),
transition: false
});
element.addEventListener('animationend', this);
}
Panel.prototype = Object.create(View.prototype);
/**
* Handles the "animationend" event. Sets the transition state to false
* and hides the element if the Panel is not active.
*/
Panel.prototype.handleEvent = function(event) {
if (event.target !== this.element) {
return;
}
// remove visibility if transition finishes on non-active view
if (!this.active) {
this.visible = false;
}
this.transition = false;
};
Object.defineProperties(Panel.prototype, {
/**
* Panel.prototype.active - Boolean
*
* Sets the internal active state, and adds or removes the "active"
* class on the element.
*/
active: {
get: function() {
return priv.get(this).active;
},
set: function(value) {
var state = priv.get(this);
value = !!value;
if (state.active !== value) {
state.active = value;
if (value) {
this.element.classList.add('active');
} else {
this.pendingVisible = true;
this.element.classList.remove('active');
}
}
return value;
}
},
/**
* Panel.prototype.transition - String or false
*
* Sets the internal transition state. When set, adds the class specified
* to the element, removing the old transition class if it exists.
*
* When set to false, it removes the current transition class.
*/
transition: {
get: function() {
return priv.get(this).transition;
},
set: function(value) {
var state = priv.get(this);
if (value) {
if (state.transition) {
this.element.classList.remove(state.transition);
}
this.element.classList.add(value);
} else if (state.transition) {
this.element.classList.remove(state.transition);
}
state.transition = value;
return value;
}
}
});
return Panel;
});
define('panels/alarm/clock_view',['require','shared/js/async_storage','utils'],function(require) {
'use strict';
/* global IntlHelper */
var asyncStorage = require('shared/js/async_storage');
var Utils = require('utils');
var SETTINGS_CLOCKMODE = 'settings_clockoptions_mode';
var viewMode = null;
IntlHelper.define('date-long', 'mozdatetime', {
weekday: 'long',
month: 'long',
day: 'numeric'
});
// Retrieve stored view mode data as early as possible.
asyncStorage.getItem(SETTINGS_CLOCKMODE, function(value) {
// If no value has been stored, don't update
// the viewMode closure.
if (value === null) {
return;
}
// If the ClockView hasn't initialized yet,
// and the stored value is different from
// the arbitrarily chosen default view (analog)
// then update the viewMode closure.
if (!ClockView.isInitialized && viewMode !== value) {
viewMode = value;
}
});
var ClockView = {
get mode() {
// Closure value, stored in settings,
// or the default (analog)
return viewMode;
},
set mode(value) {
// If the `mode` is being updated to a new value:
//
// - Update the viewMode closure
// - Store the new value in persistent data storage
//
// Always return `value`
if (viewMode !== value) {
viewMode = value;
asyncStorage.setItem(
SETTINGS_CLOCKMODE, value
);
}
return viewMode;
},
timeouts: {
analog: null,
dayDate: null,
digital: null
},
get digital() {
delete this.digital;
return (this.digital = document.getElementById('digital-clock'));
},
get analog() {
delete this.analog;
return (this.analog = document.getElementById('analog-clock'));
},
get time() {
delete this.time;
return (this.time = document.getElementById('clock-time'));
},
get dayDate() {
delete this.dayDate;
return (this.dayDate = document.getElementById('clock-day-date'));
},
get container() {
delete this.container;
return (this.container =
document.getElementById('analog-clock-container'));
},
isInitialized: false,
init: function cv_init() {
var handler = this.handleEvent.bind(this);
document.addEventListener('visibilitychange', handler);
this.analog.addEventListener('click', handler, false);
this.digital.addEventListener('click', handler, false);
window.addEventListener('alarm-list-changed',
this.resizeAnalogClock.bind(this));
this.hands = {};
['second', 'minute', 'hour'].forEach(function(hand) {
this.hands[hand] = document.getElementById(hand + 'hand');
}, this);
// Kick off the day date display (upper left string)
this.updateDayDate();
// This will get fired on language change
IntlHelper.observe('date-long', this.updateDayDate.bind(this));
// Those two will get fired on language and timeformat changes
IntlHelper.observe('time-html', this.updateDigitalClock.bind(this));
IntlHelper.observe('time-text', this.updateAnalogClock.bind(this));
// If the attempt to request and set the viewMode
// closure early has failed to respond before the
// call to ClockView.init(), make an async request,
// passing the response value as an argument to this.show()
if (this.mode === null) {
asyncStorage.getItem(
SETTINGS_CLOCKMODE, this.show.bind(this)
);
} else {
// Display the clock face
this.show();
}
this.isInitialized = true;
},
updateDayDate: function cv_updateDayDate() {
var d = new Date();
var f = IntlHelper.get('date-long');
var remainMillisecond = (24 - d.getHours()) * 3600 * 1000 -
d.getMinutes() * 60 * 1000 -
d.getMilliseconds();
// If the date of the month is part of the locale format as a
// number, insert bold tags to accentuate the number itself.
var dateString = f.format(d, {
day: '<b>$&</b>'
});
this.dayDate.innerHTML = dateString;
this.timeouts.dayDate = setTimeout(
this.updateDayDate.bind(this), remainMillisecond
);
},
update: function cv_update(opts) {
opts = opts || {};
if (this.mode === 'digital') {
this.updateDigitalClock(opts);
} else {
this.updateAnalogClock(opts);
}
},
updateDigitalClock: function cv_updateDigitalClock(opts) {
opts = opts || {};
// this function may be called manually, reset the timer
clearTimeout(this.timeouts.digital);
var d = new Date();
this.time.innerHTML = Utils.getLocalizedTimeHtml(d);
this.timeouts.digital = setTimeout(
this.updateDigitalClock.bind(this), (60 - d.getSeconds()) * 1000
);
},
updateAnalogClock: function cv_updateAnalogClock(opts) {
opts = opts || {};
// this function may be called manually, reset the timer
clearTimeout(this.timeouts.analog);
if (opts.needsResize) {
this.resizeAnalogClock();
}
var now = new Date();
var sec, min, hour;
sec = now.getSeconds();
min = now.getMinutes();
// hours progress gradually
hour = (now.getHours() % 12) + min / 60;
this.setTransform('second', sec);
this.setTransform('minute', min);
this.setTransform('hour', hour);
// Update aria label for analog view.
this.container.setAttribute('aria-label', Utils.getLocalizedTimeText(now));
// update again in one second
this.timeouts.analog = setTimeout(
this.updateAnalogClock.bind(this), 1000 - now.getMilliseconds()
);
},
setTransform: function cv_setTransform(id, angle) {
var hand = this.hands[id];
// return correct angle for different hands
function conv(timeFrag) {
var mult;
// generate a conformable number to rotate about
// 30 degrees per hour 6 per second and minute
mult = id === 'hour' ? 30 : 6;
// we generate the angle from the fractional sec/min/hour
return (timeFrag * mult);
}
// Use transform rotate on the rect itself vs on a child element
// avoids unexpected behavior if either dur and fill are set to defaults
// Use translateZ to force it on its own layer, which will invoke the GPU
// and thus do the minimum amount of work required (reduces power usage)
hand.style.transform = 'rotate(' + conv(angle) + 'deg) translateZ(1px)';
},
handleEvent: function cv_handleEvent(event) {
var newMode, target;
switch (event.type) {
case 'visibilitychange':
if (document.hidden) {
if (this.timeouts.dayDate) {
clearTimeout(this.timeouts.dayDate);
}
if (this.timeouts.digital) {
clearTimeout(this.timeouts.digital);
}
if (this.timeouts.analog) {
clearTimeout(this.timeouts.analog);
}
return;
} else if (!document.hidden) {
// Refresh the view when app return to foreground.
this.updateDayDate();
this.update();
}
break;
case 'click':
target = event.target;
if (!target) {
return;
}
if (this.digital.contains(target) ||
target.id === 'digital-clock') {
newMode = 'analog';
}
if (this.analog.contains(target) ||
target.id === 'analog-clock') {
newMode = 'digital';
}
if (newMode) {
this.show(newMode);
}
break;
}
},
calAnalogClockType: function cv_calAnalogClockType(count) {
var type = 'small';
if (count < 2) {
type = 'large';
} else if (count === 2) {
type = 'medium';
}
return type;
},
getAlarmCount: function() {
return document.querySelectorAll('#alarms .alarm-item').length;
},
resizeAnalogClock: function cv_resizeAnalogClock() {
var type = this.calAnalogClockType(this.getAlarmCount());
this.container.className = type;
document.getElementById('alarms').className = 'count' + type;
},
show: function cv_show(mode) {
var isAnalog = false;
var previous, hiding, showing;
if (location.hash !== '#alarm-panel') {
location.hash = '#alarm-panel';
}
// The clock display mode is either
//
// - Explicitly passed as the mode param
// - Set as a property of ClockView
// - Default to "analog"
//
mode = mode || this.mode || 'analog';
isAnalog = mode === 'analog';
// Determine what to hide and what to show
previous = isAnalog ? 'digital' : 'analog';
hiding = isAnalog ? this.digital : this.analog;
showing = isAnalog ? this.analog : this.digital;
// Clear any previously created timeouts.
if (this.timeouts[previous]) {
clearTimeout(
this.timeouts[previous]
);
}
hiding.classList.remove('visible');
showing.classList.add('visible');
// Update the locally stored `mode`.
this.mode = mode;
// This MUST be called after this.mode is set
// to ensure that the correct mode is used for
// updating the clockface
this.update({
needsResize: true
});
}
};
return ClockView;
});
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
(function(exports) {
'use strict';
var priv = new WeakMap();
var rmatcher = /\$\{([^}]+)\}/g;
var rentity = /[&<>"']/g;
var rentities = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&apos;'
};
function extract(node) {
var nodeId;
// Received an ID string? Find the appropriate node to continue
if (typeof node === 'string') {
nodeId = node;
node = document.getElementById(node);
} else if (node) {
nodeId = node.id;
}
if (!node) {
console.error(
'Can not find the node passed to Template', nodeId
);
return '';
}
// No firstChild means no comment node.
if (!node.firstChild) {
console.error(
'Node passed to Template should have a comment node', nodeId
);
return '';
}
// Starting with the container node's firstChild...
node = node.firstChild;
do {
// Check if it's the comment node that we're looking for...
if (node.nodeType === Node.COMMENT_NODE) {
return (node.nodeValue || '').trim();
}
// If the current child of the container node isn't
// a comment node, it's likely a text node, so hop to
// the nextSibling and repeat the operation.
} while ((node = node.nextSibling));
console.error(
'Nodes passed to Template should have a comment node', nodeId
);
return '';
}
/**
* Template
*
* Initialize a template instance from a string or node
*
* @param {String} idOrNode id string of existing node.
* {Object} idOrNode existing node.
*
*/
function Template(idOrNode) {
if (!(this instanceof Template)) {
return new Template(idOrNode);
}
// Storing the extracted template string as a private
// instance property prevents direct access to the
// template once it's been initialized.
priv.set(this, {
idOrNode: idOrNode
});
}
Template.prototype.extract = function() {
var members = priv.get(this);
if (!members.tmpl) {
members.tmpl = extract(members.idOrNode);
delete members.idOrNode;
}
return members.tmpl;
};
/**
* template.toString()
*
* Safe, read-only access to the template string
*
*/
Template.prototype.toString = function() {
// Return a copy of the stored template string.
return this.extract().slice();
};
/**
* template.interpolate
*
* Interpolate template string with values provided by data object.
* Optionally allow properties to retain HTML that is known to be safe.
*
* @param {Object} data - key/value properties correspond to substitution,
* with keys as identifiers in template string and
* values as replacement strings.
*
* @param {Object} [options] - optional
* @param {Array} options.safe - array of identifiers in the data parameter
* that contain HTML that is known and
* are "known" to ignore.
* Unless identifiers are listed here,
* there corresponding string will be escaped.
*/
Template.prototype.interpolate = function(data = {}, options = {}) {
var safeList = options.safe;
if (safeList && !Array.isArray(safeList)) {
throw new TypeError(
`If present "options.safe" must be an Array, not ${typeof options.safe}`
);
}
var hasSafeProperties = safeList && safeList.length;
return this.extract().replace(rmatcher, function(match, property) {
property = property.trim();
if (!(property in data)) {
throw new Error(
`No value provided for template identifier "${property}"`
);
}
var unsafeString = data[property];
if (typeof unsafeString !== 'string') {
throw new TypeError(
`"data.${property}" must be a String, not ${typeof unsafeString}`
);
}
var isPropertySafe = hasSafeProperties && safeList.includes(property);
return isPropertySafe ?
// If the property is explicitly listed as "safe"
// return the string of rendered markup
unsafeString:
// Otherwise the string is to be treated as suspicious
Template.escape(unsafeString);
});
};
/**
* Prepares object that can provide either interpolated template string with
* values provided by data object or ready DocumentFragment. Optionally allows
* properties to retain HTML that is known to be safe.
*
* @param {Object} data Properties correspond to substitution i.e. identifiers
* in the template string.
* @param {Object} options Optional options object. Currently supported only
* "safe" option - a list of properties that contain HTML that is known to be
* safe and don't need to be additionally escaped.
* @return {{ toString: function, toDocumentFragment: function }}
*/
Template.prototype.prepare = function(data, options) {
var self = this;
return {
toString: function t_toString() {
return self.interpolate(data, options);
},
toDocumentFragment: function t_toDocumentFragment() {
var template = document.createElement('template');
template.innerHTML = this.toString();
return template.content.cloneNode(true);
}
};
};
Template.escape = function escape(str) {
if (typeof str !== 'string') {
return '';
}
return str.replace(rentity, function(s) {
return rentities[s];
});
};
exports.Template = Template;
}(this));
define("shared/js/template", (function (global) {
return function () {
var ret, fn;
return ret || global.Template;
};
}(this)));
define('template',['require','shared/js/template'],function(require) {
'use strict';
var Template = require('shared/js/template');
function ClockTemplate(text) {
var srcNode = document.createElement('div');
var comment = document.createComment(text);
srcNode.appendChild(comment);
return new Template(srcNode);
}
return ClockTemplate;
});
define('text',{
pluginBuilder: './text_builder',
load: function(name, req, onload, config) {
'use strict';
var url = req.toUrl(name),
xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function(evt) {
var status, err;
if (xhr.readyState === 4) {
status = xhr.status;
if (status > 399 && status < 600) {
//An http 4xx or 5xx error. Signal an error.
err = new Error(url + ' HTTP status: ' + status);
err.xhr = xhr;
onload.error(err);
} else {
onload(xhr.responseText);
}
}
};
xhr.responseType = 'text';
xhr.send(null);
}
});
define('text!banner/banner.html',[],function () { return '<p data-l10n-id="${noticeId}" data-l10n-args="${noticeArgs}"></p>\n';});
define('banner/main',['require','template','utils','text!banner/banner.html'],function(require) {
'use strict';
var Template = require('template');
var Utils = require('utils');
var html = require('text!banner/banner.html');
function Banner(node) {
// Accept a reference to an element or the element id
if (typeof node === 'string') {
this.notice = document.getElementById(node);
} else {
this.notice = node;
}
// Accept an optional reference to template element id
this.tmpl = new Template(html);
// Store a reference to timeout to debounce banner
this.timeout = null;
return this;
}
Banner.prototype = {
constructor: Banner,
render: function bn_render(alarmTime) {
var timeLeft, tl, countdownType, localTimes;
timeLeft = +alarmTime - Date.now();
// generate human readable numbers to pass to localization function
tl = Utils.dateMath.fromMS(timeLeft, {
unitsPartial: ['days', 'hours', 'minutes']
});
if (tl.days) {
countdownType = 'countdown_moreThanADay';
localTimes = {
'days': tl.days,
'hours': tl.hours
};
} else if (tl.hours > 0) {
countdownType = 'countdown_moreThanAnHour';
localTimes = {
'hours': tl.hours,
'minutes': tl.minutes
};
} else {
countdownType = 'countdown_lessThanAnHour';
localTimes = {
'minutes': tl.minutes
};
}
return {
noticeId: countdownType,
noticeArgs: JSON.stringify(localTimes)
};
},
show: function bn_show(alarmTime) {
// Render the Banner notice
this.notice.innerHTML = this.tmpl.interpolate(
this.render(alarmTime)
);
// 'visible' class controls the animation
this.notice.classList.add('visible');
// use this object rather than a function to retain context
this.notice.addEventListener('click', this);
// Debounce timer in case alarms are added more quickly than 4 seconds
if (this.timeout) {
clearTimeout(this.timeout);
}
// After 4 seconds, remove the banner
this.timeout = setTimeout(this.hide.bind(this), 4000);
},
hide: function bn_hide() {
this.notice.classList.remove('visible');
this.notice.removeEventListener('click', this);
},
handleEvent: function bn_handleEvent() {
this.hide();
}
};
return Banner;
});
define('tmpl',[], function() {
'use strict';
var tmpl = {
pluginBuilder: './tmpl_builder',
toDom: function(text) {
var temp = document.createElement('div');
temp.innerHTML = text;
var node = temp.children[0];
return node;
},
load: function(id, require, onload, config) {
require(['text!' + id], function(text) {
onload(tmpl.toDom(text));
});
}
};
return tmpl;
});
define('tmpl!panels/alarm/list_item.html',['tmpl'], function (tmpl) { return tmpl.toDom('<li class="alarm-cell">\n <label class="alarmList alarmEnable">\n <input class="input-enable" data-id="" type="checkbox">\n <span></span>\n </label>\n <a href="#alarm-edit-panel" class="alarm-item" data-id="">\n <span class="time"></span>\n <span class="label"></span>\n <span class="repeat"></span>\n </a>\n</li>\n'); });
define('async_queue',['require'],function(require) {
/**
* A serial queue of functions. Call `.push()` to add a function to
* the task; when each function completes (as a result of calling
* 'done()'), the next item in the queue will be executed.
*/
function AsyncQueue() {
this.queue = [];
this.current = null;
}
AsyncQueue.prototype = {
/**
* Add a function to the execution queue. It should accept one
* argument, a 'done' callback.
*/
push: function(fn) {
if (!this.current) {
this._startTask(fn);
} else {
this.queue.push(fn);
}
},
_startTask: function(fn) {
this.current = fn;
fn(this._nextTask.bind(this));
},
_nextTask: function() {
this.current = null;
if (this.queue.length) {
this._startTask(this.queue.shift());
}
}
};
return AsyncQueue;
});
define('panels/alarm/alarm_list',['require','banner/main','alarm_database','utils','app','tmpl!panels/alarm/list_item.html','async_queue'],function(require) {
'use strict';
/* global IntlHelper */
var Banner = require('banner/main');
var alarmDatabase = require('alarm_database');
var Utils = require('utils');
var App = require('app');
var alarmTemplate = require('tmpl!panels/alarm/list_item.html');
var AsyncQueue = require('async_queue');
/**
* AlarmListPanel displays the list of alarms on the Clock tab.
*/
function AlarmListPanel(element) {
this.alarms = element;
this.newAlarmButton.addEventListener(
'click', this.onClickNewAlarm.bind(this));
this.alarms.addEventListener('click', this.onClickAlarmItem.bind(this));
this.banner = new Banner('banner-countdown');
alarmDatabase.getAll().then((alarms) => {
for (var i = 0; alarms && i < alarms.length; i++) {
this.addOrUpdateAlarm(alarms[i]);
}
this.updateAlarmStatusBar();
App.alarmListLoaded();
});
// This will be fired on language and timeformat changes
IntlHelper.observe('time-html', this.refreshDisplay.bind(this));
window.addEventListener('alarm-changed', (evt) => {
var alarm = evt.detail.alarm;
this.addOrUpdateAlarm(alarm);
if (evt.detail.showBanner) {
this.banner.show(alarm.getNextAlarmFireTime());
}
this.updateAlarmStatusBar();
});
window.addEventListener('alarm-removed', (evt) => {
this.removeAlarm(evt.detail.alarm);
this.updateAlarmStatusBar();
});
}
AlarmListPanel.prototype = {
alarmIdMap: {},
refreshDisplay: function(evt) {
for (var key in this.alarmIdMap) {
var alarm = this.alarmIdMap[key];
this.addOrUpdateAlarm(alarm);
}
},
onClickNewAlarm: function(evt) {
evt.preventDefault();
App.navigate({ hash: '#alarm-edit-panel', data: null });
},
onClickAlarmItem: function(evt) {
var link = evt.target;
var alarm = this.alarmIdMap[link.dataset.id];
if (link.classList.contains('input-enable')) {
this.toggleAlarm(alarm, link.checked);
} else if (link.classList.contains('alarm-item')) {
App.navigate({ hash: '#alarm-edit-panel', data: alarm });
evt.preventDefault();
}
},
/**
* Render an alarm into a DOM node.
*
* @param alarm The alarm to render.
* @param {Element} [li] Existing element to re-use, if any.
*/
renderAlarm: function(alarm) {
var li = (this.alarms.querySelector('#alarm-' + alarm.id) ||
alarmTemplate.cloneNode(true));
var isActive = ('normal' in alarm.registeredAlarms ||
'snooze' in alarm.registeredAlarms);
var d = new Date();
d.setHours(alarm.hour);
d.setMinutes(alarm.minute);
li.id = 'alarm-' + alarm.id;
li.dataset.id = alarm.id;
var enableButton = li.querySelector('.input-enable');
enableButton.dataset.id = alarm.id;
enableButton.checked = isActive;
var link = li.querySelector('.alarm-item');
link.classList.toggle('with-repeat', alarm.isRepeating());
link.dataset.id = alarm.id;
li.querySelector('.time').innerHTML = Utils.getLocalizedTimeHtml(d);
if (alarm.label) {
li.querySelector('.label').removeAttribute('data-l10n-id');
li.querySelector('.label').textContent = alarm.label;
} else {
li.querySelector('.label').setAttribute('data-l10n-id', 'alarm');
}
if (alarm.isRepeating()) {
Utils.summarizeDaysOfWeek(alarm.repeat).then(l10nId => {
if (typeof l10nId === 'string') {
li.querySelector('.repeat').setAttribute('data-l10n-id', l10nId);
} else if (l10nId.raw) {
li.querySelector('.repeat').removeAttribute('data-l10n-id');
li.querySelector('.repeat').textContent = l10nId.raw;
}
});
} else {
li.querySelector('.repeat').removeAttribute('data-l10n-id');
li.querySelector('.repeat').textContent = '';
}
return li;
},
refreshClockView: function() {
window.dispatchEvent(new CustomEvent('alarm-list-changed'));
},
addOrUpdateAlarm: function(alarm) {
this.alarmIdMap[alarm.id] = alarm;
var li = this.renderAlarm(alarm);
var liId = parseInt(li.dataset.id, 10) || null;
// Go through the list of existing alarms, inserting this alarm
// before the first alarm that has a lower ID than this one.
var node = this.alarms.firstChild;
while (true) {
var nodeId = (node ? parseInt(node.dataset.id, 10) : -1);
if (nodeId < liId) {
this.alarms.insertBefore(li, node);
break;
}
node = node.nextSibling;
}
this.refreshClockView();
},
removeAlarm: function(alarm) {
delete this.alarmIdMap[alarm.id];
var li = this.alarms.querySelector('#alarm-' + alarm.id);
if (li) {
li.parentNode.removeChild(li);
}
this.refreshClockView();
},
toggleAlarmQueue: new AsyncQueue(),
/**
* Toggle an alarm's enabled state. To ensure that the database
* state remains consistent with the DOM, perform operations
* serially in a queue.
*
* @param {Alarm} alarm
* @param {boolean} enabled
* @param {function} callback Optional callback.
*/
toggleAlarm: function(alarm, enabled) {
this.toggleAlarmQueue.push((done) => {
if (enabled) {
alarm.schedule('normal').then(() => {
this.addOrUpdateAlarm(alarm);
this.updateAlarmStatusBar();
if (alarm.isEnabled()) {
this.banner.show(alarm.getNextAlarmFireTime());
}
}).then(done, done);
} else {
alarm.cancel().then(done);
}
});
},
updateAlarmStatusBar: function() {
if (navigator.mozSettings) {
var anyAlarmEnabled = false;
for (var id in this.alarmIdMap) {
if (this.alarmIdMap[id].isEnabled()) {
anyAlarmEnabled = true;
break;
}
}
navigator.mozSettings.createLock().set({
'alarm.enabled': anyAlarmEnabled
});
}
}
};
Utils.extendWithDomGetters(AlarmListPanel.prototype, {
title: '#alarms-title',
newAlarmButton: '#alarm-new'
});
return AlarmListPanel;
});
define('panels/alarm/child_window_manager',['require','utils'],function(require) {
'use strict';
var Utils = require('utils');
const READY_EVENT_TYPE = 'childWindowReady';
/**
* ChildWindowManager maintains the lifecycle of a child attention
* window. When you instantiate a ChildWindowManager, no window
* exists yet; use .whenReady() or postMessage() to call up the
* child window and perform actions on it. Provided the child window
* calls ChildWindowManager.fireReady(), this class takes care of
* the bookkeeping to make sure that the child window is ready to
* receive events before sending over any messages. Similarly, this
* class acquires a CPU wake lock when opening the window to ensure
* that your messages are delivered reliably without the phone going
* to sleep while waiting for the child window to receive a message.
*/
function ChildWindowManager(url) {
this.url = url;
this.childWindow = null;
this.childWindowReady = false;
this.childOnReadyCallbacks = [];
this.releaseCpuLock = null;
window.addEventListener('message', this);
}
ChildWindowManager.prototype = {
/**
* Post a message to the child window. If the window is not yet
* open, acquire a CPU wake lock and open the window, then deliver
* the message. Subsequent calls to postMessage will similarly
* wait until the window is ready before delivery.
*/
postMessage: function(message) {
this.whenReady(() => {
this.childWindow.postMessage(message, window.location.origin);
});
},
/**
* Closes the window. You may reinstantiate a window again by
* sending another postMessage.
*/
close: function() {
if (this.childWindow && !this.childWindow.closed) {
this.childWindow.close();
}
this.childWindow = null;
},
/**
* Call a function when the window is ready and opened. This is
* used internally by postMessage.
*/
whenReady: function(callback) {
if (!this.childWindow || this.childWindow.closed) {
Utils.safeWakeLock({ timeoutMs: 30000 }, (releaseCpu) => {
this.childWindow = window.VulpesCompat ? VulpesCompat.openAttention(this.url) : window.open(this.url, '_blank', 'attention');
this.childWindowReady = false;
this.releaseCpuLock = releaseCpu;
});
}
if (this.childWindowReady) {
callback();
} else {
this.childOnReadyCallbacks.push(callback);
}
},
/** Private. Handle DOM events. */
handleEvent: function(evt) {
if (evt.data.type === READY_EVENT_TYPE) {
this.childWindowReady = true;
while (this.childOnReadyCallbacks.length) {
var callback = this.childOnReadyCallbacks.shift();
callback();
}
if (this.releaseCpuLock) {
this.releaseCpuLock();
}
}
}
};
/**
* Call this method from a child window when the child has loaded.
* This fires a message to the parent window, instructing it to pass
* along any queued events.
*/
ChildWindowManager.fireReady = function() {
if (!window.opener) {
throw new Error('fireReady must be called from the child window.');
}
window.opener.postMessage({
type: READY_EVENT_TYPE
}, window.location.origin);
};
return ChildWindowManager;
});
define('panels/alarm/post_message_proxy',['require'],function(require) {
var objectMap = {}; // Map of object handlers.
var responseMap = {}; // Map of callbacks awaiting function results.
var sequenceNumber = 0; // ID for matching requests to responses.
var PostMessageProxy = {
/**
* Create a PostMessageProxy. This returns an object that you can
* call like any other JavaScript object, by proxying methods
* through to another window.
*
* When you call a method, rather than returning its return value,
* it reutrns a Promise that you can resolve to get the return
* value from the remote function.
*
* @param whichWindow The window (usually window.parent or a child)
* @param objectId A string to identify the object you're proxying
*/
create: function(whichWindow, objectId) {
return new Proxy({ window: whichWindow }, {
get: function(target, name) {
if (name in target) {
return target[name];
}
return function() {
var args = Array.slice(arguments);
return new Promise((resolve, reject) => {
var responseId = ++sequenceNumber;
responseMap[responseId] = {
resolve: resolve,
reject: reject
};
target.window.postMessage({
postMessageProxy: objectId,
responseId: responseId,
fn: name,
args: args
}, '*');
});
};
}
});
},
/**
* On the other window, call PostMessateProxy.receive() to hook up
* an object that processes messages from a proxy in another window.
*/
receive: function(objectId, obj) {
objectMap[objectId] = obj;
},
/**
* Handle 'message' events from postMessage, both when receiving a
* message to invoke a function call, and receiving a message with
* the return value of that function call. Both callbacks are
* handled here so that we only bind one listener for each
* relevant window.
*/
_messageHandler: function(evt) {
if (!evt.source || evt.origin !== window.location.origin || !evt.data) return;
var data = evt.data;
if (data.postMessageProxy) {
// Remote side (calling a function):
var obj = objectMap[data.postMessageProxy];
var { fn, args, responseId } = data;
try {
evt.source.postMessage({
postMessageProxyResult: responseId,
result: obj[fn].apply(obj, args)
}, window.location.origin);
} catch(e) {
evt.source.postMessage({
postMessageProxyResult: responseId,
exception: e.toString(),
stack: e.stack
}, evt.origin);
}
} else if (data.postMessageProxyResult) {
// Local side (return value):
if (responseMap[data.postMessageProxyResult]) {
var { resolve, reject } = responseMap[data.postMessageProxyResult];
delete responseMap[data.postMessageProxyResult];
if (data.exception) {
var e = new Error();
e.name = data.exception;
e.stack = data.stack;
reject(e);
} else {
resolve(data.result);
}
}
}
}
};
window.addEventListener('message', PostMessageProxy._messageHandler);
return PostMessageProxy;
});
define('panels/alarm/active_alarm',['require','app','alarm_database','timer','utils','./child_window_manager','./post_message_proxy'],function(require) {
'use strict';
var App = require('app');
var alarmDatabase = require('alarm_database');
var Timer = require('timer');
var Utils = require('utils');
var ChildWindowManager = require('./child_window_manager');
var PostMessageProxy = require('./post_message_proxy');
/**
* ActiveAlarm handles the system event that fires when an alarm
* goes off. This includes opening an attention window and updating
* an alarm's schedule when the user taps 'snooze'. The interaction
* is mediated through a PostMessageProxy (as `this.ringView`),
* which makes it trivial to interact with a JavaScript object
* hosted in another window.
*/
function ActiveAlarm() {
this.alertWindow = new ChildWindowManager(
window.location.origin + '/onring.html');
// Handle the system's alarm event.
navigator.mozSetMessageHandler('alarm', this.onMozAlarm.bind(this));
window.addEventListener('test-alarm', this.onMozAlarm.bind(this));
// Handle events transparently from the child window.
PostMessageProxy.receive('activeAlarm', this);
this.ringView = PostMessageProxy.create(null, 'ringView');
}
function logForAlarmDebugging() {
// The following logs are included for debugging reported
// problems with alarm firing times: <https://bugzil.la/1052245>
console.log('[Clock] =====================================\n');
console.log('[Clock] Alarm Debug:', JSON.stringify({
'now': new Date().toJSON(),
'tz': new Date().getTimezoneOffset()
}));
alarmDatabase.getAll().then((alarms) => {
console.log('[Clock] ===== Raw IndexedDB Alarm Data: =====\n');
// Logging in a loop to ensure we don't overrun the line buffer:
alarms.forEach(function(a) {
console.log('[Clock] ', JSON.stringify(a.toJSON()));
});
console.log('[Clock] -------------------------------------\n');
});
var request = navigator.mozAlarms.getAll();
request.onsuccess = function() {
console.log('[Clock] ======= Remaining mozAlarms: ========\n');
if (!request.result) {
console.log('[Clock] mozAlarm API invariant failure?');
} else {
request.result.forEach(function(alarm) {
console.log('[Clock] ', JSON.stringify(alarm));
});
}
console.log('[Clock] -------------------------------------\n');
};
request.onerror = function() {
console.error('[Clock] Failed to get list of mozAlarms:', this.error);
};
}
// Log at startup.
logForAlarmDebugging();
// Log periodically when the Clock app is running.
setInterval(logForAlarmDebugging, 10 * 60 * 1000);
ActiveAlarm.prototype = {
/**
* Fired when the system triggers an alarm. We acquire a wake lock
* here to ensure that the system doesn't fall asleep before we
* have a chance to present the attention alert window.
*/
onMozAlarm: function(message) {
// message.detail in only for marionette test.
// We pass it via AlarmActions.fire method.
var data = message.data || message.detail;
data.date = message.date || message.detail.date;
console.log('[Clock] ### ALARM FIRED! ### Details:',
JSON.stringify(message));
logForAlarmDebugging(message);
Utils.safeWakeLock({ timeoutMs: 30000 }, (done) => {
switch (data.type) {
case 'normal':
case 'snooze':
this.onAlarmFired(data, done);
break;
case 'timer':
this.onTimerFired(data, done);
break;
}
});
},
/**
* Add `alert` to the attention screen. The child alert window
* expects to receive any number of alert messages; if the child
* window has not been presented yet, this function opens the
* window before passing along the alert.
*
* An Alert object (which can represent a timer or an alarm)
* adheres to the following structure:
*
* @param {Alert} alert An alert to pass to the child window.
* @param {string} alert.type 'alarm' or 'timer'
* @param {string} [alert.label] Optional label
* @param {string} [alert.sound] Optional filename of a sound to play
* @param {boolean} alert.vibrate True if the alarm should vibrate
* @param {Date} alert.time The time the alert was supposed to fire
* @param {string} [alert.id] The ID of the alert, if type === 'alarm'
*/
popAlert: function(alert) {
this.alertWindow.whenReady(() => {
this.ringView.window = this.alertWindow.childWindow;
this.ringView.addAlert(alert);
});
},
/**
* Handle an alarm firing. Immediately reschedule the alarm for
* its next firing interval (if the alarm was a repeat alarm).
*
* @param {object} message The message as retrieved by mozAlarm
* @param {function} done Callback to release the wake lock.
*/
onAlarmFired: function(data, done) {
var id = data.id;
var date = data.date;
var type = data.type;
alarmDatabase.get(id).then((alarm) => {
this.popAlert({
type: 'alarm',
label: alarm.label,
sound: alarm.sound,
vibrate: alarm.vibrate,
time: date,
hour: alarm.hour,
minute: alarm.minute,
id: alarm.id
});
if (type === 'normal') {
// The alarm instance doesn't yet know that a mozAlarm has
// fired, so we call .cancel() to wipe this mozAlarm ID from
// alarm.registeredAlarms().
alarm.cancel();
if (alarm.isRepeating()) {
alarm.schedule('normal').then(done);
} else {
done();
}
} else if (type === 'snooze') {
// Inform the Alarm instance that a mozAlarm snooze has fired.
alarm.cancel('snooze');
done();
} else {
done();
}
});
},
/**
* Handle a timer firing.
*
* @param {object} message The message as retrieved by mozAlarm
* @param {function} done Callback to release the wake lock.
*/
onTimerFired: function(data, done) {
Timer.getFromStorage((timer) => {
this.popAlert({
type: 'timer',
label: timer.label,
sound: timer.sound,
vibrate: timer.vibrate,
time: new Date(timer.startTime + timer.duration)
});
done();
});
},
/**
* Snooze the given alarm.
*
* @param {string} alarmId The ID of the alarm.
*/
snoozeAlarm: function(alarmId) {
alarmDatabase.get(alarmId).then((alarm) => {
alarm.schedule('snooze');
});
},
/**
* Close the current alert window.
*
* @param {string} type 'alarm' or 'timer'
* @param {string} alarmId The ID of the alarm, if type === 'alarm'
*/
close: function(type, alarmId) {
this.alertWindow.close();
if (type === 'timer') {
App.navigate({ hash: '#timer-panel' });
Timer.singleton(function(err, timer) {
if (!err) {
timer.cancel();
timer.save();
}
});
} else if (type === 'alarm') {
App.navigate({ hash: '#alarm-panel' });
}
}
};
return ActiveAlarm;
});
define('text!panels/alarm/panel.html',[],function () { return '<div id="clock-view">\n <div id="analog-clock">\n <div id="analog-clock-container" role="img">\n <div id="analog-clock-face">\n <div id="analog-clock-hands">\n <div class="analog-clock-hand" id="secondhand"></div>\n <div class="analog-clock-hand" id="minutehand"></div>\n <div class="analog-clock-hand" id="hourhand"></div>\n </div>\n </div>\n </div>\n </div>\n <div id="digital-clock">\n <div id="digital-clock-face">\n <span id="clock-time"></span>\n </div>\n </div>\n <div id="clock-day-date"></div>\n <!-- create new alarm icon -->\n <button id="alarm-new" data-l10n-id="newAlarmButton"></button>\n <!-- list of exisiting alarms, to be populated -->\n <ul id="alarms"></ul>\n</div>\n<section id="banner-countdown" role="status">\n <!-- this will be replaced dynamically -->\n</section>\n';});
define('panels/alarm/main',['require','panel','panels/alarm/clock_view','panels/alarm/alarm_list','panels/alarm/active_alarm','text!panels/alarm/panel.html'],function(require) {
'use strict';
var Panel = require('panel');
var ClockView = require('panels/alarm/clock_view');
var AlarmListPanel = require('panels/alarm/alarm_list');
var ActiveAlarm = require('panels/alarm/active_alarm');
var html = require('text!panels/alarm/panel.html');
function AlarmPanel() {
Panel.apply(this, arguments);
this.element.innerHTML = html;
ClockView.init();
this.alarmListPanel = new AlarmListPanel(document.getElementById('alarms'));
this.activeAlarm = new ActiveAlarm();
}
AlarmPanel.prototype = Object.create(Panel.prototype);
return AlarmPanel;
});