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.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

1224
gaia/apps/clock/js/alameda.js Executable file

File diff suppressed because it is too large Load diff

256
gaia/apps/clock/js/alarm.js Executable file
View file

@ -0,0 +1,256 @@
define(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;
});

View file

@ -0,0 +1,205 @@
'use strict';
define(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);
});

165
gaia/apps/clock/js/app.js Executable file
View file

@ -0,0 +1,165 @@
define(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;
});

View file

@ -0,0 +1,42 @@
'use strict';
define(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;
});

View file

@ -0,0 +1,185 @@
/* global define */
define(function(require) {
'use strict';
/**
* The Settings App stores volumes in the range [0, 15] inclusive.
* Whenever we need to play sounds, though, the Audio object
* requires a float between [0.0, 1.0]. The conversion has to happen
* somewhere. The AudioManager here draws the line right out of what
* gets read from mozSettings.
*
* In other words, the conversion is not important to clients of
* this class, who should treat the volume as a float with no
* conversion. The only weirdness here is that unit tests must be
* aware of the slight rounding differences when converting from a
* float to the system level.
*/
////////////////////////////////////////////////////////////////
// VolumeManager
function isValidVolume(volume) {
return (typeof volume === 'number' &&
volume <= 1.0 &&
volume >= 0.0);
}
var VOLUME_SETTING = 'audio.volume.alarm';
var SYSTEM_VOLUME_MAX = 15;
function systemVolumeToFloat(volume) {
return (volume / SYSTEM_VOLUME_MAX);
}
function floatToSystemVolume(volume) {
return Math.round(volume * SYSTEM_VOLUME_MAX);
}
function requestAlarmSystemVolume() {
// Asynchronously load the alarm volume from mozSettings.
return new Promise(function(resolve, reject) {
var lock = navigator.mozSettings.createLock();
var req = lock.get(VOLUME_SETTING);
req.onsuccess = function() {
var volume = systemVolumeToFloat(req.result[VOLUME_SETTING]);
if (isValidVolume(volume)) {
globalVolumeManager._volume = volume;
resolve(volume);
}
};
req.onerror = function() {
var DEFAULT_VOLUME = 1.0;
resolve(DEFAULT_VOLUME);
};
});
}
function VolumeManager() {
this.VOLUME_KEY = 'defaultAlarmVolume';
this.DEFAULT_VOLUME = 1.0;
this._volume = this.DEFAULT_VOLUME;
if (navigator.mozSettings) {
navigator.mozSettings.addObserver(
VOLUME_SETTING,
this.onSystemAlarmVolumeChange.bind(this));
}
}
VolumeManager.prototype = {
onSystemAlarmVolumeChange: function(e) {
// don't use the setter here
this._volume = systemVolumeToFloat(e.settingValue);
},
get volume() {
return this._volume;
},
set volume(volume) {
this.setVolume(volume);
},
/** Set the volume with an optional completion callback. */
setVolume: function(volume, cb) {
if (isValidVolume(volume)) {
this._volume = volume;
if (navigator.mozSettings) {
var lock = navigator.mozSettings.createLock();
var opts = {};
opts[VOLUME_SETTING] = floatToSystemVolume(volume);
var req = lock.set(opts);
if (cb) {
req.onsuccess = cb;
}
}
}
}
};
////////////////////////////////////////////////////////////////
// AudioPlayer
var globalVolumeManager = new VolumeManager();
/**
* The AudioPlayer class manages the playback of alarm ringtones. It
* is lazy-loading, so that you can instantiate it immediately;
* Audio objects are not actually created or loaded until you need
* to play a sound.
*/
function AudioPlayer() {
this._audio = null;
}
AudioPlayer.prototype = {
/**
* Play a ringtone from the shared/resources/media/alarms
* directory, using the current global volume settings by default.
* You can override the volume through opts.volume.
*
* @param {string} ringtoneName
* @param {number} opts.volume Value between 0 and 1
*/
playRingtone: function(ringtoneName, opts) {
var volume = globalVolumeManager.volume;
if (opts && 'volume' in opts) {
volume = opts.volume;
}
this._prepare(); // Load up the audio element.
this._audio.pause();
this._audio.src = 'shared/resources/media/alarms/' + ringtoneName;
this._audio.load(); // Required per MDN's HTMLMediaElement spec.
this._audio.volume = volume;
this._audio.play();
},
/**
* Pause the currently-playing audio, if possible.
*/
pause: function() {
if (this._audio) {
this._audio.pause();
}
},
// Private methods:
/**
* Instantiate the Audio element and prepare it for playback.
* For internal use only.
* @private
*/
_prepare: function() {
if (!this._audio) {
this._audio = new Audio();
this._audio.mozAudioChannelType = 'alarm';
this._audio.loop = true;
}
}
};
return {
getAlarmVolume: function() {
return globalVolumeManager.volume;
},
requestAlarmVolume: function() {
return requestAlarmSystemVolume();
},
setAlarmVolume: function(volume, cb) {
globalVolumeManager.setVolume(volume, cb);
},
createAudioPlayer: function() {
return new AudioPlayer();
},
// Exposed for tests:
systemVolumeToFloat: systemVolumeToFloat,
floatToSystemVolume: floatToSystemVolume
};
});

View file

@ -0,0 +1 @@
<p data-l10n-id="${noticeId}" data-l10n-args="${noticeArgs}"></p>

View file

@ -0,0 +1,88 @@
define(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;
});

View file

@ -0,0 +1,49 @@
define(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;
}
});

View file

@ -0,0 +1,37 @@
define(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);
};
}
});
};
});

View file

@ -0,0 +1,78 @@
define(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();
}
});

View file

@ -0,0 +1,9 @@
define(function(require, exports) {
'use strict';
// ---------------------------------------------------------
// Constants
// Random day that happens to be Sunday
exports.KNOWN_SUNDAY = new Date(2015, 8, 20);
});

17
gaia/apps/clock/js/css.js Executable file
View file

@ -0,0 +1,17 @@
'use strict';
// Loader plugin for loading CSS. Does not guarantee loading via onload
// watching, just inserts link tag.
define({
load: function(id, require, onload, config) {
if (config.isBuild) {
return onload();
}
var link = document.createElement('link');
link.type = 'text/css';
link.rel = 'stylesheet';
link.href = require.toUrl(id + '.css');
link.addEventListener('load', onload, false);
document.head.appendChild(link);
}
});

View file

@ -0,0 +1,70 @@
/**
* parse-duration v0.1.1
* Released under the MIT License
* https://github.com/jkroso/parse-duration
*/
define(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
}
});

View file

@ -0,0 +1,138 @@
/**
* parse-loose-time v1.0.0
* https://github.com/millermedeiros/parse-loose-time/
* released under the MIT License
*/
define(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 };
}
});

196
gaia/apps/clock/js/form_button.js Executable file
View file

@ -0,0 +1,196 @@
// outer IIFE
define(function(require) {
'use strict';
var Utils = require('utils');
function createButton(formButton) {
var button = document.createElement(formButton.tagName);
button.className = formButton.className;
if (formButton.id) {
button.id = formButton.id;
}
var input = formButton.input;
input.parentNode.insertBefore(button, input.nextSibling);
formButton.button = button;
}
/**
* A FormButton is a button that triggers an input. The text
* of the currently selected value will display on the buttons's face.
*
* The `config` paramater supports the following optional properties.
* `formatLabel` - A function that is given the current value of the input
* and should return a string which will be used as the textContent of
* the button.
*
* `tagName` - The name of the tag to create and insert into the
* document as the main button used to trigger the input. The default
* value is 'button'
*
* `className` The value of the className property that will be assigned to
* the button element the default value is 'icon icon-dialog'.
*
* `id` - A string that is used as the id of the button element.
*
* @constructor
* @param {HTMLElement} input The input element to trigger.
* @param {Object} config An optional config object.
*
*/
function FormButton(input, config) {
config = config || {};
Utils.extend(this, config);
this.input = input;
createButton(this);
this.input.classList.add('form-button-input');
// hide input
this.input.classList.add('form-button-hide');
// set isSelect
Object.defineProperty(this, 'isSelect', {
value: this.input.nodeName === 'SELECT'
});
this.button.addEventListener('click', this.focus.bind(this), false);
input.addEventListener('change', this.refresh.bind(this), false);
input.addEventListener('blur', this.refresh.bind(this), false);
// Bind this.refresh so that the listener can be easily removed.
this.refresh = this.refresh.bind(this);
// Update the dropdown when the language changes.
window.addEventListener('localized', this.refresh);
window.addEventListener('timeformatchange', this.refresh);
}
FormButton.prototype = {
/** Remove all event handlers. */
destroy: function() {
window.removeEventListener('localized', this.refresh);
},
/**
* focus Triggers a focus event on the input associated with this
* FormButton.
*
* @param {Object} event an event object.
*/
focus: function(event) {
event.preventDefault();
setTimeout(this.input.focus.bind(this.input), 10);
},
/**
* refresh Updates the label text on the button to reflect
* the current value of the input.
*
*/
refresh: function() {
var value = this.value;
var label = this.formatLabel(value);
// In some cases formatLabel returns a Promise that returns l10nId
var labelPromise = label.then ? label : Promise.resolve(label);
return labelPromise.then((l10nId) => {
if (typeof l10nId === 'string') {
this.button.setAttribute('data-l10n-id', l10nId);
} else if (l10nId.raw) {
this.button.removeAttribute('data-l10n-id');
this.button.textContent = l10nId.raw;
} else if (l10nId.id) {
document.l10n.setAttributes(this.button, l10nId.id, l10nId.args);
}
});
},
/**
* value Returns the current value of the input.
*
* @return {String|Object} The value of the input.
*
*/
get value() {
if (this.isSelect) {
if (this.input.multiple) {
var selectedOptions = {};
var options = this.input.options;
for (var i = 0; i < options.length; i++) {
if (options[i].selected) {
selectedOptions[options[i].value] = true;
}
}
return selectedOptions;
}
if (this.input.selectedIndex !== -1) {
return Utils.getSelectedValueByIndex(this.input);
}
return null;
}
// input node
return this.input.value;
},
/**
* value sets the current value of the input and update's the
* button text.
*
* @param {String|Object} value A string of the current values or an
* object with properties that map to input options if the input is
* a multi select.
*
*/
set value(value) {
if (this.isSelect) {
if (this.input.multiple) {
// multi select
var options = this.input.options;
for (var i = 0; i < options.length; i++) {
options[i].selected = value[options[i].value] === true;
}
} else {
// normal select element
Utils.changeSelectByValue(this.input, value);
}
} else {
// input element
this.input.value = value;
}
// Update the text on the button to reflect the new input value
this.refresh();
},
/**
* An overrideable method that is called when updating the textContent
* of the button.
*
* @return {L10nId} The formatted text to display in the label.
*
*/
formatLabel: function(value) {
return { raw: value };
},
/**
* tagName The the name of the tag to insert into the document to use
* as the button element.
*/
tagName: 'button',
/**
* class The value to assign to the className property on the
* generated button element.
*/
className: 'icon icon-dialog'
};
return FormButton;
// end outer IIFE
});

6
gaia/apps/clock/js/onring.js Executable file
View file

@ -0,0 +1,6 @@
'use strict';
requirejs(['require_config'], function() {
requirejs(['ring_view'], function(RingView) {
window.ringView = new RingView();
});
});

