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:
Mahdi Dibaiee 2015-09-06 15:53:48 +04:30
parent 764554c6b9
commit 7f6884cea8
50 changed files with 9157 additions and 5534 deletions

View File

@ -18,14 +18,15 @@ Please read the Features section below and issues to make sure your issue is not
- [x] File Size
- [x] Directory Child Count
- [x] Actions on multiple files (selection)
- [ ] Copy/Cut and Paste files
- [ ] File Preview
- [ ] Filter Files
- [ ] Search
- [x] Copy and Paste/Move files
- [x] File Preview
- [x] Filter Files
- [x] Swipe Gestures (Up directory by swiping right)
- [x] Search
- [ ] Intro
- [ ] Different views (List, Icons, etc)
- [ ] Share Files
- [ ] Preferences
- [ ] FTP Browser
- [ ] File Type Icons
- [ ] Swipe Gestures
- [ ] Wi-Fi File Transfer (is this possible?)

BIN
build/img/Search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

14
build/img/Search.svg Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="19px" height="27px" viewBox="0 0 19 27" 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>Search</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="Header" sketch:type="MSArtboardGroup" transform="translate(-327.000000, -12.000000)" fill="#FAFAFA">
<g id="Search" sketch:type="MSLayerGroup" transform="translate(327.000000, 12.000000)">
<path d="M9.4369836,17.9549819 C13.9937934,17.9549819 17.6878176,14.2609577 17.6878176,9.70414786 C17.6878176,5.14733806 13.9937934,1.45331385 9.4369836,1.45331385 C4.8801738,1.45331385 1.18614959,5.14733806 1.18614959,9.70414786 C1.18614959,14.2609577 4.8801738,17.9549819 9.4369836,17.9549819 L9.4369836,17.9549819 L9.4369836,17.9549819 Z M9.4369836,14.9549819 C6.53702805,14.9549819 4.18614959,12.6041034 4.18614959,9.70414786 C4.18614959,6.80419231 6.53702805,4.45331385 9.4369836,4.45331385 C12.3369391,4.45331385 14.6878176,6.80419231 14.6878176,9.70414786 C14.6878176,12.6041034 12.3369391,14.9549819 9.4369836,14.9549819 Z M8.55898442,17.9088177 L6.03959979,24.4720391 C5.64616063,25.4969831 4.50444038,26.0120308 3.47355444,25.6163108 C2.44980588,25.2233305 1.93924273,24.0728244 2.33227833,23.0489317 L4.82760148,16.5483926 C5.91506785,17.2822068 7.18685775,17.7636774 8.55898442,17.9088177 L8.55898442,17.9088177 Z" id="Shape" sketch:type="MSShapeGroup"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>Hawk</title>
<meta name="viewport" content="width=device-width">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<link rel='stylesheet' href='style.css' />
</head>

File diff suppressed because it is too large Load Diff

View File

