38 Commits

Author SHA1 Message Date
be8ae4f0b9 Tweaks to the header changes 2015-10-03 18:58:44 +02:00
5c5305d243 Ignore DS_Store 2015-10-03 18:41:25 +02:00
e449402928 Merge branch 'master' of https://github.com/mdibaiee/Hawk into firefoxos-header 2015-10-03 18:38:45 +02:00
88f8909fcc Header style changes 2015-10-03 18:34:45 +02:00
d22e2e5527 rebuild 2015-09-29 21:13:14 +03:30
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
b3b2ddf4f8 fix breadcrumb: Change breadcrumb style to arrow-like buttons 2015-09-27 18:40:13 +03:30
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
92b5fa2fee Use round app icon - fix #5 2015-09-25 23:03:56 +03:30
419b4010d1 feat breadcrumb.back: add back button 2015-09-19 17:27:18 +04:30
19f6960a6d rebuild 2015-09-16 19:20:39 +04:30
ccf24e513b Don't allow dialogs' inputs to be empty, show an error in that case 2015-09-16 19:20:25 +04:30
a88ff826e7 rebuild 2015-09-16 19:17:09 +04:30
11672c58f0 Fix pre-filling rename dialog with file name 2015-09-16 19:16:54 +04:30
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
d0c8c91250 Fix #1 files less than 1KB showing as negative 2015-09-16 17:52:52 +04:30
b2b71b5d10 Improve dialogs, Add Skip button to Tour 2015-09-16 15:43:40 +04:30
ce00cb25dc rebuild 2015-09-16 14:54:29 +04:30
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
43239b4a4c Fix single/twin folders being center-aligned instead of left-aligned 2015-09-15 19:45:30 +04:30
596799b6f0 Fix #ThanksTo line-break and Mobile Portrait 2015-09-15 19:37:30 +04:30
19e306712f I'm really thankful to Sergio Muriel, he is awesome. 2015-09-15 19:34:17 +04:30
29fc832287 Fix single / double files showing in center on Grid View 2015-09-15 19:26:34 +04:30
d52fe9f9bc Fix directories with nested files not showing
Fix cwd showing "/sdcard/sdcard" directories
2015-09-15 17:57:43 +04:30
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
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
8408fd5319 rebuild 2015-09-14 02:18:35 +04:30
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
8a3c5de65d rebuild 2015-09-14 01:00:19 +04:30
d925dfb082 fix compatibility: polyfill device storage "getFilesAndDirectories" method 2015-09-14 00:59:58 +04:30
2b51a7df09 fix releases: go back to release 1.0.0 2015-09-13 20:14:25 +04:30
f06d521bbf feat compatibility: Polyfill javascript 2015-09-13 20:07:29 +04:30
59f7991c10 rebuild 2015-09-13 18:56:56 +04:30
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
779d890513 fix docs: update todo-list, bump version and rebuild 2015-09-09 13:30:05 +04:30
d56ea95e9b feat view: Enable toggle view button to toggle between grid and list views 2015-09-09 13:28:15 +04:30
49eb62ef2f fix docs: add Mohammad Jahani to #thanks to section 2015-09-08 18:52:45 +04:30
1d6769d4e5 Update website concept 2015-09-08 18:42:41 +04:30
64 changed files with 1211 additions and 418 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
Sergio Muriel [@tfeserver](https://twitter.com/tfeserver) for testing application
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.
# Features
@ -40,7 +60,8 @@ 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
- [ ] Image Thumbnails

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">
<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 it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"version": "0.1.0",
"version": "1.0.0",
"name": "Hawk",
"description": "Keep an eye on your files with a full-featured file manager",
"launch_path": "/index.html",
@ -41,9 +41,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;
@ -449,26 +497,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 +579,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

@ -36,7 +36,6 @@
"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",

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

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

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,30 +34,39 @@ export async function getFile(dir = '/') {
if (dir === '/' || !dir) return parent;
return await parent.get(dir);
return await parent.get(normalize(dir));
}
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);
}
let childs = await parent.getFilesAndDirectories();
if (gatherInfo) {
if (gatherInfo && !window.needsShim) {
for (let child of childs) {
if (type(child) === '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;
}
@ -74,17 +94,22 @@ export async function createFile(...args) {
export async function createDirectory(...args) {
let parent = await root();
return parent.createDirectory(...args);
return parent.createDirectory(...args).then(() => {
if (window.needsShim) {
return createFile(args[0] + '/.empty');
}
});
}
export async function remove(file, deep) {
let path = normalize(file);
let parent = await root();
return parent[deep ? 'removeDeep' : 'remove'](file);
return parent[deep ? 'removeDeep' : 'remove'](path);
}
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 +117,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);

View File

@ -3,54 +3,72 @@ import { connect } from 'react-redux';
import changedir from 'actions/changedir';
import { bind } from 'store';
// TODO: Fix history not working when clicking on sdcard
@connect(props)
export default class Breadcrumb extends Component {
render() {
let els = [];
if (this.props.search) {
console.log('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>
<span key={index} onClick={bind(changedir(path))} style={style}>{dir}</span>
);
});
}));
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};
console.log('history', dir)
return (
<span key={directories.length + index} className='history' onClick={bind(changedir(path))}>
<i>/</i>{dir}
</span>
<span key={key} className='history' onClick={bind(changedir(path))} style={style}>{dir}</span>
)
});
els = els.concat(history);
}
}
return (
<div className='breadcrumb'>
<div className='breadcrumb' ref='container'>
<div>
{els}
</div>
</div>
);
}
componentDidUpdate() {
let container = React.findDOMNode(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

@ -7,12 +7,27 @@ 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}
return (
<button className={button.className + ' btn'} key={i}
onClick={button.action.bind(this)}>
{button.text}
</button>;
</button>
)
});
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 +37,16 @@ export default class Dialog extends Component {
{conditionalInput}
<div className='foot'>
{buttons}
</div>
{groupButtons}
</div>
)
}
componentDidUpdate() {
if (!this.props.value) return;
let input = React.findDOMNode(this.refs.input);
input.value = this.props.value;
}
}