96
gaia/apps/clock/js/panel.js Executable file
View file

@ -0,0 +1,96 @@
define(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;
});

View file

@ -0,0 +1,229 @@
define(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;
});

View file

@ -0,0 +1,211 @@
define(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;
});

View file

@ -0,0 +1,103 @@
define(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;
});

View file

@ -0,0 +1,336 @@
define(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;
});

View file

@ -0,0 +1,11 @@
<li class="alarm-cell">
<label class="alarmList alarmEnable">
<input class="input-enable" data-id="" type="checkbox">
<span></span>
</label>
<a href="#alarm-edit-panel" class="alarm-item" data-id="">
<span class="time"></span>
<span class="label"></span>
<span class="repeat"></span>
</a>
</li>

View file

@ -0,0 +1,22 @@
define(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;
});

View file

@ -0,0 +1,26 @@
<div id="clock-view">
<div id="analog-clock">
<div id="analog-clock-container" role="img">
<div id="analog-clock-face">
<div id="analog-clock-hands">
<div class="analog-clock-hand" id="secondhand"></div>
<div class="analog-clock-hand" id="minutehand"></div>
<div class="analog-clock-hand" id="hourhand"></div>
</div>
</div>
</div>
</div>
<div id="digital-clock">
<div id="digital-clock-face">
<span id="clock-time"></span>
</div>
</div>
<div id="clock-day-date"></div>
<!-- create new alarm icon -->
<button id="alarm-new" data-l10n-id="newAlarmButton"></button>
<!-- list of exisiting alarms, to be populated -->
<ul id="alarms"></ul>
</div>
<section id="banner-countdown" role="status">
<!-- this will be replaced dynamically -->
</section>

View file

@ -0,0 +1,103 @@
'use strict';
define(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;
});

View file

@ -0,0 +1,364 @@
'use strict';
/* global KeyEvent, mozIntl, IntlHelper */
define(function(require) {
var Alarm = require('alarm');
var ClockView = require('panels/alarm/clock_view');
var AudioManager = require('audio_manager');
var FormButton = require('form_button');
var Sounds = require('sounds');
var Utils = require('utils');
var Panel = require('panel');
var html = require('text!panels/alarm_edit/panel.html');
var constants = require('constants');
IntlHelper.define('weekday-long', 'datetime', {
weekday: 'long'
});
var AlarmEdit = function() {
Panel.apply(this, arguments);
this.element.innerHTML = html;
var handleDomEvent = this.handleDomEvent.bind(this);
this.element.addEventListener('panel-visibilitychange',
this.handleVisibilityChange.bind(this));
this.selects = {};
[
'time', 'repeat', 'sound', 'snooze'
].forEach(function(id) {
this.selects[id] = this.element.querySelector('#' + id + '-select');
}, this);
this.inputs = {
name: this.element.querySelector('#alarm-name'),
volume: this.element.querySelector('#alarm-volume-input')
};
this.headers = {
header: this.element.querySelector('#alarm-header')
};
this.buttons = {};
[
'delete', 'done'
].forEach(function(id) {
this.buttons[id] = this.element.querySelector('#alarm-' + id);
}, this);
this.checkboxes = {
vibrate: this.element.querySelector('#vibrate-checkbox')
};
this.buttons.time = new FormButton(this.selects.time, {
formatLabel: function(value) {
var date = new Date();
// This split(':') is locale-independent per HTML5 <input type=time>
var splitValue = value.split(':');
date.setHours(splitValue[0]);
date.setMinutes(splitValue[1]);
return { raw: Utils.getLocalizedTimeText(date) };
}.bind(this)
});
this.buttons.repeat = new FormButton(this.selects.repeat, {
id: 'repeat-menu',
formatLabel: Utils.summarizeDaysOfWeek
});
this.buttons.sound = new FormButton(this.selects.sound, {
id: 'sound-menu',
formatLabel: Sounds.formatLabel
});
this.buttons.snooze = new FormButton(this.selects.snooze, {
id: 'snooze-menu',
formatLabel: function(snooze) {
var numFormatter = IntlHelper.get('digit-nopadding');
return {
id: 'alarmMinutes',
args: {minutes: numFormatter.format(parseInt(snooze))}
};
}
});
this.scrollList = this.element.querySelector('#edit-alarm');
this.repeatSelect = this.element.querySelector('#repeat-select');
// When the system pops up the ValueSelector, it inadvertently
// messes with the scrollTop of the current panel. This is a
// workaround for bug 981255 until the Edit panel becomes a new
// window per bug 922651.
this.element.addEventListener('scroll', function() {
this.element.scrollTop = 0;
}.bind(this));
// When the language changes, the value of 'firstDayOfTheWeek'
// might change and we need to update translations of weekdays
this.updateL10n();
document.addEventListener('DOMRetranslated', this.updateL10n.bind(this));
this.headers.header.addEventListener('action', handleDomEvent);
this.buttons.done.addEventListener('click', handleDomEvent);
this.selects.sound.addEventListener('change', handleDomEvent);
this.selects.sound.addEventListener('blur', handleDomEvent);
this.selects.repeat.addEventListener('change', handleDomEvent);
this.buttons.delete.addEventListener('click', handleDomEvent);
this.inputs.name.addEventListener('keypress', this.handleNameInput);
this.inputs.volume.addEventListener('change', handleDomEvent);
this.isSaving = false;
// If the phone locks during preview, or an alarm fires, pause the sound.
// TODO: When this is no longer a singleton, unbind the listener.
window.addEventListener('visibilitychange', function() {
if (document.hidden) {
this.stopPreviewSound();
// Ensure the keyboard goes away.
document.activeElement.blur();
}
}.bind(this));
};
AlarmEdit.prototype = Object.create(Panel.prototype);
Utils.extend(AlarmEdit.prototype, {
alarm: null,
ringtonePlayer: AudioManager.createAudioPlayer(),
handleNameInput: function(evt) {
// If the user presses enter on the name label, dismiss the
// keyboard to allow them to continue filling out the other
// fields. This is not in the `handleEvent` function because we
// only want to call `.preventDefault` sometimes.
if (evt.keyCode === KeyEvent.DOM_VK_RETURN) {
evt.preventDefault();
evt.target.blur();
}
},
updateL10n: function() {
// Move the weekdays around to properly account for the firstDay.
mozIntl.calendarInfo('firstDayOfTheWeek').then(firstDay => {
var formatter = IntlHelper.get('weekday-long');
var options = [...this.repeatSelect.querySelectorAll('option')];
var optionsByDay = {};
options.forEach(option => {
var value = option.getAttribute('value');
optionsByDay[value] = option;
this.repeatSelect.removeChild(option);
});
for (var i = 0; i < 7; i++) {
var day = (i + firstDay) % options.length;
var option = optionsByDay[day.toString()];
var dayDate = new Date(constants.KNOWN_SUNDAY);
dayDate.setDate(constants.KNOWN_SUNDAY.getDate() + firstDay + i);
option.textContent = formatter.format(dayDate);
this.repeatSelect.appendChild(option);
}
});
var numFormatter = IntlHelper.get('digit-nopadding');
var elements = [...document.querySelectorAll('#snooze-select > option')];
elements.forEach(element => {
var value = parseInt(element.value);
element.setAttribute('data-l10n-args',
JSON.stringify({minutes: numFormatter.format(value)}));
});
},
// The name `handleEvent` is already defined by the Panel class, so this
// method must be named uniquely to avoid overriding that functionality.
handleDomEvent: function aev_handleDomEvent(evt) {
evt.preventDefault();
var input = evt.target;
if (!input) {
return;
}
switch (input) {
case this.headers.header:
ClockView.show();
break;
case this.buttons.done:
ClockView.show();
this.save();
break;
case this.selects.sound:
switch (evt.type) {
case 'change':
this.previewSound();
break;
case 'blur':
this.stopPreviewSound();
break;
}
break;
case this.buttons.delete:
ClockView.show();
this.delete();
break;
case this.selects.repeat:
this.alarm.repeat = this.buttons.repeat.value;
break;
case this.inputs.volume:
// Alarm Volume is applied to all alarms.
AudioManager.setAlarmVolume(this.getAlarmVolumeValue());
break;
}
},
focusMenu: function aev_focusMenu(menu) {
setTimeout(function() { menu.focus(); }, 10);
},
handleVisibilityChange: function aev_show(evt) {
var isVisible = evt.detail.isVisible;
var alarm;
if (!isVisible) {
return;
}
// `navData` is set by the App module in `navigate`.
alarm = this.navData;
// scroll to top of form list
this.scrollList.scrollTop = 0;
this.element.classList.toggle('new', !alarm);
this.alarm = new Alarm(alarm); // alarm may be null
// Set to empty string if the Alarm doesn't have an ID,
// otherwise dataset will automatically stringify it
// to be "undefined" rather than "".
this.element.dataset.id = this.alarm.id || '';
this.inputs.name.value = this.alarm.label;
AudioManager.requestAlarmVolume().then(function(volume) {
this.inputs.volume.value = AudioManager.getAlarmVolume();
}.bind(this));
// Init time, repeat, sound, snooze selection menu.
this.initTimeSelect();
this.initRepeatSelect();
this.initSoundSelect();
this.initSnoozeSelect();
this.checkboxes.vibrate.checked = this.alarm.vibrate;
// Update the labels for any FormButton dropdowns that have
// changed, because setting <select>.value does not fire a change
// event.
for (var key in this.buttons) {
var button = this.buttons[key];
if (button instanceof FormButton) {
button.refresh();
}
}
location.hash = '#alarm-edit-panel';
},
initTimeSelect: function aev_initTimeSelect() {
// HTML5 <input type=time> expects 24-hour HH:MM format.
var hour = parseInt(this.alarm.hour, 10);
var minute = parseInt(this.alarm.minute, 10);
this.selects.time.value = (hour < 10 ? '0' : '') + hour +
':' + (minute < 10 ? '0' : '') + minute;
},
getTimeSelect: function aev_getTimeSelect() {
// HTML5 <input type=time> returns data in 24-hour HH:MM format.
var splitTime = this.selects.time.value.split(':');
return { hour: splitTime[0], minute: splitTime[1] };
},
initRepeatSelect: function aev_initRepeatSelect() {
this.buttons.repeat.value = this.alarm.repeat;
},
initSoundSelect: function aev_initSoundSelect() {
this.buttons.sound.value = this.alarm.sound;
},
getSoundSelect: function aev_getSoundSelect() {
return this.buttons.sound.value !== '0' && this.buttons.sound.value;
},
previewSound: function aev_previewSound() {
var ringtoneName = this.getSoundSelect();
this.ringtonePlayer.playRingtone(ringtoneName);
},
stopPreviewSound: function aev_stopPreviewSound() {
this.ringtonePlayer.pause();
},
initSnoozeSelect: function aev_initSnoozeSelect() {
this.buttons.snooze.value = this.alarm.snooze;
},
getSnoozeSelect: function aev_getSnoozeSelect() {
return this.buttons.snooze.value;
},
getRepeatSelect: function aev_getRepeatSelect() {
return this.buttons.repeat.value;
},
getAlarmVolumeValue: function() {
return parseFloat(this.inputs.volume.value);
},
save: function aev_save(callback) {
if (this.isSaving) {
// Ignore double-taps on the "Save" button. When this view gets
// refactored, we should opt for a more coherent way of managing
// UI state to avoid glitches like this.
return;
}
var alarm = this.alarm;
if (this.element.dataset.id && this.element.dataset.id !== '') {
alarm.id = parseInt(this.element.dataset.id, 10);
} else {
delete alarm.id;
}
alarm.label = this.inputs.name.value;
var time = this.getTimeSelect();
alarm.hour = time.hour;
alarm.minute = time.minute;
alarm.repeat = this.buttons.repeat.value;
alarm.sound = this.getSoundSelect();
alarm.vibrate = this.checkboxes.vibrate.checked;
alarm.snooze = parseInt(this.getSnoozeSelect(), 10);
AudioManager.setAlarmVolume(this.getAlarmVolumeValue());
this.isSaving = true;
alarm.schedule('normal').then(() => {
this.isSaving = false;
window.dispatchEvent(new CustomEvent('alarm-changed', {
detail: { alarm: alarm, showBanner: true }
}));
callback && callback(null, alarm);
});
},
delete: function aev_delete(callback) {
if (!this.alarm.id) {
setTimeout(callback.bind(null, new Error('no alarm id')), 0);
return;
}
this.alarm.delete().then(callback);
}
});
return AlarmEdit;
});