@ -43,6 +43,22 @@
width: 6px;
height: 24px;
}
.icon-search {
display: block;
background: url(/img/Search.svg) no-repeat;
width: 19px;
height: 27px;
}
.icon-cross {
display: block;
background: url(/img/Plus.svg) no-repeat;
width: 24px;
height: 24px;
transform: rotate(45deg);
}
.icon-cross svg * {
fill: white;
}
.regular-medium {
font-weight: normal;
}
@ -104,10 +120,8 @@ input {
font-weight: 200;
font-size: 1.6rem;
}
label {
clear: left;
}
label::after {
input[type='checkbox'] + label::after,
input[type='radio'] + label::after {
content: '';
display: block;
float: right;
@ -118,14 +132,36 @@ label::after {
background: transparent;
border: 1px solid #9b9b93;
}
input[type='checkbox'] {
clear: right;
float: right;
input[type='checkbox'],
input[type='radio'] {
display: none;
}
input:checked + label::after {
background: #63b0cd;
}
.coming-soon::after {
content: 'soon...';
background: #f7c59f;
color: #39393a;
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);
}
.file,
.directory {
display: flex;
@ -141,11 +177,13 @@ input:checked + label::after {
.file p,
.directory p {
flex: 1 1;
text-overflow: ellipsis;
}
.file > span,
.directory > span {
font-weight: 100;
font-size: 1.5rem;
margin-left: 1rem;
}
.file i,
.directory i {
@ -176,6 +214,10 @@ header {
}
header h1 {
margin-left: -3rem;
flex: 1;
}
header i {
margin-right: 16px;
}
header button {
background: none;
@ -202,7 +244,7 @@ header button::before {
border-radius: 4px;
pointer-events: none;
opacity: 0;
transition: opacity 0.5s ease;
transition: opacity 0.3s ease;
box-shadow: 0 8px 16px 3px rgba(0, 0, 0, 0.2);
}
.menu.active {
@ -221,6 +263,10 @@ header button::before {
.menu li:last-of-type {
border-bottom: none;
}
.menu li.disabled {
color: #9b9b93;
pointer-events: none;
}
nav {
display: flex;
flex-flow: column;
@ -229,18 +275,22 @@ nav {
top: 0;
width: 70vw;
height: 100vh;
overflow-y: auto;
background: #39393a;
color: white;
box-shadow: 3px 0 16px 5px rgba(0, 0, 0, 0.2);
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;
}
nav ul:last-of-type li:last-of-type {
margin-bottom: 13px;
}
nav.active {
left: 0;
box-shadow: 3px 0 16px 5px rgba(0, 0, 0, 0.2), 0 0 0 1000px rgba(0, 0, 0, 0.55);
}
nav.active i {
pointer-events: all;
opacity: 0.99;
}
nav p {
margin-left: 1.6rem;
@ -252,6 +302,7 @@ nav ul {
padding-left: 0;
}
nav li {
display: flex;
font-weight: 200;
font-size: 1.6rem;
padding: 1rem 0 1rem 3rem;
@ -264,18 +315,21 @@ nav li:last-of-type {
padding-bottom: 0;
border-bottom: none;
}
nav li label {
flex: 1;
order: 0;
}
nav li input {
order: 1;
}
nav 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;
}
.toolbar {
display: flex;
@ -293,13 +347,20 @@ nav i {
flex: 1;
align-items: center;
width: 100vw;
height: 3.5rem;
height: 4.5rem;
overflow-x: auto;
padding: 8px;
box-sizing: border-box;
font-weight: 200;
font-size: 1.6rem;
background: #f8f8f8;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
}
.breadcrumb span {
white-space: nowrap;
}
.breadcrumb i {
margin: 0 2px;
@ -308,7 +369,7 @@ nav i {
color: #9b9b93;
}
.file-list {
height: calc(100vh - 13.5rem);
height: calc(100vh - 14.5rem);
overflow-x: hidden;
overflow-y: auto;
}
@ -319,13 +380,13 @@ nav i {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 335px;
width: 90vw;
height: auto;
padding: 1.5rem 1.7rem;
background: white;
box-shadow: 0 15px 24px 6px rgba(0, 0, 0, 0.2);
z-index: 3;
transition: opacity 0.5s ease;
z-index: 5;
transition: opacity 0.3s ease;
opacity: 0;
pointer-events: none;
}
@ -356,6 +417,66 @@ nav i {
.dialog .foot button:last-of-type {
margin-right: 0;
}
.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;
}
.sk-cube-grid.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);
}
}
html,
body {
margin: 0;
@ -372,3 +493,7 @@ body {
display: flex;
flex-flow: column;
}
a {
color: currentColor;
text-decoration: none;
}

Binary file not shown.

View File

@ -43,6 +43,8 @@
"grunt-contrib-watch": "^0.6.1",
"grunt-fxos": "^0.1.2",
"grunt-task-loader": "^0.6.0",
"hammerjs": "^2.0.4",
"immutable": "^3.7.5",
"less-plugin-clean-css": "^1.5.1",
"lodash": "^3.10.1",
"react": "^0.13.3",

BIN
src/img/Search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

14
src/img/Search.svg Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="19px" height="27px" viewBox="0 0 19 27" 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>Search</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="Header" sketch:type="MSArtboardGroup" transform="translate(-327.000000, -12.000000)" fill="#FAFAFA">
<g id="Search" sketch:type="MSLayerGroup" transform="translate(327.000000, 12.000000)">
<path d="M9.4369836,17.9549819 C13.9937934,17.9549819 17.6878176,14.2609577 17.6878176,9.70414786 C17.6878176,5.14733806 13.9937934,1.45331385 9.4369836,1.45331385 C4.8801738,1.45331385 1.18614959,5.14733806 1.18614959,9.70414786 C1.18614959,14.2609577 4.8801738,17.9549819 9.4369836,17.9549819 L9.4369836,17.9549819 L9.4369836,17.9549819 Z M9.4369836,14.9549819 C6.53702805,14.9549819 4.18614959,12.6041034 4.18614959,9.70414786 C4.18614959,6.80419231 6.53702805,4.45331385 9.4369836,4.45331385 C12.3369391,4.45331385 14.6878176,6.80419231 14.6878176,9.70414786 C14.6878176,12.6041034 12.3369391,14.9549819 9.4369836,14.9549819 Z M8.55898442,17.9088177 L6.03959979,24.4720391 C5.64616063,25.4969831 4.50444038,26.0120308 3.47355444,25.6163108 C2.44980588,25.2233305 1.93924273,24.0728244 2.33227833,23.0489317 L4.82760148,16.5483926 C5.91506785,17.2822068 7.18685775,17.7636774 8.55898442,17.9088177 L8.55898442,17.9088177 Z" id="Shape" sketch:type="MSShapeGroup"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>Hawk</title>
<meta name="viewport" content="width=device-width">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<link rel='stylesheet' href='style.css' />
</head>

View File

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

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

View File

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

View File

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

View File

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

View File

@ -40,7 +40,9 @@ export default class Breadcrumb extends Component {
return (
<div className='breadcrumb'>
{els}
<div>
{els}
</div>
</div>
);
}

View File

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

View File

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

View File

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

View 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')
}
}

View File

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

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

View File

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

View File

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

View 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')
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -6,3 +6,4 @@
@import 'breadcrumb';
@import 'file-list';
@import 'dialog';
@import 'spinner';

View File

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

View File

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

View File

@ -14,10 +14,14 @@
p {
flex: 1 1;
text-overflow: ellipsis;
}
> span {
.thin-small;
margin-left: 1rem;
}
i {

View File

@ -1,5 +1,5 @@
.file-list {
height: ~'calc(100vh - 13.5rem)';
height: ~'calc(100vh - 14.5rem)';
overflow-x: hidden;
overflow-y: auto;

View File

@ -15,6 +15,12 @@ header {
h1 {
margin-left: -3rem;
flex: 1;
}
i {
margin-right: 16px;
}
button {

View File

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

View File

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

View File

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

View File

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

View File

@ -25,3 +25,9 @@ body {
flex-flow: column;
}
a {
color: currentColor;
text-decoration: none;
}

View File

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

View File

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

View File

@ -5,6 +5,7 @@
@gray: #F0F0F0;
@background: #FAFAFA;
@blue: #63B0CD;
@cream: #F7C59F;
@success: #B8E986;