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:
parent
764554c6b9
commit
7f6884cea8
11
README.md
11
README.md
@ -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
BIN
build/img/Search.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 690 B |
14
build/img/Search.svg
Normal file
14
build/img/Search.svg
Normal 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 |
@ -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>
|
||||
|
13644
build/main.js
13644
build/main.js
File diff suppressed because it is too large
Load Diff
171
build/style.css
171
build/style.css
@ -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.
@ -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
BIN
src/img/Search.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 690 B |
14
src/img/Search.svg
Normal file
14
src/img/Search.svg
Normal 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 |
@ -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>
|
||||
|
@ -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,8 +40,10 @@ export default class Breadcrumb extends Component {
|
||||
|
||||
return (
|
||||
<div className='breadcrumb'>
|
||||
<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));
|
||||
let name = file.type === 'application/pdf' ? 'view' : 'open';
|
||||
new MozActivity({
|
||||
name,
|
||||
data: {
|
||||
type: file.type,
|
||||
blob: file
|
||||
}
|
||||
|
||||
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));
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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')));
|
||||
|
@ -6,3 +6,4 @@
|
||||
@import 'breadcrumb';
|
||||
@import 'file-list';
|
||||
@import 'dialog';
|
||||
@import 'spinner';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -14,10 +14,14 @@
|
||||
|
||||
p {
|
||||
flex: 1 1;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
> span {
|
||||
.thin-small;
|
||||
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
|
@ -1,5 +1,5 @@
|
||||
.file-list {
|
||||
height: ~'calc(100vh - 13.5rem)';
|
||||
height: ~'calc(100vh - 14.5rem)';
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
@ -15,6 +15,12 @@ header {
|
||||
|
||||
h1 {
|
||||
margin-left: -3rem;
|
||||
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
56
src/less/components/spinner.less
Normal file
56
src/less/components/spinner.less
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -25,3 +25,9 @@ body {
|
||||
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
a {
|
||||
color: currentColor;
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
@gray: #F0F0F0;
|
||||
@background: #FAFAFA;
|
||||
@blue: #63B0CD;
|
||||
@cream: #F7C59F;
|
||||
|
||||
@success: #B8E986;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user