diff --git a/Gruntfile.js b/Gruntfile.js index df99d29..0b97138 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,6 +2,7 @@ module.exports = function(grunt) { require('grunt-task-loader')(grunt); grunt.initConfig({ + pkg: require('./package.json'), browserify: { dev: { files: [{ @@ -53,15 +54,6 @@ module.exports = function(grunt) { } } }, - mochify: { - options: { - reporter: 'land' - }, - tests: { - src: 'test/**/*.js', - options: '<%= browserify.dev.options %>' - } - }, less: { dev: { files: [{ @@ -90,12 +82,11 @@ module.exports = function(grunt) { }] } }, - mochaTest: { - tests: { - src: ['tests/**/*.js'], - options: { - reporter: 'landing' - } + zip: { + release: { + dest: 'releases/hawk-<%= pkg.version %>.zip', + src: 'build/**/*', + cwd: 'build/' } }, watch: { @@ -116,6 +107,6 @@ module.exports = function(grunt) { }); grunt.registerTask('default', ['browserify:dev', 'less:dev', 'copy']); - grunt.registerTask('production', ['browserify:prod', 'less:prod', 'copy']); + grunt.registerTask('production', ['browserify', 'less:prod', 'copy', 'zip']); grunt.registerTask('test', 'mochaTest'); }; diff --git a/build/img/trianglified.svg b/build/img/trianglified.svg deleted file mode 100644 index 0d755fd..0000000 --- a/build/img/trianglified.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/build/js/libs/l10n.js b/build/js/libs/l10n.js deleted file mode 100644 index cfc5c51..0000000 --- a/build/js/libs/l10n.js +++ /dev/null @@ -1,1571 +0,0 @@ -// This is the Gaia version of l20n: https://github.com/l20n/l20n.js -// l20n is Apache 2.0 licensed: https://github.com/l20n/l20n.js/blob/master/LICENSE -// You can find the latest build for Gaia here: https://github.com/mozilla-b2g/gaia/blob/master/shared/js/l10n.js -(function(window, undefined) { - 'use strict'; - - /* jshint validthis:true */ - function L10nError(message, id, loc) { - this.name = 'L10nError'; - this.message = message; - this.id = id; - this.loc = loc; - } - L10nError.prototype = Object.create(Error.prototype); - L10nError.prototype.constructor = L10nError; - - - /* jshint browser:true */ - - var io = { - load: function load(url, callback, sync) { - var xhr = new XMLHttpRequest(); - - if (xhr.overrideMimeType) { - xhr.overrideMimeType('text/plain'); - } - - xhr.open('GET', url, !sync); - - xhr.addEventListener('load', function io_load(e) { - if (e.target.status === 200 || e.target.status === 0) { - callback(null, e.target.responseText); - } else { - callback(new L10nError('Not found: ' + url)); - } - }); - xhr.addEventListener('error', callback); - xhr.addEventListener('timeout', callback); - - // the app: protocol throws on 404, see https://bugzil.la/827243 - try { - xhr.send(null); - } catch (e) { - callback(new L10nError('Not found: ' + url)); - } - }, - - loadJSON: function loadJSON(url, callback) { - var xhr = new XMLHttpRequest(); - - if (xhr.overrideMimeType) { - xhr.overrideMimeType('application/json'); - } - - xhr.open('GET', url); - - xhr.responseType = 'json'; - xhr.addEventListener('load', function io_loadjson(e) { - if (e.target.status === 200 || e.target.status === 0) { - callback(null, e.target.response); - } else { - callback(new L10nError('Not found: ' + url)); - } - }); - xhr.addEventListener('error', callback); - xhr.addEventListener('timeout', callback); - - // the app: protocol throws on 404, see https://bugzil.la/827243 - try { - xhr.send(null); - } catch (e) { - callback(new L10nError('Not found: ' + url)); - } - } - }; - - function EventEmitter() {} - - EventEmitter.prototype.emit = function ee_emit() { - if (!this._listeners) { - return; - } - - var args = Array.prototype.slice.call(arguments); - var type = args.shift(); - if (!this._listeners[type]) { - return; - } - - var typeListeners = this._listeners[type].slice(); - for (var i = 0; i < typeListeners.length; i++) { - typeListeners[i].apply(this, args); - } - }; - - EventEmitter.prototype.addEventListener = function ee_add(type, listener) { - if (!this._listeners) { - this._listeners = {}; - } - if (!(type in this._listeners)) { - this._listeners[type] = []; - } - this._listeners[type].push(listener); - }; - - EventEmitter.prototype.removeEventListener = function ee_rm(type, listener) { - if (!this._listeners) { - return; - } - - var typeListeners = this._listeners[type]; - var pos = typeListeners.indexOf(listener); - if (pos === -1) { - return; - } - - typeListeners.splice(pos, 1); - }; - - - function getPluralRule(lang) { - var locales2rules = { - 'af': 3, - 'ak': 4, - 'am': 4, - 'ar': 1, - 'asa': 3, - 'az': 0, - 'be': 11, - 'bem': 3, - 'bez': 3, - 'bg': 3, - 'bh': 4, - 'bm': 0, - 'bn': 3, - 'bo': 0, - 'br': 20, - 'brx': 3, - 'bs': 11, - 'ca': 3, - 'cgg': 3, - 'chr': 3, - 'cs': 12, - 'cy': 17, - 'da': 3, - 'de': 3, - 'dv': 3, - 'dz': 0, - 'ee': 3, - 'el': 3, - 'en': 3, - 'eo': 3, - 'es': 3, - 'et': 3, - 'eu': 3, - 'fa': 0, - 'ff': 5, - 'fi': 3, - 'fil': 4, - 'fo': 3, - 'fr': 5, - 'fur': 3, - 'fy': 3, - 'ga': 8, - 'gd': 24, - 'gl': 3, - 'gsw': 3, - 'gu': 3, - 'guw': 4, - 'gv': 23, - 'ha': 3, - 'haw': 3, - 'he': 2, - 'hi': 4, - 'hr': 11, - 'hu': 0, - 'id': 0, - 'ig': 0, - 'ii': 0, - 'is': 3, - 'it': 3, - 'iu': 7, - 'ja': 0, - 'jmc': 3, - 'jv': 0, - 'ka': 0, - 'kab': 5, - 'kaj': 3, - 'kcg': 3, - 'kde': 0, - 'kea': 0, - 'kk': 3, - 'kl': 3, - 'km': 0, - 'kn': 0, - 'ko': 0, - 'ksb': 3, - 'ksh': 21, - 'ku': 3, - 'kw': 7, - 'lag': 18, - 'lb': 3, - 'lg': 3, - 'ln': 4, - 'lo': 0, - 'lt': 10, - 'lv': 6, - 'mas': 3, - 'mg': 4, - 'mk': 16, - 'ml': 3, - 'mn': 3, - 'mo': 9, - 'mr': 3, - 'ms': 0, - 'mt': 15, - 'my': 0, - 'nah': 3, - 'naq': 7, - 'nb': 3, - 'nd': 3, - 'ne': 3, - 'nl': 3, - 'nn': 3, - 'no': 3, - 'nr': 3, - 'nso': 4, - 'ny': 3, - 'nyn': 3, - 'om': 3, - 'or': 3, - 'pa': 3, - 'pap': 3, - 'pl': 13, - 'ps': 3, - 'pt': 3, - 'rm': 3, - 'ro': 9, - 'rof': 3, - 'ru': 11, - 'rwk': 3, - 'sah': 0, - 'saq': 3, - 'se': 7, - 'seh': 3, - 'ses': 0, - 'sg': 0, - 'sh': 11, - 'shi': 19, - 'sk': 12, - 'sl': 14, - 'sma': 7, - 'smi': 7, - 'smj': 7, - 'smn': 7, - 'sms': 7, - 'sn': 3, - 'so': 3, - 'sq': 3, - 'sr': 11, - 'ss': 3, - 'ssy': 3, - 'st': 3, - 'sv': 3, - 'sw': 3, - 'syr': 3, - 'ta': 3, - 'te': 3, - 'teo': 3, - 'th': 0, - 'ti': 4, - 'tig': 3, - 'tk': 3, - 'tl': 4, - 'tn': 3, - 'to': 0, - 'tr': 0, - 'ts': 3, - 'tzm': 22, - 'uk': 11, - 'ur': 3, - 've': 3, - 'vi': 0, - 'vun': 3, - 'wa': 4, - 'wae': 3, - 'wo': 0, - 'xh': 3, - 'xog': 3, - 'yo': 0, - 'zh': 0, - 'zu': 3 - }; - - // utility functions for plural rules methods - function isIn(n, list) { - return list.indexOf(n) !== -1; - } - function isBetween(n, start, end) { - return start <= n && n <= end; - } - - // list of all plural rules methods: - // map an integer to the plural form name to use - var pluralRules = { - '0': function() { - return 'other'; - }, - '1': function(n) { - if ((isBetween((n % 100), 3, 10))) { - return 'few'; - } - if (n === 0) { - return 'zero'; - } - if ((isBetween((n % 100), 11, 99))) { - return 'many'; - } - if (n === 2) { - return 'two'; - } - if (n === 1) { - return 'one'; - } - return 'other'; - }, - '2': function(n) { - if (n !== 0 && (n % 10) === 0) { - return 'many'; - } - if (n === 2) { - return 'two'; - } - if (n === 1) { - return 'one'; - } - return 'other'; - }, - '3': function(n) { - if (n === 1) { - return 'one'; - } - return 'other'; - }, - '4': function(n) { - if ((isBetween(n, 0, 1))) { - return 'one'; - } - return 'other'; - }, - '5': function(n) { - if ((isBetween(n, 0, 2)) && n !== 2) { - return 'one'; - } - return 'other'; - }, - '6': function(n) { - if (n === 0) { - return 'zero'; - } - if ((n % 10) === 1 && (n % 100) !== 11) { - return 'one'; - } - return 'other'; - }, - '7': function(n) { - if (n === 2) { - return 'two'; - } - if (n === 1) { - return 'one'; - } - return 'other'; - }, - '8': function(n) { - if ((isBetween(n, 3, 6))) { - return 'few'; - } - if ((isBetween(n, 7, 10))) { - return 'many'; - } - if (n === 2) { - return 'two'; - } - if (n === 1) { - return 'one'; - } - return 'other'; - }, - '9': function(n) { - if (n === 0 || n !== 1 && (isBetween((n % 100), 1, 19))) { - return 'few'; - } - if (n === 1) { - return 'one'; - } - return 'other'; - }, - '10': function(n) { - if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19))) { - return 'few'; - } - if ((n % 10) === 1 && !(isBetween((n % 100), 11, 19))) { - return 'one'; - } - return 'other'; - }, - '11': function(n) { - if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) { - return 'few'; - } - if ((n % 10) === 0 || - (isBetween((n % 10), 5, 9)) || - (isBetween((n % 100), 11, 14))) { - return 'many'; - } - if ((n % 10) === 1 && (n % 100) !== 11) { - return 'one'; - } - return 'other'; - }, - '12': function(n) { - if ((isBetween(n, 2, 4))) { - return 'few'; - } - if (n === 1) { - return 'one'; - } - return 'other'; - }, - '13': function(n) { - if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) { - return 'few'; - } - if (n !== 1 && (isBetween((n % 10), 0, 1)) || - (isBetween((n % 10), 5, 9)) || - (isBetween((n % 100), 12, 14))) { - return 'many'; - } - if (n === 1) { - return 'one'; - } - return 'other'; - }, - '14': function(n) { - if ((isBetween((n % 100), 3, 4))) { - return 'few'; - } - if ((n % 100) === 2) { - return 'two'; - } - if ((n % 100) === 1) { - return 'one'; - } - return 'other'; - }, - '15': function(n) { - if (n === 0 || (isBetween((n % 100), 2, 10))) { - return 'few'; - } - if ((isBetween((n % 100), 11, 19))) { - return 'many'; - } - if (n === 1) { - return 'one'; - } - return 'other'; - }, - '16': function(n) { - if ((n % 10) === 1 && n !== 11) { - return 'one'; - } - return 'other'; - }, - '17': function(n) { - if (n === 3) { - return 'few'; - } - if (n === 0) { - return 'zero'; - } - if (n === 6) { - return 'many'; - } - if (n === 2) { - return 'two'; - } - if (n === 1) { - return 'one'; - } - return 'other'; - }, - '18': function(n) { - if (n === 0) { - return 'zero'; - } - if ((isBetween(n, 0, 2)) && n !== 0 && n !== 2) { - return 'one'; - } - return 'other'; - }, - '19': function(n) { - if ((isBetween(n, 2, 10))) { - return 'few'; - } - if ((isBetween(n, 0, 1))) { - return 'one'; - } - return 'other'; - }, - '20': function(n) { - if ((isBetween((n % 10), 3, 4) || ((n % 10) === 9)) && !( - isBetween((n % 100), 10, 19) || - isBetween((n % 100), 70, 79) || - isBetween((n % 100), 90, 99) - )) { - return 'few'; - } - if ((n % 1000000) === 0 && n !== 0) { - return 'many'; - } - if ((n % 10) === 2 && !isIn((n % 100), [12, 72, 92])) { - return 'two'; - } - if ((n % 10) === 1 && !isIn((n % 100), [11, 71, 91])) { - return 'one'; - } - return 'other'; - }, - '21': function(n) { - if (n === 0) { - return 'zero'; - } - if (n === 1) { - return 'one'; - } - return 'other'; - }, - '22': function(n) { - if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99))) { - return 'one'; - } - return 'other'; - }, - '23': function(n) { - if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0) { - return 'one'; - } - return 'other'; - }, - '24': function(n) { - if ((isBetween(n, 3, 10) || isBetween(n, 13, 19))) { - return 'few'; - } - if (isIn(n, [2, 12])) { - return 'two'; - } - if (isIn(n, [1, 11])) { - return 'one'; - } - return 'other'; - } - }; - - // return a function that gives the plural form name for a given integer - var index = locales2rules[lang.replace(/-.*$/, '')]; - if (!(index in pluralRules)) { - return function() { return 'other'; }; - } - return pluralRules[index]; - } - - - - - var nestedProps = ['style', 'dataset']; - - var parsePatterns; - - function parse(ctx, source) { - var ast = {}; - - if (!parsePatterns) { - parsePatterns = { - comment: /^\s*#|^\s*$/, - entity: /^([^=\s]+)\s*=\s*(.+)$/, - multiline: /[^\\]\\$/, - macro: /\{\[\s*(\w+)\(([^\)]*)\)\s*\]\}/i, - unicode: /\\u([0-9a-fA-F]{1,4})/g, - entries: /[\r\n]+/, - controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g - }; - } - - var entries = source.split(parsePatterns.entries); - for (var i = 0; i < entries.length; i++) { - var line = entries[i]; - - if (parsePatterns.comment.test(line)) { - continue; - } - - while (parsePatterns.multiline.test(line) && i < entries.length) { - line = line.slice(0, -1) + entries[++i].trim(); - } - - var entityMatch = line.match(parsePatterns.entity); - if (entityMatch) { - try { - parseEntity(entityMatch[1], entityMatch[2], ast); - } catch (e) { - if (ctx) { - ctx._emitter.emit('error', e); - } else { - throw e; - } - } - } - } - return ast; - } - - function setEntityValue(id, attr, key, value, ast) { - var obj = ast; - var prop = id; - - if (attr) { - if (!(id in obj)) { - obj[id] = {}; - } - if (typeof(obj[id]) === 'string') { - obj[id] = {'_': obj[id]}; - } - obj = obj[id]; - prop = attr; - } - - if (!key) { - obj[prop] = value; - return; - } - - if (!(prop in obj)) { - obj[prop] = {'_': {}}; - } else if (typeof(obj[prop]) === 'string') { - obj[prop] = {'_index': parseMacro(obj[prop]), '_': {}}; - } - obj[prop]._[key] = value; - } - - function parseEntity(id, value, ast) { - var name, key; - - var pos = id.indexOf('['); - if (pos !== -1) { - name = id.substr(0, pos); - key = id.substring(pos + 1, id.length - 1); - } else { - name = id; - key = null; - } - - var nameElements = name.split('.'); - - var attr; - if (nameElements.length > 1) { - var attrElements = []; - attrElements.push(nameElements.pop()); - if (nameElements.length > 1) { - // Usually the last dot separates an attribute from an id - // - // In case when there are more than one dot in the id - // and the second to last item is "style" or "dataset" then the last two - // items are becoming the attribute. - // - // ex. - // id.style.color = foo => - // - // id: - // style.color: foo - // - // id.other.color = foo => - // - // id.other: - // color: foo - if (nestedProps.indexOf(nameElements[nameElements.length - 1]) !== -1) { - attrElements.push(nameElements.pop()); - } - } - name = nameElements.join('.'); - attr = attrElements.reverse().join('.'); - } else { - attr = null; - } - - setEntityValue(name, attr, key, unescapeString(value), ast); - } - - function unescapeControlCharacters(str) { - return str.replace(parsePatterns.controlChars, '$1'); - } - - function unescapeUnicode(str) { - return str.replace(parsePatterns.unicode, function(match, token) { - return unescape('%u' + '0000'.slice(token.length) + token); - }); - } - - function unescapeString(str) { - if (str.lastIndexOf('\\') !== -1) { - str = unescapeControlCharacters(str); - } - return unescapeUnicode(str); - } - - function parseMacro(str) { - var match = str.match(parsePatterns.macro); - if (!match) { - throw new L10nError('Malformed macro'); - } - return [match[1], match[2]]; - } - - - - var MAX_PLACEABLE_LENGTH = 2500; - var MAX_PLACEABLES = 100; - var rePlaceables = /\{\{\s*(.+?)\s*\}\}/g; - - function Entity(id, node, env) { - this.id = id; - this.env = env; - // the dirty guard prevents cyclic or recursive references from other - // Entities; see Entity.prototype.resolve - this.dirty = false; - if (typeof node === 'string') { - this.value = node; - } else { - // it's either a hash or it has attrs, or both - for (var key in node) { - if (node.hasOwnProperty(key) && key[0] !== '_') { - if (!this.attributes) { - this.attributes = {}; - } - this.attributes[key] = new Entity(this.id + '.' + key, node[key], - env); - } - } - this.value = node._ || null; - this.index = node._index; - } - } - - Entity.prototype.resolve = function E_resolve(ctxdata) { - if (this.dirty) { - return undefined; - } - - this.dirty = true; - var val; - // if resolve fails, we want the exception to bubble up and stop the whole - // resolving process; however, we still need to clean up the dirty flag - try { - val = resolve(ctxdata, this.env, this.value, this.index); - } finally { - this.dirty = false; - } - return val; - }; - - Entity.prototype.toString = function E_toString(ctxdata) { - try { - return this.resolve(ctxdata); - } catch (e) { - return undefined; - } - }; - - Entity.prototype.valueOf = function E_valueOf(ctxdata) { - if (!this.attributes) { - return this.toString(ctxdata); - } - - var entity = { - value: this.toString(ctxdata), - attributes: {} - }; - - for (var key in this.attributes) { - if (this.attributes.hasOwnProperty(key)) { - entity.attributes[key] = this.attributes[key].toString(ctxdata); - } - } - - return entity; - }; - - function subPlaceable(ctxdata, env, match, id) { - if (ctxdata && ctxdata.hasOwnProperty(id) && - (typeof ctxdata[id] === 'string' || - (typeof ctxdata[id] === 'number' && !isNaN(ctxdata[id])))) { - return ctxdata[id]; - } - - if (env.hasOwnProperty(id)) { - if (!(env[id] instanceof Entity)) { - env[id] = new Entity(id, env[id], env); - } - var value = env[id].resolve(ctxdata); - if (typeof value === 'string') { - // prevent Billion Laughs attacks - if (value.length >= MAX_PLACEABLE_LENGTH) { - throw new L10nError('Too many characters in placeable (' + - value.length + ', max allowed is ' + - MAX_PLACEABLE_LENGTH + ')'); - } - return value; - } - } - return match; - } - - function interpolate(ctxdata, env, str) { - var placeablesCount = 0; - var value = str.replace(rePlaceables, function(match, id) { - // prevent Quadratic Blowup attacks - if (placeablesCount++ >= MAX_PLACEABLES) { - throw new L10nError('Too many placeables (' + placeablesCount + - ', max allowed is ' + MAX_PLACEABLES + ')'); - } - return subPlaceable(ctxdata, env, match, id); - }); - placeablesCount = 0; - return value; - } - - function resolve(ctxdata, env, expr, index) { - if (typeof expr === 'string') { - return interpolate(ctxdata, env, expr); - } - - if (typeof expr === 'boolean' || - typeof expr === 'number' || - !expr) { - return expr; - } - - // otherwise, it's a dict - - if (index && ctxdata && ctxdata.hasOwnProperty(index[1])) { - var argValue = ctxdata[index[1]]; - - // special cases for zero, one, two if they are defined on the hash - if (argValue === 0 && 'zero' in expr) { - return resolve(ctxdata, env, expr.zero); - } - if (argValue === 1 && 'one' in expr) { - return resolve(ctxdata, env, expr.one); - } - if (argValue === 2 && 'two' in expr) { - return resolve(ctxdata, env, expr.two); - } - - var selector = env.__plural(argValue); - if (expr.hasOwnProperty(selector)) { - return resolve(ctxdata, env, expr[selector]); - } - } - - // if there was no index or no selector was found, try 'other' - if ('other' in expr) { - return resolve(ctxdata, env, expr.other); - } - - return undefined; - } - - function compile(env, ast) { - env = env || {}; - for (var id in ast) { - if (ast.hasOwnProperty(id)) { - env[id] = new Entity(id, ast[id], env); - } - } - return env; - } - - - - function Locale(id, ctx) { - this.id = id; - this.ctx = ctx; - this.isReady = false; - this.entries = { - __plural: getPluralRule(id) - }; - } - - Locale.prototype.getEntry = function L_getEntry(id) { - /* jshint -W093 */ - - var entries = this.entries; - - if (!entries.hasOwnProperty(id)) { - return undefined; - } - - if (entries[id] instanceof Entity) { - return entries[id]; - } - - return entries[id] = new Entity(id, entries[id], entries); - }; - - Locale.prototype.build = function L_build(callback) { - var sync = !callback; - var ctx = this.ctx; - var self = this; - - var l10nLoads = ctx.resLinks.length; - - function onL10nLoaded(err) { - if (err) { - ctx._emitter.emit('error', err); - } - if (--l10nLoads <= 0) { - self.isReady = true; - if (callback) { - callback(); - } - } - } - - if (l10nLoads === 0) { - onL10nLoaded(); - return; - } - - function onJSONLoaded(err, json) { - if (!err && json) { - self.addAST(json); - } - onL10nLoaded(err); - } - - function onPropLoaded(err, source) { - if (!err && source) { - var ast = parse(ctx, source); - self.addAST(ast); - } - onL10nLoaded(err); - } - - - for (var i = 0; i < ctx.resLinks.length; i++) { - var path = ctx.resLinks[i].replace('{{locale}}', this.id); - var type = path.substr(path.lastIndexOf('.') + 1); - - switch (type) { - case 'json': - io.loadJSON(path, onJSONLoaded, sync); - break; - case 'properties': - io.load(path, onPropLoaded, sync); - break; - } - } - }; - - Locale.prototype.addAST = function(ast) { - for (var id in ast) { - if (ast.hasOwnProperty(id)) { - this.entries[id] = ast[id]; - } - } - }; - - Locale.prototype.getEntity = function(id, ctxdata) { - var entry = this.getEntry(id); - - if (!entry) { - return null; - } - return entry.valueOf(ctxdata); - }; - - - - function Context(id) { - - this.id = id; - this.isReady = false; - this.isLoading = false; - - this.supportedLocales = []; - this.resLinks = []; - this.locales = {}; - - this._emitter = new EventEmitter(); - - - // Getting translations - - function getWithFallback(id) { - /* jshint -W084 */ - - if (!this.isReady) { - throw new L10nError('Context not ready'); - } - - var cur = 0; - var loc; - var locale; - while (loc = this.supportedLocales[cur]) { - locale = this.getLocale(loc); - if (!locale.isReady) { - // build without callback, synchronously - locale.build(null); - } - var entry = locale.getEntry(id); - if (entry === undefined) { - cur++; - warning.call(this, new L10nError(id + ' not found in ' + loc, id, - loc)); - continue; - } - return entry; - } - - error.call(this, new L10nError(id + ' not found', id)); - return null; - } - - this.get = function get(id, ctxdata) { - var entry = getWithFallback.call(this, id); - if (entry === null) { - return ''; - } - - return entry.toString(ctxdata) || ''; - }; - - this.getEntity = function getEntity(id, ctxdata) { - var entry = getWithFallback.call(this, id); - if (entry === null) { - return null; - } - - return entry.valueOf(ctxdata); - }; - - - // Helpers - - this.getLocale = function getLocale(code) { - /* jshint -W093 */ - - var locales = this.locales; - if (locales[code]) { - return locales[code]; - } - - return locales[code] = new Locale(code, this); - }; - - - // Getting ready - - function negotiate(available, requested, defaultLocale) { - if (available.indexOf(requested[0]) === -1 || - requested[0] === defaultLocale) { - return [defaultLocale]; - } else { - return [requested[0], defaultLocale]; - } - } - - function freeze(supported) { - var locale = this.getLocale(supported[0]); - if (locale.isReady) { - setReady.call(this, supported); - } else { - locale.build(setReady.bind(this, supported)); - } - } - - function setReady(supported) { - this.supportedLocales = supported; - this.isReady = true; - this._emitter.emit('ready'); - } - - this.requestLocales = function requestLocales() { - if (this.isLoading && !this.isReady) { - throw new L10nError('Context not ready'); - } - - this.isLoading = true; - var requested = Array.prototype.slice.call(arguments); - - var supported = negotiate(requested.concat('en-US'), requested, 'en-US'); - freeze.call(this, supported); - }; - - - // Events - - this.addEventListener = function addEventListener(type, listener) { - this._emitter.addEventListener(type, listener); - }; - - this.removeEventListener = function removeEventListener(type, listener) { - this._emitter.removeEventListener(type, listener); - }; - - this.ready = function ready(callback) { - if (this.isReady) { - setTimeout(callback); - } - this.addEventListener('ready', callback); - }; - - this.once = function once(callback) { - /* jshint -W068 */ - if (this.isReady) { - setTimeout(callback); - return; - } - - var callAndRemove = (function() { - this.removeEventListener('ready', callAndRemove); - callback(); - }).bind(this); - this.addEventListener('ready', callAndRemove); - }; - - - // Errors - - function warning(e) { - this._emitter.emit('warning', e); - return e; - } - - function error(e) { - this._emitter.emit('error', e); - return e; - } - } - - - /* jshint -W104 */ - - var DEBUG = false; - var isPretranslated = false; - var rtlList = ['ar', 'he', 'fa', 'ps', 'qps-plocm', 'ur']; - - // Public API - - navigator.mozL10n = { - ctx: new Context(), - get: function get(id, ctxdata) { - return navigator.mozL10n.ctx.get(id, ctxdata); - }, - localize: function localize(element, id, args) { - return localizeElement.call(navigator.mozL10n, element, id, args); - }, - translate: function translate(element) { - return translateFragment.call(navigator.mozL10n, element); - }, - ready: function ready(callback) { - return navigator.mozL10n.ctx.ready(callback); - }, - once: function once(callback) { - return navigator.mozL10n.ctx.once(callback); - }, - get readyState() { - return navigator.mozL10n.ctx.isReady ? 'complete' : 'loading'; - }, - language: { - set code(lang) { - navigator.mozL10n.ctx.requestLocales(lang); - }, - get code() { - return navigator.mozL10n.ctx.supportedLocales[0]; - }, - get direction() { - return getDirection(navigator.mozL10n.ctx.supportedLocales[0]); - } - }, - _getInternalAPI: function() { - return { - Error: L10nError, - Context: Context, - Locale: Locale, - Entity: Entity, - getPluralRule: getPluralRule, - rePlaceables: rePlaceables, - getTranslatableChildren: getTranslatableChildren, - getL10nAttributes: getL10nAttributes, - loadINI: loadINI, - fireLocalizedEvent: fireLocalizedEvent, - parse: parse, - compile: compile - }; - } - }; - - navigator.mozL10n.ctx.ready(onReady.bind(navigator.mozL10n)); - - if (DEBUG) { - navigator.mozL10n.ctx.addEventListener('error', console.error); - navigator.mozL10n.ctx.addEventListener('warning', console.warn); - } - - function getDirection(lang) { - return (rtlList.indexOf(lang) >= 0) ? 'rtl' : 'ltr'; - } - - var readyStates = { - 'loading': 0, - 'interactive': 1, - 'complete': 2 - }; - - function waitFor(state, callback) { - state = readyStates[state]; - if (readyStates[document.readyState] >= state) { - callback(); - return; - } - - document.addEventListener('readystatechange', function l10n_onrsc() { - if (readyStates[document.readyState] >= state) { - document.removeEventListener('readystatechange', l10n_onrsc); - callback(); - } - }); - } - - if (window.document) { - isPretranslated = (document.documentElement.lang === navigator.language); - - // this is a special case for netError bug; see https://bugzil.la/444165 - if (document.documentElement.dataset.noCompleteBug) { - pretranslate.call(navigator.mozL10n); - return; - } - - - if (isPretranslated) { - waitFor('interactive', function() { - window.setTimeout(initResources.bind(navigator.mozL10n)); - }); - } else { - if (document.readyState === 'complete') { - window.setTimeout(initResources.bind(navigator.mozL10n)); - } else { - waitFor('interactive', pretranslate.bind(navigator.mozL10n)); - } - } - - } - - function pretranslate() { - /* jshint -W068 */ - if (inlineLocalization.call(this)) { - waitFor('interactive', (function() { - window.setTimeout(initResources.bind(this)); - }).bind(this)); - } else { - initResources.call(this); - } - } - - function inlineLocalization() { - var script = document.documentElement - .querySelector('script[type="application/l10n"]' + - '[lang="' + navigator.language + '"]'); - if (!script) { - return false; - } - - var locale = this.ctx.getLocale(navigator.language); - // the inline localization is happenning very early, when the ctx is not - // yet ready and when the resources haven't been downloaded yet; add the - // inlined JSON directly to the current locale - locale.addAST(JSON.parse(script.innerHTML)); - // localize the visible DOM - var l10n = { - ctx: locale, - language: { - code: locale.id, - direction: getDirection(locale.id) - } - }; - translateFragment.call(l10n); - // the visible DOM is now pretranslated - isPretranslated = true; - return true; - } - - function initResources() { - var resLinks = document.head - .querySelectorAll('link[type="application/l10n"]'); - var iniLinks = []; - var i; - - for (i = 0; i < resLinks.length; i++) { - var link = resLinks[i]; - var url = link.getAttribute('href'); - var type = url.substr(url.lastIndexOf('.') + 1); - if (type === 'ini') { - iniLinks.push(url); - } - this.ctx.resLinks.push(url); - } - - var iniLoads = iniLinks.length; - if (iniLoads === 0) { - initLocale.call(this); - return; - } - - function onIniLoaded(err) { - if (err) { - this.ctx._emitter.emit('error', err); - } - if (--iniLoads === 0) { - initLocale.call(this); - } - } - - for (i = 0; i < iniLinks.length; i++) { - loadINI.call(this, iniLinks[i], onIniLoaded.bind(this)); - } - } - - function initLocale() { - this.ctx.requestLocales(navigator.language); - window.addEventListener('languagechange', function l10n_langchange() { - navigator.mozL10n.language.code = navigator.language; - }); - } - - function onReady() { - if (!isPretranslated) { - this.translate(); - } - isPretranslated = false; - - fireLocalizedEvent.call(this); - } - - function fireLocalizedEvent() { - var event = new CustomEvent('localized', { - 'bubbles': false, - 'cancelable': false, - 'detail': { - 'language': this.ctx.supportedLocales[0] - } - }); - window.dispatchEvent(event); - } - - /* jshint -W104 */ - - function loadINI(url, callback) { - var ctx = this.ctx; - io.load(url, function(err, source) { - var pos = ctx.resLinks.indexOf(url); - - if (err) { - // remove the ini link from resLinks - ctx.resLinks.splice(pos, 1); - return callback(err); - } - - if (!source) { - ctx.resLinks.splice(pos, 1); - return callback(new Error('Empty file: ' + url)); - } - - var patterns = parseINI(source, url).resources.map(function(x) { - return x.replace('en-US', '{{locale}}'); - }); - ctx.resLinks.splice.apply(ctx.resLinks, [pos, 1].concat(patterns)); - callback(); - }); - } - - function relativePath(baseUrl, url) { - if (url[0] === '/') { - return url; - } - - var dirs = baseUrl.split('/') - .slice(0, -1) - .concat(url.split('/')) - .filter(function(path) { - return path !== '.'; - }); - - return dirs.join('/'); - } - - var iniPatterns = { - 'section': /^\s*\[(.*)\]\s*$/, - 'import': /^\s*@import\s+url\((.*)\)\s*$/i, - 'entry': /[\r\n]+/ - }; - - function parseINI(source, iniPath) { - var entries = source.split(iniPatterns.entry); - var locales = ['en-US']; - var genericSection = true; - var uris = []; - var match; - - for (var i = 0; i < entries.length; i++) { - var line = entries[i]; - // we only care about en-US resources - if (genericSection && iniPatterns['import'].test(line)) { - match = iniPatterns['import'].exec(line); - var uri = relativePath(iniPath, match[1]); - uris.push(uri); - continue; - } - - // but we need the list of all locales in the ini, too - if (iniPatterns.section.test(line)) { - genericSection = false; - match = iniPatterns.section.exec(line); - locales.push(match[1]); - } - } - return { - locales: locales, - resources: uris - }; - } - - /* jshint -W104 */ - - function translateFragment(element) { - if (!element) { - element = document.documentElement; - document.documentElement.lang = this.language.code; - document.documentElement.dir = this.language.direction; - } - translateElement.call(this, element); - - var nodes = getTranslatableChildren(element); - for (var i = 0; i < nodes.length; i++ ) { - translateElement.call(this, nodes[i]); - } - } - - function getTranslatableChildren(element) { - return element ? element.querySelectorAll('*[data-l10n-id]') : []; - } - - function localizeElement(element, id, args) { - if (!element) { - return; - } - - if (!id) { - element.removeAttribute('data-l10n-id'); - element.removeAttribute('data-l10n-args'); - setTextContent(element, ''); - return; - } - - element.setAttribute('data-l10n-id', id); - if (args && typeof args === 'object') { - element.setAttribute('data-l10n-args', JSON.stringify(args)); - } else { - element.removeAttribute('data-l10n-args'); - } - - if (this.ctx.isReady) { - translateElement.call(this, element); - } - } - - function getL10nAttributes(element) { - if (!element) { - return {}; - } - - var l10nId = element.getAttribute('data-l10n-id'); - var l10nArgs = element.getAttribute('data-l10n-args'); - - var args = l10nArgs ? JSON.parse(l10nArgs) : null; - - return {id: l10nId, args: args}; - } - - - - function translateElement(element) { - var l10n = getL10nAttributes(element); - - if (!l10n.id) { - return; - } - - var entity = this.ctx.getEntity(l10n.id, l10n.args); - - if (!entity) { - return; - } - - if (typeof entity === 'string') { - setTextContent(element, entity); - return true; - } - - if (entity.value) { - setTextContent(element, entity.value); - } - - for (var key in entity.attributes) { - if (entity.attributes.hasOwnProperty(key)) { - var attr = entity.attributes[key]; - var pos = key.indexOf('.'); - if (pos !== -1) { - element[key.substr(0, pos)][key.substr(pos + 1)] = attr; - } else if (key === 'ariaLabel') { - element.setAttribute('aria-label', attr); - } else { - element[key] = attr; - } - } - } - - return true; - } - - function setTextContent(element, text) { - // standard case: no element children - if (!element.firstElementChild) { - element.textContent = text; - return; - } - - // this element has element children: replace the content of the first - // (non-blank) child textNode and clear other child textNodes - var found = false; - var reNotBlank = /\S/; - for (var child = element.firstChild; child; child = child.nextSibling) { - if (child.nodeType === Node.TEXT_NODE && - reNotBlank.test(child.nodeValue)) { - if (found) { - child.nodeValue = ''; - } else { - child.nodeValue = text; - found = true; - } - } - } - // if no (non-empty) textNode is found, insert a textNode before the - // element's first child. - if (!found) { - element.insertBefore(document.createTextNode(text), element.firstChild); - } - } - -})(this); - diff --git a/build/locales/en-US.properties b/build/locales/en-US.properties deleted file mode 100644 index 26f9a84..0000000 --- a/build/locales/en-US.properties +++ /dev/null @@ -1,3 +0,0 @@ -appname = Hawk -app_description.innerHTML = This app is empty. Fill it with your own stuff! -message = Hello world diff --git a/build/locales/locales.ini b/build/locales/locales.ini deleted file mode 100644 index 173dfb0..0000000 --- a/build/locales/locales.ini +++ /dev/null @@ -1 +0,0 @@ -@import url(en-US.properties) diff --git a/build/main.js b/build/main.js index 5ffd63a..a2c30ea 100644 --- a/build/main.js +++ b/build/main.js @@ -29964,16 +29964,18 @@ var children = _asyncToGenerator(function* (dir, gatherInfo) { for (var _iterator = childs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var child = _step.value; - if ((0, _utils.type)(child) !== 'Directory') continue; + if ((0, _utils.type)(child) === 'Directory') { + var subchildren = undefined; + try { + subchildren = yield child.getFilesAndDirectories(); + } catch (e) { + subchildren = []; + } - var subchildren = undefined; - try { - subchildren = yield child.getFilesAndDirectories(); - } catch (e) { - subchildren = []; + child.children = subchildren.length; + } else { + child.path = dir + '/'; } - - child.children = subchildren.length; } } catch (err) { _didIteratorError = true; @@ -29989,6 +29991,8 @@ var children = _asyncToGenerator(function* (dir, gatherInfo) { } } } + + ; } return childs; @@ -30073,7 +30077,7 @@ var copy = _asyncToGenerator(function* (file, newPath) { child.path = oldPath + '/'; } - yield move(child, newPath + '/' + child.name); + yield copy(child, newPath + '/' + child.name); } } catch (err) { _didIteratorError2 = true; @@ -30354,12 +30358,6 @@ var _actionsChangedir = require('actions/changedir'); var _actionsChangedir2 = _interopRequireDefault(_actionsChangedir); -var _actionsMenu = require('actions/menu'); - -var _actionsFile = require('actions/file'); - -var _menu = require('./menu'); - var _store = require('store'); var _store2 = _interopRequireDefault(_store); @@ -30368,8 +30366,6 @@ var _mixinsEntry = require('./mixins/entry'); var _mixinsEntry2 = _interopRequireDefault(_mixinsEntry); -var MENU_TOP_SPACE = 20; - var Directory = (function (_Component) { _inherits(Directory, _Component); @@ -30430,7 +30426,7 @@ var Directory = (function (_Component) { exports['default'] = Directory; module.exports = exports['default']; -},{"./menu":233,"./mixins/entry":234,"actions/changedir":217,"actions/file":219,"actions/menu":221,"react":207,"store":"store"}],230:[function(require,module,exports){ +},{"./mixins/entry":234,"actions/changedir":217,"react":207,"store":"store"}],230:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -30505,7 +30501,7 @@ var FileList = (function (_Component) { var settings = _store2['default'].getState().get('settings'); var els = files.map(function (file, index) { - var selected = activeFile.length && activeFile.indexOf(file) > -1; + var selected = activeFile.indexOf(file) > -1; if ((0, _utils.type)(file) === 'File') { return _react2['default'].createElement(_file2['default'], { selectView: selectView, selected: selected, key: index, index: index, name: file.name, size: file.size }); } else { @@ -30574,12 +30570,6 @@ var _react = require('react'); var _react2 = _interopRequireDefault(_react); -var _actionsMenu = require('actions/menu'); - -var _actionsFile = require('actions/file'); - -var _menu = require('./menu'); - var _store = require('store'); var _store2 = _interopRequireDefault(_store); @@ -30590,8 +30580,6 @@ var _mixinsEntry = require('./mixins/entry'); var _mixinsEntry2 = _interopRequireDefault(_mixinsEntry); -var MENU_TOP_SPACE = 20; - var File = (function (_Component) { _inherits(File, _Component); @@ -30610,7 +30598,7 @@ var File = (function (_Component) { var input = undefined, label = undefined; if (this.props.selectView) { - input = _react2['default'].createElement('input', { type: 'checkbox', id: checkId, defaultChecked: this.props.selected, readOnly: true }); + input = _react2['default'].createElement('input', { type: 'checkbox', id: checkId, checked: this.props.selected, readOnly: true }); label = _react2['default'].createElement('label', { htmlFor: checkId }); } @@ -30658,7 +30646,7 @@ var File = (function (_Component) { exports['default'] = File; module.exports = exports['default']; -},{"./menu":233,"./mixins/entry":234,"actions/file":219,"actions/menu":221,"react":207,"store":"store","utils":"utils"}],232:[function(require,module,exports){ +},{"./mixins/entry":234,"react":207,"store":"store","utils":"utils"}],232:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -30780,6 +30768,7 @@ var Menu = (function (_Component) { var items = _props.items; var active = _props.active; var style = _props.style; + var id = _props.id; items = items || []; @@ -30797,7 +30786,7 @@ var Menu = (function (_Component) { return _react2['default'].createElement( 'div', - { className: className, style: style }, + { className: className, style: style, id: id }, _react2['default'].createElement( 'ul', null, @@ -30818,25 +30807,48 @@ exports['default'] = Menu; Object.defineProperty(exports, '__esModule', { value: true }); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _react = require('react'); + +var _react2 = _interopRequireDefault(_react); + +var _actionsFile = require('actions/file'); + +var _actionsMenu = require('actions/menu'); + +var _componentsMenu = require('components/menu'); + +var MENU_TOP_SPACE = 20; + exports['default'] = { contextMenu: function contextMenu(e) { e.preventDefault(); var file = store.getState().get('files')[this.props.index]; - var rect = React.findDOMNode(this.refs.container).getBoundingClientRect(); + var rect = _react2['default'].findDOMNode(this.refs.container).getBoundingClientRect(); var x = rect.x; var y = rect.y; var width = rect.width; var height = rect.height; - var left = x + width / 2 - MENU_WIDTH / 2, + var left = x + width / 2 - _componentsMenu.MENU_WIDTH / 2, top = y + height / 2 + MENU_TOP_SPACE; - store.dispatch(show('fileMenu', { style: { left: left, top: top } })); - store.dispatch(active([file])); + + var dialogHeight = document.getElementById('fileMenu').offsetHeight; + + var diff = window.innerHeight - (dialogHeight + top); + if (diff <= 0) { + top -= Math.abs(diff); + } + + store.dispatch((0, _actionsMenu.show)('fileMenu', { style: { left: left, top: top } })); + store.dispatch((0, _actionsFile.active)([file])); }, select: function select() { - var current = store.getState().get('activeFile').slice(0); + var current = (store.getState().get('activeFile') || []).slice(0); var file = store.getState().get('files')[this.props.index]; if (current.indexOf(file) > -1) { @@ -30844,12 +30856,12 @@ exports['default'] = { } else { current.push(file); } - store.dispatch(active(current)); + store.dispatch((0, _actionsFile.active)(current)); } }; module.exports = exports['default']; -},{}],235:[function(require,module,exports){ +},{"actions/file":219,"actions/menu":221,"components/menu":233,"react":207}],235:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -30917,7 +30929,7 @@ var Navigation = (function (_Component) { _react2['default'].createElement( 'li', null, - _react2['default'].createElement('input', { id: 'filter-all', name: 'filter', value: '', type: 'radio', defaultChecked: !settings.filter }), + _react2['default'].createElement('input', { id: 'filter-all', name: 'filter', 'data-value': '', type: 'radio', defaultChecked: !settings.filter }), _react2['default'].createElement( 'label', { htmlFor: 'filter-all' }, @@ -30927,7 +30939,7 @@ var Navigation = (function (_Component) { _react2['default'].createElement( 'li', null, - _react2['default'].createElement('input', { id: 'filter-image', name: 'filter', value: 'image', type: 'radio', defaultChecked: settings.filter === 'image' }), + _react2['default'].createElement('input', { id: 'filter-image', name: 'filter', 'data-value': 'image', type: 'radio', defaultChecked: settings.filter === 'image' }), _react2['default'].createElement( 'label', { htmlFor: 'filter-image' }, @@ -30937,7 +30949,7 @@ var Navigation = (function (_Component) { _react2['default'].createElement( 'li', null, - _react2['default'].createElement('input', { id: 'filter-video', name: 'filter', value: 'video', type: 'radio', defaultChecked: settings.filter === 'video' }), + _react2['default'].createElement('input', { id: 'filter-video', name: 'filter', 'data-value': 'video', type: 'radio', defaultChecked: settings.filter === 'video' }), _react2['default'].createElement( 'label', { htmlFor: 'filter-video' }, @@ -30947,7 +30959,7 @@ var Navigation = (function (_Component) { _react2['default'].createElement( 'li', null, - _react2['default'].createElement('input', { id: 'filter-audio', name: 'filter', value: 'audio', type: 'radio', defaultChecked: settings.filter === 'audio' }), + _react2['default'].createElement('input', { id: 'filter-audio', name: 'filter', 'data-value': 'audio', type: 'radio', defaultChecked: settings.filter === 'audio' }), _react2['default'].createElement( 'label', { htmlFor: 'filter-audio' }, @@ -31078,7 +31090,7 @@ var Navigation = (function (_Component) { key: 'onChange', value: function onChange(e) { var key = e.target.name || e.target.id; - var value = e.target.value === undefined ? e.target.checked : e.target.value; + var value = typeof e.target.dataset.value !== 'undefined' ? e.target.dataset.value : e.target.checked; var action = (0, _actionsSettings2['default'])(_defineProperty({}, key, value)); @@ -31186,9 +31198,7 @@ window.changedir = _actionsChangedir2['default']; var FileMenu = (0, _reactRedux.connect)(function (state) { return state.get('fileMenu'); })(_componentsMenu2['default']); -var DirectoryMenu = (0, _reactRedux.connect)(function (state) { - return state.get('directoryMenu'); -})(_componentsMenu2['default']); +// let DirectoryMenu = connect(state => state.get('directoryMenu'))(Menu); var MoreMenu = (0, _reactRedux.connect)(function (state) { return state.get('moreMenu'); })(_componentsMenu2['default']); @@ -31223,21 +31233,22 @@ var Root = (function (_Component) { value: function render() { return _react2['default'].createElement( 'div', - { onTouchStart: this.touchStart.bind(this), onClick: this.onClick.bind(this) }, + { onTouchStart: this.touchStart.bind(this), + onClick: this.onClick.bind(this) }, _react2['default'].createElement(_componentsHeader2['default'], null), _react2['default'].createElement(_componentsBreadcrumb2['default'], null), _react2['default'].createElement(_componentsNavigation2['default'], null), _react2['default'].createElement(_componentsFileList2['default'], null), _react2['default'].createElement(_componentsToolbar2['default'], null), - _react2['default'].createElement(FileMenu, null), - _react2['default'].createElement(DirectoryMenu, null), - _react2['default'].createElement(MoreMenu, null), + _react2['default'].createElement(FileMenu, { id: 'fileMenu' }), + _react2['default'].createElement(MoreMenu, { id: 'moreMenu' }), _react2['default'].createElement(RenameDialog, null), _react2['default'].createElement(DeleteDialog, null), _react2['default'].createElement(ErrorDialog, null), _react2['default'].createElement(CreateDialog, null), _react2['default'].createElement(SearchDialog, null), _react2['default'].createElement(_componentsSpinner2['default'], null), + _react2['default'].createElement('div', { className: 'swipe-instruction tour-item' }), _react2['default'].createElement( 'div', { className: 'tour-dialog' }, @@ -31621,7 +31632,7 @@ var entryMenu = { action: function action() { var files = _store2['default'].getState().get('files'); var active = _store2['default'].getState().get('activeFile'); - var description = 'Enter the new name for ' + active[0].name + '?'; + var description = 'Enter the new name for ' + active[0].name; _store2['default'].dispatch((0, _actionsMenu.hideAll)()); _store2['default'].dispatch((0, _actionsDialog.show)('renameDialog', { description: description })); @@ -31973,7 +31984,8 @@ exports['default'] = function (state, action) { if (action.type === _actionsTypes.RENAME_FILE) { var all = Promise.all(action.file.map(function (file) { - return (0, _apiFiles.move)(file, (file.path || '') + action.name); + var cwd = _store2['default'].getState().get('cwd'); + return (0, _apiFiles.move)(file, cwd + '/' + action.name); })); all.then(boundRefresh, _utils.reportError); @@ -32214,7 +32226,11 @@ exports['default'] = function (state, action) { if (state === undefined) state = DEFAULT; if (action.type === _actionsTypes.SETTINGS) { - return Object.assign({}, state, (0, _lodashObjectOmit2['default'])(action, 'type')); + var newSettings = Object.assign({}, state, (0, _lodashObjectOmit2['default'])(action, 'type')); + + localStorage.setItem('settings', JSON.stringify(newSettings)); + + return newSettings; } return state; @@ -32245,6 +32261,13 @@ exports['default'] = function (state, action) { switch (action.type) { case _actionsTypes.CHANGE_DIRECTORY: case _actionsTypes.REFRESH: + case _actionsTypes.SETTINGS: + case _actionsTypes.CREATE_FILE: + case _actionsTypes.MOVE_FILE: + case _actionsTypes.DELETE_FILE: + case _actionsTypes.RENAME_FILE: + case _actionsTypes.COPY_FILE: + case _actionsTypes.SEARCH: return true; case _actionsTypes.LIST_FILES: return false; @@ -32288,7 +32311,8 @@ var _dialogs = require('./dialogs'); var _dialogs2 = _interopRequireDefault(_dialogs); var DEFAULT = new _immutable2['default'].Map(Object.assign({ - dir: '' + dir: '', + settings: JSON.parse(localStorage.getItem('settings') || '{}') }, _dialogs2['default'], _menus2['default'])); var store = (0, _redux.createStore)(_reducersAll2['default'], DEFAULT); @@ -32318,7 +32342,8 @@ var MESSAGES = { 'icon-select': 'Select files for batch actions', 'icon-more': 'Actions used on selected files such as Copy, Delete, Move, …', 'drawer': 'Extra options, tools and links are here', - 'icon-search': 'Search your storage for a certain file' + 'icon-search': 'Search your storage for a certain file', + 'swipe-instruction': 'Swipe from left to right to go to parent folder' }; var DIALOG_HIDE_DELAY = 2000; diff --git a/build/manifest.webapp b/build/manifest.webapp index eb4152c..509526c 100644 --- a/build/manifest.webapp +++ b/build/manifest.webapp @@ -1,7 +1,7 @@ { "version": "0.1.0", "name": "Hawk", - "description": "Keep an eye on your files with a full-featured file-manager", + "description": "Keep an eye on your files with a full-featured file manager", "launch_path": "/index.html", "icons": { "16": "/icon/Icon-16.png", @@ -15,17 +15,26 @@ }, "type": "privileged", "permissions": { - "device-storage:sdcard": {"access": "readwrite"}, - "device-storage:videos": {"access": "readwrite"}, - "device-storage:pictures": {"access": "readwrite"}, - "device-storage:music": {"access": "readwrite"}, - "device-storage:apps": {"access": "readwrite"}, - "webapps-manage": {} + "device-storage:sdcard": { + "access": "readwrite", + "description": "We need access to your files in order to give you the functionality of listing, reading, opening and writing files in your storage" + }, + "device-storage:videos": { + "access": "readwrite", + "description": "We need access to your files in order to give you the functionality of listing, reading, opening and writing files in your storage" + }, + "device-storage:pictures": { + "access": "readwrite", + "description": "We need access to your files in order to give you the functionality of listing, reading, opening and writing files in your storage" + }, + "device-storage:music": { + "access": "readwrite", + "description": "We need access to your files in order to give you the functionality of listing, reading, opening and writing files in your storage" + } }, "installs_allowed_from": [ - "*" + "https://marketplace.firefox.com", + "https://marketplace-dev.allizom.org" ], - "locales": { - }, "default_locale": "en" } diff --git a/build/style.css b/build/style.css index e4ab0eb..0ae9d64 100644 --- a/build/style.css +++ b/build/style.css @@ -172,6 +172,28 @@ input:checked + label::after { transform: scale(0); animation: pulse 2s ease-out infinite; } +.tour .swipe-instruction { + position: fixed; + left: 50%; + top: 20%; + width: 70vw; + height: 5rem; + margin-left: -35vw; + z-index: 1; + background: white; + border-radius: 3rem; +} +.tour .swipe-instruction::before { + content: ''; + position: absolute; + left: 0; + top: 0; + width: 5rem; + height: 5rem; + background: #d9d9d9; + border-radius: 50%; + animation: swipe 3s ease infinite; +} .tour .tour-dialog { display: block; box-sizing: border-box; @@ -196,6 +218,24 @@ input:checked + label::after { transform: scale(5); } } +@keyframes swipe { + 80% { + left: calc(100% - 5rem); + opacity: 1; + } + 90% { + opacity: 0; + left: calc(100% - 5rem); + } + 91% { + left: 0; + opacity: 0; + } + 100% { + left: 0; + opacity: 1; + } +} .coming-soon::after { content: 'soon...'; background: #f7c59f; diff --git a/package.json b/package.json index 9e52d28..647f110 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "grunt-contrib-watch": "^0.6.1", "grunt-fxos": "^0.1.2", "grunt-task-loader": "^0.6.0", + "grunt-zip": "^0.17.0", "hammerjs": "^2.0.4", "immutable": "^3.7.5", "less-plugin-clean-css": "^1.5.1", diff --git a/releases/hawk-1.0.0.zip b/releases/hawk-1.0.0.zip index 9759571..95f237a 100644 Binary files a/releases/hawk-1.0.0.zip and b/releases/hawk-1.0.0.zip differ diff --git a/src/js/api/files.js b/src/js/api/files.js index ae5bc8c..5e0710c 100644 --- a/src/js/api/files.js +++ b/src/js/api/files.js @@ -32,17 +32,19 @@ export async function children(dir, gatherInfo) { if (gatherInfo) { for (let child of childs) { - if (type(child) !== 'Directory') continue; + if (type(child) === 'Directory') { + let subchildren; + try { + subchildren = await child.getFilesAndDirectories(); + } catch(e) { + subchildren = []; + } - let subchildren; - try { - subchildren = await child.getFilesAndDirectories(); - } catch(e) { - subchildren = []; + child.children = subchildren.length; + } else { + child.path = dir + '/'; } - - child.children = subchildren.length; - } + }; } return childs; @@ -108,7 +110,7 @@ export async function copy(file, newPath) { child.path = oldPath + '/'; } - await move(child, newPath + '/' + child.name); + await copy(child, newPath + '/' + child.name); } return; diff --git a/src/js/components/directory.js b/src/js/components/directory.js index c2fe9c0..046eca9 100644 --- a/src/js/components/directory.js +++ b/src/js/components/directory.js @@ -1,13 +1,8 @@ import React, { Component } from 'react'; import changedir from 'actions/changedir'; -import { show } from 'actions/menu'; -import { active } from 'actions/file'; -import { MENU_WIDTH } from './menu'; import store from 'store'; import entry from './mixins/entry'; -const MENU_TOP_SPACE = 20; - export default class Directory extends Component { constructor() { super(); diff --git a/src/js/components/file-list.js b/src/js/components/file-list.js index 0519b34..48369f5 100644 --- a/src/js/components/file-list.js +++ b/src/js/components/file-list.js @@ -19,7 +19,7 @@ export default class FileList extends Component { let settings = store.getState().get('settings'); let els = files.map((file, index) => { - let selected = activeFile.length && activeFile.indexOf(file) > -1; + let selected = activeFile.indexOf(file) > -1; if (type(file) === 'File') { return ; } else { diff --git a/src/js/components/file.js b/src/js/components/file.js index a0a7316..2b6b99a 100644 --- a/src/js/components/file.js +++ b/src/js/components/file.js @@ -1,13 +1,8 @@ import React, { Component } from 'react'; -import { show } from 'actions/menu'; -import { active } from 'actions/file'; -import { MENU_WIDTH } from './menu'; import store from 'store'; import { humanSize } from 'utils'; import entry from './mixins/entry'; -const MENU_TOP_SPACE = 20; - export default class File extends Component { constructor() { super(); @@ -19,7 +14,7 @@ export default class File extends Component { let input, label; if (this.props.selectView) { - input = ; + input = ; label = ; } diff --git a/src/js/components/menu.js b/src/js/components/menu.js index 88ba53e..0bdafd1 100644 --- a/src/js/components/menu.js +++ b/src/js/components/menu.js @@ -4,7 +4,7 @@ export const MENU_WIDTH = 245; export default class Menu extends Component { render() { - let { items, active, style } = this.props; + let { items, active, style, id } = this.props; items = items || []; let els = items.map((item, index) => { @@ -16,7 +16,7 @@ export default class Menu extends Component { let className = 'menu ' + (active ? 'active' : ''); return ( -
+
    {els}
); diff --git a/src/js/components/mixins/entry.js b/src/js/components/mixins/entry.js index b87c1bb..248f287 100644 --- a/src/js/components/mixins/entry.js +++ b/src/js/components/mixins/entry.js @@ -1,3 +1,10 @@ +import React from 'react'; +import { active } from 'actions/file'; +import { show } from 'actions/menu'; +import { MENU_WIDTH } from 'components/menu'; + +const MENU_TOP_SPACE = 20; + export default { contextMenu(e) { e.preventDefault(); @@ -8,12 +15,20 @@ export default { let left = x + width / 2 - MENU_WIDTH / 2, top = y + height / 2 + MENU_TOP_SPACE; + + let dialogHeight = document.getElementById('fileMenu').offsetHeight; + + let diff = window.innerHeight - (dialogHeight + top); + if (diff <= 0) { + top -= Math.abs(diff); + } + store.dispatch(show('fileMenu', {style: {left, top}})); store.dispatch(active([file])); }, select() { - let current = store.getState().get('activeFile').slice(0); + let current = (store.getState().get('activeFile') || []).slice(0); let file = store.getState().get('files')[this.props.index]; if (current.indexOf(file) > -1) { diff --git a/src/js/components/navigation.js b/src/js/components/navigation.js index a6aac97..f037509 100644 --- a/src/js/components/navigation.js +++ b/src/js/components/navigation.js @@ -17,19 +17,19 @@ export default class Navigation extends Component {

Filter

@@ -77,7 +77,7 @@ export default class Navigation extends Component { onChange(e) { let key = e.target.name || e.target.id; - let value = e.target.value === undefined ? e.target.checked : e.target.value; + let value = typeof e.target.dataset.value !== 'undefined' ? e.target.dataset.value : e.target.checked; let action = updateSettings({ [key]: value diff --git a/src/js/components/root.js b/src/js/components/root.js index 914e65e..c929b34 100644 --- a/src/js/components/root.js +++ b/src/js/components/root.js @@ -19,7 +19,7 @@ window.store = store; window.changedir = changedir; let FileMenu = connect(state => state.get('fileMenu'))(Menu); -let DirectoryMenu = connect(state => state.get('directoryMenu'))(Menu); +// let DirectoryMenu = connect(state => state.get('directoryMenu'))(Menu); let MoreMenu = connect(state => state.get('moreMenu'))(Menu); let RenameDialog = connect(state => state.get('renameDialog'))(Dialog); @@ -31,16 +31,16 @@ let SearchDialog = connect(state => state.get('searchDialog'))(Dialog); export default class Root extends Component { render() { return ( -
+
- - - + + @@ -50,6 +50,8 @@ export default class Root extends Component { +
+
Hello! Tap each highlighted button to get an understanding of how they work.
diff --git a/src/js/menus.js b/src/js/menus.js index a6b4231..fdc8620 100644 --- a/src/js/menus.js +++ b/src/js/menus.js @@ -11,7 +11,7 @@ const entryMenu = { action() { let files = store.getState().get('files'); let active = store.getState().get('activeFile'); - const description = `Enter the new name for ${active[0].name}?`; + const description = `Enter the new name for ${active[0].name}`; store.dispatch(hideAll()); store.dispatch(show('renameDialog', {description})); diff --git a/src/js/reducers/files.js b/src/js/reducers/files.js index 2bb4ff2..f076420 100644 --- a/src/js/reducers/files.js +++ b/src/js/reducers/files.js @@ -46,7 +46,8 @@ export default function(state = [], action) { if (action.type === RENAME_FILE) { let all = Promise.all(action.file.map(file => { - return move(file, (file.path || '') + action.name); + let cwd = store.getState().get('cwd'); + return move(file, cwd + '/' + action.name); })); all.then(boundRefresh, reportError); diff --git a/src/js/reducers/settings.js b/src/js/reducers/settings.js index f81160d..5c07144 100644 --- a/src/js/reducers/settings.js +++ b/src/js/reducers/settings.js @@ -8,7 +8,11 @@ const DEFAULT = { export default function(state = DEFAULT, action) { if (action.type === SETTINGS) { - return Object.assign({}, state, omit(action, 'type')); + let newSettings = Object.assign({}, state, omit(action, 'type')); + + localStorage.setItem('settings', JSON.stringify(newSettings)); + + return newSettings; } return state; diff --git a/src/js/reducers/spinner.js b/src/js/reducers/spinner.js index 3046b9f..451466d 100644 --- a/src/js/reducers/spinner.js +++ b/src/js/reducers/spinner.js @@ -1,4 +1,5 @@ -import { SPINNER, CHANGE_DIRECTORY, LIST_FILES, REFRESH, DIALOG, CREATE_FILE, DELETE_FILE } from 'actions/types'; +import { SPINNER, CHANGE_DIRECTORY, LIST_FILES, REFRESH, DIALOG, SETTINGS, + CREATE_FILE, DELETE_FILE, RENAME_FILE, MOVE_FILE, COPY_FILE, SEARCH} from 'actions/types'; export default function(state = false, action) { if (action.type === SPINNER) { @@ -12,6 +13,13 @@ export default function(state = false, action) { switch (action.type) { case CHANGE_DIRECTORY: case REFRESH: + case SETTINGS: + case CREATE_FILE: + case MOVE_FILE: + case DELETE_FILE: + case RENAME_FILE: + case COPY_FILE: + case SEARCH: return true; case LIST_FILES: return false; diff --git a/src/js/store.js b/src/js/store.js index 6073c51..3b6762b 100644 --- a/src/js/store.js +++ b/src/js/store.js @@ -7,6 +7,7 @@ import dialogs from './dialogs'; const DEFAULT = new Immutable.Map(Object.assign({ dir: '', + settings: JSON.parse(localStorage.getItem('settings') || '{}') }, dialogs, menus)); let store = createStore(reducers, DEFAULT); diff --git a/src/js/tour.js b/src/js/tour.js index e5b0813..692b1e2 100644 --- a/src/js/tour.js +++ b/src/js/tour.js @@ -4,7 +4,8 @@ const MESSAGES = { 'icon-select': 'Select files for batch actions', 'icon-more': 'Actions used on selected files such as Copy, Delete, Move, …', 'drawer': 'Extra options, tools and links are here', - 'icon-search': 'Search your storage for a certain file' + 'icon-search': 'Search your storage for a certain file', + 'swipe-instruction': 'Swipe from left to right to go to parent folder' } const DIALOG_HIDE_DELAY = 2000; diff --git a/src/less/styles/tour.less b/src/less/styles/tour.less index 1f422d1..8882bc1 100644 --- a/src/less/styles/tour.less +++ b/src/less/styles/tour.less @@ -49,6 +49,40 @@ animation: pulse 2s ease-out infinite; } + .swipe-instruction { + position: fixed; + left: 50%; + top: 20%; + + width: 70vw; + height: 5rem; + + margin-left: -35vw; + + z-index: 1; + + background: white; + + border-radius: 3rem; + + &::before { + content: ''; + + position: absolute; + left: 0; + top: 0; + + width: 5rem; + height: 5rem; + + background: darken(white, 15); + + border-radius: 50%; + + animation: swipe 3s ease infinite; + } + } + .tour-dialog { display: block; @@ -86,3 +120,22 @@ transform: scale(5); } } + +@keyframes swipe { + 80% { + left: ~'calc(100% - 5rem)'; + opacity: 1; + } + 90% { + opacity: 0; + left: ~'calc(100% - 5rem)'; + } + 91% { + left: 0; + opacity: 0; + } + 100% { + left: 0; + opacity: 1; + } +} diff --git a/src/manifest.webapp b/src/manifest.webapp index 8fe3383..509526c 100644 --- a/src/manifest.webapp +++ b/src/manifest.webapp @@ -1,7 +1,7 @@ { "version": "0.1.0", "name": "Hawk", - "description": "Keep an eye on your files with a full-featured file-manager", + "description": "Keep an eye on your files with a full-featured file manager", "launch_path": "/index.html", "icons": { "16": "/icon/Icon-16.png", @@ -15,13 +15,26 @@ }, "type": "privileged", "permissions": { - "device-storage:sdcard": {"access": "readwrite"}, - "device-storage:videos": {"access": "readwrite"}, - "device-storage:pictures": {"access": "readwrite"}, - "device-storage:music": {"access": "readwrite"} + "device-storage:sdcard": { + "access": "readwrite", + "description": "We need access to your files in order to give you the functionality of listing, reading, opening and writing files in your storage" + }, + "device-storage:videos": { + "access": "readwrite", + "description": "We need access to your files in order to give you the functionality of listing, reading, opening and writing files in your storage" + }, + "device-storage:pictures": { + "access": "readwrite", + "description": "We need access to your files in order to give you the functionality of listing, reading, opening and writing files in your storage" + }, + "device-storage:music": { + "access": "readwrite", + "description": "We need access to your files in order to give you the functionality of listing, reading, opening and writing files in your storage" + } }, "installs_allowed_from": [ - "marketplace.firefox.com" + "https://marketplace.firefox.com", + "https://marketplace-dev.allizom.org" ], "default_locale": "en" }