View File

@ -14,7 +14,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>;
}
@ -31,14 +31,16 @@ 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>
);
}
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));
store.dispatch(changedir(file.path.replace(/^\//, '') + file.name));
}
}

View File

@ -14,21 +14,23 @@ 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} />;
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'>
<div className={className} ref='container'>
{els}
</div>
);
@ -53,7 +55,8 @@ 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'
}
}

View File

@ -14,7 +14,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>;
}
@ -37,6 +37,8 @@ export default class File extends Component {
}
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';

View File

@ -11,15 +11,17 @@ export default class Header extends Component {
let i;
if (this.props.search) {
i = <i className='icon-cross' onClick={bind(search())} />
i = <button onClick={bind(search())}><i className='icon-cross' /></button>
} else {
i = <i className='icon-search tour-item' onClick={bind(show('searchDialog'))} />
i = <button onClick={bind(show('searchDialog'))}><i className='icon-search tour-item' /></button>
}
return (
<header>
<button className='drawer tour-item' onTouchStart={bind(toggle())} />
<h1 className='regular-medium'>Hawk</h1>
<button className='drawer tour-item' onTouchStart={bind(toggle())}>
<i className='icon-menu'></i>
</button>
<h1>Hawk</h1>
{i}
</header>

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 {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 = React.findDOMNode(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

@ -10,8 +10,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 +40,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'>

View File

@ -39,8 +39,8 @@ export default class Root extends Component {
<FileList />
<Toolbar />
<FileMenu id='fileMenu' />
<MoreMenu id='moreMenu' />
<FileMenu id='file-menu' />
<MoreMenu id='more-menu' />
<RenameDialog />
<DeleteDialog />
@ -55,6 +55,7 @@ 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>
);
}

View File

@ -1,7 +1,8 @@
import React, { Component } from 'react';
import { toggle as toggleView, refresh, selectView } from 'actions/files-view';
import { refresh, selectView } from 'actions/files-view';
import { show as showDialog } from 'actions/dialog';
import { show as showMenu } from 'actions/menu';
import settings from 'actions/settings';
import store, { bind } from 'store';
import { MENU_WIDTH } from './menu';
@ -9,8 +10,8 @@ export default class Toolbar extends Component {
render() {
return (
<div className='toolbar'>
<button className='icon-back tour-item' onClick={this.goUp} />
<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' />
@ -29,6 +30,15 @@ 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));
}
newFile() {
let cwd = store.getState().get('cwd');
let action = showDialog('createDialog', {
@ -36,4 +46,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,12 @@
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 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',
@ -15,11 +18,20 @@ export default {
action() {
let input = React.findDOMNode(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 = '';
}
},
{
@ -27,11 +39,28 @@ export default {
action() {
let input = React.findDOMNode(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 = React.findDOMNode(this.refs.input);
this.props.dispatch(hideAll());
input.value = '';
}
}
]
@ -43,17 +72,29 @@ export default {
buttons: [
{
text: 'Cancel',
action: bind(hideAll())
action() {
let input = React.findDOMNode(this.refs.input);
this.props.dispatch(hideAll());
input.value = '';
}
},
{
text: 'Rename',
action() {
let input = React.findDOMNode(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 +134,28 @@ export default {
buttons: [
{
text: 'Cancel',
action: bind(hideAll())
action() {
let input = React.findDOMNode(this.refs.input);
this.props.dispatch(hideAll());
input.value = '';
}
},
{
text: 'Search',
action() {
let input = React.findDOMNode(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'
}

View File

@ -11,10 +11,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}));
}
},
{

View File

@ -1,7 +1,7 @@
import { CHANGE_DIRECTORY, REFRESH, SETTINGS } from 'actions/types';
import { children } from 'api/files';
import { children, CACHE } from 'api/files';
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,10 @@ export default function(state = '', action) {
return action.dir;
}
if (action.type === REFRESH) {
CACHE[state] = null;
}
if (action.type === REFRESH || action.type === SETTINGS) {
changeTo(state);
@ -21,7 +25,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,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 { 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,15 @@ export function getKey(object = store.getState().toJS(), key) {
}
export function reportError(err) {
console.error(err);
let action = show('errorDialog', {description: err.message});
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 +45,8 @@ 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;
}
}
}

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

@ -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.0.0",
"name": "Hawk",
"description": "Keep an eye on your files with a full-featured file manager",
"launch_path": "/index.html",
@ -41,9 +41,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