View file

@ -0,0 +1,69 @@
<gaia-header ignore-dir id="alarm-header" action="close">
<h1 class="new-alarm-title" data-l10n-id="newAlarm"></h1>
<h1 class="edit-alarm-title" data-l10n-id="editAlarm"></h1>
<button id="alarm-done" data-l10n-id="done"></button>
</gaia-header>
<ul id="edit-alarm" class="compact">
<li>
<input type="text" name="alarm.label" id="alarm-name" data-l10n-id="alarmName" placeholder="Alarm name" maxLength="50" dir="auto" />
</li>
<li>
<label data-l10n-id="time"></label>
<input id="time-select" type="time" />
</li>
<li>
<label data-l10n-id="repeat"></label>
<select id="repeat-select" multiple="true">
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
<option value="6"></option>
<option value="0"></option>
</select>
</li>
<li>
<label data-l10n-id="sound"></label>
<select id="sound-select">
<option value="0" data-l10n-id="noSound"></option>
<option value="ac_awake.opus" data-l10n-id="ac_awake_opus"></option>
<option value="ac_crystalize.opus" data-l10n-id="ac_crystalize_opus"></option>
<option value="ac_cycle.opus" data-l10n-id="ac_cycle_opus"></option>
<option value="ac_digicloud.opus" data-l10n-id="ac_digicloud_opus"></option>
<option value="ac_humming_waves.opus" data-l10n-id="ac_humming_waves_opus"></option>
<option value="ac_into_the_void.opus" data-l10n-id="ac_into_the_void_opus"></option>
<option value="ac_lightly.opus" data-l10n-id="ac_lightly_opus"></option>
<option value="ac_mobile.opus" data-l10n-id="ac_mobile_opus"></option>
<option value="ac_pinger.opus" data-l10n-id="ac_pinger_opus"></option>
<option value="ac_skip.opus" data-l10n-id="ac_skip_opus"></option>
<option value="ac_tri.opus" data-l10n-id="ac_tri_opus"></option>
<option value="ac_universal.opus" data-l10n-id="ac_universal_opus"></option>
</select>
</li>
<li>
<gaia-switch id="vibrate-checkbox" checked>
<label data-l10n-id="vibrate"></label>
</gaia-switch>
</li>
<li>
<label class="view-alarm-lbl snooze-lbl" data-l10n-id="snooze-label">Snooze</label>
<select id="snooze-select">
<option data-l10n-id="alarmMinutes" data-l10n-args='{"minutes": "5"}' value="5"></option>
<option data-l10n-id="alarmMinutes" data-l10n-args='{"minutes": "10"}' value="10"></option>
<option data-l10n-id="alarmMinutes" data-l10n-args='{"minutes": "15"}' value="15"></option>
<option data-l10n-id="alarmMinutes" data-l10n-args='{"minutes": "20"}' value="20"></option>
</select>
</li>
<li>
<label class="alarm-volume-lbl"
id="alarm-volume-lbl"
data-l10n-id="alarm-volume-label"></label>
<!-- 0.0625 is 1/16th, which matches the step size in System Settings. -->
<input id="alarm-volume-input" step="0.0625" min="0" value="1" max="1"
type="range">
</li>
<li id="delete-menu">
<button id="alarm-delete" class="danger full" data-l10n-id="delete"></button>
</li>
</ul>

View file

@ -0,0 +1,4 @@
<div class="lap-name">
</div>
<div class="lap-duration">
</div>

View file

@ -0,0 +1,324 @@
define(function(require) {
'use strict';
/* global IntlHelper */
var Panel = require('panel');
var Stopwatch = require('stopwatch');
var Template = require('template');
var html = require('text!panels/stopwatch/panel.html');
var lapHtml = require('text!panels/stopwatch/list_item.html');
var priv = new WeakMap();
// This value is chosen such that the only reason you'll hit it is
// because you're super bored, and small enough that phones won't
// puke when displaying this many rows in the lap list.
var MAX_STOPWATCH_LAPS = 1000;
IntlHelper.define('timer-msS', 'mozduration', {
minUnit: 'millisecond',
maxUnit: 'minute'
});
IntlHelper.define('lap-number', 'number', {
style: 'decimal',
useGrouping: false
});
/**
* Stopwatch.Panel
*
* Construct a UI panel for the Stopwatch panel.
*
* @return {Stopwatch.Panel} Stopwatch.Panel object.
*
*/
Stopwatch.Panel = function(element) {
Panel.apply(this, arguments);
this.nodes = {};
this.lapTemplate = Template(lapHtml);
this.interval = null;
this.screenWakeLock = null;
// Store maxLaps as a dataset attribute for easy access in tests.
this.element.dataset.maxLaps = MAX_STOPWATCH_LAPS;
this.element.innerHTML = html;
// Gather elements
[
'start', 'pause', 'resume',
'lap', 'reset', 'time',
'lap-list', 'laps'
].forEach(function(sel) {
this.nodes[sel] = this.element.querySelector('.stopwatch-' + sel);
}, this);
// Bind click events
[
'start', 'pause', 'resume', 'lap', 'reset'
].forEach(function(action) {
var e = this.nodes[action];
if (priv.has(e)) {
priv.delete(e);
}
priv.set(e, {
action: action == 'resume' ? 'start' : action
});
e.addEventListener('click', this);
}, this);
element.addEventListener(
'panel-visibilitychange', this.onvisibilitychange.bind(this));
IntlHelper.observe('timer-msS', this.updateTimeStrings.bind(this));
this.setStopwatch(new Stopwatch());
};
Stopwatch.Panel.prototype = Object.create(Panel.prototype);
Stopwatch.Panel.prototype.updateTimeStrings = function() {
IntlHelper.get('timer-msS').then(formatter => {
var stopwatch = priv.get(this).stopwatch;
var e = stopwatch.getElapsedTime();
var time = formatter.format(e);
this.nodes.time.textContent = time;
var node = this.nodes.laps;
var laps = stopwatch.getLaps();
laps.push(stopwatch.nextLap());
var lapnodes = node.querySelectorAll('li.lap-cell');
var lapFormatter = IntlHelper.get('lap-number');
for (var i = 0; i < laps.length; i++) {
var lapnum = laps.length - 1 - i;
lapnodes[i].querySelector('.lap-duration').textContent =
formatter.format(laps[lapnum].duration);
lapnodes[i].querySelector('.lap-name').setAttribute('data-l10n-args',
JSON.stringify({ n: lapFormatter.format(lapnum + 1) }));
}
});
};
Stopwatch.Panel.prototype.update = function() {
var swp = priv.get(this);
var e = swp.stopwatch.getElapsedTime();
return IntlHelper.get('timer-msS').then(formatter => {
var time = formatter.format(e);
this.nodes.time.textContent = time;
this.nodes.time.classList.toggle(
'over-100-minutes', e >= 1000 * 60 * 100);
this.activeLap(false);
});
};
Stopwatch.Panel.prototype.showButtons = function() {
Array.prototype.forEach.call(arguments, function(a) {
this.nodes[a].classList.remove('hidden');
}, this);
};
Stopwatch.Panel.prototype.hideButtons = function() {
Array.prototype.forEach.call(arguments, function(a) {
this.nodes[a].classList.add('hidden');
}, this);
};
Stopwatch.Panel.prototype.setState = function(state) {
switch (state) {
case Stopwatch.RUNNING:
this.onstart();
break;
case Stopwatch.PAUSED:
this.onpause();
break;
case Stopwatch.RESET:
this.onreset();
break;
}
return this.update();
};
Stopwatch.Panel.prototype.setStopwatch = function(stopwatch) {
priv.set(this, {
stopwatch: stopwatch
});
//Clear any existing lap indicators and make new ones
var lapsUl = this.nodes.laps;
lapsUl.textContent = '';
var laps = stopwatch.getLaps();
for (var i = 0; i < laps.length; i++) {
this.onlap(laps[i]);
}
this.checkLapButton();
return this.setState(stopwatch.getState());
};
Stopwatch.Panel.prototype.onvisibilitychange = function(evt) {
var stopwatch = priv.get(this).stopwatch;
if (evt.detail.isVisible) {
this.setState(stopwatch.getState());
}
if (this.screenWakeLock) {
this.screenWakeLock.unlock();
this.screenWakeLock = null;
}
};
Stopwatch.Panel.prototype.checkLapButton = function() {
var swp = priv.get(this);
var maxLaps = parseInt(this.element.dataset.maxLaps, 10);
// As the Stopwatch doesn't include the current "lap", we must
// subtract one from maxLaps when deciding whether or not we can
// add a lap. Using these calculations, if maxLaps is 10, the last
// lap visible in the UI will be "Lap 10". Additionally, this
// button can only be shown if the "pause" button is also visible,
// as it must respect the state of the other buttons.
var canAddLaps = (swp.stopwatch.getLaps().length < maxLaps - 1) &&
!this.nodes.pause.classList.contains('hidden');
this.nodes.lap.classList.toggle('hidden', !canAddLaps);
};
Stopwatch.Panel.prototype.handleEvent = function(event) {
if (event.type == 'animationend') {
Panel.prototype.handleEvent.apply(this, arguments);
return;
}
var swp = priv.get(this);
var button = priv.get(event.target);
if (swp.stopwatch && swp.stopwatch[button.action]) {
try {
// call action on stopwatch
var val = swp.stopwatch[button.action]();
// call panel handler
this['on' + button.action](val);
} catch (err) {
if (err instanceof Stopwatch.MaxLapsException) {
// do nothing
} else {
throw err;
}
}
this.checkLapButton();
}
};
Stopwatch.Panel.prototype.onstart = function() {
var tickfn = (function() {
this.update();
this.tick = requestAnimationFrame(tickfn);
}).bind(this);
tickfn();
//bug#983393
var laps = priv.get(this).stopwatch.getLaps().length+1;
var maxLaps = parseInt(this.element.dataset.maxLaps, 10);
if (laps<maxLaps) {
this.showButtons('pause', 'lap');
this.hideButtons('start', 'resume', 'reset');
}
this.screenWakeLock = navigator.requestWakeLock('screen');
};
Stopwatch.Panel.prototype.onpause = function() {
cancelAnimationFrame(this.tick);
this.update();
this.nodes.reset.removeAttribute('disabled');
this.showButtons('resume', 'reset');
this.hideButtons('pause', 'start', 'lap');
if (this.screenWakeLock) {
this.screenWakeLock.unlock();
this.screenWakeLock = null;
}
};
Stopwatch.Panel.prototype.onresume = function() {
this.onstart();
};
function createLapDom(num, time) {
/* jshint validthis:true */
var li = document.createElement('li');
li.setAttribute('class', 'lap-cell');
li.innerHTML = this.lapTemplate.interpolate();
IntlHelper.get('timer-msS').then(formatter => {
li.querySelector('.lap-duration').textContent = formatter.format(time);
});
var lapFormatter = IntlHelper.get('lap-number');
document.l10n.setAttributes(
li.querySelector('.lap-name'),
'lap-number',
{ n: lapFormatter.format(num) }
);
return li;
}
function updateLapDom(num, time, li) {
IntlHelper.get('timer-msS').then(formatter => {
li.querySelector('.lap-duration').textContent = formatter.format(time);
});
var lapFormatter = IntlHelper.get('lap-number');
document.l10n.setAttributes(
li.querySelector('.lap-name'),
'lap-number',
{ n: lapFormatter.format(num) }
);
return li;
}
Stopwatch.Panel.prototype.activeLap = function(force) {
var stopwatch = priv.get(this).stopwatch;
var num = stopwatch.getLaps().length + 1;
if (num === 1 && !force) {
return;
}
var node = this.nodes.laps;
var lapnodes = node.querySelectorAll('li.lap-cell');
var time = stopwatch.nextLap().duration;
if (lapnodes.length === 0) {
node.appendChild(createLapDom.call(this, num, time));
} else {
updateLapDom.call(this, num, time, lapnodes[0]);
}
};
Stopwatch.Panel.prototype.onlap = function(val) {
var stopwatch = priv.get(this).stopwatch;
var node = this.nodes.laps;
var laps = stopwatch.getLaps();
var num = laps.length;
this.activeLap(true);
var li = createLapDom.call(this, num, val ? val.duration : 0);
if (laps.length > 1) {
var lapnodes = node.querySelectorAll('li.lap-cell');
node.insertBefore(li, lapnodes[1]);
} else {
node.appendChild(li);
}
};
Stopwatch.Panel.prototype.onreset = function() {
cancelAnimationFrame(this.tick);
this.showButtons('start', 'reset');
this.hideButtons('pause', 'resume', 'lap');
this.nodes.reset.setAttribute('disabled', 'true');
// clear lap list
this.nodes.laps.textContent = '';
this.update();
};
return Stopwatch.Panel;
});

