diff --git a/client/assets/img/close_dark.svg b/client/assets/img/close_dark.svg
new file mode 100644
index 00000000..122890e2
--- /dev/null
+++ b/client/assets/img/close_dark.svg
@@ -0,0 +1,100 @@
+
+
+
+
\ No newline at end of file
diff --git a/client/components/icon.js b/client/components/icon.js
index 3b9c1b34..2098acd6 100644
--- a/client/components/icon.js
+++ b/client/components/icon.js
@@ -25,6 +25,7 @@ import img_arrow_right_white from '../assets/img/arrow_right_white.svg';
import img_arrow_left_white from '../assets/img/arrow_left_white.svg';
import img_more from '../assets/img/more.svg';
import img_close from '../assets/img/close.svg';
+import img_close_dark from '../assets/img/close_dark.svg';
import img_schedule from '../assets/img/schedule.svg';
import img_deadline from '../assets/img/deadline.svg';
import img_arrow_down from '../assets/img/arrow-down.svg';
@@ -92,6 +93,8 @@ export const Icon = (props) => {
img = img_more;
}else if(props.name === 'close'){
img = img_close;
+ }else if(props.name === 'close_dark'){
+ img = img_close_dark;
}else if(props.name === 'arrow_up_double'){
img = img_arrow_up_double;
}else if(props.name === 'arrow_down_double'){
diff --git a/client/helpers/cache.js b/client/helpers/cache.js
index 5ecd23d2..ecb0d9e9 100644
--- a/client/helpers/cache.js
+++ b/client/helpers/cache.js
@@ -8,7 +8,7 @@ function Data(){
}
Data.prototype._init = function(){
- const request = window.indexedDB.open('nuage', 1);
+ const request = indexedDB.open('nuage', 2);
request.onupgradeneeded = (e) => this._setup(e.target.result);
this.db = new Promise((done, err) => {
@@ -23,13 +23,14 @@ Data.prototype._init = function(){
Data.prototype._setup = function(db){
let store;
- if(!db.objectStoreNames.contains(this.FILE_PATH)){
- store = db.createObjectStore(this.FILE_PATH, {keyPath: "path"});
- }
+ db.deleteObjectStore(this.FILE_PATH);
+ db.deleteObjectStore(this.FILE_CONTENT);
- if(!db.objectStoreNames.contains(this.FILE_CONTENT)){
- store = db.createObjectStore(this.FILE_CONTENT, {keyPath: "path"});
- }
+ store = db.createObjectStore(this.FILE_PATH, {keyPath: "path"});
+ store.createIndex("idx_path", "path", { unique: true });
+
+ store = db.createObjectStore(this.FILE_CONTENT, {keyPath: "path"});
+ store.createIndex("idx_path", "path", { unique: true });
}
/*
@@ -142,30 +143,47 @@ Data.prototype.remove = function(type, path, exact = true){
}).catch(() => Promise.resolve(null))
}
-Data.prototype.fetchAll = function(fn, type = this.FILE_PATH){
+Data.prototype.fetchAll = function(fn, type = this.FILE_PATH, key = "/"){
return this.db.then((db) => {
- const tx = db.transaction(type, "readwrite");
+ const tx = db.transaction([type], "readonly");
const store = tx.objectStore(type);
- const request = store.openCursor();
+ const index = store.index("idx_path");
+ const request = index.openCursor(IDBKeyRange.lowerBound(key));
return new Promise((done, error) => {
request.onsuccess = function(event) {
const cursor = event.target.result;
if(!cursor) return done();
- const new_value = fn(cursor.value);
- cursor.continue();
+ const ret = fn(cursor.value);
+ if(ret !== false){
+ cursor.continue();
+ return
+ }
+ db.close();
};
+ request.onerror = () => {
+ db.close();
+ done();
+ }
});
}).catch(() => Promise.resolve(null))
}
Data.prototype.destroy = function(){
- this.db
- .then((db) => db.close())
- .catch(() => {})
clearTimeout(this.intervalId);
- window.indexedDB.deleteDatabase('nuage');
- this._init();
+ return new Promise((done, err) => {
+ this.db.then((db) => {
+ purgeAll(db, this.FILE_PATH);
+ purgeAll(db, this.FILE_CONTENT);
+ });
+ done();
+
+ function purgeAll(db, type){
+ const tx = db.transaction(type, "readwrite");
+ const store = tx.objectStore(type);
+ store.clear();
+ }
+ });
}
diff --git a/client/pages/filespage.helper.js b/client/pages/filespage.helper.js
index 4d4ff48c..6118279a 100644
--- a/client/pages/filespage.helper.js
+++ b/client/pages/filespage.helper.js
@@ -1,6 +1,8 @@
import { Files } from '../model/';
import { notify } from '../helpers/';
import Path from 'path';
+import Worker from "../worker/search.worker.js";
+import { Observable } from "rxjs/Observable";
export const sort = function(files, type){
if(type === 'name'){
@@ -270,3 +272,20 @@ export const onUpload = function(path, e){
);
}
};
+
+
+
+
+const worker = new Worker();9
+export const onSearch = (keyword, path = "/") => {
+ worker.postMessage({
+ action: "search::find",
+ path: path,
+ keyword: keyword
+ });
+ return new Observable((obs) => {
+ worker.onmessage = (m) => {
+ obs.next(m.data);
+ }
+ });
+};
diff --git a/client/pages/filespage.js b/client/pages/filespage.js
index 09073002..090df6f1 100644
--- a/client/pages/filespage.js
+++ b/client/pages/filespage.js
@@ -5,10 +5,10 @@ import HTML5Backend from 'react-dnd-html5-backend-filedrop';
import './filespage.scss';
import './error.scss';
import { Files } from '../model/';
-import { sort, onCreate, onRename, onDelete, onUpload } from './filespage.helper';
+import { sort, onCreate, onRename, onDelete, onUpload, onSearch } from './filespage.helper';
import { NgIf, Loader, Uploader, EventReceiver } from '../components/';
import { notify, debounce, goToFiles, goToViewer, event, settings_get, settings_put } from '../helpers/';
-import { BreadCrumb, FileSystem, FrequentlyAccess } from './filespage/';
+import { BreadCrumb, FileSystem, FrequentlyAccess, Submenu } from './filespage/';
import InfiniteScroll from 'react-infinite-scroller';
const PAGE_NUMBER_INIT = 3;
@@ -26,6 +26,7 @@ export class FilesPage extends React.Component {
show_hidden: settings_get('filespage_show_hidden') || CONFIG["display_hidden"],
view: settings_get('filespage_view') || 'grid',
files: [],
+ search_loading: false,
metadata: null,
frequents: [],
page_number: PAGE_NUMBER_INIT,
@@ -122,7 +123,9 @@ export class FilesPage extends React.Component {
});
this.observers.push(observer);
this.setState({error: null});
- Files.frequents().then((s) => this.setState({frequents: s}));
+ if(path === "/"){
+ Files.frequents().then((s) => console.log(s) && this.setState({frequents: s}));
+ }
}
_cleanupListeners(){
@@ -166,6 +169,32 @@ export class FilesPage extends React.Component {
});
}
+ onSearch(search){
+ if(search == null || search.length === 0){
+ this.onRefresh();
+ return;
+ }
+ if(search.length < 2){
+ return;
+ }
+
+ if(this._search){
+ this._search.unsubscribe();
+ }
+
+ this._search = onSearch(search, this.state.path).subscribe((message) => {
+ if(message.type === "search::found"){
+ this.setState({
+ files: message.files || [],
+ metadata: {
+ can_rename: false,
+ can_delete: false
+ }
+ });
+ }
+ });
+ }
+
loadMore(){
requestAnimationFrame(() => {
let page_number = this.state.page_number + 1;
@@ -189,9 +218,12 @@ export class FilesPage extends React.Component {
-
+ this.onView(value)} onSortUpdate={(value) => {this.onSort(value);}} accessRight={this.state.metadata || {}}>
+
+
+
diff --git a/client/pages/filespage/filesystem.js b/client/pages/filespage/filesystem.js
index 8b0afabb..5f2dfb53 100644
--- a/client/pages/filespage/filesystem.js
+++ b/client/pages/filespage/filesystem.js
@@ -35,7 +35,7 @@ export class FileSystem extends React.Component {
{
this.props.files.map((file, index) => {
if(file.type === 'directory' || file.type === 'file' || file.type === 'link' || file.type === 'bucket'){
- return ( );
+ return ( );
}
})
}
diff --git a/client/pages/filespage/index.js b/client/pages/filespage/index.js
index 41bf018f..e1ada558 100644
--- a/client/pages/filespage/index.js
+++ b/client/pages/filespage/index.js
@@ -1,3 +1,4 @@
-export { FileSystem } from './filesystem.js';
+export { FileSystem } from './filesystem';
+export { Submenu } from './submenu';
export { BreadCrumbTargettable as BreadCrumb } from './breadcrumb';
export { FrequentlyAccess } from './frequently_access';
diff --git a/client/pages/filespage/submenu.js b/client/pages/filespage/submenu.js
new file mode 100644
index 00000000..10111aae
--- /dev/null
+++ b/client/pages/filespage/submenu.js
@@ -0,0 +1,147 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Ripples from 'react-ripples';
+
+import { Card, NgIf, Icon, EventEmitter, Dropdown, DropdownButton, DropdownList, DropdownItem, Container } from '../../components/';
+import { pathBuilder, debounce } from '../../helpers/';
+import "./submenu.scss";
+
+@EventEmitter
+export class Submenu extends React.Component {
+ constructor(props){
+ super(props);
+ this.state = {
+ search_enabled: "ServiceWorker" in window ? true : false,
+ search_input_visible: false,
+ search_keyword: ""
+ };
+ this.onSearchChange_Backpressure = debounce(this.onSearchChange, 400);
+ this._onEscapeKeyPress = (e) => {
+ if(e.keyCode === 27){ // escape key
+ this.setState({
+ search_keyword: "",
+ search_input_visible: false
+ });
+ this.refs.$input.blur();
+ this.props.onSearch(null);
+ }else if(e.ctrlKey && e.keyCode === 70){ // 'Ctrl F' shortcut to search
+ e.preventDefault();
+ this.setState({
+ search_input_visible: true
+ });
+ this.refs.$input.focus();
+ }else if(e.altKey && (e.keyCode === 49 || e.keyCode === 50)){ // 'alt 1' 'alt 2' shortcut
+ e.preventDefault();
+ this.onViewChange();
+ }
+ };
+ }
+
+ componentDidMount(){
+ window.addEventListener('keydown', this._onEscapeKeyPress);
+ }
+ componentWillUnmount(){
+ window.removeEventListener('keydown', this._onEscapeKeyPress);
+ }
+
+ onNew(type){
+ this.props.emit("new::"+type);
+ }
+
+ onViewChange(){
+ requestAnimationFrame(() => this.props.onViewUpdate());
+ }
+
+ onSortChange(e){
+ this.props.onSortUpdate(e);
+ }
+
+ onSearchChange(search, e){
+ this.props.onSearch(search.trim());
+ }
+
+ onSearchToggle(){
+ if(new Date () - this.search_last_toggle < 200){
+ // avoid bluring event cancelling out the toogle
+ return;
+ }
+ this.refs.$input.focus();
+ this.setState({search_input_visible: !this.state.search_input_visible}, () => {
+ if(this.state.search_input_visible == false){
+ this.props.onSearch(null);
+ this.setState({search_keyword: ""});
+ }
+ });
+ }
+
+ closeIfEmpty(){
+ if(this.state.search_keyword.trim().length > 0) return;
+ this.search_last_toggle = new Date();
+ this.setState({
+ search_input_visible: false,
+ search_keyword: ""
+ });
+ this.props.onSearch(null);
+ }
+
+ onSearchKeypress(s, backpressure = true, e){
+ if(backpressure){
+ this.onSearchChange_Backpressure(s);
+ }else{
+ this.onSearchChange(s);
+ }
+ this.setState({search_keyword: s});
+
+ if(e && e.preventDefault){
+ e.preventDefault();
+ }
+ }
+
+ render(){
+ return (
+
+
+
+
+ New File
+
+
+ New Directory
+
+
+
+
+
+
+ Sort By Type
+ Sort By Date
+ Sort By Name
+
+
+
+
+
+
+
+ );
+ };
+}
+
+Submenu.PropTypes = {
+ accessRight: PropTypes.obj,
+ onCreate: PropTypes.func.isRequired,
+ onSortUpdate: PropTypes.func.isRequired,
+ sort: PropTypes.string.isRequired
+};
diff --git a/client/pages/filespage/submenu.scss b/client/pages/filespage/submenu.scss
new file mode 100644
index 00000000..5f4066c6
--- /dev/null
+++ b/client/pages/filespage/submenu.scss
@@ -0,0 +1,77 @@
+@import "../../assets/css/mixin.scss";
+.component_submenu{
+ .component_container{
+ padding-left: 0;
+ padding-right: 0;
+ padding-bottom: 0;
+ margin-bottom: -10px;
+
+ .menubar{
+ font-size: 15px;
+ line-height: 15px;
+ height: 15px;
+ margin-top: 5px;
+ color: var(--light);
+ margin: 5px 0 10px 0;
+
+ /* Create Buttons */
+ > span, form label {
+ display: inline-block;
+ margin-right: 5px;
+ margin-top: -2px;
+ border-radius: 2px;
+ cursor: pointer;
+
+ &:active, &:hover{
+ color: var(--color);
+ }
+ @include ripple(var(--bg-color));
+ }
+ > span{padding: 3px 5px;}
+
+ > div {cursor: pointer; margin-right: 15px;}
+ .view{
+ float: right;
+ padding: 2px;
+ transition: 0.15s ease-out background;
+ margin-right: 0px;
+ margin-left: 0px;
+ & .search, &.list-grid, > .dropdown_button, form > label {
+ min-width: inherit;
+ border-radius: 2px;
+ padding: 5px;
+ margin: 2px 0 2px 0;
+ }
+ &.list-grid, & .dropdown_button, & .search{ @include ripple(var(--bg-color)); }
+
+ .dropdown_container .component_icon{
+ box-sizing: border-box;
+ border: 2px solid rgba(0,0,0,0);
+ }
+ &.list-grid, & .search{margin-top: -5px!important;}
+ &.component_dropdown { margin-top: -10px; }
+ }
+ &.search_focus form input{transition: 0.2s width ease-in;}
+ form{
+ float: right;
+ margin-top: -3px;
+ input{
+ font-size: 1em;
+ margin-top: -3px;
+ border: none;
+ background: inherit;
+ border-bottom: 2px solid var(--light);
+ color: var(--color);
+ }
+ }
+ &.search_focus > span {
+ display: none;
+ }
+
+ .component_icon{
+ height: 15px;
+ width: 15px;
+ }
+ }
+ }
+}
diff --git a/client/pages/filespage/thing-existing.js b/client/pages/filespage/thing-existing.js
index 8b95438b..77a06dae 100644
--- a/client/pages/filespage/thing-existing.js
+++ b/client/pages/filespage/thing-existing.js
@@ -119,10 +119,9 @@ export class ExistingThing extends React.Component {
updateThumbnail(props){
if(props.view === "grid" && props.icon !== "loading"){
- const _path = path.join(props.path, props.file.name);
- const type = getMimeType(_path).split("/")[0];
+ const type = getMimeType(props.file.path).split("/")[0];
if(type === "image"){
- Files.url(_path).then((url) => {
+ Files.url(props.file.path).then((url) => {
this.setState({preview: url+"&thumbnail=true"});
});
}
diff --git a/client/pages/filespage/thing-new.js b/client/pages/filespage/thing-new.js
index 4c70eca3..5ac7e27c 100644
--- a/client/pages/filespage/thing-new.js
+++ b/client/pages/filespage/thing-new.js
@@ -1,11 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
+import Ripples from 'react-ripples';
-import { Card, NgIf, Icon, EventEmitter, Dropdown, DropdownButton, DropdownList, DropdownItem } from '../../components/';
-import { pathBuilder } from '../../helpers/';
+import { Card, NgIf, Icon, EventEmitter, EventReceiver, Dropdown, DropdownButton, DropdownList, DropdownItem } from '../../components/';
+import { pathBuilder, debounce } from '../../helpers/';
import "./thing.scss";
@EventEmitter
+@EventReceiver
export class NewThing extends React.Component {
constructor(props){
super(props);
@@ -14,19 +16,41 @@ export class NewThing extends React.Component {
type: null,
message: null,
icon: null,
- search: "ServiceWorker" in window ? "" : null
+ search_enabled: "ServiceWorker" in window ? true : false,
+ search_input_visible: false,
+ search_keyword: ""
};
this._onEscapeKeyPress = (e) => {
if(e.keyCode === 27) this.onDelete();
};
+ this._onSearchEvent = debounce((state) => {
+ if(typeof state === "boolean"){
+ if(this.state.search_keyword.length != 0) return;
+ this.setState({search_input_visible: state});
+ return;
+ }
+ this.setState({search_input_visible: !this.state.search_input_visible});
+ }, 200);
+
+ this.onPropageSearch = debounce(() => {
+ this.props.onSearch(this.state.search_keyword);
+ }, 1000);
}
componentDidMount(){
window.addEventListener('keydown', this._onEscapeKeyPress);
+ this.props.subscribe('new::file', () => {
+ this.onNew("file");
+ });
+ this.props.subscribe('new::directory', () => {
+ this.onNew("directory");
+ });
}
componentWillUnmount(){
window.removeEventListener('keydown', this._onEscapeKeyPress);
+ this.props.unsubscribe('new::file');
+ this.props.unsubscribe('new::directory');
}
onNew(type){
@@ -57,28 +81,14 @@ export class NewThing extends React.Component {
this.props.onSortUpdate(e);
}
+ onSearchChange(search){
+ this.setState({search_keyword: search});
+ console.log(search);
+ }
+
render(){
return (
-
-
- New File
-
-
- New Directory
-
-
-
-
-
-
- Sort By Type
- Sort By Date
- Sort By Name
-
-
-
-
diff --git a/client/pages/filespage/thing.scss b/client/pages/filespage/thing.scss
index ef9e6900..6d4869e4 100644
--- a/client/pages/filespage/thing.scss
+++ b/client/pages/filespage/thing.scss
@@ -1,79 +1,3 @@
-@import "../../assets/css/mixin.scss";
-
-// Menu for new file and directory
-.menubar{
- font-size: 15px;
- line-height: 15px;
- height: 15px;
- margin-top: 5px;
- color: var(--light);
- margin: 5px 0 10px 0;
-
- /* Create Buttons */
- > span {
- display: inline-block;
- margin-right: 5px;
- margin-top: -2px;
- border-radius: 2px;
- cursor: pointer;
- padding: 3px 5px;
-
- &:active, &:hover{
- color: var(--color);
- }
- @include ripple(var(--bg-color));
- }
-
- > div {cursor: pointer; margin-right: 15px;}
- .view{
- float: right;
- padding: 2px;
- transition: 0.15s ease-out background;
- margin-right: 0px;
- margin-left: 0px;
- &.list-grid, > .dropdown_button, > label > img{
- min-width: inherit;
- border-radius: 2px;
- padding: 5px;
- margin: 2px 0 2px 0;
- }
- &.list-grid, & .dropdown_button{ @include ripple(var(--bg-color)); }
-
- .dropdown_container .component_icon{
- box-sizing: border-box;
- border: 2px solid rgba(0,0,0,0);
- }
- &.list-grid{margin-top: -5px!important;}
- &.component_dropdown { margin-top: -10px; }
- }
- .search{
- float: right;
- margin-right: 0;
- label{
- margin-top: -5px;
- display: block;
- img{
- padding: 5px;
- vertical-align: bottom;
- &:hover, &:focus{
- background: white;
- border-color: var(--bg-color);
- box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
- }
- }
- input{
- border: none;
- float: left;
- font-size: 1em;
- }
- }
- }
- .component_icon{
- height: 15px;
- width: 15px;
- }
-}
-
.component_thing{
clear: both;
&:hover .box, .highlight.box{
diff --git a/client/pages/viewerpage/pager.js b/client/pages/viewerpage/pager.js
index d496e2a8..0d8433a2 100644
--- a/client/pages/viewerpage/pager.js
+++ b/client/pages/viewerpage/pager.js
@@ -4,7 +4,7 @@ import { Link, withRouter } from 'react-router-dom';
import { Files } from '../../model/';
import { sort } from '../../pages/filespage.helper.js';
import { Icon, NgIf, EventReceiver, EventEmitter } from '../../components/';
-import { dirname, basename, settings_get, getMimeType, debounce } from '../../helpers/';
+import { dirname, basename, settings_get, getMimeType, debounce, gid } from '../../helpers/';
import './pager.scss';
@@ -57,7 +57,7 @@ export class Pager extends React.Component {
navigatePage(n){
if(this.state.files[n]){
- this.props.history.push(this.state.files[n].link);
+ this.props.history.push(this.state.files[n].link+"?once="+gid());
if(this.refs.$page) this.refs.$page.blur();
let preload_index = (n >= this.state.n || (this.state.n === this.state.files.length - 1 && n === 0)) ? this.calculateNextPageNumber(n) : this.calculatePrevPageNumber(n);
if(!this.state.files[preload_index].path){
diff --git a/client/worker/search.worker.js b/client/worker/search.worker.js
new file mode 100644
index 00000000..fc85b0cf
--- /dev/null
+++ b/client/worker/search.worker.js
@@ -0,0 +1,48 @@
+import { Observable } from 'rxjs/Observable';
+import { cache } from '../helpers/cache';
+
+let current_search = null;
+
+self.onmessage = function(message){
+ if(message.data.action === "search::find"){
+ if(current_search != null){
+ current_search.unsubscribe();
+ }
+ current_search = Search(message.data.path, message.data.keyword).subscribe((a) => {
+ self.postMessage({type: "search::found", files: a});
+ }, null, () => {
+ self.postMessage({type: "search::completed"})
+ });
+ }else if(message.data.action === "search::index"){
+ Indexing(message.data.config);
+ }
+}
+
+function Search(path, keyword){
+ let results = [];
+ return new Observable((obs) => {
+ obs.next(results);
+ const keys = keyword.split(" ").map((e) => e.toLowerCase());
+ cache.fetchAll((record) => {
+ const found = record.results.filter((file) => {
+ for(let i=0, l=keys.length; i 0){
+ results = results.concat(found);
+ obs.next(results);
+ }
+ }, cache.FILE_PATH, path).then(() => {
+ obs.complete(results);
+ });
+ });
+}
+
+
+
+
+function Indexing(config){
+ return;
+}
diff --git a/package.json b/package.json
index 417fc4be..70d3eb38 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
"wavesurfer.js": "^1.4.0",
"webpack": "^2.7.0",
"webpack-bundle-analyzer": "^2.8.2",
- "webpack-dev-server": "^3.1.0"
+ "webpack-dev-server": "^3.1.0",
+ "worker-loader": "^2.0.0"
}
}
diff --git a/webpack.config.js b/webpack.config.js
index f4fa421d..17459965 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -40,6 +40,11 @@ let config = {
{
test: /\.(pdf|jpg|png|gif|svg|ico|woff|woff2|eot|ttf)$/,
loader: "url-loader"
+ },
+ {
+ test: /[a-z]+\.worker\.js$/,
+ loader: "worker-loader",
+ options: { name: 'assets/js/[name]_[hash].js' }
}
]
},