diff --git a/Gruntfile.js b/Gruntfile.js index 7b8a556..79f443d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -85,7 +85,7 @@ module.exports = function(grunt) { cwd: 'src', dest: 'build', src: ['index.html', 'manifest.webapp', - 'fonts/**', 'img/**', 'js/libs/**'] + 'fonts/**', 'img/**', 'js/libs/**', 'icon/**'] }] } }, diff --git a/README.md b/README.md index 7dd5915..36931be 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ Please read the Features section below and issues to make sure your issue is not - [x] Create new files and directories - [x] File Size - [x] Directory Child Count +- [x] Actions on multiple files (selection) +- [ ] Copy/Cut and Paste files - [ ] File Preview - [ ] Filter Files - [ ] Search diff --git a/build/icon/Icon-128.png b/build/icon/Icon-128.png new file mode 100644 index 0000000..b90da61 Binary files /dev/null and b/build/icon/Icon-128.png differ diff --git a/build/icon/Icon-16.png b/build/icon/Icon-16.png new file mode 100644 index 0000000..8bf30bb Binary files /dev/null and b/build/icon/Icon-16.png differ diff --git a/build/icon/Icon-48.png b/build/icon/Icon-48.png new file mode 100644 index 0000000..1b83591 Binary files /dev/null and b/build/icon/Icon-48.png differ diff --git a/build/icon/Icon-60.png b/build/icon/Icon-60.png new file mode 100644 index 0000000..aef57b6 Binary files /dev/null and b/build/icon/Icon-60.png differ diff --git a/build/icon/Icon.png b/build/icon/Icon.png new file mode 100644 index 0000000..c13776e Binary files /dev/null and b/build/icon/Icon.png differ diff --git a/build/img/Select.svg b/build/img/Select.svg new file mode 100644 index 0000000..7d0fd6c --- /dev/null +++ b/build/img/Select.svg @@ -0,0 +1,19 @@ + + + + Select + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/img/icons/icon.svg b/build/img/icons/icon.svg deleted file mode 100644 index 834968c..0000000 --- a/build/img/icons/icon.svg +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - image/svg+xml - - - - - - - empty - Created with Sketch. - - - - - - - - - - - - diff --git a/build/img/icons/icon128x128.png b/build/img/icons/icon128x128.png deleted file mode 100644 index 7c672c5..0000000 Binary files a/build/img/icons/icon128x128.png and /dev/null differ diff --git a/build/img/icons/icon16x16.png b/build/img/icons/icon16x16.png deleted file mode 100644 index abf254e..0000000 Binary files a/build/img/icons/icon16x16.png and /dev/null differ diff --git a/build/img/icons/icon48x48.png b/build/img/icons/icon48x48.png deleted file mode 100644 index 451655b..0000000 Binary files a/build/img/icons/icon48x48.png and /dev/null differ diff --git a/build/img/icons/icon60x60.png b/build/img/icons/icon60x60.png deleted file mode 100644 index c7d4115..0000000 Binary files a/build/img/icons/icon60x60.png and /dev/null differ diff --git a/build/main.js b/build/main.js index 863397a..16c3b63 100644 --- a/build/main.js +++ b/build/main.js @@ -22213,7 +22213,9 @@ function rename(file, name) { }; } -function active(file) { +function active() { + var file = arguments.length <= 0 || arguments[0] === undefined ? null : arguments[0]; + return { type: _actionsTypes.ACTIVE_FILE, file: file @@ -22221,6 +22223,7 @@ function active(file) { } function deleteFile(file) { + console.log('constructing deleteFile action', file); return { type: _actionsTypes.DELETE_FILE, file: file @@ -22237,6 +22240,7 @@ exports.refresh = refresh; exports.toggle = toggle; exports.details = details; exports.list = list; +exports.selectView = selectView; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } @@ -22252,27 +22256,36 @@ function refresh() { }; } -function toggle(state) { +function toggle() { return { type: _actionsTypes.FILES_VIEW, view: 'toggle' }; } -function details(state) { +function details() { return { type: _actionsTypes.FILES_VIEW, view: 'details' }; } -function list(state) { +function list() { return { type: _actionsTypes.FILES_VIEW, view: 'list' }; } +function selectView() { + var active = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0]; + + return { + type: _actionsTypes.SELECT_VIEW, + active: active + }; +} + },{"actions/types":223,"store":"store"}],219:[function(require,module,exports){ 'use strict'; @@ -22407,6 +22420,7 @@ var TYPES = { LIST_FILES: Symbol('LIST_FILES'), FILES_VIEW: Symbol('FILES_VIEW'), + SELECT_VIEW: Symbol('SELECT_VIEW'), NAVIGATION: Symbol('NAVIGATION'), TOGGLE: Symbol('TOGGLE'), @@ -22422,7 +22436,7 @@ var TYPES = { MENU: Symbol('MENU'), - DIALOG: Symbol('DEBUG'), + DIALOG: Symbol('DIALOG'), SETTINGS: Symbol('SETTINGS'), @@ -22540,6 +22554,14 @@ var createDirectory = _asyncToGenerator(function* () { exports.createDirectory = createDirectory; +var remove = _asyncToGenerator(function* (file) { + var parent = yield root(); + + return parent.remove(file); +}); + +exports.remove = remove; + var move = _asyncToGenerator(function* (file, newPath) { var path = (file.path || '').replace(/^\//, ''); // remove starting slash var oldPath = path + file.name; @@ -22667,12 +22689,13 @@ var Breadcrumb = (function (_Component) { _createClass(Breadcrumb, [{ key: 'render', value: function render() { - var directories = this.props.cwd.split('/'); + var directories = this.props.cwd.split('/').filter(function (a) { + return a; + }); directories.unshift('sdcard'); var els = directories.map(function (dir, index, arr) { var path = arr.slice(1, index + 1).join('/'); - var slash = index > 0 ? '/' : ''; return _react2['default'].createElement( 'span', @@ -22680,18 +22703,21 @@ var Breadcrumb = (function (_Component) { _react2['default'].createElement( 'i', null, - slash + '/' ), dir ); }); - var lastDirectories = this.props.lwd.split('/'); + var lastDirectories = this.props.lwd.split('/').filter(function (a) { + return a; + }); if (lastDirectories.length > directories.length - 1) { lastDirectories.splice(0, directories.length - 1); + var _history = lastDirectories.map(function (dir, index, arr) { var current = directories.slice(1).concat(arr.slice(0, index + 1)); - var path = current.join('/'); + var path = current.join('/').replace(/^\//, ''); // remove starting slash return _react2['default'].createElement( 'span', @@ -22865,11 +22891,24 @@ var Directory = (function (_Component) { _createClass(Directory, [{ key: 'render', value: function render() { + var checkId = 'file-' + this.props.index; + + var input = undefined, + label = undefined; + if (this.props.selectView) { + input = _react2['default'].createElement('input', { type: 'checkbox', id: checkId, checked: this.props.selected, readOnly: true }); + label = _react2['default'].createElement('label', { htmlFor: checkId }); + } + + var clickHandler = this.props.selectView ? this.select.bind(this) : this.peek.bind(this); + return _react2['default'].createElement( 'div', { className: 'directory', ref: 'container', - onClick: this.peek.bind(this), + onClick: clickHandler, onContextMenu: this.contextMenu.bind(this) }, + input, + label, _react2['default'].createElement('i', null), _react2['default'].createElement( 'p', @@ -22907,6 +22946,19 @@ var Directory = (function (_Component) { _store2['default'].dispatch((0, _actionsMenu.show)('directoryMenu', { style: { left: left, top: top } })); _store2['default'].dispatch((0, _actionsFile.active)(this.props.index)); } + }, { + key: 'select', + value: function select() { + var current = (_store2['default'].getState().get('activeFile') || []).slice(0); + var index = this.props.index; + + if (current.indexOf(index) > -1) { + current.splice(current.indexOf(index), 1); + } else { + current.push(index); + } + _store2['default'].dispatch((0, _actionsFile.active)(current)); + } }]); return Directory; @@ -22973,29 +23025,20 @@ var FileList = (function (_Component) { _createClass(FileList, [{ key: 'render', value: function render() { - var files = this.props.files; + var _props = this.props; + var files = _props.files; + var selectView = _props.selectView; + var activeFile = _props.activeFile; + activeFile = activeFile || []; var settings = _store2['default'].getState().get('settings'); - if (settings.showDirectoriesFirst) { - files = files.sort(function (a, b) { - if ((0, _utils.type)(a) === 'Directory') return -1; - if ((0, _utils.type)(b) === 'Directory') return 1; - return 0; - }); - } - - if (!settings.showHiddenFiles) { - files = files.filter(function (file) { - return file.name[0] !== '.'; - }); - } - var els = files.map(function (file, index) { + var selected = activeFile.indexOf(index) > -1; if ((0, _utils.type)(file) === 'File') { - return _react2['default'].createElement(_file2['default'], { key: index, index: index, name: file.name, size: file.size }); + return _react2['default'].createElement(_file2['default'], { selectView: selectView, selected: selected, key: index, index: index, name: file.name, size: file.size }); } else { - return _react2['default'].createElement(_directory2['default'], { key: index, index: index, name: file.name, children: file.children }); + return _react2['default'].createElement(_directory2['default'], { selectView: selectView, selected: selected, key: index, index: index, name: file.name, children: file.children }); } }); @@ -23016,7 +23059,9 @@ exports['default'] = FileList; function props(state) { return { - files: state.get('files') + files: state.get('files'), + selectView: state.get('selectView'), + activeFile: state.get('activeFile') }; } @@ -23069,10 +23114,24 @@ var File = (function (_Component) { _createClass(File, [{ key: 'render', value: function render() { + var checkId = 'file-' + this.props.index; + + var input = undefined, + label = undefined; + if (this.props.selectView) { + input = _react2['default'].createElement('input', { type: 'checkbox', id: checkId, defaultChecked: this.props.selected, readOnly: true }); + label = _react2['default'].createElement('label', { htmlFor: checkId }); + } + + var clickHandler = this.props.selectView ? this.select.bind(this) : null; + return _react2['default'].createElement( 'div', { className: 'file', ref: 'container', + onClick: clickHandler, onContextMenu: this.contextMenu.bind(this) }, + input, + label, _react2['default'].createElement('i', null), _react2['default'].createElement( 'p', @@ -23102,6 +23161,19 @@ var File = (function (_Component) { _store2['default'].dispatch((0, _actionsMenu.show)('fileMenu', { style: { left: left, top: top } })); _store2['default'].dispatch((0, _actionsFile.active)(this.props.index)); } + }, { + key: 'select', + value: function select() { + var current = (_store2['default'].getState().get('activeFile') || []).slice(0); + var index = this.props.index; + + if (current.indexOf(index) > -1) { + current.splice(current.indexOf(index), 1); + } else { + current.push(index); + } + _store2['default'].dispatch((0, _actionsFile.active)(current)); + } }]); return File; @@ -23482,6 +23554,9 @@ var FileMenu = (0, _reactRedux.connect)(function (state) { var DirectoryMenu = (0, _reactRedux.connect)(function (state) { return state.get('directoryMenu'); })(_componentsMenu2['default']); +var MoreMenu = (0, _reactRedux.connect)(function (state) { + return state.get('moreMenu'); +})(_componentsMenu2['default']); var RenameDialog = (0, _reactRedux.connect)(function (state) { return state.get('renameDialog'); @@ -23518,6 +23593,7 @@ var Root = (function (_Component) { _react2['default'].createElement(_componentsToolbar2['default'], null), _react2['default'].createElement(FileMenu, null), _react2['default'].createElement(DirectoryMenu, null), + _react2['default'].createElement(MoreMenu, null), _react2['default'].createElement(RenameDialog, null), _react2['default'].createElement(DeleteDialog, null), _react2['default'].createElement(ErrorDialog, null), @@ -23527,7 +23603,12 @@ var Root = (function (_Component) { }, { key: 'touchStart', value: function touchStart(e) { - if (!e.target.closest('.menu')) { + var active = document.querySelector('.active'); + var inside = e.target.closest('.menu') || e.target.closest('.dialog'); + if (!inside && active) { + e.preventDefault(); + e.stopPropagation(); + _store2['default'].dispatch((0, _actionsMenu.hideAll)()); _store2['default'].dispatch((0, _actionsDialog.hideAll)()); } @@ -23565,10 +23646,14 @@ var _actionsFilesView = require('actions/files-view'); var _actionsDialog = require('actions/dialog'); +var _actionsMenu = require('actions/menu'); + var _store = require('store'); var _store2 = _interopRequireDefault(_store); +var _menu = require('./menu'); + var Toolbar = (function (_Component) { _inherits(Toolbar, _Component); @@ -23587,13 +23672,25 @@ var Toolbar = (function (_Component) { _react2['default'].createElement('button', { className: 'icon-plus', onClick: this.newFile }), _react2['default'].createElement('button', { className: 'icon-view', onClick: (0, _store.bind)((0, _actionsFilesView.toggle)()) }), _react2['default'].createElement('button', { className: 'icon-refresh', onClick: (0, _store.bind)((0, _actionsFilesView.refresh)()) }), - _react2['default'].createElement('button', { className: 'icon-share', onClick: this.share }), - _react2['default'].createElement('button', { className: 'icon-more', onClick: this.showMore }) + _react2['default'].createElement('button', { className: 'icon-select', onClick: (0, _store.bind)((0, _actionsFilesView.selectView)('toggle')) }), + _react2['default'].createElement('button', { className: 'icon-more', onClick: this.showMore.bind(this), ref: 'more' }) ); } }, { key: 'showMore', - value: function showMore() {} + value: function showMore() { + var rect = _react2['default'].findDOMNode(this.refs.more).getBoundingClientRect(); + var x = rect.x; + var y = rect.y; + var width = rect.width; + var height = rect.height; + + var left = x + width - _menu.MENU_WIDTH, + top = y + height; + + var transform = 'translate(0, -100%)'; + _store2['default'].dispatch((0, _actionsMenu.show)('moreMenu', { style: { left: left, top: top, transform: transform } })); + } }, { key: 'newFile', value: function newFile() { @@ -23611,7 +23708,7 @@ var Toolbar = (function (_Component) { exports['default'] = Toolbar; module.exports = exports['default']; -},{"actions/dialog":216,"actions/files-view":218,"react":205,"store":"store"}],235:[function(require,module,exports){ +},{"./menu":231,"actions/dialog":216,"actions/files-view":218,"actions/menu":220,"react":205,"store":"store"}],235:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -23646,6 +23743,7 @@ exports['default'] = { var action = (0, _actionsFile.create)(cwd + input.value); this.props.dispatch(action); this.props.dispatch((0, _actionsDialog.hideAll)()); + this.props.dispatch((0, _actionsFile.active)()); } }, { text: 'Directory', @@ -23656,6 +23754,7 @@ exports['default'] = { var action = (0, _actionsFile.create)(cwd + input.value, true); this.props.dispatch(action); this.props.dispatch((0, _actionsDialog.hideAll)()); + this.props.dispatch((0, _actionsFile.active)()); } }] }, @@ -23674,6 +23773,7 @@ exports['default'] = { var activeFile = _store2['default'].getState().get('activeFile'); this.props.dispatch((0, _actionsFile.rename)(activeFile, input.value)); this.props.dispatch((0, _actionsDialog.hideAll)()); + this.props.dispatch((0, _actionsFile.active)()); }, className: 'success' }] @@ -23690,6 +23790,7 @@ exports['default'] = { var activeFile = _store2['default'].getState().get('activeFile'); this.props.dispatch((0, _actionsFile.deleteFile)(activeFile)); this.props.dispatch((0, _actionsDialog.hideAll)()); + this.props.dispatch((0, _actionsFile.active)()); }, className: 'success' }] @@ -23774,9 +23875,32 @@ var entryMenu = { }] }; +var moreMenu = { + items: [{ + name: 'Delete', + action: function action() { + var files = _store2['default'].getState().get('files'); + var active = _store2['default'].getState().get('activeFile'); + + var description = undefined; + if (active.length) { + var count = active.length; + description = 'Are you sure you want to remove ' + count + ' files?'; + } else { + var _name = files[active].name; + description = 'Are you sure you want to remove ' + _name + '?'; + } + + _store2['default'].dispatch((0, _actionsMenu.hideAll)()); + _store2['default'].dispatch((0, _actionsDialog.show)('deleteDialog', { description: description })); + } + }] +}; + exports['default'] = { fileMenu: Object.assign({}, entryMenu), - directoryMenu: Object.assign({}, entryMenu) + directoryMenu: Object.assign({}, entryMenu), + moreMenu: moreMenu }; module.exports = exports['default']; @@ -23790,7 +23914,7 @@ Object.defineProperty(exports, '__esModule', { var _actionsTypes = require('actions/types'); exports['default'] = function (state, action) { - if (state === undefined) state = -1; + if (state === undefined) state = null; if (action.type === _actionsTypes.ACTIVE_FILE) { return action.file; @@ -23846,6 +23970,10 @@ var _settings = require('./settings'); var _settings2 = _interopRequireDefault(_settings); +var _selectView = require('./select-view'); + +var _selectView2 = _interopRequireDefault(_selectView); + exports['default'] = function (state, action) { if (state === undefined) state = new _immutable2['default'].Map(); @@ -23854,11 +23982,13 @@ exports['default'] = function (state, action) { lwd: (0, _lwd2['default'])(state, action), // last working directory cwd: (0, _cwd2['default'])(state.get('cwd'), action), files: (0, _files2['default'])(state.get('files'), action), + selectView: (0, _selectView2['default'])(state.get('selectView'), action), activeFile: (0, _activeFile2['default'])(state.get('activeFile'), action), navigation: (0, _navigation2['default'])(state.get('navigation'), action), settings: (0, _settings2['default'])(state.get('settings'), action), fileMenu: (0, _menu2['default'])(state, action, 'fileMenu'), directoryMenu: (0, _menu2['default'])(state, action, 'directoryMenu'), + moreMenu: (0, _menu2['default'])(state, action, 'moreMenu'), renameDialog: (0, _dialog2['default'])(state, action, 'renameDialog'), deleteDialog: (0, _dialog2['default'])(state, action, 'deleteDialog'), errorDialog: (0, _dialog2['default'])(state, action, 'errorDialog'), @@ -23868,7 +23998,7 @@ exports['default'] = function (state, action) { module.exports = exports['default']; -},{"./active-file":238,"./cwd":240,"./dialog":241,"./files":242,"./lwd":243,"./menu":244,"./navigation":245,"./settings":246,"immutable":247}],240:[function(require,module,exports){ +},{"./active-file":238,"./cwd":240,"./dialog":241,"./files":242,"./lwd":243,"./menu":244,"./navigation":245,"./select-view":246,"./settings":247,"immutable":248}],240:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -23958,7 +24088,7 @@ exports['default'] = function (state, action, id) { module.exports = exports['default']; -},{"actions/types":223,"immutable":247,"lodash/object/omit":37}],242:[function(require,module,exports){ +},{"actions/types":223,"immutable":248,"lodash/object/omit":37}],242:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -23987,6 +24117,22 @@ exports['default'] = function (state, action) { if (state === undefined) state = []; if (action.type === _actionsTypes.LIST_FILES) { + + var settings = _store2['default'].getState().get('settings'); + + if (settings.showDirectoriesFirst) { + action.files = action.files.sort(function (a, b) { + if ((0, _utils.type)(a) === 'Directory') return -1; + if ((0, _utils.type)(a) === 'File') return 1; + }); + } + + if (!settings.showHiddenFiles) { + action.files = action.files.filter(function (file) { + return file.name[0] !== '.'; + }); + } + return action.files; } @@ -24006,17 +24152,52 @@ exports['default'] = function (state, action) { } if (action.type === _actionsTypes.DELETE_FILE) { - var file = state[action.file]; - - (0, _apiFiles.sdcard)()['delete']((file.path || '') + '/' + file.name); var copy = state.slice(0); - copy.splice(action.file, 1); + + if (action.file.length) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = action.file[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var index = _step.value; + + del(state, index); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator['return']) { + _iterator['return'](); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + copy = copy.filter(function (a, i) { + return action.file.indexOf(i) === -1; + }); + } else { + del(state, action.file); + copy.splice(action.file, 1); + } + return copy; } return state; }; +function del(state, index) { + var file = state[index]; + return (0, _apiFiles.remove)((file.path || '') + '/' + file.name)['catch'](_utils.reportError); +} module.exports = exports['default']; },{"actions/dialog":216,"actions/files-view":218,"actions/types":223,"api/files":224,"store":"store","utils":"utils"}],243:[function(require,module,exports){ @@ -24081,7 +24262,7 @@ exports['default'] = function (state, action, id) { module.exports = exports['default']; -},{"actions/types":223,"immutable":247,"lodash/object/omit":37}],245:[function(require,module,exports){ +},{"actions/types":223,"immutable":248,"lodash/object/omit":37}],245:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -24109,6 +24290,27 @@ Object.defineProperty(exports, '__esModule', { value: true }); +var _actionsTypes = require('actions/types'); + +exports['default'] = function (state, action) { + if (state === undefined) state = false; + + if (action.type === _actionsTypes.SELECT_VIEW) { + return action.active === 'toggle' ? !state : action.active; + } + + return state; +}; + +module.exports = exports['default']; + +},{"actions/types":223}],247:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _actionsTypes = require('actions/types'); @@ -24134,7 +24336,7 @@ exports['default'] = function (state, action) { module.exports = exports['default']; -},{"actions/types":223,"lodash/object/omit":37}],247:[function(require,module,exports){ +},{"actions/types":223,"lodash/object/omit":37}],248:[function(require,module,exports){ /** * Copyright (c) 2014-2015, Facebook, Inc. * All rights reserved. @@ -29109,7 +29311,7 @@ function bind(action) { exports['default'] = store; -},{"./dialogs":235,"./menus":237,"actions/changedir":215,"immutable":247,"reducers/all":239,"redux":207}],"utils":[function(require,module,exports){ +},{"./dialogs":235,"./menus":237,"actions/changedir":215,"immutable":248,"reducers/all":239,"redux":207}],"utils":[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -29168,15 +29370,13 @@ var sizes = { }; function humanSize(size) { - console.log(size); for (var key in sizes) { var value = sizes[key]; - console.log(value); if (size > value) { return Math.round(size / value) + key; } } } -},{"actions/dialog":216,"store":"store"}]},{},[215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,"store","utils"]); +},{"actions/dialog":216,"store":"store"}]},{},[215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,"store","utils"]); diff --git a/build/manifest.webapp b/build/manifest.webapp index bfa7f4f..eb4152c 100644 --- a/build/manifest.webapp +++ b/build/manifest.webapp @@ -4,10 +4,10 @@ "description": "Keep an eye on your files with a full-featured file-manager", "launch_path": "/index.html", "icons": { - "16": "/img/icons/icon16x16.png", - "48": "/img/icons/icon48x48.png", - "60": "/img/icons/icon60x60.png", - "128": "/img/icons/icon128x128.png" + "16": "/icon/Icon-16.png", + "48": "/icon/Icon-48.png", + "60": "/icon/Icon-60.png", + "128": "/icon/Icon-128.png" }, "developer": { "name": "Mahdi Dibaiee", diff --git a/build/style.css b/build/style.css index 0a0ccec..768499f 100644 --- a/build/style.css +++ b/build/style.css @@ -31,11 +31,11 @@ width: 26px; height: 28px; } -.icon-share { +.icon-select { display: block; - background: url(/img/Share.svg) no-repeat; - width: 25px; - height: 27px; + background: url(/img/Select.svg) no-repeat; + width: 32px; + height: 34px; } .icon-more { display: block; @@ -104,21 +104,26 @@ input { font-weight: 200; font-size: 1.6rem; } -input[type="checkbox"] { - width: 0; - height: 0; +label { + clear: left; } -input[type="checkbox"]::before { +label::after { content: ''; display: block; - border: 1px solid #f0f0f0; - background: transparent; + float: right; + margin-right: 13px; border-radius: 50%; width: 10px; height: 10px; - box-sizing: border-box; + background: transparent; + border: 1px solid #9b9b93; } -input[type="checkbox"]::before:checked { +input[type='checkbox'] { + clear: right; + float: right; + display: none; +} +input:checked + label::after { background: #63b0cd; } .file, @@ -259,28 +264,6 @@ nav li:last-of-type { padding-bottom: 0; border-bottom: none; } -nav li label { - clear: left; -} -nav li label::after { - content: ''; - display: block; - float: right; - margin-right: 13px; - border-radius: 50%; - width: 10px; - height: 10px; - background: transparent; - border: 1px solid #9b9b93; -} -nav li input { - clear: right; - float: right; - display: none; -} -nav li input:checked + label::after { - background: #63b0cd; -} nav i { display: block; position: fixed; diff --git a/design/userinterface.sketch b/design/userinterface.sketch index 75627a2..325b44a 100644 Binary files a/design/userinterface.sketch and b/design/userinterface.sketch differ diff --git a/src/icon/Icon-128.png b/src/icon/Icon-128.png new file mode 100644 index 0000000..b90da61 Binary files /dev/null and b/src/icon/Icon-128.png differ diff --git a/src/icon/Icon-16.png b/src/icon/Icon-16.png new file mode 100644 index 0000000..8bf30bb Binary files /dev/null and b/src/icon/Icon-16.png differ diff --git a/src/icon/Icon-48.png b/src/icon/Icon-48.png new file mode 100644 index 0000000..1b83591 Binary files /dev/null and b/src/icon/Icon-48.png differ diff --git a/src/icon/Icon-60.png b/src/icon/Icon-60.png new file mode 100644 index 0000000..aef57b6 Binary files /dev/null and b/src/icon/Icon-60.png differ diff --git a/src/icon/Icon.png b/src/icon/Icon.png new file mode 100644 index 0000000..c13776e Binary files /dev/null and b/src/icon/Icon.png differ diff --git a/src/img/Select.svg b/src/img/Select.svg new file mode 100644 index 0000000..7d0fd6c --- /dev/null +++ b/src/img/Select.svg @@ -0,0 +1,19 @@ + + + + Select + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/js/actions/file.js b/src/js/actions/file.js index 4b899e9..4214aa8 100644 --- a/src/js/actions/file.js +++ b/src/js/actions/file.js @@ -21,7 +21,7 @@ export function rename(file, name) { } } -export function active(file) { +export function active(file = null) { return { type: ACTIVE_FILE, file @@ -29,6 +29,7 @@ export function active(file) { } export function deleteFile(file) { + console.log('constructing deleteFile action', file); return { type: DELETE_FILE, file diff --git a/src/js/actions/files-view.js b/src/js/actions/files-view.js index 32afacb..a9fd852 100644 --- a/src/js/actions/files-view.js +++ b/src/js/actions/files-view.js @@ -1,4 +1,4 @@ -import { LIST_FILES, FILES_VIEW, REFRESH } from 'actions/types'; +import { LIST_FILES, FILES_VIEW, SELECT_VIEW, REFRESH } from 'actions/types'; import store from 'store'; export function refresh() { @@ -7,23 +7,30 @@ export function refresh() { } } -export function toggle(state) { +export function toggle() { return { type: FILES_VIEW, view: 'toggle' } } -export function details(state) { +export function details() { return { type: FILES_VIEW, view: 'details' } } -export function list(state) { +export function list() { return { type: FILES_VIEW, view: 'list' } } + +export function selectView(active = true) { + return { + type: SELECT_VIEW, + active + } +} diff --git a/src/js/actions/types.js b/src/js/actions/types.js index ef8e3af..ebca1c2 100644 --- a/src/js/actions/types.js +++ b/src/js/actions/types.js @@ -3,6 +3,7 @@ const TYPES = { LIST_FILES: Symbol('LIST_FILES'), FILES_VIEW: Symbol('FILES_VIEW'), + SELECT_VIEW: Symbol('SELECT_VIEW'), NAVIGATION: Symbol('NAVIGATION'), TOGGLE: Symbol('TOGGLE'), @@ -18,7 +19,7 @@ const TYPES = { MENU: Symbol('MENU'), - DIALOG: Symbol('DEBUG'), + DIALOG: Symbol('DIALOG'), SETTINGS: Symbol('SETTINGS'), diff --git a/src/js/api/files.js b/src/js/api/files.js index 5a8ec3d..121f27a 100644 --- a/src/js/api/files.js +++ b/src/js/api/files.js @@ -75,6 +75,12 @@ export async function createDirectory(...args) { return parent.createDirectory(...args); } +export async function remove(file) { + let parent = await root(); + + return parent.remove(file); +} + export async function move(file, newPath) { let path = (file.path || '').replace(/^\//, ''); // remove starting slash let oldPath = path + file.name; diff --git a/src/js/components/breadcrumb.js b/src/js/components/breadcrumb.js index 44b0184..4bf5701 100644 --- a/src/js/components/breadcrumb.js +++ b/src/js/components/breadcrumb.js @@ -7,26 +7,26 @@ import { bind } from 'store'; @connect(props) export default class Breadcrumb extends Component { render() { - let directories = this.props.cwd.split('/'); + let directories = this.props.cwd.split('/').filter(a => a); directories.unshift('sdcard'); let els = directories.map((dir, index, arr) => { let path = arr.slice(1, index + 1).join('/'); - let slash = index > 0 ? '/' : ''; return ( - {slash}{dir} + /{dir} ); }); - let lastDirectories = this.props.lwd.split('/'); + let lastDirectories = this.props.lwd.split('/').filter(a => a); if (lastDirectories.length > directories.length - 1) { lastDirectories.splice(0, directories.length - 1); + let history = lastDirectories.map((dir, index, arr) => { let current = directories.slice(1).concat(arr.slice(0, index + 1)); - let path = current.join('/'); + let path = current.join('/').replace(/^\//, ''); // remove starting slash return ( diff --git a/src/js/components/directory.js b/src/js/components/directory.js index c9e5b1c..f1e02fc 100644 --- a/src/js/components/directory.js +++ b/src/js/components/directory.js @@ -9,10 +9,25 @@ const MENU_TOP_SPACE = 20; export default class Directory extends Component { render() { + let checkId = `file-${this.props.index}`; + + let input, label; + if (this.props.selectView) { + input = ; + label = ; + } + + let clickHandler = this.props.selectView ? this.select.bind(this) + : this.peek.bind(this); + return (
+ + {input} + {label} +

{this.props.name}

{this.props.children} items @@ -37,4 +52,16 @@ export default class Directory extends Component { store.dispatch(show('directoryMenu', {style: {left, top}})); store.dispatch(active(this.props.index)); } + + select() { + let current = (store.getState().get('activeFile') || []).slice(0); + let index = this.props.index; + + if (current.indexOf(index) > -1) { + current.splice(current.indexOf(index), 1); + } else { + current.push(index) + } + store.dispatch(active(current)); + } } diff --git a/src/js/components/file-list.js b/src/js/components/file-list.js index 5f2b6be..c774fdc 100644 --- a/src/js/components/file-list.js +++ b/src/js/components/file-list.js @@ -12,29 +12,16 @@ export default class FileList extends Component { } render() { - let { files } = this.props; - + let { files, selectView, activeFile } = this.props; + activeFile = activeFile || []; let settings = store.getState().get('settings'); - if (settings.showDirectoriesFirst) { - files = files.sort((a, b) => { - if (type(a) === 'Directory') return -1; - if (type(b) === 'Directory') return 1; - return 0; - }) - } - - if (!settings.showHiddenFiles) { - files = files.filter(file => { - return file.name[0] !== '.'; - }) - } - let els = files.map((file, index) => { + let selected = activeFile.indexOf(index) > -1; if (type(file) === 'File') { - return ; + return ; } else { - return + return } }); @@ -48,7 +35,9 @@ export default class FileList extends Component { function props(state) { return { - files: state.get('files') + files: state.get('files'), + selectView: state.get('selectView'), + activeFile: state.get('activeFile') } } diff --git a/src/js/components/file.js b/src/js/components/file.js index 30e2561..cb160e1 100644 --- a/src/js/components/file.js +++ b/src/js/components/file.js @@ -13,9 +13,25 @@ export default class File extends Component { } render() { + let checkId = `file-${this.props.index}`; + + let input, label; + if (this.props.selectView) { + input = ; + label = ; + } + + let clickHandler = this.props.selectView ? this.select.bind(this) + : null; + return (
+ + {input} + {label} +

{this.props.name}

{humanSize(this.props.size)} @@ -34,4 +50,16 @@ export default class File extends Component { store.dispatch(show('fileMenu', {style: {left, top}})); store.dispatch(active(this.props.index)); } + + select() { + let current = (store.getState().get('activeFile') || []).slice(0); + let index = this.props.index; + + if (current.indexOf(index) > -1) { + current.splice(current.indexOf(index), 1); + } else { + current.push(index) + } + store.dispatch(active(current)); + } } diff --git a/src/js/components/root.js b/src/js/components/root.js index 8121471..bf9c163 100644 --- a/src/js/components/root.js +++ b/src/js/components/root.js @@ -18,6 +18,7 @@ window.changedir = changedir; let FileMenu = connect(state => state.get('fileMenu'))(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); let DeleteDialog = connect(state => state.get('deleteDialog'))(Dialog); @@ -36,6 +37,7 @@ export default class Root extends Component { + @@ -46,7 +48,12 @@ export default class Root extends Component { } touchStart(e) { - if (!e.target.closest('.menu')) { + let active = document.querySelector('.active'); + let inside = e.target.closest('.menu') || e.target.closest('.dialog'); + if (!inside && active) { + e.preventDefault(); + e.stopPropagation(); + store.dispatch(hideAllMenus()); store.dispatch(hideAllDialogs()); } diff --git a/src/js/components/toolbar.js b/src/js/components/toolbar.js index 31340f9..4c39f66 100644 --- a/src/js/components/toolbar.js +++ b/src/js/components/toolbar.js @@ -1,7 +1,9 @@ import React, { Component } from 'react'; -import { toggle as toggleView, refresh } from 'actions/files-view'; +import { toggle as toggleView, refresh, selectView } from 'actions/files-view'; import { show as showDialog } from 'actions/dialog'; +import { show as showMenu } from 'actions/menu'; import store, { bind } from 'store'; +import { MENU_WIDTH } from './menu'; export default class Toolbar extends Component { render() { @@ -10,14 +12,21 @@ export default class Toolbar extends Component {
); } showMore() { + let rect = React.findDOMNode(this.refs.more).getBoundingClientRect(); + let {x, y, width, height} = rect; + let left = x + width - MENU_WIDTH, + top = y + height; + + let transform = 'translate(0, -100%)'; + store.dispatch(showMenu('moreMenu', {style: {left, top, transform}})); } newFile() { diff --git a/src/js/dialogs.js b/src/js/dialogs.js index a4d1b2a..f44b6b1 100644 --- a/src/js/dialogs.js +++ b/src/js/dialogs.js @@ -1,6 +1,6 @@ import React from 'react'; import { hide, hideAll } from 'actions/dialog'; -import { rename, deleteFile, create } from 'actions/file'; +import { rename, deleteFile, create, active } from 'actions/file'; import store, { bind } from 'store'; export default { @@ -18,6 +18,7 @@ export default { let action = create(cwd + input.value); this.props.dispatch(action); this.props.dispatch(hideAll()); + this.props.dispatch(active()); } }, { @@ -29,6 +30,7 @@ export default { let action = create(cwd + input.value, true); this.props.dispatch(action); this.props.dispatch(hideAll()); + this.props.dispatch(active()); } } ] @@ -50,6 +52,7 @@ export default { let activeFile = store.getState().get('activeFile'); this.props.dispatch(rename(activeFile, input.value)) this.props.dispatch(hideAll()); + this.props.dispatch(active()); }, className: 'success' } @@ -69,6 +72,7 @@ export default { let activeFile = store.getState().get('activeFile'); this.props.dispatch(deleteFile(activeFile)); this.props.dispatch(hideAll()); + this.props.dispatch(active()); }, className: 'success' } diff --git a/src/js/libs/l10n.js b/src/js/libs/l10n.js deleted file mode 100644 index cfc5c51..0000000 --- a/src/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/src/js/menus.js b/src/js/menus.js index af12900..ebcba28 100644 --- a/src/js/menus.js +++ b/src/js/menus.js @@ -30,7 +30,32 @@ const entryMenu = { ] }; +const moreMenu = { + items: [ + { + name: 'Delete', + action() { + let files = store.getState().get('files'); + let active = store.getState().get('activeFile'); + + let description; + if (active.length) { + const count = active.length; + description = `Are you sure you want to remove ${count} files?`; + } else { + const name = files[active].name; + description = `Are you sure you want to remove ${name}?`; + } + + store.dispatch(hideAll()); + store.dispatch(show('deleteDialog', {description})); + } + } + ] +} + export default { fileMenu: Object.assign({}, entryMenu), - directoryMenu: Object.assign({}, entryMenu) + directoryMenu: Object.assign({}, entryMenu), + moreMenu } diff --git a/src/js/reducers/active-file.js b/src/js/reducers/active-file.js index d3a4f4b..0c7fc89 100644 --- a/src/js/reducers/active-file.js +++ b/src/js/reducers/active-file.js @@ -1,6 +1,6 @@ import { ACTIVE_FILE } from 'actions/types'; -export default function(state = -1, action) { +export default function(state = null, action) { if (action.type === ACTIVE_FILE) { return action.file; } diff --git a/src/js/reducers/all.js b/src/js/reducers/all.js index 41397a1..e6d231c 100644 --- a/src/js/reducers/all.js +++ b/src/js/reducers/all.js @@ -7,6 +7,7 @@ import activeFile from './active-file'; import menu from './menu'; import dialog from './dialog'; import settings from './settings'; +import selectView from './select-view'; export default function(state = new Immutable.Map(), action) { console.log('action', action); @@ -14,14 +15,16 @@ export default function(state = new Immutable.Map(), action) { lwd: lwd(state, action), // last working directory cwd: cwd(state.get('cwd'), action), files: files(state.get('files'), action), + selectView: selectView(state.get('selectView'), action), activeFile: activeFile(state.get('activeFile'), action), navigation: navigation(state.get('navigation'), action), settings: settings(state.get('settings'), action), fileMenu: menu(state, action, 'fileMenu'), directoryMenu: menu(state, action, 'directoryMenu'), + moreMenu: menu(state, action, 'moreMenu'), renameDialog: dialog(state, action, 'renameDialog'), deleteDialog: dialog(state, action, 'deleteDialog'), errorDialog: dialog(state, action, 'errorDialog'), - createDialog: dialog(state, action, 'createDialog') + createDialog: dialog(state, action, 'createDialog'), }); } diff --git a/src/js/reducers/files.js b/src/js/reducers/files.js index 64f89e2..dae28bd 100644 --- a/src/js/reducers/files.js +++ b/src/js/reducers/files.js @@ -1,14 +1,30 @@ import { LIST_FILES, RENAME_FILE, DELETE_FILE, CREATE_FILE } from 'actions/types'; import { refresh } from 'actions/files-view'; -import { move, sdcard, createFile, createDirectory } from 'api/files'; +import { move, remove, sdcard, createFile, createDirectory } from 'api/files'; import { show } from 'actions/dialog'; import store, { bind } from 'store'; -import { reportError } from 'utils'; +import { reportError, type } from 'utils'; let boundRefresh = bind(refresh()); export default function(state = [], action) { if (action.type === LIST_FILES) { + + let settings = store.getState().get('settings'); + + if (settings.showDirectoriesFirst) { + action.files = action.files.sort((a, b) => { + if (type(a) === 'Directory') return -1; + if (type(a) === 'File') return 1; + }); + } + + if (!settings.showHiddenFiles) { + action.files = action.files.filter(file => { + return file.name[0] !== '.'; + }) + } + return action.files; } @@ -28,13 +44,26 @@ export default function(state = [], action) { } if (action.type === DELETE_FILE) { - let file = state[action.file]; - - sdcard().delete((file.path || '') + '/' + file.name); let copy = state.slice(0); - copy.splice(action.file, 1); + + if (action.file.length) { + for (let index of action.file) { + del(state, index); + } + + copy = copy.filter((a, i) => action.file.indexOf(i) === -1); + } else { + del(state, action.file); + copy.splice(action.file, 1); + } + return copy; } return state; } + +function del(state, index) { + let file = state[index]; + return remove((file.path || '') + '/' + file.name).catch(reportError); +} diff --git a/src/js/reducers/select-view.js b/src/js/reducers/select-view.js new file mode 100644 index 0000000..0d719db --- /dev/null +++ b/src/js/reducers/select-view.js @@ -0,0 +1,9 @@ +import { SELECT_VIEW } from 'actions/types'; + +export default function(state = false, action) { + if (action.type === SELECT_VIEW) { + return action.active === 'toggle' ? !state : action.active; + } + + return state; +} diff --git a/src/js/utils.js b/src/js/utils.js index 1349914..4c7a585 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -37,11 +37,9 @@ const sizes = { 'B': -1 } export function humanSize(size) { - console.log(size); for (let key in sizes) { let value = sizes[key]; - console.log(value); if (size > value) { return Math.round(size / value) + key; } diff --git a/src/less/components/navigation.less b/src/less/components/navigation.less index 29d8e5b..5d88f93 100644 --- a/src/less/components/navigation.less +++ b/src/less/components/navigation.less @@ -52,39 +52,6 @@ nav { padding-bottom: 0; border-bottom: none; } - - label { - clear: left; - - &::after { - content: ''; - display: block; - - float: right; - - margin-right: 13px; - - border-radius: 50%; - - width: 10px; - height: 10px; - - background: transparent; - - border: 1px solid @overlay; - } - } - - input { - clear: right; - float: right; - - display: none; - } - - input:checked + label::after { - background: @blue; - } } i { diff --git a/src/less/icons.less b/src/less/icons.less index 2d44909..fb69cac 100644 --- a/src/less/icons.less +++ b/src/less/icons.less @@ -37,11 +37,11 @@ height: 28px; } -.icon-share { +.icon-select { .icon; - background: url(/img/Share.svg) no-repeat; - width: 25px; - height: 27px; + background: url(/img/Select.svg) no-repeat; + width: 32px; + height: 34px; } .icon-more { diff --git a/src/less/styles/forms.less b/src/less/styles/forms.less index 6e5eea5..08230bf 100644 --- a/src/less/styles/forms.less +++ b/src/less/styles/forms.less @@ -14,27 +14,35 @@ input { .light-medium; } -input[type="checkbox"] { - width: 0; - height: 0; -} +label { + clear: left; -input[type="checkbox"]::before { - content: ''; - display: block; + &::after { + content: ''; + display: block; - border: 1px solid @gray; + float: right; - background: transparent; + margin-right: 13px; - border-radius: 50%; + border-radius: 50%; - width: 10px; - height: 10px; + width: 10px; + height: 10px; - box-sizing: border-box; + background: transparent; - &:checked { - background: @blue; + border: 1px solid @overlay; } } + +input[type='checkbox'] { + clear: right; + float: right; + + display: none; +} + +input:checked + label::after { + background: @blue; +} diff --git a/src/manifest.webapp b/src/manifest.webapp index bfa7f4f..eb4152c 100644 --- a/src/manifest.webapp +++ b/src/manifest.webapp @@ -4,10 +4,10 @@ "description": "Keep an eye on your files with a full-featured file-manager", "launch_path": "/index.html", "icons": { - "16": "/img/icons/icon16x16.png", - "48": "/img/icons/icon48x48.png", - "60": "/img/icons/icon60x60.png", - "128": "/img/icons/icon128x128.png" + "16": "/icon/Icon-16.png", + "48": "/icon/Icon-48.png", + "60": "/icon/Icon-60.png", + "128": "/icon/Icon-128.png" }, "developer": { "name": "Mahdi Dibaiee",