View file

@ -0,0 +1,12 @@
<div class="stopwatch-time"></div>
<menu id="stopwatch-controls">
<button class="stopwatch-start edit-button" data-l10n-id="start"></button>
<button class="stopwatch-pause edit-button recommend" data-l10n-id="pause"></button>
<button class="stopwatch-resume edit-button" data-l10n-id="resume"></button>
<button class="stopwatch-reset edit-button danger" data-l10n-id="reset"></button>
<button class="stopwatch-lap edit-button" data-l10n-id="lap"></button>
</menu>
<div class="stopwatch-lap-list">
<!-- list of exisiting alarms, to be populated -->
<ul class="stopwatch-laps"></ul>
</div>

View file

@ -0,0 +1,318 @@
define(function(require) {
'use strict';
/* global IntlHelper */
var Panel = require('panel');
var Picker = require('picker/picker');
var View = require('view');
var Utils = require('utils');
var Timer = require('timer');
var Sounds = require('sounds');
var FormButton = require('form_button');
var html = require('text!panels/timer/panel.html');
var AudioManager = require('audio_manager');
var asyncStorage = require('shared/js/async_storage');
var priv = new WeakMap();
function timeFromPicker(value) {
var hm, ms;
hm = value.split(':');
ms = Utils.dateMath.toMS({
hours: hm[0],
minutes: hm[1]
});
return ms;
}
IntlHelper.define('timer-hms', 'mozduration', {
minUnit: 'second',
maxUnit: 'hour'
});
/**
* Timer.Panel
*
* Construct a UI panel for the Timer panel.
*
* @return {Timer.Panel} Timer.Panel object.
*
*/
Timer.Panel = function(element) {
Panel.apply(this, arguments);
element.innerHTML = html;
this.timer = null;
this.nodes = {};
this.picker = new Picker({
element: this.element.querySelector('#time-picker'),
pickers: {
hours: {
range: [0, 23],
l10nId: 'nSpinnerHours'
},
minutes: {
range: [0, 59],
isPadded: true,
l10nId: 'nSpinnerMinutes'
}
}
});
this.ringtonePlayer = AudioManager.createAudioPlayer();
// Gather elements
[
'create', 'cancel', 'dialog', 'pause', 'start', 'sound', 'time', 'vibrate',
'plus'
].forEach(function(id) {
this.nodes[id] = this.element.querySelector('#timer-' + id);
}, this);
// Bind click events
[
'create', 'cancel', 'pause', 'start', 'plus'
].forEach(function(action) {
var element = this.nodes[action];
if (priv.has(element)) {
priv.delete(element);
}
priv.set(element, {
action: action,
panel: this
});
element.addEventListener('click', this.onclick.bind(this), false);
}, this);
var sound = this.nodes.sound;
sound.addEventListener('blur', this.pauseAlarm.bind(this), false);
sound.addEventListener('change', this.previewAlarm.bind(this), false);
var soundMenuConfig = {
id: 'timer-sound-menu',
formatLabel: Sounds.formatLabel
};
this.soundButton = new FormButton(sound, soundMenuConfig);
this.soundButton.refresh();
element.addEventListener('panel-visibilitychange',
this.onvisibilitychange.bind(this));
// The start button is disable by default (picker at 00:00 by default)
this.nodes.create.setAttribute('disabled', 'true');
var create = this.nodes.create;
var picker = this.picker;
var enableButton = () => {
if(timeFromPicker(picker.value) === 0) {
create.setAttribute('disabled', 'true');
} else {
create.removeAttribute('disabled');
}
this.saveCurrentSettingsAsDefaults();
};
// The start button is enable if the value of the timer is not 00:00
picker.nodes.minutes.addEventListener('transitionend', enableButton);
picker.nodes.hours.addEventListener('transitionend', enableButton);
this.restoreDefaults().then(() => {
enableButton(); // Update the enabled state to match the new defaults.
});
this.nodes.vibrate.addEventListener(
'change', (e) => this.saveCurrentSettingsAsDefaults());
this.nodes.sound.addEventListener(
'change', (e) => this.saveCurrentSettingsAsDefaults());
Timer.singleton(function(err, timer) {
this.timer = timer;
var onTimerEvent = this.onTimerEvent.bind(this);
window.addEventListener('timer-start', onTimerEvent);
window.addEventListener('timer-pause', onTimerEvent);
window.addEventListener('timer-tick', onTimerEvent);
window.addEventListener('timer-end', onTimerEvent);
IntlHelper.observe('timer-hms', this.update.bind(this));
if (this.visible) {
// If the timer panel already became visible before we fetched
// the timer, we must update the display to show the proper
// timer status.
this.onvisibilitychange({ detail: { isVisible: true } });
}
}.bind(this));
};
Timer.Panel.prototype = Object.create(Panel.prototype);
Timer.Panel.prototype.restoreDefaults = function() {
return new Promise((resolve) => {
asyncStorage.getItem('timer-defaults', (values) => {
values = values || {};
if (values.sound != null) {
this.soundButton.value = values.sound;
}
if (values.time != null) {
this.picker.value = values.time;
}
if (values.vibrate != null) {
this.nodes.vibrate.checked = values.vibrate;
}
resolve();
});
});
};
Timer.Panel.prototype.saveCurrentSettingsAsDefaults = function() {
asyncStorage.setItem('timer-defaults', {
sound: this.soundButton.value,
time: this.picker.value,
vibrate: this.nodes.vibrate.checked
});
};
Timer.Panel.prototype.onvisibilitychange = function(evt) {
var isVisible = evt.detail.isVisible;
var timer = this.timer;
if (isVisible) {
// No active timer, or timer has expired...
// - Show the new timer dialog
if (timer === null || timer.state === Timer.INITIAL) {
this.showDialog();
} else {
// Active timer exists, might be started by an external app, so we need
// to make sure dialog is hidden and buttons/time are on correct state
this.toggleButtons();
this.hideDialog();
this.update();
}
} else {
this.pauseAlarm();
}
};
Timer.Panel.prototype._toggleDialog = function(isVisible = true) {
View.instance(this.nodes.dialog).visible = isVisible;
return this;
};
Timer.Panel.prototype.showDialog = function() {
return this._toggleDialog(true);
};
Timer.Panel.prototype.hideDialog = function() {
return this._toggleDialog(false);
};
Timer.Panel.prototype.onTimerEvent = function(event) {
switch (event.type) {
case 'timer-start':
// new timer dialog should always be hidden during timer-start (even
// if started from external app)
this.hideDialog();
/* falls through */
case 'timer-pause':
this.toggleButtons();
/* falls through */
case 'timer-tick':
this.update();
break;
case 'timer-end':
this.showDialog();
break;
}
};
/**
* Given milliseconds, render the time as a rounded-to-seconds
* countdown.
*/
Timer.Panel.prototype.update = function() {
return IntlHelper.get('timer-hms').then(formatter => {
var remaining = this.timer.remaining;
var newText = formatter.format(remaining);
// Use localized caching here to prevent unnecessary DOM repaints.
if (this._cachedTimerText !== newText) {
this.nodes.time.textContent = this._cachedTimerText = newText;
}
});
};
Timer.Panel.prototype.toggleButtons = function() {
var nodes = this.nodes;
var started = this.timer.state === Timer.STARTED;
nodes.pause.classList.toggle('hidden', !started);
nodes.start.classList.toggle('hidden', started);
};
/**
* previewAlarm Plays the currently selected alarm value on a loop.
*/
Timer.Panel.prototype.previewAlarm = function() {
var ringtoneName = Utils.getSelectedValueByIndex(this.nodes.sound);
this.ringtonePlayer.playRingtone(ringtoneName);
};
/**
* pauseAlarm stops the alarm if it is playing
*/
Timer.Panel.prototype.pauseAlarm = function() {
this.ringtonePlayer.pause();
};
/**
* handleEvent Handler for all panel bound UI events.
* (`this` context object is not Timer.Panel)
*
* @param {Event} event The Event object.
*/
Timer.Panel.prototype.onclick = function(event) {
var meta = priv.get(event.target);
var panel = meta.panel;
var nodes = panel.nodes;
var timer = panel.timer;
var time;
if (meta.action === 'create') {
time = timeFromPicker(panel.picker.value);
if (!time) {
return;
}
timer.duration = time;
timer.sound = nodes.sound.value;
timer.vibrate = nodes.vibrate.checked;
timer.start();
// Hide the new timer dialog
panel.hideDialog();
} else if (meta.action === 'plus') {
timer.plus(+event.target.dataset.value);
// need to update immediately cause timer might be paused during click
panel.update();
} else {
// meta.action => timer[meta.action]()
//
// ie.
//
// if start => timer.start()
// if pause => timer.pause()
// if cancel => timer.cancel()
//
timer[meta.action]();
}
timer.commit();
};
return Timer.Panel;
});

