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.
15
gaia/apps/clock/.jshintrc
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "../../.jshintrc",
|
||||
"globals": {
|
||||
"define": false,
|
||||
"require": false,
|
||||
"KeyEvent": false,
|
||||
"requirejs": false,
|
||||
"indexedDB": false,
|
||||
"requestAnimationFrame": false,
|
||||
"cancelAnimationFrame": false,
|
||||
"Event": false,
|
||||
"Proxy": false,
|
||||
"Promise": false
|
||||
}
|
||||
}
|
||||
41
gaia/apps/clock/build/build.js
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
'use strict';
|
||||
|
||||
/* jshint node: true */
|
||||
|
||||
var utils = require('utils');
|
||||
|
||||
exports.execute = function(options) {
|
||||
var appName = utils.basename(options.APP_DIR);
|
||||
var config = utils.getFile(options.APP_DIR, 'build',
|
||||
'require_config.jslike');
|
||||
var rjsPath = utils.joinPath(options.GAIA_DIR, 'build', 'r.js');
|
||||
var requirejs;
|
||||
|
||||
if (utils.isNode()) {
|
||||
requirejs = require(rjsPath);
|
||||
} else {
|
||||
var sandbox = utils.createSandbox();
|
||||
sandbox.arguments = [];
|
||||
sandbox.requirejsAsLib = true;
|
||||
sandbox.print = function() {
|
||||
utils.log(appName, Array.prototype.join.call(arguments, ' '));
|
||||
};
|
||||
utils.runScriptInSandbox(rjsPath, sandbox);
|
||||
requirejs = sandbox.requirejs;
|
||||
}
|
||||
|
||||
// logLevel set 4 for silent, set 0 for all
|
||||
var log = 'logLevel=' + (options.VERBOSE === '1' ? '0' : '4');
|
||||
var optimize = 'optimize=none';
|
||||
var build = new Promise(function(resolve, reject) {
|
||||
requirejs.optimize([config.path, optimize, log], resolve, reject);
|
||||
});
|
||||
|
||||
return build
|
||||
.then(function() {
|
||||
utils.log(appName, 'require.js optimize done');
|
||||
}).catch(function(err) {
|
||||
utils.log(appName, 'require.js optimize failed');
|
||||
utils.log(appName, err);
|
||||
});
|
||||
};
|
||||
110
gaia/apps/clock/build/make_gaia_shared.js
Executable file
|
|
@ -0,0 +1,110 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Scans the built directory's JS and CSS files looking for shared resources
|
||||
* to generate a gaia_shared.json
|
||||
*/
|
||||
/*jshint moz: true */
|
||||
/*global load */
|
||||
/* jshint unused:false */
|
||||
var requirejsAsLib = true;
|
||||
load('../../build/r.js');
|
||||
|
||||
var shared = {
|
||||
js: [],
|
||||
style: [],
|
||||
style_unstable: []
|
||||
};
|
||||
|
||||
// This file is run from the clock directory.
|
||||
var srcDir = './',
|
||||
buildDir = '../../build_stage/clock/';
|
||||
|
||||
// Taken from r.js css optimizing step.
|
||||
var cssImportRegExp = /\@import\s+(url\()?\s*([^);]+)\s*(\))?([\w, ]*)(;)?/ig;
|
||||
|
||||
function stripCssComments(contents) {
|
||||
var index,
|
||||
start = 0,
|
||||
end = contents.length,
|
||||
result = '',
|
||||
startComment = '/*',
|
||||
endComment = '*/';
|
||||
|
||||
while ((index = contents.indexOf(startComment, start)) !== -1) {
|
||||
result += contents.substring(start, index);
|
||||
start = index;
|
||||
index = contents.indexOf(endComment, start);
|
||||
if (index === -1) {
|
||||
start = end - 1;
|
||||
} else {
|
||||
start = index + 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (start !== end - 1) {
|
||||
result += contents.substring(start, end);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
requirejs.tools.useLib(function(require) {
|
||||
require(['env!env/file', 'parse'], function(file, parse) {
|
||||
var jsExtRegExp = /\.js$/;
|
||||
|
||||
// Find all the HTML and JS files. Use the srcDir instead of
|
||||
// buildDir since uglification can mangle the files such that
|
||||
// the dependency scanning fails.
|
||||
var files = file.getFilteredFileList(srcDir + 'js', /\.js$|\.html$/);
|
||||
files.forEach(function(fileName) {
|
||||
var contents = file.readFile(fileName);
|
||||
|
||||
// If JS, scan for shared resources.
|
||||
if (jsExtRegExp.test(fileName)) {
|
||||
var deps = parse.findDependencies(fileName, contents);
|
||||
deps.forEach(function(dep) {
|
||||
|
||||
if (dep.indexOf('shared/') === 0) {
|
||||
var sharedDep = dep.replace(/shared\/js\//, '') + '.js';
|
||||
// Avoid duplicate entries for cleanliness
|
||||
if (shared.js.indexOf(sharedDep) === -1) {
|
||||
shared.js.push(sharedDep);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Find CSS references. Use the source area, instead of the build
|
||||
// area since the built version has @import statements inlined.
|
||||
files = file.getFilteredFileList(srcDir + 'style', /\.css$/);
|
||||
files.forEach(function(fileName) {
|
||||
var url, match, index,
|
||||
contents = file.readFile(fileName);
|
||||
|
||||
contents = stripCssComments(contents);
|
||||
cssImportRegExp.lastIndex = 0;
|
||||
|
||||
while ((match = cssImportRegExp.exec(contents))) {
|
||||
// Grab the URL without quotes
|
||||
url = match[2].replace(/['"]/g, '');
|
||||
if (url) {
|
||||
index = url.indexOf('/shared/');
|
||||
if (index !== -1) {
|
||||
if (url.indexOf('style_unstable') === -1) {
|
||||
shared.style.push(url.substring(index + 1)
|
||||
.replace(/shared\/style\//, ''));
|
||||
} else {
|
||||
shared.style_unstable.push(url.substring(index + 1)
|
||||
.replace(/shared\/style_unstable\//, ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Save the shared resources file.
|
||||
file.saveFile(buildDir + 'gaia_shared.json',
|
||||
JSON.stringify(shared, null, ' '));
|
||||
});
|
||||
});
|
||||
79
gaia/apps/clock/build/require_config.jslike
Executable file
|
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
appDir: '..',
|
||||
baseUrl: 'js',
|
||||
dir: '../../../build_stage/clock',
|
||||
mainConfigFile: '../js/require_config.js',
|
||||
paths: {
|
||||
prim: 'empty:'
|
||||
},
|
||||
findNestedDependencies: true,
|
||||
|
||||
// Be sure to normalize all define() calls by extracting
|
||||
// dependencies so Function toString is not needed, and
|
||||
// lower capability devices like Tarako can optimize
|
||||
// memory by discarding function sources. This is
|
||||
// automatically done when an 'optimize' value other than
|
||||
// 'none' is used. This setting makes sure it happens for
|
||||
// builds where 'none' is used for 'optimize'.
|
||||
normalizeDirDefines: 'all',
|
||||
|
||||
// Build a separate module for each panel. Because the application
|
||||
// consistently initializes with the `startup` module, this module and all
|
||||
// its dependencies do not need to be included by the "secondary" panels
|
||||
// ("Alarm Edit", "Timer", and "Stopwatch").
|
||||
modules: [
|
||||
{
|
||||
name: 'startup',
|
||||
include: ['panels/alarm/main']
|
||||
},
|
||||
{
|
||||
name: 'onring'
|
||||
},
|
||||
{
|
||||
name: 'panels/alarm_edit/main',
|
||||
exclude: ['startup']
|
||||
},
|
||||
{
|
||||
name: 'panels/timer/main',
|
||||
exclude: ['startup']
|
||||
},
|
||||
{
|
||||
name: 'panels/stopwatch/main',
|
||||
exclude: ['startup']
|
||||
}
|
||||
],
|
||||
|
||||
// Set to 'uglify2' to get uglify to run using the
|
||||
// uglify2 settings below.
|
||||
optimize: 'none',
|
||||
|
||||
// Just strip comments, no code compression or mangling.
|
||||
// Only active if optimize: 'uglify2'
|
||||
uglify2: {
|
||||
// Comment out the output section to get rid of line
|
||||
// returns and tabs spacing.
|
||||
output: {
|
||||
beautify: true
|
||||
},
|
||||
compress: false,
|
||||
mangle: false
|
||||
},
|
||||
|
||||
// Rewrite the waitSeconds directive so that we never time out
|
||||
// waiting for modules to load in production. See
|
||||
// js/require_config.js for more details.
|
||||
onBuildWrite: function(id, url, contents) {
|
||||
if (id === 'require_config') {
|
||||
return contents.replace(/waitSeconds:\s*\d+/, 'waitSeconds: 0');
|
||||
} else {
|
||||
return contents;
|
||||
}
|
||||
},
|
||||
|
||||
fileExclusionRegExp: /^\.|^test$|^build$/,
|
||||
|
||||
// Keeping build dir since Makefile cleans it up and
|
||||
// preps build dir with the shared directory
|
||||
keepBuildDir: true,
|
||||
removeCombined: true
|
||||
}
|
||||
78
gaia/apps/clock/index.html
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<meta charset="utf-8">
|
||||
<meta name="theme-color" content="#000">
|
||||
<title>Clock</title>
|
||||
<link rel="stylesheet" type="text/css" href="style/clock.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/alarm.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/stopwatch.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/timer.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/views.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/form_button.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/picker/value_selector.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/picker/picker.css">
|
||||
|
||||
<!-- Shared styles -->
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/status.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/tabs.css">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="style/hacks.css">
|
||||
|
||||
<!-- Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/shared/locales/date/date.{locale}.properties">
|
||||
<link rel="localization" href="locales/clock.{locale}.properties">
|
||||
<link rel="localization" href="locales/alarms.{locale}.properties">
|
||||
<script defer src="/shared/js/intl/l20n.js"></script>
|
||||
<script defer src="/shared/js/date_time_helper.js"></script>
|
||||
<script defer src="/shared/js/moz_intl.js"></script>
|
||||
<script defer src="/shared/js/intl_helper.js"></script>
|
||||
|
||||
<!-- These are lazy loaded. Need to be included here so the build picks them up
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/input_areas.css" >
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/buttons.css" >
|
||||
-->
|
||||
|
||||
<!--
|
||||
The following scripts are lazy loaded but left here to ensure
|
||||
they get copied over from shared during the build
|
||||
-->
|
||||
<!-- <script defer src="/shared/js/async_storage.js"></script> -->
|
||||
<!-- <script defer src="/shared/js/gesture_detector.js"></script> -->
|
||||
<!-- <script defer src="/shared/js/accessibility_helper.js"></script> -->
|
||||
<!-- <script defer src="/shared/js/template.js"></script> -->
|
||||
|
||||
<!-- Web Components -->
|
||||
<script defer src="/shared/elements/config.js"></script>
|
||||
<script defer src="/shared/elements/gaia-header/dist/gaia-header.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css" />
|
||||
<!-- <link rel="stylesheet" type="text/css" href="shared/elements/gaia-icons/gaia-icons.css" /> -->
|
||||
<script defer src="/shared/js/component_utils.js"></script>
|
||||
<script defer src="/shared/elements/gaia_switch/script.js"></script>
|
||||
|
||||
<!-- Shared sounds -->
|
||||
<link rel="resources" type="directory" href="shared/resources/media/alarms/">
|
||||
</head>
|
||||
|
||||
<body class="theme-media">
|
||||
<div id="views">
|
||||
<div id="main-panel-wrapper">
|
||||
<div id="alarm-panel" class="active panel" data-panel-id="alarm" role="tabpanel" aria-labelledby="alarm-tab"></div>
|
||||
<div id="timer-panel" class="panel hidden" data-panel-id="timer" role="tabpanel" aria-labelledby="timer-tab"></div>
|
||||
<div id="stopwatch-panel" class="panel hidden" data-panel-id="stopwatch" role="tabpanel" aria-labelledby="stopwatch-tab"></div>
|
||||
</div>
|
||||
<section role="region" class="skin-dark panel hidden" data-panel-id="alarm_edit">
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<ul role="tablist" class="bb-tablist skin-dark bottom" id="clock-tabs">
|
||||
<li role="presentation"><a id="alarm-tab" aria-controls="alarm-panel" aria-selected="true" role="tab" href="#alarm-panel" data-l10n-id="alarm"></a></li>
|
||||
<li role="presentation"><a id="timer-tab" aria-controls="timer-panel" aria-selected="false" role="tab" href="#timer-panel" data-l10n-id="timer"></a></li>
|
||||
<li role="presentation"><a id="stopwatch-tab" aria-controls="stopwatch-panel" aria-selected="false" role="tab" href="#stopwatch-panel" data-l10n-id="stopwatch"></a></li>
|
||||
</ul>
|
||||
<script defer data-main="js/startup.js" src="js/alameda.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1224
gaia/apps/clock/js/alameda.js
Executable file
256
gaia/apps/clock/js/alarm.js
Executable file
|
|
@ -0,0 +1,256 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Alarm represents one alarm instance. It tracks any mozAlarms it
|
||||
* has registered, its IndexedDB ID, and any other properties
|
||||
* relating to the alarm's schedule and firing options.
|
||||
*/
|
||||
function Alarm(opts) {
|
||||
opts = opts || {};
|
||||
var now = new Date();
|
||||
var defaults = {
|
||||
id: null,
|
||||
registeredAlarms: {}, // keys: ('normal' or 'snooze') => mozAlarmID
|
||||
// Map of weekdays { "0": true, "1": false, ... }
|
||||
// "0" is for Sunday in Gregorian calendar
|
||||
repeat: {},
|
||||
hour: now.getHours(),
|
||||
minute: now.getMinutes(),
|
||||
label: '',
|
||||
sound: 'ac_awake.opus',
|
||||
vibrate: true,
|
||||
snooze: 10 // Number of minutes to snooze
|
||||
};
|
||||
|
||||
for (var key in defaults) {
|
||||
this[key] = (key in opts ? opts[key] : defaults[key]);
|
||||
}
|
||||
}
|
||||
|
||||
Alarm.prototype = {
|
||||
toJSON: function() {
|
||||
return {
|
||||
id: this.id,
|
||||
registeredAlarms: this.registeredAlarms,
|
||||
repeat: this.repeat,
|
||||
hour: this.hour,
|
||||
minute: this.minute,
|
||||
label: this.label,
|
||||
sound: this.sound,
|
||||
vibrate: this.vibrate,
|
||||
snooze: this.snooze
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* An alarm is enabled if and only if it has a registeredAlarm set
|
||||
* with a type of 'normal'. To disable an alarm, any
|
||||
* registeredAlarms are unregistered with mozAlarms and removed
|
||||
* from this.registeredAlarms.
|
||||
*/
|
||||
isEnabled: function() {
|
||||
for (var i in this.registeredAlarms) {
|
||||
// Both 'normal' and 'snooze' registered alarms should be
|
||||
// treated as enabled, because the alarm will imminently fire.
|
||||
if (i === 'normal' || i === 'snooze') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isRepeating: function() {
|
||||
for (var key in this.repeat) {
|
||||
if (this.repeat[key]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
getNextAlarmFireTime: function(relativeTo) {
|
||||
var now = relativeTo || new Date();
|
||||
var nextFire = new Date(now.getTime());
|
||||
nextFire.setHours(this.hour, this.minute, 0, 0);
|
||||
|
||||
while (nextFire <= now ||
|
||||
(this.isRepeating() &&
|
||||
!this.repeat[nextFire.getDay().toString()])) {
|
||||
nextFire.setDate(nextFire.getDate() + 1);
|
||||
}
|
||||
return nextFire;
|
||||
},
|
||||
|
||||
getNextSnoozeFireTime: function(relativeTo) {
|
||||
var now = relativeTo || new Date();
|
||||
return new Date(now.getTime() + this.snooze * 60 * 1000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Schedule an alarm to ring in the future.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @param {'normal'|'snooze'} type
|
||||
*/
|
||||
schedule: function(type) {
|
||||
var alarmDatabase = require('alarm_database'); // circular dependency
|
||||
|
||||
var firedate, promise;
|
||||
if (type === 'normal') {
|
||||
promise = this.cancel(); // Cancel both snooze and regular mozAlarms.
|
||||
firedate = this.getNextAlarmFireTime();
|
||||
} else if (type === 'snooze') {
|
||||
promise = this.cancel('snooze'); // Cancel any snooze mozAlarms.
|
||||
firedate = this.getNextSnoozeFireTime();
|
||||
} else {
|
||||
return Promise.reject('Invalid type for Alarm.schedule().');
|
||||
}
|
||||
|
||||
// Save the alarm to the database first. This ensures we have a
|
||||
// valid ID, and that we've saved any modified properties before
|
||||
// attempting to schedule the alarm.
|
||||
return promise.then(() => alarmDatabase.put(this)).then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Then, schedule the alarm.
|
||||
var req = navigator.mozAlarms.add(firedate, 'ignoreTimezone',
|
||||
{ id: this.id, type: type });
|
||||
req.onerror = reject;
|
||||
req.onsuccess = (evt) => {
|
||||
this.registeredAlarms[type] = evt.target.result;
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
// After scheduling the alarm, this.registeredAlarms has
|
||||
// changed, so we must save that too.
|
||||
}).then(() => alarmDatabase.put(this))
|
||||
.then(() => {
|
||||
this._notifyChanged();
|
||||
}).catch((e) => {
|
||||
console.log('Alarm scheduling error: ' + e.toString());
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel an alarm. If `type` is provided, cancel only that type
|
||||
* ('normal' or 'snooze'). Returns a Promise.
|
||||
*/
|
||||
cancel: function(/* optional */ type) {
|
||||
var types = (type ? [type] : Object.keys(this.registeredAlarms));
|
||||
var alarmDatabase = require('alarm_database'); // circular dependency
|
||||
types.forEach((type) => {
|
||||
var id = this.registeredAlarms[type];
|
||||
navigator.mozAlarms.remove(id);
|
||||
delete this.registeredAlarms[type];
|
||||
});
|
||||
return alarmDatabase.put(this).then(() => {
|
||||
this._notifyChanged();
|
||||
}).catch((e) => {
|
||||
console.log('Alarm cancel error: ' + e.toString());
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
_notifyChanged: function(removed) {
|
||||
// Only update the application if this alarm was actually saved
|
||||
// (i.e. it has an ID).
|
||||
if (this.id) {
|
||||
var alarmDatabase = require('alarm_database');
|
||||
// Updating Alarm info from the DataStore
|
||||
alarmDatabase.getAll()
|
||||
.then((alarms) => {
|
||||
var storeAlarms = [];
|
||||
|
||||
alarms.forEach(function(a) {
|
||||
// Grabbing alarms that are 'normal'
|
||||
if(a.registeredAlarms.normal) {
|
||||
storeAlarms.push(a.getNextAlarmFireTime());
|
||||
}
|
||||
});
|
||||
|
||||
return this.getNextAlarm(storeAlarms);
|
||||
}).then((nextAlarm) => {
|
||||
|
||||
navigator.getDataStores('alarms')
|
||||
.then((stores) => {
|
||||
return [stores[0].getLength(), stores[0]];
|
||||
}).then(([len, store]) => {
|
||||
if(0 === len) {
|
||||
return store.add(nextAlarm);
|
||||
} else {
|
||||
return store.put(nextAlarm, 1);
|
||||
}
|
||||
}).then((id) => {
|
||||
if(id) {
|
||||
console.log('[Clock] ==== Updated Alarm on LockScreen ====');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(removed ? 'alarm-removed' : 'alarm-changed', {
|
||||
detail: { alarm: this }
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an alarm completely from the database, canceling any
|
||||
* pending scheduled mozAlarms.
|
||||
*/
|
||||
delete: function() {
|
||||
var alarmDatabase = require('alarm_database'); // circular dependency
|
||||
return this.cancel().then(() => {
|
||||
return alarmDatabase.delete(this.id).then(() => {
|
||||
this._notifyChanged(/* removed = */ true);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the next scheduled alarm within the next 24 hours
|
||||
* @param {Array} type 'Date'
|
||||
* @returns {Object} This is either an empty obj or hour and minute
|
||||
*/
|
||||
getNextAlarm: function(storeAlarms) {
|
||||
var alarmIndex = [],
|
||||
nextAlarm = {};
|
||||
|
||||
// remove alarms that have more than 24 hrs
|
||||
storeAlarms.forEach( function(val,index) {
|
||||
if((val.getTime() - new Date().getTime()) / 36e5 > 24 ) {
|
||||
alarmIndex.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
// remove last index first
|
||||
alarmIndex.sort(function(a,b) {
|
||||
return b - a;
|
||||
});
|
||||
alarmIndex.forEach( function(val) {
|
||||
storeAlarms.splice(val, 1);
|
||||
});
|
||||
|
||||
// sort alarms
|
||||
storeAlarms.sort(function(a,b){
|
||||
return new Date(a.getTime()) - new Date(b.getTime());
|
||||
});
|
||||
|
||||
if(storeAlarms.length > 0) {
|
||||
var mins = storeAlarms[0].getMinutes();
|
||||
nextAlarm = {
|
||||
hour : '' + storeAlarms[0].getHours(),
|
||||
minute : mins < 10 ? '0' + mins : '' + mins
|
||||
};
|
||||
}
|
||||
return nextAlarm;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
module.exports = Alarm;
|
||||
|
||||
});
|
||||
205
gaia/apps/clock/js/alarm_database.js
Executable file
|
|
@ -0,0 +1,205 @@
|
|||
'use strict';
|
||||
define(function(require) {
|
||||
|
||||
var sounds = require('./sounds');
|
||||
|
||||
/**
|
||||
* The AlarmDatabase stores a list of alarms in IndexedDB. All
|
||||
* mutation operations return Promises, for easy chaining and state
|
||||
* management. This module returns the one-and-only instance of
|
||||
* AlarmDatabase.
|
||||
*/
|
||||
function AlarmDatabase(dbName, storeName, version) {
|
||||
this.dbName = dbName;
|
||||
this.storeName = storeName;
|
||||
this.version = version;
|
||||
|
||||
this.withDatabase = new Promise((resolve, reject) => {
|
||||
var request = indexedDB.open(this.dbName, this.version);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
var db = event.target.result;
|
||||
// Ensure the object store exists.
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
db.createObjectStore(this.storeName, {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = (() => reject(request.errorCode));
|
||||
request.onsuccess = ((event) => resolve(event.target.result));
|
||||
}).then((db) => {
|
||||
// Only return when all of the alarms have been upgraded.
|
||||
return new Promise((resolve, reject) => {
|
||||
// Go through existing alarms here, and make sure they conform
|
||||
// to the latest spec (upgrade old versions, etc.).
|
||||
var transaction = db.transaction(this.storeName, 'readwrite');
|
||||
var store = transaction.objectStore(this.storeName);
|
||||
var cursor = store.openCursor();
|
||||
cursor.onsuccess = (event) => {
|
||||
var cursor = event.target.result;
|
||||
if (cursor) {
|
||||
store.put(this.normalizeAlarmRecord(cursor.value));
|
||||
cursor.continue();
|
||||
}
|
||||
};
|
||||
|
||||
transaction.oncomplete = (() => resolve(db));
|
||||
transaction.onerror = ((evt) => reject(evt.target.errorCode));
|
||||
});
|
||||
}).catch(function(err) {
|
||||
// Explicit err.toString() coercion needed to see a message.
|
||||
console.error('AlarmDatabase Fatal Error:', err.toString());
|
||||
});
|
||||
}
|
||||
|
||||
AlarmDatabase.prototype = {
|
||||
|
||||
/**
|
||||
* Given an Alarm's JSON data (as returned by IndexedDB),
|
||||
* normalize any properties to ensure it conforms to the most
|
||||
* current Alarm specification.
|
||||
*/
|
||||
normalizeAlarmRecord: function(alarm) {
|
||||
if (!alarm.registeredAlarms) {
|
||||
alarm.registeredAlarms = {};
|
||||
}
|
||||
|
||||
if (typeof alarm.enabled !== 'undefined') {
|
||||
delete alarm.enabled;
|
||||
}
|
||||
|
||||
if (typeof alarm.normalAlarmId !== 'undefined') {
|
||||
alarm.registeredAlarms.normal = alarm.normalAlarmId;
|
||||
delete alarm.normalAlarmId;
|
||||
}
|
||||
|
||||
if (typeof alarm.snoozeAlarmId !== 'undefined') {
|
||||
alarm.registeredAlarms.snooze = alarm.snoozeAlarmId;
|
||||
delete alarm.snoozeAlarmId;
|
||||
}
|
||||
|
||||
var newRepeat = {};
|
||||
var i;
|
||||
|
||||
// Map '1111100' string bitmap to a repeat object with day properties.
|
||||
if (typeof alarm.repeat === 'string') {
|
||||
for (i = 0; i < alarm.repeat.length; i++) {
|
||||
if (alarm.repeat[i.toString()] === '1') {
|
||||
newRepeat[i.toString()] = true;
|
||||
}
|
||||
}
|
||||
alarm.repeat = newRepeat;
|
||||
} else if (typeof alarm.repeat === 'object') {
|
||||
var keys = Object.keys(alarm.repeat);
|
||||
if (keys.length !== 0 && Number.isNaN(parseInt(keys[0]))) {
|
||||
var oldKeys = ['sunday', 'monday', 'tuesday', 'wednesday',
|
||||
'thursday', 'friday', 'saturday'];
|
||||
|
||||
for (i = 0; i < keys.length; i++) {
|
||||
var index = oldKeys.indexOf(keys[i]);
|
||||
if (index !== -1) {
|
||||
newRepeat[index] = alarm.repeat[keys[i]];
|
||||
}
|
||||
}
|
||||
alarm.repeat = newRepeat;
|
||||
}
|
||||
} else {
|
||||
alarm.repeat = newRepeat;
|
||||
}
|
||||
|
||||
// Pre-April-2014 code may have stored 'vibrate' and 'sound' as
|
||||
// the string "0", and hour/minute as strings.
|
||||
alarm.sound = sounds.normalizeSound(
|
||||
alarm.sound !== '0' ? alarm.sound : null);
|
||||
alarm.vibrate = (alarm.vibrate && alarm.vibrate !== '0') || false;
|
||||
alarm.hour = parseInt(alarm.hour, 10);
|
||||
alarm.minute = parseInt(alarm.minute, 10);
|
||||
|
||||
return alarm;
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute a database store request with the given method and
|
||||
* arguments, returning a Promise that will be fulfilled with the
|
||||
* Store's result.
|
||||
*/
|
||||
withStoreRequest: function(method /*, args... */) {
|
||||
var args = Array.slice(arguments, 1);
|
||||
var readmode = (/get/.test(method) ? 'readonly' : 'readwrite');
|
||||
return this.withDatabase.then((database) => {
|
||||
var store = database
|
||||
.transaction(this.storeName, readmode)
|
||||
.objectStore(this.storeName);
|
||||
if (method === 'getAll') {
|
||||
return objectStoreGetAll(store);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
var request = store[method].apply(store, args);
|
||||
request.onsuccess = (() => resolve(request.result));
|
||||
request.onerror = (() => reject(request.errorCode));
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
put: function(alarm) {
|
||||
var data = alarm.toJSON();
|
||||
if (!data.id) {
|
||||
delete data.id; // IndexedDB requires _no_ ID key, not null/undefined.
|
||||
}
|
||||
return this.withStoreRequest('put', data).then((id) => {
|
||||
alarm.id = id;
|
||||
});
|
||||
},
|
||||
|
||||
getAll: function() {
|
||||
var Alarm = require('alarm'); // Circular dependency.
|
||||
return this.withStoreRequest('getAll').then((alarms) => {
|
||||
return alarms.map((data) => new Alarm(data));
|
||||
});
|
||||
},
|
||||
|
||||
get: function(id) {
|
||||
var Alarm = require('alarm'); // Circular dependency.
|
||||
return this.withStoreRequest('get', id).then((data) => {
|
||||
return new Alarm(data);
|
||||
});
|
||||
},
|
||||
|
||||
delete: function(id) {
|
||||
return this.withStoreRequest('delete', id);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Return all records from an ObjectStore. This function is
|
||||
* non-standard, but is such a common pattern that it has actually
|
||||
* been included in certain implementations of IndexedDB. It is
|
||||
* extracted here for clarity.
|
||||
*/
|
||||
function objectStoreGetAll(objectStore) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var items = [];
|
||||
var cursor = objectStore.openCursor();
|
||||
cursor.onerror = reject;
|
||||
cursor.onsuccess = function(event) {
|
||||
var cursor = event.target.result;
|
||||
if (cursor) {
|
||||
items.push(cursor.value);
|
||||
cursor.continue();
|
||||
}
|
||||
else {
|
||||
resolve(items);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// For Clock, we only use one database and store, both named 'alarms'.
|
||||
// Right now, we're on version 7.
|
||||
return new AlarmDatabase('alarms', 'alarms', 7);
|
||||
});
|
||||
165
gaia/apps/clock/js/app.js
Executable file
|
|
@ -0,0 +1,165 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
/* global IntlHelper */
|
||||
|
||||
var Tabs = require('tabs');
|
||||
var View = require('view');
|
||||
var connectionHandler = require('connection/handler');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Global Application event handling and paging
|
||||
*/
|
||||
var App = {
|
||||
/**
|
||||
* Load the Tabs and Panels, attach events and navigate to the default view.
|
||||
*/
|
||||
init: function() {
|
||||
connectionHandler.init();
|
||||
IntlHelper.define('digit-nopadding', 'number', {
|
||||
style: 'decimal',
|
||||
useGrouping: false
|
||||
});
|
||||
this.tabs = new Tabs(document.getElementById('clock-tabs'));
|
||||
|
||||
window.addEventListener('hashchange', this);
|
||||
window.addEventListener('visibilitychange', this);
|
||||
// Tell audio channel manager that we want to adjust the alarm channel
|
||||
// if the user press the volumeup/volumedown buttons in Clock.
|
||||
if (navigator.mozAudioChannelManager) {
|
||||
navigator.mozAudioChannelManager.volumeControlChannel = 'alarm';
|
||||
}
|
||||
|
||||
this.visible = !document.hidden;
|
||||
this.panels = Array.prototype.map.call(
|
||||
document.querySelectorAll('[data-panel-id]'),
|
||||
function(element) {
|
||||
var panel = {
|
||||
el: element,
|
||||
fragment: element.dataset.panelId.replace('_', '-') + '-panel',
|
||||
instance: null
|
||||
};
|
||||
|
||||
return panel;
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
window.performance.mark('navigationLoaded');
|
||||
|
||||
this.navigate({ hash: '#alarm-panel' }, function() {
|
||||
// Dispatch an event to mark when we've finished loading.
|
||||
// At this point, the navigation is usable, and the primary
|
||||
// alarm list tab has begun loading.
|
||||
window.performance.mark('navigationInteractive');
|
||||
}.bind(this));
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load and instantiate the specified panel (when necessary).
|
||||
*
|
||||
* @param {Object} panel - An object describing the panel. It must contain
|
||||
* either an `el` attribute (defining the panel's
|
||||
* containing element) or an `instance` attribute
|
||||
* (defining the instantiated Panel itself).
|
||||
* @param {Function} [callback] - A function that will be invoked with the
|
||||
* instantiated panel once it is loaded.
|
||||
*/
|
||||
loadPanel: function(panel, callback) {
|
||||
if (panel.instance) {
|
||||
callback && setTimeout(callback, 0, panel);
|
||||
return;
|
||||
}
|
||||
|
||||
var moduleId = 'panels/' + panel.el.dataset.panelId + '/main';
|
||||
|
||||
require([moduleId], function(PanelModule) {
|
||||
panel.instance = View.instance(panel.el, PanelModule);
|
||||
callback && callback(panel);
|
||||
});
|
||||
},
|
||||
|
||||
alarmListLoaded: function() {
|
||||
// Performance testing markers. See <https://bugzil.la/996038>.
|
||||
// At this point, the alarm list has been loaded, and all facets
|
||||
// of Clock are now interactive. The other panels are lazily
|
||||
// loaded when the user switches tabs.
|
||||
window.performance.mark('visuallyLoaded');
|
||||
window.performance.mark('contentInteractive');
|
||||
window.performance.mark('fullyLoaded');
|
||||
},
|
||||
|
||||
/**
|
||||
* split each event handler into it's own method
|
||||
*/
|
||||
handleEvent: function(event) {
|
||||
var handler = this['on' + event.type];
|
||||
if (handler) {
|
||||
return handler.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* navigate between pages.
|
||||
*
|
||||
* @param {object} data Options for navigation.
|
||||
* @param {string} data.hash The hash of the panel id. I.E. '#alarm-panel'.
|
||||
* @param {function} callback Callback to invoke when done.
|
||||
*/
|
||||
navigate: function(data, callback) {
|
||||
var currentIndex = this.panels.indexOf(this.currentPanel);
|
||||
this.panels.forEach(function(panel, panelIndex) {
|
||||
if ('#' + panel.fragment === data.hash) {
|
||||
this.loadPanel(panel, function() {
|
||||
var instance = panel.instance;
|
||||
instance.navData = data.data || null;
|
||||
instance.active = true;
|
||||
instance.visible = true;
|
||||
if (currentIndex !== -1 && currentIndex !== panelIndex) {
|
||||
var direction = currentIndex < panelIndex;
|
||||
requestAnimationFrame(function startAnimation(oldPanel) {
|
||||
instance.transition =
|
||||
direction ? 'slide-in-right' : 'slide-in-left';
|
||||
|
||||
oldPanel.instance.transition =
|
||||
direction ? 'slide-out-left' : 'slide-out-right';
|
||||
}.bind(null, this.currentPanel));
|
||||
}
|
||||
this.currentPanel = panel;
|
||||
callback && callback();
|
||||
}.bind(this));
|
||||
} else {
|
||||
if (panel.instance) {
|
||||
panel.instance.active = false;
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
this.currentHash = data.hash;
|
||||
},
|
||||
|
||||
/**
|
||||
* Navigate to the new hash.
|
||||
*/
|
||||
onhashchange: function(event) {
|
||||
if (this.currentHash === location.hash) {
|
||||
return;
|
||||
}
|
||||
this.navigate({ hash: location.hash });
|
||||
},
|
||||
|
||||
/**
|
||||
* Whenever the application gains/loses focus, inform the current panel of
|
||||
* its visibility loss.
|
||||
*/
|
||||
onvisibilitychange: function(event) {
|
||||
this.visible = !document.hidden;
|
||||
if (this.currentPanel) {
|
||||
this.currentPanel.visible = this.visible;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return App;
|
||||
|
||||
});
|
||||
42
gaia/apps/clock/js/async_queue.js
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
'use strict';
|
||||
define(function(require) {
|
||||
|
||||
/**
|
||||
* A serial queue of functions. Call `.push()` to add a function to
|
||||
* the task; when each function completes (as a result of calling
|
||||
* 'done()'), the next item in the queue will be executed.
|
||||
*/
|
||||
function AsyncQueue() {
|
||||
this.queue = [];
|
||||
this.current = null;
|
||||
}
|
||||
|
||||
AsyncQueue.prototype = {
|
||||
/**
|
||||
* Add a function to the execution queue. It should accept one
|
||||
* argument, a 'done' callback.
|
||||
*/
|
||||
push: function(fn) {
|
||||
if (!this.current) {
|
||||
this._startTask(fn);
|
||||
} else {
|
||||
this.queue.push(fn);
|
||||
}
|
||||
},
|
||||
|
||||
_startTask: function(fn) {
|
||||
this.current = fn;
|
||||
fn(this._nextTask.bind(this));
|
||||
},
|
||||
|
||||
_nextTask: function() {
|
||||
this.current = null;
|
||||
if (this.queue.length) {
|
||||
this._startTask(this.queue.shift());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return AsyncQueue;
|
||||
});
|
||||
|
||||
185
gaia/apps/clock/js/audio_manager.js
Executable file
|
|
@ -0,0 +1,185 @@
|
|||
/* global define */
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* The Settings App stores volumes in the range [0, 15] inclusive.
|
||||
* Whenever we need to play sounds, though, the Audio object
|
||||
* requires a float between [0.0, 1.0]. The conversion has to happen
|
||||
* somewhere. The AudioManager here draws the line right out of what
|
||||
* gets read from mozSettings.
|
||||
*
|
||||
* In other words, the conversion is not important to clients of
|
||||
* this class, who should treat the volume as a float with no
|
||||
* conversion. The only weirdness here is that unit tests must be
|
||||
* aware of the slight rounding differences when converting from a
|
||||
* float to the system level.
|
||||
*/
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// VolumeManager
|
||||
|
||||
function isValidVolume(volume) {
|
||||
return (typeof volume === 'number' &&
|
||||
volume <= 1.0 &&
|
||||
volume >= 0.0);
|
||||
}
|
||||
|
||||
var VOLUME_SETTING = 'audio.volume.alarm';
|
||||
var SYSTEM_VOLUME_MAX = 15;
|
||||
function systemVolumeToFloat(volume) {
|
||||
return (volume / SYSTEM_VOLUME_MAX);
|
||||
}
|
||||
|
||||
function floatToSystemVolume(volume) {
|
||||
return Math.round(volume * SYSTEM_VOLUME_MAX);
|
||||
}
|
||||
|
||||
function requestAlarmSystemVolume() {
|
||||
// Asynchronously load the alarm volume from mozSettings.
|
||||
return new Promise(function(resolve, reject) {
|
||||
var lock = navigator.mozSettings.createLock();
|
||||
var req = lock.get(VOLUME_SETTING);
|
||||
req.onsuccess = function() {
|
||||
var volume = systemVolumeToFloat(req.result[VOLUME_SETTING]);
|
||||
if (isValidVolume(volume)) {
|
||||
globalVolumeManager._volume = volume;
|
||||
resolve(volume);
|
||||
}
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
var DEFAULT_VOLUME = 1.0;
|
||||
resolve(DEFAULT_VOLUME);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function VolumeManager() {
|
||||
this.VOLUME_KEY = 'defaultAlarmVolume';
|
||||
this.DEFAULT_VOLUME = 1.0;
|
||||
this._volume = this.DEFAULT_VOLUME;
|
||||
|
||||
if (navigator.mozSettings) {
|
||||
navigator.mozSettings.addObserver(
|
||||
VOLUME_SETTING,
|
||||
this.onSystemAlarmVolumeChange.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
VolumeManager.prototype = {
|
||||
onSystemAlarmVolumeChange: function(e) {
|
||||
// don't use the setter here
|
||||
this._volume = systemVolumeToFloat(e.settingValue);
|
||||
},
|
||||
|
||||
get volume() {
|
||||
return this._volume;
|
||||
},
|
||||
|
||||
set volume(volume) {
|
||||
this.setVolume(volume);
|
||||
},
|
||||
|
||||
/** Set the volume with an optional completion callback. */
|
||||
setVolume: function(volume, cb) {
|
||||
if (isValidVolume(volume)) {
|
||||
this._volume = volume;
|
||||
|
||||
if (navigator.mozSettings) {
|
||||
var lock = navigator.mozSettings.createLock();
|
||||
|
||||
var opts = {};
|
||||
opts[VOLUME_SETTING] = floatToSystemVolume(volume);
|
||||
var req = lock.set(opts);
|
||||
|
||||
if (cb) {
|
||||
req.onsuccess = cb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// AudioPlayer
|
||||
|
||||
var globalVolumeManager = new VolumeManager();
|
||||
|
||||
/**
|
||||
* The AudioPlayer class manages the playback of alarm ringtones. It
|
||||
* is lazy-loading, so that you can instantiate it immediately;
|
||||
* Audio objects are not actually created or loaded until you need
|
||||
* to play a sound.
|
||||
*/
|
||||
function AudioPlayer() {
|
||||
this._audio = null;
|
||||
}
|
||||
|
||||
AudioPlayer.prototype = {
|
||||
|
||||
/**
|
||||
* Play a ringtone from the shared/resources/media/alarms
|
||||
* directory, using the current global volume settings by default.
|
||||
* You can override the volume through opts.volume.
|
||||
*
|
||||
* @param {string} ringtoneName
|
||||
* @param {number} opts.volume Value between 0 and 1
|
||||
*/
|
||||
playRingtone: function(ringtoneName, opts) {
|
||||
var volume = globalVolumeManager.volume;
|
||||
if (opts && 'volume' in opts) {
|
||||
volume = opts.volume;
|
||||
}
|
||||
this._prepare(); // Load up the audio element.
|
||||
this._audio.pause();
|
||||
this._audio.src = 'shared/resources/media/alarms/' + ringtoneName;
|
||||
this._audio.load(); // Required per MDN's HTMLMediaElement spec.
|
||||
this._audio.volume = volume;
|
||||
this._audio.play();
|
||||
},
|
||||
|
||||
/**
|
||||
* Pause the currently-playing audio, if possible.
|
||||
*/
|
||||
pause: function() {
|
||||
if (this._audio) {
|
||||
this._audio.pause();
|
||||
}
|
||||
},
|
||||
|
||||
// Private methods:
|
||||
|
||||
/**
|
||||
* Instantiate the Audio element and prepare it for playback.
|
||||
* For internal use only.
|
||||
* @private
|
||||
*/
|
||||
_prepare: function() {
|
||||
if (!this._audio) {
|
||||
this._audio = new Audio();
|
||||
this._audio.mozAudioChannelType = 'alarm';
|
||||
this._audio.loop = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getAlarmVolume: function() {
|
||||
return globalVolumeManager.volume;
|
||||
},
|
||||
requestAlarmVolume: function() {
|
||||
return requestAlarmSystemVolume();
|
||||
},
|
||||
setAlarmVolume: function(volume, cb) {
|
||||
globalVolumeManager.setVolume(volume, cb);
|
||||
},
|
||||
createAudioPlayer: function() {
|
||||
return new AudioPlayer();
|
||||
},
|
||||
// Exposed for tests:
|
||||
systemVolumeToFloat: systemVolumeToFloat,
|
||||
floatToSystemVolume: floatToSystemVolume
|
||||
};
|
||||
});
|
||||
1
gaia/apps/clock/js/banner/banner.html
Executable file
|
|
@ -0,0 +1 @@
|
|||
<p data-l10n-id="${noticeId}" data-l10n-args="${noticeArgs}"></p>
|
||||
88
gaia/apps/clock/js/banner/main.js
Executable file
|
|
@ -0,0 +1,88 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Template = require('template');
|
||||
var Utils = require('utils');
|
||||
var html = require('text!banner/banner.html');
|
||||
|
||||
function Banner(node) {
|
||||
// Accept a reference to an element or the element id
|
||||
if (typeof node === 'string') {
|
||||
this.notice = document.getElementById(node);
|
||||
} else {
|
||||
this.notice = node;
|
||||
}
|
||||
// Accept an optional reference to template element id
|
||||
this.tmpl = new Template(html);
|
||||
// Store a reference to timeout to debounce banner
|
||||
this.timeout = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
Banner.prototype = {
|
||||
|
||||
constructor: Banner,
|
||||
|
||||
render: function bn_render(alarmTime) {
|
||||
var timeLeft, tl, countdownType, localTimes;
|
||||
|
||||
timeLeft = +alarmTime - Date.now();
|
||||
// generate human readable numbers to pass to localization function
|
||||
tl = Utils.dateMath.fromMS(timeLeft, {
|
||||
unitsPartial: ['days', 'hours', 'minutes']
|
||||
});
|
||||
|
||||
if (tl.days) {
|
||||
countdownType = 'countdown_moreThanADay';
|
||||
localTimes = {
|
||||
'days': tl.days,
|
||||
'hours': tl.hours
|
||||
};
|
||||
} else if (tl.hours > 0) {
|
||||
countdownType = 'countdown_moreThanAnHour';
|
||||
localTimes = {
|
||||
'hours': tl.hours,
|
||||
'minutes': tl.minutes
|
||||
};
|
||||
} else {
|
||||
countdownType = 'countdown_lessThanAnHour';
|
||||
localTimes = {
|
||||
'minutes': tl.minutes
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
noticeId: countdownType,
|
||||
noticeArgs: JSON.stringify(localTimes)
|
||||
};
|
||||
},
|
||||
|
||||
show: function bn_show(alarmTime) {
|
||||
// Render the Banner notice
|
||||
this.notice.innerHTML = this.tmpl.interpolate(
|
||||
this.render(alarmTime)
|
||||
);
|
||||
// 'visible' class controls the animation
|
||||
this.notice.classList.add('visible');
|
||||
// use this object rather than a function to retain context
|
||||
this.notice.addEventListener('click', this);
|
||||
// Debounce timer in case alarms are added more quickly than 4 seconds
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
// After 4 seconds, remove the banner
|
||||
this.timeout = setTimeout(this.hide.bind(this), 4000);
|
||||
},
|
||||
|
||||
hide: function bn_hide() {
|
||||
this.notice.classList.remove('visible');
|
||||
this.notice.removeEventListener('click', this);
|
||||
},
|
||||
|
||||
handleEvent: function bn_handleEvent() {
|
||||
this.hide();
|
||||
}
|
||||
};
|
||||
|
||||
return Banner;
|
||||
});
|
||||
49
gaia/apps/clock/js/connection/alarm.js
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
define(function(require, exports) {
|
||||
'use strict';
|
||||
|
||||
var Alarm = require('alarm');
|
||||
var parseTime = require('ext/parse_loose_time');
|
||||
|
||||
exports.onmessage = function(event, port) {
|
||||
if (event.data.type !== 'create') {
|
||||
port.postMessage({
|
||||
error: `Error: Invalid gaia_alarm message type "${event.data.type}"`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var time = event.data.time;
|
||||
var timeObject = typeof time === 'object' ? time : parseTime(time);
|
||||
|
||||
if (!isValidTime(timeObject)) {
|
||||
port.postMessage({
|
||||
error: `Invalid alarm time "${time}"`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var alarm = new Alarm(timeObject);
|
||||
alarm.schedule('normal').then(() => {
|
||||
window.dispatchEvent(new CustomEvent('alarm-changed', {
|
||||
detail: { alarm: alarm, showBanner: false }
|
||||
}));
|
||||
port.postMessage({
|
||||
hour: timeObject.hour,
|
||||
minute: timeObject.minute,
|
||||
time: alarm.getNextAlarmFireTime().getTime()
|
||||
});
|
||||
}).catch(err => {
|
||||
port.postMessage({
|
||||
error: `${err.message} ${err.stack}`
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function isValidTime(time) {
|
||||
// time can be "null" and "typeof null === 'object'"
|
||||
return time && typeof time === 'object' && 'hour' in time &&
|
||||
time.hour >= 0 && time.hour <= 24 && 'minute' in time &&
|
||||
time.minute >= 0 && time.minute <= 59;
|
||||
}
|
||||
|
||||
});
|
||||
37
gaia/apps/clock/js/connection/handler.js
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
define(function(require, exports) {
|
||||
'use strict';
|
||||
|
||||
var handlers = {
|
||||
'gaia_alarm': require('./alarm'),
|
||||
'gaia_timer': require('./timer')
|
||||
};
|
||||
|
||||
exports.init = function() {
|
||||
navigator.mozSetMessageHandler('connection', req => {
|
||||
var port = req.port;
|
||||
var handler = handlers[req.keyword];
|
||||
|
||||
if (!handler) {
|
||||
console.error(`can't find handler for connection "${req.keyword}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
// IACMessagePort don't support onstart & onclose events as of 2015-07-21
|
||||
// but for timer we do need to emulate this behavior
|
||||
if (handler.onstart) {
|
||||
handler.onstart(req);
|
||||
}
|
||||
|
||||
if (handler.onmessage || handler.onclose) {
|
||||
port.onmessage = event => {
|
||||
if (event.data.type === 'close' && handler.onclose) {
|
||||
handler.onclose(req);
|
||||
return;
|
||||
}
|
||||
handler.onmessage && handler.onmessage(event, port);
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
78
gaia/apps/clock/js/connection/timer.js
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
define(function(require, exports) {
|
||||
'use strict';
|
||||
|
||||
var Timer = require('timer');
|
||||
var parseDuration = require('ext/parse_duration');
|
||||
var port;
|
||||
var timer;
|
||||
|
||||
exports.onstart = function(request) {
|
||||
port = request.port;
|
||||
|
||||
window.addEventListener('timer-start', onTimerEvent);
|
||||
window.addEventListener('timer-pause', onTimerEvent);
|
||||
window.addEventListener('timer-end', onTimerEvent);
|
||||
window.addEventListener('timer-tick', onTimerEvent);
|
||||
};
|
||||
|
||||
exports.onclose = function() {
|
||||
port = null;
|
||||
timer = null;
|
||||
|
||||
window.removeEventListener('timer-start', onTimerEvent);
|
||||
window.removeEventListener('timer-pause', onTimerEvent);
|
||||
window.removeEventListener('timer-end', onTimerEvent);
|
||||
window.removeEventListener('timer-tick', onTimerEvent);
|
||||
};
|
||||
|
||||
function onTimerEvent(evt) {
|
||||
if (!port) {
|
||||
return;
|
||||
}
|
||||
|
||||
port.postMessage({
|
||||
type: evt.type,
|
||||
remaining: evt.detail.remaining
|
||||
});
|
||||
}
|
||||
|
||||
exports.onmessage = function(event) {
|
||||
if (timer) {
|
||||
exec(event);
|
||||
} else {
|
||||
Timer.singleton((err, t) => {
|
||||
timer = t;
|
||||
exec(event);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function exec(event) {
|
||||
var { type, duration } = event.data;
|
||||
if (type === 'create') {
|
||||
create(duration);
|
||||
return;
|
||||
}
|
||||
// type: [start, pause, cancel]
|
||||
timer[type]();
|
||||
timer.commit();
|
||||
}
|
||||
|
||||
function create(inputDuration) {
|
||||
var duration = parseDuration(String(inputDuration));
|
||||
|
||||
if (!duration) {
|
||||
port.postMessage({
|
||||
error: `Invalid timer duration "${inputDuration}"`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// we don't check the timer state since we assume the user wants to override
|
||||
// any running timer and start it immediately
|
||||
timer.duration = duration;
|
||||
timer.start();
|
||||
timer.commit();
|
||||
}
|
||||
|
||||
});
|
||||
9
gaia/apps/clock/js/constants.js
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
define(function(require, exports) {
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Constants
|
||||
|
||||
// Random day that happens to be Sunday
|
||||
exports.KNOWN_SUNDAY = new Date(2015, 8, 20);
|
||||
});
|
||||
17
gaia/apps/clock/js/css.js
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
'use strict';
|
||||
// Loader plugin for loading CSS. Does not guarantee loading via onload
|
||||
// watching, just inserts link tag.
|
||||
define({
|
||||
load: function(id, require, onload, config) {
|
||||
if (config.isBuild) {
|
||||
return onload();
|
||||
}
|
||||
|
||||
var link = document.createElement('link');
|
||||
link.type = 'text/css';
|
||||
link.rel = 'stylesheet';
|
||||
link.href = require.toUrl(id + '.css');
|
||||
link.addEventListener('load', onload, false);
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
});
|
||||
70
gaia/apps/clock/js/ext/parse_duration.js
Executable file
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* parse-duration v0.1.1
|
||||
* Released under the MIT License
|
||||
* https://github.com/jkroso/parse-duration
|
||||
*/
|
||||
define(function(require, exports, module) {
|
||||
|
||||
var duration = /(-?\d*\.?\d+(?:e[-+]?\d+)?)\s*([a-zμ]*)/ig
|
||||
|
||||
module.exports = parse
|
||||
|
||||
/**
|
||||
* conversion ratios
|
||||
*/
|
||||
|
||||
parse.nanosecond =
|
||||
parse.ns = 1 / 1e6
|
||||
|
||||
parse.μs =
|
||||
parse.microsecond = 1 / 1e3
|
||||
|
||||
parse.millisecond =
|
||||
parse.ms = 1
|
||||
|
||||
parse.second =
|
||||
parse.sec =
|
||||
parse.s = parse.ms * 1000
|
||||
|
||||
parse.minute =
|
||||
parse.min =
|
||||
parse.m = parse.s * 60
|
||||
|
||||
parse.hour =
|
||||
parse.hr =
|
||||
parse.h = parse.m * 60
|
||||
|
||||
parse.day =
|
||||
parse.d = parse.h * 24
|
||||
|
||||
parse.week =
|
||||
parse.wk =
|
||||
parse.w = parse.d * 7
|
||||
|
||||
parse.month = parse.d * (365.25 / 12)
|
||||
|
||||
parse.year =
|
||||
parse.yr =
|
||||
parse.y = parse.d * 365.25
|
||||
|
||||
/**
|
||||
* convert `str` to ms
|
||||
*
|
||||
* @param {String} str
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
function parse(str){
|
||||
var result = 0
|
||||
// ignore commas
|
||||
str = str.replace(/(\d),(\d)/g, '$1$2')
|
||||
str.replace(duration, function(_, n, units){
|
||||
units = parse[units]
|
||||
|| parse[units.toLowerCase().replace(/s$/, '')]
|
||||
|| 1
|
||||
result += parseFloat(n, 10) * units
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
});
|
||||
138
gaia/apps/clock/js/ext/parse_loose_time.js
Executable file
|
|
@ -0,0 +1,138 @@
|
|||
/**
|
||||
* parse-loose-time v1.0.0
|
||||
* https://github.com/millermedeiros/parse-loose-time/
|
||||
* released under the MIT License
|
||||
*/
|
||||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
// Release the ZALGO!
|
||||
var numeric = /\d{1,2}[^\d]*(\d{2})?/i;
|
||||
var oclock = /\d+.*o.*clock/i;
|
||||
var british = /(half|quarter|\d+).*(to|past)[^\d]*\d{1,2}/i;
|
||||
var knownWords = [
|
||||
'noon',
|
||||
'noontime',
|
||||
'midday',
|
||||
'half-day',
|
||||
'midnight'
|
||||
];
|
||||
// match "p" after digit for "pm"
|
||||
var rPm = /\d\s*(o.*clock\s*)?p/i;
|
||||
|
||||
var checkKnowWords = {
|
||||
test: function(str) {
|
||||
return knownWords.indexOf(str) !== -1;
|
||||
}
|
||||
};
|
||||
|
||||
var map = [
|
||||
[checkKnowWords, parseWords],
|
||||
[british, parseBritish],
|
||||
[oclock, parseOClock],
|
||||
[numeric, parseNumeric]
|
||||
];
|
||||
|
||||
module.exports = parseTime;
|
||||
function parseTime(time) {
|
||||
time = time.trim().toLowerCase();
|
||||
// node 0.12 don't support Array#find :/
|
||||
var fn;
|
||||
map.some(function(a) {
|
||||
if (a[0].test(time)) {
|
||||
fn = a[1];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return fn ? fn(time) : null;
|
||||
}
|
||||
|
||||
function parseNumeric(str) {
|
||||
var hour;
|
||||
var minute;
|
||||
var num = extractNumbers(str);
|
||||
|
||||
switch (num.length) {
|
||||
case 4:
|
||||
hour = num.slice(0, 2);
|
||||
minute = num.slice(2, 4);
|
||||
break;
|
||||
case 3:
|
||||
hour = num.slice(0, 1);
|
||||
minute = num.slice(1, 3);
|
||||
break;
|
||||
case 2:
|
||||
case 1:
|
||||
hour = num.slice(0, 2);
|
||||
minute = 0;
|
||||
break;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
hour = Number(hour);
|
||||
minute = Number(minute);
|
||||
|
||||
if (num.length === 2 && hour > 24) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hour < 12 && str.match(rPm)) {
|
||||
hour += 12;
|
||||
}
|
||||
|
||||
return {
|
||||
hour: Math.min(hour, 24),
|
||||
minute: Math.min(minute, 59)
|
||||
};
|
||||
}
|
||||
|
||||
function extractNumbers(str) {
|
||||
return str.replace(/[^0-9]/g, '');
|
||||
}
|
||||
|
||||
function parseOClock(str) {
|
||||
// yes, we will consider "1:23 o'clock" the same as "1 o'clock"
|
||||
var time = parseNumeric(str);
|
||||
if (!time) {
|
||||
return null;
|
||||
}
|
||||
time.minute = 0;
|
||||
return time;
|
||||
}
|
||||
|
||||
function parseBritish(str) {
|
||||
// need to handle case where it is a custom numeric value
|
||||
var parts = str.match(/(.+)(to|past)(.+)/i);
|
||||
var time = parseNumeric(parts[3]);
|
||||
if (!time) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var diff;
|
||||
if (str.indexOf('quarter') !== -1) {
|
||||
diff = 15;
|
||||
} else if (str.indexOf('half') !== -1) {
|
||||
diff = 30;
|
||||
} else {
|
||||
diff = Number(extractNumbers(parts[1]));
|
||||
}
|
||||
|
||||
var isTo = str.indexOf('to') !== -1;
|
||||
if (isTo) {
|
||||
time.hour -= 1;
|
||||
time.minute = 60 - diff;
|
||||
} else {
|
||||
time.minute = diff;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
function parseWords(str) {
|
||||
if (str === 'midnight') {
|
||||
return { hour: 0, minute: 0 };
|
||||
}
|
||||
return { hour: 12, minute: 0 };
|
||||
}
|
||||
});
|
||||
196
gaia/apps/clock/js/form_button.js
Executable file
|
|
@ -0,0 +1,196 @@
|
|||
// outer IIFE
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Utils = require('utils');
|
||||
|
||||
function createButton(formButton) {
|
||||
var button = document.createElement(formButton.tagName);
|
||||
button.className = formButton.className;
|
||||
if (formButton.id) {
|
||||
button.id = formButton.id;
|
||||
}
|
||||
var input = formButton.input;
|
||||
input.parentNode.insertBefore(button, input.nextSibling);
|
||||
formButton.button = button;
|
||||
}
|
||||
|
||||
/**
|
||||
* A FormButton is a button that triggers an input. The text
|
||||
* of the currently selected value will display on the buttons's face.
|
||||
*
|
||||
* The `config` paramater supports the following optional properties.
|
||||
* `formatLabel` - A function that is given the current value of the input
|
||||
* and should return a string which will be used as the textContent of
|
||||
* the button.
|
||||
*
|
||||
* `tagName` - The name of the tag to create and insert into the
|
||||
* document as the main button used to trigger the input. The default
|
||||
* value is 'button'
|
||||
*
|
||||
* `className` The value of the className property that will be assigned to
|
||||
* the button element the default value is 'icon icon-dialog'.
|
||||
*
|
||||
* `id` - A string that is used as the id of the button element.
|
||||
*
|
||||
* @constructor
|
||||
* @param {HTMLElement} input The input element to trigger.
|
||||
* @param {Object} config An optional config object.
|
||||
*
|
||||
*/
|
||||
function FormButton(input, config) {
|
||||
config = config || {};
|
||||
Utils.extend(this, config);
|
||||
|
||||
this.input = input;
|
||||
createButton(this);
|
||||
|
||||
this.input.classList.add('form-button-input');
|
||||
// hide input
|
||||
this.input.classList.add('form-button-hide');
|
||||
|
||||
// set isSelect
|
||||
Object.defineProperty(this, 'isSelect', {
|
||||
value: this.input.nodeName === 'SELECT'
|
||||
});
|
||||
|
||||
this.button.addEventListener('click', this.focus.bind(this), false);
|
||||
|
||||
input.addEventListener('change', this.refresh.bind(this), false);
|
||||
input.addEventListener('blur', this.refresh.bind(this), false);
|
||||
|
||||
// Bind this.refresh so that the listener can be easily removed.
|
||||
this.refresh = this.refresh.bind(this);
|
||||
// Update the dropdown when the language changes.
|
||||
window.addEventListener('localized', this.refresh);
|
||||
window.addEventListener('timeformatchange', this.refresh);
|
||||
}
|
||||
|
||||
FormButton.prototype = {
|
||||
|
||||
/** Remove all event handlers. */
|
||||
destroy: function() {
|
||||
window.removeEventListener('localized', this.refresh);
|
||||
},
|
||||
|
||||
/**
|
||||
* focus Triggers a focus event on the input associated with this
|
||||
* FormButton.
|
||||
*
|
||||
* @param {Object} event an event object.
|
||||
*/
|
||||
focus: function(event) {
|
||||
event.preventDefault();
|
||||
setTimeout(this.input.focus.bind(this.input), 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* refresh Updates the label text on the button to reflect
|
||||
* the current value of the input.
|
||||
*
|
||||
*/
|
||||
refresh: function() {
|
||||
var value = this.value;
|
||||
|
||||
var label = this.formatLabel(value);
|
||||
|
||||
// In some cases formatLabel returns a Promise that returns l10nId
|
||||
var labelPromise = label.then ? label : Promise.resolve(label);
|
||||
|
||||
return labelPromise.then((l10nId) => {
|
||||
if (typeof l10nId === 'string') {
|
||||
this.button.setAttribute('data-l10n-id', l10nId);
|
||||
} else if (l10nId.raw) {
|
||||
this.button.removeAttribute('data-l10n-id');
|
||||
this.button.textContent = l10nId.raw;
|
||||
} else if (l10nId.id) {
|
||||
document.l10n.setAttributes(this.button, l10nId.id, l10nId.args);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* value Returns the current value of the input.
|
||||
*
|
||||
* @return {String|Object} The value of the input.
|
||||
*
|
||||
*/
|
||||
get value() {
|
||||
if (this.isSelect) {
|
||||
if (this.input.multiple) {
|
||||
var selectedOptions = {};
|
||||
var options = this.input.options;
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
if (options[i].selected) {
|
||||
selectedOptions[options[i].value] = true;
|
||||
}
|
||||
}
|
||||
return selectedOptions;
|
||||
}
|
||||
if (this.input.selectedIndex !== -1) {
|
||||
return Utils.getSelectedValueByIndex(this.input);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// input node
|
||||
return this.input.value;
|
||||
},
|
||||
|
||||
/**
|
||||
* value sets the current value of the input and update's the
|
||||
* button text.
|
||||
*
|
||||
* @param {String|Object} value A string of the current values or an
|
||||
* object with properties that map to input options if the input is
|
||||
* a multi select.
|
||||
*
|
||||
*/
|
||||
set value(value) {
|
||||
if (this.isSelect) {
|
||||
if (this.input.multiple) {
|
||||
// multi select
|
||||
var options = this.input.options;
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
options[i].selected = value[options[i].value] === true;
|
||||
}
|
||||
} else {
|
||||
// normal select element
|
||||
Utils.changeSelectByValue(this.input, value);
|
||||
}
|
||||
} else {
|
||||
// input element
|
||||
this.input.value = value;
|
||||
}
|
||||
// Update the text on the button to reflect the new input value
|
||||
this.refresh();
|
||||
},
|
||||
|
||||
/**
|
||||
* An overrideable method that is called when updating the textContent
|
||||
* of the button.
|
||||
*
|
||||
* @return {L10nId} The formatted text to display in the label.
|
||||
*
|
||||
*/
|
||||
formatLabel: function(value) {
|
||||
return { raw: value };
|
||||
},
|
||||
|
||||
/**
|
||||
* tagName The the name of the tag to insert into the document to use
|
||||
* as the button element.
|
||||
*/
|
||||
tagName: 'button',
|
||||
|
||||
/**
|
||||
* class The value to assign to the className property on the
|
||||
* generated button element.
|
||||
*/
|
||||
className: 'icon icon-dialog'
|
||||
|
||||
};
|
||||
|
||||
return FormButton;
|
||||
|
||||
// end outer IIFE
|
||||
});
|
||||
6
gaia/apps/clock/js/onring.js
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
'use strict';
|
||||
requirejs(['require_config'], function() {
|
||||
requirejs(['ring_view'], function(RingView) {
|
||||
window.ringView = new RingView();
|
||||
});
|
||||
});
|
||||
96
gaia/apps/clock/js/panel.js
Executable file
|
|
@ -0,0 +1,96 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
var View = require('view');
|
||||
var priv = new WeakMap();
|
||||
|
||||
/**
|
||||
* A Panel is a "full screen" style tab/dialog. Panels have an active state
|
||||
* which can be true or false, and can transition in and out using CSS3
|
||||
* classes and animation events.
|
||||
*
|
||||
* @constructor
|
||||
* @param {HTMLElement} element The element to wrap.
|
||||
*/
|
||||
function Panel(element) {
|
||||
View.apply(this, arguments);
|
||||
priv.set(this, {
|
||||
active: element.classList.contains('active'),
|
||||
transition: false
|
||||
});
|
||||
element.addEventListener('animationend', this);
|
||||
}
|
||||
|
||||
Panel.prototype = Object.create(View.prototype);
|
||||
|
||||
/**
|
||||
* Handles the "animationend" event. Sets the transition state to false
|
||||
* and hides the element if the Panel is not active.
|
||||
*/
|
||||
Panel.prototype.handleEvent = function(event) {
|
||||
if (event.target !== this.element) {
|
||||
return;
|
||||
}
|
||||
// remove visibility if transition finishes on non-active view
|
||||
if (!this.active) {
|
||||
this.visible = false;
|
||||
}
|
||||
this.transition = false;
|
||||
};
|
||||
|
||||
Object.defineProperties(Panel.prototype, {
|
||||
/**
|
||||
* Panel.prototype.active - Boolean
|
||||
*
|
||||
* Sets the internal active state, and adds or removes the "active"
|
||||
* class on the element.
|
||||
*/
|
||||
active: {
|
||||
get: function() {
|
||||
return priv.get(this).active;
|
||||
},
|
||||
set: function(value) {
|
||||
var state = priv.get(this);
|
||||
value = !!value;
|
||||
if (state.active !== value) {
|
||||
state.active = value;
|
||||
if (value) {
|
||||
this.element.classList.add('active');
|
||||
} else {
|
||||
this.pendingVisible = true;
|
||||
this.element.classList.remove('active');
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Panel.prototype.transition - String or false
|
||||
*
|
||||
* Sets the internal transition state. When set, adds the class specified
|
||||
* to the element, removing the old transition class if it exists.
|
||||
*
|
||||
* When set to false, it removes the current transition class.
|
||||
*/
|
||||
transition: {
|
||||
get: function() {
|
||||
return priv.get(this).transition;
|
||||
},
|
||||
set: function(value) {
|
||||
var state = priv.get(this);
|
||||
if (value) {
|
||||
if (state.transition) {
|
||||
this.element.classList.remove(state.transition);
|
||||
}
|
||||
this.element.classList.add(value);
|
||||
} else if (state.transition) {
|
||||
this.element.classList.remove(state.transition);
|
||||
}
|
||||
state.transition = value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Panel;
|
||||
|
||||
});
|
||||
229
gaia/apps/clock/js/panels/alarm/active_alarm.js
Executable 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;
|
||||
});
|
||||
211
gaia/apps/clock/js/panels/alarm/alarm_list.js
Executable 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;
|
||||
|
||||
});
|
||||
103
gaia/apps/clock/js/panels/alarm/child_window_manager.js
Executable 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;
|
||||
|
||||
});
|
||||
336
gaia/apps/clock/js/panels/alarm/clock_view.js
Executable 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;
|
||||
});
|
||||
11
gaia/apps/clock/js/panels/alarm/list_item.html
Executable 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>
|
||||
22
gaia/apps/clock/js/panels/alarm/main.js
Executable 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;
|
||||
});
|
||||
26
gaia/apps/clock/js/panels/alarm/panel.html
Executable 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>
|
||||
103
gaia/apps/clock/js/panels/alarm/post_message_proxy.js
Executable 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;
|
||||
|
||||
});
|
||||
364
gaia/apps/clock/js/panels/alarm_edit/main.js
Executable 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;
|
||||
});
|
||||
69
gaia/apps/clock/js/panels/alarm_edit/panel.html
Executable 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>
|
||||
4
gaia/apps/clock/js/panels/stopwatch/list_item.html
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
<div class="lap-name">
|
||||
</div>
|
||||
<div class="lap-duration">
|
||||
</div>
|
||||
324
gaia/apps/clock/js/panels/stopwatch/main.js
Executable 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;
|
||||
});
|
||||
12
gaia/apps/clock/js/panels/stopwatch/panel.html
Executable 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>
|
||||
318
gaia/apps/clock/js/panels/timer/main.js
Executable 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;
|
||||
});
|
||||
78
gaia/apps/clock/js/panels/timer/panel.html
Executable 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>
|
||||
91
gaia/apps/clock/js/picker/picker.js
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
var Spinner = require('picker/spinner');
|
||||
|
||||
/**
|
||||
* Picker
|
||||
*
|
||||
* Create an inline "picker", comprised of n "spinners".
|
||||
*
|
||||
* @param {Object} setup An object containing setup data.
|
||||
* - element, The container element.
|
||||
* - pickers, an object whose properties
|
||||
* correspond to a Picker that will
|
||||
* be created.
|
||||
*
|
||||
*
|
||||
* new Picker({
|
||||
* element: 'time-picker',
|
||||
* pickers: {
|
||||
* hours: {
|
||||
* range: [0, 24],
|
||||
* l10nId: 'nSpinnerHours'
|
||||
* },
|
||||
* minutes: {
|
||||
* range: [0, 60],
|
||||
* isPadded: true,
|
||||
* l10nId: 'nSpinnerMinutes'
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
*
|
||||
*/
|
||||
function Picker(setup) {
|
||||
this.nodes = {};
|
||||
this.spinners = {};
|
||||
this.pickers = Object.keys(setup.pickers);
|
||||
|
||||
this.pickers.forEach(function(picker) {
|
||||
var values = [];
|
||||
var range = setup.pickers[picker].range;
|
||||
var isPadded = setup.pickers[picker].isPadded || false;
|
||||
|
||||
this.nodes[picker] = setup.element.querySelector('.picker-' + picker);
|
||||
|
||||
for (var i = range[0]; i <= range[1]; i++) {
|
||||
values.push(i);
|
||||
}
|
||||
|
||||
this.spinners[picker] = new Spinner({
|
||||
element: this.nodes[picker],
|
||||
values: values,
|
||||
isPadded: isPadded,
|
||||
l10nId: setup.pickers[picker].l10nId
|
||||
});
|
||||
}, this);
|
||||
}
|
||||
|
||||
Picker.prototype = {
|
||||
get value() {
|
||||
// Protect against uninitialized [[Get]] access
|
||||
if (typeof this.pickers === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pickers.map(function(picker) {
|
||||
return this.spinners[picker].value;
|
||||
}, this).join(':');
|
||||
},
|
||||
|
||||
set value(value) {
|
||||
// Protect against uninitialized [[Set]] access
|
||||
if (typeof this.pickers === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
value.split(':').forEach(function(value, i) {
|
||||
this.spinners[this.pickers[i]].value = value;
|
||||
}, this);
|
||||
|
||||
return this.value;
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.pickers.forEach(function(picker) {
|
||||
this.spinners[picker].reset();
|
||||
}, this);
|
||||
}
|
||||
};
|
||||
|
||||
return Picker;
|
||||
});
|
||||
1
gaia/apps/clock/js/picker/spinner.html
Executable file
|
|
@ -0,0 +1 @@
|
|||
<div class="picker-unit" data-value="${value}">${text}</div>
|
||||
289
gaia/apps/clock/js/picker/spinner.js
Executable file
|
|
@ -0,0 +1,289 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
/* global IntlHelper */
|
||||
|
||||
var Template = require('template');
|
||||
var GestureDetector = require('shared/js/gesture_detector');
|
||||
var spinnerHtml = require('text!picker/spinner.html');
|
||||
|
||||
// units covered per millisecond threshold to kick off inertia
|
||||
var SPEED_THRESHOLD = 0.01;
|
||||
|
||||
// max units covered by inertia
|
||||
var INERTIA_MAXIMUM = 15;
|
||||
|
||||
// not the same as animation duration, this accounts for "slowing down",
|
||||
// measured in miliseconds
|
||||
var INERTIA_DURATION = 300;
|
||||
|
||||
// number of milliseconds after last motion without leting go
|
||||
// we will select whatever is being "hovered" and cancel momentum
|
||||
var DRAGGING_TIMEOUT = 200;
|
||||
|
||||
IntlHelper.define('digit-padding', 'number', {
|
||||
style: 'decimal',
|
||||
useGrouping: false,
|
||||
minimumIntegerDigits: 2
|
||||
});
|
||||
|
||||
function calculateSpeed(previous, current) {
|
||||
/* jshint validthis:true */
|
||||
var motion = (previous.y - current.y) / this.unitHeight;
|
||||
var delta = current.time - previous.time;
|
||||
var speed = motion / delta;
|
||||
|
||||
return parseFloat(speed.toFixed(4)) || 0;
|
||||
}
|
||||
|
||||
function Touch(touch = {}) {
|
||||
this.x = touch.x || 0;
|
||||
this.y = touch.y || 0;
|
||||
this.time = touch.time || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spinner
|
||||
*
|
||||
* Create a select list spinner.
|
||||
*
|
||||
* @param {Object} setup An object containing setup data.
|
||||
* - element, a DOM element to create
|
||||
* the spinner with.
|
||||
* - values, an array of values to
|
||||
* populate the spinner with.
|
||||
*
|
||||
*
|
||||
* new Spinner({
|
||||
* element: this.selector[picker],
|
||||
* values: values
|
||||
* });
|
||||
*
|
||||
*
|
||||
*/
|
||||
function Spinner(setup = {}) {
|
||||
// define some non writable properties
|
||||
Object.defineProperties(this, {
|
||||
value: {
|
||||
get: function() {
|
||||
return this.values[this.index];
|
||||
},
|
||||
set: function(value) {
|
||||
this.select(this.values.indexOf(parseInt(value)));
|
||||
}
|
||||
},
|
||||
unitHeight: {
|
||||
get: function() {
|
||||
return this.element.children[0].clientHeight;
|
||||
}
|
||||
},
|
||||
space: {
|
||||
get: function() {
|
||||
return this.unitHeight * this.length;
|
||||
}
|
||||
},
|
||||
container: {
|
||||
value: setup.element.parentNode
|
||||
},
|
||||
element: {
|
||||
value: setup.element
|
||||
},
|
||||
values: {
|
||||
value: setup.values
|
||||
},
|
||||
length: {
|
||||
value: setup.values.length
|
||||
},
|
||||
textValues: {
|
||||
value: setup.textValues || setup.values
|
||||
},
|
||||
isPadded: {
|
||||
value: setup.isPadded || false
|
||||
},
|
||||
l10nId: {
|
||||
value: setup.l10nId || null
|
||||
}
|
||||
});
|
||||
|
||||
this.template = new Template(spinnerHtml);
|
||||
|
||||
this.top = 0;
|
||||
this.index = 0;
|
||||
|
||||
this.previous = new Touch();
|
||||
this.current = new Touch();
|
||||
|
||||
this.timeout = null;
|
||||
|
||||
var html = '';
|
||||
|
||||
var formatter = IntlHelper.get(
|
||||
this.isPadded ? 'digit-padding' : 'digit-nopadding');
|
||||
|
||||
for (var i = 0; i < this.length; i++) {
|
||||
html += this.template.interpolate({
|
||||
value: this.values[i].toString(),
|
||||
text: formatter.format(this.values[i])
|
||||
});
|
||||
}
|
||||
|
||||
this.element.innerHTML = html;
|
||||
|
||||
this.container.addEventListener('touchstart', this, false);
|
||||
this.container.addEventListener('pan', this, false);
|
||||
this.container.addEventListener('swipe', this, false);
|
||||
this.container.addEventListener('keypress', this, false);
|
||||
this.container.setAttribute('aria-valuemax', this.upper);
|
||||
this.container.setAttribute('aria-valuemin', this.lower);
|
||||
this.container.setAttribute('aria-valuenow', this.index);
|
||||
if (this.l10nId) {
|
||||
document.l10n.setAttributes(
|
||||
this.container,
|
||||
this.l10nId,
|
||||
{n: this.values[i]}
|
||||
);
|
||||
}
|
||||
|
||||
IntlHelper.observe(['digit-padding', 'digit-nopadding'],
|
||||
this.resetValues.bind(this));
|
||||
|
||||
this.reset();
|
||||
|
||||
new GestureDetector(this.container).startDetecting();
|
||||
}
|
||||
|
||||
Spinner.prototype.resetValues = function() {
|
||||
var formatter = IntlHelper.get(
|
||||
this.isPadded ? 'digit-padding' : 'digit-nopadding');
|
||||
|
||||
var units = [...this.container.querySelectorAll('.picker-unit')];
|
||||
|
||||
units.forEach(unit => {
|
||||
var value = parseInt(unit.dataset.value);
|
||||
unit.textContent = formatter.format(value);
|
||||
});
|
||||
};
|
||||
|
||||
Spinner.prototype.reset = function() {
|
||||
this.index = 0;
|
||||
this.top = 0;
|
||||
this.update();
|
||||
};
|
||||
|
||||
Spinner.prototype.update = function() {
|
||||
this.container.setAttribute('aria-valuenow', this.index);
|
||||
this.container.setAttribute('aria-valuetext', this.textValues[this.index]);
|
||||
this.element.style.transform = 'translateY(' + this.top + 'px)';
|
||||
};
|
||||
|
||||
Spinner.prototype.select = function(index) {
|
||||
index = Math.round(index);
|
||||
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
if (index > this.length - 1) {
|
||||
index = this.length - 1;
|
||||
}
|
||||
|
||||
if (index !== this.index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
this.top = -this.index * this.unitHeight;
|
||||
this.update();
|
||||
|
||||
return index;
|
||||
};
|
||||
|
||||
Spinner.prototype.handleEvent = function(event) {
|
||||
this['on' + event.type](event);
|
||||
};
|
||||
|
||||
Spinner.prototype.stopInteraction = function() {
|
||||
this.element.classList.add('animation-on');
|
||||
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
this.select(this.index);
|
||||
this.speed = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* ontouchstart - prevent default action (stops scrolling)
|
||||
*/
|
||||
Spinner.prototype.ontouchstart = function(event) {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
Spinner.prototype.onpan = function(event) {
|
||||
event.stopPropagation();
|
||||
var position = event.detail.position;
|
||||
var diff;
|
||||
|
||||
// If this is the first pan event after a swipe...
|
||||
if (this.element.classList.contains('animation-on')) {
|
||||
this.element.classList.remove('animation-on');
|
||||
|
||||
this.select(this.index);
|
||||
|
||||
this.previous.y = position.clientY;
|
||||
this.previous.time = position.timeStamp;
|
||||
return;
|
||||
}
|
||||
|
||||
this.current.y = position.clientY;
|
||||
this.current.time = position.timeStamp;
|
||||
this.speed = calculateSpeed.call(this, this.previous, this.current);
|
||||
|
||||
diff = this.current.y - this.previous.y;
|
||||
|
||||
this.top = this.top + diff;
|
||||
|
||||
if (this.top > 0) {
|
||||
this.top = 0;
|
||||
}
|
||||
if (this.top < -this.space) {
|
||||
this.top = -this.space;
|
||||
}
|
||||
|
||||
this.index = Math.round(Math.abs(this.top) / this.unitHeight);
|
||||
this.update();
|
||||
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
var stopInteraction = this.stopInteraction.bind(this);
|
||||
this.timeout = setTimeout(stopInteraction, DRAGGING_TIMEOUT);
|
||||
|
||||
this.previous.y = this.current.y;
|
||||
this.previous.time = this.current.time;
|
||||
};
|
||||
|
||||
Spinner.prototype.onswipe = function(event) {
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
// Add momentum if speed is higher than a given threshold.
|
||||
var direction = this.speed > 0 ? 1 : -1;
|
||||
var speed = this.speed / direction;
|
||||
if (speed >= SPEED_THRESHOLD) {
|
||||
this.index += Math.round(
|
||||
Math.min(speed * INERTIA_DURATION, INERTIA_MAXIMUM) * direction
|
||||
);
|
||||
}
|
||||
|
||||
this.stopInteraction();
|
||||
|
||||
};
|
||||
|
||||
Spinner.prototype.onkeypress = function(event) {
|
||||
this.element.classList.add('animation-on');
|
||||
if (event.keyCode == KeyEvent.DOM_VK_DOWN) {
|
||||
this.select(this.index - 1);
|
||||
} else {
|
||||
this.select(this.index + 1);
|
||||
}
|
||||
};
|
||||
|
||||
return Spinner;
|
||||
});
|
||||
26
gaia/apps/clock/js/require_config.js
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
requirejs.config({
|
||||
// waitSeconds is set to the default here; the build step rewrites
|
||||
// it to 0 in build/require_config.jslike so that we never timeout
|
||||
// waiting for modules in production. This is important when the
|
||||
// device is under super-low-memory stress, as it may take a while
|
||||
// for the device to get around to loading things like Clock's alarm
|
||||
// ringing screen, and we absolutely do not want that to time out.
|
||||
waitSeconds: 7,
|
||||
paths: {
|
||||
shared: '../shared'
|
||||
},
|
||||
shim: {
|
||||
'shared/js/template': {
|
||||
exports: 'Template'
|
||||
},
|
||||
'shared/js/gesture_detector': {
|
||||
exports: 'GestureDetector'
|
||||
},
|
||||
'shared/js/async_storage': {
|
||||
exports: 'asyncStorage'
|
||||
},
|
||||
'shared/js/accessibility_helper': {
|
||||
exports: 'AccessibilityHelper'
|
||||
}
|
||||
}
|
||||
});
|
||||
274
gaia/apps/clock/js/ring_view.js
Executable file
|
|
@ -0,0 +1,274 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Utils = require('utils');
|
||||
var AudioManager = require('audio_manager');
|
||||
var PostMessageProxy = require('./panels/alarm/post_message_proxy');
|
||||
var ChildWindowManager = require('./panels/alarm/child_window_manager');
|
||||
|
||||
/**
|
||||
* RingView displays the alert screen when a timer or alarm fires.
|
||||
* This screen may receive multiple different alarm/timer events;
|
||||
* ideally it would display all events that fire, but currently it
|
||||
* only shows the most recent event.
|
||||
*/
|
||||
|
||||
function whenDocumentReady() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (document.readyState === 'complete') {
|
||||
resolve();
|
||||
} else {
|
||||
const listener = function() {
|
||||
if (document.readyState === 'complete') {
|
||||
resolve();
|
||||
document.removeEventListener('readystatechange', listener);
|
||||
}
|
||||
};
|
||||
document.addEventListener('readystatechange', listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function RingView() {
|
||||
this.alerts = [];
|
||||
this.ringtonePlayer = AudioManager.createAudioPlayer();
|
||||
|
||||
this.snoozeButton.addEventListener('click', this.onClickSnooze.bind(this));
|
||||
this.closeButton.addEventListener('click', this.onClickClose.bind(this));
|
||||
window.addEventListener('beforeunload', this.onBeforeUnload.bind(this));
|
||||
window.addEventListener('timeformatchange', this.refreshDisplay.bind(this));
|
||||
|
||||
this.activeAlarm = PostMessageProxy.create(window.opener, 'activeAlarm');
|
||||
|
||||
PostMessageProxy.receive('ringView', this);
|
||||
|
||||
if (window.opener) {
|
||||
// We have to wait for the document.readyState to be complete before
|
||||
// we fireReady. Otherwise, when the screen is off, it doesn't get
|
||||
// turned back on at all
|
||||
whenDocumentReady().then(() => {
|
||||
ChildWindowManager.fireReady();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The time, in milliseconds, to keep the screen awake while showing
|
||||
// an alarm. After this time, the screen shuts off and the alarm
|
||||
// silences itself.
|
||||
const WAKE_DURATION = 10 * 60 * 1000;
|
||||
|
||||
RingView.prototype = {
|
||||
|
||||
/**
|
||||
* Fire the notification for an alarm or timer.
|
||||
*
|
||||
* Presently, we only display one notification at a time, and the
|
||||
* _most recent_ one at that. Each notification gets its own wake
|
||||
* lock, ensuring that the screen will remain on for WAKE_DURATION.
|
||||
*
|
||||
* @param {string} alert.type 'alarm' or 'timer'
|
||||
* @param {string} alert.label Label to display (optional).
|
||||
* @param {string} alert.sound Filename of a sound to play (optional).
|
||||
* @param {boolean} alert.vibrate True if the alert should vibrate.
|
||||
*/
|
||||
addAlert: function(alert) {
|
||||
// If we previously had an alert visible, this one is
|
||||
// going to override it, as though the previous alert was
|
||||
// dismissed.
|
||||
if (this.alerts.length) {
|
||||
var oldAlert = this.alerts.shift();
|
||||
oldAlert.releaseScreenWakeLock();
|
||||
}
|
||||
|
||||
alert.releaseScreenWakeLock = function() { };
|
||||
|
||||
if (this._silenceTimeout) {
|
||||
clearTimeout(this._silenceTimeout);
|
||||
this._silenceTimeout = null;
|
||||
}
|
||||
this._silenceTimeout = setTimeout(() => {
|
||||
this.silence();
|
||||
alert.releaseScreenWakeLock();
|
||||
}, WAKE_DURATION);
|
||||
|
||||
// Insert this alert at the front of the stack, so that it
|
||||
// overrides any previous alert that was being displayed.
|
||||
this.alerts.unshift(alert);
|
||||
|
||||
this.refreshDisplay();
|
||||
|
||||
// Acquire a CPU wake lock so that we don't fall asleep waiting
|
||||
// for the document to become visible. We'll only try to hold a
|
||||
// lock for a few seconds as we wait for the document to become
|
||||
// visible, out of an abundance of caution.
|
||||
Utils.safeWakeLock({ type: 'cpu', timeoutMs: 5000 }, (releaseCpu) => {
|
||||
// When the document is visible, acquire a screen wake lock so
|
||||
// that we can safely display the alert.
|
||||
this.whenVisible(() => {
|
||||
Utils.safeWakeLock({ type: 'screen', timeoutMs: WAKE_DURATION },
|
||||
(releaseScreenWakeLock) => {
|
||||
// Once we have acquired the screen wake lock, we can
|
||||
// release the CPU lock.
|
||||
releaseCpu();
|
||||
|
||||
// Save off the screen wake lock for when we dismiss the
|
||||
// alert; all alarms each have their own screen wake lock.
|
||||
alert.releaseScreenWakeLock = releaseScreenWakeLock;
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the display to show the currently active alert. If there
|
||||
* are a stack of alerts pending, only the most recent alert is
|
||||
* shown, as added to this.alerts by this.addAlert().
|
||||
*/
|
||||
refreshDisplay: function() {
|
||||
// First, silence any existing sound or vibration. If a previous
|
||||
// alarm was going off, this alarm may have different settings.
|
||||
// The new alarm will replace any prior settings.
|
||||
this.silence();
|
||||
|
||||
var alert = this.alerts[0];
|
||||
|
||||
// Set the label (blank or falsey becomes a default string).
|
||||
if (alert.label) {
|
||||
this.ringLabel.removeAttribute('data-l10n-id');
|
||||
this.ringLabel.textContent = alert.label;
|
||||
} else {
|
||||
this.ringLabel.setAttribute('data-l10n-id',
|
||||
alert.type === 'alarm' ? 'alarm' : 'timer');
|
||||
}
|
||||
|
||||
// Display the proper screen widgets.
|
||||
this.ringDisplay.dataset.ringType = alert.type;
|
||||
|
||||
var displayedTime;
|
||||
if (alert.type === 'alarm') {
|
||||
// Display the time as specified with "hour" and "minute". The timestamp
|
||||
// stored by mozAlarms does not move when the timezone changes, so we
|
||||
// cannot just use the value stored in '.time'. See
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1178208#c4
|
||||
displayedTime = new Date();
|
||||
displayedTime.setHours(alert.hour);
|
||||
displayedTime.setMinutes(alert.minute);
|
||||
} else {
|
||||
// XXX: Timers may be affected by timezone changes just like alarms, but
|
||||
// the fix here is not as straightforward, since we cannot take the
|
||||
// same shortcut as above. Since timers are rarely used across timezone
|
||||
// changes (unlike alarms), I'm punting on this for now.
|
||||
displayedTime = alert.time;
|
||||
}
|
||||
|
||||
// Set the time to display.
|
||||
this.time.innerHTML = Utils.getLocalizedTimeText(displayedTime);
|
||||
|
||||
if (alert.sound) {
|
||||
this.ringtonePlayer.playRingtone(alert.sound);
|
||||
}
|
||||
|
||||
// Vibrate if we want to shakey shakey.
|
||||
if (alert.vibrate && ('vibrate' in navigator)) {
|
||||
clearInterval(this.vibrateInterval);
|
||||
var vibrateOnce = function() {
|
||||
navigator.vibrate([1000]);
|
||||
};
|
||||
this.vibrateInterval = setInterval(vibrateOnce, 2000);
|
||||
vibrateOnce();
|
||||
}
|
||||
|
||||
document.documentElement.classList.add('ready');
|
||||
|
||||
// If the window has been hidden, show the window.
|
||||
if (document.hidden) {
|
||||
window.focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop all sounds and vibration immediately.
|
||||
*/
|
||||
silence: function() {
|
||||
// Stop the alert sound, if one was playin'.
|
||||
this.ringtonePlayer.pause();
|
||||
|
||||
// Stop vibrating, if we were shakin'.
|
||||
clearInterval(this.vibrateInterval);
|
||||
this.vibrateInterval = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clean up any state when we close this alert window. This includes
|
||||
* silencing the alarm and releasing any locks we have acquired.
|
||||
*/
|
||||
onBeforeUnload: function(evt) {
|
||||
// Clean up any wake locks we still have.
|
||||
while (this.alerts.length) {
|
||||
var alert = this.alerts.shift();
|
||||
alert.releaseScreenWakeLock();
|
||||
}
|
||||
this.silence();
|
||||
},
|
||||
|
||||
/**
|
||||
* Snooze the current alarm. (The snooze button is only visible for
|
||||
* alarms, not timers. Alarms have an ID; timers do not.)
|
||||
*/
|
||||
onClickSnooze: function(evt) {
|
||||
var alert = this.alerts[0];
|
||||
this.activeAlarm.snoozeAlarm(alert.id).then(() => window.close()).catch(console.error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Close this window, notifying ActiveAlarm, which will pop the user
|
||||
* back to the appropriate location if they are still using the
|
||||
* Clock app.
|
||||
*/
|
||||
onClickClose: function(evt) {
|
||||
var alert = this.alerts[0];
|
||||
this.activeAlarm.close(alert.type, alert.id).then(() => window.close()).catch(console.error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Call the callback when `document.hidden` is false. Due to a bug
|
||||
* in the B2G Browser API <https://bugzil.la/810431>, the window may
|
||||
* not be immediately visible, particularly if the screen is off.
|
||||
* The recommended workaround for that bug was to use setTimeout. If
|
||||
* the page is still hidden after that, we listen for
|
||||
* `visibilitychange`. When that bug has some action, we should
|
||||
* revisit how much of this method is needed.
|
||||
*/
|
||||
whenVisible: function(cb) {
|
||||
if (!document.hidden) {
|
||||
cb();
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (!document.hidden) {
|
||||
cb();
|
||||
} else {
|
||||
var listener = function(e) {
|
||||
if (!document.hidden) {
|
||||
document.removeEventListener('visibilitychange', listener);
|
||||
cb();
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Utils.extendWithDomGetters(RingView.prototype, {
|
||||
time: '#ring-clock-time',
|
||||
ringLabel: '#ring-label',
|
||||
snoozeButton: '#ring-button-snooze',
|
||||
closeButton: '#ring-button-stop',
|
||||
ringDisplay: '.ring-display'
|
||||
});
|
||||
|
||||
return RingView;
|
||||
|
||||
});
|
||||
43
gaia/apps/clock/js/sounds.js
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
define(function(require, exports) {
|
||||
'use strict';
|
||||
|
||||
// Sadly, this is needed because when sound l10n ids change, they no
|
||||
// longer match up with the sound filename.
|
||||
var DEFAULT_SOUND = 'ac_awake.opus';
|
||||
var SOUND_FILE_TO_L10N_ID = {
|
||||
'0': 'noSound',
|
||||
'ac_awake.opus': 'ac_awake_opus',
|
||||
'ac_crystalize.opus': 'ac_crystalize_opus',
|
||||
'ac_cycle.opus': 'ac_cycle_opus',
|
||||
'ac_digicloud.opus': 'ac_digicloud_opus',
|
||||
'ac_humming_waves.opus': 'ac_humming_waves_opus',
|
||||
'ac_into_the_void.opus': 'ac_into_the_void_opus',
|
||||
'ac_lightly.opus': 'ac_lightly_opus',
|
||||
'ac_mobile.opus': 'ac_mobile_opus',
|
||||
'ac_pinger.opus': 'ac_pinger_opus',
|
||||
'ac_skip.opus': 'ac_skip_opus',
|
||||
'ac_tri.opus': 'ac_tri_opus',
|
||||
'ac_universal.opus': 'ac_universal_opus'
|
||||
};
|
||||
|
||||
exports.normalizeSound = function(sound) {
|
||||
// Since ringtones are stored on the system, they may be
|
||||
// version-dependent. Ensure the sound exists (based upon our
|
||||
// understanding of the available sounds); if not, default to
|
||||
// something else.
|
||||
if (sound && !SOUND_FILE_TO_L10N_ID.hasOwnProperty(sound)) {
|
||||
return DEFAULT_SOUND;
|
||||
} else {
|
||||
return sound;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a sound ID, return the label to be displayed, for instance,
|
||||
* on a FormButton.
|
||||
*/
|
||||
exports.formatLabel = function(sound) {
|
||||
return (sound === null || sound === '0') ?
|
||||
'noSound' : SOUND_FILE_TO_L10N_ID[sound];
|
||||
};
|
||||
});
|
||||
10
gaia/apps/clock/js/startup.js
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
define('startup_init', function(require) {
|
||||
var App = require('app');
|
||||
App.init();
|
||||
});
|
||||
|
||||
require(['require_config'], function() {
|
||||
requirejs(['startup_init']);
|
||||
});
|
||||
179
gaia/apps/clock/js/stopwatch.js
Executable file
|
|
@ -0,0 +1,179 @@
|
|||
define(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
var priv = new WeakMap();
|
||||
|
||||
function Defaults() {
|
||||
this.startTime = 0;
|
||||
this.totalElapsed = 0;
|
||||
this.state = Stopwatch.RESET;
|
||||
this.laps = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stopwatch
|
||||
*
|
||||
* Create new or revive existing stopwatch objects.
|
||||
*
|
||||
* @param {Object} opts Optional stopwatch object to create or revive
|
||||
* a new or existing stopwatch object.
|
||||
* - startTime, number time in ms.
|
||||
* - totalElapsed, number time in ms.
|
||||
* - isStarted, started state boolean.
|
||||
* - laps, array of lap objects (lap = {time:, duration:}).
|
||||
*/
|
||||
function Stopwatch(opts = {}) {
|
||||
var defaults = new Defaults();
|
||||
var obj = {};
|
||||
|
||||
obj.startTime = opts.startTime || defaults.startTime;
|
||||
obj.totalElapsed = opts.totalElapsed || defaults.totalElapsed;
|
||||
obj.state = opts.state || defaults.state;
|
||||
obj.laps = opts.laps ? opts.laps.slice() : defaults.laps;
|
||||
|
||||
priv.set(this, obj);
|
||||
}
|
||||
|
||||
Stopwatch.RUNNING = 'RUNNING';
|
||||
Stopwatch.PAUSED = 'PAUSED';
|
||||
Stopwatch.RESET = 'RESET';
|
||||
|
||||
Stopwatch.prototype = {
|
||||
|
||||
constructor: Stopwatch,
|
||||
|
||||
getState: function() {
|
||||
var sw = priv.get(this);
|
||||
return sw.state;
|
||||
},
|
||||
|
||||
setState: function(state) {
|
||||
var sw = priv.get(this);
|
||||
sw.state = state;
|
||||
},
|
||||
|
||||
/**
|
||||
* start Starts the stopwatch, either from a reset or paused state
|
||||
*/
|
||||
start: function sw_start() {
|
||||
var sw = priv.get(this);
|
||||
if (sw.state === Stopwatch.RUNNING) {
|
||||
return;
|
||||
}
|
||||
var now = Date.now() - sw.totalElapsed;
|
||||
sw.startTime = now;
|
||||
this.setState(Stopwatch.RUNNING);
|
||||
},
|
||||
|
||||
/**
|
||||
* getElapsedTime Calculates the total elapsed duration since the
|
||||
* stopwatch was started
|
||||
* @return {Number} return total elapsed duration.
|
||||
*/
|
||||
getElapsedTime: function sw_getElapsedTime() {
|
||||
var sw = priv.get(this);
|
||||
var elapsed = 0;
|
||||
if (sw.state === Stopwatch.RUNNING) {
|
||||
elapsed = Date.now() - sw.startTime;
|
||||
} else {
|
||||
elapsed = sw.totalElapsed;
|
||||
}
|
||||
return elapsed;
|
||||
},
|
||||
|
||||
/**
|
||||
* pause Pauses the stopwatch
|
||||
*/
|
||||
pause: function sw_pause() {
|
||||
var sw = priv.get(this);
|
||||
if (sw.state === Stopwatch.PAUSED) {
|
||||
return;
|
||||
}
|
||||
sw.totalElapsed = Date.now() - sw.startTime;
|
||||
this.setState(Stopwatch.PAUSED);
|
||||
},
|
||||
|
||||
/**
|
||||
* nextLap Calculates the duration of the next lap.
|
||||
* @return {object} return an object containing:
|
||||
* duration - the duration of this lap in ms.
|
||||
* time - the start time of this lap in ms from epoch.
|
||||
*/
|
||||
nextLap: function sw_nextLap() {
|
||||
var sw = priv.get(this);
|
||||
var now;
|
||||
if (sw.state === Stopwatch.RUNNING) {
|
||||
now = Date.now();
|
||||
} else {
|
||||
now = sw.startTime + sw.totalElapsed;
|
||||
}
|
||||
|
||||
var lastLapTime;
|
||||
var newLap = {};
|
||||
|
||||
if (sw.laps.length > 0) {
|
||||
lastLapTime = sw.laps[sw.laps.length - 1].time;
|
||||
} else {
|
||||
lastLapTime = 0;
|
||||
}
|
||||
|
||||
newLap.duration = now - (sw.startTime + lastLapTime);
|
||||
newLap.time = now - sw.startTime;
|
||||
|
||||
return newLap;
|
||||
},
|
||||
|
||||
/**
|
||||
* lap Calculates a new lap duration since the last lap time,
|
||||
* and mutates `priv[this].laps` to contain the new value.
|
||||
* If the stopwatch isn't currently running, returns 0.
|
||||
* @return {number} return the lap duration in ms.
|
||||
*/
|
||||
lap: function sw_lap() {
|
||||
var sw = priv.get(this);
|
||||
if (sw.state !== Stopwatch.RUNNING) {
|
||||
return 0;
|
||||
}
|
||||
var nl = this.nextLap();
|
||||
sw.laps.push(nl);
|
||||
return nl;
|
||||
},
|
||||
|
||||
/**
|
||||
* getLaps Returns an array of laps, sorted by oldest first
|
||||
* @return {Array} return an array of laps.
|
||||
*/
|
||||
getLaps: function sw_getLaps() {
|
||||
var sw = priv.get(this);
|
||||
return sw.laps.map(function(lap) {
|
||||
return lap;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* reset Resets the stopwatch back to 0, clears laps
|
||||
*/
|
||||
reset: function sw_reset() {
|
||||
priv.set(this, new Defaults());
|
||||
},
|
||||
|
||||
/**
|
||||
* toSerializable Returns a serializable object for persisting Stopwatch data
|
||||
* @return {Object} A serializable object.
|
||||
*/
|
||||
toSerializable: function sw_toSerializable() {
|
||||
var sw = priv.get(this);
|
||||
var obj = {};
|
||||
for (var i in sw) {
|
||||
if (sw.hasOwnProperty(i)) {
|
||||
obj[i] = Array.isArray(sw[i]) ? sw[i].slice() : sw[i];
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return Stopwatch;
|
||||
});
|
||||
27
gaia/apps/clock/js/tabs.js
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var AccessibilityHelper = require('shared/js/accessibility_helper');
|
||||
|
||||
/**
|
||||
* Abstraction for handling the Tabs links at the bottom of the UI.
|
||||
* @param {HTMLElement} element The containing element for the Tabs UI.
|
||||
*/
|
||||
function Tabs(element) {
|
||||
this.element = element;
|
||||
this.links = element.querySelectorAll('a');
|
||||
this.element.addEventListener('click', this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selected attributes for the selected tab.
|
||||
* Also emit a 'selected' event with the relevant data.
|
||||
*/
|
||||
Tabs.prototype.handleEvent = function tabsHandleEvent(event) {
|
||||
AccessibilityHelper.setAriaSelected(event.target, this.links);
|
||||
};
|
||||
|
||||
|
||||
return Tabs;
|
||||
|
||||
});
|
||||
13
gaia/apps/clock/js/template.js
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
var Template = require('shared/js/template');
|
||||
|
||||
function ClockTemplate(text) {
|
||||
var srcNode = document.createElement('div');
|
||||
var comment = document.createComment(text);
|
||||
srcNode.appendChild(comment);
|
||||
return new Template(srcNode);
|
||||
}
|
||||
|
||||
return ClockTemplate;
|
||||
});
|
||||
27
gaia/apps/clock/js/text.js
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
define({
|
||||
pluginBuilder: './text_builder',
|
||||
load: function(name, req, onload, config) {
|
||||
'use strict';
|
||||
var url = req.toUrl(name),
|
||||
xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('GET', url, true);
|
||||
xhr.onreadystatechange = function(evt) {
|
||||
var status, err;
|
||||
if (xhr.readyState === 4) {
|
||||
status = xhr.status;
|
||||
if (status > 399 && status < 600) {
|
||||
//An http 4xx or 5xx error. Signal an error.
|
||||
err = new Error(url + ' HTTP status: ' + status);
|
||||
err.xhr = xhr;
|
||||
onload.error(err);
|
||||
} else {
|
||||
onload(xhr.responseText);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.responseType = 'text';
|
||||
xhr.send(null);
|
||||
}
|
||||
});
|
||||
|
||||
386
gaia/apps/clock/js/text_builder.js
Executable file
|
|
@ -0,0 +1,386 @@
|
|||
/**
|
||||
* @license RequireJS text 2.0.10 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
|
||||
* Available via the MIT or new BSD license.
|
||||
* see: http://github.com/requirejs/text for details
|
||||
*/
|
||||
/*jslint regexp: true */
|
||||
/*global require, XMLHttpRequest, ActiveXObject,
|
||||
define, window, process, Packages,
|
||||
java, location, Components, FileUtils */
|
||||
|
||||
define(['module'], function (module) {
|
||||
'use strict';
|
||||
|
||||
var text, fs, Cc, Ci, xpcIsWindows,
|
||||
progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
|
||||
xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,
|
||||
bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
|
||||
hasLocation = typeof location !== 'undefined' && location.href,
|
||||
defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''),
|
||||
defaultHostName = hasLocation && location.hostname,
|
||||
defaultPort = hasLocation && (location.port || undefined),
|
||||
buildMap = {},
|
||||
masterConfig = (module.config && module.config()) || {};
|
||||
|
||||
text = {
|
||||
version: '2.0.10',
|
||||
|
||||
strip: function (content) {
|
||||
//Strips <?xml ...?> declarations so that external SVG and XML
|
||||
//documents can be added to a document without worry. Also, if the string
|
||||
//is an HTML document, only the part inside the body tag is returned.
|
||||
if (content) {
|
||||
content = content.replace(xmlRegExp, "");
|
||||
var matches = content.match(bodyRegExp);
|
||||
if (matches) {
|
||||
content = matches[1];
|
||||
}
|
||||
} else {
|
||||
content = "";
|
||||
}
|
||||
return content;
|
||||
},
|
||||
|
||||
jsEscape: function (content) {
|
||||
return content.replace(/(['\\])/g, '\\$1')
|
||||
.replace(/[\f]/g, "\\f")
|
||||
.replace(/[\b]/g, "\\b")
|
||||
.replace(/[\n]/g, "\\n")
|
||||
.replace(/[\t]/g, "\\t")
|
||||
.replace(/[\r]/g, "\\r")
|
||||
.replace(/[\u2028]/g, "\\u2028")
|
||||
.replace(/[\u2029]/g, "\\u2029");
|
||||
},
|
||||
|
||||
createXhr: masterConfig.createXhr || function () {
|
||||
//Would love to dump the ActiveX crap in here. Need IE 6 to die first.
|
||||
var xhr, i, progId;
|
||||
if (typeof XMLHttpRequest !== "undefined") {
|
||||
return new XMLHttpRequest();
|
||||
} else if (typeof ActiveXObject !== "undefined") {
|
||||
for (i = 0; i < 3; i += 1) {
|
||||
progId = progIds[i];
|
||||
try {
|
||||
xhr = new ActiveXObject(progId);
|
||||
} catch (e) {}
|
||||
|
||||
if (xhr) {
|
||||
progIds = [progId]; // so faster next time
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return xhr;
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses a resource name into its component parts. Resource names
|
||||
* look like: module/name.ext!strip, where the !strip part is
|
||||
* optional.
|
||||
* @param {String} name the resource name
|
||||
* @returns {Object} with properties "moduleName", "ext" and "strip"
|
||||
* where strip is a boolean.
|
||||
*/
|
||||
parseName: function (name) {
|
||||
var modName, ext, temp,
|
||||
strip = false,
|
||||
index = name.indexOf("."),
|
||||
isRelative = name.indexOf('./') === 0 ||
|
||||
name.indexOf('../') === 0;
|
||||
|
||||
if (index !== -1 && (!isRelative || index > 1)) {
|
||||
modName = name.substring(0, index);
|
||||
ext = name.substring(index + 1, name.length);
|
||||
} else {
|
||||
modName = name;
|
||||
}
|
||||
|
||||
temp = ext || modName;
|
||||
index = temp.indexOf("!");
|
||||
if (index !== -1) {
|
||||
//Pull off the strip arg.
|
||||
strip = temp.substring(index + 1) === "strip";
|
||||
temp = temp.substring(0, index);
|
||||
if (ext) {
|
||||
ext = temp;
|
||||
} else {
|
||||
modName = temp;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
moduleName: modName,
|
||||
ext: ext,
|
||||
strip: strip
|
||||
};
|
||||
},
|
||||
|
||||
xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/,
|
||||
|
||||
/**
|
||||
* Is an URL on another domain. Only works for browser use, returns
|
||||
* false in non-browser environments. Only used to know if an
|
||||
* optimized .js version of a text resource should be loaded
|
||||
* instead.
|
||||
* @param {String} url
|
||||
* @returns Boolean
|
||||
*/
|
||||
useXhr: function (url, protocol, hostname, port) {
|
||||
var uProtocol, uHostName, uPort,
|
||||
match = text.xdRegExp.exec(url);
|
||||
if (!match) {
|
||||
return true;
|
||||
}
|
||||
uProtocol = match[2];
|
||||
uHostName = match[3];
|
||||
|
||||
uHostName = uHostName.split(':');
|
||||
uPort = uHostName[1];
|
||||
uHostName = uHostName[0];
|
||||
|
||||
return (!uProtocol || uProtocol === protocol) &&
|
||||
(!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) &&
|
||||
((!uPort && !uHostName) || uPort === port);
|
||||
},
|
||||
|
||||
finishLoad: function (name, strip, content, onLoad) {
|
||||
content = strip ? text.strip(content) : content;
|
||||
if (masterConfig.isBuild) {
|
||||
buildMap[name] = content;
|
||||
}
|
||||
onLoad(content);
|
||||
},
|
||||
|
||||
load: function (name, req, onLoad, config) {
|
||||
//Name has format: some.module.filext!strip
|
||||
//The strip part is optional.
|
||||
//if strip is present, then that means only get the string contents
|
||||
//inside a body tag in an HTML string. For XML/SVG content it means
|
||||
//removing the <?xml ...?> declarations so the content can be inserted
|
||||
//into the current doc without problems.
|
||||
|
||||
// Do not bother with the work if a build and text will
|
||||
// not be inlined.
|
||||
if (config.isBuild && !config.inlineText) {
|
||||
onLoad();
|
||||
return;
|
||||
}
|
||||
|
||||
masterConfig.isBuild = config.isBuild;
|
||||
|
||||
var parsed = text.parseName(name),
|
||||
nonStripName = parsed.moduleName +
|
||||
(parsed.ext ? '.' + parsed.ext : ''),
|
||||
url = req.toUrl(nonStripName),
|
||||
useXhr = (masterConfig.useXhr) ||
|
||||
text.useXhr;
|
||||
|
||||
// Do not load if it is an empty: url
|
||||
if (url.indexOf('empty:') === 0) {
|
||||
onLoad();
|
||||
return;
|
||||
}
|
||||
|
||||
//Load the text. Use XHR if possible and in a browser.
|
||||
if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) {
|
||||
text.get(url, function (content) {
|
||||
text.finishLoad(name, parsed.strip, content, onLoad);
|
||||
}, function (err) {
|
||||
if (onLoad.error) {
|
||||
onLoad.error(err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
//Need to fetch the resource across domains. Assume
|
||||
//the resource has been optimized into a JS module. Fetch
|
||||
//by the module name + extension, but do not include the
|
||||
//!strip part to avoid file system issues.
|
||||
req([nonStripName], function (content) {
|
||||
text.finishLoad(parsed.moduleName + '.' + parsed.ext,
|
||||
parsed.strip, content, onLoad);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
write: function (pluginName, moduleName, write, config) {
|
||||
if (buildMap.hasOwnProperty(moduleName)) {
|
||||
var content = text.jsEscape(buildMap[moduleName]);
|
||||
write.asModule(pluginName + "!" + moduleName,
|
||||
"define(function () { return '" +
|
||||
content +
|
||||
"';});\n");
|
||||
}
|
||||
},
|
||||
|
||||
writeFile: function (pluginName, moduleName, req, write, config) {
|
||||
var parsed = text.parseName(moduleName),
|
||||
extPart = parsed.ext ? '.' + parsed.ext : '',
|
||||
nonStripName = parsed.moduleName + extPart,
|
||||
//Use a '.js' file name so that it indicates it is a
|
||||
//script that can be loaded across domains.
|
||||
fileName = req.toUrl(parsed.moduleName + extPart) + '.js';
|
||||
|
||||
//Leverage own load() method to load plugin value, but only
|
||||
//write out values that do not have the strip argument,
|
||||
//to avoid any potential issues with ! in file names.
|
||||
text.load(nonStripName, req, function (value) {
|
||||
//Use own write() method to construct full module value.
|
||||
//But need to create shell that translates writeFile's
|
||||
//write() to the right interface.
|
||||
var textWrite = function (contents) {
|
||||
return write(fileName, contents);
|
||||
};
|
||||
textWrite.asModule = function (moduleName, contents) {
|
||||
return write.asModule(moduleName, fileName, contents);
|
||||
};
|
||||
|
||||
text.write(pluginName, nonStripName, textWrite, config);
|
||||
}, config);
|
||||
}
|
||||
};
|
||||
|
||||
if (masterConfig.env === 'node' || (!masterConfig.env &&
|
||||
typeof process !== "undefined" &&
|
||||
process.versions &&
|
||||
!!process.versions.node &&
|
||||
!process.versions['node-webkit'])) {
|
||||
//Using special require.nodeRequire, something added by r.js.
|
||||
fs = require.nodeRequire('fs');
|
||||
|
||||
text.get = function (url, callback, errback) {
|
||||
try {
|
||||
var file = fs.readFileSync(url, 'utf8');
|
||||
//Remove BOM (Byte Mark Order) from utf8 files if it is there.
|
||||
if (file.indexOf('\uFEFF') === 0) {
|
||||
file = file.substring(1);
|
||||
}
|
||||
callback(file);
|
||||
} catch (e) {
|
||||
errback(e);
|
||||
}
|
||||
};
|
||||
} else if (masterConfig.env === 'xhr' || (!masterConfig.env &&
|
||||
text.createXhr())) {
|
||||
text.get = function (url, callback, errback, headers) {
|
||||
var xhr = text.createXhr(), header;
|
||||
xhr.open('GET', url, true);
|
||||
|
||||
//Allow plugins direct access to xhr headers
|
||||
if (headers) {
|
||||
for (header in headers) {
|
||||
if (headers.hasOwnProperty(header)) {
|
||||
xhr.setRequestHeader(header.toLowerCase(), headers[header]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Allow overrides specified in config
|
||||
if (masterConfig.onXhr) {
|
||||
masterConfig.onXhr(xhr, url);
|
||||
}
|
||||
|
||||
xhr.onreadystatechange = function (evt) {
|
||||
var status, err;
|
||||
//Do not explicitly handle errors, those should be
|
||||
//visible via console output in the browser.
|
||||
if (xhr.readyState === 4) {
|
||||
status = xhr.status;
|
||||
if (status > 399 && status < 600) {
|
||||
//An http 4xx or 5xx error. Signal an error.
|
||||
err = new Error(url + ' HTTP status: ' + status);
|
||||
err.xhr = xhr;
|
||||
errback(err);
|
||||
} else {
|
||||
callback(xhr.responseText);
|
||||
}
|
||||
|
||||
if (masterConfig.onXhrComplete) {
|
||||
masterConfig.onXhrComplete(xhr, url);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
};
|
||||
} else if (masterConfig.env === 'rhino' || (!masterConfig.env &&
|
||||
typeof Packages !== 'undefined' && typeof java !== 'undefined')) {
|
||||
//Why Java, why is this so awkward?
|
||||
text.get = function (url, callback) {
|
||||
var stringBuffer, line,
|
||||
encoding = "utf-8",
|
||||
file = new java.io.File(url),
|
||||
lineSeparator = java.lang.System.getProperty("line.separator"),
|
||||
input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)),
|
||||
content = '';
|
||||
try {
|
||||
stringBuffer = new java.lang.StringBuffer();
|
||||
line = input.readLine();
|
||||
|
||||
// Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324
|
||||
// http://www.unicode.org/faq/utf_bom.html
|
||||
|
||||
// Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK:
|
||||
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058
|
||||
if (line && line.length() && line.charAt(0) === 0xfeff) {
|
||||
// Eat the BOM, since we've already found the encoding on this file,
|
||||
// and we plan to concatenating this buffer with others; the BOM should
|
||||
// only appear at the top of a file.
|
||||
line = line.substring(1);
|
||||
}
|
||||
|
||||
if (line !== null) {
|
||||
stringBuffer.append(line);
|
||||
}
|
||||
|
||||
while ((line = input.readLine()) !== null) {
|
||||
stringBuffer.append(lineSeparator);
|
||||
stringBuffer.append(line);
|
||||
}
|
||||
//Make sure we return a JavaScript string and not a Java string.
|
||||
content = String(stringBuffer.toString()); //String
|
||||
} finally {
|
||||
input.close();
|
||||
}
|
||||
callback(content);
|
||||
};
|
||||
} else if (masterConfig.env === 'xpconnect' || (!masterConfig.env &&
|
||||
typeof Components !== 'undefined' && Components.classes &&
|
||||
Components.interfaces)) {
|
||||
//Avert your gaze!
|
||||
Cc = Components.classes,
|
||||
Ci = Components.interfaces;
|
||||
Components.utils['import']('resource://gre/modules/FileUtils.jsm');
|
||||
xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc);
|
||||
|
||||
text.get = function (url, callback) {
|
||||
var inStream, convertStream, fileObj,
|
||||
readData = {};
|
||||
|
||||
if (xpcIsWindows) {
|
||||
url = url.replace(/\//g, '\\');
|
||||
}
|
||||
|
||||
fileObj = new FileUtils.File(url);
|
||||
|
||||
//XPCOM, you so crazy
|
||||
try {
|
||||
inStream = Cc['@mozilla.org/network/file-input-stream;1']
|
||||
.createInstance(Ci.nsIFileInputStream);
|
||||
inStream.init(fileObj, 1, 0, false);
|
||||
|
||||
convertStream = Cc['@mozilla.org/intl/converter-input-stream;1']
|
||||
.createInstance(Ci.nsIConverterInputStream);
|
||||
convertStream.init(inStream, "utf-8", inStream.available(),
|
||||
Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
|
||||
|
||||
convertStream.readString(inStream.available(), readData);
|
||||
convertStream.close();
|
||||
inStream.close();
|
||||
callback(readData.value);
|
||||
} catch (e) {
|
||||
throw new Error((fileObj && fileObj.path || '') + ': ' + e);
|
||||
}
|
||||
};
|
||||
}
|
||||
return text;
|
||||
});
|
||||
294
gaia/apps/clock/js/timer.js
Executable file
|
|
@ -0,0 +1,294 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var asyncStorage = require('shared/js/async_storage');
|
||||
var Utils = require('utils');
|
||||
|
||||
var timerPrivate = new WeakMap();
|
||||
|
||||
/**
|
||||
* Timer
|
||||
*
|
||||
* Create new or revive existing timer objects.
|
||||
*
|
||||
* @param {Object} opts Optional timer object to create or revive
|
||||
* a new or existing timer object.
|
||||
* - startTime, number time in ms.
|
||||
* - duration, time to count from `start`.
|
||||
* - configuredDuration, time requested by user.
|
||||
* - sound, string sound name.
|
||||
* - vibrate, boolean, vibrate or not.
|
||||
* - id, integer, mozAlarm API id number.
|
||||
*/
|
||||
function Timer(opts) {
|
||||
opts = opts || {};
|
||||
|
||||
var now = Date.now();
|
||||
if (opts.id !== undefined) {
|
||||
delete opts.id;
|
||||
}
|
||||
// private properties
|
||||
timerPrivate.set(this, Utils.extend({
|
||||
state: Timer.INITIAL
|
||||
}, extractProtected(opts)));
|
||||
// public properties
|
||||
Utils.extend(this, {
|
||||
startTime: now,
|
||||
duration: null,
|
||||
configuredDuration: null,
|
||||
sound: 'ac_awake.opus',
|
||||
vibrate: true
|
||||
}, opts);
|
||||
}
|
||||
|
||||
Timer.prototype.constructor = Timer;
|
||||
|
||||
/**
|
||||
* request - get the persisted Timer object.
|
||||
*
|
||||
* @param {function} [callback] - called with (err, timer_raw).
|
||||
*/
|
||||
Timer.getFromStorage = function(callback) {
|
||||
asyncStorage.getItem('active_timer', function(timer) {
|
||||
if (timer) {
|
||||
// Normalize the timer data. Pre-April-2014 code may have stored
|
||||
// 'vibrate' and 'sound' as the string "0".
|
||||
timer.sound = (timer.sound !== '0' ? timer.sound : null);
|
||||
timer.vibrate = (timer.vibrate && timer.vibrate !== '0');
|
||||
}
|
||||
callback && callback(timer || null);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* singleton - get the unique persisted Timer object.
|
||||
*
|
||||
* @param {function} [callback] - called with (err, timer).
|
||||
*/
|
||||
var timerSingleton = Utils.singleton(Timer);
|
||||
Timer.singleton = function tm_singleton(callback) {
|
||||
Timer.getFromStorage(function(err, obj) {
|
||||
var ts = timerSingleton(obj);
|
||||
callback && callback(null, ts);
|
||||
});
|
||||
};
|
||||
|
||||
function extractProtected(config) {
|
||||
var ret = {};
|
||||
var protectedProperties = new Set(['state']);
|
||||
for (var i in config) {
|
||||
if (protectedProperties.has(i)) {
|
||||
ret[i] = config[i];
|
||||
delete config[i];
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* toSerializable - convert `this` to a serialized format.
|
||||
*
|
||||
* @return {object} - object representation of this Timer.
|
||||
*/
|
||||
Timer.prototype.toSerializable = function timerToSerializable() {
|
||||
var timer = Utils.extend({}, this, timerPrivate.get(this));
|
||||
|
||||
// Normalize the data. TODO: Perform this normalization immediately
|
||||
// at the getter/setter level when this class is refactored.
|
||||
return {
|
||||
startTime: timer.startTime,
|
||||
duration: timer.duration,
|
||||
configuredDuration: timer.configuredDuration,
|
||||
sound: (timer.sound !== '0' ? timer.sound : null),
|
||||
vibrate: (timer.vibrate !== '0' ? timer.vibrate : null),
|
||||
state: timer.state
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* save - Save the timer to the database.
|
||||
*
|
||||
* @param {function} [callback] - callback to call after the timer
|
||||
* has been saved.
|
||||
*/
|
||||
Timer.prototype.save = function timerSave(callback) {
|
||||
asyncStorage.setItem('active_timer', this.toSerializable(), function() {
|
||||
callback && callback(null, this);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* register - Register the timer with mozAlarm API.
|
||||
*
|
||||
* @param {function} [callback] - callback to call after the timer
|
||||
* has been registered.
|
||||
*/
|
||||
Timer.prototype.register = function timerRegister(callback) {
|
||||
var data = {
|
||||
type: 'timer'
|
||||
};
|
||||
var request;
|
||||
|
||||
// Remove previously-created mozAlarm for this alarm, if necessary.
|
||||
this.unregister();
|
||||
|
||||
request = navigator.mozAlarms.add(
|
||||
new Date(Date.now() + this.remaining), 'ignoreTimezone', data
|
||||
);
|
||||
|
||||
request.onsuccess = (function(ev) {
|
||||
this.id = ev.target.result;
|
||||
callback && callback(null, this);
|
||||
}.bind(this));
|
||||
request.onerror = function(ev) {
|
||||
callback && callback(ev.target.error);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* commit - save and register the timer as necessary.
|
||||
*
|
||||
* @param {function} [callback] - callback to call after the timer
|
||||
* has been registered.
|
||||
*/
|
||||
Timer.prototype.commit = function timerCommit(callback) {
|
||||
var saveSelf = this.save.bind(this, callback);
|
||||
if (this.state === Timer.STARTED) {
|
||||
this.register(saveSelf);
|
||||
} else {
|
||||
this.unregister();
|
||||
saveSelf();
|
||||
}
|
||||
};
|
||||
|
||||
Timer.prototype.unregister = function timerUnregister() {
|
||||
if (typeof this.id === 'number') {
|
||||
navigator.mozAlarms.remove(this.id);
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(Timer.prototype, 'remaining', {
|
||||
get: function() {
|
||||
if (this.state === Timer.INITIAL) {
|
||||
return this.configuredDuration;
|
||||
} else if (this.state === Timer.PAUSED) {
|
||||
return this.duration;
|
||||
} else if (this.state === Timer.STARTED) {
|
||||
if (typeof this.startTime === 'undefined' ||
|
||||
typeof this.duration === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
var r = (this.startTime + this.duration) - Date.now();
|
||||
return r >= 0 ? r : 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(Timer.prototype, 'state', {
|
||||
get: function() {
|
||||
var priv = timerPrivate.get(this);
|
||||
return priv.state;
|
||||
}
|
||||
});
|
||||
|
||||
Timer.prototype.start = function timerStart() {
|
||||
if (this.state !== Timer.STARTED) {
|
||||
var priv = timerPrivate.get(this);
|
||||
priv.state = Timer.STARTED;
|
||||
this.startTime = Date.now();
|
||||
this.duration = (typeof this.duration === 'number') ? this.duration :
|
||||
this.configuredDuration;
|
||||
window.dispatchEvent(new CustomEvent('timer-start', {
|
||||
detail: { remaining: this.remaining }
|
||||
}));
|
||||
this._tick();
|
||||
}
|
||||
};
|
||||
|
||||
Timer.prototype.pause = function timerPause() {
|
||||
if (this.state === Timer.STARTED) {
|
||||
this.duration = this.remaining; // remaining getter observes private state
|
||||
var priv = timerPrivate.get(this);
|
||||
priv.state = Timer.PAUSED;
|
||||
this.startTime = null;
|
||||
this._cancelTick();
|
||||
window.dispatchEvent(new CustomEvent('timer-pause', {
|
||||
detail: { remaining: this.remaining }
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
Timer.prototype.cancel = function timerReset() {
|
||||
if (this.state !== Timer.INITIAL) {
|
||||
var priv = timerPrivate.get(this);
|
||||
priv.state = Timer.INITIAL;
|
||||
this.startTime = null;
|
||||
this.duration = this.configuredDuration;
|
||||
this._cancelTick();
|
||||
this._dispatchEnd();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* plus Increase the duration and extend the endAt time
|
||||
*
|
||||
* @param {Number} seconds The time in seconds to add.
|
||||
*
|
||||
* @return {Timer} Timer instance.
|
||||
*/
|
||||
Timer.prototype.plus = function timerPlus(seconds) {
|
||||
// Convert to ms
|
||||
var ms = seconds * 1000;
|
||||
|
||||
this.duration += ms;
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
Timer.prototype._dispatchEnd = function timerDispatchEnd() {
|
||||
// cancel and end are basically the same thing
|
||||
window.dispatchEvent(new CustomEvent('timer-end', {
|
||||
detail: { remaining: 0 }
|
||||
}));
|
||||
};
|
||||
|
||||
Timer.prototype._tick = function timerTick() {
|
||||
if (this.state !== Timer.STARTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we cache the previous value posted to avoid triggering multiple "tick"
|
||||
// messages for the same second
|
||||
var remaining = this.remaining;
|
||||
var current = Math.round(remaining / 1000);
|
||||
if (current !== this._previousTick) {
|
||||
this._previousTick = current;
|
||||
window.dispatchEvent(new CustomEvent('timer-tick', {
|
||||
detail: { remaining: remaining }
|
||||
}));
|
||||
}
|
||||
|
||||
if (remaining > 0) {
|
||||
// use setTimeout instead of requestAnimationFrame because window will
|
||||
// probably be hidden when handling connections
|
||||
this._tickTimeout = window.setTimeout(() => this._tick(), 100);
|
||||
} else {
|
||||
this._dispatchEnd();
|
||||
}
|
||||
};
|
||||
|
||||
Timer.prototype._cancelTick = function timerCancelTick() {
|
||||
window.clearTimeout(this._tickTimeout);
|
||||
};
|
||||
|
||||
/**
|
||||
* Static "const" Timer states.
|
||||
*/
|
||||
Object.defineProperties(Timer, {
|
||||
INITIAL: { value: 0 },
|
||||
STARTED: { value: 1 },
|
||||
PAUSED: { value: 2 }
|
||||
});
|
||||
|
||||
return Timer;
|
||||
});
|
||||
22
gaia/apps/clock/js/tmpl.js
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
define([], function() {
|
||||
'use strict';
|
||||
|
||||
var tmpl = {
|
||||
pluginBuilder: './tmpl_builder',
|
||||
|
||||
toDom: function(text) {
|
||||
var temp = document.createElement('div');
|
||||
temp.innerHTML = text;
|
||||
var node = temp.children[0];
|
||||
return node;
|
||||
},
|
||||
|
||||
load: function(id, require, onload, config) {
|
||||
require(['text!' + id], function(text) {
|
||||
onload(tmpl.toDom(text));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return tmpl;
|
||||
});
|
||||
368
gaia/apps/clock/js/tmpl_builder.js
Executable file
|
|
@ -0,0 +1,368 @@
|
|||
//MODIFIED requirejs text plugin, that just changes the write function
|
||||
//to write out a 'tmpl!'-based resource instead, and removes writeFile.
|
||||
|
||||
/**
|
||||
* @license RequireJS text 2.0.8 Copyright (c) 2010-2012, The Dojo
|
||||
* Foundation All Rights Reserved. Available via the MIT or new BSD
|
||||
* license. see: http://github.com/requirejs/text for details
|
||||
*/
|
||||
/*jslint regexp: true */
|
||||
/*global require, XMLHttpRequest, ActiveXObject,
|
||||
define, process, Packages,
|
||||
java, location, Components, FileUtils */
|
||||
|
||||
define(['module'], function (module) {
|
||||
'use strict';
|
||||
|
||||
var text, fs, Cc, Ci, xpcIsWindows,
|
||||
progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
|
||||
xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,
|
||||
bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
|
||||
hasLocation = typeof location !== 'undefined' && location.href,
|
||||
defaultProtocol = hasLocation && location.protocol &&
|
||||
location.protocol.replace(/\:/, ''),
|
||||
defaultHostName = hasLocation && location.hostname,
|
||||
defaultPort = hasLocation && (location.port || undefined),
|
||||
buildMap = {},
|
||||
masterConfig = (module.config && module.config()) || {};
|
||||
|
||||
text = {
|
||||
version: '2.0.8',
|
||||
|
||||
strip: function (content) {
|
||||
//Strips <?xml ...?> declarations so that external SVG and
|
||||
//XML documents can be added to a document without worry.
|
||||
//Also, if the string is an HTML document, only the part
|
||||
//inside the body tag is returned.
|
||||
if (content) {
|
||||
content = content.replace(xmlRegExp, '');
|
||||
var matches = content.match(bodyRegExp);
|
||||
if (matches) {
|
||||
content = matches[1];
|
||||
}
|
||||
} else {
|
||||
content = '';
|
||||
}
|
||||
return content;
|
||||
},
|
||||
|
||||
jsEscape: function (content) {
|
||||
return content.replace(/(['\\])/g, '\\$1')
|
||||
.replace(/[\f]/g, '\\f')
|
||||
.replace(/[\b]/g, '\\b')
|
||||
.replace(/[\n]/g, '\\n')
|
||||
.replace(/[\t]/g, '\\t')
|
||||
.replace(/[\r]/g, '\\r')
|
||||
.replace(/[\u2028]/g, '\\u2028')
|
||||
.replace(/[\u2029]/g, '\\u2029');
|
||||
},
|
||||
|
||||
createXhr: masterConfig.createXhr || function () {
|
||||
//Would love to dump the ActiveX crap in here. Need IE 6
|
||||
//to die first.
|
||||
var xhr, i, progId;
|
||||
if (typeof XMLHttpRequest !== 'undefined') {
|
||||
return new XMLHttpRequest();
|
||||
} else if (typeof ActiveXObject !== 'undefined') {
|
||||
for (i = 0; i < 3; i += 1) {
|
||||
progId = progIds[i];
|
||||
try {
|
||||
xhr = new ActiveXObject(progId);
|
||||
} catch (e) {}
|
||||
|
||||
if (xhr) {
|
||||
progIds = [progId]; // so faster next time
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return xhr;
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses a resource name into its component parts. Resource names
|
||||
* look like: module/name.ext!strip, where the !strip part is
|
||||
* optional.
|
||||
* @param {String} name the resource name
|
||||
* @returns {Object} with properties "moduleName", "ext" and "strip"
|
||||
* where strip is a boolean.
|
||||
*/
|
||||
parseName: function (name) {
|
||||
var modName, ext, temp,
|
||||
strip = false,
|
||||
index = name.indexOf('.'),
|
||||
isRelative = name.indexOf('./') === 0 ||
|
||||
name.indexOf('../') === 0;
|
||||
|
||||
if (index !== -1 && (!isRelative || index > 1)) {
|
||||
modName = name.substring(0, index);
|
||||
ext = name.substring(index + 1, name.length);
|
||||
} else {
|
||||
modName = name;
|
||||
}
|
||||
|
||||
temp = ext || modName;
|
||||
index = temp.indexOf('!');
|
||||
if (index !== -1) {
|
||||
//Pull off the strip arg.
|
||||
strip = temp.substring(index + 1) === 'strip';
|
||||
temp = temp.substring(0, index);
|
||||
if (ext) {
|
||||
ext = temp;
|
||||
} else {
|
||||
modName = temp;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
moduleName: modName,
|
||||
ext: ext,
|
||||
strip: strip
|
||||
};
|
||||
},
|
||||
|
||||
xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/,
|
||||
|
||||
/**
|
||||
* Is an URL on another domain. Only works for browser use, returns
|
||||
* false in non-browser environments. Only used to know if an
|
||||
* optimized .js version of a text resource should be loaded
|
||||
* instead.
|
||||
* @param {String} url
|
||||
* @returns Boolean
|
||||
*/
|
||||
useXhr: function (url, protocol, hostname, port) {
|
||||
var uProtocol, uHostName, uPort,
|
||||
match = text.xdRegExp.exec(url);
|
||||
if (!match) {
|
||||
return true;
|
||||
}
|
||||
uProtocol = match[2];
|
||||
uHostName = match[3];
|
||||
|
||||
uHostName = uHostName.split(':');
|
||||
uPort = uHostName[1];
|
||||
uHostName = uHostName[0];
|
||||
|
||||
return (!uProtocol || uProtocol === protocol) &&
|
||||
(!uHostName || uHostName.toLowerCase() ===
|
||||
hostname.toLowerCase()) &&
|
||||
((!uPort && !uHostName) || uPort === port);
|
||||
},
|
||||
|
||||
finishLoad: function (name, strip, content, onLoad) {
|
||||
content = strip ? text.strip(content) : content;
|
||||
if (masterConfig.isBuild) {
|
||||
buildMap[name] = content;
|
||||
}
|
||||
onLoad(content);
|
||||
},
|
||||
|
||||
load: function (name, req, onLoad, config) {
|
||||
//Name has format: some.module.filext!strip The strip part
|
||||
//is optional. if strip is present, then that means only
|
||||
//get the string contents inside a body tag in an HTML
|
||||
//string. For XML/SVG content it means removing the <?xml
|
||||
//...?> declarations so the content can be inserted into
|
||||
//the current doc without problems.
|
||||
|
||||
// Do not bother with the work if a build and text will
|
||||
// not be inlined.
|
||||
if (config.isBuild && !config.inlineText) {
|
||||
onLoad();
|
||||
return;
|
||||
}
|
||||
|
||||
masterConfig.isBuild = config.isBuild;
|
||||
|
||||
var parsed = text.parseName(name),
|
||||
nonStripName = parsed.moduleName +
|
||||
(parsed.ext ? '.' + parsed.ext : ''),
|
||||
url = req.toUrl(nonStripName),
|
||||
useXhr = (masterConfig.useXhr) ||
|
||||
text.useXhr;
|
||||
|
||||
//Load the text. Use XHR if possible and in a browser.
|
||||
if (!hasLocation || useXhr(url, defaultProtocol,
|
||||
defaultHostName, defaultPort)) {
|
||||
text.get(url, function (content) {
|
||||
text.finishLoad(name, parsed.strip, content, onLoad);
|
||||
}, function (err) {
|
||||
if (onLoad.error) {
|
||||
onLoad.error(err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
//Need to fetch the resource across domains. Assume
|
||||
//the resource has been optimized into a JS module. Fetch
|
||||
//by the module name + extension, but do not include the
|
||||
//!strip part to avoid file system issues.
|
||||
req([nonStripName], function (content) {
|
||||
text.finishLoad(parsed.moduleName + '.' + parsed.ext,
|
||||
parsed.strip, content, onLoad);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
write: function (pluginName, moduleName, write, config) {
|
||||
if (buildMap.hasOwnProperty(moduleName)) {
|
||||
var content = text.jsEscape(buildMap[moduleName]);
|
||||
|
||||
write.asModule(
|
||||
pluginName + '!' + moduleName,
|
||||
'define([\'tmpl\'], function (tmpl) { return tmpl.toDom(\'' +
|
||||
content + '\'); });\n');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (masterConfig.env === 'node' || (!masterConfig.env &&
|
||||
typeof process !== 'undefined' &&
|
||||
process.versions &&
|
||||
!!process.versions.node)) {
|
||||
//Using special require.nodeRequire, something added by r.js.
|
||||
fs = require.nodeRequire('fs');
|
||||
|
||||
text.get = function (url, callback, errback) {
|
||||
try {
|
||||
var file = fs.readFileSync(url, 'utf8');
|
||||
//Remove BOM (Byte Mark Order) from utf8 files if it is there.
|
||||
if (file.indexOf('\uFEFF') === 0) {
|
||||
file = file.substring(1);
|
||||
}
|
||||
callback(file);
|
||||
} catch (e) {
|
||||
errback(e);
|
||||
}
|
||||
};
|
||||
} else if (masterConfig.env === 'xhr' || (!masterConfig.env &&
|
||||
text.createXhr())) {
|
||||
text.get = function (url, callback, errback, headers) {
|
||||
var xhr = text.createXhr(), header;
|
||||
xhr.open('GET', url, true);
|
||||
|
||||
//Allow plugins direct access to xhr headers
|
||||
if (headers) {
|
||||
for (header in headers) {
|
||||
if (headers.hasOwnProperty(header)) {
|
||||
xhr.setRequestHeader(header.toLowerCase(), headers[header]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Allow overrides specified in config
|
||||
if (masterConfig.onXhr) {
|
||||
masterConfig.onXhr(xhr, url);
|
||||
}
|
||||
|
||||
xhr.onreadystatechange = function (evt) {
|
||||
var status, err;
|
||||
//Do not explicitly handle errors, those should be
|
||||
//visible via console output in the browser.
|
||||
if (xhr.readyState === 4) {
|
||||
status = xhr.status;
|
||||
if (status > 399 && status < 600) {
|
||||
//An http 4xx or 5xx error. Signal an error.
|
||||
err = new Error(url + ' HTTP status: ' + status);
|
||||
err.xhr = xhr;
|
||||
errback(err);
|
||||
} else {
|
||||
callback(xhr.responseText);
|
||||
}
|
||||
|
||||
if (masterConfig.onXhrComplete) {
|
||||
masterConfig.onXhrComplete(xhr, url);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
};
|
||||
} else if (masterConfig.env === 'rhino' ||
|
||||
(!masterConfig.env &&
|
||||
typeof Packages !== 'undefined' && typeof java !== 'undefined')) {
|
||||
//Why Java, why is this so awkward?
|
||||
text.get = function (url, callback) {
|
||||
var stringBuffer, line,
|
||||
encoding = 'utf-8',
|
||||
file = new java.io.File(url),
|
||||
lineSeparator = java.lang.System.getProperty('line.separator'),
|
||||
input = new java.io.BufferedReader(
|
||||
new java.io.InputStreamReader(
|
||||
new java.io.FileInputStream(file), encoding)),
|
||||
content = '';
|
||||
try {
|
||||
stringBuffer = new java.lang.StringBuffer();
|
||||
line = input.readLine();
|
||||
|
||||
// Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324
|
||||
// http://www.unicode.org/faq/utf_bom.html
|
||||
|
||||
// Note that when we use utf-8, the BOM should appear as "EF
|
||||
// BB BF", but it doesn't due to this bug in the JDK:
|
||||
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058
|
||||
if (line && line.length() && line.charAt(0) === 0xfeff) {
|
||||
// Eat the BOM, since we've already found the encoding on
|
||||
// this file, and we plan to concatenating this buffer with
|
||||
// others; the BOM should only appear at the top of a file.
|
||||
line = line.substring(1);
|
||||
}
|
||||
|
||||
if (line !== null) {
|
||||
stringBuffer.append(line);
|
||||
}
|
||||
|
||||
while ((line = input.readLine()) !== null) {
|
||||
stringBuffer.append(lineSeparator);
|
||||
stringBuffer.append(line);
|
||||
}
|
||||
//Make sure we return a JavaScript string and not a Java string.
|
||||
content = String(stringBuffer.toString()); //String
|
||||
} finally {
|
||||
input.close();
|
||||
}
|
||||
callback(content);
|
||||
};
|
||||
} else if (masterConfig.env === 'xpconnect' ||
|
||||
(!masterConfig.env &&
|
||||
typeof Components !== 'undefined' && Components.classes &&
|
||||
Components.interfaces)) {
|
||||
//Avert your gaze!
|
||||
Cc = Components.classes,
|
||||
Ci = Components.interfaces;
|
||||
Components.utils['import']('resource://gre/modules/FileUtils.jsm');
|
||||
xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc);
|
||||
|
||||
text.get = function (url, callback) {
|
||||
var inStream, convertStream, fileObj,
|
||||
readData = {};
|
||||
|
||||
if (xpcIsWindows) {
|
||||
url = url.replace(/\//g, '\\');
|
||||
}
|
||||
|
||||
fileObj = new FileUtils.File(url);
|
||||
|
||||
//XPCOM, you so crazy
|
||||
try {
|
||||
inStream = Cc['@mozilla.org/network/file-input-stream;1']
|
||||
.createInstance(Ci.nsIFileInputStream);
|
||||
inStream.init(fileObj, 1, 0, false);
|
||||
|
||||
convertStream = Cc['@mozilla.org/intl/converter-input-stream;1']
|
||||
.createInstance(Ci.nsIConverterInputStream);
|
||||
convertStream.init(
|
||||
inStream, 'utf-8', inStream.available(),
|
||||
Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
|
||||
|
||||
convertStream.readString(inStream.available(), readData);
|
||||
convertStream.close();
|
||||
inStream.close();
|
||||
callback(readData.value);
|
||||
} catch (e) {
|
||||
throw new Error((fileObj && fileObj.path || '') + ': ' + e);
|
||||
}
|
||||
};
|
||||
}
|
||||
return text;
|
||||
});
|
||||
569
gaia/apps/clock/js/utils.js
Executable file
|
|
@ -0,0 +1,569 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
/* global mozIntl, IntlHelper */
|
||||
|
||||
var constants = require('constants');
|
||||
|
||||
var Utils = {};
|
||||
// Maintain references to millisecond multipliers
|
||||
var dateMultipliers = {
|
||||
days: 1000 * 60 * 60 * 24,
|
||||
hours: 1000 * 60 * 60,
|
||||
minutes: 1000 * 60,
|
||||
seconds: 1000,
|
||||
milliseconds: 1
|
||||
};
|
||||
var units = Object.keys(dateMultipliers);
|
||||
|
||||
IntlHelper.define('weekday-short', 'datetime', {
|
||||
weekday: 'short'
|
||||
});
|
||||
|
||||
IntlHelper.define('time-html', 'mozdatetime', {
|
||||
dayperiod: true,
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
});
|
||||
|
||||
IntlHelper.define('time-text', 'datetime', {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
});
|
||||
|
||||
/**
|
||||
* Define a singleton method that returns a unified instance
|
||||
* based on arguments.
|
||||
*
|
||||
* @param {function} constructor - A constructor function used
|
||||
* to create a new instance.
|
||||
* @param {function} [getKey] - A function called with (arguments),
|
||||
* and returns a lookup key for this singleton.
|
||||
* @return {object} - returns the instance either created or retrieved
|
||||
* from the singleton-map by the key.
|
||||
*/
|
||||
Utils.singleton = function(constructor, getKey) {
|
||||
var singletonMap = new Map();
|
||||
return function() {
|
||||
var arglist = Array.prototype.slice.call(arguments);
|
||||
var key = (typeof getKey === 'function') ? getKey(arglist) : constructor;
|
||||
var instance = singletonMap.get(key);
|
||||
if (!instance) {
|
||||
instance = Object.create(constructor.prototype);
|
||||
constructor.apply(instance, arglist);
|
||||
singletonMap.set(key, instance);
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
};
|
||||
|
||||
Utils.memoizedDomPropertyDescriptor = function(selector) {
|
||||
var memoizedValue = null;
|
||||
return {
|
||||
get: function() {
|
||||
if (memoizedValue === null) {
|
||||
memoizedValue = document.querySelector(selector);
|
||||
}
|
||||
return memoizedValue;
|
||||
},
|
||||
set: function(value) {
|
||||
memoizedValue = value;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Extend the given prototype object with lazy getters.
|
||||
* selectorMap is a mapping of { propertyName: selector }.
|
||||
*/
|
||||
Utils.extendWithDomGetters = function(proto, selectorMap) {
|
||||
for (var property in selectorMap) {
|
||||
Object.defineProperty(proto, property,
|
||||
Utils.memoizedDomPropertyDescriptor(selectorMap[property]));
|
||||
}
|
||||
return proto;
|
||||
};
|
||||
|
||||
Utils.dateMath = {
|
||||
/**
|
||||
* Convert object literals containing interval length to milliseconds
|
||||
*
|
||||
* @param {Object|Date|Number} interval An object literal containing days,
|
||||
* hours, minutes etc.
|
||||
* Optionally a number or date object.
|
||||
* @param {Object} opts Options object with a unitsPartial property containing
|
||||
* (if desired) a restriction on which properties will be
|
||||
* searched on the interval.
|
||||
* @return {Number} Millisecond value for interval length.
|
||||
*/
|
||||
toMS: function(interval, opts) {
|
||||
var converted, sign, unitsPartial;
|
||||
|
||||
// if a millisecond interval or a Date is passed in, return that
|
||||
if (interval instanceof Date || typeof interval === 'number') {
|
||||
return +interval;
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
unitsPartial = opts.unitsPartial || units;
|
||||
// Allow for 'hours' or 'hour'
|
||||
unitsPartial = unitsPartial.map(function(unit) {
|
||||
// String.prototype.endsWith is available in FF17+
|
||||
return unit.endsWith('s') ? unit : unit.concat('s');
|
||||
});
|
||||
|
||||
// some will exit early when it returns a truthy value
|
||||
sign = unitsPartial.some(function(unit) {
|
||||
return interval[unit] < 0;
|
||||
});
|
||||
// Using as a multiplier later
|
||||
sign = sign ? -1 : 1;
|
||||
// collect passed in units and multiply by their millisecond/unit count
|
||||
converted = unitsPartial.map(function(unit) {
|
||||
var partial;
|
||||
// we're storing the sign out of the iterator
|
||||
partial = Math.abs(interval[unit]);
|
||||
// A missing property and 0 should be treated the same
|
||||
return partial ? partial * dateMultipliers[unit] : 0;
|
||||
});
|
||||
|
||||
// add up each millisecond-converted term and multiply total by sign
|
||||
return sign * converted.reduce(function(a, b) { return a + b; });
|
||||
},
|
||||
/**
|
||||
* Convert millisecond values to object literals conformable to toMS()
|
||||
*
|
||||
* @param {Number} interval A millisecond value.
|
||||
* @param {Object} [opts] Options object with a unitsPartial property
|
||||
* containing (if desired) a restriction on which
|
||||
* properties will be reated for the return value.
|
||||
* @return {Object} Object literal with properties as deliniated by opts.
|
||||
*/
|
||||
fromMS: function(interval, opts) {
|
||||
var times, sign, unitsPartial;
|
||||
|
||||
opts = opts || {};
|
||||
unitsPartial = opts.unitsPartial || units;
|
||||
// Allow for 'hours' or 'hour'
|
||||
unitsPartial = unitsPartial.map(function(unit) {
|
||||
// String.prototype.endsWith is available in FF17+
|
||||
return unit.endsWith('s') ? unit : unit.concat('s');
|
||||
});
|
||||
// For negative intervals (time previous to now)
|
||||
// update interval to absolute value and store the sign
|
||||
// to apply to all units
|
||||
if (interval < 0) {
|
||||
sign = -1;
|
||||
interval = Math.abs(interval);
|
||||
} else {
|
||||
sign = 1;
|
||||
}
|
||||
|
||||
// divide the time interval by the highest millisecond multiplier
|
||||
// store the truncated result and subtract that from the interval
|
||||
// update the interval to be the remainder
|
||||
times = unitsPartial.map(function(unit, index) {
|
||||
var truncated, mult;
|
||||
mult = dateMultipliers[unit];
|
||||
truncated = Math.floor(interval / mult);
|
||||
interval = interval - (truncated * mult);
|
||||
// units are either all positive or negative
|
||||
// only iterate to needed specificity
|
||||
return sign * truncated;
|
||||
});
|
||||
|
||||
// Populate the returned object using units as property names
|
||||
// and times for values
|
||||
return times.reduce(function(out, unitTime, index) {
|
||||
out[unitsPartial[index]] = unitTime;
|
||||
return out;
|
||||
}, {});
|
||||
}
|
||||
};
|
||||
|
||||
Utils.extend = function(initialObject, extensions) {
|
||||
// extend({}, a, b, c ... d) -> {...}
|
||||
// rightmost properties (on 'd') take precedence
|
||||
extensions = Array.prototype.slice.call(arguments, 1);
|
||||
for (var i = 0; i < extensions.length; i++) {
|
||||
var extender = extensions[i];
|
||||
for (var prop in extender) {
|
||||
var descriptor = Object.getOwnPropertyDescriptor(extender, prop);
|
||||
if (descriptor && descriptor.value !== undefined) {
|
||||
initialObject[prop] = extender[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
return initialObject;
|
||||
};
|
||||
|
||||
Utils.escapeHTML = function(str, escapeQuotes) {
|
||||
var span = document.createElement('span');
|
||||
span.textContent = str;
|
||||
|
||||
if (escapeQuotes) {
|
||||
return span.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
return span.innerHTML;
|
||||
};
|
||||
|
||||
Utils.getLocalizedTimeHtml = function(date) {
|
||||
var f = IntlHelper.get('time-html');
|
||||
return f.format(date, {
|
||||
dayperiod: '<small>$&</small>'
|
||||
});
|
||||
};
|
||||
|
||||
Utils.getLocalizedTimeText = function(date) {
|
||||
var f = IntlHelper.get('time-text');
|
||||
return f.format(date);
|
||||
};
|
||||
|
||||
Utils.changeSelectByValue = function(selectElement, value) {
|
||||
var options = selectElement.options;
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
if (options[i].value == value) {
|
||||
if (selectElement.selectedIndex != i) {
|
||||
selectElement.selectedIndex = i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Utils.getSelectedValueByIndex = function(selectElement) {
|
||||
return selectElement.options[selectElement.selectedIndex].value;
|
||||
};
|
||||
|
||||
var wakeTarget = {
|
||||
requests: {
|
||||
cpu: new Map(), screen: new Map(), wifi: new Map()
|
||||
},
|
||||
locks: {
|
||||
cpu: null, screen: null, wifi: null
|
||||
},
|
||||
timeouts: {
|
||||
cpu: null, screen: null, wifi: null
|
||||
}
|
||||
};
|
||||
function getLongestLock(type) {
|
||||
var max = 0;
|
||||
for (var i of wakeTarget.requests[type]) {
|
||||
var request = i[1];
|
||||
if (request.time > max) {
|
||||
max = request.time;
|
||||
}
|
||||
}
|
||||
return {
|
||||
time: max,
|
||||
lock: wakeTarget.locks[type],
|
||||
timeout: wakeTarget.timeouts[type]
|
||||
};
|
||||
}
|
||||
Utils.safeWakeLock = function(opts, fn) {
|
||||
/*
|
||||
* safeWakeLock
|
||||
*
|
||||
* Create a Wake lock that is automatically released after
|
||||
* timeoutMs. Locks are reentrant, and have no meaningful mutual
|
||||
* exclusion behavior.
|
||||
*
|
||||
* @param {Object} options - an object containing
|
||||
* [type] {string} a string passed to requestWakeLock
|
||||
* default = 'cpu'. This string can be any
|
||||
* resource exposed by the environment that
|
||||
* this application was designed to run in.
|
||||
* Gaia exposes three of them: 'cpu',
|
||||
* 'screen', and 'wifi'. Certified apps may
|
||||
* expose more.
|
||||
* timeoutMs {number} number of milliseconds to hold
|
||||
* the lock.
|
||||
* @param {Function} callback - a function to be called after all other
|
||||
* generated callbacks have been called.
|
||||
* function ([err]) -> undefined.
|
||||
*/
|
||||
opts = opts || {};
|
||||
var type = opts.type || 'cpu';
|
||||
var timeoutMs = opts.timeoutMs | 0;
|
||||
var now = Date.now();
|
||||
var myKey = {};
|
||||
wakeTarget.requests[type].set(myKey, {
|
||||
time: now + timeoutMs
|
||||
});
|
||||
var max = getLongestLock(type);
|
||||
var unlockFn = function() {
|
||||
if (!myKey) {
|
||||
return;
|
||||
}
|
||||
wakeTarget.requests[type]. delete(myKey);
|
||||
var now = Date.now();
|
||||
var max = getLongestLock(type);
|
||||
if (max.time > now) {
|
||||
clearTimeout(wakeTarget.timeouts[type]);
|
||||
wakeTarget.timeouts[type] = setTimeout(unlockFn, max.time - now);
|
||||
} else {
|
||||
if (wakeTarget.locks[type]) {
|
||||
wakeTarget.locks[type].unlock();
|
||||
}
|
||||
wakeTarget.locks[type] = null;
|
||||
clearTimeout(wakeTarget.timeouts[type]);
|
||||
wakeTarget.timeouts[type] = null;
|
||||
}
|
||||
myKey = null;
|
||||
};
|
||||
clearTimeout(wakeTarget.timeouts[type]);
|
||||
wakeTarget.timeouts[type] = setTimeout(unlockFn, max.time - now);
|
||||
try {
|
||||
if (!wakeTarget.locks[type] && max.time > now) {
|
||||
wakeTarget.locks[type] = navigator.requestWakeLock(type);
|
||||
}
|
||||
fn(unlockFn);
|
||||
} catch (err) {
|
||||
unlockFn();
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
Utils.async = {
|
||||
|
||||
generator: function(latchCallback) {
|
||||
/*
|
||||
* Generator
|
||||
*
|
||||
* Create an async generator. Each time the generator is
|
||||
* called, it will return a new callback. When all issued
|
||||
* callbacks have been called, the latchCallback is called.
|
||||
*
|
||||
* If any of the callbacks are called with and error as
|
||||
* the first argument, the latchCallback will be called
|
||||
* immediately with that error.
|
||||
*
|
||||
* @latchCallback {Function} a function to be called after
|
||||
* all other generated callbacks have been
|
||||
* called
|
||||
* function ([err]) -> undefined
|
||||
*/
|
||||
var tracker = new Map();
|
||||
var issuedCallbackCount = 0;
|
||||
var disabled = false;
|
||||
var testFn = function(err) {
|
||||
var trackerSize;
|
||||
if (!disabled) {
|
||||
// FF18 defines size to be a method, so we need to test here:
|
||||
// Remove with FF18 support
|
||||
if (typeof tracker.size === 'function') {
|
||||
trackerSize = tracker.size();
|
||||
} else {
|
||||
trackerSize = tracker.size;
|
||||
}
|
||||
if (err || trackerSize === issuedCallbackCount) {
|
||||
disabled = true;
|
||||
latchCallback && latchCallback(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
return function() {
|
||||
return (function() {
|
||||
var i = issuedCallbackCount++;
|
||||
return function(err) {
|
||||
tracker.set(i, true);
|
||||
testFn(err);
|
||||
};
|
||||
})();
|
||||
};
|
||||
},
|
||||
|
||||
namedParallel: function(names, latchCallback) {
|
||||
/*
|
||||
* namedParallel
|
||||
*
|
||||
* Create an async namedParallel.
|
||||
*
|
||||
* The return value is an object containing the parameters
|
||||
* specified in the names array. Each parameter is set to
|
||||
* a callback. When all callbacks have been called, latchCallback
|
||||
* is called.
|
||||
*
|
||||
* If any named callback is called with an error as the first
|
||||
* parameter, latchCallback is immediately called with that
|
||||
* error. Future calls to callbacks are then no-ops.
|
||||
*
|
||||
* @names {List<String>} - A list of strings to be used as
|
||||
* parameter names for callbacks on the returned object.
|
||||
*/
|
||||
var generator = Utils.async.generator(latchCallback);
|
||||
var done = generator();
|
||||
var ret = {};
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
ret[names[i]] = generator();
|
||||
}
|
||||
done();
|
||||
return ret;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Utils.data = {
|
||||
|
||||
defaultCompare: function ud_defaultCompare(a, b) {
|
||||
if (typeof a === 'number' && typeof b === 'number') {
|
||||
var diff = a - b;
|
||||
return diff !== 0 ? diff / Math.abs(diff) : diff;
|
||||
} else if ((typeof a === 'string' || a instanceof String) &&
|
||||
(typeof b === 'string' || b instanceof String)) {
|
||||
return (a < b) ? -1 : ((a > b) ? 1 : 0);
|
||||
} else if (Array.isArray(a) && Array.isArray(b)) {
|
||||
var commonLength = Math.min(a.length, b.length);
|
||||
for (var i = 0; i < commonLength; i++) {
|
||||
var compareResult = Utils.data.defaultCompare(a[i], b[i]);
|
||||
if (compareResult !== 0) {
|
||||
return compareResult;
|
||||
}
|
||||
}
|
||||
return b.length - a.length;
|
||||
} else {
|
||||
throw new Error('Cannot compare ' + JSON.stringify([a, b]));
|
||||
}
|
||||
},
|
||||
|
||||
keyedCompare: function ud_keyedCompare(key) {
|
||||
return function internal_keyedCompare(a, b) {
|
||||
return Utils.data.defaultCompare(a[key], b[key]);
|
||||
};
|
||||
},
|
||||
|
||||
binarySearch: function ud_binarySearch(key, list, compare) {
|
||||
compare = compare || Utils.data.defaultCompare;
|
||||
var botI = 0;
|
||||
var topI = list.length;
|
||||
var midI, comp, value;
|
||||
if (list.length > 0) {
|
||||
do {
|
||||
midI = botI + ((topI - botI) / 2) | 0;
|
||||
value = list[midI];
|
||||
comp = compare(value, key);
|
||||
if (comp < 0) {
|
||||
botI = midI + 1;
|
||||
} else if (comp > 0) {
|
||||
topI = midI - 1;
|
||||
}
|
||||
} while (comp !== 0 && botI < topI);
|
||||
}
|
||||
midI = comp === 0 ? midI : topI;
|
||||
value = list[midI];
|
||||
if (comp === 0 || value && compare(value, key) === 0) {
|
||||
return { match: true, index: midI, value: value };
|
||||
} else {
|
||||
var index;
|
||||
if (0 > midI) {
|
||||
index = 0;
|
||||
} else if (midI >= list.length) {
|
||||
index = list.length;
|
||||
} else {
|
||||
index = midI + (compare(value, key) < 0 ? 1 : 0);
|
||||
}
|
||||
return {
|
||||
match: false,
|
||||
index: index
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
sortedInsert: function ud_sortedInsert(item, list, compare, unique) {
|
||||
compare = compare || Utils.data.defaultCompare;
|
||||
var bs = Utils.data.binarySearch(item, list, compare);
|
||||
var inserted = !bs.match || !unique;
|
||||
if (inserted) {
|
||||
list.splice(bs.index, 0, item);
|
||||
}
|
||||
return (inserted) ? bs.index : null;
|
||||
},
|
||||
|
||||
sortedRemove: function ud_sortedRemove(item, list, compare, multiple) {
|
||||
compare = compare || Utils.data.defaultCompare;
|
||||
var removed = false;
|
||||
if (!multiple) {
|
||||
var bs = Utils.data.binarySearch(item, list, compare);
|
||||
if (bs.match) {
|
||||
list.splice(bs.index, 1);
|
||||
removed = true;
|
||||
}
|
||||
} else {
|
||||
var biasedCompare = function(target, result, ic) {
|
||||
return function(a, b) {
|
||||
if (ic(a, target) === 0) {
|
||||
return result;
|
||||
} else {
|
||||
return ic(a, b);
|
||||
}
|
||||
};
|
||||
};
|
||||
var leftBound = Utils.data.binarySearch(item, list,
|
||||
biasedCompare(item, 1, compare));
|
||||
var rightBound = Utils.data.binarySearch(item, list,
|
||||
biasedCompare(item, -1, compare));
|
||||
if (leftBound.index < rightBound.index) {
|
||||
list.splice(leftBound.index, rightBound.index - leftBound.index);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
};
|
||||
|
||||
Utils.addEventListenerOnce = function(element, type, fn, useCapture) {
|
||||
var handler = function(evt) {
|
||||
element.removeEventListener(type, handler, useCapture);
|
||||
fn(evt);
|
||||
};
|
||||
element.addEventListener(type, handler, useCapture);
|
||||
};
|
||||
|
||||
Utils.summarizeDaysOfWeek = function(repeat) {
|
||||
var days = [];
|
||||
if (repeat) {
|
||||
for (var day in repeat) {
|
||||
if (repeat[day]) {
|
||||
days.push(day);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (days.length === 7) {
|
||||
return Promise.resolve('everyday');
|
||||
} else if (days.length === 5 &&
|
||||
days.indexOf('6') === -1 && // Saturday
|
||||
days.indexOf('0') === -1) { // Sunday
|
||||
return Promise.resolve('weekdays');
|
||||
} else if (days.length === 2 &&
|
||||
days.indexOf('6') !== -1 && // Saturday
|
||||
days.indexOf('0') !== -1) { // Sunday
|
||||
return Promise.resolve('weekends');
|
||||
} else if (days.length === 0) {
|
||||
return Promise.resolve('never');
|
||||
} else {
|
||||
var formatter = IntlHelper.get('weekday-short');
|
||||
|
||||
var repeatStrings = [];
|
||||
|
||||
for (var i = 0; i < 7; i++) {
|
||||
if (days.indexOf(i.toString()) !== -1) {
|
||||
var dayDate = new Date(constants.KNOWN_SUNDAY);
|
||||
dayDate.setDate(constants.KNOWN_SUNDAY.getDate() + i);
|
||||
repeatStrings.push(formatter.format(dayDate));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return mozIntl.formatList(repeatStrings).then(listString => {
|
||||
return { raw: listString };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return Utils;
|
||||
|
||||
});
|
||||
91
gaia/apps/clock/js/view.js
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
var priv = new WeakMap();
|
||||
var elementMap = new WeakMap();
|
||||
|
||||
/**
|
||||
* A View is simply a wrapper around an element.
|
||||
*
|
||||
* @constructor
|
||||
* @param {HTMLElement} element The element that will be wrapped by this view.
|
||||
*/
|
||||
function View(element) {
|
||||
if (!(this instanceof View)) {
|
||||
throw new Error('View must be called as a constructor');
|
||||
}
|
||||
elementMap.set(element, this);
|
||||
|
||||
Object.defineProperties(this, {
|
||||
id: { value: element.id },
|
||||
element: { value: element }
|
||||
});
|
||||
|
||||
priv.set(this, {
|
||||
visible: !element.classList.contains('hidden'),
|
||||
pendingVisible: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create a view instance for an element.
|
||||
*
|
||||
* @param {HTMLElement} element The element that will be wrapped by the view.
|
||||
* @param {Function} ctor The constructor method for the view, defaults to View.
|
||||
*/
|
||||
View.instance = function(element, ctor = View) {
|
||||
if (elementMap.has(element)) {
|
||||
return elementMap.get(element);
|
||||
}
|
||||
return new ctor(element);
|
||||
};
|
||||
|
||||
Object.defineProperties(View.prototype, {
|
||||
/**
|
||||
* View.prototype.visible - set to true or false to toggle the "hidden" class
|
||||
* on the element.
|
||||
*
|
||||
* Also emits a 'visibilitychange' event passing either true or false to show
|
||||
* the new visible state. The event happens before the class is changed to
|
||||
* allow time to modify the DOM before something becomes visible.
|
||||
*/
|
||||
visible: {
|
||||
get: function() {
|
||||
return priv.get(this).visible;
|
||||
},
|
||||
set: function(value) {
|
||||
var state = priv.get(this);
|
||||
value = !!value;
|
||||
if (state.visible !== value || state.pendingVisible) {
|
||||
state.pendingVisible = false;
|
||||
state.visible = value;
|
||||
|
||||
var event = new CustomEvent('panel-visibilitychange', {
|
||||
detail: {
|
||||
isVisible: value
|
||||
}
|
||||
});
|
||||
this.element.dispatchEvent(event);
|
||||
|
||||
if (!value) {
|
||||
this.element.classList.add('hidden');
|
||||
} else {
|
||||
this.element.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
|
||||
pendingVisible: {
|
||||
get: function() {
|
||||
return priv.get(this).pendingVisible;
|
||||
},
|
||||
set: function(value) {
|
||||
return (priv.get(this).pendingVisible = !!value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return View;
|
||||
|
||||
});
|
||||
18
gaia/apps/clock/jsdoc.json
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "clock",
|
||||
"tags": {
|
||||
"dictionaries": ["jsdoc"]
|
||||
},
|
||||
"source": {
|
||||
"include": ["apps/clock/js/**/*.js"]
|
||||
},
|
||||
"plugins": [
|
||||
"plugins/markdown",
|
||||
"plugins/summarize"
|
||||
],
|
||||
"opts": {
|
||||
"private": true,
|
||||
"recurse": true,
|
||||
"destination": "docs/clock"
|
||||
}
|
||||
}
|
||||
12
gaia/apps/clock/locales/alarms.en-US.properties
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
ac_awake_opus = Awake
|
||||
ac_crystalize_opus = Crystalize
|
||||
ac_cycle_opus = Cycle
|
||||
ac_digicloud_opus = Digicloud
|
||||
ac_humming_waves_opus = Humming Waves
|
||||
ac_into_the_void_opus = Into the Void
|
||||
ac_lightly_opus = Lightly
|
||||
ac_mobile_opus = Mobile
|
||||
ac_pinger_opus = Pinger
|
||||
ac_skip_opus = Skip
|
||||
ac_tri_opus = Tri
|
||||
ac_universal_opus = Universal
|
||||
93
gaia/apps/clock/locales/clock.en-US.properties
Executable file
|
|
@ -0,0 +1,93 @@
|
|||
reset = Reset
|
||||
start = Start
|
||||
stop = Stop
|
||||
cancel = Cancel
|
||||
|
||||
hours = Hours
|
||||
minutes = Minutes
|
||||
|
||||
alarm = Alarm
|
||||
timer = Timer
|
||||
stopwatch = Stopwatch
|
||||
alarmName.placeholder = Alarm name
|
||||
newAlarm = New alarm
|
||||
editAlarm = Edit alarm
|
||||
|
||||
pause = Pause
|
||||
resume = Resume
|
||||
lap = Lap
|
||||
lap-number = Lap {{n}}
|
||||
|
||||
delete = Delete
|
||||
time = Time
|
||||
snooze-label = Snooze
|
||||
snooze-button = Snooze
|
||||
repeat = Repeat
|
||||
vibrate = Vibrate
|
||||
close = Close
|
||||
save = Save
|
||||
done = Done
|
||||
|
||||
closeButton.ariaLabel = Close
|
||||
newAlarmButton.ariaLabel = New alarm
|
||||
|
||||
# The label for a slider that allows the user to change
|
||||
# the System Alarm Volume for all clock alarms.
|
||||
alarm-volume-label = System Alarm Volume
|
||||
|
||||
sound = Sound
|
||||
noSound = No Sound
|
||||
|
||||
everyday = Every day
|
||||
weekdays = Weekdays
|
||||
weekends = Weekends
|
||||
never = Never
|
||||
|
||||
vibrateOn = On
|
||||
vibrateOff = Off
|
||||
|
||||
plusMinute = +1 Min
|
||||
|
||||
countdown_lessThanAnHour = The alarm is set for <strong>{{alarmMinutes}}</strong> from now.
|
||||
countdown_moreThanAnHour = The alarm is set for {{alarmHours}} and {{alarmMinutes}} from now.
|
||||
countdown_moreThanADay = The alarm is set for {{alarmDays}} and {{alarmHours}} from now.
|
||||
|
||||
alarmMinutes = {[ plural(minutes) ]}
|
||||
alarmMinutes[zero] = less than a minute
|
||||
alarmMinutes[one] = one minute
|
||||
alarmMinutes[two] = {{minutes}} minutes
|
||||
alarmMinutes[few] = {{minutes}} minutes
|
||||
alarmMinutes[many] = {{minutes}} minutes
|
||||
alarmMinutes[other] = {{minutes}} minutes
|
||||
|
||||
alarmHours = {[ plural(hours) ]}
|
||||
alarmHours[zero] = <strong>{{hours}} hours</strong>
|
||||
alarmHours[one] = <strong>{{hours}} hour</strong>
|
||||
alarmHours[two] = <strong>{{hours}} hours</strong>
|
||||
alarmHours[few] = <strong>{{hours}} hours</strong>
|
||||
alarmHours[many] = <strong>{{hours}} hours</strong>
|
||||
alarmHours[other] = <strong>{{hours}} hours</strong>
|
||||
|
||||
alarmDays = {[ plural(days) ]}
|
||||
alarmDays[zero] = <strong>{{days}} days</strong>
|
||||
alarmDays[one] = <strong>{{days}} day</strong>
|
||||
alarmDays[two] = <strong>{{days}} days</strong>
|
||||
alarmDays[few] = <strong>{{days}} days</strong>
|
||||
alarmDays[many] = <strong>{{days}} days</strong>
|
||||
alarmDays[other] = <strong>{{days}} days</strong>
|
||||
|
||||
nSpinnerMinutes.ariaValuetext = {[ plural(n) ]}
|
||||
nSpinnerMinutes.ariaValuetext[zero] = zero minutes
|
||||
nSpinnerMinutes.ariaValuetext[one] = one minute
|
||||
nSpinnerMinutes.ariaValuetext[two] = {{n}} minutes
|
||||
nSpinnerMinutes.ariaValuetext[few] = {{n}} minutes
|
||||
nSpinnerMinutes.ariaValuetext[many] = {{n}} minutes
|
||||
nSpinnerMinutes.ariaValuetext[other] = {{n}} minutes
|
||||
|
||||
nSpinnerHours.ariaValuetext = {[ plural(n) ]}
|
||||
nSpinnerHours.ariaValuetext[zero] = zero hours
|
||||
nSpinnerHours.ariaValuetext[one] = {{n}} hour
|
||||
nSpinnerHours.ariaValuetext[two] = {{n}} hours
|
||||
nSpinnerHours.ariaValuetext[few] = {{n}} hours
|
||||
nSpinnerHours.ariaValuetext[many] = {{n}} hours
|
||||
nSpinnerHours.ariaValuetext[other] = {{n}} hours
|
||||
78
gaia/apps/clock/manifest.webapp
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"name": "Clock",
|
||||
"description": "Gaia Clock",
|
||||
"type": "certified",
|
||||
"launch_path": "/index.html",
|
||||
"developer": {
|
||||
"name": "The Gaia Team",
|
||||
"url": "https://github.com/mozilla-b2g/gaia"
|
||||
},
|
||||
"permissions": {
|
||||
"themeable": {},
|
||||
"alarms": {},
|
||||
"settings": {
|
||||
"access": "readwrite"
|
||||
},
|
||||
"attention": {},
|
||||
"audio-channel-alarm": {},
|
||||
"moz-extremely-unstable-and-will-change-webcomponents": {}
|
||||
},
|
||||
"datastores-owned": {
|
||||
"alarms": {
|
||||
"access": "readwrite",
|
||||
"description": "The alarms data store for the Data Store API."
|
||||
}
|
||||
},
|
||||
"locales": {
|
||||
"ar": {
|
||||
"name": "الساعة",
|
||||
"description": "Gaia الساعة"
|
||||
},
|
||||
"en-US": {
|
||||
"name": "Clock",
|
||||
"description": "Gaia Clock"
|
||||
},
|
||||
"fr": {
|
||||
"name": "Horloge",
|
||||
"description": "Horloge de Gaia"
|
||||
},
|
||||
"zh-TW": {
|
||||
"name": "時鐘",
|
||||
"description": "Gaia 時鐘"
|
||||
}
|
||||
},
|
||||
"default_locale": "en-US",
|
||||
"icons": {
|
||||
"84": "/style/icons/clock_84.png",
|
||||
"126": "/style/icons/clock_126.png",
|
||||
"142": "/style/icons/clock_142.png",
|
||||
"189": "/style/icons/clock_189.png",
|
||||
"284": "/style/icons/clock_284.png"
|
||||
},
|
||||
"orientation": "default",
|
||||
"connections": {
|
||||
"gaia_alarm": {
|
||||
"handler_path": "/index.html",
|
||||
"description": "Create alarm",
|
||||
"rules": {
|
||||
"manifestURLs": [
|
||||
"app://vaani.gaiamobile.org/manifest.webapp"
|
||||
]
|
||||
}
|
||||
},
|
||||
"gaia_timer": {
|
||||
"handler_path": "/index.html",
|
||||
"description": "Create, pause, start and cancel Timer",
|
||||
"rules": {
|
||||
"manifestURLs": [
|
||||
"app://vaani.gaiamobile.org/manifest.webapp"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"alarm": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
43
gaia/apps/clock/onring.html
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<meta charset="utf-8">
|
||||
<meta name="theme-color" content="#000">
|
||||
<title>Clock</title>
|
||||
<link rel="stylesheet" type="text/css" href="style/clock.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/alarm.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/onring.css">
|
||||
|
||||
<!-- Date/Time Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="locales/clock.{locale}.properties">
|
||||
<link rel="localization" href="locales/alarms.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/date/date.{locale}.properties">
|
||||
<script defer src="/shared/js/intl/l20n.js"></script>
|
||||
<script defer src="/shared/js/date_time_helper.js"></script>
|
||||
<script defer src="/shared/js/moz_intl.js"></script>
|
||||
<!-- We need IntlHelper synchronously for onring.js -->
|
||||
<script src="/shared/js/intl_helper.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="ring-display" data-ring-type="alarm">
|
||||
<div id="ring-icon"></div>
|
||||
<div id="ring-clock">
|
||||
<div id="ring-clock-display">
|
||||
<span id="ring-clock-time"></span>
|
||||
</div>
|
||||
<div id="ring-label"></div>
|
||||
</div>
|
||||
<div id="footer-button-container">
|
||||
<button id="ring-button-stop" class="recommend" data-l10n-id="stop"></button>
|
||||
<button id="ring-button-snooze" data-l10n-id="snooze-button"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script data-main="js/onring.js" src="js/alameda.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
10
gaia/apps/clock/package.json
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"webide": {
|
||||
"prepackage": {
|
||||
"command": "make",
|
||||
"env": ["APP=clock"],
|
||||
"cwd": "../.."
|
||||
},
|
||||
"packageDir": "../../build_stage/clock/"
|
||||
}
|
||||
}
|
||||
339
gaia/apps/clock/style/alarm.css
Executable file
|
|
@ -0,0 +1,339 @@
|
|||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#alarm-name {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ul.compact {
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
ul.compact li {
|
||||
position: relative;
|
||||
min-height: 5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 0.1rem solid #232626;
|
||||
}
|
||||
|
||||
ul.compact li:first-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
ul.compact li:last-child {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ul.compact li label {
|
||||
padding: 0 1.5rem 0.5rem;
|
||||
color: #9a9898;
|
||||
font-size: 1.6rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
ul.compact li label:active {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#alarm-new {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
margin-top: 2.1rem;
|
||||
margin-right: 1.5rem;
|
||||
|
||||
background: url(images/add_alarm.png) no-repeat center / 3rem;
|
||||
background-size: 100% auto;
|
||||
}
|
||||
|
||||
#edit-alarm {
|
||||
overflow-x: hidden;
|
||||
height: calc(100% - 7rem);
|
||||
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#alarm-volume-input {
|
||||
width: 100%;
|
||||
margin: 1rem 0 1.5rem;
|
||||
}
|
||||
|
||||
.new #delete-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#time-select {
|
||||
/* Bug 1117652: copypaste bubble would be triggered if tapping or longpressing
|
||||
on the input field, so we let the cursor out of the screen to
|
||||
prevent it. */
|
||||
text-indent: 110%;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. COMPLEX: Scoped style-sheets within <gaia-header>
|
||||
* trump all other rules. !important is required
|
||||
* for an app to over-ride the default appearance
|
||||
* of content within the custom-element. This may
|
||||
* change when we get `:host` and `:content` selectors
|
||||
* in shadow-dom.
|
||||
*/
|
||||
|
||||
section[role="region"] > gaia-header h1.edit-alarm-title {
|
||||
display: block !important; /* 1 */
|
||||
}
|
||||
section[role="region"] > gaia-header h1.new-alarm-title {
|
||||
display: none !important; /* 1 */
|
||||
}
|
||||
section[role="region"].new > gaia-header h1.edit-alarm-title {
|
||||
display: none !important; /* 1 */
|
||||
}
|
||||
section[role="region"].new > gaia-header h1.new-alarm-title {
|
||||
display: block !important; /* 1 */
|
||||
}
|
||||
|
||||
/* ----------- Alarm List ---------- */
|
||||
#alarms {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: calc(100% - 22rem);
|
||||
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
ul#alarms li.alarm-cell:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 1.5rem;
|
||||
width: calc(100% - 3rem);
|
||||
height: .1rem;
|
||||
background-color: rgba(115, 125, 128, 0.2);
|
||||
}
|
||||
|
||||
ul#alarms li.alarm-cell { /* 6rem, including a 0.13rem border */
|
||||
position: relative;
|
||||
height: 6rem;
|
||||
}
|
||||
|
||||
ul#alarms li.alarm-cell > a:active {
|
||||
background-color: rgba(52,140,158, .6);
|
||||
}
|
||||
|
||||
ul#alarms li.alarm-cell > a:active {
|
||||
color: #cfe2e5;
|
||||
}
|
||||
|
||||
ul#alarms li.alarm-cell label:active {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#alarms .alarmEnable {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
width: calc(8.13rem);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#alarms .alarmEnable input[type="checkbox"] + span {
|
||||
position: absolute;
|
||||
top: calc(50% - 1.7rem);
|
||||
right: 1.5rem;
|
||||
|
||||
width: 3.4rem;
|
||||
height: 3.4rem;
|
||||
margin: 0;
|
||||
|
||||
background: url(images/alarm_off.png) no-repeat center top / 3.4rem;
|
||||
}
|
||||
#alarms .alarmEnable input[type="checkbox"]:checked + span {
|
||||
border-color: #00acce;
|
||||
background: url(images/alarm_on.png) no-repeat center top / 3.4rem;
|
||||
}
|
||||
|
||||
#alarms .alarmEnable > input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#alarms a.alarm-item {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
font: lighter 1.4rem sans-serif;
|
||||
color: #737D80;
|
||||
}
|
||||
|
||||
#alarms a.alarm-item:active {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a.alarm-item > span {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
a.alarm-item > .time {
|
||||
position: absolute;
|
||||
top: 1.0rem;
|
||||
left: 1.7rem;
|
||||
font-size: 2.8rem;
|
||||
font-family: sans-serif;
|
||||
font-weight: 300;
|
||||
color: #cfe2e5;
|
||||
}
|
||||
|
||||
a.alarm-item small {
|
||||
font-size: 1.4rem;
|
||||
font-weight: normal;
|
||||
color: #5C6466;
|
||||
}
|
||||
|
||||
a.alarm-item > .label {
|
||||
position: absolute;
|
||||
left: 12.5rem;
|
||||
right: 4.5rem;
|
||||
top: 2.35rem;
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
color: #737D80;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
a.alarm-item > .repeat {
|
||||
position: absolute;
|
||||
top: 4.2rem;
|
||||
left: 1.7rem;
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
text-overflow: ellipsis;
|
||||
color: #A1B0B2;
|
||||
}
|
||||
|
||||
a.alarm-item.with-repeat > .time {
|
||||
top: 0.3rem;
|
||||
}
|
||||
|
||||
a.alarm-item.with-repeat > .label {
|
||||
top: 1.65rem;
|
||||
}
|
||||
|
||||
a.alarm-item.with-repeat > .repeat {
|
||||
top: 3.6rem;
|
||||
}
|
||||
|
||||
#digital-clock.visible ~ #alarms.count4 li:first-child {
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* These range input values were taken from the System App and
|
||||
* retooled for the dark theme currently used by Clock. When
|
||||
* changing to a light theme in the UI overhaul, it may make
|
||||
* sense to pull this directly from System again.
|
||||
*/
|
||||
|
||||
input[type=range] {
|
||||
-moz-appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
input[type=range]:-moz-focusring {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
ul li > label input[type="range"] {
|
||||
position: relative;
|
||||
top: calc(50% - 1.5rem);
|
||||
left: 5rem;
|
||||
right: 7rem;
|
||||
height: 3rem;
|
||||
width: calc(100% - 15rem);
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-track {
|
||||
height: 0.1rem;
|
||||
background-color: #7e7e7e;
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-progress {
|
||||
height: 0.3rem;
|
||||
background-color: #01c5ed;
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-thumb {
|
||||
width: 2.8rem;
|
||||
height: 2.8rem;
|
||||
border: solid 0.1rem transparent;
|
||||
border-radius: 3rem;
|
||||
transition: border 0.15s ease;
|
||||
-moz-box-sizing: border-box;
|
||||
background: #fff no-repeat 50% 50%;
|
||||
background-size: 2.8rem 2.8rem;
|
||||
}
|
||||
|
||||
input[type=range]:active::-moz-range-thumb {
|
||||
border: solid 0.4rem #01c5ed;
|
||||
}
|
||||
|
||||
#alarm-delete {
|
||||
border-radius: 2rem;
|
||||
background-color: #e51e1e;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* RTL View */
|
||||
html[dir=rtl] #alarm-new {
|
||||
margin-left: 1.5rem;
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
html[dir=rtl] #alarms .alarmEnable {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
html[dir=rtl] a.alarm-item > .time {
|
||||
left: auto;
|
||||
right: 1.7rem;
|
||||
|
||||
}
|
||||
html[dir=rtl] a.alarm-item > .label {
|
||||
left: 4.5rem;
|
||||
right: 12.5rem;
|
||||
}
|
||||
html[dir=rtl] a.alarm-item > .repeat {
|
||||
left: auto;
|
||||
right: 1.7rem;
|
||||
}
|
||||
html[dir=rtl] a.alarm-item > .repeat {
|
||||
left: auto;
|
||||
right: 1.7rem;
|
||||
}
|
||||
html[dir=rtl] #alarms .alarmEnable input[type="checkbox"] + span {
|
||||
left: 1.5rem;
|
||||
right: auto;
|
||||
}
|
||||
189
gaia/apps/clock/style/clock.css
Executable file
|
|
@ -0,0 +1,189 @@
|
|||
@import url('../shared/style/input_areas.css');
|
||||
@import url('../shared/style/buttons.css');
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
background-color: #000;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
/* Absolutely-positioned elements don't inherit -moz-user-select. */
|
||||
#clock-tabs {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
#clock-view {
|
||||
width: 100%;
|
||||
height: calc(100% - 0.13rem);
|
||||
|
||||
color: black;
|
||||
}
|
||||
|
||||
.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#clock-time {
|
||||
letter-spacing: -0.5rem;
|
||||
font-weight: 300;
|
||||
font-size: 11rem;
|
||||
color: #CFE2E6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#clock-time small {
|
||||
font-weight: 300;
|
||||
font-size: 3.7rem;
|
||||
color: #5C6466;
|
||||
text-transform: uppercase;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#clock-day-date {
|
||||
font-weight: 300;
|
||||
font-size: 2.1rem;
|
||||
margin: 2.6rem 0 0 1.5rem;
|
||||
line-height: 1.7rem;
|
||||
position: absolute;
|
||||
color: #737D80;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#clock-day-date b {
|
||||
font-weight: normal;
|
||||
color: #A1B0B2;
|
||||
}
|
||||
|
||||
/* digital clock */
|
||||
#digital-clock {
|
||||
display: none;
|
||||
margin-top: 6.50rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#digital-clock.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#digital-clock-face {
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* analog clock */
|
||||
#analog-clock {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#analog-clock.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Make sure the rotating hand elements are never clickable outside of our box */
|
||||
#analog-clock-container {
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#analog-clock-container.large {
|
||||
width: 27rem;
|
||||
height: 27rem;
|
||||
margin: 6.9rem auto 0;
|
||||
}
|
||||
|
||||
#analog-clock-container.medium {
|
||||
width: 21.8rem;
|
||||
height: 21.8rem;
|
||||
margin: 6.8rem auto 0;
|
||||
}
|
||||
|
||||
#analog-clock-container.small {
|
||||
width: 14.8rem;
|
||||
height: 14.8rem;
|
||||
margin: 5.8rem auto 0;
|
||||
}
|
||||
|
||||
#analog-clock-face {
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-image: url('images/analog_clock_face.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
#analog-clock-hands {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 1.5%; /* to correctly snap the hands to the face */
|
||||
}
|
||||
|
||||
.analog-clock-hand {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
#secondhand {
|
||||
background-image: url('images/analog_clock_second_hand.svg');
|
||||
}
|
||||
|
||||
#minutehand {
|
||||
background-image: url('images/analog_clock_minute_hand.svg');
|
||||
}
|
||||
|
||||
#hourhand {
|
||||
background-image: url('images/analog_clock_hour_hand.svg');
|
||||
}
|
||||
|
||||
/* banner*/
|
||||
#banner-countdown {
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
-moz-transition: opacity 600ms ease;
|
||||
}
|
||||
#banner-countdown.visible {
|
||||
z-index: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#banner-countdown strong {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* localization */
|
||||
body.hidden *[data-l10n-id] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* RTL View */
|
||||
html[dir=rtl] #clock-day-date {
|
||||
margin-left: 0;
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
4
gaia/apps/clock/style/form_button.css
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
.form-button-hide {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
17
gaia/apps/clock/style/hacks.css
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Clock Hacks: Place any temporary, horrible, or otherwise sad
|
||||
* but depressingly needed CSS hacks in this file. Review this
|
||||
* file regularly to see if any of these temporary fixes can later
|
||||
* be removed.
|
||||
*
|
||||
* NOTE: This file is included after all other styles, so that
|
||||
* you can override other styles without using !important.
|
||||
*/
|
||||
|
||||
|
||||
/* This tab override CSS should be removed when you change the app
|
||||
layout to correctly use the BB tabs CSS. See bug 1017398. */
|
||||
.bb-tablist > li > a[aria-selected="true"] {
|
||||
color: #0AC;
|
||||
font-weight: 500;
|
||||
}
|
||||
BIN
gaia/apps/clock/style/icons/clock_126.png
Executable file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
gaia/apps/clock/style/icons/clock_142.png
Executable file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
gaia/apps/clock/style/icons/clock_189.png
Executable file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
gaia/apps/clock/style/icons/clock_284.png
Executable file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
gaia/apps/clock/style/icons/clock_84.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
gaia/apps/clock/style/images/add_alarm.png
Executable file
|
After Width: | Height: | Size: 393 B |
BIN
gaia/apps/clock/style/images/add_alarm@1.5x.png
Executable file
|
After Width: | Height: | Size: 521 B |
BIN
gaia/apps/clock/style/images/add_alarm@2.25x.png
Executable file
|
After Width: | Height: | Size: 759 B |
BIN
gaia/apps/clock/style/images/add_alarm@2x.png
Executable file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
gaia/apps/clock/style/images/alarm_off.png
Executable file
|
After Width: | Height: | Size: 487 B |
BIN
gaia/apps/clock/style/images/alarm_off@1.5x.png
Executable file
|
After Width: | Height: | Size: 892 B |
BIN
gaia/apps/clock/style/images/alarm_off@2.25x.png
Executable file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
gaia/apps/clock/style/images/alarm_off@2x.png
Executable file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
gaia/apps/clock/style/images/alarm_on.png
Executable file
|
After Width: | Height: | Size: 612 B |
BIN
gaia/apps/clock/style/images/alarm_on@1.5x.png
Executable file
|
After Width: | Height: | Size: 970 B |
BIN
gaia/apps/clock/style/images/alarm_on@2.25x.png
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
gaia/apps/clock/style/images/alarm_on@2x.png
Executable file
|
After Width: | Height: | Size: 2.6 KiB |
1
gaia/apps/clock/style/images/analog_clock_face.svg
Executable file
|
After Width: | Height: | Size: 4.9 KiB |
1
gaia/apps/clock/style/images/analog_clock_hour_hand.svg
Executable file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="236.713" height="245.966" viewBox="0 0 236.713 245.966"><path d="m 159.5,216.193 c 1.61,0 3.029,-0.813 3.873,-2.05 -0.028,-25.928 -0.304,-49.834 -0.755,-69.356 -1.028,-0.099 -2.064,-0.169 -3.118,-0.169 -1.054,0 -2.09,0.07 -3.118,0.169 -0.451,19.522 -0.727,43.428 -0.755,69.356 0.844,1.237 2.263,2.05 3.873,2.05 z" fill="#b7c4c6" fill-rule="evenodd" transform="translate(-41.209 -84.004)"/></svg>
|
||||
|
After Width: | Height: | Size: 442 B |
1
gaia/apps/clock/style/images/analog_clock_minute_hand.svg
Executable file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="236.713" height="245.966" viewBox="0 0 236.713 245.966"><path d="m 162.453,226.531 c -0.123,-40.105 -0.493,-75.388 -1.017,-100.614 -0.643,-0.024 -1.286,-0.049 -1.936,-0.049 -0.649,0 -1.292,0.025 -1.935,0.049 -0.524,25.221 -0.894,60.493 -1.017,100.586 -0.032,0.096 -0.054,0.193 -0.054,0.294 0,0.911 1.345,1.649 3.004,1.649 1.659,0 3.004,-0.738 3.004,-1.649 0,-0.091 -0.023,-0.179 -0.049,-0.266 z" fill="#b6c4c6" fill-rule="evenodd" transform="translate(-41.209 -84.004)"/></svg>
|
||||
|
After Width: | Height: | Size: 524 B |
1
gaia/apps/clock/style/images/analog_clock_second_hand.svg
Executable file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="236.713" height="245.966" viewBox="0 0 236.713 245.966"><path d="m 158.93,122.507 -0.359,107.722 c 0.31,0.005 0.617,0.023 0.929,0.023 0.312,0 0.619,-0.018 0.93,-0.023 l -0.359,-107.722 c -0.191,-0.002 -0.379,-0.014 -0.57,-0.014 -0.192,-10e-4 -0.38,0.012 -0.571,0.014 z" fill="#53929e" fill-rule="evenodd" transform="translate(-41.209 -84.004)"/></svg>
|
||||
|
After Width: | Height: | Size: 398 B |
BIN
gaia/apps/clock/style/images/attention_background.png
Executable file
|
After Width: | Height: | Size: 82 KiB |
BIN
gaia/apps/clock/style/images/attention_background@1.5x.png
Executable file
|
After Width: | Height: | Size: 42 KiB |
BIN
gaia/apps/clock/style/images/attention_background@2.25x.png
Executable file
|
After Width: | Height: | Size: 64 KiB |
BIN
gaia/apps/clock/style/images/attention_background@2x.png
Executable file
|
After Width: | Height: | Size: 408 KiB |
BIN
gaia/apps/clock/style/images/attention_button_container.png
Executable file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
gaia/apps/clock/style/images/attention_button_container@1.5x.png
Executable file
|
After Width: | Height: | Size: 7 KiB |
BIN
gaia/apps/clock/style/images/attention_button_container@2.25x.png
Executable file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
gaia/apps/clock/style/images/attention_button_container@2x.png
Executable file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
gaia/apps/clock/style/images/attention_overlay_icon.png
Executable file
|
After Width: | Height: | Size: 797 B |
BIN
gaia/apps/clock/style/images/attention_overlay_icon@1.5x.png
Executable file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
gaia/apps/clock/style/images/attention_overlay_icon@2.25x.png
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
gaia/apps/clock/style/images/attention_overlay_icon@2x.png
Executable file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
gaia/apps/clock/style/images/onring.png
Executable file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
gaia/apps/clock/style/images/onring@1.5x.png
Executable file
|
After Width: | Height: | Size: 3.7 KiB |