+
@@ -43,19 +45,46 @@ export default class Root extends Component {
+
+
+
);
}
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
+ }
+ })
+ }
+ }
}
}
diff --git a/src/js/components/spinner.js b/src/js/components/spinner.js
new file mode 100644
index 0000000..ef23903
--- /dev/null
+++ b/src/js/components/spinner.js
@@ -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 (
+
+ );
+ }
+}
+
+function props(state) {
+ return {
+ active: state.get('spinner')
+ }
+}
diff --git a/src/js/components/toolbar.js b/src/js/components/toolbar.js
index 4c39f66..d5786a1 100644
--- a/src/js/components/toolbar.js
+++ b/src/js/components/toolbar.js
@@ -10,7 +10,7 @@ export default class Toolbar extends Component {
return (
-
+
diff --git a/src/js/dialogs.js b/src/js/dialogs.js
index f44b6b1..378e853 100644
--- a/src/js/dialogs.js
+++ b/src/js/dialogs.js
@@ -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());
+ }
+ }
+ ]
}
}
diff --git a/src/js/menus.js b/src/js/menus.js
index ebcba28..a6b4231 100644
--- a/src/js/menus.js
+++ b/src/js/menus.js
@@ -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());
}
}
]
diff --git a/src/js/reducers/all.js b/src/js/reducers/all.js
index e6d231c..6954ea3 100644
--- a/src/js/reducers/all.js
+++ b/src/js/reducers/all.js
@@ -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')
});
}
diff --git a/src/js/reducers/cwd.js b/src/js/reducers/cwd.js
index ae358b1..306e52c 100644
--- a/src/js/reducers/cwd.js
+++ b/src/js/reducers/cwd.js
@@ -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) {
diff --git a/src/js/reducers/files.js b/src/js/reducers/files.js
index dae28bd..2bb4ff2 100644
--- a/src/js/reducers/files.js
+++ b/src/js/reducers/files.js
@@ -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
}
diff --git a/src/js/reducers/search.js b/src/js/reducers/search.js
new file mode 100644
index 0000000..20d24cf
--- /dev/null
+++ b/src/js/reducers/search.js
@@ -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);
+}
diff --git a/src/js/reducers/spinner.js b/src/js/reducers/spinner.js
new file mode 100644
index 0000000..3046b9f
--- /dev/null
+++ b/src/js/reducers/spinner.js
@@ -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;
+}
diff --git a/src/js/store.js b/src/js/store.js
index 734db73..6073c51 100644
--- a/src/js/store.js
+++ b/src/js/store.js
@@ -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')));
diff --git a/src/less/components/all.less b/src/less/components/all.less
index cec920f..564e48d 100644
--- a/src/less/components/all.less
+++ b/src/less/components/all.less
@@ -6,3 +6,4 @@
@import 'breadcrumb';
@import 'file-list';
@import 'dialog';
+@import 'spinner';
diff --git a/src/less/components/breadcrumb.less b/src/less/components/breadcrumb.less
index b93b56c..acf11f3 100644
--- a/src/less/components/breadcrumb.less
+++ b/src/less/components/breadcrumb.less
@@ -4,7 +4,9 @@
align-items: center;
width: 100vw;
- height: 3.5rem;
+ height: 4.5rem;
+
+ overflow-x: auto;
padding: 8px;
@@ -16,6 +18,14 @@
border-bottom: 1px solid @dark-transparent;
+ overflow-x: scroll;
+ overflow-y: hidden;
+ white-space: nowrap;
+
+ span {
+ white-space: nowrap;
+ }
+
i {
margin: 0 2px;
}
diff --git a/src/less/components/dialog.less b/src/less/components/dialog.less
index b5d63e9..3b5991e 100644
--- a/src/less/components/dialog.less
+++ b/src/less/components/dialog.less
@@ -8,8 +8,7 @@
transform: translate(-50%, -50%);
-
- width: 335px;
+ width: 90vw;
height: auto;
padding: 1.5rem 1.7rem;
@@ -18,9 +17,9 @@
.shadow-16;
- z-index: 3;
+ z-index: 5;
- transition: opacity 0.5s ease;
+ transition: opacity 0.3s ease;
opacity: 0;
pointer-events: none;
diff --git a/src/less/components/entries.less b/src/less/components/entries.less
index 9138148..9e8d46c 100644
--- a/src/less/components/entries.less
+++ b/src/less/components/entries.less
@@ -14,10 +14,14 @@
p {
flex: 1 1;
+
+ text-overflow: ellipsis;
}
> span {
.thin-small;
+
+ margin-left: 1rem;
}
i {
diff --git a/src/less/components/file-list.less b/src/less/components/file-list.less
index 5077233..d7fb3a8 100644
--- a/src/less/components/file-list.less
+++ b/src/less/components/file-list.less
@@ -1,5 +1,5 @@
.file-list {
- height: ~'calc(100vh - 13.5rem)';
+ height: ~'calc(100vh - 14.5rem)';
overflow-x: hidden;
overflow-y: auto;
diff --git a/src/less/components/header.less b/src/less/components/header.less
index c53e601..434aa7b 100644
--- a/src/less/components/header.less
+++ b/src/less/components/header.less
@@ -15,6 +15,12 @@ header {
h1 {
margin-left: -3rem;
+
+ flex: 1;
+ }
+
+ i {
+ margin-right: 16px;
}
button {
diff --git a/src/less/components/menu.less b/src/less/components/menu.less
index 4e4a5dc..db164f2 100644
--- a/src/less/components/menu.less
+++ b/src/less/components/menu.less
@@ -13,7 +13,7 @@
opacity: 0;
- transition: opacity 0.5s ease;
+ transition: opacity 0.3s ease;
.shadow-8;
@@ -39,5 +39,11 @@
&:last-of-type {
border-bottom: none;
}
+
+ &.disabled {
+ color: @overlay;
+
+ pointer-events: none;
+ }
}
}
diff --git a/src/less/components/navigation.less b/src/less/components/navigation.less
index 5d88f93..8c7244b 100644
--- a/src/less/components/navigation.less
+++ b/src/less/components/navigation.less
@@ -9,20 +9,28 @@ nav {
width: 70vw;
height: 100vh;
+ overflow-y: auto;
+
background: @dark;
color: white;
- box-shadow: 3px 0 16px 5px @dark-transparent;
+ box-shadow: 3px 0 16px 5px transparent,
+ 0 0 0 1000px rgba(0, 0, 0, 0);
z-index: 6;
- transition: left 0.5s ease;
+ transition: left 0.3s ease, box-shadow 0.3s ease;
+
+ ul:last-of-type li:last-of-type {
+ margin-bottom: 13px;
+ }
&.active {
left: 0;
+ box-shadow: 3px 0 16px 5px @dark-transparent,
+ 0 0 0 1000px rgba(0, 0, 0, 0.55);
i {
pointer-events: all;
- opacity: 0.99;
}
}
@@ -39,6 +47,8 @@ nav {
}
li {
+ display: flex;
+
.light-medium;
padding: 1rem 0 1rem 3rem;
@@ -52,26 +62,27 @@ nav {
padding-bottom: 0;
border-bottom: none;
}
+
+ label {
+ flex: 1;
+ order: 0;
+ }
+
+ input {
+ order: 1;
+ }
}
i {
display: block;
position: fixed;
- left: 0;
+ left: 70vw;
top: 0;
- pointer-events: none;
-
- width: 100vw;
+ width: 30vw;
height: 100vh;
- background: rgba(0, 0, 0, 0.55);
-
- opacity: 0;
-
- z-index: -1;
-
- transition: opacity 0.5s ease;
+ pointer-events: none;
}
}
diff --git a/src/less/components/spinner.less b/src/less/components/spinner.less
new file mode 100644
index 0000000..5965af0
--- /dev/null
+++ b/src/less/components/spinner.less
@@ -0,0 +1,56 @@
+.sk-cube-grid {
+ opacity: 0;
+ pointer-events: none;
+
+ width: 4rem;
+ height: 4rem;
+
+ position: fixed;
+ left: 50%;
+ top: 50%;
+
+ margin-top: -2rem;
+ margin-left: -2rem;
+
+ z-index: 5;
+
+ transition: opacity 0.3s ease;
+
+ &.show {
+ opacity: 1;
+ }
+}
+
+.sk-cube-grid .sk-cube {
+ width: 33.33%;
+ height: 33.33%;
+ background-color: #333;
+ float: left;
+ animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
+}
+.sk-cube-grid .sk-cube1 {
+ animation-delay: 0.2s; }
+.sk-cube-grid .sk-cube2 {
+ animation-delay: 0.3s; }
+.sk-cube-grid .sk-cube3 {
+ animation-delay: 0.4s; }
+.sk-cube-grid .sk-cube4 {
+ animation-delay: 0.1s; }
+.sk-cube-grid .sk-cube5 {
+ animation-delay: 0.2s; }
+.sk-cube-grid .sk-cube6 {
+ animation-delay: 0.3s; }
+.sk-cube-grid .sk-cube7 {
+ animation-delay: 0s; }
+.sk-cube-grid .sk-cube8 {
+ animation-delay: 0.1s; }
+.sk-cube-grid .sk-cube9 {
+ animation-delay: 0.2s; }
+
+@keyframes sk-cubeGridScaleDelay {
+ 0%, 70%, 100% {
+ transform: scale3D(1, 1, 1);
+ } 35% {
+ transform: scale3D(0, 0, 1);
+ }
+}
diff --git a/src/less/icons.less b/src/less/icons.less
index fb69cac..839d764 100644
--- a/src/less/icons.less
+++ b/src/less/icons.less
@@ -50,3 +50,18 @@
width: 6px;
height: 24px;
}
+
+.icon-search {
+ .icon;
+ background: url(/img/Search.svg) no-repeat;
+ width: 19px;
+ height: 27px;
+}
+
+.icon-cross {
+ .icon-plus;
+ transform: rotate(45deg);
+ svg * {
+ fill: white;
+ }
+}
diff --git a/src/less/main.less b/src/less/main.less
index 8662067..034ec93 100644
--- a/src/less/main.less
+++ b/src/less/main.less
@@ -25,3 +25,9 @@ body {
flex-flow: column;
}
+
+a {
+ color: currentColor;
+
+ text-decoration: none;
+}
diff --git a/src/less/styles/all.less b/src/less/styles/all.less
index d5e7a5e..c9a71d8 100644
--- a/src/less/styles/all.less
+++ b/src/less/styles/all.less
@@ -2,3 +2,37 @@
@import 'shadows';
@import 'buttons';
@import 'forms';
+
+.coming-soon::after {
+ content: 'soon...';
+
+ background: @cream;
+
+ color: @dark;
+
+ padding: 2px 8px;
+
+ border-radius: 12px;
+
+ font-size: 11px;
+ font-weight: normal;
+}
+
+li.coming-soon::after {
+ margin-right: 13px;
+ float: right;
+}
+
+button.coming-soon {
+ position: relative;
+}
+
+button.coming-soon::after {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+
+ opacity: 0.8;
+
+ transform: translate(-50%, -50%) rotate(-45deg);
+}
diff --git a/src/less/styles/forms.less b/src/less/styles/forms.less
index 08230bf..b18818c 100644
--- a/src/less/styles/forms.less
+++ b/src/less/styles/forms.less
@@ -14,9 +14,8 @@ input {
.light-medium;
}
-label {
- clear: left;
-
+input[type='checkbox'] + label,
+input[type='radio'] + label {
&::after {
content: '';
display: block;
@@ -36,10 +35,8 @@ label {
}
}
-input[type='checkbox'] {
- clear: right;
- float: right;
-
+input[type='checkbox'],
+input[type='radio'] {
display: none;
}
diff --git a/src/less/variables.less b/src/less/variables.less
index 6e53cb4..0559796 100644
--- a/src/less/variables.less
+++ b/src/less/variables.less
@@ -5,6 +5,7 @@
@gray: #F0F0F0;
@background: #FAFAFA;
@blue: #63B0CD;
+@cream: #F7C59F;
@success: #B8E986;