View file

@ -0,0 +1,78 @@
<div id="timer-dialog" class="panel skin-dark">
<div id="value-selector">
<!-- Time Picker -->
<div id="time-picker" data-type="picker">
<div class="picker-container">
<div class="picker-labels">
<div class="picker-label" id="hours" data-l10n-id="hours">
</div>
<div class="picker-label" id="minutes" data-l10n-id="minutes">
</div>
</div>
<div class="picker-bar-background">
</div>
<div class="picker-hours-wrapper" role="spinbutton" aria-labelledby="hours">
<div class="picker-hours">
</div>
</div>
<div class="picker-minutes-wrapper" role="spinbutton" aria-labelledby="minutes">
<div class="picker-minutes">
</div>
</div>
<div class="value-indicator">
<div class="value-indicator-colon" aria-hidden="true" data-l10n-id="hourMinutesSeparator"></div>
</div>
</div>
</div>
<ul id="timer-edit" class="compact">
<li>
<button id="timer-create" class="recommend" data-l10n-id="start">
</button>
</li>
<li>
<label data-l10n-id="sound"></label>
<select id="timer-sound">
<option value="0" data-l10n-id="noSound"></option>
<option value="ac_awake.opus" data-l10n-id="ac_awake_opus" selected></option>
<option value="ac_crystalize.opus" data-l10n-id="ac_crystalize_opus"></option>
<option value="ac_cycle.opus" data-l10n-id="ac_cycle_opus"></option>
<option value="ac_digicloud.opus" data-l10n-id="ac_digicloud_opus"></option>
<option value="ac_humming_waves.opus" data-l10n-id="ac_humming_waves_opus"></option>
<option value="ac_into_the_void.opus" data-l10n-id="ac_into_the_void_opus"></option>
<option value="ac_lightly.opus" data-l10n-id="ac_lightly_opus"></option>
<option value="ac_mobile.opus" data-l10n-id="ac_mobile_opus"></option>
<option value="ac_pinger.opus" data-l10n-id="ac_pinger_opus"></option>
<option value="ac_skip.opus" data-l10n-id="ac_skip_opus"></option>
<option value="ac_tri.opus" data-l10n-id="ac_tri_opus"></option>
<option value="ac_universal.opus" data-l10n-id="ac_universal_opus"></option>
</select>
</li>
<li>
<gaia-switch id="timer-vibrate" checked>
<label data-l10n-id="vibrate"></label>
</gaia-switch>
</li>
</ul>
</div>
</div>
<div id="timer-active">
<div id="timer-time-display">
<div id="timer-time"></div>
<menu id="timer-controls">
<button id="timer-start" class="edit-button" data-l10n-id="resume">
</button>
<button id="timer-pause" class="edit-button" data-l10n-id="pause">
</button>
<button id="timer-cancel" class="danger" data-l10n-id="cancel">
</button>
</menu>
</div>
<div id="timer-plus-time">
<button id="timer-plus" data-value="60" data-l10n-id="plusMinute">
</button>
</div>
</div>

View file

@ -0,0 +1,91 @@
define(function(require) {
'use strict';
var Spinner = require('picker/spinner');
/**
* Picker
*
* Create an inline "picker", comprised of n "spinners".
*
* @param {Object} setup An object containing setup data.
* - element, The container element.
* - pickers, an object whose properties
* correspond to a Picker that will
* be created.
*
*
* new Picker({
* element: 'time-picker',
* pickers: {
* hours: {
* range: [0, 24],
* l10nId: 'nSpinnerHours'
* },
* minutes: {
* range: [0, 60],
* isPadded: true,
* l10nId: 'nSpinnerMinutes'
* }
* }
* });
*
*/
function Picker(setup) {
this.nodes = {};
this.spinners = {};
this.pickers = Object.keys(setup.pickers);
this.pickers.forEach(function(picker) {
var values = [];
var range = setup.pickers[picker].range;
var isPadded = setup.pickers[picker].isPadded || false;
this.nodes[picker] = setup.element.querySelector('.picker-' + picker);
for (var i = range[0]; i <= range[1]; i++) {
values.push(i);
}
this.spinners[picker] = new Spinner({
element: this.nodes[picker],
values: values,
isPadded: isPadded,
l10nId: setup.pickers[picker].l10nId
});
}, this);
}
Picker.prototype = {
get value() {
// Protect against uninitialized [[Get]] access
if (typeof this.pickers === 'undefined') {
return null;
}
return this.pickers.map(function(picker) {
return this.spinners[picker].value;
}, this).join(':');
},
set value(value) {
// Protect against uninitialized [[Set]] access
if (typeof this.pickers === 'undefined') {
return null;
}
value.split(':').forEach(function(value, i) {
this.spinners[this.pickers[i]].value = value;
}, this);
return this.value;
},
reset: function() {
this.pickers.forEach(function(picker) {
this.spinners[picker].reset();
}, this);
}
};
return Picker;
});

View file

@ -0,0 +1 @@
<div class="picker-unit" data-value="${value}">${text}</div>

View file

@ -0,0 +1,289 @@
define(function(require) {
'use strict';
/* global IntlHelper */
var Template = require('template');
var GestureDetector = require('shared/js/gesture_detector');
var spinnerHtml = require('text!picker/spinner.html');
// units covered per millisecond threshold to kick off inertia
var SPEED_THRESHOLD = 0.01;
// max units covered by inertia
var INERTIA_MAXIMUM = 15;
// not the same as animation duration, this accounts for "slowing down",
// measured in miliseconds
var INERTIA_DURATION = 300;
// number of milliseconds after last motion without leting go
// we will select whatever is being "hovered" and cancel momentum
var DRAGGING_TIMEOUT = 200;
IntlHelper.define('digit-padding', 'number', {
style: 'decimal',
useGrouping: false,
minimumIntegerDigits: 2
});
function calculateSpeed(previous, current) {
/* jshint validthis:true */
var motion = (previous.y - current.y) / this.unitHeight;
var delta = current.time - previous.time;
var speed = motion / delta;
return parseFloat(speed.toFixed(4)) || 0;
}
function Touch(touch = {}) {
this.x = touch.x || 0;
this.y = touch.y || 0;
this.time = touch.time || 0;
}
/**
* Spinner
*
* Create a select list spinner.
*
* @param {Object} setup An object containing setup data.
* - element, a DOM element to create
* the spinner with.
* - values, an array of values to
* populate the spinner with.
*
*
* new Spinner({
* element: this.selector[picker],
* values: values
* });
*
*
*/
function Spinner(setup = {}) {
// define some non writable properties
Object.defineProperties(this, {
value: {
get: function() {
return this.values[this.index];
},
set: function(value) {
this.select(this.values.indexOf(parseInt(value)));
}
},
unitHeight: {
get: function() {
return this.element.children[0].clientHeight;
}
},
space: {
get: function() {
return this.unitHeight * this.length;
}
},
container: {
value: setup.element.parentNode
},
element: {
value: setup.element
},
values: {
value: setup.values
},
length: {
value: setup.values.length
},
textValues: {
value: setup.textValues || setup.values
},
isPadded: {
value: setup.isPadded || false
},
l10nId: {
value: setup.l10nId || null
}
});
this.template = new Template(spinnerHtml);
this.top = 0;
this.index = 0;
this.previous = new Touch();
this.current = new Touch();
this.timeout = null;
var html = '';
var formatter = IntlHelper.get(
this.isPadded ? 'digit-padding' : 'digit-nopadding');
for (var i = 0; i < this.length; i++) {
html += this.template.interpolate({
value: this.values[i].toString(),
text: formatter.format(this.values[i])
});
}
this.element.innerHTML = html;
this.container.addEventListener('touchstart', this, false);
this.container.addEventListener('pan', this, false);
this.container.addEventListener('swipe', this, false);
this.container.addEventListener('keypress', this, false);
this.container.setAttribute('aria-valuemax', this.upper);
this.container.setAttribute('aria-valuemin', this.lower);
this.container.setAttribute('aria-valuenow', this.index);
if (this.l10nId) {
document.l10n.setAttributes(
this.container,
this.l10nId,
{n: this.values[i]}
);
}
IntlHelper.observe(['digit-padding', 'digit-nopadding'],
this.resetValues.bind(this));
this.reset();
new GestureDetector(this.container).startDetecting();
}
Spinner.prototype.resetValues = function() {
var formatter = IntlHelper.get(
this.isPadded ? 'digit-padding' : 'digit-nopadding');
var units = [...this.container.querySelectorAll('.picker-unit')];
units.forEach(unit => {
var value = parseInt(unit.dataset.value);
unit.textContent = formatter.format(value);
});
};
Spinner.prototype.reset = function() {
this.index = 0;
this.top = 0;
this.update();
};
Spinner.prototype.update = function() {
this.container.setAttribute('aria-valuenow', this.index);
this.container.setAttribute('aria-valuetext', this.textValues[this.index]);
this.element.style.transform = 'translateY(' + this.top + 'px)';
};
Spinner.prototype.select = function(index) {
index = Math.round(index);
if (index < 0) {
index = 0;
}
if (index > this.length - 1) {
index = this.length - 1;
}
if (index !== this.index) {
this.index = index;
}
this.top = -this.index * this.unitHeight;
this.update();
return index;
};
Spinner.prototype.handleEvent = function(event) {
this['on' + event.type](event);
};
Spinner.prototype.stopInteraction = function() {
this.element.classList.add('animation-on');
clearTimeout(this.timeout);
this.select(this.index);
this.speed = 0;
};
/**
* ontouchstart - prevent default action (stops scrolling)
*/
Spinner.prototype.ontouchstart = function(event) {
event.preventDefault();
};
Spinner.prototype.onpan = function(event) {
event.stopPropagation();
var position = event.detail.position;
var diff;
// If this is the first pan event after a swipe...
if (this.element.classList.contains('animation-on')) {
this.element.classList.remove('animation-on');
this.select(this.index);
this.previous.y = position.clientY;
this.previous.time = position.timeStamp;
return;
}
this.current.y = position.clientY;
this.current.time = position.timeStamp;
this.speed = calculateSpeed.call(this, this.previous, this.current);
diff = this.current.y - this.previous.y;
this.top = this.top + diff;
if (this.top > 0) {
this.top = 0;
}
if (this.top < -this.space) {
this.top = -this.space;
}
this.index = Math.round(Math.abs(this.top) / this.unitHeight);
this.update();
clearTimeout(this.timeout);
var stopInteraction = this.stopInteraction.bind(this);
this.timeout = setTimeout(stopInteraction, DRAGGING_TIMEOUT);
this.previous.y = this.current.y;
this.previous.time = this.current.time;
};
Spinner.prototype.onswipe = function(event) {
event.stopPropagation();
// Add momentum if speed is higher than a given threshold.
var direction = this.speed > 0 ? 1 : -1;
var speed = this.speed / direction;
if (speed >= SPEED_THRESHOLD) {
this.index += Math.round(
Math.min(speed * INERTIA_DURATION, INERTIA_MAXIMUM) * direction
);
}
this.stopInteraction();
};
Spinner.prototype.onkeypress = function(event) {
this.element.classList.add('animation-on');
if (event.keyCode == KeyEvent.DOM_VK_DOWN) {
this.select(this.index - 1);
} else {
this.select(this.index + 1);
}
};
return Spinner;
});

View file

@ -0,0 +1,26 @@
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: 7,
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'
}
}
});

274
gaia/apps/clock/js/ring_view.js Executable file
View file

@ -0,0 +1,274 @@
define(function(require) {
'use strict';
var Utils = require('utils');
var AudioManager = require('audio_manager');
var PostMessageProxy = require('./panels/alarm/post_message_proxy');
var ChildWindowManager = require('./panels/alarm/child_window_manager');
/**
* RingView displays the alert screen when a timer or alarm fires.
* This screen may receive multiple different alarm/timer events;
* ideally it would display all events that fire, but currently it
* only shows the most recent event.
*/
function whenDocumentReady() {
return new Promise(function(resolve, reject) {
if (document.readyState === 'complete') {
resolve();
} else {
const listener = function() {
if (document.readyState === 'complete') {
resolve();
document.removeEventListener('readystatechange', listener);
}
};
document.addEventListener('readystatechange', listener);
}
});
}
function RingView() {
this.alerts = [];
this.ringtonePlayer = AudioManager.createAudioPlayer();
this.snoozeButton.addEventListener('click', this.onClickSnooze.bind(this));
this.closeButton.addEventListener('click', this.onClickClose.bind(this));
window.addEventListener('beforeunload', this.onBeforeUnload.bind(this));
window.addEventListener('timeformatchange', this.refreshDisplay.bind(this));
this.activeAlarm = PostMessageProxy.create(window.opener, 'activeAlarm');
PostMessageProxy.receive('ringView', this);
if (window.opener) {
// We have to wait for the document.readyState to be complete before
// we fireReady. Otherwise, when the screen is off, it doesn't get
// turned back on at all
whenDocumentReady().then(() => {
ChildWindowManager.fireReady();
});
}
}
// The time, in milliseconds, to keep the screen awake while showing
// an alarm. After this time, the screen shuts off and the alarm
// silences itself.
const WAKE_DURATION = 10 * 60 * 1000;
RingView.prototype = {
/**
* Fire the notification for an alarm or timer.
*
* Presently, we only display one notification at a time, and the
* _most recent_ one at that. Each notification gets its own wake
* lock, ensuring that the screen will remain on for WAKE_DURATION.
*
* @param {string} alert.type 'alarm' or 'timer'
* @param {string} alert.label Label to display (optional).
* @param {string} alert.sound Filename of a sound to play (optional).
* @param {boolean} alert.vibrate True if the alert should vibrate.
*/
addAlert: function(alert) {
// If we previously had an alert visible, this one is
// going to override it, as though the previous alert was
// dismissed.
if (this.alerts.length) {
var oldAlert = this.alerts.shift();
oldAlert.releaseScreenWakeLock();
}
alert.releaseScreenWakeLock = function() { };
if (this._silenceTimeout) {
clearTimeout(this._silenceTimeout);
this._silenceTimeout = null;
}
this._silenceTimeout = setTimeout(() => {
this.silence();
alert.releaseScreenWakeLock();
}, WAKE_DURATION);
// Insert this alert at the front of the stack, so that it
// overrides any previous alert that was being displayed.
this.alerts.unshift(alert);
this.refreshDisplay();
// Acquire a CPU wake lock so that we don't fall asleep waiting
// for the document to become visible. We'll only try to hold a
// lock for a few seconds as we wait for the document to become
// visible, out of an abundance of caution.
Utils.safeWakeLock({ type: 'cpu', timeoutMs: 5000 }, (releaseCpu) => {
// When the document is visible, acquire a screen wake lock so
// that we can safely display the alert.
this.whenVisible(() => {
Utils.safeWakeLock({ type: 'screen', timeoutMs: WAKE_DURATION },
(releaseScreenWakeLock) => {
// Once we have acquired the screen wake lock, we can
// release the CPU lock.
releaseCpu();
// Save off the screen wake lock for when we dismiss the
// alert; all alarms each have their own screen wake lock.
alert.releaseScreenWakeLock = releaseScreenWakeLock;
});
});
});
},
/**
* Update the display to show the currently active alert. If there
* are a stack of alerts pending, only the most recent alert is
* shown, as added to this.alerts by this.addAlert().
*/
refreshDisplay: function() {
// First, silence any existing sound or vibration. If a previous
// alarm was going off, this alarm may have different settings.
// The new alarm will replace any prior settings.
this.silence();
var alert = this.alerts[0];
// Set the label (blank or falsey becomes a default string).
if (alert.label) {
this.ringLabel.removeAttribute('data-l10n-id');
this.ringLabel.textContent = alert.label;
} else {
this.ringLabel.setAttribute('data-l10n-id',
alert.type === 'alarm' ? 'alarm' : 'timer');
}
// Display the proper screen widgets.
this.ringDisplay.dataset.ringType = alert.type;
var displayedTime;
if (alert.type === 'alarm') {
// Display the time as specified with "hour" and "minute". The timestamp
// stored by mozAlarms does not move when the timezone changes, so we
// cannot just use the value stored in '.time'. See
// https://bugzilla.mozilla.org/show_bug.cgi?id=1178208#c4
displayedTime = new Date();
displayedTime.setHours(alert.hour);
displayedTime.setMinutes(alert.minute);
} else {
// XXX: Timers may be affected by timezone changes just like alarms, but
// the fix here is not as straightforward, since we cannot take the
// same shortcut as above. Since timers are rarely used across timezone
// changes (unlike alarms), I'm punting on this for now.
displayedTime = alert.time;
}
// Set the time to display.
this.time.innerHTML = Utils.getLocalizedTimeText(displayedTime);
if (alert.sound) {
this.ringtonePlayer.playRingtone(alert.sound);
}
// Vibrate if we want to shakey shakey.
if (alert.vibrate && ('vibrate' in navigator)) {
clearInterval(this.vibrateInterval);
var vibrateOnce = function() {
navigator.vibrate([1000]);
};
this.vibrateInterval = setInterval(vibrateOnce, 2000);
vibrateOnce();
}
document.documentElement.classList.add('ready');
// If the window has been hidden, show the window.
if (document.hidden) {
window.focus();
}
},
/**
* Stop all sounds and vibration immediately.
*/
silence: function() {
// Stop the alert sound, if one was playin'.
this.ringtonePlayer.pause();
// Stop vibrating, if we were shakin'.
clearInterval(this.vibrateInterval);
this.vibrateInterval = null;
},
/**
* Clean up any state when we close this alert window. This includes
* silencing the alarm and releasing any locks we have acquired.
*/
onBeforeUnload: function(evt) {
// Clean up any wake locks we still have.
while (this.alerts.length) {
var alert = this.alerts.shift();
alert.releaseScreenWakeLock();
}
this.silence();
},
/**
* Snooze the current alarm. (The snooze button is only visible for
* alarms, not timers. Alarms have an ID; timers do not.)
*/
onClickSnooze: function(evt) {
var alert = this.alerts[0];
this.activeAlarm.snoozeAlarm(alert.id).then(() => window.close()).catch(console.error);
},
/**
* Close this window, notifying ActiveAlarm, which will pop the user
* back to the appropriate location if they are still using the
* Clock app.
*/
onClickClose: function(evt) {
var alert = this.alerts[0];
this.activeAlarm.close(alert.type, alert.id).then(() => window.close()).catch(console.error);
},
/**
* Call the callback when `document.hidden` is false. Due to a bug
* in the B2G Browser API <https://bugzil.la/810431>, the window may
* not be immediately visible, particularly if the screen is off.
* The recommended workaround for that bug was to use setTimeout. If
* the page is still hidden after that, we listen for
* `visibilitychange`. When that bug has some action, we should
* revisit how much of this method is needed.
*/
whenVisible: function(cb) {
if (!document.hidden) {
cb();
} else {
setTimeout(() => {
if (!document.hidden) {
cb();
} else {
var listener = function(e) {
if (!document.hidden) {
document.removeEventListener('visibilitychange', listener);
cb();
}
};
document.addEventListener('visibilitychange', listener);
}
});
}
}
};
Utils.extendWithDomGetters(RingView.prototype, {
time: '#ring-clock-time',
ringLabel: '#ring-label',
snoozeButton: '#ring-button-snooze',
closeButton: '#ring-button-stop',
ringDisplay: '.ring-display'
});
return RingView;
});

43
gaia/apps/clock/js/sounds.js Executable file
View file

@ -0,0 +1,43 @@
define(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];
};
});

10
gaia/apps/clock/js/startup.js Executable file
View file

@ -0,0 +1,10 @@
'use strict';
define('startup_init', function(require) {
var App = require('app');
App.init();
});
require(['require_config'], function() {
requirejs(['startup_init']);
});

179
gaia/apps/clock/js/stopwatch.js Executable file
View file

@ -0,0 +1,179 @@
define(function() {
'use strict';
var priv = new WeakMap();
function Defaults() {
this.startTime = 0;
this.totalElapsed = 0;
this.state = Stopwatch.RESET;
this.laps = [];
}
/**
* Stopwatch
*
* Create new or revive existing stopwatch objects.
*
* @param {Object} opts Optional stopwatch object to create or revive
* a new or existing stopwatch object.
* - startTime, number time in ms.
* - totalElapsed, number time in ms.
* - isStarted, started state boolean.
* - laps, array of lap objects (lap = {time:, duration:}).
*/
function Stopwatch(opts = {}) {
var defaults = new Defaults();
var obj = {};
obj.startTime = opts.startTime || defaults.startTime;
obj.totalElapsed = opts.totalElapsed || defaults.totalElapsed;
obj.state = opts.state || defaults.state;
obj.laps = opts.laps ? opts.laps.slice() : defaults.laps;
priv.set(this, obj);
}
Stopwatch.RUNNING = 'RUNNING';
Stopwatch.PAUSED = 'PAUSED';
Stopwatch.RESET = 'RESET';
Stopwatch.prototype = {
constructor: Stopwatch,
getState: function() {
var sw = priv.get(this);
return sw.state;
},
setState: function(state) {
var sw = priv.get(this);
sw.state = state;
},
/**
* start Starts the stopwatch, either from a reset or paused state
*/
start: function sw_start() {
var sw = priv.get(this);
if (sw.state === Stopwatch.RUNNING) {
return;
}
var now = Date.now() - sw.totalElapsed;
sw.startTime = now;
this.setState(Stopwatch.RUNNING);
},
/**
* getElapsedTime Calculates the total elapsed duration since the
* stopwatch was started
* @return {Number} return total elapsed duration.
*/
getElapsedTime: function sw_getElapsedTime() {
var sw = priv.get(this);
var elapsed = 0;
if (sw.state === Stopwatch.RUNNING) {
elapsed = Date.now() - sw.startTime;
} else {
elapsed = sw.totalElapsed;
}
return elapsed;
},
/**
* pause Pauses the stopwatch
*/
pause: function sw_pause() {
var sw = priv.get(this);
if (sw.state === Stopwatch.PAUSED) {
return;
}
sw.totalElapsed = Date.now() - sw.startTime;
this.setState(Stopwatch.PAUSED);
},
/**
* nextLap Calculates the duration of the next lap.
* @return {object} return an object containing:
* duration - the duration of this lap in ms.
* time - the start time of this lap in ms from epoch.
*/
nextLap: function sw_nextLap() {
var sw = priv.get(this);
var now;
if (sw.state === Stopwatch.RUNNING) {
now = Date.now();
} else {
now = sw.startTime + sw.totalElapsed;
}
var lastLapTime;
var newLap = {};
if (sw.laps.length > 0) {
lastLapTime = sw.laps[sw.laps.length - 1].time;
} else {
lastLapTime = 0;
}
newLap.duration = now - (sw.startTime + lastLapTime);
newLap.time = now - sw.startTime;
return newLap;
},
/**
* lap Calculates a new lap duration since the last lap time,
* and mutates `priv[this].laps` to contain the new value.
* If the stopwatch isn't currently running, returns 0.
* @return {number} return the lap duration in ms.
*/
lap: function sw_lap() {
var sw = priv.get(this);
if (sw.state !== Stopwatch.RUNNING) {
return 0;
}
var nl = this.nextLap();
sw.laps.push(nl);
return nl;
},
/**
* getLaps Returns an array of laps, sorted by oldest first
* @return {Array} return an array of laps.
*/
getLaps: function sw_getLaps() {
var sw = priv.get(this);
return sw.laps.map(function(lap) {
return lap;
});
},
/**
* reset Resets the stopwatch back to 0, clears laps
*/
reset: function sw_reset() {
priv.set(this, new Defaults());
},
/**
* toSerializable Returns a serializable object for persisting Stopwatch data
* @return {Object} A serializable object.
*/
toSerializable: function sw_toSerializable() {
var sw = priv.get(this);
var obj = {};
for (var i in sw) {
if (sw.hasOwnProperty(i)) {
obj[i] = Array.isArray(sw[i]) ? sw[i].slice() : sw[i];
}
}
return obj;
}
};
return Stopwatch;
});

