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,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,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,751 @@
/* global define */
define('audio_manager',['require'],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
};
});
// outer IIFE
define('form_button',['require','utils'],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
});
define('text!panels/alarm_edit/panel.html',[],function () { return '<gaia-header ignore-dir id="alarm-header" action="close">\n <h1 class="new-alarm-title" data-l10n-id="newAlarm"></h1>\n <h1 class="edit-alarm-title" data-l10n-id="editAlarm"></h1>\n <button id="alarm-done" data-l10n-id="done"></button>\n</gaia-header>\n<ul id="edit-alarm" class="compact">\n <li>\n <input type="text" name="alarm.label" id="alarm-name" data-l10n-id="alarmName" placeholder="Alarm name" maxLength="50" dir="auto" />\n </li>\n <li>\n <label data-l10n-id="time"></label>\n <input id="time-select" type="time" />\n </li>\n <li>\n <label data-l10n-id="repeat"></label>\n <select id="repeat-select" multiple="true">\n <option value="1"></option>\n <option value="2"></option>\n <option value="3"></option>\n <option value="4"></option>\n <option value="5"></option>\n <option value="6"></option>\n <option value="0"></option>\n </select>\n </li>\n <li>\n <label data-l10n-id="sound"></label>\n <select id="sound-select">\n <option value="0" data-l10n-id="noSound"></option>\n <option value="ac_awake.opus" data-l10n-id="ac_awake_opus"></option>\n <option value="ac_crystalize.opus" data-l10n-id="ac_crystalize_opus"></option>\n <option value="ac_cycle.opus" data-l10n-id="ac_cycle_opus"></option>\n <option value="ac_digicloud.opus" data-l10n-id="ac_digicloud_opus"></option>\n <option value="ac_humming_waves.opus" data-l10n-id="ac_humming_waves_opus"></option>\n <option value="ac_into_the_void.opus" data-l10n-id="ac_into_the_void_opus"></option>\n <option value="ac_lightly.opus" data-l10n-id="ac_lightly_opus"></option>\n <option value="ac_mobile.opus" data-l10n-id="ac_mobile_opus"></option>\n <option value="ac_pinger.opus" data-l10n-id="ac_pinger_opus"></option>\n <option value="ac_skip.opus" data-l10n-id="ac_skip_opus"></option>\n <option value="ac_tri.opus" data-l10n-id="ac_tri_opus"></option>\n <option value="ac_universal.opus" data-l10n-id="ac_universal_opus"></option>\n </select>\n </li>\n <li>\n <gaia-switch id="vibrate-checkbox" checked>\n <label data-l10n-id="vibrate"></label>\n </gaia-switch>\n </li>\n <li>\n <label class="view-alarm-lbl snooze-lbl" data-l10n-id="snooze-label">Snooze</label>\n <select id="snooze-select">\n <option data-l10n-id="alarmMinutes" data-l10n-args=\'{"minutes": "5"}\' value="5"></option>\n <option data-l10n-id="alarmMinutes" data-l10n-args=\'{"minutes": "10"}\' value="10"></option>\n <option data-l10n-id="alarmMinutes" data-l10n-args=\'{"minutes": "15"}\' value="15"></option>\n <option data-l10n-id="alarmMinutes" data-l10n-args=\'{"minutes": "20"}\' value="20"></option>\n </select>\n </li>\n <li>\n <label class="alarm-volume-lbl"\n id="alarm-volume-lbl"\n data-l10n-id="alarm-volume-label"></label>\n <!-- 0.0625 is 1/16th, which matches the step size in System Settings. -->\n <input id="alarm-volume-input" step="0.0625" min="0" value="1" max="1"\n type="range">\n </li>\n <li id="delete-menu">\n <button id="alarm-delete" class="danger full" data-l10n-id="delete"></button>\n </li>\n</ul>\n';});
/* global KeyEvent, mozIntl, IntlHelper */
define('panels/alarm_edit/main',['require','alarm','panels/alarm/clock_view','audio_manager','form_button','sounds','utils','panel','text!panels/alarm_edit/panel.html','constants'],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,511 @@
define('stopwatch',[],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;
});
define('text!panels/stopwatch/panel.html',[],function () { return '<div class="stopwatch-time"></div>\n<menu id="stopwatch-controls">\n <button class="stopwatch-start edit-button" data-l10n-id="start"></button>\n <button class="stopwatch-pause edit-button recommend" data-l10n-id="pause"></button>\n <button class="stopwatch-resume edit-button" data-l10n-id="resume"></button>\n <button class="stopwatch-reset edit-button danger" data-l10n-id="reset"></button>\n <button class="stopwatch-lap edit-button" data-l10n-id="lap"></button>\n</menu>\n<div class="stopwatch-lap-list">\n <!-- list of exisiting alarms, to be populated -->\n <ul class="stopwatch-laps"></ul>\n</div>\n';});
define('text!panels/stopwatch/list_item.html',[],function () { return '<div class="lap-name">\n</div>\n<div class="lap-duration">\n</div>\n';});
define('panels/stopwatch/main',['require','panel','stopwatch','template','text!panels/stopwatch/panel.html','text!panels/stopwatch/list_item.html'],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>

File diff suppressed because it is too large Load diff

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>