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

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>