diff --git a/client/assets/css/reset.scss b/client/assets/css/reset.scss
index 88de1147..3bfc7cdc 100644
--- a/client/assets/css/reset.scss
+++ b/client/assets/css/reset.scss
@@ -35,19 +35,17 @@ a{color: inherit; text-decoration: none;}
select{-moz-appearance: none;}
select:-moz-focusring {
- color: inherit;
+ color: transparent;
outline: none;
border: none;
}
-select::-ms-expand {
- display: none;
-}
+select::-ms-expand { display: none; }
+
button::-moz-focus-inner {
border: 0;
}
-
input, textarea{
transition: border 0.2s;
outline: none;
diff --git a/client/assets/img/check.svg b/client/assets/img/check.svg
new file mode 100644
index 00000000..43bbf0e2
--- /dev/null
+++ b/client/assets/img/check.svg
@@ -0,0 +1,52 @@
+
+
diff --git a/client/assets/img/file.svg b/client/assets/img/file.svg
index 7842cf00..38d84d87 100644
--- a/client/assets/img/file.svg
+++ b/client/assets/img/file.svg
@@ -40,12 +40,20 @@
id="namedview4654"
showgrid="false"
inkscape:zoom="49.75"
- inkscape:cx="8.4924623"
+ inkscape:cx="7.4070352"
inkscape:cy="8"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
- inkscape:current-layer="svg4652" />
+ inkscape:current-layer="svg4652"
+ showguides="false"
+ inkscape:guide-bbox="true">
+
+
+ sodipodi:nodetypes="ccc" />
diff --git a/client/assets/img/grid.svg b/client/assets/img/grid.svg
new file mode 100644
index 00000000..01de071a
--- /dev/null
+++ b/client/assets/img/grid.svg
@@ -0,0 +1,55 @@
+
+
diff --git a/client/assets/img/list.svg b/client/assets/img/list.svg
new file mode 100644
index 00000000..46e90c38
--- /dev/null
+++ b/client/assets/img/list.svg
@@ -0,0 +1,55 @@
+
+
diff --git a/client/assets/img/placeholder.png b/client/assets/img/placeholder.png
new file mode 100644
index 00000000..2ae5caba
Binary files /dev/null and b/client/assets/img/placeholder.png differ
diff --git a/client/assets/img/sort.svg b/client/assets/img/sort.svg
new file mode 100644
index 00000000..b83c1031
--- /dev/null
+++ b/client/assets/img/sort.svg
@@ -0,0 +1,52 @@
+
+
diff --git a/client/components/breadcrumb.scss b/client/components/breadcrumb.scss
index ba868662..0b0a2eb9 100644
--- a/client/components/breadcrumb.scss
+++ b/client/components/breadcrumb.scss
@@ -62,7 +62,7 @@
font-size: 18px;
display: inline-block;
- a{border-radius: 2px;}
+ a{border-radius: 2px; letter-spacing: -0.5px;}
&.highlight a{
background: var(--emphasis-primary);
border: 2px solid var(--primary);
diff --git a/client/components/dropdown.js b/client/components/dropdown.js
index 4e5d9c33..a501b6c4 100644
--- a/client/components/dropdown.js
+++ b/client/components/dropdown.js
@@ -59,9 +59,11 @@ export class Dropdown extends React.Component {
const button = this.props.children[0];
const dropdown = React.cloneElement(this.props.children[1], {onSelect: this.onSelect.bind(this)});
-
+ let className = "component_dropdown ";
+ className += this.props.className ? this.props.className+" " : "";
+ className += this.state.button ? " active" : "";
return (
-
+
{ button }
{ dropdown }
diff --git a/client/components/icon.js b/client/components/icon.js
index 38e16e07..ee8da3f7 100644
--- a/client/components/icon.js
+++ b/client/components/icon.js
@@ -29,8 +29,13 @@ import img_arrow_down from '../assets/img/arrow-down.svg';
import img_arrow_up_double from '../assets/img/arrow-up-double.svg';
import img_arrow_down_double from '../assets/img/arrow-down-double.svg';
import img_search from '../assets/img/search.svg';
+import img_grid from '../assets/img/grid.svg';
+import img_list from '../assets/img/list.svg';
+import img_sort from '../assets/img/sort.svg';
+import img_check from '../assets/img/check.svg';
export const Icon = (props) => {
+ if(props.name === null) return null;
let img;
if(props.name === 'directory'){
img = img_folder;
@@ -84,6 +89,14 @@ export const Icon = (props) => {
img = img_arrow_down;
}else if(props.name === 'search'){
img = img_search;
+ }else if(props.name === 'grid'){
+ img = img_grid;
+ }else if(props.name === 'list'){
+ img = img_list;
+ }else if(props.name === 'sort'){
+ img = img_sort;
+ }else if(props.name === 'check'){
+ img = img_check;
}else{
throw('unknown icon');
}
diff --git a/client/helpers/index.js b/client/helpers/index.js
index bc70d0bd..6d189ca2 100644
--- a/client/helpers/index.js
+++ b/client/helpers/index.js
@@ -12,3 +12,5 @@ export { prompt } from './prompt';
export { notify } from './notify';
export { gid } from './random';
export { leftPad } from './common';
+export { getMimeType } from './mimetype';
+export { settings_get, settings_put } from './settings';
diff --git a/client/helpers/memory.js b/client/helpers/memory.js
index 1ef9b2eb..e70bcd43 100644
--- a/client/helpers/memory.js
+++ b/client/helpers/memory.js
@@ -9,6 +9,9 @@ function Memory(){
set: function(key, value){
if(!data[key]) data[key] = {};
data[key] = value;
+ },
+ all: function(){
+ return data;
}
}
}
diff --git a/client/helpers/mimetype.js b/client/helpers/mimetype.js
index d4e81b1c..bb6a61dc 100644
--- a/client/helpers/mimetype.js
+++ b/client/helpers/mimetype.js
@@ -20,11 +20,13 @@ export function opener(file){
return 'image';
}else if(['application/javascript', 'application/xml', 'application/json', 'application/x-perl'].indexOf(mime) !== -1){
return 'editor';
- }else if(['audio/wav', 'audio/mp3', 'audio/flac'].indexOf(mime) !== -1){
+ }else if(['audio/wav', 'audio/mp3', 'audio/flac', 'audio/ogg'].indexOf(mime) !== -1){
return 'audio';
- }else if(['video/webm', 'video/mp4', 'application/ogg'].indexOf(mime) !== -1){
+ }else if(['video/webm', 'video/mp4'].indexOf(mime) !== -1){
return 'video';
- }else{
+ }else if(mime.split('/')[0] === "application")
+ return 'download';
+ else{
return 'editor';
}
}
@@ -55,7 +57,6 @@ const db = {
'mp4': 'video/mp4',
'mov': 'video/quicktime',
'avi': 'video/x-msvideo',
- 'ogg': 'application/ogg',
'ogv': 'application/ogg',
'js': 'application/javascript',
'xml': 'application/xml',
@@ -166,7 +167,6 @@ const db = {
"mid": "audio/midi",
"kar": "audio/midi",
"midi": "audio/midi",
- "ogg": "audio/ogg",
"m4a": "audio/x-m4a",
"ra": "audio/x-realaudio",
"swf": "application/x-shockwave-flash",
diff --git a/client/helpers/settings.js b/client/helpers/settings.js
new file mode 100644
index 00000000..e6373fea
--- /dev/null
+++ b/client/helpers/settings.js
@@ -0,0 +1,16 @@
+let settings = JSON.parse(window.localStorage.getItem("settings")) || {};
+
+export function settings_get(key){
+ return settings[key];
+}
+
+export function settings_put(key, value){
+ settings[key] = value;
+ save(settings);
+}
+
+function save(d){
+ setInterval(() => {
+ window.localStorage.setItem("settings", JSON.stringify(d));
+ }, 500);
+}
diff --git a/client/pages/filespage.helper.js b/client/pages/filespage.helper.js
new file mode 100644
index 00000000..8ed9e986
--- /dev/null
+++ b/client/pages/filespage.helper.js
@@ -0,0 +1,268 @@
+import { Files } from '../model/';
+import { notify } from '../helpers/';
+import Path from 'path';
+
+export const sort = function(files, type){
+ if(type === 'name'){
+ return sortByName(files);
+ }else if(type === 'date'){
+ return sortByDate(files);
+ }else{
+ return sortByType(files);
+ }
+ function sortByType(files){
+ return files.sort((fileA, fileB) => {
+ if(fileA.icon === 'loading' && fileB.icon !== 'loading'){return +1;}
+ else if(fileA.icon !== 'loading' && fileB.icon === 'loading'){return -1;}
+ else{
+ if(['directory', 'link'].indexOf(fileA.type) === -1 && ['directory', 'link'].indexOf(fileB.type) !== -1){
+ return +1;
+ }else if(['directory', 'link'].indexOf(fileA.type) !== -1 && ['directory', 'link'].indexOf(fileB.type) === -1){
+ return -1;
+ }else{
+ if(fileA.name[0] === "." && fileB.name[0] !== ".") return +1;
+ else if(fileA.name[0] !== "." && fileB.name[0] === ".") return -1;
+ else{
+ if(fileA.name.toLowerCase() === fileB.name.toLowerCase()){
+ return fileA.name > fileB.name ? +1 : -1;
+ }
+ return fileA.name.toLowerCase() > fileB.name.toLowerCase() ? +1 : -1;
+ }
+ }
+ }
+ });
+ }
+ function sortByName(files){
+ return files.sort((fileA, fileB) => {
+ if(fileA.icon === 'loading' && fileB.icon !== 'loading'){return +1;}
+ else if(fileA.icon !== 'loading' && fileB.icon === 'loading'){return -1;}
+ else{
+ if(fileA.name[0] === "." && fileB.name[0] !== ".") return +1;
+ else if(fileA.name[0] !== "." && fileB.name[0] === ".") return -1;
+ else{
+ if(fileA.name.toLowerCase() === fileB.name.toLowerCase()){
+ return fileA.name > fileB.name ? +1 : -1;
+ }
+ return fileA.name.toLowerCase() > fileB.name.toLowerCase() ? +1 : -1;
+ }
+ }
+ });
+ }
+ function sortByDate(files){
+ return files.sort((fileA, fileB) => {
+ if(fileA.icon === 'loading' && fileB.icon !== 'loading'){return +1;}
+ else if(fileA.icon !== 'loading' && fileB.icon === 'loading'){return -1;}
+ else{
+ if(fileB.time === fileA.time){
+ return fileA.name > fileB.name ? +1 : -1;
+ }
+ return fileB.time - fileA.time;
+ }
+ });
+ }
+};
+
+export const onCreate = function(path, type, file){
+ if(type === 'file'){
+ return Files.touch(path, file)
+ .then(() => {
+ notify.send('A file named "'+Path.basename(path)+'" was created', 'success');
+ return Promise.resolve();
+ })
+ .catch((err) => {
+ notify.send(err, 'error');
+ return Promise.reject(err);
+ });
+ }else if(type === 'directory'){
+ return Files.mkdir(path)
+ .then(() => notify.send('A folder named "'+Path.basename(path)+'" was created', 'success'))
+ .catch((err) => notify.send(err, 'error'));
+ }else{
+ return Promise.reject({message: 'internal error: can\'t create a '+type.toString(), code: 'UNKNOWN_TYPE'});
+ }
+};
+
+export const onRename = function(from, to, type){
+ return Files.mv(from, to, type)
+ .then(() => notify.send('The file "'+Path.basename(from)+'" was renamed', 'success'))
+ .catch((err) => notify.send(err, 'error'));
+};
+
+export const onDelete = function(path, type){
+ return Files.rm(path, type)
+ .then(() => notify.send('The file "'+Path.basename(path)+'" was deleted', 'success'))
+ .catch((err) => notify.send(err, 'error'));
+};
+
+export const onUpload = function(path, e){
+ const MAX_POOL_SIZE = 10;
+ let PRIOR_STATUS = {};
+ extract_upload_directory_the_way_that_works_but_non_official(e.dataTransfer.items || [], [])
+ .then((files) => {
+ if(files.length === 0){
+ return extract_upload_crappy_hack_but_official_way(e.dataTransfer);
+ }
+ return Promise.resolve(files);
+ })
+ .then((files) => {
+ const processes = files.map((file) => {
+ file.path = Path.join(path, file.path);
+ if(file.type === 'file'){
+ Files.touch(file.path, file.file, 'prepare_only');
+ return {
+ parent: file._prior || null,
+ fn: Files.touch.bind(Files, file.path, file.file, 'execute_only')
+ };
+ }else{
+ Files.mkdir(file.path, 'prepare_only');
+ return {
+ id: file._id || null,
+ parent: file._prior || null,
+ fn: Files.mkdir.bind(Files, file.path, 'execute_only')
+ };
+ }
+ });
+ function runner(id){
+ let current_process = null;
+ if(processes.length === 0) return Promise.resolve();
+
+ for(let i=0; i
{
+ if(current_process.id) PRIOR_STATUS[current_process.id] = true;
+ return runner(id);
+ })
+ .catch((err) => {
+ notify.send(err, 'error');
+ return runner(id);
+ });
+ }else{
+ return waitABit()
+ .then(() => runner(id));
+ function waitABit(){
+ return new Promise((done) => {
+ window.setTimeout(() => {
+ requestAnimationFrame(() => {
+ done();
+ });
+ }, 100);
+ });
+ }
+ }
+ }
+
+ Promise.all(Array.apply(null, Array(MAX_POOL_SIZE)).map((process,index) => {
+ return runner();
+ })).then(() => {
+ notify.send('Upload completed', 'success');
+ }).catch((err) => {
+ notify.send(err, 'error');
+ });
+ });
+
+
+
+ // adapted from: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
+ function _rand_id(){
+ function s4() {
+ return Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
+ }
+ return s4() + s4() + s4() + s4();
+ }
+
+ function extract_upload_directory_the_way_that_works_but_non_official(items, files = []){
+ const traverseDirectory = (item, _files, parent_id) => {
+ let file = {
+ path: item.fullPath
+ };
+ if(item.isFile){
+ return new Promise((done, err) => {
+ file.type = "file";
+ item.file((_file, _err) => {
+ if(!_err){
+ file.file = _file;
+ if(parent_id) file._prior = parent_id;
+ _files.push(file);
+ }
+ done(_files);
+ });
+ });
+ }else if(item.isDirectory){
+ file.type = "directory";
+ file.path += "/";
+ file._id = _rand_id();
+ if(parent_id) file._prior = parent_id;
+ _files.push(file);
+
+ return new Promise((done, err) => {
+ item.createReader().readEntries(function(entries){
+ Promise.all(entries.map((entry) => {
+ return traverseDirectory(entry, _files, file._id);
+ })).then(() => done(_files));
+ });
+ });
+ }else{
+ return Promise.resolve();
+ }
+ };
+ return Promise.all(
+ Array.prototype.slice.call(items).map((item) => {
+ if(typeof item.webkitGetAsEntry === 'function'){
+ return traverseDirectory(item.webkitGetAsEntry(), files.slice(0));
+ }
+ }).filter((e) => e)
+ ).then((res) => Promise.resolve([].concat.apply([], res)));
+ }
+
+ function extract_upload_crappy_hack_but_official_way(data){
+ const _files = data.files;
+ return Promise.all(
+ Array.prototype.slice.call(_files).map((_file) => {
+ return detectType(_file)
+ .then(transform);
+ function detectType(_f){
+ // the 4096 is an heuristic I've observed and taken from: https://stackoverflow.com/questions/25016442/how-to-distinguish-if-a-file-or-folder-is-being-dragged-prior-to-it-being-droppe
+ // however the proposed answer is just wrong as it doesn't consider folder with name such as: test.png
+ // and as Stackoverflow favor consanguinity with their point system, I couldn't rectify the proposed answer.
+ // The following code is actually working as expected
+ if(_file.size % 4096 !== 0){
+ return Promise.resolve('file');
+ }
+ return new Promise((done, err) => {
+ let reader = new window.FileReader();
+ reader.onload = function() {
+ done('file');
+ };
+ reader.onerror = function() {
+ done('directory');
+ };
+ reader.readAsText(_f);
+ });
+ }
+
+ function transform(_type){
+ let file = {
+ type: _type,
+ path: _file.name
+ };
+ if(file.type === 'file'){
+ file.file = _file;
+ }else{
+ file.path += "/";
+ }
+ return Promise.resolve(file);
+ }
+ })
+ );
+ }
+};
diff --git a/client/pages/filespage.js b/client/pages/filespage.js
index 3e6e6147..6fee41fe 100644
--- a/client/pages/filespage.js
+++ b/client/pages/filespage.js
@@ -1,14 +1,17 @@
import React from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend-filedrop';
-import Path from 'path';
import './filespage.scss';
import './error.scss';
import { Files } from '../model/';
+import { sort, onCreate, onRename, onDelete, onUpload } from './filespage.helper';
import { NgIf, Loader, Uploader, EventReceiver } from '../components/';
-import { notify, debounce, goToFiles, goToViewer, event } from '../helpers/';
+import { notify, debounce, goToFiles, goToViewer, event, settings_get, settings_put } from '../helpers/';
import { BreadCrumb, FileSystem, FrequentlyAccess } from './filespage/';
+import InfiniteScroll from 'react-infinite-scroller';
+
+const PAGE_NUMBER_INIT = 3;
@EventReceiver
@DragDropContext(('ontouchstart' in window)? HTML5Backend : HTML5Backend)
@@ -17,11 +20,13 @@ export class FilesPage extends React.Component {
super(props);
this.state = {
path: props.match.url.replace('/files', '') || '/',
+ sort: settings_get('filespage_sort') || 'type',
+ view: settings_get('filespage_view') || 'grid',
files: [],
frequents: [],
+ page_number: PAGE_NUMBER_INIT,
loading: true,
- error: null,
- height: null
+ error: null
};
this.goToFiles = goToFiles.bind(null, this.props.history);
@@ -33,10 +38,10 @@ export class FilesPage extends React.Component {
this.onRefresh(this.state.path, 'directory');
// subscriptions
- this.props.subscribe('file.upload', this.onUpload.bind(this));
- this.props.subscribe('file.create', this.onCreate.bind(this));
- this.props.subscribe('file.rename', this.onRename.bind(this));
- this.props.subscribe('file.delete', this.onDelete.bind(this));
+ this.props.subscribe('file.upload', onUpload.bind(this));
+ this.props.subscribe('file.create', onCreate.bind(this));
+ this.props.subscribe('file.rename', onRename.bind(this));
+ this.props.subscribe('file.delete', onDelete.bind(this));
this.props.subscribe('file.refresh', this.onRefresh.bind(this));
this.hideError();
@@ -79,7 +84,7 @@ export class FilesPage extends React.Component {
file.link = file.type === "file" ? "/view"+path : "/files"+path+"/";
return file;
});
- this.setState({files: files, loading: false});
+ this.setState({files: sort(files, this.state.sort), loading: false, page_number: PAGE_NUMBER_INIT});
}else{
notify.send(res, 'error');
}
@@ -100,209 +105,40 @@ export class FilesPage extends React.Component {
}
}
- onCreate(path, type, file){
- if(type === 'file'){
- return Files.touch(path, file)
- .then(() => {
- notify.send('A file named "'+Path.basename(path)+'" was created', 'success');
- return Promise.resolve();
- })
- .catch((err) => {
- notify.send(err, 'error');
- return Promise.reject(err);
- });
- }else if(type === 'directory'){
- return Files.mkdir(path)
- .then(() => notify.send('A folder named "'+Path.basename(path)+'" was created', 'success'))
- .catch((err) => notify.send(err, 'error'));
- }else{
- return Promise.reject({message: 'internal error: can\'t create a '+type.toString(), code: 'UNKNOWN_TYPE'});
- }
- }
- onRename(from, to, type){
- return Files.mv(from, to, type)
- .then(() => notify.send('The file "'+Path.basename(from)+'" was renamed', 'success'))
- .catch((err) => notify.send(err, 'error'));
- }
- onDelete(path, type){
- return Files.rm(path, type)
- .then(() => notify.send('The file "'+Path.basename(path)+'" was deleted', 'success'))
- .catch((err) => notify.send(err, 'error'));
- }
-
- onUpload(path, e){
- const MAX_POOL_SIZE = 10;
- let PRIOR_STATUS = {};
- extract_upload_directory_the_way_that_works_but_non_official(e.dataTransfer.items || [], [])
- .then((files) => {
- if(files.length === 0){
- return extract_upload_crappy_hack_but_official_way(e.dataTransfer);
- }
- return Promise.resolve(files);
- })
- .then((files) => {
- const processes = files.map((file) => {
- file.path = Path.join(path, file.path);
- if(file.type === 'file'){
- Files.touch(file.path, file.file, 'prepare_only');
- return {
- parent: file._prior || null,
- fn: Files.touch.bind(Files, file.path, file.file, 'execute_only')
- };
- }else{
- Files.mkdir(file.path, 'prepare_only');
- return {
- id: file._id || null,
- parent: file._prior || null,
- fn: Files.mkdir.bind(Files, file.path, 'execute_only')
- };
- }
- });
- function runner(id){
- let current_process = null;
- if(processes.length === 0) return Promise.resolve();
-
- for(let i=0; i {
- if(current_process.id) PRIOR_STATUS[current_process.id] = true;
- return runner(id);
- })
- .catch((err) => {
- notify.send(err, 'error');
- return runner(id);
- });
- }else{
- return waitABit()
- .then(() => runner(id));
- function waitABit(){
- return new Promise((done) => {
- window.setTimeout(() => {
- requestAnimationFrame(() => {
- done();
- });
- }, 100);
- });
- }
- }
- }
-
- Promise.all(Array.apply(null, Array(MAX_POOL_SIZE)).map((process,index) => {
- return runner();
- })).then(() => {
- notify.send('Upload completed', 'success');
- }).catch((err) => {
- notify.send(err, 'error');
+ onSort(_sort){
+ settings_put('filespage_sort', _sort);
+ this.setState({
+ sort: _sort
+ }, () => {
+ requestAnimationFrame(() => {
+ this.setState({
+ page_number: PAGE_NUMBER_INIT,
+ files: sort(this.state.files, _sort)
});
});
-
-
-
- // adapted from: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
- function _rand_id(){
- function s4() {
- return Math.floor((1 + Math.random()) * 0x10000)
- .toString(16)
- .substring(1);
- }
- return s4() + s4() + s4() + s4();
- }
-
- function extract_upload_directory_the_way_that_works_but_non_official(items, files = []){
- const traverseDirectory = (item, _files, parent_id) => {
- let file = {
- path: item.fullPath
- };
- if(item.isFile){
- return new Promise((done, err) => {
- file.type = "file";
- item.file((_file, _err) => {
- if(!_err){
- file.file = _file;
- if(parent_id) file._prior = parent_id;
- _files.push(file);
- }
- done(_files);
- });
- });
- }else if(item.isDirectory){
- file.type = "directory";
- file.path += "/";
- file._id = _rand_id();
- if(parent_id) file._prior = parent_id;
- _files.push(file);
-
- return new Promise((done, err) => {
- item.createReader().readEntries(function(entries){
- Promise.all(entries.map((entry) => {
- return traverseDirectory(entry, _files, file._id);
- })).then(() => done(_files));
- });
- });
- }else{
- return Promise.resolve();
- }
- };
- return Promise.all(
- Array.prototype.slice.call(items).map((item) => {
- if(typeof item.webkitGetAsEntry === 'function'){
- return traverseDirectory(item.webkitGetAsEntry(), files.slice(0));
- }
- }).filter((e) => e)
- ).then((res) => Promise.resolve([].concat.apply([], res)));
- }
-
- function extract_upload_crappy_hack_but_official_way(data){
- const _files = data.files;
- return Promise.all(
- Array.prototype.slice.call(_files).map((_file) => {
- return detectType(_file)
- .then(transform);
- function detectType(_f){
- // the 4096 is an heuristic I've observed and taken from: https://stackoverflow.com/questions/25016442/how-to-distinguish-if-a-file-or-folder-is-being-dragged-prior-to-it-being-droppe
- // however the proposed answer is just wrong as it doesn't consider folder with name such as: test.png
- // and as Stackoverflow favor consanguinity with their point system, I couldn't rectify the proposed answer.
- // The following code is actually working as expected
- if(_file.size % 4096 !== 0){
- return Promise.resolve('file');
- }
- return new Promise((done, err) => {
- let reader = new window.FileReader();
- reader.onload = function() {
- done('file');
- };
- reader.onerror = function() {
- done('directory');
- };
- reader.readAsText(_f);
- });
- }
-
- function transform(_type){
- let file = {
- type: _type,
- path: _file.name
- };
- if(file.type === 'file'){
- file.file = _file;
- }else{
- file.path += "/";
- }
- return Promise.resolve(file);
- }
- })
- );
- }
+ });
}
+ onView(){
+ const _view = this.state.view === "list" ? "grid" : "list";
+ settings_put('filespage_view', _view);
+ this.setState({
+ view: _view
+ }, () => {
+ requestAnimationFrame(() => {
+ this.setState({
+ page_number: PAGE_NUMBER_INIT
+ });
+ });
+ });
+ }
+
+ loadMore(){
+ requestAnimationFrame(() => {
+ let page_number = this.state.page_number + 1;
+ this.setState({page_number: page_number});
+ });
+ }
render() {
return (
@@ -310,13 +146,15 @@ export class FilesPage extends React.Component {
-
-
-
+ 70} initialLoad={false} useWindow={false} loadMore={this.loadMore.bind(this)} threshold={40} loader={
}>
+
+
+
+
+
+
-
-
-
+
diff --git a/client/pages/filespage.scss b/client/pages/filespage.scss
index 08fa522f..09602a7c 100644
--- a/client/pages/filespage.scss
+++ b/client/pages/filespage.scss
@@ -15,6 +15,16 @@
display: flex;
overflow: hidden;
}
+
+ .infinite_scroll_loading{
+ text-align: center;
+ .component_loader{
+ margin-top: -50px;
+ svg{
+ height: 40px;
+ }
+ }
+ }
}
.scroll-y{
diff --git a/client/pages/filespage/filesystem.js b/client/pages/filespage/filesystem.js
index f7a922d9..7d0f0a63 100644
--- a/client/pages/filespage/filesystem.js
+++ b/client/pages/filespage/filesystem.js
@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import { DropTarget } from 'react-dnd';
+
import Path from 'path';
import "./filesystem.scss";
@@ -19,8 +20,7 @@ export class FileSystem extends React.Component {
super(props);
this.state = {
creating: null,
- access_right: this._findAccessRight(props.files),
- sort: 'type'
+ access_right: this._findAccessRight(props.files)
};
}
@@ -34,66 +34,6 @@ export class FileSystem extends React.Component {
return {can_create_file: true, can_create_directory: true};
}
- sort(files, type){
- if(type === 'name'){
- return sortByName(files);
- }else if(type === 'date'){
- return sortByDate(files);
- }else{
- return sortByType(files);
- }
- function sortByType(files){
- return files.sort((fileA, fileB) => {
- if(fileA.icon === 'loading' && fileB.icon !== 'loading'){return +1;}
- else if(fileA.icon !== 'loading' && fileB.icon === 'loading'){return -1;}
- else{
- if(['directory', 'link'].indexOf(fileA.type) === -1 && ['directory', 'link'].indexOf(fileB.type) !== -1){
- return +1;
- }else if(['directory', 'link'].indexOf(fileA.type) !== -1 && ['directory', 'link'].indexOf(fileB.type) === -1){
- return -1;
- }else{
- if(fileA.name[0] === "." && fileB.name[0] !== ".") return +1;
- else if(fileA.name[0] !== "." && fileB.name[0] === ".") return -1;
- else{
- if(fileA.name.toLowerCase() === fileB.name.toLowerCase()){
- return fileA.name > fileB.name ? +1 : -1;
- }
- return fileA.name.toLowerCase() > fileB.name.toLowerCase() ? +1 : -1;
- }
- }
- }
- });
- }
- function sortByName(files){
- return files.sort((fileA, fileB) => {
- if(fileA.icon === 'loading' && fileB.icon !== 'loading'){return +1;}
- else if(fileA.icon !== 'loading' && fileB.icon === 'loading'){return -1;}
- else{
- if(fileA.name[0] === "." && fileB.name[0] !== ".") return +1;
- else if(fileA.name[0] !== "." && fileB.name[0] === ".") return -1;
- else{
- if(fileA.name.toLowerCase() === fileB.name.toLowerCase()){
- return fileA.name > fileB.name ? +1 : -1;
- }
- return fileA.name.toLowerCase() > fileB.name.toLowerCase() ? +1 : -1;
- }
- }
- });
- }
- function sortByDate(files){
- return files.sort((fileA, fileB) => {
- if(fileA.icon === 'loading' && fileB.icon !== 'loading'){return +1;}
- else if(fileA.icon !== 'loading' && fileB.icon === 'loading'){return -1;}
- else{
- if(fileB.time === fileA.time){
- return fileA.name > fileB.name ? +1 : -1;
- }
- return fileB.time - fileA.time;
- }
- });
- }
- }
-
onComponentPropsUpdate(props){
this.setState({access_right: this._findAccessRight(props.files)});
}
@@ -102,16 +42,16 @@ export class FileSystem extends React.Component {
return this.props.connectDropFile(
- {this.setState({sort: value})}} accessRight={this.state.access_right}>
+ this.props.onView(value)} onSortUpdate={(value) => {this.props.onSort(value);}} accessRight={this.state.access_right}>
0}>
{
- this.sort(this.props.files, this.state.sort).map((file, index) => {
+ 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/thing-existing.js b/client/pages/filespage/thing-existing.js
index d016641b..bc259710 100644
--- a/client/pages/filespage/thing-existing.js
+++ b/client/pages/filespage/thing-existing.js
@@ -1,11 +1,14 @@
import React from 'react';
+import path from 'path';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { DragSource, DropTarget } from 'react-dnd';
import './thing.scss';
import { Card, NgIf, Icon, EventEmitter } from '../../components/';
-import { pathBuilder, prompt, leftPad } from '../../helpers/';
+import { pathBuilder, prompt, leftPad, getMimeType, debounce, memory } from '../../helpers/';
+import { Files } from '../../model/';
+import img_placeholder from '../../assets/img/placeholder.png';
const fileSource = {
beginDrag(props, monitor, component) {
@@ -80,22 +83,67 @@ export class ExistingThing extends React.Component {
hover: null,
message: null,
filename: props.file.name,
- is_renaming: false
+ is_renaming: false,
+ preview: null
};
}
+ shouldComponentUpdate(nextProps, nextState){
+ if(nextState.hover !== this.state.hover ||
+ nextState.message !== this.state.message ||
+ nextState.is_renaming !== this.state.is_renaming ||
+ nextProps.view !== this.props.view ||
+ this.state.preview !== nextState.preview ||
+ this.props.fileIsOver !== nextProps.fileIsOver ||
+ this.props.canDropFile !== nextProps.canDropFile ||
+ this.props.nativeFileIsOver !== nextProps.nativeFileIsOver ||
+ this.props.canDropNativeFile !== nextProps.canDropNativeFile
+ ){
+ return true;
+ }
+ return false;
+ }
+
+ componentDidMount(){
+ this.updateThumbnail(this.props);
+ }
+
+ componentWillReceiveProps(props){
+ if(props.view !== this.props.view){
+ this.updateThumbnail(props);
+ }
+ }
+
+ updateThumbnail(props){
+ if(props.view === "grid" && props.icon !== "loading"){
+ const _path = path.join(props.path, props.file.name);
+ const type = getMimeType(_path).split("/")[0];
+ if(type === "image"){
+ Files.url(_path).then((url) => {
+ this.setState({preview: url+"&size=250"});
+ });
+ }
+ }
+ }
+
onRename(newFilename){
- this.props.emit(
- 'file.rename',
- pathBuilder(this.props.path, this.props.file.name, this.props.file.type),
- pathBuilder(this.props.path, newFilename, this.props.file.type),
- this.props.file.type
- );
+ if(typeof newFilename === "string"){
+ this.props.emit(
+ 'file.rename',
+ pathBuilder(this.props.path, this.props.file.name, this.props.file.type),
+ pathBuilder(this.props.path, newFilename, this.props.file.type),
+ this.props.file.type
+ );
+ }
this.setState({is_renaming: false});
}
- onRenameRequest(){
- this.setState({is_renaming: !this.state.is_renaming});
+ onRenameRequest(force){
+ let new_state = !this.state.is_renaming;
+ if(typeof force === "boolean"){
+ new_state = force;
+ }
+ this.setState({is_renaming: new_state});
}
onDeleteRequest(filename){
@@ -150,18 +198,20 @@ export class ExistingThing extends React.Component {
if(this.props.file.icon === 'loading'){
className += "loading ";
}
-
+ if(this.state.preview){
+ className += "preview ";
+ }
className = className.trim();
return connectDragSource(connectDropNativeFile(connectDropFile(
-
+
@@ -185,12 +235,22 @@ class Filename extends React.Component {
};
}
+
+ moveCaretAtEnd(e){
+ const length = e.target.value.length;
+ e.target.setSelectionRange(length, length);
+ }
+
onRename(e){
e.preventDefault();
e.stopPropagation();
this.props.onRename(this.state.filename);
}
+ onCancel(){
+ this.props.onRenameCancel();
+ }
+
preventSelect(e){
e.preventDefault();
}
@@ -204,7 +264,7 @@ class Filename extends React.Component {
@@ -226,7 +286,7 @@ const ActionButton = (props) => {
return (
-
+
@@ -292,3 +352,73 @@ const Message = (props) => {
);
};
+
+
+class Image extends React.Component{
+ constructor(props){
+ super(props);
+ }
+
+ render(){
+ if(this.props.preview && this.props.view === "grid"){
+ return (
+
+
+
+
+ );
+ }
+
+ const ext = path.extname(this.props.path).replace(/^\./, "");
+ return (
+
+
+
+ {ext}
+
+
+ );
+ }
+};
+
+
+class LazyLoadImage extends React.Component {
+ constructor(props){
+ super(props);
+ this.state = {
+ appear: false
+ };
+ this.$scroll = document.querySelector(props.scroller);
+ this.onScroll = debounce(this.onScroll.bind(this), 100);
+ }
+
+ componentDidMount(){
+ if(!this.$scroll){ throw("No scroll detected on LazyLoadImage"); }
+ this.$scroll.addEventListener("scroll", this.onScroll, {passive: true});
+ this.onScroll();
+ }
+ componentWillUnmount(){
+ this.$scroll.removeEventListener("scroll", this.onScroll);
+ }
+
+ onScroll(){
+ if(!this.refs.$el) return this.componentWillUnmount();
+ const dim_el = this.refs.$el.getBoundingClientRect();
+ if(dim_el.top + dim_el.height > 0 && dim_el.top < window.innerHeight){
+ this.componentWillUnmount();
+ memory.set(this.props.src, true);
+ this.setState({appear: true});
+ }
+ }
+
+ render(){
+ if(this.props.preview || memory.get(this.props.src) === null){
+ return (
+
+ );
+ }
+ return (
+
+ );
+ }
+}
diff --git a/client/pages/filespage/thing-new.js b/client/pages/filespage/thing-new.js
index 32518101..657f9886 100644
--- a/client/pages/filespage/thing-new.js
+++ b/client/pages/filespage/thing-new.js
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Card, NgIf, Icon, EventEmitter } from '../../components/';
+import { Card, NgIf, Icon, EventEmitter, Dropdown, DropdownButton, DropdownList, DropdownItem } from '../../components/';
import { pathBuilder } from '../../helpers/';
import "./thing.scss";
@@ -18,7 +18,7 @@ export class NewThing extends React.Component {
this._onEscapeKeyPress = (e) => {
if(e.keyCode === 27) this.onDelete();
- }
+ };
}
componentDidMount(){
@@ -48,8 +48,12 @@ export class NewThing extends React.Component {
}
}
- onSortUpdate(e){
- this.props.onSortUpdate(e.target.value);
+ onViewChange(e){
+ this.props.onViewUpdate();
+ }
+
+ onSortChange(e){
+ this.props.onSortUpdate(e);
}
render(){
@@ -58,11 +62,17 @@ export class NewThing extends React.Component {
New File
New Directory
-
+
+
+
+
+
+ Sort By Type
+ Sort By Date
+ Sort By Name
+
+
+
@@ -94,4 +104,4 @@ NewThing.PropTypes = {
onCreate: PropTypes.func.isRequired,
onSortUpdate: PropTypes.func.isRequired,
sort: PropTypes.string.isRequired
-}
+};
diff --git a/client/pages/filespage/thing.scss b/client/pages/filespage/thing.scss
index 210fdf13..22605028 100644
--- a/client/pages/filespage/thing.scss
+++ b/client/pages/filespage/thing.scss
@@ -5,17 +5,41 @@
height: 15px;
margin-top: 5px;
color: var(--light);
- margin: 0 0 10px 0;
+ margin: 5px 0 10px 0;
- > span{cursor: pointer; margin-right: 15px;}
- select{
+ > div, > span{cursor: pointer; margin-right: 15px;}
+ .view{
float: right;
- color: var(--light);
- background: none;
- border-radius: 3px;
- outline: none;
- border: 1px solid var(--light);
- font-size: 12px;
+ padding: 2px;
+ transition: 0.15s ease-out background;
+ margin-right: 0px;
+ margin-left: 0px;
+ .component_icon{
+ min-width: 20px;
+ height: 15px;
+ width: 15px;
+ }
+ &.list-grid, > .dropdown_button{
+ border-radius: 2px;
+ padding: 5px;
+ margin: 2px 0 2px 0;
+ &:hover, &:focus{
+ background: white;
+ border-color: var(--bg-color);
+ box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
+ }
+ }
+ &.active > .dropdown_button{
+ background: white;
+ border-color: var(--bg-color);
+ box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
+ }
+ .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; }
}
}
@@ -107,3 +131,137 @@
}
}
+
+
+/* GRID & LIST VIEW */
+.list > span{
+ display: flex;
+ flex-wrap: wrap;
+ > .component_thing{
+ width: 100%;
+ .box{margin: 0px 0 2px 0;}
+ }
+
+ // GRID VIEW
+ > .component_thing.view-grid{
+ width: 25%; .box{ height: 160px;}
+ @media (max-width: 800px){width: 33.33%; .box{height: 200px} }
+ @media (max-width: 670px){.box{height: 170px}}
+ @media (max-width: 550px){width: 50%; .box{height: 200px} }
+ @media (max-width: 480px){.box{height: 170px}}
+ @media (max-width: 400px){.box{height: 140px; .info_extension{font-size: 0.85em; padding: 3px 10px;}}}
+ @media (max-width: 340px){.box{height: 130px}}
+ text-align: center;
+ .box{
+ margin: 2px;
+ padding: 0;
+ position: relative;
+ > span > img{
+ padding: 0;
+ margin: 0;
+ display: block;
+ }
+ > span > img.component_icon{
+ padding: 30px;
+ box-sizing: border-box;
+ width: auto;
+ height: 100%;
+ margin: 0 auto;
+ z-index: 0;
+ }
+ > span > img.thumbnail{
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: 50% 50%;
+ background: var(--dark);
+ z-index: 0;
+ }
+ .info_extension{
+ position: absolute;
+ top: 50%;
+ text-align: right;
+ left: 0; right: 20%;
+ margin: 0 auto;
+ span{
+ background: var(--color);
+ color: var(--bg-color);
+ border-radius: 4px;
+ padding: 4px 12px;
+ }
+ }
+ .component_filename{
+ letter-spacing: -0.5px;
+ z-index: 2;
+ position: absolute;
+ bottom: 2px;
+ left: 2px;
+ right: 2px;
+ border-radius: 2px;
+ padding: 3px 0px;
+ .file-details{
+ width: calc(100% - 10px);
+ display: block;
+ > span{
+ width: 100%;
+ form input{
+ letter-spacing: -0.5px;
+ text-align: center;
+ width: 100%;
+ padding: 0;
+ }
+ }
+ }
+ }
+ .image_layer{
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 1;
+ background: rgba(0,0,0,0);
+ transition: 0.2s ease-out background;
+ }
+ &:hover{
+ .component_action{
+ transition-delay: 0.1s;
+ opacity: 1;
+ transform: translateX(0);
+ }
+ img.thumbnail{
+ transform: scale(0.6);
+ }
+ .image_layer{
+ background: rgba(242, 242, 242, 0.7);
+ }
+ }
+ &.preview{
+ &:hover{
+ .component_filename{
+ opacity: 1;
+ }
+ }
+ .component_filename{
+ transition: 0.2s ease-out opacity;
+ opacity: 0;
+ }
+ }
+ .component_filesize, .component_datetime{ display: none; }
+ .component_action{
+ opacity: 0;
+ transform: translateX(5px);
+ transition: 0.15s ease-out all;
+ z-index: 2;
+ display: block;
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ border-radius: 5px;
+ margin-right: 0px;
+ padding: 0px;
+ }
+ img.thumbnail{transition: 0.2s ease-out transform;}
+ }
+ }
+}
diff --git a/client/pages/viewerpage/imageviewer.js b/client/pages/viewerpage/imageviewer.js
index 1d33b819..34e3dd37 100644
--- a/client/pages/viewerpage/imageviewer.js
+++ b/client/pages/viewerpage/imageviewer.js
@@ -3,11 +3,16 @@ import React from 'react';
import { MenuBar } from './menubar';
export const ImageViewer = (props) => {
+ const image_url = (size) => {
+ return props.data+"&meta=true&size="+parseInt((window.innerWidth - 40)*size);
+ };
return (
-

+
);
diff --git a/config_server.js b/config_server.js
index 2b9ed052..ba1a699b 100644
--- a/config_server.js
+++ b/config_server.js
@@ -22,5 +22,12 @@ module.exports = {
clientID: process.env.DROPBOX_CLIENT_ID,
redirectURI: process.env.APPLICATION_URL+"/login"
},
+ transcoder: {
+ url: process.env.TRANSCODER_URL || null,
+ audio_bitrate: 128, // in kbps
+ audio_quality: 0,
+ video_codec: 'libx264',
+ video_bitrate: 1000 // in kbps
+ },
secret_key: process.env.SECRET_KEY || (Math.random()*Math.pow(10,16)).toString(32)
};
diff --git a/package.json b/package.json
index 2c1e0045..6574ce9e 100644
--- a/package.json
+++ b/package.json
@@ -81,6 +81,7 @@
"react-dnd-html5-backend-filedrop": "^1.0.0",
"react-dom": "^15.3.2",
"react-draggable": "^2.2.6",
+ "react-infinite-scroller": "^1.1.4",
"react-router": "^4.1.1",
"react-router-dom": "^4.1.1",
"requirejs": "^2.3.5",
diff --git a/server/ctrl/files.js b/server/ctrl/files.js
index bb4f38a0..ac94b881 100644
--- a/server/ctrl/files.js
+++ b/server/ctrl/files.js
@@ -3,7 +3,10 @@ var express = require('express'),
path = require('path'),
crypto = require('../utils/crypto'),
Files = require('../model/files'),
+ config = require('../../config_server'),
multiparty = require('multiparty'),
+ request = require('request'),
+ FormData = require('form-data'),
mime = require('../utils/mimetype.js');
app.use(function(req, res, next){
@@ -35,8 +38,8 @@ app.get('/cat', function(req, res){
res.clearCookie("download");
if(path){
Files.cat(path, req.cookies.auth, res)
- .then(function(stream){
- stream = stream.on('error', function (error) {
+ .then(function(_stream){
+ _stream = _stream.on('error', function (error) {
let status = 404;
if(error && typeof error.status === "number"){
status = error.status;
@@ -44,8 +47,33 @@ app.get('/cat', function(req, res){
res.status(status).send({status: status, message: "There's nothing here"});
if(typeof this.end === "function") this.end();
});
- res.set('Content-Type', mime.getMimeType(path));
- stream.pipe(res);
+ const mType = mime.getMimeType(path);
+ res.set('Content-Type', mType);
+
+ if((/^image\//.test(mType) || /^video\//.test(mType) ) && config.transcoder.url){
+ const form = new FormData();
+ form.append('video', _stream, {
+ filename: 'tmp',
+ contentType: mType,
+ });
+
+ let endpoint = config.transcoder.url;
+ if(req.query.size){
+ endpoint += "?size="+req.query.size;
+ }
+ const post_request = request({
+ method: "POST",
+ url: endpoint,
+ headers: form.getHeaders()
+ });
+ return form.pipe(post_request)
+ .on('error', (err) => {
+ console.log(err);
+ res.status(500).end();
+ })
+ .pipe(res);
+ }
+ return _stream.pipe(res);
})
.catch(function(err){ errorHandler(res, err, 'couldn\'t read the file'); });
}else{