27
gaia/apps/clock/js/tabs.js Executable file
View file

@ -0,0 +1,27 @@
define(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;
});

13
gaia/apps/clock/js/template.js Executable file
View file

@ -0,0 +1,13 @@
define(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;
});

27
gaia/apps/clock/js/text.js Executable file
View file

@ -0,0 +1,27 @@
define({
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);
}
});

View file

@ -0,0 +1,386 @@
/**
* @license RequireJS text 2.0.10 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
* Available via the MIT or new BSD license.
* see: http://github.com/requirejs/text for details
*/
/*jslint regexp: true */
/*global require, XMLHttpRequest, ActiveXObject,
define, window, process, Packages,
java, location, Components, FileUtils */
define(['module'], function (module) {
'use strict';
var text, fs, Cc, Ci, xpcIsWindows,
progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,
bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
hasLocation = typeof location !== 'undefined' && location.href,
defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''),
defaultHostName = hasLocation && location.hostname,
defaultPort = hasLocation && (location.port || undefined),
buildMap = {},
masterConfig = (module.config && module.config()) || {};
text = {
version: '2.0.10',
strip: function (content) {
//Strips <?xml ...?> declarations so that external SVG and XML
//documents can be added to a document without worry. Also, if the string
//is an HTML document, only the part inside the body tag is returned.
if (content) {
content = content.replace(xmlRegExp, "");
var matches = content.match(bodyRegExp);
if (matches) {
content = matches[1];
}
} else {
content = "";
}
return content;
},
jsEscape: function (content) {
return content.replace(/(['\\])/g, '\\$1')
.replace(/[\f]/g, "\\f")
.replace(/[\b]/g, "\\b")
.replace(/[\n]/g, "\\n")
.replace(/[\t]/g, "\\t")
.replace(/[\r]/g, "\\r")
.replace(/[\u2028]/g, "\\u2028")
.replace(/[\u2029]/g, "\\u2029");
},
createXhr: masterConfig.createXhr || function () {
//Would love to dump the ActiveX crap in here. Need IE 6 to die first.
var xhr, i, progId;
if (typeof XMLHttpRequest !== "undefined") {
return new XMLHttpRequest();
} else if (typeof ActiveXObject !== "undefined") {
for (i = 0; i < 3; i += 1) {
progId = progIds[i];
try {
xhr = new ActiveXObject(progId);
} catch (e) {}
if (xhr) {
progIds = [progId]; // so faster next time
break;
}
}
}
return xhr;
},
/**
* Parses a resource name into its component parts. Resource names
* look like: module/name.ext!strip, where the !strip part is
* optional.
* @param {String} name the resource name
* @returns {Object} with properties "moduleName", "ext" and "strip"
* where strip is a boolean.
*/
parseName: function (name) {
var modName, ext, temp,
strip = false,
index = name.indexOf("."),
isRelative = name.indexOf('./') === 0 ||
name.indexOf('../') === 0;
if (index !== -1 && (!isRelative || index > 1)) {
modName = name.substring(0, index);
ext = name.substring(index + 1, name.length);
} else {
modName = name;
}
temp = ext || modName;
index = temp.indexOf("!");
if (index !== -1) {
//Pull off the strip arg.
strip = temp.substring(index + 1) === "strip";
temp = temp.substring(0, index);
if (ext) {
ext = temp;
} else {
modName = temp;
}
}
return {
moduleName: modName,
ext: ext,
strip: strip
};
},
xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/,
/**
* Is an URL on another domain. Only works for browser use, returns
* false in non-browser environments. Only used to know if an
* optimized .js version of a text resource should be loaded
* instead.
* @param {String} url
* @returns Boolean
*/
useXhr: function (url, protocol, hostname, port) {
var uProtocol, uHostName, uPort,
match = text.xdRegExp.exec(url);
if (!match) {
return true;
}
uProtocol = match[2];
uHostName = match[3];
uHostName = uHostName.split(':');
uPort = uHostName[1];
uHostName = uHostName[0];
return (!uProtocol || uProtocol === protocol) &&
(!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) &&
((!uPort && !uHostName) || uPort === port);
},
finishLoad: function (name, strip, content, onLoad) {
content = strip ? text.strip(content) : content;
if (masterConfig.isBuild) {
buildMap[name] = content;
}
onLoad(content);
},
load: function (name, req, onLoad, config) {
//Name has format: some.module.filext!strip
//The strip part is optional.
//if strip is present, then that means only get the string contents
//inside a body tag in an HTML string. For XML/SVG content it means
//removing the <?xml ...?> declarations so the content can be inserted
//into the current doc without problems.
// Do not bother with the work if a build and text will
// not be inlined.
if (config.isBuild && !config.inlineText) {
onLoad();
return;
}
masterConfig.isBuild = config.isBuild;
var parsed = text.parseName(name),
nonStripName = parsed.moduleName +
(parsed.ext ? '.' + parsed.ext : ''),
url = req.toUrl(nonStripName),
useXhr = (masterConfig.useXhr) ||
text.useXhr;
// Do not load if it is an empty: url
if (url.indexOf('empty:') === 0) {
onLoad();
return;
}
//Load the text. Use XHR if possible and in a browser.
if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) {
text.get(url, function (content) {
text.finishLoad(name, parsed.strip, content, onLoad);
}, function (err) {
if (onLoad.error) {
onLoad.error(err);
}
});
} else {
//Need to fetch the resource across domains. Assume
//the resource has been optimized into a JS module. Fetch
//by the module name + extension, but do not include the
//!strip part to avoid file system issues.
req([nonStripName], function (content) {
text.finishLoad(parsed.moduleName + '.' + parsed.ext,
parsed.strip, content, onLoad);
});
}
},
write: function (pluginName, moduleName, write, config) {
if (buildMap.hasOwnProperty(moduleName)) {
var content = text.jsEscape(buildMap[moduleName]);
write.asModule(pluginName + "!" + moduleName,
"define(function () { return '" +
content +
"';});\n");
}
},
writeFile: function (pluginName, moduleName, req, write, config) {
var parsed = text.parseName(moduleName),
extPart = parsed.ext ? '.' + parsed.ext : '',
nonStripName = parsed.moduleName + extPart,
//Use a '.js' file name so that it indicates it is a
//script that can be loaded across domains.
fileName = req.toUrl(parsed.moduleName + extPart) + '.js';
//Leverage own load() method to load plugin value, but only
//write out values that do not have the strip argument,
//to avoid any potential issues with ! in file names.
text.load(nonStripName, req, function (value) {
//Use own write() method to construct full module value.
//But need to create shell that translates writeFile's
//write() to the right interface.
var textWrite = function (contents) {
return write(fileName, contents);
};
textWrite.asModule = function (moduleName, contents) {
return write.asModule(moduleName, fileName, contents);
};
text.write(pluginName, nonStripName, textWrite, config);
}, config);
}
};
if (masterConfig.env === 'node' || (!masterConfig.env &&
typeof process !== "undefined" &&
process.versions &&
!!process.versions.node &&
!process.versions['node-webkit'])) {
//Using special require.nodeRequire, something added by r.js.
fs = require.nodeRequire('fs');
text.get = function (url, callback, errback) {
try {
var file = fs.readFileSync(url, 'utf8');
//Remove BOM (Byte Mark Order) from utf8 files if it is there.
if (file.indexOf('\uFEFF') === 0) {
file = file.substring(1);
}
callback(file);
} catch (e) {
errback(e);
}
};
} else if (masterConfig.env === 'xhr' || (!masterConfig.env &&
text.createXhr())) {
text.get = function (url, callback, errback, headers) {
var xhr = text.createXhr(), header;
xhr.open('GET', url, true);
//Allow plugins direct access to xhr headers
if (headers) {
for (header in headers) {
if (headers.hasOwnProperty(header)) {
xhr.setRequestHeader(header.toLowerCase(), headers[header]);
}
}
}
//Allow overrides specified in config
if (masterConfig.onXhr) {
masterConfig.onXhr(xhr, url);
}
xhr.onreadystatechange = function (evt) {
var status, err;
//Do not explicitly handle errors, those should be
//visible via console output in the browser.
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;
errback(err);
} else {
callback(xhr.responseText);
}
if (masterConfig.onXhrComplete) {
masterConfig.onXhrComplete(xhr, url);
}
}
};
xhr.send(null);
};
} else if (masterConfig.env === 'rhino' || (!masterConfig.env &&
typeof Packages !== 'undefined' && typeof java !== 'undefined')) {
//Why Java, why is this so awkward?
text.get = function (url, callback) {
var stringBuffer, line,
encoding = "utf-8",
file = new java.io.File(url),
lineSeparator = java.lang.System.getProperty("line.separator"),
input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)),
content = '';
try {
stringBuffer = new java.lang.StringBuffer();
line = input.readLine();
// Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324
// http://www.unicode.org/faq/utf_bom.html
// Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK:
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058
if (line && line.length() && line.charAt(0) === 0xfeff) {
// Eat the BOM, since we've already found the encoding on this file,
// and we plan to concatenating this buffer with others; the BOM should
// only appear at the top of a file.
line = line.substring(1);
}
if (line !== null) {
stringBuffer.append(line);
}
while ((line = input.readLine()) !== null) {
stringBuffer.append(lineSeparator);
stringBuffer.append(line);
}
//Make sure we return a JavaScript string and not a Java string.
content = String(stringBuffer.toString()); //String
} finally {
input.close();
}
callback(content);
};
} else if (masterConfig.env === 'xpconnect' || (!masterConfig.env &&
typeof Components !== 'undefined' && Components.classes &&
Components.interfaces)) {
//Avert your gaze!
Cc = Components.classes,
Ci = Components.interfaces;
Components.utils['import']('resource://gre/modules/FileUtils.jsm');
xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc);
text.get = function (url, callback) {
var inStream, convertStream, fileObj,
readData = {};
if (xpcIsWindows) {
url = url.replace(/\//g, '\\');
}
fileObj = new FileUtils.File(url);
//XPCOM, you so crazy
try {
inStream = Cc['@mozilla.org/network/file-input-stream;1']
.createInstance(Ci.nsIFileInputStream);
inStream.init(fileObj, 1, 0, false);
convertStream = Cc['@mozilla.org/intl/converter-input-stream;1']
.createInstance(Ci.nsIConverterInputStream);
convertStream.init(inStream, "utf-8", inStream.available(),
Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
convertStream.readString(inStream.available(), readData);
convertStream.close();
inStream.close();
callback(readData.value);
} catch (e) {
throw new Error((fileObj && fileObj.path || '') + ': ' + e);
}
};
}
return text;
});

