Compare commits
54 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d4d8b81e40 | ||
|
7bcf39bdb7 | ||
|
2eaf2ac1f0 | ||
|
9aa5bcf384 | ||
|
bec675e7ee | ||
|
4253732492 | ||
|
a9c5890c3c | ||
|
f0f6a684a7 | ||
|
629b6f7e61 | ||
|
1833a5e3c1 | ||
|
735ef7fa7b | ||
|
dfb7d8aa72 | ||
|
44340abb61 | ||
|
0018380759 | ||
|
c13315d61e | ||
|
59af3b9e10 | ||
|
be8ae4f0b9 | ||
|
5c5305d243 | ||
|
e449402928 | ||
|
88f8909fcc | ||
|
d22e2e5527 | ||
|
25ff79af90 | ||
|
b3b2ddf4f8 | ||
|
cb30112c40 | ||
|
92b5fa2fee | ||
|
419b4010d1 | ||
|
19f6960a6d | ||
|
ccf24e513b | ||
|
a88ff826e7 | ||
|
11672c58f0 | ||
|
31a873d2bb | ||
|
d0c8c91250 | ||
|
b2b71b5d10 | ||
|
ce00cb25dc | ||
|
427cbca2dc | ||
|
43239b4a4c | ||
|
596799b6f0 | ||
|
19e306712f | ||
|
29fc832287 | ||
|
d52fe9f9bc | ||
|
6e52ca6246 | ||
|
66504df4cb | ||
|
8408fd5319 | ||
|
ceb8cd3b21 | ||
|
8a3c5de65d | ||
|
d925dfb082 | ||
|
2b51a7df09 | ||
|
f06d521bbf | ||
|
59f7991c10 | ||
|
336dd01dcb | ||
|
779d890513 | ||
|
d56ea95e9b | ||
|
49eb62ef2f | ||
|
1d6769d4e5 |
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
# DS_Store
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
@ -77,7 +77,7 @@ module.exports = function(grunt) {
|
||||
expand: true,
|
||||
cwd: 'src',
|
||||
dest: 'build',
|
||||
src: ['index.html', 'manifest.webapp',
|
||||
src: ['index.html', 'manifest.webapp', 'polyfill.js',
|
||||
'fonts/**', 'img/**', 'js/libs/**', 'icon/**']
|
||||
}]
|
||||
}
|
||||
@ -99,7 +99,7 @@ module.exports = function(grunt) {
|
||||
tasks: ['browserify:dev']
|
||||
},
|
||||
assets: {
|
||||
files: ['src/index.html', 'src/manifest.webapp',
|
||||
files: ['src/index.html', 'src/manifest.webapp', 'src/polyfill.js',
|
||||
'src/fonts/**', 'src/img/**', 'src/data/**'],
|
||||
tasks: ['copy']
|
||||
}
|
||||
|
31
README.md
@ -7,11 +7,31 @@ Please read the Features section below and issues to make sure your issue is not
|
||||
|
||||
Firefox OS 2.2 and up are supported. Sadly 2.0 and 1.3 miss a lot of ES6 functionalities as well as CSS3 features (flexbox, etc) which break our application.
|
||||
|
||||
![Mobile Portrait Mockup](https://github.com/mdibaiee/Hawk/raw/master/Mobile%20Portrait.png)
|
||||
<p align='center'>
|
||||
<a href='https://github.com/mdibaiee/Hawk/raw/master/Mobile%20Portrait.png'>
|
||||
<img src='https://github.com/mdibaiee/Hawk/raw/master/Mobile%20Portrait.png' width='300px' />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
#Thanks to
|
||||
# Thanks to
|
||||
|
||||
Sergio Muriel [@tfeserver](https://twitter.com/tfeserver) for testing the application and helping me fix issues. ❤️🙏
|
||||
|
||||
Mohammad Jahani [@mamal72](https://twitter.com/mamal72) for ideas, and helping in designing the [webpage](http://dibaiee.ir/Hawk)
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
**Q: Why does Hawk create an `.empty` file inside new folders I create?**
|
||||
|
||||
This happens on Firefox OS devices below version 3, and that's because the API doesn't allow
|
||||
listing empty folders, in order to show you the folder, Hawk has to fake the folder to have a child.
|
||||
|
||||
|
||||
**Q: Why is Hawk slow?**
|
||||
|
||||
Hawk is much faster on Firefox OS 3.0 and up, and that's because the way old Device Storage API works,
|
||||
it's slow by nature. Nothing we can do about it, sadly.
|
||||
|
||||
Sergio Muriel [@tfeserver](https://twitter.com/tfeserver) for testing application
|
||||
|
||||
# Features
|
||||
|
||||
@ -40,9 +60,10 @@ Version 1.0
|
||||
|
||||
Version 2.0
|
||||
------------
|
||||
- [ ] Different views (List, Icons, etc)
|
||||
- [x] Different views (List, Grid)
|
||||
- [ ] Show storage usage statistics (free/used)
|
||||
- [ ] Sort Files
|
||||
- [ ] Zip / Unzip
|
||||
- [x] Zip / Unzip
|
||||
- [ ] Image Thumbnails
|
||||
- [ ] FTP Browser
|
||||
- [ ] Preferences
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 753 B After Width: | Height: | Size: 796 B |
BIN
build/icon/Icon-340.png
Normal file
After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 58 KiB |
16
build/img/Back.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="15px" height="24px" viewBox="0 0 15 24" 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>Back</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="Toolbar" sketch:type="MSArtboardGroup" transform="translate(-26.000000, -14.000000)" stroke="#63B0CD" fill="#63B0CD">
|
||||
<g sketch:type="MSLayerGroup" id="Buttons">
|
||||
<g transform="translate(26.000000, 7.000000)" sketch:type="MSShapeGroup">
|
||||
<path d="M7.57710348,12.9048675 C7.04180408,12.7876066 6.46141311,12.9389926 6.04498016,13.3554255 L-2.89216446,22.2925701 C-3.53902314,22.9394288 -3.5418885,23.9953224 -2.89240043,24.6448105 C-2.23838425,25.2988266 -1.18977849,25.2941929 -0.540160097,24.6445745 L7.22721202,16.8772024 L14.9945841,24.6445745 C15.6414428,25.2914332 16.6973364,25.2942985 17.3468245,24.6448105 C18.0008407,23.9907943 17.9962069,22.9421885 17.3465885,22.2925701 L8.40944389,13.3554255 C8.17328851,13.1192701 7.88261547,12.9689485 7.57710348,12.9048675 Z" id="Back" transform="translate(7.228579, 19.000000) rotate(-90.000000) translate(-7.228579, -19.000000) "></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
3
build/img/Close.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30">
|
||||
<path fill="#fff" d="m 17.402,14.33 4.023,-4.022 C 21.117,9.142 20.695,8.076 20.185,7.132 L 14.515,12.8 8.83,7.115 C 8.32,8.057 7.895,9.121 7.586,10.285 l 4.044,4.046 c 0.006,0.01 0.015,0.02 0.02,0.02 0.33,0.33 0.34,0.86 0.033,1.2 l 0.003,0.01 -4.1,4.1 c 0.31,1.166 0.734,2.228 1.244,3.168 l 5.686,-5.685 5.668,5.668 c 0.51,-0.94 0.933,-2.004 1.24,-3.17 l -4.08,-4.076 0.005,-0.004 c -0.31,-0.343 -0.3,-0.87 0.03,-1.2 l 0.02,-0.017 z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 510 B |
3
build/img/Menu.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30">
|
||||
<path fill="#fff" d="M 10.993,9 H 19 C 19.415,9 19.758,8.275 19.758,7.446 19.758,6.617 19.415,6 19,6 H 10.993 C 10.578,6 10.249,6.617 10.249,7.446 10.249,8.275 10.57,9 10.99,9 z M 19,14 h -8.007 c -0.415,0 -0.744,0.625 -0.744,1.454 0,0.829 0.33,1.5 0.75,1.5 h 8 c 0.41,0 0.75,-0.67 0.75,-1.5 C 19.749,14.624 19.41,14 19,14 z m 0,8 h -8.007 c -0.415,0 -0.744,0.633 -0.744,1.462 0,0.829 0.321,1.538 0.741,1.538 H 19 c 0.415,0 0.758,-0.71 0.758,-1.538 C 19.758,22.634 19.415,22 19,22 z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 579 B |
@ -1,14 +1,3 @@
|
||||
<?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 xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30">
|
||||
<path fill="#fff" d="m 24.98,21.794 -3.973,-4.248 c 0.788,-1.312 1.25,-2.847 1.25,-4.495 0,-4.79 -3.836,-8.67 -8.568,-8.67 -4.732,0 -8.57,3.88 -8.57,8.67 0,4.8 3.83,8.68 8.57,8.68 1.61,0 3.12,-0.46 4.41,-1.25 l 4.212,4.014 c 0.372,0.378 1.27,0.08 2.004,-0.66 0.734,-0.74 1.03,-1.654 0.654,-2.032 z M 13.69,19.31 c -3.414,0 -6.182,-2.803 -6.182,-6.26 0,-3.457 2.768,-6.258 6.182,-6.258 3.414,0 6.18,2.802 6.18,6.26 0,3.455 -2.766,6.258 -6.18,6.258 z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 545 B |
@ -3,14 +3,15 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hawk</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
|
||||
<link rel='stylesheet' href='style.css' />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1"/>
|
||||
<meta name="theme-color" content="#39393A"/>
|
||||
<link rel='stylesheet' href='style.css'/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id='wrapper'></div>
|
||||
|
||||
<script src='polyfill.js'></script>
|
||||
<script src='main.js'></script>
|
||||
</body>
|
||||
</html>
|
||||
|
32045
build/main.js
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"version": "1.2.0",
|
||||
"name": "Hawk",
|
||||
"description": "Keep an eye on your files with a full-featured file manager",
|
||||
"launch_path": "/index.html",
|
||||
@ -30,6 +30,9 @@
|
||||
"device-storage:music": {
|
||||
"access": "readwrite",
|
||||
"description": "We need access to your files in order to give you the functionality of listing, reading, opening and writing files in your storage"
|
||||
},
|
||||
"tcp-socket": {
|
||||
"description": "FTP Browser: Used to connect to FTP servers"
|
||||
}
|
||||
},
|
||||
"installs_allowed_from": [
|
||||
@ -41,9 +44,6 @@
|
||||
"pick": {
|
||||
"href": "./index.html",
|
||||
"disposition": "inline",
|
||||
"filters": {
|
||||
"type": "*"
|
||||
},
|
||||
"returnValue": true
|
||||
}
|
||||
}
|
||||
|
85
build/polyfill.js
Normal file
164
build/style.css
@ -1,6 +1,12 @@
|
||||
.icon {
|
||||
display: block;
|
||||
}
|
||||
.icon-menu {
|
||||
display: block;
|
||||
background: url(/img/Menu.svg) no-repeat;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.icon-directory {
|
||||
display: block;
|
||||
background: url(/img/Directory.svg) no-repeat;
|
||||
@ -46,18 +52,20 @@
|
||||
.icon-search {
|
||||
display: block;
|
||||
background: url(/img/Search.svg) no-repeat;
|
||||
width: 19px;
|
||||
height: 27px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.icon-cross {
|
||||
display: block;
|
||||
background: url(/img/Plus.svg) no-repeat;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transform: rotate(45deg);
|
||||
background: url(/img/Close.svg) no-repeat;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.icon-cross svg * {
|
||||
fill: white;
|
||||
.icon-back {
|
||||
display: block;
|
||||
background: url(/img/Back.svg) no-repeat;
|
||||
width: 15px;
|
||||
height: 24px;
|
||||
}
|
||||
.regular-medium {
|
||||
font-weight: normal;
|
||||
@ -209,7 +217,21 @@ input:checked + label::after {
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 5;
|
||||
}
|
||||
.tour-dialog {
|
||||
.tour #skip-tour {
|
||||
font-size: 2rem;
|
||||
display: block;
|
||||
padding: 0.5rem 5rem;
|
||||
margin: 1rem auto;
|
||||
background: #b8e986;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 65%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 15px 24px 6px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
.tour-dialog,
|
||||
#skip-tour {
|
||||
display: none;
|
||||
}
|
||||
@keyframes pulse {
|
||||
@ -260,34 +282,62 @@ button.coming-soon::after {
|
||||
opacity: 0.8;
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
.list .file,
|
||||
.list .directory {
|
||||
flex: 1 1 100%;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.list .file p,
|
||||
.list .directory p {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
.list .file > span,
|
||||
.list .directory > span {
|
||||
font-weight: 100;
|
||||
font-size: 1.5rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.list .file i,
|
||||
.list .directory i {
|
||||
margin-right: 1.4rem;
|
||||
}
|
||||
.grid .file,
|
||||
.grid .directory {
|
||||
flex: 1 0 33.33%;
|
||||
max-width: 33.33%;
|
||||
padding: 1.4rem 0.5rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
.grid .file p,
|
||||
.grid .directory p {
|
||||
max-height: 1.5em;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.grid .file span,
|
||||
.grid .directory span {
|
||||
display: none;
|
||||
}
|
||||
.file,
|
||||
.directory {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
padding: 1.4rem;
|
||||
width: 100%;
|
||||
font-weight: 200;
|
||||
font-size: 1.8rem;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.file p,
|
||||
.directory p {
|
||||
flex: 1 1;
|
||||
max-width: calc(100% - 9rem);
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.file > span,
|
||||
.directory > span {
|
||||
font-weight: 100;
|
||||
font-size: 1.5rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.file i,
|
||||
.directory i {
|
||||
margin-right: 1.4rem;
|
||||
.file:active,
|
||||
.directory:active {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.directory i {
|
||||
display: block;
|
||||
@ -313,27 +363,22 @@ header {
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
header h1 {
|
||||
margin-left: -3rem;
|
||||
font-size: 2.3rem;
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
header i {
|
||||
margin-right: 16px;
|
||||
}
|
||||
header button {
|
||||
background: none;
|
||||
border: none;
|
||||
width: 8rem;
|
||||
height: 4rem;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
padding-top: 1rem;
|
||||
margin-top: -1rem;
|
||||
}
|
||||
header button::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 2rem;
|
||||
height: 4px;
|
||||
margin-top: -9px;
|
||||
border-radius: 4px;
|
||||
background: #9b9b93;
|
||||
box-shadow: 0 7px 0 #9b9b93, 0 14px 0 #9b9b93;
|
||||
header button i {
|
||||
background-position: center;
|
||||
}
|
||||
.menu {
|
||||
width: 24.5rem;
|
||||
@ -367,6 +412,9 @@ header button::before {
|
||||
color: #9b9b93;
|
||||
pointer-events: none;
|
||||
}
|
||||
.menu li:active {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
nav {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
@ -442,6 +490,11 @@ nav i {
|
||||
box-sizing: border-box;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
.toolbar button {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
background-position: center center;
|
||||
}
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@ -449,26 +502,50 @@ nav i {
|
||||
width: 100vw;
|
||||
height: 4.5rem;
|
||||
overflow-x: auto;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
font-weight: 200;
|
||||
font-size: 1.6rem;
|
||||
padding-right: 8px;
|
||||
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 div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.breadcrumb i {
|
||||
margin: 0 2px;
|
||||
.breadcrumb span {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 4.5rem;
|
||||
white-space: nowrap;
|
||||
padding: 0 5px 0 30px;
|
||||
background: #f0f0f0;
|
||||
filter: drop-shadow(1px 0 0 rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
.breadcrumb span:first-of-type {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.breadcrumb span::after {
|
||||
position: absolute;
|
||||
right: -46px;
|
||||
top: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
border: 23px solid transparent;
|
||||
border-left-color: #f0f0f0;
|
||||
}
|
||||
.breadcrumb span.history {
|
||||
color: #9b9b93;
|
||||
}
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
height: calc(100vh - 14.5rem);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
@ -507,6 +584,7 @@ nav i {
|
||||
.dialog .foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.dialog .foot button {
|
||||
flex: 1;
|
||||
|
BIN
design/.DS_Store
vendored
Normal file
14
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hawk",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@ -25,7 +25,14 @@
|
||||
"node": ">=0.12.0"
|
||||
},
|
||||
"homepage": "https://github.com/mdibaiee/",
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"jszip": "2.5.0",
|
||||
"mime": "1.3.4",
|
||||
"react": "15.0.0",
|
||||
"react-dom": "15.0.0",
|
||||
"react-hammerjs": "0.4.5",
|
||||
"react-redux": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel": "^5.8.23",
|
||||
"babelify": "^6.2.0",
|
||||
@ -36,14 +43,13 @@
|
||||
"grunt-contrib-copy": "^0.8.1",
|
||||
"grunt-contrib-less": "^1.0.1",
|
||||
"grunt-contrib-watch": "^0.6.1",
|
||||
"grunt-fxos": "^0.1.2",
|
||||
"grunt-task-loader": "^0.6.0",
|
||||
"grunt-zip": "^0.17.0",
|
||||
"hammerjs": "^2.0.4",
|
||||
"immutable": "^3.7.5",
|
||||
"less-plugin-clean-css": "^1.5.1",
|
||||
"lodash": "^3.10.1",
|
||||
"react": "^0.13.3",
|
||||
"react": "^15.0.0",
|
||||
"react-redux": "^1.0.1",
|
||||
"redux": "^1.0.1",
|
||||
"redux-devtools": "^1.1.2",
|
||||
|
BIN
releases/hawk-1.1.0.zip
Normal file
BIN
releases/hawk-1.1.1.zip
Normal file
BIN
releases/hawk-1.1.2.zip
Normal file
BIN
releases/hawk-1.2.0.zip
Normal file
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 753 B After Width: | Height: | Size: 796 B |
BIN
src/icon/Icon-340.png
Normal file
After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 58 KiB |
16
src/img/Back.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="15px" height="24px" viewBox="0 0 15 24" 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>Back</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="Toolbar" sketch:type="MSArtboardGroup" transform="translate(-26.000000, -14.000000)" stroke="#63B0CD" fill="#63B0CD">
|
||||
<g sketch:type="MSLayerGroup" id="Buttons">
|
||||
<g transform="translate(26.000000, 7.000000)" sketch:type="MSShapeGroup">
|
||||
<path d="M7.57710348,12.9048675 C7.04180408,12.7876066 6.46141311,12.9389926 6.04498016,13.3554255 L-2.89216446,22.2925701 C-3.53902314,22.9394288 -3.5418885,23.9953224 -2.89240043,24.6448105 C-2.23838425,25.2988266 -1.18977849,25.2941929 -0.540160097,24.6445745 L7.22721202,16.8772024 L14.9945841,24.6445745 C15.6414428,25.2914332 16.6973364,25.2942985 17.3468245,24.6448105 C18.0008407,23.9907943 17.9962069,22.9421885 17.3465885,22.2925701 L8.40944389,13.3554255 C8.17328851,13.1192701 7.88261547,12.9689485 7.57710348,12.9048675 Z" id="Back" transform="translate(7.228579, 19.000000) rotate(-90.000000) translate(-7.228579, -19.000000) "></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
3
src/img/Close.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30">
|
||||
<path fill="#fff" d="m 17.402,14.33 4.023,-4.022 C 21.117,9.142 20.695,8.076 20.185,7.132 L 14.515,12.8 8.83,7.115 C 8.32,8.057 7.895,9.121 7.586,10.285 l 4.044,4.046 c 0.006,0.01 0.015,0.02 0.02,0.02 0.33,0.33 0.34,0.86 0.033,1.2 l 0.003,0.01 -4.1,4.1 c 0.31,1.166 0.734,2.228 1.244,3.168 l 5.686,-5.685 5.668,5.668 c 0.51,-0.94 0.933,-2.004 1.24,-3.17 l -4.08,-4.076 0.005,-0.004 c -0.31,-0.343 -0.3,-0.87 0.03,-1.2 l 0.02,-0.017 z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 510 B |
3
src/img/Menu.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30">
|
||||
<path fill="#fff" d="M 10.993,9 H 19 C 19.415,9 19.758,8.275 19.758,7.446 19.758,6.617 19.415,6 19,6 H 10.993 C 10.578,6 10.249,6.617 10.249,7.446 10.249,8.275 10.57,9 10.99,9 z M 19,14 h -8.007 c -0.415,0 -0.744,0.625 -0.744,1.454 0,0.829 0.33,1.5 0.75,1.5 h 8 c 0.41,0 0.75,-0.67 0.75,-1.5 C 19.749,14.624 19.41,14 19,14 z m 0,8 h -8.007 c -0.415,0 -0.744,0.633 -0.744,1.462 0,0.829 0.321,1.538 0.741,1.538 H 19 c 0.415,0 0.758,-0.71 0.758,-1.538 C 19.758,22.634 19.415,22 19,22 z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 579 B |
@ -1,14 +1,3 @@
|
||||
<?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 xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30">
|
||||
<path fill="#fff" d="m 24.98,21.794 -3.973,-4.248 c 0.788,-1.312 1.25,-2.847 1.25,-4.495 0,-4.79 -3.836,-8.67 -8.568,-8.67 -4.732,0 -8.57,3.88 -8.57,8.67 0,4.8 3.83,8.68 8.57,8.68 1.61,0 3.12,-0.46 4.41,-1.25 l 4.212,4.014 c 0.372,0.378 1.27,0.08 2.004,-0.66 0.734,-0.74 1.03,-1.654 0.654,-2.032 z M 13.69,19.31 c -3.414,0 -6.182,-2.803 -6.182,-6.26 0,-3.457 2.768,-6.258 6.182,-6.258 3.414,0 6.18,2.802 6.18,6.26 0,3.455 -2.766,6.258 -6.18,6.258 z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 545 B |
@ -3,14 +3,15 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hawk</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
|
||||
<link rel='stylesheet' href='style.css' />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1"/>
|
||||
<meta name="theme-color" content="#39393A"/>
|
||||
<link rel='stylesheet' href='style.css'/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id='wrapper'></div>
|
||||
|
||||
<script src='polyfill.js'></script>
|
||||
<script src='main.js'></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -5,5 +5,5 @@ export default function changedir(dir) {
|
||||
return {
|
||||
type: CHANGE_DIRECTORY,
|
||||
dir
|
||||
};
|
||||
}
|
||||
}
|
||||
|
15
src/js/actions/compress.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { COMPRESS, DECOMPRESS } from './types';
|
||||
|
||||
export function compress(file, name) {
|
||||
return {
|
||||
type: COMPRESS,
|
||||
file, name
|
||||
}
|
||||
}
|
||||
|
||||
export function decompress(file) {
|
||||
return {
|
||||
type: DECOMPRESS,
|
||||
file
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { LIST_FILES, FILES_VIEW, SELECT_VIEW, REFRESH, SEARCH } from 'actions/types';
|
||||
import { LIST_FILES, VIEW, SELECT_VIEW, REFRESH, SEARCH } from 'actions/types';
|
||||
import store from 'store';
|
||||
|
||||
export function listFiles(files) {
|
||||
@ -14,27 +14,6 @@ export function refresh() {
|
||||
}
|
||||
}
|
||||
|
||||
export function toggle() {
|
||||
return {
|
||||
type: FILES_VIEW,
|
||||
view: 'toggle'
|
||||
}
|
||||
}
|
||||
|
||||
export function details() {
|
||||
return {
|
||||
type: FILES_VIEW,
|
||||
view: 'details'
|
||||
}
|
||||
}
|
||||
|
||||
export function list() {
|
||||
return {
|
||||
type: FILES_VIEW,
|
||||
view: 'list'
|
||||
}
|
||||
}
|
||||
|
||||
export function selectView(active = true) {
|
||||
return {
|
||||
type: SELECT_VIEW,
|
||||
|
@ -2,7 +2,6 @@ const TYPES = {
|
||||
CHANGE_DIRECTORY: Symbol('CHANGE_DIRECTORY'),
|
||||
|
||||
LIST_FILES: Symbol('LIST_FILES'),
|
||||
FILES_VIEW: Symbol('FILES_VIEW'),
|
||||
SELECT_VIEW: Symbol('SELECT_VIEW'),
|
||||
|
||||
NAVIGATION: Symbol('NAVIGATION'),
|
||||
@ -10,6 +9,9 @@ const TYPES = {
|
||||
REFRESH: Symbol('REFRESH'),
|
||||
SORT: Symbol('SORT'),
|
||||
|
||||
COMPRESS: Symbol('COMPRESS'),
|
||||
DECOMPRESS: Symbol('DECOMPRESS'),
|
||||
|
||||
NEW_FILE: Symbol('NEW_FILE'),
|
||||
CREATE_FILE: Symbol('CREATE_FILE'),
|
||||
SHARE_FILE: Symbol('SHARE_FILE'),
|
||||
|
14
src/js/api/auto.js
Normal file
@ -0,0 +1,14 @@
|
||||
import * as ftp from './ftp';
|
||||
import * as files from './files';
|
||||
|
||||
['getFile', 'children', 'isDirectory', 'readFile', 'writeFile',
|
||||
'createFile', 'createDirectory', 'remove', 'move', 'copy'].forEach(method => {
|
||||
exports[method] = (...args) => {
|
||||
return window.ftpMode ? ftp[method](...args) : files[method](...args);
|
||||
}
|
||||
});
|
||||
|
||||
let CACHE = files.CACHE;
|
||||
let FTP_CACHE = ftp.FTP_CACHE;
|
||||
|
||||
export { CACHE, FTP_CACHE };
|
@ -1,11 +1,18 @@
|
||||
import { type } from 'utils';
|
||||
import { type, normalize } from 'utils';
|
||||
import { refresh } from 'actions/files-view';
|
||||
import { bind } from 'store';
|
||||
|
||||
let SD_CACHE;
|
||||
export let CACHE = {};
|
||||
|
||||
localStorage.setItem('cache', '{}');
|
||||
|
||||
export function sdcard() {
|
||||
if (SD_CACHE) return SD_CACHE;
|
||||
|
||||
SD_CACHE = navigator.getDeviceStorage('sdcard');
|
||||
window.sdcard = SD_CACHE;
|
||||
|
||||
return SD_CACHE;
|
||||
}
|
||||
|
||||
@ -13,7 +20,11 @@ let ROOT_CACHE;
|
||||
export async function root() {
|
||||
if (ROOT_CACHE) return ROOT_CACHE;
|
||||
|
||||
ROOT_CACHE = await sdcard().getRoot();
|
||||
ROOT_CACHE = shimDirectory(await sdcard().getRoot());
|
||||
Object.defineProperty(ROOT_CACHE, 'name', {
|
||||
value: '',
|
||||
enumerable: true
|
||||
});
|
||||
window.root = ROOT_CACHE;
|
||||
return ROOT_CACHE;
|
||||
}
|
||||
@ -23,33 +34,76 @@ export async function getFile(dir = '/') {
|
||||
|
||||
if (dir === '/' || !dir) return parent;
|
||||
|
||||
return await parent.get(dir);
|
||||
let file = await parent.get(normalize(dir));
|
||||
|
||||
Object.defineProperty(file, 'type', {
|
||||
value: type(file),
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
export async function children(dir, gatherInfo) {
|
||||
let parent = await getFile(dir);
|
||||
if (CACHE[dir]) return CACHE[dir];
|
||||
|
||||
let parent = shimDirectory(await getFile(dir));
|
||||
if (!parent.path) {
|
||||
parent.path = dir.slice(0, dir.lastIndexOf('/') + 1);
|
||||
}
|
||||
if (parent.path.endsWith(parent.name)) {
|
||||
Object.defineProperty(parent, 'path', {
|
||||
value: normalize(parent.path.slice(0, -parent.name.length)),
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
|
||||
let childs = await parent.getFilesAndDirectories();
|
||||
|
||||
if (gatherInfo) {
|
||||
for (let child of childs) {
|
||||
if (type(child) === 'Directory') {
|
||||
Object.defineProperty(child, 'type', {
|
||||
value: type(child),
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
if (child.path && child.path.endsWith(child.name)) {
|
||||
Object.defineProperty(child, 'path', {
|
||||
value: normalize(child.path.slice(0, -child.name.length)),
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (gatherInfo && !window.needsShim) {
|
||||
for (let child of childs) {
|
||||
if (child.type === 'Directory') {
|
||||
let subchildren;
|
||||
try {
|
||||
subchildren = await child.getFilesAndDirectories();
|
||||
subchildren = await shimDirectory(child).getFilesAndDirectories();
|
||||
} catch(e) {
|
||||
subchildren = [];
|
||||
}
|
||||
|
||||
child.children = subchildren.length;
|
||||
} else {
|
||||
if (typeof child.path === 'undefined') {
|
||||
child.path = dir + '/';
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
CACHE[dir] = childs;
|
||||
|
||||
return childs;
|
||||
}
|
||||
|
||||
export async function isDirectory(path) {
|
||||
let file = await getFile(path);
|
||||
|
||||
return !(file instanceof Blob);
|
||||
}
|
||||
|
||||
export async function readFile(path) {
|
||||
let file = await getFile(path);
|
||||
|
||||
@ -65,26 +119,77 @@ export async function readFile(path) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function createFile(...args) {
|
||||
let parent = await root();
|
||||
export async function writeFile(path, content) {
|
||||
try {
|
||||
let file = await getFile(path);
|
||||
|
||||
return await parent.createFile(...args);
|
||||
return Promise.reject(new Error('File already exists: ' + path));
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
|
||||
let request = sdcard().addNamed(content, path);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = resolve;
|
||||
request.onerror = reject;
|
||||
request.onabort = reject;
|
||||
});
|
||||
}
|
||||
|
||||
export async function createDirectory(...args) {
|
||||
let parent = await root();
|
||||
export async function createFile(path = '') {
|
||||
const parentPath = path.split('/').slice(0, -1).join('/');
|
||||
let filename = path.slice(path.lastIndexOf('/') + 1);
|
||||
let parent = await getFile(parentPath);
|
||||
|
||||
return parent.createDirectory(...args);
|
||||
if (!parent.createFile) {
|
||||
parent = await root();
|
||||
filename = path;
|
||||
}
|
||||
|
||||
CACHE[parentPath] = null;
|
||||
return await parent.createFile(filename);
|
||||
}
|
||||
|
||||
export async function remove(file, deep) {
|
||||
let parent = await root();
|
||||
export async function createDirectory(path) {
|
||||
const parentPath = path.split('/').slice(0, -1).join('/');
|
||||
let filename = path.slice(path.lastIndexOf('/') + 1);
|
||||
let parent = await getFile(parentPath);
|
||||
|
||||
return parent[deep ? 'removeDeep' : 'remove'](file);
|
||||
if (!parent.createDirectory) {
|
||||
parent = await root();
|
||||
filename = path;
|
||||
}
|
||||
|
||||
CACHE[parentPath] = null;
|
||||
|
||||
return parent.createDirectory(filename).then(() => {
|
||||
if (window.needsShim) {
|
||||
return createFile(path + '/.empty');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function remove(file, deep = true) {
|
||||
// const method = deep ? 'removeDeep' : 'remove';
|
||||
const method = 'removeDeep';
|
||||
let path = normalize(file);
|
||||
const parentPath = path.split('/').slice(0, -1).join('/');
|
||||
let filename = path.slice(path.lastIndexOf('/') + 1);
|
||||
let parent = await getFile(parentPath);
|
||||
|
||||
if (!parent[method]) {
|
||||
parent = await root();
|
||||
filename = path;
|
||||
}
|
||||
|
||||
CACHE[parentPath] = null;
|
||||
|
||||
return parent[method](filename);
|
||||
}
|
||||
|
||||
export async function move(file, newPath) {
|
||||
let path = (file.path || '').replace(/^\//, ''); // remove starting slash
|
||||
let path = normalize(file.path || '');
|
||||
let oldPath = path + file.name;
|
||||
|
||||
let process = await copy(file, newPath);
|
||||
@ -92,21 +197,25 @@ export async function move(file, newPath) {
|
||||
}
|
||||
|
||||
export async function copy(file, newPath) {
|
||||
let path = (file.path || '').replace(/^\//, ''); // remove starting slash
|
||||
let oldPath = path + file.name;
|
||||
let path = normalize(file.path || '').replace(/^\//, '');
|
||||
let oldPath = normalize(path + file.name);
|
||||
|
||||
newPath = newPath.replace(/^\//, '');
|
||||
newPath = normalize(newPath);
|
||||
|
||||
let target = await getFile(oldPath);
|
||||
let parent = await root();
|
||||
|
||||
if (type(target) === 'Directory') {
|
||||
await parent.createDirectory(newPath);
|
||||
let childs = await target.getFilesAndDirectories();
|
||||
let childs = await shimDirectory(target).getFilesAndDirectories();
|
||||
|
||||
for (let child of childs) {
|
||||
if (type(child) === 'File') {
|
||||
child.path = oldPath + '/';
|
||||
Object.defineProperty(child, 'path', {
|
||||
value: oldPath + '/',
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
await copy(child, newPath + '/' + child.name);
|
||||
@ -118,11 +227,6 @@ export async function copy(file, newPath) {
|
||||
|
||||
let blob = new Blob([content], {type: target.type});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = sdcard().addNamed(blob, newPath);
|
||||
request.onsuccess = resolve;
|
||||
request.onerror = reject;
|
||||
request.onabort = reject;
|
||||
});
|
||||
return writeFile(newPath, blob);
|
||||
}
|
||||
}
|
||||
|
338
src/js/api/ftp.js
Normal file
@ -0,0 +1,338 @@
|
||||
import { refresh } from 'actions/files-view';
|
||||
import { bind } from 'store';
|
||||
import EventEmitter from 'events';
|
||||
import { humanSize, reportError, normalize, type, getLength } from 'utils';
|
||||
|
||||
export let FTP_CACHE = {};
|
||||
let socket;
|
||||
let connection = new EventEmitter();
|
||||
connection.setMaxListeners(99);
|
||||
|
||||
export let queue = Object.assign([], EventEmitter.prototype);
|
||||
|
||||
export async function connect(properties = {}) {
|
||||
let { host, port, username, password } = properties;
|
||||
|
||||
let url = encodeURI(host);
|
||||
socket = navigator.mozTCPSocket.open(url, port);
|
||||
|
||||
socket.ondata = e => {
|
||||
console.log('<', e.data);
|
||||
connection.emit('data', e.data);
|
||||
}
|
||||
|
||||
socket.onerror = e => {
|
||||
connection.emit('error', e.data);
|
||||
}
|
||||
|
||||
socket.onclose = e => {
|
||||
connection.emit('close', e.data);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.onopen = () => {
|
||||
send(`USER ${username}`);
|
||||
send(`PASS ${password}`);
|
||||
resolve(socket);
|
||||
|
||||
window.ftpMode = true;
|
||||
}
|
||||
|
||||
socket.onerror = reject;
|
||||
socket.onclose = reject;
|
||||
});
|
||||
}
|
||||
|
||||
export async function disconnect() {
|
||||
socket.close();
|
||||
window.ftpMode = false;
|
||||
}
|
||||
|
||||
export function listen(ev, fn) {
|
||||
socket.listen(ev, fn);
|
||||
}
|
||||
|
||||
export function send(command, ...args) {
|
||||
args = args.filter(arg => arg);
|
||||
let cmd = command + (args.length ? ' ' : '') + args.join(' ');
|
||||
|
||||
console.log('>', cmd);
|
||||
socket.send(cmd + '\n');
|
||||
}
|
||||
|
||||
export async function cwd(dir = '') {
|
||||
send('CWD', dir);
|
||||
}
|
||||
|
||||
const PWD_REGEX = /257 "(.*)"/;
|
||||
export async function pwd() {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.on('data', function listener(data) {
|
||||
if (data.indexOf('current directory') === -1) return;
|
||||
let dir = data.match(PWD_REGEX)[1];
|
||||
resolve(normalize(dir));
|
||||
|
||||
connection.removeListener('data', listener);
|
||||
});
|
||||
|
||||
send('PWD');
|
||||
});
|
||||
}
|
||||
|
||||
export async function pasv() {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.on('data', function listener(data) {
|
||||
if (data.indexOf('Passive') === -1) return;
|
||||
|
||||
// format: |||port|
|
||||
let port = parseInt(data.match(/\|{3}(\d+)\|/)[1]);
|
||||
|
||||
connection.removeListener('data', listener);
|
||||
|
||||
resolve(port);
|
||||
});
|
||||
|
||||
send('EPSV');
|
||||
});
|
||||
}
|
||||
|
||||
const LIST_EXTRACTOR = /(.*?)\s+(\d+)\s+(\w+)\s+(\w+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\d+:?\d+)+\s+(.*)/;
|
||||
export async function list(dir = '') {
|
||||
let index = queue.push(port => {
|
||||
return secondary({ host: socket.host, port }).then(({data}) => {
|
||||
send('LIST', dir);
|
||||
|
||||
return data.then(items => {
|
||||
return items.split('\n').map(item => {
|
||||
if (item.indexOf('total') > -1 || !item) return;
|
||||
|
||||
let match = item.match(LIST_EXTRACTOR);
|
||||
|
||||
return {
|
||||
path: normalize(dir + '/'),
|
||||
type: match[1][0] === 'd' ? 'Directory' : 'File',
|
||||
permissions: match[1].slice(1),
|
||||
links: +match[2],
|
||||
owner: match[3],
|
||||
group: match[4],
|
||||
size: +match[5],
|
||||
lastModification: {
|
||||
month: match[6],
|
||||
day: match[7],
|
||||
time: match[8]
|
||||
},
|
||||
name: match[9]
|
||||
}
|
||||
}).filter(item => item);
|
||||
}, reportError)
|
||||
});
|
||||
});
|
||||
|
||||
return handleQueue(index - 1);
|
||||
}
|
||||
|
||||
export async function namelist(dir = '') {
|
||||
let index = queue.push(port => {
|
||||
return secondary({ host: socket.host, port }).then(({data}) => {
|
||||
send('NLST', dir);
|
||||
|
||||
return data.then(names => names.split('\n'), reportError);
|
||||
});
|
||||
});
|
||||
|
||||
return handleQueue(index - 1);
|
||||
}
|
||||
|
||||
export async function secondary(properties = {}) {
|
||||
let { host, port } = properties;
|
||||
|
||||
let url = encodeURI(host);
|
||||
|
||||
send('TYPE', 'I');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let alt = navigator.mozTCPSocket.open(url, port);
|
||||
|
||||
alt.onopen = e => {
|
||||
let data = new Promise((resolve, reject) => {
|
||||
let d = '';
|
||||
alt.ondata = e => {
|
||||
d += e.data;
|
||||
}
|
||||
alt.onerror = e => {
|
||||
reject(e.data);
|
||||
}
|
||||
alt.onclose = e => {
|
||||
console.log('<<', d);
|
||||
resolve(d);
|
||||
}
|
||||
});
|
||||
resolve({data});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const BUFFER_SIZE = 32000;
|
||||
export async function secondaryWrite(properties = {}, content) {
|
||||
let { host, port } = properties;
|
||||
|
||||
let url = encodeURI(host);
|
||||
|
||||
send('TYPE', 'I');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let alt = navigator.mozTCPSocket.open(url, port);
|
||||
|
||||
alt.onopen = () => {
|
||||
console.log('>>', content);
|
||||
let step = 0;
|
||||
|
||||
(function send() {
|
||||
if (!content) return;
|
||||
|
||||
let chunk = content.slice(0, BUFFER_SIZE);
|
||||
content = content.slice(BUFFER_SIZE);
|
||||
|
||||
if (alt.send(chunk)) {
|
||||
send();
|
||||
} else {
|
||||
alt.ondrain = () => {
|
||||
console.log('drain');
|
||||
send();
|
||||
}
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
alt.onclose = () => {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function children(dir = '', gatherInfo) {
|
||||
dir = normalize(dir);
|
||||
if (FTP_CACHE[dir]) return FTP_CACHE[dir];
|
||||
|
||||
let childs = gatherInfo ? await list(dir) : await namelist(dir);
|
||||
|
||||
FTP_CACHE[dir] = childs;
|
||||
|
||||
return childs;
|
||||
}
|
||||
|
||||
export async function getFile(path = '') {
|
||||
path = normalize(path);
|
||||
|
||||
let ls = await list(path);
|
||||
|
||||
return ls[0];
|
||||
}
|
||||
|
||||
export async function isDirectory(path = '') {
|
||||
return (await getFile(path)).type === 'Directory';
|
||||
}
|
||||
|
||||
export async function readFile(path = '') {
|
||||
path = normalize(path);
|
||||
|
||||
let index = queue.push(port => {
|
||||
return secondary({ host: socket.host, port }).then(({data}) => {
|
||||
send('RETR', path);
|
||||
|
||||
return data;
|
||||
});
|
||||
})
|
||||
|
||||
return handleQueue(index - 1);
|
||||
}
|
||||
|
||||
export async function writeFile(path = '', content) {
|
||||
let index;
|
||||
path = normalize(path);
|
||||
|
||||
if (type(content) === 'Blob') {
|
||||
let reader = new FileReader();
|
||||
|
||||
index = queue.push(port => {
|
||||
return new Promise((resolve, reject) => {
|
||||
reader.addEventListener('loadend', () => {
|
||||
send('TYPE', 'I');
|
||||
send('STOR', path);
|
||||
|
||||
secondaryWrite({ host: socket.host, port }, reader.result)
|
||||
.then(resolve, reject);
|
||||
});
|
||||
|
||||
reader.readAsBinaryString(content);
|
||||
})
|
||||
});
|
||||
} else {
|
||||
index = queue.push(port => {
|
||||
send('STOR', path);
|
||||
return secondaryWrite({ host: socket.host, port }, content);
|
||||
});
|
||||
}
|
||||
|
||||
return handleQueue(index - 1);
|
||||
}
|
||||
|
||||
export async function createFile(path = '') {
|
||||
return writeFile(path, '');
|
||||
}
|
||||
|
||||
export async function createDirectory(path = '') {
|
||||
path = normalize(path);
|
||||
|
||||
send('MKD', path);
|
||||
}
|
||||
|
||||
export async function remove(path = '') {
|
||||
path = normalize(path);
|
||||
|
||||
let ls = await list(path);
|
||||
send('DELE', path);
|
||||
send('DELE', path + '/*.*');
|
||||
send('RMD', path);
|
||||
}
|
||||
|
||||
export async function move(file, newPath = '') {
|
||||
let path = normalize(file.path + file.name);
|
||||
newPath = normalize(newPath);
|
||||
|
||||
send('RNFR', path);
|
||||
send('RNTO', newPath);
|
||||
}
|
||||
|
||||
export async function copy(file, newPath = '') {
|
||||
let path = normalize(file.path + file.name);
|
||||
newPath = normalize(newPath);
|
||||
|
||||
let content = await readFile(path);
|
||||
console.log(content);
|
||||
|
||||
return writeFile(newPath, content);
|
||||
}
|
||||
|
||||
const LOOP_INTERVAL = 100;
|
||||
(function loopQueue() {
|
||||
if (queue.length) {
|
||||
pasv().then(queue[0]).then(result => {
|
||||
queue.emit('done', {listener: queue[0], result});
|
||||
queue.splice(0, 1);
|
||||
loopQueue();
|
||||
});
|
||||
} else {
|
||||
setTimeout(loopQueue, LOOP_INTERVAL);
|
||||
}
|
||||
}());
|
||||
|
||||
async function handleQueue(index) {
|
||||
let fn = queue[index];
|
||||
|
||||
return new Promise(resolve => {
|
||||
queue.on('done', ({listener, result}) => {
|
||||
if (listener === fn) resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
@ -1,56 +1,78 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import changedir from 'actions/changedir';
|
||||
import { bind } from 'store';
|
||||
import Hammer from 'react-hammerjs';
|
||||
|
||||
// TODO: Fix history not working when clicking on sdcard
|
||||
@connect(props)
|
||||
export default class Breadcrumb extends Component {
|
||||
render() {
|
||||
let els = [];
|
||||
|
||||
if (this.props.search) {
|
||||
els = [
|
||||
<span key='000'>Search: {this.props.search}</span>
|
||||
]
|
||||
} else {
|
||||
let directories = this.props.cwd.split('/').filter(a => a);
|
||||
let lastDirectories = this.props.lwd.split('/').filter(a => a);
|
||||
directories.unshift('sdcard');
|
||||
|
||||
let els = directories.map((dir, index, arr) => {
|
||||
let sumLength = directories.length + lastDirectories.length;
|
||||
|
||||
els = els.concat(directories.map((dir, index, arr) => {
|
||||
let path = arr.slice(1, index + 1).join('/');
|
||||
let style = { zIndex: sumLength - index };
|
||||
|
||||
return (
|
||||
<span key={index} onClick={bind(changedir(path))}>
|
||||
<i>/</i>{dir}
|
||||
</span>
|
||||
<Hammer onTap={bind(changedir(path))} key={index}>
|
||||
<span style={style}>{dir}</span>
|
||||
</Hammer>
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
||||
let lastDirectories = this.props.lwd.split('/').filter(a => a);
|
||||
if (lastDirectories.length > directories.length - 1) {
|
||||
lastDirectories.splice(0, directories.length - 1);
|
||||
|
||||
let history = lastDirectories.map((dir, index, arr) => {
|
||||
let current = directories.slice(1).concat(arr.slice(0, index + 1));
|
||||
let path = current.join('/').replace(/^\//, ''); // remove starting slash
|
||||
let key = directories.length + index;
|
||||
let style = { zIndex: arr.length - index};
|
||||
|
||||
return (
|
||||
<span key={directories.length + index} className='history' onClick={bind(changedir(path))}>
|
||||
<i>/</i>{dir}
|
||||
</span>
|
||||
<Hammer onTap={bind(changedir(path))} key={key}>
|
||||
<span className='history' style={style}>{dir}</span>
|
||||
</Hammer>
|
||||
)
|
||||
});
|
||||
|
||||
els = els.concat(history);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='breadcrumb'>
|
||||
<div className='breadcrumb' ref='container'>
|
||||
<div>
|
||||
{els}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
let container = this.refs.container;
|
||||
let currents = container.querySelectorAll('span:not(.history)');
|
||||
|
||||
container.scrollLeft = currents[currents.length - 1].offsetLeft;
|
||||
}
|
||||
}
|
||||
|
||||
function props(state) {
|
||||
return {
|
||||
lwd: state.get('lwd'), // last working directory
|
||||
cwd: state.get('cwd')
|
||||
cwd: state.get('cwd'),
|
||||
search: state.get('search')
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { template } from 'utils';
|
||||
import Hammer from 'react-hammerjs';
|
||||
|
||||
export default class Dialog extends Component {
|
||||
render() {
|
||||
@ -7,12 +9,28 @@ export default class Dialog extends Component {
|
||||
let conditionalInput = input ? <input ref='input' /> : '';
|
||||
|
||||
let buttons = this.props.buttons.map((button, i) => {
|
||||
return <button className={button.className + ' btn'} key={i}
|
||||
onClick={button.action.bind(this)}>
|
||||
return (
|
||||
<Hammer onTap={button.action.bind(this)}>
|
||||
<button className={button.className + ' btn'} key={i}>
|
||||
{button.text}
|
||||
</button>;
|
||||
</button>
|
||||
</Hammer>
|
||||
)
|
||||
});
|
||||
|
||||
let groupButtons = [];
|
||||
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
if (i % 2 === 0) {
|
||||
groupButtons.push(
|
||||
<div className='foot' key={i / 2}>
|
||||
{buttons[i]}
|
||||
{buttons[i+1]}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let className = active ? 'dialog active' : 'dialog';
|
||||
|
||||
return (
|
||||
@ -22,10 +40,16 @@ export default class Dialog extends Component {
|
||||
|
||||
{conditionalInput}
|
||||
|
||||
<div className='foot'>
|
||||
{buttons}
|
||||
</div>
|
||||
{groupButtons}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!this.props.value) return;
|
||||
|
||||
let input = this.refs.input;
|
||||
|
||||
input.value = this.props.value;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import changedir from 'actions/changedir';
|
||||
import store from 'store';
|
||||
import entry from './mixins/entry';
|
||||
import Hammer from 'react-hammerjs';
|
||||
|
||||
export default class Directory extends Component {
|
||||
constructor() {
|
||||
@ -14,7 +16,7 @@ export default class Directory extends Component {
|
||||
|
||||
let input, label;
|
||||
if (this.props.selectView) {
|
||||
input = <input type='checkbox' id={checkId} checked={this.props.selected} readOnly />;
|
||||
input = <input type='checkbox' id={checkId} checked={this.props.selected} readOnly ref='check' />;
|
||||
label = <label htmlFor={checkId}></label>;
|
||||
}
|
||||
|
||||
@ -22,8 +24,8 @@ export default class Directory extends Component {
|
||||
: this.peek.bind(this);
|
||||
|
||||
return (
|
||||
<Hammer onTap={clickHandler}>
|
||||
<div className='directory' ref='container'
|
||||
onClick={clickHandler}
|
||||
onContextMenu={this.contextMenu.bind(this)}>
|
||||
|
||||
{input}
|
||||
@ -31,14 +33,18 @@ export default class Directory extends Component {
|
||||
|
||||
<i></i>
|
||||
<p>{this.props.name}</p>
|
||||
<span>{this.props.children} items</span>
|
||||
<span>{this.props.children ? this.props.children + ' items' : ''}</span>
|
||||
</div>
|
||||
</Hammer>
|
||||
);
|
||||
}
|
||||
|
||||
peek() {
|
||||
if (document.querySelector('#file-menu.active')) return;
|
||||
|
||||
let file = store.getState().get('files')[this.props.index];
|
||||
|
||||
store.dispatch(changedir(file.path.slice(1) + file.name));
|
||||
const path = file.path.endsWith(file.name) ? file.path : file.path + file.name;
|
||||
store.dispatch(changedir(path));
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import File from './file';
|
||||
import Directory from './directory';
|
||||
import store from 'store';
|
||||
import { type } from 'utils';
|
||||
import Hammer from 'hammerjs';
|
||||
import Hammer from 'react-hammerjs';
|
||||
import changedir from 'actions/changedir';
|
||||
|
||||
@connect(props)
|
||||
@ -14,38 +15,37 @@ export default class FileList extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
let { files, selectView, activeFile } = this.props;
|
||||
let { files, selectView, activeFile, view } = this.props;
|
||||
activeFile = activeFile || [];
|
||||
let settings = store.getState().get('settings');
|
||||
|
||||
let els = files.map((file, index) => {
|
||||
let selected = activeFile.indexOf(file) > -1;
|
||||
if (type(file) === 'File') {
|
||||
return <File selectView={selectView} selected={selected} key={index} index={index} name={file.name} size={file.size} />;
|
||||
if (file.type === 'File') {
|
||||
return <File selectView={selectView} selected={selected} key={index} index={index} name={file.name} size={file.size} type={file.type} />;
|
||||
} else {
|
||||
return <Directory selectView={selectView} selected={selected} key={index} index={index} name={file.name} children={file.children} />
|
||||
return <Directory selectView={selectView} selected={selected} key={index} index={index} name={file.name} children={file.children} type={file.type} />
|
||||
}
|
||||
});
|
||||
|
||||
let className= `file-list ${view}`;
|
||||
|
||||
return (
|
||||
<div className='file-list' ref='container'>
|
||||
<Hammer onSwipe={this.swipe} options={{ direction: Hammer.DIRECTION_RIGHT }}>
|
||||
<div className={className} ref='container'>
|
||||
{els}
|
||||
</div>
|
||||
</Hammer>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let container = React.findDOMNode(this.refs.container);
|
||||
let touch = Hammer(container);
|
||||
|
||||
touch.on('swipe', e => {
|
||||
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});
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,13 +53,7 @@ function props(state) {
|
||||
return {
|
||||
files: state.get('files'),
|
||||
selectView: state.get('selectView'),
|
||||
activeFile: state.get('activeFile')
|
||||
activeFile: state.get('activeFile'),
|
||||
view: state.get('settings').view || 'list'
|
||||
}
|
||||
}
|
||||
|
||||
async function getFiles(dir) {
|
||||
let storage = navigator.getDeviceStorage('sdcard');
|
||||
let root = await storage.get(dir);
|
||||
|
||||
return await root.getFilesAndDirectories();
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import store from 'store';
|
||||
import { humanSize } from 'utils';
|
||||
import entry from './mixins/entry';
|
||||
import Hammer from 'react-hammerjs';
|
||||
import mime from 'mime';
|
||||
|
||||
export default class File extends Component {
|
||||
constructor() {
|
||||
@ -14,7 +17,7 @@ export default class File extends Component {
|
||||
|
||||
let input, label;
|
||||
if (this.props.selectView) {
|
||||
input = <input type='checkbox' id={checkId} checked={this.props.selected} readOnly />;
|
||||
input = <input type='checkbox' id={checkId} checked={this.props.selected} readOnly ref='check' />;
|
||||
label = <label htmlFor={checkId}></label>;
|
||||
}
|
||||
|
||||
@ -22,8 +25,8 @@ export default class File extends Component {
|
||||
: this.open.bind(this);
|
||||
|
||||
return (
|
||||
<Hammer onTap={clickHandler}>
|
||||
<div className='file' ref='container'
|
||||
onClick={clickHandler}
|
||||
onContextMenu={this.contextMenu.bind(this)}>
|
||||
|
||||
{input}
|
||||
@ -33,18 +36,22 @@ export default class File extends Component {
|
||||
<p>{this.props.name}</p>
|
||||
<span>{humanSize(this.props.size)}</span>
|
||||
</div>
|
||||
</Hammer>
|
||||
);
|
||||
}
|
||||
|
||||
open(e) {
|
||||
if (document.querySelector('#file-menu.active')) return;
|
||||
|
||||
let file = store.getState().get('files')[this.props.index];
|
||||
|
||||
let name = file.type === 'application/pdf' ? 'view' : 'open';
|
||||
const type = mime.lookup(file.name);
|
||||
let name = type === 'application/pdf' ? 'view' : 'open';
|
||||
new MozActivity({
|
||||
name,
|
||||
data: {
|
||||
type: file.type,
|
||||
blob: file
|
||||
blob: file,
|
||||
type
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { toggle } from 'actions/navigation';
|
||||
import { show } from 'actions/dialog';
|
||||
import { search } from 'actions/files-view';
|
||||
import { bind } from 'store';
|
||||
import { connect } from 'react-redux';
|
||||
import Hammer from 'react-hammerjs';
|
||||
|
||||
@connect(props)
|
||||
export default class Header extends Component {
|
||||
@ -11,15 +13,23 @@ export default class Header extends Component {
|
||||
let i;
|
||||
|
||||
if (this.props.search) {
|
||||
i = <i className='icon-cross' onClick={bind(search())} />
|
||||
i = <Hammer onTap={bind(search())}>
|
||||
<button><i className='icon-cross' /></button>
|
||||
</Hammer>
|
||||
} else {
|
||||
i = <i className='icon-search tour-item' onClick={bind(show('searchDialog'))} />
|
||||
i = <Hammer onTap={bind(show('searchDialog'))}>
|
||||
<button><i className='icon-search tour-item' /></button>
|
||||
</Hammer>
|
||||
}
|
||||
|
||||
return (
|
||||
<header>
|
||||
<button className='drawer tour-item' onTouchStart={bind(toggle())} />
|
||||
<h1 className='regular-medium'>Hawk</h1>
|
||||
<Hammer onTap={bind(toggle())}>
|
||||
<button className='drawer tour-item'>
|
||||
<i className='icon-menu'></i>
|
||||
</button>
|
||||
</Hammer>
|
||||
<h1>Hawk</h1>
|
||||
|
||||
{i}
|
||||
</header>
|
||||
|
@ -1,4 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Hammer from 'react-hammerjs';
|
||||
|
||||
export const MENU_WIDTH = 245;
|
||||
|
||||
@ -11,7 +13,11 @@ export default class Menu extends Component {
|
||||
let enabled = typeof item.enabled === 'function' ? item.enabled() : true
|
||||
let className = enabled ? '' : 'disabled';
|
||||
|
||||
return <li key={index} className={className} onClick={item.action.bind(this)}>{item.name}</li>
|
||||
return (
|
||||
<Hammer key={index} onTap={item.action.bind(this)}>
|
||||
<li className={className}>{item.name}</li>
|
||||
</Hammer>
|
||||
);
|
||||
});
|
||||
let className = 'menu ' + (active ? 'active' : '');
|
||||
|
||||
|
@ -8,15 +8,16 @@ const MENU_TOP_SPACE = 20;
|
||||
export default {
|
||||
contextMenu(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
let file = store.getState().get('files')[this.props.index];
|
||||
let rect = React.findDOMNode(this.refs.container).getBoundingClientRect();
|
||||
let rect = this.refs.container.getBoundingClientRect();
|
||||
let {x, y, width, height} = rect;
|
||||
|
||||
let left = x + width / 2 - MENU_WIDTH / 2,
|
||||
let left = window.innerWidth / 2 - MENU_WIDTH / 2,
|
||||
top = y + height / 2 + MENU_TOP_SPACE;
|
||||
|
||||
let dialogHeight = document.getElementById('fileMenu').offsetHeight;
|
||||
let dialogHeight = document.getElementById('file-menu').offsetHeight;
|
||||
|
||||
let diff = window.innerHeight - (dialogHeight + top);
|
||||
if (diff <= 0) {
|
||||
@ -27,14 +28,20 @@ export default {
|
||||
store.dispatch(active([file]));
|
||||
},
|
||||
|
||||
select() {
|
||||
select(e) {
|
||||
if (document.querySelector('#file-menu.active')) return;
|
||||
|
||||
let current = (store.getState().get('activeFile') || []).slice(0);
|
||||
let file = store.getState().get('files')[this.props.index];
|
||||
|
||||
let check = this.refs.check;
|
||||
|
||||
if (current.indexOf(file) > -1) {
|
||||
current.splice(current.indexOf(file), 1);
|
||||
check.checked = false;
|
||||
} else {
|
||||
current.push(file)
|
||||
check.checked = true;
|
||||
}
|
||||
store.dispatch(active(current));
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { hide as hideNavigation } from 'actions/navigation';
|
||||
import camelCase from 'lodash/string/camelCase';
|
||||
@ -10,8 +11,14 @@ export default class Navigation extends Component {
|
||||
render() {
|
||||
let { settings } = this.props;
|
||||
|
||||
let noFlex = typeof getComputedStyle(document.body)['flex-flow'] === 'undefined';
|
||||
|
||||
let style = noFlex ? {display: 'block'} : {};
|
||||
|
||||
return (
|
||||
<nav className={this.props.active ? 'active' : ''} onChange={this.onChange.bind(this)}>
|
||||
<nav className={this.props.active ? 'active' : ''}
|
||||
onChange={this.onChange.bind(this)}
|
||||
style={style}>
|
||||
<i onTouchStart={this.hide} />
|
||||
|
||||
<p>Filter</p>
|
||||
@ -34,6 +41,18 @@ export default class Navigation extends Component {
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>View</p>
|
||||
<ul>
|
||||
<li>
|
||||
<input id='view-list' name='view' data-value='list' type='radio' defaultChecked={settings.filter === 'list'} />
|
||||
<label htmlFor='view-list'>List</label>
|
||||
</li>
|
||||
<li>
|
||||
<input id='view-grid' name='view' data-value='grid' type='radio' defaultChecked={settings.filter === 'grid'} />
|
||||
<label htmlFor='view-grid'>Grid</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Tools</p>
|
||||
<ul>
|
||||
<li className='coming-soon'>
|
||||
@ -51,9 +70,6 @@ export default class Navigation extends Component {
|
||||
<input id='showDirectoriesFirst' type='checkbox' defaultChecked={settings.showDirectoriesFirst} />
|
||||
<label htmlFor='showDirectoriesFirst'>Show Directories First</label>
|
||||
</li>
|
||||
<li className='coming-soon'>
|
||||
<label>Advanced Preferences</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>External</p>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import ReactDOM from 'react-dom';
|
||||
import FileList from 'components/file-list';
|
||||
import Navigation from 'components/navigation';
|
||||
import Header from 'components/header';
|
||||
@ -10,6 +11,7 @@ import Spinner from 'components/spinner';
|
||||
import { connect } from 'react-redux';
|
||||
import { hideAll as hideAllMenus } from 'actions/menu';
|
||||
import { hideAll as hideAllDialogs} from 'actions/dialog';
|
||||
import Hammer from 'react-hammerjs';
|
||||
|
||||
import tour from 'tour';
|
||||
import changedir from 'actions/changedir';
|
||||
@ -27,26 +29,28 @@ 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);
|
||||
let CompressDialog = connect(state => state.get('compressDialog'))(Dialog);
|
||||
|
||||
export default class Root extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div onTouchStart={this.touchStart.bind(this)}
|
||||
onClick={this.onClick.bind(this)}>
|
||||
<Hammer onTap={this.onClick.bind(this)}>
|
||||
<div onTouchStart={this.touchStart.bind(this)}>
|
||||
<Header />
|
||||
<Breadcrumb />
|
||||
<Navigation />
|
||||
<FileList />
|
||||
<Toolbar />
|
||||
|
||||
<FileMenu id='fileMenu' />
|
||||
<MoreMenu id='moreMenu' />
|
||||
<FileMenu id='file-menu' />
|
||||
<MoreMenu id='more-menu' />
|
||||
|
||||
<RenameDialog />
|
||||
<DeleteDialog />
|
||||
<ErrorDialog />
|
||||
<CreateDialog />
|
||||
<SearchDialog />
|
||||
<CompressDialog />
|
||||
|
||||
<Spinner />
|
||||
|
||||
@ -55,7 +59,9 @@ export default class Root extends Component {
|
||||
<div className='tour-dialog'>
|
||||
Hello! Tap each highlighted button to get an understanding of how they work.
|
||||
</div>
|
||||
<button id='skip-tour'>Skip</button>
|
||||
</div>
|
||||
</Hammer>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
@connect(props)
|
||||
|
@ -1,25 +1,39 @@
|
||||
import React, { Component } from 'react';
|
||||
import { toggle as toggleView, refresh, selectView } from 'actions/files-view';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { refresh, selectView } from 'actions/files-view';
|
||||
import { show as showDialog } from 'actions/dialog';
|
||||
import { show as showMenu } from 'actions/menu';
|
||||
import { active } from 'actions/file';
|
||||
import settings from 'actions/settings';
|
||||
import store, { bind } from 'store';
|
||||
import { MENU_WIDTH } from './menu';
|
||||
import Hammer from 'react-hammerjs';
|
||||
|
||||
export default class Toolbar extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className='toolbar'>
|
||||
<button className='icon-plus tour-item' onClick={this.newFile} />
|
||||
<button className='icon-view coming-soon' onClick={bind(toggleView())} />
|
||||
<button className='icon-refresh tour-item' onClick={bind(refresh())} />
|
||||
<button className='icon-select tour-item' onClick={bind(selectView('toggle'))} />
|
||||
<button className='icon-more tour-item' onClick={this.showMore.bind(this)} ref='more' />
|
||||
<Hammer onTap={this.goUp}>
|
||||
<button className='icon-back tour-item' />
|
||||
</Hammer>
|
||||
<Hammer onTap={this.newFile}>
|
||||
<button className='icon-plus tour-item'/>
|
||||
</Hammer>
|
||||
<Hammer onTap={bind(refresh())}>
|
||||
<button className='icon-refresh tour-item'/>
|
||||
</Hammer>
|
||||
<Hammer onTap={this.selectView}>
|
||||
<button className='icon-select tour-item'/>
|
||||
</Hammer>
|
||||
<Hammer onTap={this.showMore.bind(this)}>
|
||||
<button className='icon-more tour-item' ref='more'/>
|
||||
</Hammer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
showMore() {
|
||||
let rect = React.findDOMNode(this.refs.more).getBoundingClientRect();
|
||||
let rect = this.refs.more.getBoundingClientRect();
|
||||
let {x, y, width, height} = rect;
|
||||
|
||||
let left = x + width - MENU_WIDTH,
|
||||
@ -29,6 +43,20 @@ export default class Toolbar extends Component {
|
||||
store.dispatch(showMenu('moreMenu', {style: {left, top, transform}}));
|
||||
}
|
||||
|
||||
goUp() {
|
||||
let current = store.getState().get('cwd');
|
||||
let up = current.split('/').slice(0, -1).join('/');
|
||||
|
||||
if (up === current) return;
|
||||
|
||||
store.dispatch(changedir(up));
|
||||
}
|
||||
|
||||
selectView() {
|
||||
store.dispatch(selectView('toggle'));
|
||||
store.dispatch(active());
|
||||
}
|
||||
|
||||
newFile() {
|
||||
let cwd = store.getState().get('cwd');
|
||||
let action = showDialog('createDialog', {
|
||||
@ -36,4 +64,11 @@ export default class Toolbar extends Component {
|
||||
});
|
||||
store.dispatch(action);
|
||||
}
|
||||
|
||||
toggleView() {
|
||||
let current = store.getState().get('settings').view;
|
||||
let value = current === 'list' ? 'grid' : 'list';
|
||||
|
||||
store.dispatch(settings({view: value}));
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
import React from 'react';
|
||||
import { hide, hideAll } from 'actions/dialog';
|
||||
import { hide, hideAll, show } from 'actions/dialog';
|
||||
import { rename, remove, create, active } from 'actions/file';
|
||||
import { search } from 'actions/files-view';
|
||||
import { compress } from 'actions/compress';
|
||||
import store, { bind } from 'store';
|
||||
|
||||
const INVALID_NAME = 'Please enter a valid name.';
|
||||
const INVALID_SEARCH = 'You can\'t leave the input empty';
|
||||
|
||||
export default {
|
||||
createDialog: {
|
||||
title: 'Create',
|
||||
@ -13,25 +17,51 @@ export default {
|
||||
{
|
||||
text: 'File',
|
||||
action() {
|
||||
let input = React.findDOMNode(this.refs.input);
|
||||
let input = this.refs.input;
|
||||
|
||||
if (!input.value) {
|
||||
this.props.dispatch(hideAll());
|
||||
this.props.dispatch(active());
|
||||
this.props.dispatch(show('errorDialog', {description: INVALID_NAME}));
|
||||
return;
|
||||
}
|
||||
|
||||
let cwd = store.getState().get('cwd');
|
||||
let action = create(cwd + input.value);
|
||||
let path = cwd + '/' + input.value;
|
||||
let action = create(path.replace(/^\//, ''));
|
||||
this.props.dispatch(action);
|
||||
this.props.dispatch(hideAll());
|
||||
this.props.dispatch(active());
|
||||
input.value = '';
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Directory',
|
||||
action() {
|
||||
let input = React.findDOMNode(this.refs.input);
|
||||
let input = this.refs.input;
|
||||
|
||||
if (!input.value) {
|
||||
this.props.dispatch(hideAll());
|
||||
this.props.dispatch(active());
|
||||
this.props.dispatch(show('errorDialog', {description: INVALID_NAME}));
|
||||
return;
|
||||
}
|
||||
|
||||
let cwd = store.getState().get('cwd');
|
||||
let action = create(cwd + input.value, true);
|
||||
let path = cwd + '/' + input.value;
|
||||
let action = create(path.replace(/^\//, ''), true);
|
||||
this.props.dispatch(action);
|
||||
this.props.dispatch(hideAll());
|
||||
this.props.dispatch(active());
|
||||
input.value = '';
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Cancel',
|
||||
action() {
|
||||
let input = this.refs.input;
|
||||
this.props.dispatch(hideAll());
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -43,17 +73,29 @@ export default {
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
action: bind(hideAll())
|
||||
action() {
|
||||
let input = this.refs.input;
|
||||
this.props.dispatch(hideAll());
|
||||
input.value = '';
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Rename',
|
||||
action() {
|
||||
let input = React.findDOMNode(this.refs.input);
|
||||
let input = this.refs.input;
|
||||
|
||||
if (!input.value) {
|
||||
this.props.dispatch(hideAll());
|
||||
this.props.dispatch(active());
|
||||
this.props.dispatch(show('errorDialog', {description: INVALID_NAME}));
|
||||
return;
|
||||
}
|
||||
|
||||
let activeFile = store.getState().get('activeFile');
|
||||
this.props.dispatch(rename(activeFile, input.value))
|
||||
this.props.dispatch(hideAll());
|
||||
this.props.dispatch(active());
|
||||
input.value = '';
|
||||
},
|
||||
className: 'success'
|
||||
}
|
||||
@ -93,16 +135,69 @@ export default {
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
action: bind(hideAll())
|
||||
action() {
|
||||
let input = this.refs.input;
|
||||
this.props.dispatch(hideAll());
|
||||
input.value = '';
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Search',
|
||||
action() {
|
||||
let input = React.findDOMNode(this.refs.input);
|
||||
let input = this.refs.input;
|
||||
|
||||
if (!input.value) {
|
||||
this.props.dispatch(hideAll());
|
||||
this.props.dispatch(active());
|
||||
this.props.dispatch(show('errorDialog',
|
||||
{description: INVALID_SEARCH}));
|
||||
return;
|
||||
}
|
||||
|
||||
let action = search(input.value);
|
||||
this.props.dispatch(action);
|
||||
this.props.dispatch(hideAll());
|
||||
input.value = '';
|
||||
},
|
||||
className: 'success'
|
||||
}
|
||||
]
|
||||
},
|
||||
compressDialog: {
|
||||
title: 'Archive',
|
||||
description: 'Enter your desired archive name',
|
||||
input: true,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
action() {
|
||||
let input = this.refs.input;
|
||||
this.props.dispatch(hideAll());
|
||||
input.value = '';
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Create',
|
||||
action() {
|
||||
let input = this.refs.input;
|
||||
|
||||
if (!input.value) {
|
||||
this.props.dispatch(hideAll());
|
||||
this.props.dispatch(active());
|
||||
this.props.dispatch(show('errorDialog',
|
||||
{description: INVALID_NAME}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.value.slice(-4) !== '.zip') {
|
||||
input.value += '.zip';
|
||||
}
|
||||
|
||||
let activeFile = store.getState().get('activeFile');
|
||||
this.props.dispatch(compress(activeFile, input.value))
|
||||
this.props.dispatch(hideAll());
|
||||
this.props.dispatch(active());
|
||||
input.value = '';
|
||||
},
|
||||
className: 'success'
|
||||
}
|
||||
|
@ -1,8 +1,22 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as ftp from 'api/ftp';
|
||||
import Root from 'components/root';
|
||||
import store from 'store';
|
||||
import { Provider } from 'react-redux';
|
||||
import './activities';
|
||||
|
||||
ftp.connect({
|
||||
host: '192.168.1.5',
|
||||
port: 21,
|
||||
username: 'mahdi',
|
||||
password: 'heater0!'
|
||||
}).then(socket => {
|
||||
window.socket = socket;
|
||||
window.ftp = ftp;
|
||||
}, console.error.bind(console))
|
||||
|
||||
let wrapper = document.getElementById('wrapper');
|
||||
React.render(<Provider store={store}>{() => <Root />}</Provider>, wrapper);
|
||||
ReactDOM.render(<Provider store={store}>
|
||||
<Root />
|
||||
</Provider>, wrapper);
|
||||
|
@ -2,6 +2,7 @@ import { hideAll } from 'actions/menu';
|
||||
import { show } from 'actions/dialog';
|
||||
import { selectView } from 'actions/files-view';
|
||||
import { copy, move } from 'actions/file';
|
||||
import { compress, decompress } from 'actions/compress';
|
||||
import store from 'store';
|
||||
|
||||
const entryMenu = {
|
||||
@ -11,10 +12,11 @@ const entryMenu = {
|
||||
action() {
|
||||
let files = store.getState().get('files');
|
||||
let active = store.getState().get('activeFile');
|
||||
const description = `Enter the new name for ${active[0].name}`;
|
||||
let name = active[0].name;
|
||||
const description = `Enter the new name for ${name}`;
|
||||
|
||||
store.dispatch(hideAll());
|
||||
store.dispatch(show('renameDialog', {description}));
|
||||
store.dispatch(show('renameDialog', {description, value: name}));
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -63,6 +65,27 @@ const entryMenu = {
|
||||
blob
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Extract',
|
||||
enabled() {
|
||||
let active = store.getState().get('activeFile');
|
||||
|
||||
return active && active[0].name.slice(-4) === '.zip';
|
||||
},
|
||||
action() {
|
||||
let active = store.getState().get('activeFile');
|
||||
|
||||
store.dispatch(decompress(active));
|
||||
store.dispatch(hideAll());
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Archive',
|
||||
action() {
|
||||
store.dispatch(hideAll());
|
||||
store.dispatch(show('compressDialog'));
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@ -141,6 +164,13 @@ const moreMenu = {
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Archive',
|
||||
action() {
|
||||
store.dispatch(hideAll());
|
||||
store.dispatch(show('compressDialog'));
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ACTIVE_FILE } from 'actions/types';
|
||||
import { ACTIVE_FILE, SELECT_VIEW } from 'actions/types';
|
||||
|
||||
export default function(state = null, action) {
|
||||
if (action.type === ACTIVE_FILE) {
|
||||
|
@ -32,6 +32,7 @@ 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')
|
||||
searchDialog: dialog(state, action, 'searchDialog'),
|
||||
compressDialog: dialog(state, action, 'compressDialog')
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CHANGE_DIRECTORY, REFRESH, SETTINGS } from 'actions/types';
|
||||
import { children } from 'api/files';
|
||||
import { children, CACHE, FTP_CACHE } from 'api/auto';
|
||||
import store from 'store';
|
||||
import { reportError } from 'utils';
|
||||
import { reportError, normalize } from 'utils';
|
||||
import { listFiles } from 'actions/files-view';
|
||||
|
||||
export default function(state = '', action) {
|
||||
@ -11,6 +11,11 @@ export default function(state = '', action) {
|
||||
return action.dir;
|
||||
}
|
||||
|
||||
if (action.type === REFRESH) {
|
||||
CACHE[state] = null;
|
||||
FTP_CACHE[state] = null;
|
||||
}
|
||||
|
||||
if (action.type === REFRESH || action.type === SETTINGS) {
|
||||
changeTo(state);
|
||||
|
||||
@ -21,7 +26,8 @@ export default function(state = '', action) {
|
||||
}
|
||||
|
||||
function changeTo(dir) {
|
||||
dir = normalize(dir);
|
||||
children(dir, true).then(files => {
|
||||
store.dispatch(listFiles(files));
|
||||
}, reportError);
|
||||
}, reportError)
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { LIST_FILES, RENAME_FILE, DELETE_FILE, CREATE_FILE, MOVE_FILE, COPY_FILE, SEARCH } from 'actions/types';
|
||||
import { LIST_FILES, RENAME_FILE, DELETE_FILE, CREATE_FILE, MOVE_FILE, COPY_FILE, SEARCH, COMPRESS, DECOMPRESS } from 'actions/types';
|
||||
import zip from 'jszip';
|
||||
import { refresh } from 'actions/files-view';
|
||||
import { move, remove, sdcard, createFile, createDirectory, copy } from 'api/files';
|
||||
import * as auto from 'api/auto';
|
||||
import { show } from 'actions/dialog';
|
||||
import store, { bind } from 'store';
|
||||
import { reportError, type } from 'utils';
|
||||
import { reportError, type, normalize } from 'utils';
|
||||
|
||||
let boundRefresh = bind(refresh());
|
||||
|
||||
@ -38,7 +39,7 @@ export default function(state = [], action) {
|
||||
}
|
||||
|
||||
if (action.type === CREATE_FILE) {
|
||||
let fn = action.directory ? createDirectory : createFile;
|
||||
let fn = action.directory ? auto.createDirectory : auto.createFile;
|
||||
fn(action.path).then(boundRefresh, reportError);
|
||||
|
||||
return state;
|
||||
@ -47,7 +48,7 @@ export default function(state = [], action) {
|
||||
if (action.type === RENAME_FILE) {
|
||||
let all = Promise.all(action.file.map(file => {
|
||||
let cwd = store.getState().get('cwd');
|
||||
return move(file, cwd + '/' + action.name);
|
||||
return auto.move(file, cwd + '/' + action.name);
|
||||
}));
|
||||
|
||||
all.then(boundRefresh, reportError);
|
||||
@ -56,7 +57,7 @@ export default function(state = [], action) {
|
||||
|
||||
if (action.type === MOVE_FILE) {
|
||||
let all = Promise.all(action.file.map(file => {
|
||||
return move(file, action.newPath + '/' + file.name);
|
||||
return auto.move(file, action.newPath + '/' + file.name);
|
||||
}));
|
||||
|
||||
all.then(boundRefresh, reportError);
|
||||
@ -65,7 +66,7 @@ export default function(state = [], action) {
|
||||
|
||||
if (action.type === COPY_FILE) {
|
||||
let all = Promise.all(action.file.map(file => {
|
||||
return copy(file, action.newPath + '/' + file.name);
|
||||
return auto.copy(file, action.newPath + '/' + file.name);
|
||||
}));
|
||||
|
||||
all.then(boundRefresh, reportError);
|
||||
@ -74,17 +75,71 @@ export default function(state = [], action) {
|
||||
|
||||
if (action.type === DELETE_FILE) {
|
||||
let all = Promise.all(action.file.map(file => {
|
||||
let path = ((file.path || '') + file.name).replace(/^\//, '');
|
||||
return remove(path, true);
|
||||
let path = normalize((file.path || '') + file.name);
|
||||
return auto.remove(path, true);
|
||||
}))
|
||||
|
||||
all.then(boundRefresh, reportError);
|
||||
return state;
|
||||
}
|
||||
|
||||
if (action.type === COMPRESS) {
|
||||
let archive = new zip();
|
||||
let cwd = store.getState().get('cwd');
|
||||
|
||||
let all = Promise.all(action.file.map(function addFile(file) {
|
||||
let path = normalize((file.path || '') + file.name);
|
||||
let archivePath = path.slice(cwd.length);
|
||||
// directory
|
||||
console.log(file);
|
||||
if (file.type === 'Directory') {
|
||||
let folder = archive.folder(file.name);
|
||||
|
||||
return auto.children(path).then(files => {
|
||||
files = files.filter(file => file);
|
||||
|
||||
return Promise.all(files.map(child => {
|
||||
return addFile(child);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return auto.readFile(path).then(content => {
|
||||
archive.file(archivePath, content);
|
||||
});
|
||||
}))
|
||||
|
||||
all.then(() => {
|
||||
let blob = archive.generate({ type: 'blob' });
|
||||
|
||||
let cwd = store.getState().get('cwd');
|
||||
let path = normalize(cwd + '/' + action.name);
|
||||
return auto.writeFile(path, blob);
|
||||
}).then(boundRefresh).catch(reportError);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
if (action.type === DECOMPRESS) {
|
||||
let file = action.file[0];
|
||||
let path = normalize((file.path || '') + file.name);
|
||||
auto.readFile(path).then(content => {
|
||||
let archive = new zip(content);
|
||||
let files = Object.keys(archive.files);
|
||||
|
||||
let all = Promise.all(files.map(name => {
|
||||
let buffer = archive.files[name].asArrayBuffer();
|
||||
let blob = new Blob([buffer]);
|
||||
|
||||
let cwd = store.getState().get('cwd');
|
||||
let filePath = normalize(cwd + '/' + name);
|
||||
|
||||
return auto.writeFile(filePath, blob);
|
||||
}));
|
||||
|
||||
all.then(boundRefresh, reportError);
|
||||
});
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function mov(file, newPath) {
|
||||
return
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { SEARCH } from 'actions/types';
|
||||
import { SEARCH, CHANGE_DIRECTORY, REFRESH } 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';
|
||||
import { children } from 'api/auto';
|
||||
import { type, normalize } from 'utils';
|
||||
|
||||
export default function(state = '', action) {
|
||||
if (action.type === SEARCH) {
|
||||
@ -12,6 +12,10 @@ export default function(state = '', action) {
|
||||
return action.keywords;
|
||||
}
|
||||
|
||||
if (action.type === CHANGE_DIRECTORY || action.type === REFRESH) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@ -28,7 +32,7 @@ function search(keywords) {
|
||||
// We don't want to show all the currently visible files from the
|
||||
// first iteration
|
||||
let once = true;
|
||||
children('/', true).then(function showResults(files) {
|
||||
children('', true).then(function showResults(files) {
|
||||
if (!store.getState().get('search')) return;
|
||||
|
||||
let current = once ? [] : store.getState().get('files');
|
||||
@ -36,7 +40,7 @@ function search(keywords) {
|
||||
|
||||
let filtered = files.filter(file => {
|
||||
if (type(file) === 'Directory') {
|
||||
let path = (file.path + file.name).replace(/^\//, '');
|
||||
let path = normalize(file.path + file.name);
|
||||
children(path, true).then(showResults, reportError);
|
||||
}
|
||||
return keys.some(key => {
|
||||
|
@ -3,7 +3,8 @@ import omit from 'lodash/object/omit';
|
||||
|
||||
const DEFAULT = {
|
||||
showHiddenFiles: false,
|
||||
showDirectoriesFirst: true
|
||||
showDirectoriesFirst: true,
|
||||
view: 'list'
|
||||
}
|
||||
|
||||
export default function(state = DEFAULT, action) {
|
||||
|
@ -3,6 +3,7 @@ const MESSAGES = {
|
||||
'icon-refresh': 'Refresh File List',
|
||||
'icon-select': 'Select files for batch actions',
|
||||
'icon-more': 'Actions used on selected files such as Copy, Delete, Move, …',
|
||||
'icon-back': 'Navigate to top directory',
|
||||
'drawer': 'Extra options, tools and links are here',
|
||||
'icon-search': 'Search your storage for a certain file',
|
||||
'swipe-instruction': 'Swipe from left to right to go to parent folder'
|
||||
@ -14,30 +15,39 @@ export default function() {
|
||||
let tourRan = localStorage.getItem('tourRan');
|
||||
let wrapper = document.querySelector('#wrapper');
|
||||
let tour = document.querySelector('.tour-dialog');
|
||||
let skip = document.querySelector('#skip-tour');
|
||||
|
||||
let timeout;
|
||||
let shown = 0;
|
||||
|
||||
if (!tourRan) {
|
||||
let listeners = [];
|
||||
|
||||
wrapper.classList.add('tour');
|
||||
|
||||
skip.addEventListener('touchstart', () => {
|
||||
wrapper.classList.remove('tour');
|
||||
localStorage.setItem('tourRan', 'true');
|
||||
|
||||
for (let {item, listener} of listeners) {
|
||||
item.removeEventListener('touchstart', listener);
|
||||
}
|
||||
})
|
||||
|
||||
let items = [...document.querySelectorAll('.tour-item')].sort((a, b) => {
|
||||
return (+a.dataset.index) - (+b.dataset.index);
|
||||
});
|
||||
|
||||
let listeners = [];
|
||||
|
||||
for (let item of items) {
|
||||
|
||||
let firstClass = item.className.slice(0, item.className.indexOf(' '));
|
||||
let ev = firstClass === 'drawer' ? 'touchstart' : 'click';
|
||||
|
||||
item.addEventListener(ev, function listener(e) {
|
||||
item.addEventListener('touchstart', function listener(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
clearTimeout(timeout);
|
||||
listeners.push({item, listener, ev});
|
||||
listeners.push({item, listener});
|
||||
|
||||
shown++;
|
||||
|
||||
@ -48,8 +58,8 @@ export default function() {
|
||||
wrapper.classList.remove('tour');
|
||||
localStorage.setItem('tourRan', 'true');
|
||||
|
||||
for (let {item, listener, ev} of listeners) {
|
||||
item.removeEventListener(ev, listener);
|
||||
for (let {item, listener} of listeners) {
|
||||
item.removeEventListener('touchstart', listener);
|
||||
}
|
||||
}
|
||||
}, DIALOG_HIDE_DELAY);
|
||||
|
@ -2,7 +2,7 @@ import store from 'store';
|
||||
import { show } from 'actions/dialog';
|
||||
|
||||
export function type(obj) {
|
||||
return Object.prototype.toString.call(obj).slice(8, -1);
|
||||
return obj.toString().slice(8, -1);
|
||||
}
|
||||
|
||||
export function template(string, props) {
|
||||
@ -26,10 +26,16 @@ export function getKey(object = store.getState().toJS(), key) {
|
||||
}
|
||||
|
||||
export function reportError(err) {
|
||||
let action = show('errorDialog', {description: err.message});
|
||||
console.error(err);
|
||||
let msg = err.message || err.target.error.message;
|
||||
let action = show('errorDialog', {description: msg});
|
||||
store.dispatch(action);
|
||||
}
|
||||
|
||||
export function normalize(path) {
|
||||
return path.replace(/^\//, '').replace('sdcard/', '');
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
'GB': Math.pow(2, 30),
|
||||
'MB': Math.pow(2, 20),
|
||||
@ -40,8 +46,22 @@ export function humanSize(size) {
|
||||
for (let key in sizes) {
|
||||
let value = sizes[key];
|
||||
|
||||
if (size > value) {
|
||||
return Math.round(size / value) + key;
|
||||
if (size >= value) {
|
||||
return Math.abs(Math.round(size / value)) + key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getLength(string) {
|
||||
var byteLen = 0;
|
||||
for (var i = 0; i < string.length; i++) {
|
||||
var c = string.charCodeAt(i);
|
||||
byteLen += c < (1 << 7) ? 1 :
|
||||
c < (1 << 11) ? 2 :
|
||||
c < (1 << 16) ? 3 :
|
||||
c < (1 << 21) ? 4 :
|
||||
c < (1 << 26) ? 5 :
|
||||
c < (1 << 31) ? 6 : Number.NaN;
|
||||
}
|
||||
return byteLen;
|
||||
}
|
||||
|
@ -8,12 +8,12 @@
|
||||
|
||||
overflow-x: auto;
|
||||
|
||||
padding: 8px;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
.light-medium;
|
||||
|
||||
padding-right: 8px;
|
||||
|
||||
background: @light-gray;
|
||||
|
||||
border-bottom: 1px solid @dark-transparent;
|
||||
@ -22,12 +22,43 @@
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
div {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
i {
|
||||
margin: 0 2px;
|
||||
span {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 4.5rem;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
padding: 0 5px 0 30px;
|
||||
background: @gray;
|
||||
|
||||
filter: drop-shadow(1px 0 0 @dark-transparent);
|
||||
|
||||
&:first-of-type {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
right: -46px;
|
||||
top: 0;
|
||||
|
||||
content: '';
|
||||
|
||||
display: block;
|
||||
|
||||
border: 23px solid transparent;
|
||||
border-left-color: @gray;
|
||||
}
|
||||
}
|
||||
|
||||
span.history {
|
||||
|
@ -48,6 +48,8 @@
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
margin-bottom: 1rem;
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
|
||||
|
@ -1,25 +1,11 @@
|
||||
.file, .directory {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
|
||||
padding: 1.4rem;
|
||||
|
||||
width: 100%;
|
||||
.light-big;
|
||||
|
||||
box-sizing: border-box;
|
||||
.list .file, .list .directory {
|
||||
flex: 1 1 100%;
|
||||
|
||||
border-bottom: 1px solid @dark-separator;
|
||||
|
||||
p {
|
||||
flex: 1 1;
|
||||
|
||||
max-width: ~'calc(100% - 9rem)';
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
> span {
|
||||
@ -33,6 +19,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
.grid .file, .grid .directory {
|
||||
flex: 1 0 33.33%;
|
||||
|
||||
max-width: 33.33%;
|
||||
|
||||
padding: 1.4rem 0.5rem;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
p {
|
||||
max-height: 1.5em;
|
||||
max-width: 100%;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file, .directory {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
|
||||
padding: 1.4rem;
|
||||
|
||||
.light-big;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
p {
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: @gray;
|
||||
}
|
||||
}
|
||||
|
||||
.directory i {
|
||||
.icon-directory;
|
||||
}
|
||||
|
@ -1,4 +1,10 @@
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
height: ~'calc(100vh - 14.5rem)';
|
||||
|
||||
overflow-x: hidden;
|
||||
|
@ -14,37 +14,25 @@ header {
|
||||
.shadow;
|
||||
|
||||
h1 {
|
||||
margin-left: -3rem;
|
||||
font-size: 2.3rem;
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
width: 8rem;
|
||||
height: 4rem;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
padding-top: 1rem;
|
||||
margin-top: -1rem;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
|
||||
width: 2rem;
|
||||
height: 4px;
|
||||
|
||||
margin-top: -9px;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
background: @overlay;
|
||||
|
||||
box-shadow: 0 7px 0 @overlay,
|
||||
0 14px 0 @overlay;
|
||||
i {
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,5 +45,9 @@
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: @gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,4 +11,10 @@
|
||||
box-sizing: border-box;
|
||||
|
||||
background: @light-gray;
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
background-position: center center;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,12 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-menu {
|
||||
.icon;
|
||||
background: url(/img/Menu.svg) no-repeat;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.icon-directory {
|
||||
.icon;
|
||||
background: url(/img/Directory.svg) no-repeat;
|
||||
@ -54,14 +60,20 @@
|
||||
.icon-search {
|
||||
.icon;
|
||||
background: url(/img/Search.svg) no-repeat;
|
||||
width: 19px;
|
||||
height: 27px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.icon-cross {
|
||||
.icon-plus;
|
||||
transform: rotate(45deg);
|
||||
svg * {
|
||||
fill: white;
|
||||
}
|
||||
.icon;
|
||||
background: url(/img/Close.svg) no-repeat;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.icon-back {
|
||||
.icon;
|
||||
background: url(/img/Back.svg) no-repeat;
|
||||
width: 15px;
|
||||
height: 24px;
|
||||
}
|
||||
|
@ -108,9 +108,32 @@
|
||||
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
#skip-tour {
|
||||
font-size: 2rem;
|
||||
|
||||
display: block;
|
||||
|
||||
padding: 0.5rem 5rem;
|
||||
|
||||
margin: 1rem auto;
|
||||
|
||||
.btn.success;
|
||||
|
||||
position: fixed;
|
||||
|
||||
left: 50%;
|
||||
top: 65%;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.shadow-16;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tour-dialog {
|
||||
.tour-dialog, #skip-tour {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"version": "1.2.0",
|
||||
"name": "Hawk",
|
||||
"description": "Keep an eye on your files with a full-featured file manager",
|
||||
"launch_path": "/index.html",
|
||||
@ -30,6 +30,9 @@
|
||||
"device-storage:music": {
|
||||
"access": "readwrite",
|
||||
"description": "We need access to your files in order to give you the functionality of listing, reading, opening and writing files in your storage"
|
||||
},
|
||||
"tcp-socket": {
|
||||
"description": "FTP Browser: Used to connect to FTP servers"
|
||||
}
|
||||
},
|
||||
"installs_allowed_from": [
|
||||
@ -41,9 +44,6 @@
|
||||
"pick": {
|
||||
"href": "./index.html",
|
||||
"disposition": "inline",
|
||||
"filters": {
|
||||
"type": "*"
|
||||
},
|
||||
"returnValue": true
|
||||
}
|
||||
}
|
||||
|