feature (#79): media view, infinite scroll and persistent user settings

This commit is contained in:
Mickael KERJEAN
2018-05-29 22:43:19 +10:00
parent 8fae0a3ee6
commit aebca52060
25 changed files with 988 additions and 337 deletions
+3 -5
View File
@@ -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;
+52
View File
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
viewBox="0 0 512 512"
version="1.1"
id="svg4334"
sodipodi:docname="check.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata4340">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4338" />
<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="namedview4336"
showgrid="false"
inkscape:zoom="0.4609375"
inkscape:cx="-208.27119"
inkscape:cy="256"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg4334" />
<path
d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"
id="path4332"
style="fill:#909090;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+12 -4
View File
@@ -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:guide
position="-5.5075377,9.4874372"
orientation="1,0"
id="guide7161"
inkscape:locked="false" />
</sodipodi:namedview>
<path
inkscape:connector-curvature="0"
style="color:#000000;text-indent:0;text-transform:none;fill:#8c8c8c;fill-opacity:1;stroke-width:0.98481041"
@@ -60,8 +68,8 @@
sodipodi:nodetypes="ccccccccccccc" />
<path
style="display:inline;fill:#8c8c8c;fill-opacity:1;stroke:#9e7575;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 10.63457,1.0286114 c -0.0416,3.1491307 -0.01814,3.1380869 3.3121,3.0978656 z"
d="m 10.63457,1.0286114 c -0.0416,3.7119448 -0.476797,3.1977577 3.3121,3.0978656 z"
id="path5283"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csc" />
sodipodi:nodetypes="ccc" />
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

+55
View File
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
viewBox="0 0 512 512"
version="1.1"
id="svg875"
sodipodi:docname="grid.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata881">
<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="defs879" />
<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="namedview877"
showgrid="false"
inkscape:zoom="1.6035156"
inkscape:cx="184.79579"
inkscape:cy="276.62026"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg875" />
<path
d="m 141.333,76 v 128 c 0,13.255 -10.745,24 -24,24 H 24 C 10.745,228 0,217.255 0,204 V 76 C 0,62.745 10.745,52 24,52 h 93.333 c 13.255,0 24,10.745 24,24 z m 229.334,0 v 128 c 0,13.255 10.745,24 24,24 H 488 c 13.255,0 24,-10.745 24,-24 V 76 C 512,62.745 501.255,52 488,52 h -93.333 c -13.255,0 -24,10.745 -24,24 z m -44,128 V 76 c 0,-13.255 -10.745,-24 -24,-24 H 201.333 c -13.255,0 -24,10.745 -24,24 v 128 c 0,13.255 10.745,24 24,24 h 101.333 c 13.256,0 24.001,-10.745 24.001,-24 z M 0,316 v 120 c 0,13.255 10.745,24 24,24 h 93.333 c 13.255,0 24,-10.745 24,-24 V 316 c 0,-13.255 -10.745,-24 -24,-24 H 24 C 10.745,292 0,302.745 0,316 Z M 394.667,460 H 488 c 13.255,0 24,-10.745 24,-24 V 316 c 0,-13.255 -10.745,-24 -24,-24 h -93.333 c -13.255,0 -24,10.745 -24,24 v 120 c 0,13.255 10.745,24 24,24 z M 177.333,316 v 120 c 0,13.255 10.745,24 24,24 h 101.333 c 13.255,0 24,-10.745 24,-24 V 316 c 0,-13.255 -10.745,-24 -24,-24 H 201.333 c -13.255,0 -24,10.745 -24,24 z"
id="path873"
style="fill:#626469;fill-opacity:1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ssssssssssssssssssssssssssssssssssssssssssssssssssssss" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+55
View File
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
viewBox="0 0 512 512"
version="1.1"
id="svg875"
sodipodi:docname="list.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata881">
<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="defs879" />
<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="namedview877"
showgrid="false"
inkscape:zoom="1.6035156"
inkscape:cx="181.16443"
inkscape:cy="252.08427"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg875" />
<path
d="m 133.333,56 v 64 c 0,13.255 -10.745,24 -24,24 H 24 C 10.745,144 0,133.255 0,120 V 56 C 0,42.745 10.745,32 24,32 h 85.333 c 13.255,0 24,10.745 24,24 z m 379.334,232 v -64 c 0,-13.255 -10.745,-24 -24,-24 H 213.333 c -13.255,0 -24,10.745 -24,24 v 64 c 0,13.255 10.745,24 24,24 h 275.333 c 13.256,0 24.001,-10.745 24.001,-24 z m 0,-168 V 56 c 0,-13.255 -10.745,-24 -24,-24 H 213.333 c -13.255,0 -24,10.745 -24,24 v 64 c 0,13.255 10.745,24 24,24 h 275.333 c 13.256,0 24.001,-10.745 24.001,-24 z M 109.333,200 H 24 C 10.745,200 0,210.745 0,224 v 64 c 0,13.255 10.745,24 24,24 h 85.333 c 13.255,0 24,-10.745 24,-24 v -64 c 0,-13.255 -10.745,-24 -24,-24 z M 0,392 v 64 c 0,13.255 10.745,24 24,24 h 85.333 c 13.255,0 24,-10.745 24,-24 v -64 c 0,-13.255 -10.745,-24 -24,-24 H 24 C 10.745,368 0,378.745 0,392 Z m 189.333,0 v 64 c 0,13.255 10.745,24 24,24 h 275.333 c 13.255,0 24,-10.745 24,-24 v -64 c 0,-13.255 -10.745,-24 -24,-24 H 213.333 c -13.255,0 -24,10.745 -24,24 z"
id="path873"
style="fill:#626469;fill-opacity:1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ssssssssssssssssssssssssssssssssssssssssssssssssssssss" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

+52
View File
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
viewBox="0 0 320 512"
version="1.1"
id="svg3752"
sodipodi:docname="sort.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata3758">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3756" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="940"
inkscape:window-height="1027"
id="namedview3754"
showgrid="false"
inkscape:zoom="0.4609375"
inkscape:cx="-43.932203"
inkscape:cy="256"
inkscape:window-x="964"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg3752" />
<path
d="M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41zm255-105L177 64c-9.4-9.4-24.6-9.4-33.9 0L24 183c-15.1 15.1-4.4 41 17 41h238c21.4 0 32.1-25.9 17-41z"
id="path3750"
style="fill:#626469;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+1 -1
View File
@@ -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);
+4 -2
View File
@@ -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 (
<div className={"component_dropdown"+(this.state.button ? " active" : "")}>
<div className={className}>
{ button }
{ dropdown }
</div>
+13
View File
@@ -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');
}
+2
View File
@@ -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';
+3
View File
@@ -9,6 +9,9 @@ function Memory(){
set: function(key, value){
if(!data[key]) data[key] = {};
data[key] = value;
},
all: function(){
return data;
}
}
}
+5 -5
View File
@@ -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",
+16
View File
@@ -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);
}
+268
View File
@@ -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<processes.length; i++){
if(processes[i].parent === null || PRIOR_STATUS[processes[i].parent] === true){
current_process = processes[i];
processes.splice(i, 1);
break;
}
}
if(current_process){
return current_process.fn(id)
.then(() => {
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);
}
})
);
}
};
+52 -214
View File
@@ -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<processes.length; i++){
if(processes[i].parent === null || PRIOR_STATUS[processes[i].parent] === true){
current_process = processes[i];
processes.splice(i, 1);
break;
}
}
if(current_process){
return current_process.fn(id)
.then(() => {
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 {
<BreadCrumb className="breadcrumb" path={this.state.path} />
<div className="page_container">
<div className="scroll-y">
<NgIf className="container" cond={this.state.loading === false && this.state.error === null}>
<NgIf cond={this.state.path === '/'}>
<FrequentlyAccess files={this.state.frequents}/>
<InfiniteScroll pageStart={0} hasMore={this.state.files.length > 70} initialLoad={false} useWindow={false} loadMore={this.loadMore.bind(this)} threshold={40} loader={<div className="infinite_scroll_loading" key={-1}><Loader/></div>}>
<NgIf className="container" cond={this.state.loading === false && this.state.error === null}>
<NgIf cond={this.state.path === '/'}>
<FrequentlyAccess files={this.state.frequents}/>
</NgIf>
<FileSystem path={this.state.path} sort={this.state.sort} view={this.state.view} onSort={this.onSort.bind(this)} onView={this.onView.bind(this)} files={this.state.files.slice(0, this.state.page_number * 24)} />
<Uploader path={this.state.path} />
</NgIf>
<FileSystem path={this.state.path} files={this.state.files} />
<Uploader path={this.state.path} />
</NgIf>
</InfiniteScroll>
<NgIf cond={this.state.loading && this.state.error === null}>
<Loader/>
</NgIf>
+10
View File
@@ -15,6 +15,16 @@
display: flex;
overflow: hidden;
}
.infinite_scroll_loading{
text-align: center;
.component_loader{
margin-top: -50px;
svg{
height: 40px;
}
}
}
}
.scroll-y{
+5 -65
View File
@@ -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(
<div className="component_filesystem">
<Container>
<NewThing path={this.props.path} sort={this.state.sort} onSortUpdate={(value) => {this.setState({sort: value})}} accessRight={this.state.access_right}></NewThing>
<NewThing path={this.props.path} sort={this.props.sort} view={this.props.view} onViewUpdate={(value) => this.props.onView(value)} onSortUpdate={(value) => {this.props.onSort(value);}} accessRight={this.state.access_right}></NewThing>
<NgIf cond={this.props.fileIsOver}>
<FileZone path={this.props.path} />
</NgIf>
<NgIf className="list" cond={this.props.files.length > 0}>
<ReactCSSTransitionGroup transitionName="filelist-item" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={200}>
{
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 <ExistingThing key={file.name+(file.icon || '')} file={file} path={this.props.path} />
return ( <ExistingThing view={this.props.view} key={file.name+(file.icon || '')} file={file} path={this.props.path} /> );
}
})
}
+147 -17
View File
@@ -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(
<div className="component_thing">
<div className={"component_thing view-"+this.props.view}>
<Link to={this.props.file.link || "#"}>
<Card className={this.state.hover} className={className}>
<Icon name={this.props.file.icon || this.props.file.type} />
<Filename filename={this.props.file.name} filesize={this.props.file.size} filetype={this.props.file.type} onRename={this.onRename.bind(this)} is_renaming={this.state.is_renaming} />
<Image preview={this.state.preview} icon={this.props.file.icon || this.props.file.type} view={this.props.view} path={path.join(this.props.path, this.props.file.name)} />
<Filename filename={this.props.file.name} filesize={this.props.file.size} filetype={this.props.file.type} onRename={this.onRename.bind(this)} is_renaming={this.state.is_renaming} onRenameCancel={this.onRenameRequest.bind(this, false)}/>
<Message message={this.state.message} />
<DateTime show={this.state.icon !== 'loading'} timestamp={this.props.file.time} />
<ActionButton onClickRename={this.onRenameRequest.bind(this)} onClickDelete={this.onDeleteRequest.bind(this)} can_move={this.props.file.can_move !== false} can_delete={this.props.file.can_delete !== false} />
<ActionButton onClickRename={this.onRenameRequest.bind(this)} onClickDelete={this.onDeleteRequest.bind(this)} is_renaming={this.state.is_renaming} can_move={this.props.file.can_move !== false} can_delete={this.props.file.can_delete !== false} />
</Card>
</Link>
</div>
@@ -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 {
</NgIf>
<NgIf cond={this.props.is_renaming === true} type='inline'>
<form onClick={this.preventSelect} onSubmit={this.onRename.bind(this)}>
<input value={this.state.filename} onChange={(e) => this.setState({filename: e.target.value})} autoFocus />
<input value={this.state.filename} onChange={(e) => this.setState({filename: e.target.value})} onBlur={this.onCancel.bind(this)} onFocus={this.moveCaretAtEnd.bind(this)} autoFocus />
</form>
</NgIf>
</span>
@@ -226,7 +286,7 @@ const ActionButton = (props) => {
return (
<div className="component_action">
<NgIf cond={props.can_move === true} type="inline">
<NgIf cond={props.can_move === true && props.is_renaming === false} type="inline">
<Icon name="edit" onClick={onRename} className="component_updater--icon" />
</NgIf>
<NgIf cond={props.can_delete === true} type="inline">
@@ -292,3 +352,73 @@ const Message = (props) => {
</NgIf>
);
};
class Image extends React.Component{
constructor(props){
super(props);
}
render(){
if(this.props.preview && this.props.view === "grid"){
return (
<span>
<div className="image_layer"></div>
<LazyLoadImage scroller=".scroll-y" className="thumbnail" src={this.props.preview} />
</span>
);
}
const ext = path.extname(this.props.path).replace(/^\./, "");
return (
<span>
<Icon name={this.props.icon} />
<NgIf cond={!!ext && this.props.view === "grid" && this.props.icon === "file"} className="info_extension">
<span>{ext}</span>
</NgIf>
</span>
);
}
};
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 (
<img ref="$el" className={this.props.className} src={img_placeholder} />
);
}
return (
<img className={this.props.className} src={this.props.src} />
);
}
}
+20 -10
View File
@@ -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 {
<div className="menubar no-select">
<NgIf cond={this.props.accessRight.can_create_file === true} onClick={this.onNew.bind(this, 'file')} type="inline">New File</NgIf>
<NgIf cond={this.props.accessRight.can_create_directory === true} onClick={this.onNew.bind(this, 'directory')} type="inline">New Directory</NgIf>
<select value={this.props.sort} onChange={this.onSortUpdate.bind(this)}>
<option value="type">Sort By Type</option>
<option value="date">Sort By Date</option>
<option value="name">Sort By Name</option>
</select>
<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">
@@ -94,4 +104,4 @@ NewThing.PropTypes = {
onCreate: PropTypes.func.isRequired,
onSortUpdate: PropTypes.func.isRequired,
sort: PropTypes.string.isRequired
}
};
+167 -9
View File
@@ -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;}
}
}
}
+6 -1
View File
@@ -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 (
<div style={{height: '100%'}}>
<MenuBar title={props.filename} download={props.data} />
<div style={{textAlign: 'center', background: '#525659', height: 'calc(100% - 34px)', overflow: 'hidden', padding: '20px', boxSizing: 'border-box'}}>
<img src={props.data} style={{maxHeight: '100%', maxWidth: '100%', minHeight: '100px', background: '#f1f1f1', boxShadow: 'rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, rgba(0, 0, 0, 0.2) 0px 2px 4px -1px'}} />
<img src={image_url(1)}
srcSet={image_url(1)+", "+image_url(3/2)+" 1.5x, "+image_url(2)+" 2x"}
style={{maxHeight: '100%', maxWidth: '100%', minHeight: '100px', background: '#f1f1f1', boxShadow: 'rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, rgba(0, 0, 0, 0.2) 0px 2px 4px -1px'}} />
</div>
</div>
);
+7
View File
@@ -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)
};
+1
View File
@@ -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",
+32 -4
View File
@@ -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{