294
gaia/apps/clock/js/timer.js Executable file
View file

@ -0,0 +1,294 @@
define(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;
});

22
gaia/apps/clock/js/tmpl.js Executable file
View file

@ -0,0 +1,22 @@
define([], 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;
});

View file

@ -0,0 +1,368 @@
//MODIFIED requirejs text plugin, that just changes the write function
//to write out a 'tmpl!'-based resource instead, and removes writeFile.
/**
* @license RequireJS text 2.0.8 Copyright (c) 2010-2012, The Dojo
* Foundation All Rights Reserved. Available via the MIT or new BSD
* license. see: http://github.com/requirejs/text for details
*/
/*jslint regexp: true */
/*global require, XMLHttpRequest, ActiveXObject,
define, process, Packages,
java, location, Components, FileUtils */
define(['module'], function (module) {
'use strict';
var text, fs, Cc, Ci, xpcIsWindows,
progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,
bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
hasLocation = typeof location !== 'undefined' && location.href,
defaultProtocol = hasLocation && location.protocol &&
location.protocol.replace(/\:/, ''),
defaultHostName = hasLocation && location.hostname,
defaultPort = hasLocation && (location.port || undefined),
buildMap = {},
masterConfig = (module.config && module.config()) || {};
text = {
version: '2.0.8',
strip: function (content) {
//Strips <?xml ...?> declarations so that external SVG and
//XML documents can be added to a document without worry.
//Also, if the string is an HTML document, only the part
//inside the body tag is returned.
if (content) {
content = content.replace(xmlRegExp, '');
var matches = content.match(bodyRegExp);
if (matches) {
content = matches[1];
}
} else {
content = '';
}
return content;
},
jsEscape: function (content) {
return content.replace(/(['\\])/g, '\\$1')
.replace(/[\f]/g, '\\f')
.replace(/[\b]/g, '\\b')
.replace(/[\n]/g, '\\n')
.replace(/[\t]/g, '\\t')
.replace(/[\r]/g, '\\r')
.replace(/[\u2028]/g, '\\u2028')
.replace(/[\u2029]/g, '\\u2029');
},
createXhr: masterConfig.createXhr || function () {
//Would love to dump the ActiveX crap in here. Need IE 6
//to die first.
var xhr, i, progId;
if (typeof XMLHttpRequest !== 'undefined') {
return new XMLHttpRequest();
} else if (typeof ActiveXObject !== 'undefined') {
for (i = 0; i < 3; i += 1) {
progId = progIds[i];
try {
xhr = new ActiveXObject(progId);
} catch (e) {}
if (xhr) {
progIds = [progId]; // so faster next time
break;
}
}
}
return xhr;
},
/**
* Parses a resource name into its component parts. Resource names
* look like: module/name.ext!strip, where the !strip part is
* optional.
* @param {String} name the resource name
* @returns {Object} with properties "moduleName", "ext" and "strip"
* where strip is a boolean.
*/
parseName: function (name) {
var modName, ext, temp,
strip = false,
index = name.indexOf('.'),
isRelative = name.indexOf('./') === 0 ||
name.indexOf('../') === 0;
if (index !== -1 && (!isRelative || index > 1)) {
modName = name.substring(0, index);
ext = name.substring(index + 1, name.length);
} else {
modName = name;
}
temp = ext || modName;
index = temp.indexOf('!');
if (index !== -1) {
//Pull off the strip arg.
strip = temp.substring(index + 1) === 'strip';
temp = temp.substring(0, index);
if (ext) {
ext = temp;
} else {
modName = temp;
}
}
return {
moduleName: modName,
ext: ext,
strip: strip
};
},
xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/,
/**
* Is an URL on another domain. Only works for browser use, returns
* false in non-browser environments. Only used to know if an
* optimized .js version of a text resource should be loaded
* instead.
* @param {String} url
* @returns Boolean
*/
useXhr: function (url, protocol, hostname, port) {
var uProtocol, uHostName, uPort,
match = text.xdRegExp.exec(url);
if (!match) {
return true;
}
uProtocol = match[2];
uHostName = match[3];
uHostName = uHostName.split(':');
uPort = uHostName[1];
uHostName = uHostName[0];
return (!uProtocol || uProtocol === protocol) &&
(!uHostName || uHostName.toLowerCase() ===
hostname.toLowerCase()) &&
((!uPort && !uHostName) || uPort === port);
},
finishLoad: function (name, strip, content, onLoad) {
content = strip ? text.strip(content) : content;
if (masterConfig.isBuild) {
buildMap[name] = content;
}
onLoad(content);
},
load: function (name, req, onLoad, config) {
//Name has format: some.module.filext!strip The strip part
//is optional. if strip is present, then that means only
//get the string contents inside a body tag in an HTML
//string. For XML/SVG content it means removing the <?xml
//...?> declarations so the content can be inserted into
//the current doc without problems.
// Do not bother with the work if a build and text will
// not be inlined.
if (config.isBuild && !config.inlineText) {
onLoad();
return;
}
masterConfig.isBuild = config.isBuild;
var parsed = text.parseName(name),
nonStripName = parsed.moduleName +
(parsed.ext ? '.' + parsed.ext : ''),
url = req.toUrl(nonStripName),
useXhr = (masterConfig.useXhr) ||
text.useXhr;
//Load the text. Use XHR if possible and in a browser.
if (!hasLocation || useXhr(url, defaultProtocol,
defaultHostName, defaultPort)) {
text.get(url, function (content) {
text.finishLoad(name, parsed.strip, content, onLoad);
}, function (err) {
if (onLoad.error) {
onLoad.error(err);
}
});
} else {
//Need to fetch the resource across domains. Assume
//the resource has been optimized into a JS module. Fetch
//by the module name + extension, but do not include the
//!strip part to avoid file system issues.
req([nonStripName], function (content) {
text.finishLoad(parsed.moduleName + '.' + parsed.ext,
parsed.strip, content, onLoad);
});
}
},
write: function (pluginName, moduleName, write, config) {
if (buildMap.hasOwnProperty(moduleName)) {
var content = text.jsEscape(buildMap[moduleName]);
write.asModule(
pluginName + '!' + moduleName,
'define([\'tmpl\'], function (tmpl) { return tmpl.toDom(\'' +
content + '\'); });\n');
}
}
};
if (masterConfig.env === 'node' || (!masterConfig.env &&
typeof process !== 'undefined' &&
process.versions &&
!!process.versions.node)) {
//Using special require.nodeRequire, something added by r.js.
fs = require.nodeRequire('fs');
text.get = function (url, callback, errback) {
try {
var file = fs.readFileSync(url, 'utf8');
//Remove BOM (Byte Mark Order) from utf8 files if it is there.
if (file.indexOf('\uFEFF') === 0) {
file = file.substring(1);
}
callback(file);
} catch (e) {
errback(e);
}
};
} else if (masterConfig.env === 'xhr' || (!masterConfig.env &&
text.createXhr())) {
text.get = function (url, callback, errback, headers) {
var xhr = text.createXhr(), header;
xhr.open('GET', url, true);
//Allow plugins direct access to xhr headers
if (headers) {
for (header in headers) {
if (headers.hasOwnProperty(header)) {
xhr.setRequestHeader(header.toLowerCase(), headers[header]);
}
}
}
//Allow overrides specified in config
if (masterConfig.onXhr) {
masterConfig.onXhr(xhr, url);
}
xhr.onreadystatechange = function (evt) {
var status, err;
//Do not explicitly handle errors, those should be
//visible via console output in the browser.
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;
errback(err);
} else {
callback(xhr.responseText);
}
if (masterConfig.onXhrComplete) {
masterConfig.onXhrComplete(xhr, url);
}
}
};
xhr.send(null);
};
} else if (masterConfig.env === 'rhino' ||
(!masterConfig.env &&
typeof Packages !== 'undefined' && typeof java !== 'undefined')) {
//Why Java, why is this so awkward?
text.get = function (url, callback) {
var stringBuffer, line,
encoding = 'utf-8',
file = new java.io.File(url),
lineSeparator = java.lang.System.getProperty('line.separator'),
input = new java.io.BufferedReader(
new java.io.InputStreamReader(
new java.io.FileInputStream(file), encoding)),
content = '';
try {
stringBuffer = new java.lang.StringBuffer();
line = input.readLine();
// Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324
// http://www.unicode.org/faq/utf_bom.html
// Note that when we use utf-8, the BOM should appear as "EF
// BB BF", but it doesn't due to this bug in the JDK:
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058
if (line && line.length() && line.charAt(0) === 0xfeff) {
// Eat the BOM, since we've already found the encoding on
// this file, and we plan to concatenating this buffer with
// others; the BOM should only appear at the top of a file.
line = line.substring(1);
}
if (line !== null) {
stringBuffer.append(line);
}
while ((line = input.readLine()) !== null) {
stringBuffer.append(lineSeparator);
stringBuffer.append(line);
}
//Make sure we return a JavaScript string and not a Java string.
content = String(stringBuffer.toString()); //String
} finally {
input.close();
}
callback(content);
};
} else if (masterConfig.env === 'xpconnect' ||
(!masterConfig.env &&
typeof Components !== 'undefined' && Components.classes &&
Components.interfaces)) {
//Avert your gaze!
Cc = Components.classes,
Ci = Components.interfaces;
Components.utils['import']('resource://gre/modules/FileUtils.jsm');
xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc);
text.get = function (url, callback) {
var inStream, convertStream, fileObj,
readData = {};
if (xpcIsWindows) {
url = url.replace(/\//g, '\\');
}
fileObj = new FileUtils.File(url);
//XPCOM, you so crazy
try {
inStream = Cc['@mozilla.org/network/file-input-stream;1']
.createInstance(Ci.nsIFileInputStream);
inStream.init(fileObj, 1, 0, false);
convertStream = Cc['@mozilla.org/intl/converter-input-stream;1']
.createInstance(Ci.nsIConverterInputStream);
convertStream.init(
inStream, 'utf-8', inStream.available(),
Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
convertStream.readString(inStream.available(), readData);
convertStream.close();
inStream.close();
callback(readData.value);
} catch (e) {
throw new Error((fileObj && fileObj.path || '') + ': ' + e);
}
};
}
return text;
});

569
gaia/apps/clock/js/utils.js Executable file
View file

@ -0,0 +1,569 @@
define(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;
});

91
gaia/apps/clock/js/view.js Executable file
View file

@ -0,0 +1,91 @@
define(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;
});