Compare commits

...

54 Commits

Author SHA1 Message Date
Mahdi Dibaiee
d4d8b81e40 zip support was added in the last version 2016-10-05 14:18:40 +03:30
Mahdi Dibaiee
7bcf39bdb7 fix(compatibility): make it compatible with the new Firefox OS sdcard API (tested on Sony Z3C)
fix(toolbar): increase tap area of buttons for easier access
fix(touch): improve touch sensitivity for ease of use
2016-04-08 17:31:24 +04:30
Mahdi Dibaiee
2eaf2ac1f0 feat(ftp): implement ftp browser, most things are functioning except archiving and some actions of multiple files 2015-10-29 11:31:28 +03:30
Mahdi Dibaiee
9aa5bcf384 chore: rebuild 2015-10-26 15:45:18 +03:30
Mahdi Dibaiee
bec675e7ee fix(archive): fix all files being put into folders while archiving 2015-10-26 15:44:54 +03:30
Mahdi Dibaiee
4253732492 fix(archive): automatically add .zip extension if omitted while creating archives 2015-10-24 21:33:15 +03:30
Mahdi Dibaiee
a9c5890c3c chore(version): bump to 1.1.2, rebuild 2015-10-24 20:24:51 +03:30
Mahdi Dibaiee
f0f6a684a7 fix(errors): show verbose error for duplicate files
progress #14
2015-10-24 20:24:15 +03:30
Mahdi Dibaiee
629b6f7e61 chore(version): bump to 1.1.1, rebuild 2015-10-24 20:01:11 +03:30
Mahdi Dibaiee
1833a5e3c1 feat(archive-name): ask for a name to set for new archives
resolve #13
2015-10-24 19:41:54 +03:30
Mahdi Dibaiee
735ef7fa7b fix(selectview): small typo in module name
chore(version): bump version to 1.1.0, rebuild
2015-10-24 18:49:25 +03:30
Mahdi Dibaiee
dfb7d8aa72 feat(archive): ability to archive / extract files in zip format
fix(selectview): clear active files when user taps on select-view button in toolbar

resolve #10
resolve #12
2015-10-24 18:46:17 +03:30
Mahdi Dibaiee
44340abb61 bump version 2015-10-23 11:23:32 +03:30
Mahdi Dibaiee
0018380759 fix(selection): clear selection after changing directory 2015-10-23 11:23:20 +03:30
Mahdi Dibaiee
c13315d61e rebuild 2015-10-03 21:03:08 +03:30
Mahdi Dibaiee
59af3b9e10 Merge branch 'firefoxos-header' 2015-10-03 21:01:12 +03:30
Tim Nguyen
be8ae4f0b9 Tweaks to the header changes 2015-10-03 18:58:44 +02:00
Tim Nguyen
5c5305d243 Ignore DS_Store 2015-10-03 18:41:25 +02:00
Tim Nguyen
e449402928 Merge branch 'master' of https://github.com/mdibaiee/Hawk into firefoxos-header 2015-10-03 18:38:45 +02:00
Tim Nguyen
88f8909fcc Header style changes 2015-10-03 18:34:45 +02:00
Mahdi Dibaiee
d22e2e5527 rebuild 2015-09-29 21:13:14 +03:30
Mahdi Dibaiee
25ff79af90 feat back: replaced toolbar's "Toggle View" button with "Back" - resolve #6
Views are now listed in navigation drawer as a radio-button group
2015-09-28 16:33:56 +03:30
Mahdi Dibaiee
b3b2ddf4f8 fix breadcrumb: Change breadcrumb style to arrow-like buttons 2015-09-27 18:40:13 +03:30
Mahdi Dibaiee
cb30112c40 fix search.ux: While searching, show the search term in place of breadcrumb (resolve #8)
fix search: Navigating to search result's folders should exit search mode
2015-09-26 20:53:34 +03:30
Mahdi Dibaiee
92b5fa2fee Use round app icon - fix #5 2015-09-25 23:03:56 +03:30
Mahdi Dibaiee
419b4010d1 feat breadcrumb.back: add back button 2015-09-19 17:27:18 +04:30
Mahdi Dibaiee
19f6960a6d rebuild 2015-09-16 19:20:39 +04:30
Mahdi Dibaiee
ccf24e513b Don't allow dialogs' inputs to be empty, show an error in that case 2015-09-16 19:20:25 +04:30
Mahdi Dibaiee
a88ff826e7 rebuild 2015-09-16 19:17:09 +04:30
Mahdi Dibaiee
11672c58f0 Fix pre-filling rename dialog with file name 2015-09-16 19:16:54 +04:30
Mahdi Dibaiee
31a873d2bb Fix long-tapping on items in directories with a lot of items emitting open action on item
Try to reduce the time between selecting files and visual feedback
2015-09-16 19:10:00 +04:30
Mahdi Dibaiee
d0c8c91250 Fix #1 files less than 1KB showing as negative 2015-09-16 17:52:52 +04:30
Mahdi Dibaiee
b2b71b5d10 Improve dialogs, Add Skip button to Tour 2015-09-16 15:43:40 +04:30
Mahdi Dibaiee
ce00cb25dc rebuild 2015-09-16 14:54:29 +04:30
Mahdi Dibaiee
427cbca2dc Improve dialogs:
- Clear dialogs after closing them
- Rename dialog should be filled with File name intiially
- Add a Cancel button to Create dialog (buttons grouped by twos)
2015-09-16 14:54:16 +04:30
Mahdi Dibaiee
43239b4a4c Fix single/twin folders being center-aligned instead of left-aligned 2015-09-15 19:45:30 +04:30
Mahdi Dibaiee
596799b6f0 Fix #ThanksTo line-break and Mobile Portrait 2015-09-15 19:37:30 +04:30
Mahdi Dibaiee
19e306712f I'm really thankful to Sergio Muriel, he is awesome. 2015-09-15 19:34:17 +04:30
Mahdi Dibaiee
29fc832287 Fix single / double files showing in center on Grid View 2015-09-15 19:26:34 +04:30
Mahdi Dibaiee
d52fe9f9bc Fix directories with nested files not showing
Fix cwd showing "/sdcard/sdcard" directories
2015-09-15 17:57:43 +04:30
Mahdi Dibaiee
6e52ca6246 Improve compatibility with old versions
- Remove starting "/sdcard/" from file paths on old Firefox OS versions
- Cache results for faster navigation on old Firefox OS devices
2015-09-15 17:26:07 +04:30
Mahdi Dibaiee
66504df4cb fix file.create: don't create an .empty folder on devices with new API support
feat docs: add Frequently Asked Questions section
2015-09-14 12:42:50 +04:30
Mahdi Dibaiee
8408fd5319 rebuild 2015-09-14 02:18:35 +04:30
Mahdi Dibaiee
ceb8cd3b21 fix compatibility: fix getting an empty error due to corrupted path properties
fix compatibility: switch navigation drawer to `display: block` if there is no flexbox support
2015-09-14 02:18:19 +04:30
Mahdi Dibaiee
8a3c5de65d rebuild 2015-09-14 01:00:19 +04:30
Mahdi Dibaiee
d925dfb082 fix compatibility: polyfill device storage "getFilesAndDirectories" method 2015-09-14 00:59:58 +04:30
Mahdi Dibaiee
2b51a7df09 fix releases: go back to release 1.0.0 2015-09-13 20:14:25 +04:30
Mahdi Dibaiee
f06d521bbf feat compatibility: Polyfill javascript 2015-09-13 20:07:29 +04:30
Mahdi Dibaiee
59f7991c10 rebuild 2015-09-13 18:56:56 +04:30
Mahdi Dibaiee
336dd01dcb fix file.create: fix creating files and directories
fix files.list: fix single files showing in the middle of screen instead of top
feat files.style: introduce file hover color (feedback on touch)
2015-09-13 18:56:35 +04:30
Mahdi Dibaiee
779d890513 fix docs: update todo-list, bump version and rebuild 2015-09-09 13:30:05 +04:30
Mahdi Dibaiee
d56ea95e9b feat view: Enable toggle view button to toggle between grid and list views 2015-09-09 13:28:15 +04:30
Mahdi Dibaiee
49eb62ef2f fix docs: add Mohammad Jahani to #thanks to section 2015-09-08 18:52:45 +04:30
Mahdi Dibaiee
1d6769d4e5 Update website concept 2015-09-08 18:42:41 +04:30
78 changed files with 24050 additions and 10398 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
# DS_Store
.DS_Store
# Logs
logs
*.log

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 753 B

After

Width:  |  Height:  |  Size: 796 B

BIN
build/icon/Icon-340.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 58 KiB

16
build/img/Back.svg Normal file
View 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
View 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
View 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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

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

Binary file not shown.

Binary file not shown.

View File

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

Binary file not shown.

BIN
releases/hawk-1.1.0.zip Normal file

Binary file not shown.

BIN
releases/hawk-1.1.1.zip Normal file

Binary file not shown.

BIN
releases/hawk-1.1.2.zip Normal file

Binary file not shown.

BIN
releases/hawk-1.2.0.zip Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 753 B

After

Width:  |  Height:  |  Size: 796 B

BIN
src/icon/Icon-340.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 58 KiB

16
src/img/Back.svg Normal file
View 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
View 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
View 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

View File

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

View File

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

View File

@ -5,5 +5,5 @@ export default function changedir(dir) {
return {
type: CHANGE_DIRECTORY,
dir
};
}
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
@connect(props)

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,6 +48,8 @@
justify-content: space-between;
margin-bottom: 1rem;
button {
flex: 1;

View File

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

View File

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

View File

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

View File

@ -45,5 +45,9 @@
pointer-events: none;
}
&:active {
background: @gray;
}
}
}

View File

@ -11,4 +11,10 @@
box-sizing: border-box;
background: @light-gray;
button {
flex: 1;
width: auto;
background-position: center center;
}
}

View File

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

View File

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

View File

@ -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
src/polyfill.js Normal file

File diff suppressed because one or more lines are too long