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

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