feat search: search files, depth-first
feat files.view: Open files using Web Activities feat copy/paste: Copy and Paste/Move files fix filters: add "all" filter which clears filters out
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { CREATE_FILE, SHARE_FILE, RENAME_FILE, ACTIVE_FILE, DELETE_FILE } from 'actions/types';
|
||||
import { CREATE_FILE, SHARE_FILE, RENAME_FILE, ACTIVE_FILE, DELETE_FILE, MOVE_FILE, COPY_FILE } from 'actions/types';
|
||||
|
||||
export function create(path, directory = false) {
|
||||
return {
|
||||
@ -21,6 +21,20 @@ export function rename(file, name) {
|
||||
}
|
||||
}
|
||||
|
||||
export function move(file, newPath) {
|
||||
return {
|
||||
type: MOVE_FILE,
|
||||
file, newPath
|
||||
}
|
||||
}
|
||||
|
||||
export function copy(file, newPath) {
|
||||
return {
|
||||
type: COPY_FILE,
|
||||
file, newPath
|
||||
}
|
||||
}
|
||||
|
||||
export function active(file = null) {
|
||||
return {
|
||||
type: ACTIVE_FILE,
|
||||
@ -28,8 +42,7 @@ export function active(file = null) {
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteFile(file) {
|
||||
console.log('constructing deleteFile action', file);
|
||||
export function remove(file) {
|
||||
return {
|
||||
type: DELETE_FILE,
|
||||
file
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { LIST_FILES, FILES_VIEW, SELECT_VIEW, REFRESH } from 'actions/types';
|
||||
import { LIST_FILES, FILES_VIEW, SELECT_VIEW, REFRESH, SEARCH } from 'actions/types';
|
||||
import store from 'store';
|
||||
|
||||
export function listFiles(files) {
|
||||
return {
|
||||
type: LIST_FILES,
|
||||
files
|
||||
};
|
||||
}
|
||||
|
||||
export function refresh() {
|
||||
return {
|
||||
type: REFRESH
|
||||
@ -34,3 +41,10 @@ export function selectView(active = true) {
|
||||
active
|
||||
}
|
||||
}
|
||||
|
||||
export function search(keywords) {
|
||||
return {
|
||||
type: SEARCH,
|
||||
keywords
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { LIST_FILES } from 'actions/types';
|
||||
|
||||
export default function listFiles(files) {
|
||||
return {
|
||||
type: LIST_FILES,
|
||||
files
|
||||
};
|
||||
}
|
22
src/js/actions/spinner.js
Normal file
22
src/js/actions/spinner.js
Normal file
@ -0,0 +1,22 @@
|
||||
import { SPINNER } from 'actions/types';
|
||||
|
||||
export function show() {
|
||||
return {
|
||||
type: SPINNER,
|
||||
active: true
|
||||
}
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
return {
|
||||
type: SPINNER,
|
||||
active: false
|
||||
}
|
||||
}
|
||||
|
||||
export function toggle() {
|
||||
return {
|
||||
type: SPINNER,
|
||||
active: 'toggle'
|
||||
}
|
||||
}
|
@ -16,11 +16,15 @@ const TYPES = {
|
||||
RENAME_FILE: Symbol('RENAME_FILE'),
|
||||
ACTIVE_FILE: Symbol('ACTIVE_FILE'),
|
||||
DELETE_FILE: Symbol('DELETE_FILE'),
|
||||
COPY_FILE: Symbol('COPY_FILE'),
|
||||
MOVE_FILE: Symbol('MOVE_FILE'),
|
||||
|
||||
MENU: Symbol('MENU'),
|
||||
|
||||
DIALOG: Symbol('DIALOG'),
|
||||
|
||||
SPINNER: Symbol('SPINNER'),
|
||||
|
||||
SETTINGS: Symbol('SETTINGS'),
|
||||
|
||||
SEARCH: Symbol('SEARCH')
|
||||
|
@ -75,16 +75,25 @@ export async function createDirectory(...args) {
|
||||
return parent.createDirectory(...args);
|
||||
}
|
||||
|
||||
export async function remove(file) {
|
||||
export async function remove(file, deep) {
|
||||
let parent = await root();
|
||||
|
||||
return parent.remove(file);
|
||||
console.log(deep);
|
||||
return parent[deep ? 'removeDeep' : 'remove'](file);
|
||||
}
|
||||
|
||||
export async function move(file, newPath) {
|
||||
let path = (file.path || '').replace(/^\//, ''); // remove starting slash
|
||||
let oldPath = path + file.name;
|
||||
|
||||
let process = await copy(file, newPath);
|
||||
return remove(oldPath, true);
|
||||
}
|
||||
|
||||
export async function copy(file, newPath) {
|
||||
let path = (file.path || '').replace(/^\//, ''); // remove starting slash
|
||||
let oldPath = path + file.name;
|
||||
|
||||
newPath = newPath.replace(/^\//, '');
|
||||
|
||||
let target = await getFile(oldPath);
|
||||
@ -102,7 +111,6 @@ export async function move(file, newPath) {
|
||||
await move(child, newPath + '/' + child.name);
|
||||
}
|
||||
|
||||
await parent.remove(oldPath);
|
||||
return;
|
||||
} else {
|
||||
let content = await readFile(oldPath);
|
||||
@ -114,6 +122,6 @@ export async function move(file, newPath) {
|
||||
request.onsuccess = resolve;
|
||||
request.onerror = reject;
|
||||
request.onabort = reject;
|
||||
}).then(() => sdcard().delete(oldPath));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,9 @@ export default class Breadcrumb extends Component {
|
||||
|
||||
return (
|
||||
<div className='breadcrumb'>
|
||||
{els}
|
||||
<div>
|
||||
{els}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -4,10 +4,16 @@ import { show } from 'actions/menu';
|
||||
import { active } from 'actions/file';
|
||||
import { MENU_WIDTH } from './menu';
|
||||
import store from 'store';
|
||||
import entry from './mixins/entry';
|
||||
|
||||
const MENU_TOP_SPACE = 20;
|
||||
|
||||
export default class Directory extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
Object.assign(this, entry);
|
||||
}
|
||||
|
||||
render() {
|
||||
let checkId = `file-${this.props.index}`;
|
||||
|
||||
@ -40,28 +46,4 @@ export default class Directory extends Component {
|
||||
|
||||
store.dispatch(changedir(file.path.slice(1) + file.name));
|
||||
}
|
||||
|
||||
contextMenu(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let rect = React.findDOMNode(this.refs.container).getBoundingClientRect();
|
||||
let {x, y, width, height} = rect;
|
||||
|
||||
let left = x + width / 2 - MENU_WIDTH / 2,
|
||||
top = y + height / 2 + MENU_TOP_SPACE;
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import File from './file';
|
||||
import Directory from './directory';
|
||||
import store from 'store';
|
||||
import { type } from 'utils';
|
||||
import Hammer from 'hammerjs';
|
||||
import changedir from 'actions/changedir';
|
||||
|
||||
@connect(props)
|
||||
export default class FileList extends Component {
|
||||
@ -17,7 +19,7 @@ export default class FileList extends Component {
|
||||
let settings = store.getState().get('settings');
|
||||
|
||||
let els = files.map((file, index) => {
|
||||
let selected = activeFile.indexOf(index) > -1;
|
||||
let selected = activeFile.length && activeFile.indexOf(file) > -1;
|
||||
if (type(file) === 'File') {
|
||||
return <File selectView={selectView} selected={selected} key={index} index={index} name={file.name} size={file.size} />;
|
||||
} else {
|
||||
@ -26,11 +28,25 @@ export default class FileList extends Component {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='file-list'>
|
||||
<div className='file-list' ref='container'>
|
||||
{els}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let container = React.findDOMNode(this.refs.container);
|
||||
let touch = Hammer(container);
|
||||
|
||||
touch.on('swipe', e => {
|
||||
let current = store.getState().get('cwd');
|
||||
let up = current.split('/').slice(0, -1).join('/');
|
||||
|
||||
if (up === current) return;
|
||||
|
||||
store.dispatch(changedir(up));
|
||||
}).set({direction: Hammer.DIRECTION_RIGHT});
|
||||
}
|
||||
}
|
||||
|
||||
function props(state) {
|
||||
|
@ -4,12 +4,14 @@ import { active } from 'actions/file';
|
||||
import { MENU_WIDTH } from './menu';
|
||||
import store from 'store';
|
||||
import { humanSize } from 'utils';
|
||||
import entry from './mixins/entry';
|
||||
|
||||
const MENU_TOP_SPACE = 20;
|
||||
|
||||
export default class File extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
Object.assign(this, entry);
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -22,7 +24,7 @@ export default class File extends Component {
|
||||
}
|
||||
|
||||
let clickHandler = this.props.selectView ? this.select.bind(this)
|
||||
: null;
|
||||
: this.open.bind(this);
|
||||
|
||||
return (
|
||||
<div className='file' ref='container'
|
||||
@ -39,27 +41,16 @@ export default class File extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
contextMenu(e) {
|
||||
e.preventDefault();
|
||||
open(e) {
|
||||
let file = store.getState().get('files')[this.props.index];
|
||||
|
||||
let rect = React.findDOMNode(this.refs.container).getBoundingClientRect();
|
||||
let {x, y, width, height} = rect;
|
||||
|
||||
let left = x + width / 2 - MENU_WIDTH / 2,
|
||||
top = y + height / 2 + MENU_TOP_SPACE;
|
||||
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));
|
||||
let name = file.type === 'application/pdf' ? 'view' : 'open';
|
||||
new MozActivity({
|
||||
name,
|
||||
data: {
|
||||
type: file.type,
|
||||
blob: file
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,34 @@
|
||||
import React, { Component } from 'react';
|
||||
import { toggle } from 'actions/navigation';
|
||||
import store from 'store';
|
||||
import { show } from 'actions/dialog';
|
||||
import { search } from 'actions/files-view';
|
||||
import { bind } from 'store';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
@connect(props)
|
||||
export default class Header extends Component {
|
||||
render() {
|
||||
let i;
|
||||
|
||||
if (this.props.search) {
|
||||
i = <i className='icon-cross' onClick={bind(search())} />
|
||||
} else {
|
||||
i = <i className='icon-search' onClick={bind(show('searchDialog'))} />
|
||||
}
|
||||
|
||||
return (
|
||||
<header>
|
||||
<button className='drawer' onClick={this.toggleNavigation.bind(this)}></button>
|
||||
<button className='drawer' onTouchStart={bind(toggle())} />
|
||||
<h1 className='regular-medium'>Hawk</h1>
|
||||
|
||||
{i}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toggleNavigation() {
|
||||
store.dispatch(toggle());
|
||||
function props(state) {
|
||||
return {
|
||||
search: state.get('search')
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,10 @@ export default class Menu extends Component {
|
||||
items = items || [];
|
||||
|
||||
let els = items.map((item, index) => {
|
||||
return <li key={index} onClick={item.action.bind(this)}>{item.name}</li>
|
||||
let disabled = !(typeof item.enabled === 'function' ? item.enabled() : true)
|
||||
let className = disabled ? 'disabled' : '';
|
||||
|
||||
return <li key={index} className={className} onClick={item.action.bind(this)}>{item.name}</li>
|
||||
});
|
||||
let className = 'menu ' + (active ? 'active' : '');
|
||||
|
||||
|
26
src/js/components/mixins/entry.js
Normal file
26
src/js/components/mixins/entry.js
Normal file
@ -0,0 +1,26 @@
|
||||
export default {
|
||||
contextMenu(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let file = store.getState().get('files')[this.props.index];
|
||||
let rect = React.findDOMNode(this.refs.container).getBoundingClientRect();
|
||||
let {x, y, width, height} = rect;
|
||||
|
||||
let left = x + width / 2 - MENU_WIDTH / 2,
|
||||
top = y + height / 2 + MENU_TOP_SPACE;
|
||||
store.dispatch(show('fileMenu', {style: {left, top}}));
|
||||
store.dispatch(active([file]));
|
||||
},
|
||||
|
||||
select() {
|
||||
let current = store.getState().get('activeFile').slice(0);
|
||||
let file = store.getState().get('files')[this.props.index];
|
||||
|
||||
if (current.indexOf(file) > -1) {
|
||||
current.splice(current.indexOf(file), 1);
|
||||
} else {
|
||||
current.push(file)
|
||||
}
|
||||
store.dispatch(active(current));
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { hide } from 'actions/navigation';
|
||||
import { hide as hideNavigation } from 'actions/navigation';
|
||||
import camelCase from 'lodash/string/camelCase';
|
||||
import updateSettings from 'actions/settings';
|
||||
import store from 'store';
|
||||
import store, { bind } from 'store';
|
||||
|
||||
@connect(props)
|
||||
export default class Navigation extends Component {
|
||||
@ -11,23 +11,38 @@ export default class Navigation extends Component {
|
||||
let { settings } = this.props;
|
||||
|
||||
return (
|
||||
<nav className={this.props.active ? 'active' : ''}>
|
||||
<i onClick={this.hide.bind(this)} />
|
||||
<nav className={this.props.active ? 'active' : ''} onChange={this.onChange.bind(this)}>
|
||||
<i onTouchStart={this.hide} />
|
||||
|
||||
<p>Filter</p>
|
||||
<ul>
|
||||
<li>Picture</li>
|
||||
<li>Video</li>
|
||||
<li>Audio</li>
|
||||
<li>
|
||||
<input id='filter-all' name='filter' value='' type='radio' defaultChecked={!settings.filter} />
|
||||
<label htmlFor='filter-all'>All</label>
|
||||
</li>
|
||||
<li>
|
||||
<input id='filter-image' name='filter' value='image' type='radio' defaultChecked={settings.filter === 'image'} />
|
||||
<label htmlFor='filter-image'>Image</label>
|
||||
</li>
|
||||
<li>
|
||||
<input id='filter-video' name='filter' value='video' type='radio' defaultChecked={settings.filter === 'video'} />
|
||||
<label htmlFor='filter-video'>Video</label>
|
||||
</li>
|
||||
<li>
|
||||
<input id='filter-audio' name='filter' value='audio' type='radio' defaultChecked={settings.filter === 'audio'} />
|
||||
<label htmlFor='filter-audio'>Audio</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Tools</p>
|
||||
<ul>
|
||||
<li>FTP Browser</li>
|
||||
<li className='coming-soon'>
|
||||
<label>FTP Browser</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Preferences</p>
|
||||
<ul onChange={this.onChange.bind(this)}>
|
||||
<ul>
|
||||
<li>
|
||||
<input type='checkbox' id='showHiddenFiles' defaultChecked={settings.showHiddenFiles} />
|
||||
<label htmlFor='showHiddenFiles'>Show Hidden Files</label>
|
||||
@ -36,28 +51,47 @@ export default class Navigation extends Component {
|
||||
<input id='showDirectoriesFirst' type='checkbox' defaultChecked={settings.showDirectoriesFirst} />
|
||||
<label htmlFor='showDirectoriesFirst'>Show Directories First</label>
|
||||
</li>
|
||||
<li>Advanced Preferences</li>
|
||||
<li className='coming-soon'>
|
||||
<label>Advanced Preferences</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>External</p>
|
||||
<ul>
|
||||
<li>
|
||||
<label><a href='https://github.com/mdibaiee/Hawk'>GitHub</a></label>
|
||||
</li>
|
||||
<li>
|
||||
<label><a href='https://github.com/mdibaiee/Hawk/issues'>Report Bugs</a></label>
|
||||
</li>
|
||||
<li>
|
||||
<label><a href='http://dibaiee.ir/Hawk'>Website</a></label>
|
||||
</li>
|
||||
<li>
|
||||
<label><a href='http://dibaiee.ir'>Mahdi Dibaiee</a></label>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.props.dispatch(hide());
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
if (e.target.nodeName.toLowerCase() !== 'input') return;
|
||||
|
||||
let key = e.target.id;
|
||||
let value = this.props.settings[key];
|
||||
let key = e.target.name || e.target.id;
|
||||
let value = e.target.value === undefined ? e.target.checked : e.target.value;
|
||||
|
||||
let action = updateSettings({
|
||||
[key]: e.target.checked
|
||||
[key]: value
|
||||
});
|
||||
|
||||
store.dispatch(action);
|
||||
}
|
||||
|
||||
hide(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
store.dispatch(hideNavigation());
|
||||
}
|
||||
}
|
||||
|
||||
function props(store) {
|
||||
|
@ -6,6 +6,7 @@ import Breadcrumb from 'components/breadcrumb';
|
||||
import Toolbar from 'components/toolbar';
|
||||
import Menu from 'components/menu';
|
||||
import Dialog from 'components/dialog';
|
||||
import Spinner from 'components/spinner';
|
||||
import { connect } from 'react-redux';
|
||||
import { hideAll as hideAllMenus } from 'actions/menu';
|
||||
import { hideAll as hideAllDialogs} from 'actions/dialog';
|
||||
@ -24,11 +25,12 @@ let RenameDialog = connect(state => state.get('renameDialog'))(Dialog);
|
||||
let DeleteDialog = connect(state => state.get('deleteDialog'))(Dialog);
|
||||
let ErrorDialog = connect(state => state.get('errorDialog'))(Dialog);
|
||||
let CreateDialog = connect(state => state.get('createDialog'))(Dialog);
|
||||
let SearchDialog = connect(state => state.get('searchDialog'))(Dialog);
|
||||
|
||||
export default class Root extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div onTouchStart={this.touchStart.bind(this)}>
|
||||
<div onTouchStart={this.touchStart.bind(this)} onClick={this.onClick.bind(this)}>
|
||||
<Header />
|
||||
<Breadcrumb />
|
||||
<Navigation />
|
||||
@ -43,19 +45,46 @@ export default class Root extends Component {
|
||||
<DeleteDialog />
|
||||
<ErrorDialog />
|
||||
<CreateDialog />
|
||||
<SearchDialog />
|
||||
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
touchStart(e) {
|
||||
let active = document.querySelector('.active');
|
||||
let inside = e.target.closest('.menu') || e.target.closest('.dialog');
|
||||
if (!inside && active) {
|
||||
let inside = e.target.closest('.active');
|
||||
if (active && !inside) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
store.dispatch(hideAllMenus());
|
||||
store.dispatch(hideAllDialogs());
|
||||
}
|
||||
|
||||
if (document.querySelector('.sk-cube-grid.show')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
onClick(e) {
|
||||
let tag = e.target.nodeName.toLowerCase();
|
||||
if (tag === 'a') {
|
||||
let url = new URL(e.target.href);
|
||||
|
||||
if (url.origin !== location.origin) {
|
||||
e.preventDefault();
|
||||
new MozActivity({
|
||||
name: 'view',
|
||||
|
||||
data: {
|
||||
type: 'url',
|
||||
url: e.target.href
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
28
src/js/components/spinner.js
Normal file
28
src/js/components/spinner.js
Normal file
@ -0,0 +1,28 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
@connect(props)
|
||||
export default class Spinner extends Component {
|
||||
render() {
|
||||
let className = 'sk-cube-grid ' + (this.props.active ? ' show' : '');
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="sk-cube sk-cube1"></div>
|
||||
<div className="sk-cube sk-cube2"></div>
|
||||
<div className="sk-cube sk-cube3"></div>
|
||||
<div className="sk-cube sk-cube4"></div>
|
||||
<div className="sk-cube sk-cube5"></div>
|
||||
<div className="sk-cube sk-cube6"></div>
|
||||
<div className="sk-cube sk-cube7"></div>
|
||||
<div className="sk-cube sk-cube8"></div>
|
||||
<div className="sk-cube sk-cube9"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function props(state) {
|
||||
return {
|
||||
active: state.get('spinner')
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ export default class Toolbar extends Component {
|
||||
return (
|
||||
<div className='toolbar'>
|
||||
<button className='icon-plus' onClick={this.newFile} />
|
||||
<button className='icon-view' onClick={bind(toggleView())} />
|
||||
<button className='icon-view coming-soon' onClick={bind(toggleView())} />
|
||||
<button className='icon-refresh' onClick={bind(refresh())} />
|
||||
<button className='icon-select' onClick={bind(selectView('toggle'))} />
|
||||
<button className='icon-more' onClick={this.showMore.bind(this)} ref='more' />
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { hide, hideAll } from 'actions/dialog';
|
||||
import { rename, deleteFile, create, active } from 'actions/file';
|
||||
import { rename, remove, create, active } from 'actions/file';
|
||||
import { search } from 'actions/files-view';
|
||||
import store, { bind } from 'store';
|
||||
|
||||
export default {
|
||||
@ -70,7 +71,7 @@ export default {
|
||||
text: 'Yes',
|
||||
action() {
|
||||
let activeFile = store.getState().get('activeFile');
|
||||
this.props.dispatch(deleteFile(activeFile));
|
||||
this.props.dispatch(remove(activeFile));
|
||||
this.props.dispatch(hideAll());
|
||||
this.props.dispatch(active());
|
||||
},
|
||||
@ -84,5 +85,26 @@ export default {
|
||||
text: 'Continue',
|
||||
action: bind(hideAll())
|
||||
}]
|
||||
},
|
||||
searchDialog: {
|
||||
title: 'Search',
|
||||
description: 'Enter keywords to search for',
|
||||
input: true,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
action: bind(hideAll())
|
||||
},
|
||||
{
|
||||
text: 'Search',
|
||||
action() {
|
||||
let input = React.findDOMNode(this.refs.input);
|
||||
|
||||
let action = search(input.value);
|
||||
this.props.dispatch(action);
|
||||
this.props.dispatch(hideAll());
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { hideAll } from 'actions/menu';
|
||||
import { show } from 'actions/dialog';
|
||||
import { selectView } from 'actions/files-view';
|
||||
import { copy, move } from 'actions/file';
|
||||
import store from 'store';
|
||||
|
||||
const entryMenu = {
|
||||
@ -9,8 +11,7 @@ const entryMenu = {
|
||||
action() {
|
||||
let files = store.getState().get('files');
|
||||
let active = store.getState().get('activeFile');
|
||||
const name = files[active].name;
|
||||
const description = `Are you sure you want to remove ${name}?`;
|
||||
const description = `Enter the new name for ${active[0].name}?`;
|
||||
|
||||
store.dispatch(hideAll());
|
||||
store.dispatch(show('renameDialog', {description}));
|
||||
@ -21,11 +22,17 @@ const entryMenu = {
|
||||
action() {
|
||||
let files = store.getState().get('files');
|
||||
let active = store.getState().get('activeFile');
|
||||
const name = files[active].name;
|
||||
const description = `Are you sure you want to remove ${name}?`;
|
||||
const description = `Are you sure you want to remove ${active[0].name}?`;
|
||||
store.dispatch(hideAll());
|
||||
store.dispatch(show('deleteDialog', {description}));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Copy',
|
||||
action() {
|
||||
store.dispatch(selectView(false));
|
||||
store.dispatch(hideAll());
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@ -39,16 +46,55 @@ const moreMenu = {
|
||||
let active = store.getState().get('activeFile');
|
||||
|
||||
let description;
|
||||
if (active.length) {
|
||||
if (active.length > 1) {
|
||||
const count = active.length;
|
||||
description = `Are you sure you want to remove ${count} files?`;
|
||||
} else {
|
||||
const name = files[active].name;
|
||||
const name = active[0].name;
|
||||
description = `Are you sure you want to remove ${name}?`;
|
||||
}
|
||||
|
||||
store.dispatch(hideAll());
|
||||
store.dispatch(show('deleteDialog', {description}));
|
||||
},
|
||||
enabled() {
|
||||
return store.getState().get('activeFile');
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Copy',
|
||||
action() {
|
||||
store.dispatch(selectView(false));
|
||||
store.dispatch(hideAll());
|
||||
},
|
||||
enabled() {
|
||||
return store.getState().get('activeFile');
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Paste',
|
||||
enabled() {
|
||||
return store.getState().get('activeFile');
|
||||
},
|
||||
action() {
|
||||
let active = store.getState().get('activeFile');
|
||||
let cwd = store.getState().get('cwd');
|
||||
|
||||
store.dispatch(copy(active, cwd));
|
||||
store.dispatch(hideAll());
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Move',
|
||||
enabled() {
|
||||
return store.getState().get('activeFile');
|
||||
},
|
||||
action() {
|
||||
let active = store.getState().get('activeFile');
|
||||
let cwd = store.getState().get('cwd');
|
||||
|
||||
store.dispatch(move(active, cwd));
|
||||
store.dispatch(hideAll());
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -8,6 +8,8 @@ import menu from './menu';
|
||||
import dialog from './dialog';
|
||||
import settings from './settings';
|
||||
import selectView from './select-view';
|
||||
import spinner from './spinner';
|
||||
import search from './search';
|
||||
|
||||
export default function(state = new Immutable.Map(), action) {
|
||||
console.log('action', action);
|
||||
@ -15,6 +17,8 @@ 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),
|
||||
search: search(state.get('search'), action),
|
||||
spinner: spinner(state.get('spinner'), action),
|
||||
selectView: selectView(state.get('selectView'), action),
|
||||
activeFile: activeFile(state.get('activeFile'), action),
|
||||
navigation: navigation(state.get('navigation'), action),
|
||||
@ -26,5 +30,6 @@ export default function(state = new Immutable.Map(), action) {
|
||||
deleteDialog: dialog(state, action, 'deleteDialog'),
|
||||
errorDialog: dialog(state, action, 'errorDialog'),
|
||||
createDialog: dialog(state, action, 'createDialog'),
|
||||
searchDialog: dialog(state, action, 'searchDialog')
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { CHANGE_DIRECTORY, REFRESH, SETTINGS } from 'actions/types';
|
||||
import listFiles from 'actions/list-files';
|
||||
import { children } from 'api/files';
|
||||
import store from 'store';
|
||||
import { reportError } from 'utils';
|
||||
import { listFiles } from 'actions/files-view';
|
||||
|
||||
export default function(state = '', action) {
|
||||
if (action.type === CHANGE_DIRECTORY) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { LIST_FILES, RENAME_FILE, DELETE_FILE, CREATE_FILE } from 'actions/types';
|
||||
import { LIST_FILES, RENAME_FILE, DELETE_FILE, CREATE_FILE, MOVE_FILE, COPY_FILE, SEARCH } from 'actions/types';
|
||||
import { refresh } from 'actions/files-view';
|
||||
import { move, remove, sdcard, createFile, createDirectory } from 'api/files';
|
||||
import { move, remove, sdcard, createFile, createDirectory, copy } from 'api/files';
|
||||
import { show } from 'actions/dialog';
|
||||
import store, { bind } from 'store';
|
||||
import { reportError, type } from 'utils';
|
||||
@ -8,8 +8,8 @@ import { reportError, type } from 'utils';
|
||||
let boundRefresh = bind(refresh());
|
||||
|
||||
export default function(state = [], action) {
|
||||
if (action.type === LIST_FILES) {
|
||||
|
||||
if (action.type === LIST_FILES) {
|
||||
let settings = store.getState().get('settings');
|
||||
|
||||
if (settings.showDirectoriesFirst) {
|
||||
@ -22,7 +22,16 @@ export default function(state = [], action) {
|
||||
if (!settings.showHiddenFiles) {
|
||||
action.files = action.files.filter(file => {
|
||||
return file.name[0] !== '.';
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.filter) {
|
||||
action.files = action.files.filter(file => {
|
||||
if (type(file) === 'Directory') return true;
|
||||
|
||||
let fileType = file.type.slice(0, file.type.indexOf('/'));
|
||||
return fileType === settings.filter;
|
||||
});
|
||||
}
|
||||
|
||||
return action.files;
|
||||
@ -36,34 +45,45 @@ export default function(state = [], action) {
|
||||
}
|
||||
|
||||
if (action.type === RENAME_FILE) {
|
||||
let file = state[action.file];
|
||||
let all = Promise.all(action.file.map(file => {
|
||||
return move(file, (file.path || '') + action.name);
|
||||
}));
|
||||
|
||||
move(file, (file.path || '') + action.name).then(boundRefresh, reportError);
|
||||
all.then(boundRefresh, reportError);
|
||||
return state;
|
||||
}
|
||||
|
||||
if (action.type === MOVE_FILE) {
|
||||
let all = Promise.all(action.file.map(file => {
|
||||
return move(file, action.newPath + '/' + file.name);
|
||||
}));
|
||||
|
||||
all.then(boundRefresh, reportError);
|
||||
return state;
|
||||
}
|
||||
|
||||
if (action.type === COPY_FILE) {
|
||||
let all = Promise.all(action.file.map(file => {
|
||||
return copy(file, action.newPath + '/' + file.name);
|
||||
}));
|
||||
|
||||
all.then(boundRefresh, reportError);
|
||||
return state;
|
||||
}
|
||||
|
||||
if (action.type === DELETE_FILE) {
|
||||
let copy = state.slice(0);
|
||||
let all = Promise.all(action.file.map(file => {
|
||||
let path = ((file.path || '') + file.name).replace(/^\//, '');
|
||||
return remove(path, true);
|
||||
}))
|
||||
|
||||
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;
|
||||
all.then(boundRefresh, reportError);
|
||||
return state;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function del(state, index) {
|
||||
let file = state[index];
|
||||
return remove((file.path || '') + '/' + file.name).catch(reportError);
|
||||
function mov(file, newPath) {
|
||||
return
|
||||
}
|
||||
|
50
src/js/reducers/search.js
Normal file
50
src/js/reducers/search.js
Normal file
@ -0,0 +1,50 @@
|
||||
import { SEARCH } from 'actions/types';
|
||||
import store from 'store';
|
||||
import { reportError } from 'utils';
|
||||
import { listFiles } from 'actions/files-view';
|
||||
import { children } from 'api/files';
|
||||
import { type } from 'utils';
|
||||
|
||||
export default function(state = '', action) {
|
||||
if (action.type === SEARCH) {
|
||||
search(action.keywords);
|
||||
|
||||
return action.keywords;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function search(keywords) {
|
||||
if (!keywords) {
|
||||
let cwd = store.getState().get('cwd');
|
||||
console.log(cwd);
|
||||
children(cwd, true).then(files => {
|
||||
store.dispatch(listFiles(files));
|
||||
}, reportError);
|
||||
return '';
|
||||
}
|
||||
let keys = keywords.split(' ');
|
||||
|
||||
// We don't want to show all the currently visible files from the
|
||||
// first iteration
|
||||
let once = true;
|
||||
children('/', true).then(function showResults(files) {
|
||||
if (!store.getState().get('search')) return;
|
||||
|
||||
let current = once ? [] : store.getState().get('files');
|
||||
once = false;
|
||||
|
||||
let filtered = files.filter(file => {
|
||||
if (type(file) === 'Directory') {
|
||||
let path = (file.path + file.name).replace(/^\//, '');
|
||||
children(path, true).then(showResults, reportError);
|
||||
}
|
||||
return keys.some(key => {
|
||||
return file.name.indexOf(key) > -1;
|
||||
});
|
||||
});
|
||||
|
||||
store.dispatch(listFiles(current.concat(filtered)));
|
||||
}, reportError);
|
||||
}
|
21
src/js/reducers/spinner.js
Normal file
21
src/js/reducers/spinner.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { SPINNER, CHANGE_DIRECTORY, LIST_FILES, REFRESH, DIALOG, CREATE_FILE, DELETE_FILE } from 'actions/types';
|
||||
|
||||
export default function(state = false, action) {
|
||||
if (action.type === SPINNER) {
|
||||
return action.active === 'toggle' ? !state : action.active;
|
||||
}
|
||||
|
||||
if (action.type === DIALOG && action.id === 'errorDialog') {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case CHANGE_DIRECTORY:
|
||||
case REFRESH:
|
||||
return true;
|
||||
case LIST_FILES:
|
||||
return false;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
@ -10,10 +10,11 @@ const DEFAULT = new Immutable.Map(Object.assign({
|
||||
}, dialogs, menus));
|
||||
|
||||
let store = createStore(reducers, DEFAULT);
|
||||
store.dispatch(changedir(DEFAULT.get('dir')));
|
||||
|
||||
export function bind(action) {
|
||||
return () => store.dispatch(action);
|
||||
}
|
||||
|
||||
export default store;
|
||||
|
||||
store.dispatch(changedir(DEFAULT.get('dir')));
|
||||
|
Reference in New Issue
Block a user