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:
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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'),
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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))}>
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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'
|
||||
}
|
||||
|
1571
src/js/libs/l10n.js
1571
src/js/libs/l10n.js
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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'),
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
9
src/js/reducers/select-view.js
Normal file
9
src/js/reducers/select-view.js
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user