feat multiselection: select multiple files and act on them

fix breadcrumb: fixed breadcrumb history not working properly when clicking on "sdcard"
fix dialogs/menus: fixed clicking out of menus and dialogs triggering actions other than hiding the dialog/event
This commit is contained in:
Mahdi Dibaiee 2015-09-05 16:09:09 +04:30
parent 39dd4903f9
commit 764554c6b9
45 changed files with 531 additions and 1867 deletions

View File

@ -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/**']
}]
}
},

View File

@ -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

BIN
build/icon/Icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
build/icon/Icon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

BIN
build/icon/Icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
build/icon/Icon-60.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
build/icon/Icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

19
build/img/Select.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="32px" height="34px" viewBox="0 0 32 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.3 (12072) - http://www.bohemiancoding.com/sketch -->
<title>Select</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Components" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="Toolbar" sketch:type="MSArtboardGroup" transform="translate(-257.000000, -7.000000)" stroke-linecap="round" stroke="#63B0CD" stroke-width="3">
<g sketch:type="MSLayerGroup" id="Buttons">
<g transform="translate(26.000000, 7.000000)" sketch:type="MSShapeGroup">
<g id="Select" transform="translate(232.000000, 2.000000)">
<rect id="Rectangle-31" stroke-dasharray="7,8,7,8" x="0" y="1" width="29" height="29" rx="4"></rect>
<path d="M6,17 L10,23 L24,0" id="Path-26" stroke-linejoin="round"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,106 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128px"
height="128px"
viewBox="0 0 128 128"
version="1.1"
id="svg2"
inkscape:version="0.48.2 r9819"
sodipodi:docname="icon.svg">
<metadata
id="metadata14">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1142"
inkscape:window-height="849"
id="namedview12"
showgrid="false"
inkscape:zoom="1.84375"
inkscape:cx="-32.542373"
inkscape:cy="64"
inkscape:window-x="672"
inkscape:window-y="146"
inkscape:window-maximized="0"
inkscape:current-layer="Page-1" />
<!-- Generator: Sketch 3.0.2 (7799) - http://www.bohemiancoding.com/sketch -->
<title
id="title4">empty</title>
<description
id="description6">Created with Sketch.</description>
<defs
id="defs8">
<linearGradient
id="linearGradient3761">
<stop
style="stop-color:#4e748b;stop-opacity:1;"
offset="0"
id="stop3763" />
<stop
style="stop-color:#393e3f;stop-opacity:1;"
offset="1"
id="stop3765" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3761"
id="linearGradient3767"
x1="129.66949"
y1="65.8983"
x2="129.66949"
y2="188.59828"
gradientUnits="userSpaceOnUse" />
</defs>
<path
sodipodi:type="arc"
style="fill:url(#linearGradient3767);fill-opacity:1;fill-rule:evenodd;stroke:none"
id="path2991"
sodipodi:cx="129.35593"
sodipodi:cy="128.27118"
sodipodi:rx="63.18644"
sodipodi:ry="63.18644"
d="m 192.54237,128.27118 a 63.18644,63.18644 0 1 1 -126.372883,0 63.18644,63.18644 0 1 1 126.372883,0 z"
transform="translate(-65.355927,-64.271179)" />
<g
id="Page-1"
sketch:type="MSPage"
transform="matrix(0.9,0,0,0.9,6.4,6.4)"
style="fill:none;stroke:none">
<circle
id="empty"
sketch:type="MSShapeGroup"
cx="64"
cy="64"
r="58"
sodipodi:cx="64"
sodipodi:cy="64"
sodipodi:rx="58"
sodipodi:ry="58"
style="stroke:#bcc6c5;stroke-width:11;stroke-linecap:round;stroke-dasharray:20, 20;fill:none;fill-opacity:1"
d="M 122,64 C 122,96.032515 96.032515,122 64,122 31.967485,122 6,96.032515 6,64 6,31.967485 31.967485,6 64,6 c 32.032515,0 58,25.967485 58,58 z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -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"]);

View File

@ -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",

View File

@ -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;

Binary file not shown.

BIN
src/icon/Icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/icon/Icon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

BIN
src/icon/Icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
src/icon/Icon-60.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
src/icon/Icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

19
src/img/Select.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="32px" height="34px" viewBox="0 0 32 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.3 (12072) - http://www.bohemiancoding.com/sketch -->
<title>Select</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Components" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="Toolbar" sketch:type="MSArtboardGroup" transform="translate(-257.000000, -7.000000)" stroke-linecap="round" stroke="#63B0CD" stroke-width="3">
<g sketch:type="MSLayerGroup" id="Buttons">
<g transform="translate(26.000000, 7.000000)" sketch:type="MSShapeGroup">
<g id="Select" transform="translate(232.000000, 2.000000)">
<rect id="Rectangle-31" stroke-dasharray="7,8,7,8" x="0" y="1" width="29" height="29" rx="4"></rect>
<path d="M6,17 L10,23 L24,0" id="Path-26" stroke-linejoin="round"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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

View File

@ -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
}
}

View File

@ -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'),

View File

@ -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;

View File

@ -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 (
<span key={index} onClick={bind(changedir(path))}>
<i>{slash}</i>{dir}
<i>/</i>{dir}
</span>
);
});
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 (
<span key={directories.length + index} className='history' onClick={bind(changedir(path))}>

View File

@ -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 = <input type='checkbox' id={checkId} checked={this.props.selected} readOnly />;
label = <label htmlFor={checkId}></label>;
}
let clickHandler = this.props.selectView ? this.select.bind(this)
: this.peek.bind(this);
return (
<div className='directory' ref='container'
onClick={this.peek.bind(this)}
onClick={clickHandler}
onContextMenu={this.contextMenu.bind(this)}>
{input}
{label}
<i></i>
<p>{this.props.name}</p>
<span>{this.props.children} items</span>
@ -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));
}
}

View File

@ -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 <File key={index} index={index} name={file.name} size={file.size} />;
return <File selectView={selectView} selected={selected} key={index} index={index} name={file.name} size={file.size} />;
} else {
return <Directory key={index} index={index} name={file.name} children={file.children} />
return <Directory selectView={selectView} selected={selected} key={index} index={index} name={file.name} children={file.children} />
}
});
@ -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')
}
}

View File

@ -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 = <input type='checkbox' id={checkId} defaultChecked={this.props.selected} readOnly />;
label = <label htmlFor={checkId}></label>;
}
let clickHandler = this.props.selectView ? this.select.bind(this)
: null;
return (
<div className='file' ref='container'
onClick={clickHandler}
onContextMenu={this.contextMenu.bind(this)}>
{input}
{label}
<i></i>
<p>{this.props.name}</p>
<span>{humanSize(this.props.size)}</span>
@ -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));
}
}

View File

@ -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 {
<FileMenu />
<DirectoryMenu />
<MoreMenu />
<RenameDialog />
<DeleteDialog />
@ -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());
}

View File

@ -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 {
<button className='icon-plus' onClick={this.newFile} />
<button className='icon-view' onClick={bind(toggleView())} />
<button className='icon-refresh' onClick={bind(refresh())} />
<button className='icon-share' onClick={this.share} />
<button className='icon-more' onClick={this.showMore} />
<button className='icon-select' onClick={bind(selectView('toggle'))} />
<button className='icon-more' onClick={this.showMore.bind(this)} ref='more' />
</div>
);
}
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() {

View File

@ -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'
}

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -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;
}

View File

@ -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'),
});
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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;
}

View File

@ -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",