mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 08:22:24 +01:00
feature (search): basic search
This commit is contained in:
parent
bbb9572012
commit
8561193f8b
16 changed files with 513 additions and 129 deletions
100
client/assets/img/close_dark.svg
Normal file
100
client/assets/img/close_dark.svg
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 51.976 51.976"
|
||||
style="enable-background:new 0 0 51.976 51.976;"
|
||||
xml:space="preserve"
|
||||
width="512px"
|
||||
height="512px"
|
||||
sodipodi:docname="close_dark.svg"
|
||||
inkscape:version="0.92.2 2405546, 2018-03-11"><metadata
|
||||
id="metadata1544"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs1542" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1894"
|
||||
inkscape:window-height="1027"
|
||||
id="namedview1540"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.921875"
|
||||
inkscape:cx="153.6191"
|
||||
inkscape:cy="213.26215"
|
||||
inkscape:window-x="10"
|
||||
inkscape:window-y="37"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="g1507" />
|
||||
<g
|
||||
id="g1507">
|
||||
<path
|
||||
d="m 45.930983,45.603013 c -1.506621,1.506622 -3.948855,1.506622 -5.455477,0 L 26.151986,31.279492 11.147494,46.283983 c -1.5066223,1.506623 -3.948855,1.506623 -5.4554772,0 -1.5066222,-1.506621 -1.5066222,-3.948855 0,-5.455477 L 20.696508,25.824014 6.3729868,11.500494 c -1.5066221,-1.5066223 -1.5066221,-3.9507841 0,-5.4554772 1.5066223,-1.5066222 3.9488552,-1.5066222 5.4554762,0 L 26.151986,20.368538 39.792606,6.727916 c 1.506623,-1.5066223 3.948856,-1.5066223 5.455478,0 1.506623,1.5066222 1.506623,3.948855 0,5.455478 l -13.640622,13.64062 14.323521,14.323523 c 1.506623,1.506621 1.506623,3.948853 0,5.455476 z"
|
||||
id="path1505"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#626469;fill-opacity:1;stroke-width:1.92909384"
|
||||
sodipodi:nodetypes="sscssscscscssscss" />
|
||||
</g>
|
||||
<g
|
||||
id="g1509">
|
||||
</g>
|
||||
<g
|
||||
id="g1511">
|
||||
</g>
|
||||
<g
|
||||
id="g1513">
|
||||
</g>
|
||||
<g
|
||||
id="g1515">
|
||||
</g>
|
||||
<g
|
||||
id="g1517">
|
||||
</g>
|
||||
<g
|
||||
id="g1519">
|
||||
</g>
|
||||
<g
|
||||
id="g1521">
|
||||
</g>
|
||||
<g
|
||||
id="g1523">
|
||||
</g>
|
||||
<g
|
||||
id="g1525">
|
||||
</g>
|
||||
<g
|
||||
id="g1527">
|
||||
</g>
|
||||
<g
|
||||
id="g1529">
|
||||
</g>
|
||||
<g
|
||||
id="g1531">
|
||||
</g>
|
||||
<g
|
||||
id="g1533">
|
||||
</g>
|
||||
<g
|
||||
id="g1535">
|
||||
</g>
|
||||
<g
|
||||
id="g1537">
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -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'){
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
<NgIf cond={this.state.path === '/'}>
|
||||
<FrequentlyAccess files={this.state.frequents}/>
|
||||
</NgIf>
|
||||
<FileSystem path={this.state.path} sort={this.state.sort} view={this.state.view}
|
||||
files={this.state.files.slice(0, this.state.page_number * LOAD_PER_SCROLL)}
|
||||
metadata={this.state.metadata} onSort={this.onSort.bind(this)} onView={this.onView.bind(this)} />
|
||||
<Submenu path={this.state.path} sort={this.state.sort} view={this.state.view} onSearch={this.onSearch.bind(this)} onViewUpdate={(value) => this.onView(value)} onSortUpdate={(value) => {this.onSort(value);}} accessRight={this.state.metadata || {}}></Submenu>
|
||||
<NgIf cond={true}>
|
||||
<FileSystem path={this.state.path} sort={this.state.sort} view={this.state.view}
|
||||
files={this.state.files.slice(0, this.state.page_number * LOAD_PER_SCROLL)}
|
||||
metadata={this.state.metadata} onSort={this.onSort.bind(this)} onView={this.onView.bind(this)} />
|
||||
</NgIf>
|
||||
<Uploader path={this.state.path} />
|
||||
</NgIf>
|
||||
</InfiniteScroll>
|
||||
|
|
|
|||
|
|
@ -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 ( <ExistingThing view={this.props.view} key={file.name+(file.icon || '')} file={file} path={this.props.path} metadata={metadata} /> );
|
||||
return ( <ExistingThing view={this.props.view} key={file.name+file.path+(file.icon || '')} file={file} path={this.props.path} metadata={metadata} /> );
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
147
client/pages/filespage/submenu.js
Normal file
147
client/pages/filespage/submenu.js
Normal file
|
|
@ -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 (
|
||||
<div className="component_submenu">
|
||||
<Container>
|
||||
<div className={"menubar no-select "+(this.state.search_input_visible ? "search_focus" : "")}>
|
||||
<NgIf cond={this.props.accessRight.can_create_file !== false} onClick={this.onNew.bind(this, 'file')} type="inline">
|
||||
New File
|
||||
</NgIf>
|
||||
<NgIf cond={this.props.accessRight.can_create_directory !== false} onClick={this.onNew.bind(this, 'directory')} type="inline">
|
||||
New Directory
|
||||
</NgIf>
|
||||
<Dropdown className="view sort" onChange={this.onSortChange.bind(this)}>
|
||||
<DropdownButton>
|
||||
<Icon name="sort"/>
|
||||
</DropdownButton>
|
||||
<DropdownList>
|
||||
<DropdownItem name="type" icon={this.props.sort === "type" ? "check" : null}> Sort By Type </DropdownItem>
|
||||
<DropdownItem name="date" icon={this.props.sort === "date" ? "check" : null}> Sort By Date </DropdownItem>
|
||||
<DropdownItem name="name" icon={this.props.sort === "name" ? "check" : null}> Sort By Name </DropdownItem>
|
||||
</DropdownList>
|
||||
</Dropdown>
|
||||
<div className="view list-grid" onClick={this.onViewChange.bind(this)}><Icon name={this.props.view === "grid" ? "list" : "grid"}/></div>
|
||||
<form onSubmit={(e) => this.onSearchKeypress(this.state.search_keyword, false, e)} className="view" style={{display: this.state.search_enabled === true ? "block" : "none"}}>
|
||||
<label className="view search" onClick={this.onSearchToggle.bind(this, null)}>
|
||||
<NgIf cond={this.state.search_input_visible !== true}>
|
||||
<Icon name="search_dark"/>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.search_input_visible === true}>
|
||||
<Icon name="close_dark"/>
|
||||
</NgIf>
|
||||
</label>
|
||||
<NgIf cond={this.state.search_input_visible !== null} type="inline">
|
||||
<input ref="$input" onBlur={this.closeIfEmpty.bind(this, false)} style={{"width": this.state.search_input_visible ? "180px" : "0px"}} value={this.state.search_keyword} onChange={(e) => this.onSearchKeypress(e.target.value, true)} type="text" id="search" placeholder="search" name="search" autoComplete="off" />
|
||||
</NgIf>
|
||||
</form>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Submenu.PropTypes = {
|
||||
accessRight: PropTypes.obj,
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
onSortUpdate: PropTypes.func.isRequired,
|
||||
sort: PropTypes.string.isRequired
|
||||
};
|
||||
77
client/pages/filespage/submenu.scss
Normal file
77
client/pages/filespage/submenu.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div>
|
||||
<div className="menubar no-select">
|
||||
<NgIf cond={this.props.accessRight.can_create_file !== false} onClick={this.onNew.bind(this, 'file')} type="inline">
|
||||
New File
|
||||
</NgIf>
|
||||
<NgIf cond={this.props.accessRight.can_create_directory !== false} onClick={this.onNew.bind(this, 'directory')} type="inline">
|
||||
New Directory
|
||||
</NgIf>
|
||||
<Dropdown className="view sort" onChange={this.onSortChange.bind(this)}>
|
||||
<DropdownButton>
|
||||
<Icon name="sort"/>
|
||||
</DropdownButton>
|
||||
<DropdownList>
|
||||
<DropdownItem name="type" icon={this.props.sort === "type" ? "check" : null}> Sort By Type </DropdownItem>
|
||||
<DropdownItem name="date" icon={this.props.sort === "date" ? "check" : null}> Sort By Date </DropdownItem>
|
||||
<DropdownItem name="name" icon={this.props.sort === "name" ? "check" : null}> Sort By Name </DropdownItem>
|
||||
</DropdownList>
|
||||
</Dropdown>
|
||||
<div className="view list-grid" onClick={this.onViewChange.bind(this)}><Icon name={this.props.view === "grid" ? "list" : "grid"}/></div>
|
||||
</div>
|
||||
<NgIf cond={this.state.type !== null} className="component_thing">
|
||||
<Card className="mouse-is-hover highlight">
|
||||
<Icon className="component_updater--icon" name={this.state.icon} />
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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){
|
||||
|
|
|
|||
48
client/worker/search.worker.js
Normal file
48
client/worker/search.worker.js
Normal file
|
|
@ -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<l; i++){
|
||||
if(file.name.toLowerCase().indexOf(keys[i]) === -1) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if(found.length > 0){
|
||||
results = results.concat(found);
|
||||
obs.next(results);
|
||||
}
|
||||
}, cache.FILE_PATH, path).then(() => {
|
||||
obs.complete(results);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function Indexing(config){
|
||||
return;
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue