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 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 + + +
+
this.onSearchKeypress(this.state.search_keyword, false, e)} className="view" style={{display: this.state.search_enabled === true ? "block" : "none"}}> + + + this.onSearchKeypress(e.target.value, true)} type="text" id="search" placeholder="search" name="search" autoComplete="off" /> + +
+
+
+
+ ); + }; +} + +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' } } ] },