maintain (eslint): linting on frontend

This commit is contained in:
Mickael Kerjean 2021-12-21 01:32:37 +11:00
parent 48a6763380
commit 5156432b52
62 changed files with 1920 additions and 1655 deletions

View file

@ -53,6 +53,12 @@ steps:
- git clone https://mickael-kerjean:$PASSWORD@github.com/mickael-kerjean/filestash-test test
- chmod -R 777 ./test/
- name: lint_frontend
image: node:8-alpine
depends_on: [ test_prepare, build_frontend ]
commands:
- npx eslint ./client/
- name: test_frontend
image: node:8-alpine
depends_on: [ test_prepare, build_frontend ]

View file

@ -4,21 +4,38 @@
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
"plugin:react/recommended",
"google"
],
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
}
},
"plugins": [
"react"
],
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "double"]
"indent": [2, 4],
"quotes": [2, "double"],
"object-curly-spacing": [2, "always"],
"require-jsdoc": [0],
"camelcase": [0],
"max-len": ["error", 100],
"prefer-promise-reject-errors": [0],
"guard-for-in": [0],
"react/prop-types": [0],
"new-cap": [0],
"prefer-spread": [0],
"no-throw-literal": [1],
"react/no-find-dom-node": [1],
"no-invalid-this": [1],
"react/no-string-refs": [1],
"react/display-name": [1],
"prefer-rest-params": [1],
"react/no-deprecated": [1]
}
}

View file

@ -8,37 +8,40 @@ import { t } from "../locales/";
import "./alert.scss";
export class ModalAlert extends Popup {
constructor(props){
constructor(props) {
super(props);
}
componentDidMount(){
componentDidMount() {
alert.subscribe((Component, okCallback) => {
this.setState({
appear: true,
value: Component,
fn: okCallback
fn: okCallback,
});
});
}
onSubmit(){
this.setState({appear: false}, () => {
onSubmit() {
this.setState({ appear: false }, () => {
requestAnimationFrame(() => this.state.fn && this.state.fn());
});
}
modalContentBody(){
modalContentBody() {
return (
<div className="modal-message">
{this.state.value}
{this.state.value}
</div>
);
}
modalContentFooter(){
modalContentFooter() {
return (
<Button type="submit" theme="emphasis" onClick={this.onSubmit.bind(this)}>{ t("OK") }</Button>
<Button type="submit" theme="emphasis"
onClick={this.onSubmit.bind(this)}>
{ t("OK") }
</Button>
);
}
}
@ -46,7 +49,7 @@ export class ModalAlert extends Popup {
export function Alert({ children = null, className = null }) {
return (
<div className={"alert" + (className ? ` ${className}`: "")}>
{ children }
{ children }
</div>
);
}

View file

@ -1,19 +1,21 @@
import React, { useState, useEffect } from "react";
/**
/*
* Back when I started this project, there used to be a library for animation made by facebook
* named: react-addons-css-transition-group
* Facebook got away from it and things haven't stayed backward compatible at the point where I got
* to realise it would be easier to write this simpler wrapper than migrate things over
*/
export function CSSTransition({ transitionName = "animate", children = null, transitionAppearTimeout = 300 }) {
export function CSSTransition({
transitionName = "animate", children = null, transitionAppearTimeout = 300,
}) {
const [className, setClassName] = useState(`${transitionName} ${transitionName}-appear`);
useEffect(() => {
setClassName(`${transitionName} ${transitionName}-appear ${transitionName}-appear-active`)
setClassName(`${transitionName} ${transitionName}-appear ${transitionName}-appear-active`);
const timeout = setTimeout(() => {
setClassName(`${transitionName}`)
setClassName(`${transitionName}`);
}, transitionAppearTimeout);
return () => clearTimeout(timeout);
}, []);
@ -22,5 +24,5 @@ export function CSSTransition({ transitionName = "animate", children = null, tra
<div className={className}>
{ children }
</div>
)
);
}

View file

@ -1,43 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { NgIf, Icon, EventEmitter, EventReceiver } from './';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
// eslint-disable
import React from "react";
import PropTypes from "prop-types";
import { Link } from "react-router-dom";
import { NgIf, Icon, EventEmitter } from "./";
import ReactCSSTransitionGroup from "react-addons-css-transition-group";
import './breadcrumb.scss';
import "./breadcrumb.scss";
export class BreadCrumb extends React.Component {
constructor(props){
constructor(props) {
super(props);
this.state = {
path: this._formatPath(props.path)
path: this._formatPath(props.path),
};
}
componentWillReceiveProps(props){
this.setState({path: this._formatPath(props.path)});
UNSAFE_componentWillReceiveProps(props) {
this.setState({ path: this._formatPath(props.path) });
}
_formatPath(full_path){
_formatPath(full_path) {
let paths = full_path.split("/");
if(paths.slice(-1)[0] === ''){
if (paths.slice(-1)[0] === "") {
paths.pop();
}
paths = paths.map((path, index) => {
let sub_path = paths.slice(0, index+1).join('/'),
label = path === '' ? (CONFIG.name || 'Filestash') : path;
if(index === paths.length - 1){
return {full: null, label: label};
}else{
const sub_path = paths.slice(0, index+1).join("/");
const label = path === "" ? (CONFIG.name || "Filestash") : path;
if (index === paths.length - 1) {
return { full: null, label: label };
} else {
return {
full: sub_path+'/',
full: sub_path + "/",
label: label,
minify: function(){
if(index === 0){ return false; }
if(paths.length <= (document.body.clientWidth > 800 ? 5 : 4)){ return false; }
if(index > paths.length - (document.body.clientWidth > 1000? 4 : 3)) return false;
minify: function() {
if (index === 0) {
return false;
}
if (paths.length <= (document.body.clientWidth > 800 ? 5 : 4)) {
return false;
}
if (index > paths.length - (document.body.clientWidth > 1000? 4 : 3)) {
return false;
}
return true;
}()
}(),
};
}
});
@ -45,43 +52,48 @@ export class BreadCrumb extends React.Component {
}
render(Element) {
if(new window.URL(location.href).searchParams.get("nav") === "false") return null;
if (new window.URL(location.href).searchParams.get("nav") === "false") return null;
const Path = Element? Element : PathElement;
return (
<div className="component_breadcrumb" role="navigation">
<BreadCrumbContainer className={this.props.className+' no-select'}>
<Logout />
<ReactCSSTransitionGroup transitionName="breadcrumb" transitionLeave={true} transitionEnter={true} transitionLeaveTimeout={150} transitionEnterTimeout={200} transitionAppear={false}>
{
this.state.path.map((path, index) => {
return (
<Path key={"breadcrumb_"+index} currentSelection={this.props.currentSelection} path={path} isLast={this.state.path.length === index + 1} needSaving={this.props.needSaving} />
);
})
}
</ReactCSSTransitionGroup>
</BreadCrumbContainer>
</div>
<BreadCrumbContainer className={this.props.className + " no-select"}>
<Logout />
<ReactCSSTransitionGroup transitionName="breadcrumb" transitionLeave={true}
transitionEnter={true} transitionLeaveTimeout={150}
transitionEnterTimeout={200} transitionAppear={false}>
{
this.state.path.map((path, index) => {
return (
<Path key={"breadcrumb_"+index}
currentSelection={this.props.currentSelection} path={path}
isLast={this.state.path.length === index + 1}
needSaving={this.props.needSaving} />
);
})
}
</ReactCSSTransitionGroup>
</BreadCrumbContainer>
</div>
);
}
}
};
BreadCrumb.propTypes = {
path: PropTypes.string.isRequired,
needSaving: PropTypes.bool
}
needSaving: PropTypes.bool,
};
const BreadCrumbContainer = (props) => {
return (
<div className={props.className}>
<div className="ul">
{props.children}
</div>
<div className="ul">
{ props.children }
</div>
</div>
);
}
};
const Logout = (props) => {
const isRunningFromAnIframe = window.self !== window.top;
return (
@ -95,99 +107,108 @@ const Logout = (props) => {
}
</div>
);
}
};
const Saving = (props) => {
return (
<ReactCSSTransitionGroup transitionName="saving_indicator" transitionLeave={true} transitionEnter={true} transitionAppear={true} transitionLeaveTimeout={200} transitionEnterTimeout={500} transitionAppearTimeout={500}>
<NgIf key={props.needSaving} className="component_saving" cond={props.needSaving === true}>
*
</NgIf>
<ReactCSSTransitionGroup transitionName="saving_indicator"
transitionLeave={true} transitionEnter={true} transitionAppear={true}
transitionLeaveTimeout={200} transitionEnterTimeout={500}
transitionAppearTimeout={500}>
<NgIf key={props.needSaving} className="component_saving"
cond={props.needSaving === true}>
*
</NgIf>
</ReactCSSTransitionGroup>
);
}
};
const Separator = (props) => {
/* eslint-disable-next-line max-len */
const src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAA30lEQVQ4T63T7Q2CMBAG4OuVPdQNcAPdBCYwDdclCAQ3ACfRDXQDZQMHgNRcAoYApfWjv0jIPX3b3gn4wxJjI03TUAhRBkGwV0o9ffaYIEVRrJumuQHA3ReaILxzl+bCkNZ660ozi/QQIl4BoCKieAmyIlyU53lkjCld0CIyhIwxSmt9nEvkRLgoyzIuPggh4iRJqjHkhXTQAwBWUsqNUoq/38sL+TlJf7lf38ngdU5EFNme2adPFgGGrR2LiGcAqIko/LhjeXbatuVOraWUO58hnJ1iRKx8AetxXPHH/1+y62USursaSgAAAABJRU5ErkJggg==";
return (
<div className="component_separator">
<img alt="path_separator" width="16" height="16" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAA30lEQVQ4T63T7Q2CMBAG4OuVPdQNcAPdBCYwDdclCAQ3ACfRDXQDZQMHgNRcAoYApfWjv0jIPX3b3gn4wxJjI03TUAhRBkGwV0o9ffaYIEVRrJumuQHA3ReaILxzl+bCkNZ660ozi/QQIl4BoCKieAmyIlyU53lkjCld0CIyhIwxSmt9nEvkRLgoyzIuPggh4iRJqjHkhXTQAwBWUsqNUoq/38sL+TlJf7lf38ngdU5EFNme2adPFgGGrR2LiGcAqIko/LhjeXbatuVOraWUO58hnJ1iRKx8AetxXPHH/1+y62USursaSgAAAABJRU5ErkJggg=="/>
<img alt="path_separator" width="16" height="16" src={src} />
</div>
);
}
};
@EventEmitter
export class PathElementWrapper extends React.Component {
constructor(props){
constructor(props) {
super(props);
}
limitSize(str, is_highlight = false){
if(is_highlight === true){
if(str.length > 30){
return str.substring(0,12).trim()+'...'+str.substring(str.length - 10, str.length).trim();
limitSize(str, is_highlight = false) {
if (is_highlight === true) {
if (str.length > 30) {
return str.substring(0, 12).trim() + "..." +
str.substring(str.length - 10, str.length).trim();
}
}else{
if(str.length > 27){
return str.substring(0,20).trim()+'...';
} else {
if (str.length > 27) {
return str.substring(0, 20).trim() + "...";
}
}
return str;
}
render(){
render() {
let className = "component_path-element-wrapper";
if(this.props.highlight) { className += " highlight"; }
if (this.props.highlight) {
className += " highlight";
}
let href = "/files" + (this.props.path.full || "")
let href = "/files" + (this.props.path.full || "");
href = href
.replace(/\%/g, "%2525") // Hack to get the Link Component to work
// See ExistingThing in 'thing-existing.js'
.replace(/#/g, "%23")
.replace(/\?/g, "%3F");
href = href || "/"
href = href || "/";
href += location.search;
return (
<div className={"li "+className}>
<NgIf cond={this.props.isLast === false}>
<Link to={href} className="label">
<NgIf cond={this.props.path.minify !== true}>
<NgIf cond={this.props.isLast === false}>
<Link to={href} className="label">
<NgIf cond={this.props.path.minify !== true}>
{ this.limitSize(this.props.path.label) }
</NgIf>
<NgIf cond={this.props.path.minify === true}>
...
<span className="title">
{ this.limitSize(this.props.path.label, true) }
</span>
</NgIf>
</Link>
<Separator/>
</NgIf>
<NgIf cond={this.props.isLast === true} className="label">
{this.limitSize(this.props.path.label)}
</NgIf>
<NgIf cond={this.props.path.minify === true}>
...
<span className="title">
{this.limitSize(this.props.path.label, true)}
</span>
</NgIf>
</Link>
<Separator/>
</NgIf>
<NgIf cond={this.props.isLast === true} className="label">
{this.limitSize(this.props.path.label)}
<Saving needSaving={this.props.needSaving} />
</NgIf>
<Saving needSaving={this.props.needSaving} />
</NgIf>
</div>
);
}
}
// just a hack to make it play nicely with react-dnd as it refuses to use our custom component if it's not wrap by something it knows ...
// just a hack to make it play nicely with react-dnd as it refuses to use our
// custom component if it's not wrap by something it knows ...
export class PathElement extends PathElementWrapper {
constructor(props){
constructor(props) {
super(props);
}
render(highlight = false){
render(highlight = false) {
let className = "component_path-element";
if(this.props.isLast){
if (this.props.isLast) {
className += " is-last";
}
return (
<div className={className}>
<PathElementWrapper highlight={highlight} {...this.props} />
<PathElementWrapper highlight={highlight} {...this.props} />
</div>
);
}

View file

@ -1,59 +1,63 @@
// taken from https://reacttraining.com/react-router/web/guides/code-splitting
import React from 'react';
import Path from 'path';
import React from "react";
import load from "little-loader";
export class Bundle extends React.Component {
state = { mod: null };
constructor(props) {
super(props);
this.state = { mod: null };
}
componentDidMount() {
this.load(this.props)
this.load(this.props);
}
componentWillReceiveProps(nextProps) {
if(nextProps.load !== this.props.load){ this.load(nextProps) }
if (nextProps.load !== this.props.load) {
this.load(nextProps);
}
}
load(props) {
this.setState({
mod: null
mod: null,
});
Promise.all(
[props.loader].concat(
(props.overrides || []).map((src) => loadAsPromise(src))
)
(props.overrides || []).map((src) => loadAsPromise(src)),
),
).then((m) => {
const _mod = m[0];
this.setState({
mod: function(mod){
if(mod['default']){
mod: function(mod) {
if (mod["default"]) {
return mod.default;
}else if(mod['__esModule'] === true){
} else if (mod["__esModule"] === true) {
return mod[props.symbol] ? mod[props.symbol] : null;
}else{
} else {
return mod;
}
}(_mod)
}(_mod),
});
}).catch((err) => {
console.warn(err)
console.warn(err);
});
}
render() {
return this.state.mod ? this.props.children(this.state.mod) : null
return this.state.mod ? this.props.children(this.state.mod) : null;
}
}
function loadAsPromise(src){
function loadAsPromise(src) {
return new Promise((done, error) => {
load(src, function(err){
if(err){
load(src, function(err) {
if (err) {
error(err);
return;
}
done();
})
});
});
}

View file

@ -2,27 +2,27 @@ import React from "react";
import "./card.scss";
export class Card extends React.Component {
constructor(props){
constructor(props) {
super(props);
this.state = {
dragging: false
dragging: false,
};
}
onClick(){
if(this.props.onClick){
onClick() {
if (this.props.onClick) {
this.props.onClick();
}
}
onMouseEnter(){
if(this.props.onMouseEnter){
onMouseEnter() {
if (this.props.onMouseEnter) {
this.props.onMouseEnter();
}
}
onMouseLeave(){
if(this.props.onMouseLeave){
onMouseLeave() {
if (this.props.onMouseLeave) {
this.props.onMouseLeave();
}
}
@ -31,7 +31,7 @@ export class Card extends React.Component {
const _className = this.props.className ? this.props.className+" box" : "box";
return (
<div {...this.props} className={_className}>
{this.props.children}
{ this.props.children }
</div>
);
}

View file

@ -5,47 +5,49 @@ import { confirm } from "../helpers/";
import { Popup } from "./popup";
import { t } from "../locales/";
export class ModalConfirm extends Popup{
constructor(props){
export class ModalConfirm extends Popup {
constructor(props) {
super(props);
}
componentDidMount(){
componentDidMount() {
confirm.subscribe((Component, yesCallback, noCallback) => {
this.setState({
appear: true,
value: Component,
fns: {yes: yesCallback, no: noCallback}
fns: { yes: yesCallback, no: noCallback },
});
});
}
modalContentBody(){
modalContentBody() {
return (
<div className="modal-message">
{this.state.value}
{this.state.value}
</div>
);
}
yes(){
if(this.state.fns && typeof this.state.fns.yes === "function"){
yes() {
if (this.state.fns && typeof this.state.fns.yes === "function") {
this.state.fns.yes();
}
this.setState({appear: false});
this.setState({ appear: false });
}
no(){
if(this.state.fns && typeof this.state.fns.no === "function"){
no() {
if (this.state.fns && typeof this.state.fns.no === "function") {
this.state.fns.no();
}
this.setState({appear: false});
this.setState({ appear: false });
}
modalContentFooter(){
modalContentFooter() {
return (
<div>
<Button type="button" onClick={this.no.bind(this)}>{ t("NO") } </Button>
<Button type="submit" theme="emphasis" onClick={this.yes.bind(this)}>{ t("YES") }</Button>
<Button type="button" onClick={this.no.bind(this)}>{ t("NO") } </Button>
<Button type="submit" theme="emphasis" onClick={this.yes.bind(this)}>
{ t("YES") }
</Button>
</div>
);
}

View file

@ -2,16 +2,16 @@ import React from "react";
import "./container.scss";
export class Container extends React.Component {
constructor(props){
constructor(props) {
super(props);
}
render() {
const style = this.props.maxWidth ? {maxWidth: this.props.maxWidth} : {};
const style = this.props.maxWidth ? { maxWidth: this.props.maxWidth } : {};
let className = "component_container";
if(this.props.className) className += " "+this.props.className;
if (this.props.className) className += " " + this.props.className;
return (
<div className={className} style={style}>
{this.props.children}
{this.props.children}
</div>
);
}

View file

@ -8,29 +8,29 @@ import { t } from "../locales/";
import "../pages/error.scss";
export function LoggedInOnly(WrappedComponent){
export function LoggedInOnly(WrappedComponent) {
memory.set("user::authenticated", false);
return class extends React.Component {
constructor(props){
constructor(props) {
super(props);
this.state = {
is_logged_in: memory.get("user::authenticated")
is_logged_in: memory.get("user::authenticated"),
};
}
componentDidMount(){
if(this.state.is_logged_in === false && currentShare() === null){
componentDidMount() {
if (this.state.is_logged_in === false && currentShare() === null) {
Session.currentUser().then((res) => {
if(res.is_authenticated === false){
this.props.error({message: "Authentication Required"});
if (res.is_authenticated === false) {
this.props.error({ message: "Authentication Required" });
return;
}
memory.set("user::authenticated", true);
this.setState({is_logged_in: true});
this.setState({ is_logged_in: true });
}).catch((err) => {
if(err.code === "NO_INTERNET"){
this.setState({is_logged_in: true});
if (err.code === "NO_INTERNET") {
this.setState({ is_logged_in: true });
return;
}
this.props.error(err);
@ -38,8 +38,8 @@ export function LoggedInOnly(WrappedComponent){
}
}
render(){
if(this.state.is_logged_in === true || currentShare() !== null){
render() {
if (this.state.is_logged_in === true || currentShare() !== null) {
return <WrappedComponent {...this.props} />;
}
return null;
@ -47,49 +47,53 @@ export function LoggedInOnly(WrappedComponent){
};
}
export function ErrorPage(WrappedComponent){
export function ErrorPage(WrappedComponent) {
return class extends React.Component {
constructor(props){
constructor(props) {
super(props);
this.state = {
error: null,
has_back_button: false
has_back_button: false,
};
this.unlisten = this.props.history.listen(() => {
this.setState({has_back_button: false});
this.setState({ has_back_button: false });
this.unlisten();
});
}
componentWillUnmount(){
if(this.unlisten) this.unlisten();
componentWillUnmount() {
if (this.unlisten) this.unlisten();
}
update(obj){
this.setState({error: obj});
update(obj) {
this.setState({ error: obj });
}
navigate(e) {
if(this.state.has_back_button){
if (this.state.has_back_button) {
e.preventDefault();
this.props.history.goBack();
}
}
render(){
if(this.state.error !== null){
render() {
if (this.state.error !== null) {
const message = this.state.error.message || t("There is nothing in here");
return (
<div>
<Link onClick={this.navigate.bind(this)} to={`/${window.location.search}`} className="backnav">
<Icon name="arrow_left" />{ this.state.has_back_button ? "back" : "home" }
</Link>
<Container>
<div className="error-page">
<h1>{ t("Oops!") }</h1>
<h2>{ message }</h2>
</div>
</Container>
<Link to={`/${window.location.search}`}
className="backnav" onClick={this.navigate.bind(this)}
>
<Icon name="arrow_left" />{
this.state.has_back_button ? "back" : "home"
}
</Link>
<Container>
<div className="error-page">
<h1>{ t("Oops!") }</h1>
<h2>{ message }</h2>
</div>
</Container>
</div>
);
}
@ -102,8 +106,8 @@ export function ErrorPage(WrappedComponent){
export const LoadingPage = () => {
return (
<div style={{marginTop: parseInt(window.innerHeight / 3)+"px"}}>
<Loader />
<div style={{ marginTop: parseInt(window.innerHeight / 3) + "px" }}>
<Loader />
</div>
);
};

View file

@ -10,17 +10,17 @@ import { Icon, NgIf } from "./";
import "./dropdown.scss";
export class Dropdown extends React.Component {
constructor(props){
constructor(props) {
super(props);
this.state = {
button: false
button: false,
};
this.$dropdown = null;
this.closeDropdown = this.closeDropdown.bind(this);
this.toggleDropdown = this.toggleDropdown.bind(this);
}
componentDidMount(){
componentDidMount() {
this.$dropdown = ReactDOM.findDOMNode(this).querySelector(".dropdown_button");
// This is not really the "react" way of doing things but we needed to use both a
// click on the button and on the body (to exit the dropdown). we had issues
@ -29,27 +29,27 @@ export class Dropdown extends React.Component {
this.$dropdown.addEventListener("click", this.toggleDropdown);
}
componentWillUnmount(){
componentWillUnmount() {
this.$dropdown.removeEventListener("click", this.toggleDropdown);
document.body.removeEventListener("click", this.closeDropdown);
}
onSelect(name){
onSelect(name) {
this.props.onChange(name);
}
closeDropdown(){
closeDropdown() {
document.body.removeEventListener("click", this.closeDropdown);
this.setState({button: false});
this.setState({ button: false });
}
toggleDropdown(){
if(this.props.enable === false){
toggleDropdown() {
if (this.props.enable === false) {
return;
}
document.body.removeEventListener("click", this.closeDropdown);
this.setState({button: !this.state.button}, () => {
if(this.state.button === true){
this.setState({ button: !this.state.button }, () => {
if (this.state.button === true) {
requestAnimationFrame(() => {
document.body.addEventListener("click", this.closeDropdown);
});
@ -57,17 +57,19 @@ export class Dropdown extends React.Component {
});
}
render(){
render() {
const button = this.props.children[0];
const dropdown = React.cloneElement(this.props.children[1], {onSelect: this.onSelect.bind(this)});
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={className}>
{ button }
{ dropdown }
{ button }
{ dropdown }
</div>
);
}
@ -77,7 +79,7 @@ export class Dropdown extends React.Component {
export const DropdownButton = (props) => {
return (
<div className="dropdown_button">
{ props.children }
{ props.children }
</div>
);
};
@ -87,16 +89,16 @@ export const DropdownList = (props) => {
const childrens = Array.isArray(props.children) ? props.children : [props.children];
return (
<div className="dropdown_container">
<ul>
{
childrens.map((children, index) => {
const child = React.cloneElement(children, {onSelect: props.onSelect });
return (
<li key={index}>{child}</li>
);
})
}
</ul>
<ul>
{
childrens.map((children, index) => {
const child = React.cloneElement(children, { onSelect: props.onSelect });
return (
<li key={index}>{child}</li>
);
})
}
</ul>
</div>
);
};
@ -104,12 +106,12 @@ export const DropdownList = (props) => {
export const DropdownItem = (props) => {
return (
<div onClick={props.onSelect.bind(null, props.name)}>
{props.children}
<NgIf cond={!!props.icon} type="inline">
<span style={{float: "right"}}>
<Icon name={props.icon} />
</span>
</NgIf>
{props.children}
<NgIf cond={!!props.icon} type="inline">
<span style={{ float: "right" }}>
<Icon name={props.icon} />
</span>
</NgIf>
</div>
);
};

View file

@ -1,71 +1,74 @@
// cheap event system that handle subscription, unsubscriptions and event emitions
import React from "react";
let emitters = {};
const emitters = {};
function subscribe(key, event, fn){
if(emitters[event]){
function subscribe(key, event, fn) {
if (emitters[event]) {
emitters[event][key] = fn;
}else{
} else {
emitters[event] = {};
emitters[event][key] = fn;
}
}
function unsubscribe(key, event){
if(emitters[event]){
if(key){
function unsubscribe(key, event) {
if (emitters[event]) {
if (key) {
delete emitters[event][key];
}else{
} else {
delete emitters[event];
}
}
}
function emit(event, payload){
function emit(event, payload) {
// trigger events if needed
if(emitters[event]){
if (emitters[event]) {
return Promise.all(Object.keys(emitters[event]).map((key) => {
return emitters[event][key].apply(null, payload);
})).then((res) => {
return emitters[event] ?
Promise.resolve(res) :
Promise.reject({message: "do not exist", code: "CANCELLED"});
Promise.reject({ message: "do not exist", code: "CANCELLED" });
});
}else{
return Promise.reject({message: "oups, something went wrong", code: "NO_LISTENERS"});
} else {
return Promise.reject({ message: "oups, something went wrong", code: "NO_LISTENERS" });
}
}
export function EventReceiver(WrappedComponent){
let id = Math.random().toString();
export function EventReceiver(WrappedComponent) {
const id = Math.random().toString();
return class extends React.Component {
subscribe(event, callback){
subscribe(event, callback) {
subscribe(id, event, callback);
}
unsubscribe(event){
unsubscribe(event) {
unsubscribe(id, event);
}
render(){
return <WrappedComponent subscribe={this.subscribe} unsubscribe={this.unsubscribe} {...this.props} />;
render() {
return (
<WrappedComponent subscribe={this.subscribe} unsubscribe={this.unsubscribe}
{...this.props} />
);
}
};
}
export function EventEmitter(WrappedComponent) {
return class extends React.Component {
emit(){
emit() {
// reconstruct arguments
let args = Array.prototype.slice.call(arguments);
let event = args.shift();
let payload = args;
const args = Array.prototype.slice.call(arguments);
const event = args.shift();
const payload = args;
let res = emit(event, payload);
if(res.then){
const res = emit(event, payload);
if (res.then) {
return res;
}else{
} else {
return Promise.resolve(res);
}
}

View file

@ -1,11 +1,11 @@
import React from "react";
import "./fab.scss";
export function Fab(props){
export function Fab(props) {
return (
<div className="component_fab" onClick={props.onClick}>
<div className="content">
{props.children}
{props.children}
</div>
</div>
);

View file

@ -8,147 +8,161 @@ import { t } from "../locales/";
import "./formbuilder.scss";
export class FormBuilder extends React.Component {
constructor(props){
constructor(props) {
super(props);
}
section(struct, key, level = 0){
if(struct == null) struct = "";
const isALeaf = function(struct){
if("label" in struct && "type" in struct &&
"value" in struct && "default" in struct){
section(struct, key, level = 0) {
if (struct == null) struct = "";
const isALeaf = function(struct) {
if ("label" in struct && "type" in struct &&
"value" in struct && "default" in struct) {
return true;
}
return false;
};
if(Array.isArray(struct)) return null;
else if(isALeaf(struct) === false){
const [normal, advanced] = function(s){
let _normal = [];
let _advanced = [];
for (let key in s){
const tmp = {key: key, data: s[key]};
if (Array.isArray(struct)) return null;
else if (isALeaf(struct) === false) {
const [normal, advanced] = function(s) {
const _normal = [];
const _advanced = [];
for (const key in s) {
const tmp = { key: key, data: s[key] };
"id" in s[key] ? _advanced.push(tmp) : _normal.push(tmp);
}
return [_normal, _advanced];
}(struct);
if(level <= 1){
if (level <= 1) {
return (
<div className="formbuilder">
{
key ? <h2 className="no-select">{ format(key) }</h2> : ""
}
{
normal.map((s, index) => {
return (
<div key={s.key+"-"+index}>
{ this.section(s.data, s.key, level + 1) }
</div>
);
})
}
<div className="advanced_form">
{
advanced.map((s, index) => {
key ? <h2 className="no-select">{ format(key) }</h2> : ""
}
{
normal.map((s, index) => {
return (
<div key={s.key+"-"+index}>
{ this.section(s.data, s.key, level + 1) }
{ this.section(s.data, s.key, level + 1) }
</div>
);
})
}
</div>
<div className="advanced_form">
{
advanced.map((s, index) => {
return (
<div key={s.key+"-"+index}>
{ this.section(s.data, s.key, level + 1) }
</div>
);
})
}
</div>
</div>
);
}
return (
<div>
<fieldset>
<legend className="no-select">{ format(key) }</legend>
{
Object.keys(struct).map((key, index) => {
return (
<div key={key+"-"+index}>
{ this.section(struct[key], key, level + 1) }
</div>
);
})
}
</fieldset>
<fieldset>
<legend className="no-select">{ format(key) }</legend>
{
Object.keys(struct).map((key, index) => {
return (
<div key={key+"-"+index}>
{ this.section(struct[key], key, level + 1) }
</div>
);
})
}
</fieldset>
</div>
);
}
let id = {};
const id = {};
let target = [];
if(struct.id !== undefined){
if (struct.id !== undefined) {
id.id = this.props.idx === undefined ? struct.id : struct.id + "_" + this.props.idx;
}
if(struct.type === "enable"){
if (struct.type === "enable") {
target = struct.target.map((target) => {
return this.props.idx === undefined ? target : target + "_" + this.props.idx;
});
}
const onChange = function(e, fn){
const onChange = function(e, fn) {
struct.value = e;
if(typeof fn === "function"){
if (typeof fn === "function") {
fn(struct);
}
this.props.onChange.call(
this,
FormObjToJSON(this.props.form)
FormObjToJSON(this.props.form),
);
};
return ( <FormElement render={this.props.render} onChange={onChange.bind(this)} {...id} params={struct} target={target} name={ format(struct.label) } autoComplete={ this.props.autoComplete || "off" }/> );
return (
<FormElement render={this.props.render}
onChange={onChange.bind(this)} {...id}
params={struct} target={target} name={ format(struct.label) }
autoComplete={ this.props.autoComplete || "off" } />
);
}
render(){
render() {
return this.section(this.props.form || {});
}
}
const FormElement = (props) => {
const id = props.id !== undefined ? {id: props.id} : {};
let struct = props.params;
let autoCompleteProp = props.autoComplete || "off"
let $input = ( <Input onChange={(e) => props.onChange(e.target.value)} {...id} name={struct.label} type="text" defaultValue={struct.value} placeholder={ t(struct.placeholder) } /> );
switch(props.params["type"]){
const id = props.id !== undefined ? { id: props.id } : {};
const struct = props.params;
const autoCompleteProp = props.autoComplete || "off";
let $input = (
<Input onChange={(e) => props.onChange(e.target.value)} {...id} name={struct.label}
type="text" defaultValue={struct.value} placeholder={ t(struct.placeholder) } />
);
switch (props.params["type"]) {
case "text": {
const onTextChange = (value) => {
if(value === ""){
if (value === "") {
value = null;
}
props.onChange(value);
};
const list_id = struct.datalist ? "list_"+Math.random() : null;
$input = ( <Input list={list_id} onChange={(e) => onTextChange(e.target.value)} {...id} name={struct.label} type="text" value={struct.value || ""} placeholder={ t(struct.placeholder) } readOnly={struct.readonly} autoComplete={autoCompleteProp} autoCorrect="off" autoCapitalize="off" spellCheck="false" /> );
if(list_id != null){
const filtered = function(multi, datalist, currentValue){
if(multi !== true || currentValue == null) return datalist;
$input = (
<Input list={list_id} onChange={(e) => onTextChange(e.target.value)} {...id}
name={struct.label} type="text" value={struct.value || ""}
placeholder={ t(struct.placeholder) } readOnly={struct.readonly}
autoComplete={autoCompleteProp} autoCorrect="off" autoCapitalize="off"
spellCheck="false" />
);
if (list_id != null) {
const filtered = function(multi, datalist, currentValue) {
if (multi !== true || currentValue == null) return datalist;
return autocomplete(
currentValue
.split(",")
.map((t) => t.trim())
.filter((t) => t),
datalist
datalist,
);
};
$input = (
<span>
{ $input }
<datalist id={list_id}>
{
filtered(struct.multi, struct.datalist, struct.value).map((item,i) => {
return ( <option key={i} value={item} /> );
})
}
</datalist>
{ $input }
<datalist id={list_id}>
{
filtered(struct.multi, struct.datalist, struct.value).map((item, i) => {
return ( <option key={i} value={item} /> );
})
}
</datalist>
</span>
);
}
@ -159,37 +173,56 @@ const FormElement = (props) => {
value = value === "" ? null : parseInt(value);
props.onChange(value);
};
$input = ( <Input onChange={(e) => onNumberChange(e.target.value)} {...id} name={struct.label} type="number" value={struct.value === null ? "" : struct.value} placeholder={ t(struct.placeholder) } /> );
$input = (
<Input onChange={(e) => onNumberChange(e.target.value)} {...id} name={struct.label}
type="number" value={struct.value === null ? "" : struct.value}
placeholder={ t(struct.placeholder) } />
);
break;
}
case "password": {
const onPasswordChange = (value) => {
if(value === ""){
value = null;
}
props.onChange(value);
};
$input = ( <Input onChange={(e) => onPasswordChange(e.target.value)} {...id} name={struct.label} type="password" value={struct.value || ""} placeholder={ t(struct.placeholder) } autoComplete={autoCompleteProp} autoCorrect="off" autoCapitalize="off" spellCheck="false"/> );
break;
}
case "long_password": {
const onLongPasswordChange = (value) => {
if(value === ""){
if (value === "") {
value = null;
}
props.onChange(value);
};
$input = (
<Textarea {...id} disabledEnter={true} value={struct.value || ""} onChange={(e) => onLongPasswordChange(e.target.value)} type="text" rows="1" name={struct.label} placeholder={ t(struct.placeholder) } autoComplete={autoCompleteProp} autoCorrect="off" autoCapitalize="off" spellCheck="false" />
<Input onChange={(e) => onPasswordChange(e.target.value)} {...id} name={struct.label}
type="password" value={struct.value || ""} placeholder={ t(struct.placeholder) }
autoComplete={autoCompleteProp} autoCorrect="off" autoCapitalize="off"
spellCheck="false"/>
);
break;
}
case "long_password": {
const onLongPasswordChange = (value) => {
if (value === "") {
value = null;
}
props.onChange(value);
};
$input = (
<Textarea {...id} disabledEnter={true} value={struct.value || ""}
onChange={(e) => onLongPasswordChange(e.target.value)} type="text" rows="1"
name={struct.label} placeholder={ t(struct.placeholder) }
autoComplete={autoCompleteProp} autoCorrect="off" autoCapitalize="off"
spellCheck="false" />
);
break;
}
case "long_text":
$input = ( <Textarea {...id} disabledEnter={true} value={struct.value || ""} onChange={(e) => props.onChange(e.target.value)} type="text" rows="3" name={struct.label} placeholder={ t(struct.placeholder) } autoComplete={autoCompleteProp} autoCorrect="off" autoCapitalize="off" spellCheck="false" /> );
$input = (
<Textarea {...id} disabledEnter={true} value={struct.value || ""}
onChange={(e) => props.onChange(e.target.value)}
type="text" rows="3" name={struct.label} placeholder={ t(struct.placeholder) }
autoComplete={autoCompleteProp} autoCorrect="off" autoCapitalize="off"
spellCheck="false" />
);
break;
case "bcrypt": {
const onBcryptChange = (value) => {
if(value === ""){
if (value === "") {
return props.onChange(null);
}
return import(/* webpackChunkName: "bcrypt" */"../helpers/bcrypt")
@ -197,42 +230,67 @@ const FormElement = (props) => {
.then((bcrypt) => bcrypt.bcrypt_password(value))
.then((hash) => props.onChange(hash));
};
$input = ( <Input onChange={(e) => onBcryptChange(e.target.value)} {...id} name={struct.label} type="password" defaultValue={struct.value || ""} placeholder={ t(struct.placeholder) } /> );
$input = (
<Input onChange={(e) => onBcryptChange(e.target.value)} {...id} name={struct.label}
type="password" defaultValue={struct.value || ""}
placeholder={ t(struct.placeholder) } />
);
break;
}
case "hidden":
$input = ( <Input name={struct.label} type="hidden" defaultValue={struct.value} /> );
$input = (
<Input name={struct.label} type="hidden" defaultValue={struct.value} />
);
break;
case "boolean":
$input = ( <Input onChange={(e) => props.onChange(e.target.checked)} {...id} name={struct.label} type="checkbox" checked={struct.value === null ? !!struct.default : struct.value} /> );
$input = (
<Input onChange={(e) => props.onChange(e.target.checked)} {...id} name={struct.label}
type="checkbox" checked={struct.value === null ? !!struct.default : struct.value} />
);
break;
case "select":
$input = ( <Select onChange={(e) => props.onChange(e.target.value)} {...id} name={struct.label} choices={struct.options} value={struct.value === null ? struct.default : struct.value} placeholder={ t(struct.placeholder) } />);
$input = (
<Select onChange={(e) => props.onChange(e.target.value)} {...id} name={struct.label}
choices={struct.options} placeholder={ t(struct.placeholder) }
value={struct.value === null ? struct.default : struct.value} />
);
break;
case "enable":
$input = ( <Enabler onChange={(e) => props.onChange(e.target.checked)} {...id} name={struct.label} target={props.target} defaultValue={struct.value === null ? struct.default : struct.value} /> );
$input = (
<Enabler onChange={(e) => props.onChange(e.target.checked)} {...id} name={struct.label}
target={props.target}
defaultValue={struct.value === null ? struct.default : struct.value} />
);
break;
case "date":
$input = ( <Input onChange={(e) => props.onChange(e.target.value)} {...id} name={struct.label} type="date" defaultValue={struct.value || ""} placeholder={ t(struct.placeholder) } /> );
$input = (
<Input onChange={(e) => props.onChange(e.target.value)} {...id} name={struct.label}
type="date" defaultValue={struct.value || ""}
placeholder={ t(struct.placeholder) } />
);
break;
case "datetime":
$input = ( <Input onChange={(e) => props.onChange(e.target.value)} {...id} name={struct.label} type="datetime-local" defaultValue={struct.value || ""} placeholder={ t(struct.placeholder) } /> );
$input = (
<Input onChange={(e) => props.onChange(e.target.value)} {...id} name={struct.label}
type="datetime-local" defaultValue={struct.value || ""}
placeholder={ t(struct.placeholder) } />
);
break;
case "image":
$input = ( <img {...id} src={struct.value} /> );
break;
case "file": {
const getBase64 = function(file){
const getBase64 = function(file) {
return new Promise((resolve, reject) => {
const reader = new window.FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
reader.onerror = (error) => reject(error);
});
};
const onFileUpload = (e) => {
if(e.target.files.length !== 1) return;
if(e.target.files[0].size < 200000000){
if (e.target.files.length !== 1) return;
if (e.target.files[0].size < 200000000) {
getBase64(e.target.files[0]).then((a) => {
props.onChange(a);
}).catch(() => {});
@ -242,9 +300,15 @@ const FormElement = (props) => {
};
$input = (
<div className="fileupload-image">
<input onChange={(e) => onFileUpload(e)} type="file" {...id} name={struct.label} />
{ struct.value.substring(0,10) === "data:image" ? <img src={struct.value} /> : null }
{ struct.value.substring(0,20) === "data:application/pdf" ? <object data={struct.value} type="application/pdf" /> : null }
<input onChange={(e) => onFileUpload(e)} type="file" {...id} name={struct.label} />
{
struct.value.substring(0, 10) === "data:image" ?
<img src={struct.value} /> : null
}
{
struct.value.substring(0, 20) === "data:application/pdf" ?
<object data={struct.value} type="application/pdf" /> : null
}
</div>
);
break;
@ -258,5 +322,5 @@ const FormElement = (props) => {
};
FormElement.propTypes = {
render: PropTypes.func.isRequired
render: PropTypes.func.isRequired,
};

View file

@ -44,89 +44,89 @@ import img_copy from "../assets/img/copy.svg";
export const img_placeholder = "/assets/icons/placeholder.png";
export const Icon = (props) => {
if(props.name === null) return null;
if (props.name === null) return null;
let img;
if(props.name === "directory"){
if (props.name === "directory") {
img = "/assets/icons/folder.svg";
}else if(props.name === "file"){
} else if (props.name === "file") {
img = "/assets/icons/file.svg";
}else if(props.name === "save"){
} else if (props.name === "save") {
img = img_save;
}else if(props.name === "power"){
} else if (props.name === "power") {
img = img_power;
}else if(props.name === "edit"){
} else if (props.name === "edit") {
img = "/assets/icons/edit.svg";
}else if(props.name === "delete"){
} else if (props.name === "delete") {
img = "/assets/icons/delete.svg";
}else if(props.name === "share"){
} else if (props.name === "share") {
img = "/assets/icons/share.svg";
}else if(props.name === "bucket"){
} else if (props.name === "bucket") {
img = img_bucket;
}else if(props.name === "download_white"){
} else if (props.name === "download_white") {
img = img_download_white;
}else if(props.name === "upload_white"){
} else if (props.name === "upload_white") {
img = img_upload_white;
}else if(props.name === "play"){
} else if (props.name === "play") {
img = img_play;
}else if(props.name === "pause"){
} else if (props.name === "pause") {
img = img_pause;
}else if(props.name === "error"){
} else if (props.name === "error") {
img = img_error;
}else if(props.name === "loading_white"){
} else if (props.name === "loading_white") {
img = img_loading_white;
}else if(props.name === "loading"){
} else if (props.name === "loading") {
img = img_loading;
}else if(props.name === "calendar_white"){
} else if (props.name === "calendar_white") {
img = img_calendar_white;
}else if(props.name === "schedule"){
} else if (props.name === "schedule") {
img = img_calendar;
}else if(props.name === "deadline"){
} else if (props.name === "deadline") {
img = img_alarm;
}else if(props.name === "todo_white"){
} else if (props.name === "todo_white") {
img = img_todo_white;
}else if(props.name === "arrow_bottom"){
} else if (props.name === "arrow_bottom") {
img = img_arrow_bottom;
}else if(props.name === "arrow_top"){
} else if (props.name === "arrow_top") {
img = img_arrow_top;
}else if(props.name === "arrow_right"){
} else if (props.name === "arrow_right") {
img = img_arrow_right;
}else if(props.name === "arrow_right_white"){
} else if (props.name === "arrow_right_white") {
img = img_arrow_right_white;
}else if(props.name === "arrow_left_white"){
} else if (props.name === "arrow_left_white") {
img = img_arrow_left_white;
}else if(props.name === "arrow_left"){
} else if (props.name === "arrow_left") {
img = img_arrow_left;
}else if(props.name === "more"){
} else if (props.name === "more") {
img = img_more;
}else if(props.name === "close"){
} else if (props.name === "close") {
img = img_close;
}else if(props.name === "close_dark"){
} else if (props.name === "close_dark") {
img = img_close_dark;
}else if(props.name === "arrow_up_double"){
} else if (props.name === "arrow_up_double") {
img = img_arrow_up_double;
}else if(props.name === "arrow_down_double"){
} else if (props.name === "arrow_down_double") {
img = img_arrow_down_double;
}else if(props.name === "arrow_down"){
} else if (props.name === "arrow_down") {
img = img_arrow_down;
}else if(props.name === "search"){
} else if (props.name === "search") {
img = img_search;
}else if(props.name === "search_dark"){
} else if (props.name === "search_dark") {
img = img_search_dark;
}else if(props.name === "grid"){
} else if (props.name === "grid") {
img = img_grid;
}else if(props.name === "list"){
} else if (props.name === "list") {
img = img_list;
}else if(props.name === "sort"){
} else if (props.name === "sort") {
img = img_sort;
}else if(props.name === "check"){
} else if (props.name === "check") {
img = img_check;
}else if(props.name === "info"){
} else if (props.name === "info") {
img = img_info;
}else if(props.name === "fullscreen"){
} else if (props.name === "fullscreen") {
img = img_fullscreen;
}else if(props.name === "camera"){
} else if (props.name === "camera") {
img = img_camera;
}else if(props.name === "location"){
} else if (props.name === "location") {
img = img_location;
} else if (props.name === "stop") {
img = img_stop;
@ -134,16 +134,16 @@ export const Icon = (props) => {
img = img_refresh;
} else if (props.name === "copy") {
img = img_copy;
} else{
throw(`unknown icon: "${props.name}"`);
} else {
throw (`unknown icon: "${props.name}"`);
}
return (
<img className="component_icon"
onError={() => Log.send(`cannot load icon ${props.name}`)}
style={props.style}
onClick={props.onClick}
src={img}
alt={props.name}/>
onError={() => Log.send(`cannot load icon ${props.name}`)}
style={props.style}
onClick={props.onClick}
src={img}
alt={props.name}/>
);
};

View file

@ -3,66 +3,67 @@ import PropTypes from "prop-types";
import "./input.scss";
export class Input extends React.Component {
constructor(props){
constructor(props) {
super(props);
}
render() {
return (
<input
className="component_input"
onChange={this.props.onChange}
{...this.props}
ref={(comp) => { this.ref = comp; }} />
className="component_input"
onChange={this.props.onChange}
{...this.props}
ref={(comp) => this.ref = comp } />
);
}
}
Input.propTypes = {
type: PropTypes.string,
placeholder: PropTypes.string
placeholder: PropTypes.string,
};
export const Select = (props) => {
const choices = props.choices || [];
const id = props.id ? {id: props.id} : {};
const id = props.id ? { id: props.id } : {};
return (
<select className="component_select" {...id} name={props.name} onChange={props.onChange} defaultValue={props.value}>
<option hidden>{props.placeholder}</option>
{
choices.map((choice, index) => {
return (
<option key={index} name={choice}>{choice}</option>
);
})
}
<select className="component_select" onChange={props.onChange} {...id}
name={props.name} defaultValue={props.value}>
<option hidden>{ props.placeholder }</option>
{
choices.map((choice, index) => {
return (
<option key={index} name={choice}>{choice}</option>
);
})
}
</select>
);
};
export class Enabler extends React.Component {
constructor(props){
constructor(props) {
super(props);
}
componentDidMount(){
componentDidMount() {
requestAnimationFrame(() => {
this.toggle(this.props.defaultValue || false);
});
}
onChange(e){
onChange(e) {
this.toggle(e.target.checked);
this.props.onChange(e);
}
toggle(value){
toggle(value) {
const target = this.props.target || [];
target.map((t) => {
let $el = document.getElementById(t);
if(!$el) return;
if(value === true){
const $el = document.getElementById(t);
if (!$el) return;
if (value === true) {
$el.parentElement.parentElement.parentElement.style.display = "block";
$el.parentElement.parentElement.parentElement.style.opacity = "1";
} else {
@ -70,9 +71,9 @@ export class Enabler extends React.Component {
$el.parentElement.parentElement.parentElement.style.opacity = "0";
// reset value
if($el.value){
if ($el.value) {
$el.value = null;
let event = new Event("input", { bubbles: true});
const event = new Event("input", { bubbles: true });
event.simulated = true;
$el.dispatchEvent(event);
}
@ -80,9 +81,10 @@ export class Enabler extends React.Component {
});
}
render(){
render() {
return (
<Input type="checkbox" onChange={this.onChange.bind(this)} defaultChecked={this.props.defaultValue} />
<Input type="checkbox" onChange={this.onChange.bind(this)}
defaultChecked={this.props.defaultValue} />
);
}
}

View file

@ -5,16 +5,21 @@ import "./loader.scss";
export const Loader = () => {
return (
<CSSTransition transitionName="loader" transitionAppearTimeout={700}>
<div className="component_loader">
<svg width="120px" height="120px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<rect x="0" y="0" width="100" height="100" fill="none"></rect>
<circle cx="50" cy="50" r="40" stroke="rgba(100%,100%,100%,0.679)" fill="none" strokeWidth="10" strokeLinecap="round"></circle>
<circle cx="50" cy="50" r="40" stroke="#6f6f6f" fill="none" strokeWidth="6" strokeLinecap="round">
<animate attributeName="stroke-dashoffset" dur="2s" repeatCount="indefinite" from="0" to="502"></animate>
<animate attributeName="stroke-dasharray" dur="2s" repeatCount="indefinite" values="150.6 100.4;1 250;150.6 100.4"></animate>
</circle>
</svg>
</div>
<div className="component_loader">
<svg width="120px" height="120px" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<rect x="0" y="0" width="100" height="100" fill="none"></rect>
<circle cx="50" cy="50" r="40" stroke="rgba(100%,100%,100%,0.679)" fill="none"
strokeWidth="10" strokeLinecap="round"></circle>
<circle cx="50" cy="50" r="40" stroke="#6f6f6f" fill="none" strokeWidth="6"
strokeLinecap="round">
<animate attributeName="stroke-dashoffset" dur="2s" repeatCount="indefinite"
from="0" to="502"></animate>
<animate attributeName="stroke-dasharray" dur="2s" repeatCount="indefinite"
values="150.6 100.4;1 250;150.6 100.4"></animate>
</circle>
</svg>
</div>
</CSSTransition>
);
};

View file

@ -4,121 +4,165 @@ import { t } from "../locales/";
import "./mapshot.scss";
export class MapShot extends React.Component {
constructor(props){
constructor(props) {
super(props);
this.state = {
tile_size: 0,
tile_loaded: 0,
error: false
error: false,
};
this.onRefresh = this.onRefresh.bind(this);
}
onRefresh(){
onRefresh() {
requestAnimationFrame(() => {
if(this.refs.$wrapper){
if (this.refs.$wrapper) {
this.setState({
tile_size: this.calculateSize()
tile_size: this.calculateSize(),
});
}
});
}
calculateSize(){
if(!this.refs.$wrapper) return 0;
calculateSize() {
if (!this.refs.$wrapper) return 0;
return parseInt(this.refs.$wrapper.clientWidth / 3 * 100) / 100;
}
componentDidMount(){
componentDidMount() {
this.onRefresh();
window.addEventListener("resize", this.onRefresh);
}
componentWillUnmount(){
componentWillUnmount() {
window.removeEventListener("resize", this.onRefresh);
}
insert_marker(position){
if(!(this.state.tile_size > 0)) return null;
insert_marker(position) {
if (!(this.state.tile_size > 0)) return null;
return (
<div className="marker" style={{
left: this.state.tile_size * (1 + position[0]) - 15,
top: this.state.tile_size * (1 + position[1]) - 30 }}>
<Icon name="location"/>
left: this.state.tile_size * (1 + position[0]) - 15,
top: this.state.tile_size * (1 + position[1]) - 30,
}}>
<Icon name="location"/>
</div>
);
}
onLoad(){
this.setState({tile_loaded: this.state.tile_loaded + 1});
onLoad() {
this.setState({ tile_loaded: this.state.tile_loaded + 1 });
}
onError(){
this.setState({error: true});
onError() {
this.setState({ error: true });
}
render(){
if(this.calculateSize() !== this.state.tile_size && this.calculateSize() !== 0){
render() {
if (this.calculateSize() !== this.state.tile_size && this.calculateSize() !== 0) {
this.onRefresh();
}
const tile_server = this.props.tile || "https://maps.wikimedia.org/osm-intl/${z}/${x}/${y}.png";
function map_url(lat, lng, zoom){
const tile_server = this.props.tile ||
"https://maps.wikimedia.org/osm-intl/${z}/${x}/${y}.png";
function map_url(lat, lng, zoom) {
// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenamse
const n = Math.pow(2, zoom);
const tile_numbers = [
(lng+180)/360*n,
(1-Math.log(Math.tan(lat*Math.PI/180) + 1/Math.cos(lat*Math.PI/180))/Math.PI)/2*n,
zoom
zoom,
];
return {
tile: function(tile_server, x = 0, y = 0){
tile: function(tile_server, x = 0, y = 0) {
return tile_server
.replace("${x}", Math.floor(tile_numbers[0])+x)
.replace("${y}", Math.floor(tile_numbers[1])+y)
.replace("${z}", Math.floor(zoom));
},
position: function(){
position: function() {
return [
tile_numbers[0] - Math.floor(tile_numbers[0]),
tile_numbers[1] - Math.floor(tile_numbers[1]),
];
}
},
};
}
const mapper = map_url(this.props.lat, this.props.lng, 11);
const center= (position, i) => {
return parseInt(this.state.tile_size * (1 + position[i]) * 1000)/1000;
};
const link = "https://www.google.com/maps/search/?api=1&query=" +
this.props.lat + "," + this.props.lng;
return (
<div ref="$wrapper" className={"component_mapshot"+(this.state.tile_loaded === 9 ? " loaded" : "")+(this.state.error === true ? " error": "")} style={{height: (this.state.tile_size*3)+"px"}}>
<div className="wrapper">
<div className="mapshot_placeholder error">
<span><div>{ t("ERROR") }</div></span>
<div ref="$wrapper"
className={"component_mapshot" + (this.state.tile_loaded === 9 ? " loaded" : "") +
(this.state.error === true ? " error": "")}
style={{ height: (this.state.tile_size*3)+"px" }}
>
<div className="wrapper">
<div className="mapshot_placeholder error">
<span><div>{ t("ERROR") }</div></span>
</div>
<div className="mapshot_placeholder loading">
<Loader/>
</div>
<a href={link}>
{ this.insert_marker(mapper.position()) }
<div className="bigpicture"
style={{
transformOrigin: center(mapper.position(), 0)+"px "+
center(mapper.position(), 1)+"px",
}}>
<div className="line">
<img onLoad={this.onLoad.bind(this)}
onError={this.onError.bind(this)}
src={mapper.tile(tile_server, -1, -1)}
ref="$tile"
style={{ height: this.state.tile_size+"px" }}
className="btl" />
<img onLoad={this.onLoad.bind(this)}
onError={this.onError.bind(this)}
src={mapper.tile(tile_server, 0, -1)}
style={{ height: this.state.tile_size+"px" }} />
<img onLoad={this.onLoad.bind(this)}
onError={this.onError.bind(this)}
src={mapper.tile(tile_server, 1, -1)}
style={{ height: this.state.tile_size+"px" }}
className="btr" />
</div>
<div className="line">
<img onLoad={this.onLoad.bind(this)}
onError={this.onError.bind(this)}
src={mapper.tile(tile_server, -1, 0)}
style={{ height: this.state.tile_size+"px" }} />
<img onLoad={this.onLoad.bind(this)}
onError={this.onError.bind(this)}
src={mapper.tile(tile_server, 0, 0)}
style={{ height: this.state.tile_size+"px" }} />
<img onLoad={this.onLoad.bind(this)}
onError={this.onError.bind(this)}
src={mapper.tile(tile_server, 1, 0)}
style={{ height: this.state.tile_size+"px" }} />
</div>
<div className="line">
<img onLoad={this.onLoad.bind(this)}
onError={this.onError.bind(this)}
src={mapper.tile(tile_server, -1, 1)}
style={{ height: this.state.tile_size+"px" }}
className="bbl" />
<img onLoad={this.onLoad.bind(this)}
onError={this.onError.bind(this)}
src={mapper.tile(tile_server, 0, 1)}
style={{ height: this.state.tile_size+"px" }} />
<img onLoad={this.onLoad.bind(this)}
onError={this.onError.bind(this)}
src={mapper.tile(tile_server, 1, 1)}
style={{ height: this.state.tile_size+"px" }}
className="bbr" />
</div>
</div>
</a>
</div>
<div className="mapshot_placeholder loading">
<Loader/>
</div>
<a href={"https://www.google.com/maps/search/?api=1&query="+this.props.lat+","+this.props.lng}>
{this.insert_marker(mapper.position())}
<div className="bigpicture" style={{transformOrigin: center(mapper.position(), 0)+"px "+center(mapper.position(), 1)+"px"}}>
<div className="line">
<img onLoad={this.onLoad.bind(this)} onError={this.onError.bind(this)} src={mapper.tile(tile_server, -1, -1)} ref="$tile" style={{height: this.state.tile_size+"px"}} className="btl"/>
<img onLoad={this.onLoad.bind(this)} onError={this.onError.bind(this)} src={mapper.tile(tile_server, 0, -1)} style={{height: this.state.tile_size+"px"}}/>
<img onLoad={this.onLoad.bind(this)} onError={this.onError.bind(this)} src={mapper.tile(tile_server, 1, -1)} style={{height: this.state.tile_size+"px"}} className="btr"/>
</div>
<div className="line">
<img onLoad={this.onLoad.bind(this)} onError={this.onError.bind(this)} src={mapper.tile(tile_server, -1, 0)} style={{height: this.state.tile_size+"px"}}/>
<img onLoad={this.onLoad.bind(this)} onError={this.onError.bind(this)} src={mapper.tile(tile_server, 0, 0)} style={{height: this.state.tile_size+"px"}}/>
<img onLoad={this.onLoad.bind(this)} onError={this.onError.bind(this)} src={mapper.tile(tile_server, 1, 0)} style={{height: this.state.tile_size+"px"}}/>
</div>
<div className="line">
<img onLoad={this.onLoad.bind(this)} onError={this.onError.bind(this)} src={mapper.tile(tile_server, -1, 1)} style={{height: this.state.tile_size+"px"}} className="bbl"/>
<img onLoad={this.onLoad.bind(this)} onError={this.onError.bind(this)} src={mapper.tile(tile_server, 0, 1)} style={{height: this.state.tile_size+"px"}}/>
<img onLoad={this.onLoad.bind(this)} onError={this.onError.bind(this)} src={mapper.tile(tile_server, 1, 1)} style={{height: this.state.tile_size+"px"}} className="bbr"/>
</div>
</div>
</a>
</div>
</div>
);
}

View file

@ -1,34 +1,33 @@
import React, { useEffect, useState } from "react";
import ReactCSSTransitionGroup from "react-addons-css-transition-group";
import PropTypes from "prop-types";
import { NgIf } from "./";
import { debounce, nop } from "../helpers/";
import "./modal.scss";
export function Modal({
isActive = false, children = null, className = null, onQuit = nop
isActive = false, children = null, className = null, onQuit = nop,
}) {
const calculateMarginTop = () => {
let size = 300;
const $box = document.querySelector("#modal-box > div");
if($box) size = $box.offsetHeight;
if ($box) size = $box.offsetHeight;
size = Math.round((document.body.offsetHeight - size) / 2);
if(size < 0) return 0;
if(size > 250) return 250;
if (size < 0) return 0;
if (size > 250) return 250;
return size;
};
const resizeHandler = debounce(() => {
setMarginTop(calculateMarginTop())
}, 100)
setMarginTop(calculateMarginTop());
}, 100);
const keydownHandler = (e) => {
if(e.keyCode === 27){
if (e.keyCode === 27) {
onQuit();
}
};
const onClick = (e) => {
if(e.target.getAttribute("id") === "modal-box"){
if (e.target.getAttribute("id") === "modal-box") {
onQuit();
}
};
@ -40,14 +39,19 @@ export function Modal({
return () => {
window.removeEventListener("resize", resizeHandler);
window.removeEventListener("keydown", keydownHandler);
}
};
});
return (
<ReactCSSTransitionGroup transitionName="modal" transitionLeaveTimeout={300} transitionEnterTimeout={300} transitionAppear={true} transitionAppearTimeout={300}>
<ReactCSSTransitionGroup transitionName="modal" transitionLeaveTimeout={300}
transitionEnterTimeout={300} transitionAppear={true} transitionAppearTimeout={300}>
<NgIf key={"modal-"+isActive} cond={isActive}>
<div className={"component_modal"+(className? " " + className : "")} onClick={onClick} id="modal-box">
<div style={{margin: marginTop+"px auto 0 auto", visibility: marginTop === -1 ? "hidden" : "visible"}}>
<div className={"component_modal"+(className? " " + className : "")}
onClick={onClick} id="modal-box">
<div style={{
margin: marginTop+"px auto 0 auto",
visibility: marginTop === -1 ? "hidden" : "visible",
}}>
{children}
</div>
</div>

View file

@ -2,22 +2,22 @@ import React from "react";
import PropTypes from "prop-types";
export class NgIf extends React.Component {
constructor(props){
constructor(props) {
super(props);
}
render() {
let clean_prop = Object.assign({}, this.props);
const clean_prop = Object.assign({}, this.props);
delete clean_prop.cond;
delete clean_prop.children;
delete clean_prop.type;
if(this.props.cond){
if(this.props.type === "inline"){
if (this.props.cond) {
if (this.props.type === "inline") {
return <span {...clean_prop}>{this.props.children}</span>;
}else{
} else {
return <div {...clean_prop}>{this.props.children}</div>;
}
}else{
} else {
return null;
}
}
@ -25,30 +25,30 @@ export class NgIf extends React.Component {
NgIf.propTypes = {
cond: PropTypes.bool.isRequired,
type: PropTypes.string
type: PropTypes.string,
};
export class NgShow extends React.Component {
constructor(props){
constructor(props) {
super(props);
}
render() {
let clean_prop = Object.assign({}, this.props);
const clean_prop = Object.assign({}, this.props);
delete clean_prop.cond;
delete clean_prop.children;
delete clean_prop.type;
if(this.props.cond){
if(this.props.type === "inline"){
return <span {...clean_prop}>{this.props.children}</span>;
}else{
return <div {...clean_prop}>{this.props.children}</div>;
if (this.props.cond) {
if (this.props.type === "inline") {
return <span {...clean_prop}>{ this.props.children }</span>;
} else {
return <div {...clean_prop}>{ this.props.children }</div>;
}
}else{
} else {
return (
<div style={{display: "none"}}>
{this.props.children}
<div style={{ display: "none" }}>
{ this.props.children }
</div>
);
}
@ -57,5 +57,5 @@ export class NgShow extends React.Component {
NgShow.propTypes = {
cond: PropTypes.bool.isRequired,
type: PropTypes.string
type: PropTypes.string,
};

View file

@ -7,128 +7,133 @@ import { t } from "../locales/";
import "./notification.scss";
export class Notification extends React.Component {
constructor(props){
constructor(props) {
super(props);
this.state = {
appear: false,
message_text: null,
message_type: null
message_type: null,
};
this.runner = new TaskManager();
this.notification_current = null;
}
componentDidMount(){
componentDidMount() {
this.runner.before_run((task) => {
this.notification_current = task;
});
notify.subscribe((message, type) => {
this.runner.addTask(Task(
this.openNotification.bind(this, {text: stringify(message), type: type}),
this.openNotification.bind(this, { text: stringify(message), type: type }),
this.closeNotification.bind(this),
8000,
500
500,
));
});
function stringify(data){
if(typeof data === "object" && data.message){
function stringify(data) {
if (typeof data === "object" && data.message) {
return data.message;
}else if(typeof data === "string"){
} else if (typeof data === "string") {
return data;
}
return JSON.stringify(data);
}
}
closeNotification(){
closeNotification() {
return new Promise((done) => {
this.setState({
appear: false
appear: false,
}, done);
});
}
openNotification(message){
openNotification(message) {
return new Promise((done) => {
this.setState({
appear: true,
message_text: message.text,
message_type: message.type
message_type: message.type,
}, done);
});
}
cancelAnimation(){
cancelAnimation() {
return this.notification_current.cancel();
}
render(){
render() {
return (
<ReactCSSTransitionGroup transitionName="notification" transitionLeave={true} transitionLeaveTimeout={200} transitionEnter={true} transitionEnterTimeout={100} transitionAppear={false} className="component_notification">
<NgIf key={this.state.message_text+this.state.message_type+this.state.appear} cond={this.state.appear === true} className="no-select">
<div className={"component_notification--container "+(this.state.message_type || "info")}>
<div className="message">
{ t(this.state.message_text || "") }
</div>
<div className="close" onClick={this.cancelAnimation.bind(this)}>
<Icon name="close" />
</div>
</div>
</NgIf>
<ReactCSSTransitionGroup transitionName="notification" transitionLeave={true}
transitionLeaveTimeout={200} transitionEnter={true} transitionEnterTimeout={100}
transitionAppear={false} className="component_notification"
>
<NgIf key={this.state.message_text+this.state.message_type+this.state.appear}
cond={this.state.appear === true} className="no-select">
<div className={
"component_notification--container "+(this.state.message_type || "info")
}>
<div className="message">
{ t(this.state.message_text || "") }
</div>
<div className="close" onClick={this.cancelAnimation.bind(this)}>
<Icon name="close" />
</div>
</div>
</NgIf>
</ReactCSSTransitionGroup>
);
}
}
function TaskManager(){
let tasks = [];
function TaskManager() {
const tasks = [];
let is_running = false;
let subscriber = null;
let current_task = null;
const ret ={
addTask: function(task){
addTask: function(task) {
current_task && current_task.cancel();
tasks.push(task);
if(tasks.length > 20){
if (tasks.length > 20) {
tasks.splice(0, tasks.length - 10);
}
if(is_running === false){
if (is_running === false) {
is_running = true;
ret._run();
}
},
before_run: function(fn){
before_run: function(fn) {
subscriber = fn;
},
_run: function(){
_run: function() {
current_task = tasks.shift();
if(!current_task){
if (!current_task) {
is_running = false;
return Promise.resolve();
}else{
} else {
const mode = tasks.length > 0 ? "minimal" : "normal";
subscriber(current_task, mode);
return current_task.run(mode).then(ret._run);
}
}
},
};
return ret;
}
function Task(_runCallback, _finishCallback, wait_time_before_finish, minimum_running_time){
function Task(_runCallback, _finishCallback, wait_time_before_finish, minimum_running_time) {
let start_date = null;
let done = null;
let promise = new Promise((_done) => { done = _done; });
const promise = new Promise((_done) => {
done = _done;
});
let timeout = null;
const ret = {
run: function(mode = "normal"){
run: function(mode = "normal") {
const wait = mode === "minimal" ? minimum_running_time : wait_time_before_finish;
start_date = new Date();
@ -148,28 +153,28 @@ function Task(_runCallback, _finishCallback, wait_time_before_finish, minimum_ru
});
return promise;
},
cancel: function(){
cancel: function() {
window.clearTimeout(timeout);
timeout = null;
let elapsed_time = new Date() - start_date;
const elapsed_time = new Date() - start_date;
if(elapsed_time < minimum_running_time){
if (elapsed_time < minimum_running_time) {
window.setTimeout(() => {
ret._complete();
}, minimum_running_time - elapsed_time);
}else{
} else {
ret._complete();
}
return promise;
},
_complete: function(){
if(done){
_complete: function() {
if (done) {
_finishCallback();
done();
}
done = null;
return Promise.resolve();
}
},
};
return ret;
}

View file

@ -4,37 +4,37 @@ import { Modal } from "./";
import "./popup.scss";
export class Popup extends React.Component {
constructor(props){
constructor(props) {
super(props);
if(new.target === Popup){
if (new.target === Popup) {
throw new TypeError("Cannot construct Popup instances directly");
}
this.state = {
appear: false
appear: false,
};
}
onSubmit(e){
onSubmit(e) {
e && e.preventDefault && e.preventDefault();
this.setState({appear: false});
this.setState({ appear: false });
}
onCancel(){
this.setState({appear: false});
onCancel() {
this.setState({ appear: false });
}
render() {
return (
<Modal isActive={this.state.appear} onQuit={this.onCancel.bind(this)}>
<div className="component_popup">
<div className="popup--content">
{ this.modalContentBody() }
<div className="component_popup">
<div className="popup--content">
{ this.modalContentBody() }
</div>
<div className="buttons">
{ this.modalContentFooter() }
</div>
</div>
<div className="buttons">
{ this.modalContentFooter() }
</div>
</div>
</Modal>
);
}

View file

@ -6,11 +6,11 @@ import { Popup } from "./popup";
import { t } from "../locales/";
export class ModalPrompt extends Popup {
constructor(props){
constructor(props) {
super(props);
}
componentDidMount(){
componentDidMount() {
prompt.subscribe((text, okCallback, cancelCallback, type) => {
this.setState({
appear: true,
@ -18,37 +18,42 @@ export class ModalPrompt extends Popup {
error: null,
type: type || "text",
text: text || "",
fns: {ok: okCallback, cancel: cancelCallback}
fns: { ok: okCallback, cancel: cancelCallback },
});
});
}
onSubmit(e){
onSubmit(e) {
e && e.preventDefault && e.preventDefault();
this.state.fns.ok(this.state.value)
.then(() => this.setState({appear: false}))
.catch((message) => this.setState({error: message}));
.then(() => this.setState({ appear: false }))
.catch((message) => this.setState({ error: message }));
}
modalContentBody(){
modalContentBody() {
return (
<div>
{this.state.text }
<form onSubmit={this.onSubmit.bind(this)} style={{marginTop: "10px"}}>
<Input autoFocus={true} value={this.state.value} type={this.state.type} autoComplete="new-password" onChange={(e) => this.setState({value: e.target.value})} />
<div className="modal-error-message">{this.state.error}&nbsp;</div>
</form>
{ this.state.text }
<form onSubmit={this.onSubmit.bind(this)} style={{ marginTop: "10px" }}>
<Input autoFocus={true} value={this.state.value}
type={this.state.type} autoComplete="new-password"
onChange={(e) => this.setState({ value: e.target.value })} />
<div className="modal-error-message">{this.state.error}&nbsp;</div>
</form>
</div>
);
}
modalContentFooter(){
modalContentFooter() {
return (
<div>
<Button type="button" onClick={this.onCancel.bind(this)}>{ t("CANCEL") }</Button>
<Button type="submit" theme="emphasis" onClick={this.onSubmit.bind(this)}>{ t("OK") }</Button>
<Button type="button" onClick={this.onCancel.bind(this)}>
{ t("CANCEL") }
</Button>
<Button type="submit" theme="emphasis" onClick={this.onSubmit.bind(this)}>
{ t("OK") }
</Button>
</div>
);
}
}

View file

@ -1,34 +1,35 @@
import React, { useRef, useState, useLayoutEffect } from "react";
import PropTypes from "prop-types";
import "./textarea.scss";
export function Textarea({ ...props }) {
const $el = useRef();
const [className, setClassName] = useState(
"component_textarea"
+ (/Firefox/.test(navigator.userAgent) ? " firefox" : "")
+ (props.value && props.value.length > 0 ? " hasText" : "")
"component_textarea" +
(/Firefox/.test(navigator.userAgent) ? " firefox" : "") +
(props.value && props.value.length > 0 ? " hasText" : ""),
);
useLayoutEffect(() => {
if($el.current && $el.current.value.length > 0 && className.indexOf("hasText") === -1){
setClassName(`${className} hasText`)
if ($el.current && $el.current.value.length > 0 &&
className.indexOf("hasText") === -1) {
setClassName(`${className} hasText`);
}
}, []);
const disabledEnter = (e) => {
if(e.key === "Enter" && e.shiftKey === false){
if (e.key === "Enter" && e.shiftKey === false) {
e.preventDefault();
const $form = getForm($el.current);
if($form){
if ($form) {
$form.dispatchEvent(new Event("submit", { cancelable: true }));
}
}
function getForm($el){
if(!$el.parentElement) return $el;
if($el.parentElement.nodeName == "FORM"){
function getForm($el) {
console.log("GET FORM $el", $el);
if (!$el.parentElement) return $el;
if ($el.parentElement.nodeName == "FORM") {
return $el.parentElement;
}
return getForm($el.parentElement);
@ -36,7 +37,7 @@ export function Textarea({ ...props }) {
};
const inputProps = (p) => {
return Object.keys(p).reduce((acc, key) => {
if(key === "disabledEnter") return acc;
if (key === "disabledEnter") return acc;
acc[key] = p[key];
return acc;
}, {});
@ -49,59 +50,5 @@ export function Textarea({ ...props }) {
className={className}
ref={$el}>
</textarea>
)
);
}
export class Textarea2 extends React.Component {
constructor(props){
super(props);
}
render() {
let className = "component_textarea";
if(/Firefox/.test(navigator.userAgent)){
className += " firefox";
}
if((this.refs.el !== undefined && this.refs.el.value.length > 0) || (this.props.value !== undefined && this.props.value.length > 0)){
className += " hasText";
}
const disabledEnter = (e) => {
if(e.key === "Enter" && e.shiftKey === false){
e.preventDefault();
const $form = getForm(this.refs.el);
if($form){
$form.dispatchEvent(new Event("submit", { cancelable: true }));
}
}
function getForm($el){
if(!$el.parentElement) return $el;
if($el.parentElement.nodeName == "FORM"){
return $el.parentElement;
}
return getForm($el.parentElement);
}
};
const inputProps = (p) => {
return Object.keys(p).reduce((acc, key) => {
if(key === "disabledEnter") return acc;
acc[key] = p[key];
return acc;
}, {});
};
return (
<textarea
onKeyPress={disabledEnter}
{...inputProps(this.props)}
className={className}
ref="el"
></textarea>
);
}
}
Textarea.propTypes = {
type: PropTypes.string,
placeholder: PropTypes.string,
disabledEnter: PropTypes.bool
};

View file

@ -3,16 +3,16 @@ import { Icon } from "./";
import "./video.scss";
export class Video extends React.Component {
constructor(props){
constructor(props) {
super(props);
}
render(){
render() {
return (
<div className="component_video">
<div className="loader">
<Icon name="loading"/>
</div>
<div className="loader">
<Icon name="loading"/>
</div>
</div>
);
}

View file

@ -1,171 +1,181 @@
export function http_get(url, type = 'json'){
export function http_get(url, type = "json") {
return new Promise((done, err) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.withCredentials = true;
xhr.setRequestHeader('X-Requested-With', 'XmlHttpRequest');
xhr.setRequestHeader("X-Requested-With", "XmlHttpRequest");
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if(xhr.status === 200){
if(type === 'json'){
try{
let data = JSON.parse(xhr.responseText);
if("status" in data === false || data.status === 'ok'){
if (xhr.status === 200) {
if (type === "json") {
try {
const data = JSON.parse(xhr.responseText);
if ("status" in data === false || data.status === "ok") {
done(data);
}else{
} else {
err(data);
}
}catch(error){
err({message: 'oups', trace: error});
} catch (error) {
err({ message: "oups", trace: error });
}
}else{
} else {
done(xhr.responseText);
}
}else{
} else {
handle_error_response(xhr, err);
}
}
}
};
xhr.send(null);
xhr.onerror = function(){
handle_error_response(xhr, err)
}
xhr.onerror = function() {
handle_error_response(xhr, err);
};
});
}
export function http_post(url, data, type = 'json', params){
export function http_post(url, data, type = "json", params) {
return new Promise((done, err) => {
var xhr = new XMLHttpRequest();
const xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.withCredentials = true;
xhr.setRequestHeader('X-Requested-With', 'XmlHttpRequest');
if(type === 'json'){
xhr.setRequestHeader("X-Requested-With", "XmlHttpRequest");
if (type === "json") {
data = JSON.stringify(data);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader("Content-Type", "application/json");
}
if (params && params.progress) {
xhr.upload.addEventListener("progress", params.progress, false);
}
xhr.send(data);
xhr.onload = function () {
xhr.onload = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if(xhr.status === 200){
try{
let data = JSON.parse(xhr.responseText);
if(data.status === 'ok'){
if (xhr.status === 200) {
try {
const data = JSON.parse(xhr.responseText);
if (data.status === "ok") {
done(data);
}else{
} else {
err(data);
}
}catch(error){
err({message: 'oups', trace: error});
} catch (error) {
err({ message: "oups", trace: error });
}
}else{
} else {
handle_error_response(xhr, err);
}
}
}
xhr.onerror = function(){
handle_error_response(xhr, err)
}
};
xhr.onerror = function() {
handle_error_response(xhr, err);
};
if (params && params.abort) {
params.abort(() => {
xhr.abort();
err({ message: 'aborted' });
})
err({ message: "aborted" });
});
}
});
}
export function http_delete(url){
export function http_delete(url) {
return new Promise((done, err) => {
var xhr = new XMLHttpRequest();
const xhr = new XMLHttpRequest();
xhr.open("DELETE", url, true);
xhr.withCredentials = true;
xhr.setRequestHeader('X-Requested-With', 'XmlHttpRequest');
xhr.onload = function () {
xhr.setRequestHeader("X-Requested-With", "XmlHttpRequest");
xhr.onload = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if(xhr.status === 200){
try{
let data = JSON.parse(xhr.responseText);
if(data.status === 'ok'){
if (xhr.status === 200) {
try {
const data = JSON.parse(xhr.responseText);
if (data.status === "ok") {
done(data);
}else{
} else {
err(data);
}
}catch(error){
err({message: 'oups', trace: error});
} catch (error) {
err({ message: "oups", trace: error });
}
}else{
} else {
handle_error_response(xhr, err);
}
}
}
xhr.onerror = function(){
handle_error_response(xhr, err)
}
};
xhr.onerror = function() {
handle_error_response(xhr, err);
};
xhr.send(null);
});
}
export function http_options(url){
export function http_options(url) {
return new Promise((done, err) => {
var xhr = new XMLHttpRequest();
const xhr = new XMLHttpRequest();
xhr.open("OPTIONS", url, true);
xhr.withCredentials = true;
xhr.setRequestHeader('X-Requested-With', 'XmlHttpRequest');
xhr.onload = function(){
if(xhr.readyState === XMLHttpRequest.DONE){
if(xhr.status !== 200){
xhr.setRequestHeader("X-Requested-With", "XmlHttpRequest");
xhr.onload = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status !== 200) {
handle_error_response(xhr, err);
return
return;
}
done(xhr.getAllResponseHeaders()
.split("\n")
.reduce((acc, r) => {
const a = r.split(": ");
acc[a[0]] = a[1];
return acc;
}, {}));
done(
xhr.getAllResponseHeaders()
.split("\n")
.reduce((acc, r) => {
const a = r.split(": ");
acc[a[0]] = a[1];
return acc;
}, {}),
);
}
}
};
xhr.send(null);
})
});
}
function handle_error_response(xhr, err){
const response = (function(content){
function handle_error_response(xhr, err) {
const response = (function(content) {
let message = content;
try{
try {
message = JSON.parse(content);
}catch(err){}
return message || {};
} catch (err) {
return { message: content };
}
return message || { message: "empty response" };
})(xhr.responseText);
const message = response.message || null;
if(navigator.onLine === false){
err({message: 'Connection Lost', code: "NO_INTERNET"});
}else if(xhr.status === 0 && xhr.responseText === "") {
err({message: "Service unavailable, if the problem persist, contact your administrator", code: "INTERNAL_SERVER_ERROR"});
}else if(xhr.status === 500){
err({message: message || "Oups something went wrong with our servers", code: "INTERNAL_SERVER_ERROR"});
}else if(xhr.status === 401){
err({message: message || "Authentication error", code: "Unauthorized"});
}else if(xhr.status === 403){
err({message: message || "You can\'t do that", code: "Forbidden"});
}else if(xhr.status === 413){
err({message: message || "Payload too large", code: "PAYLOAD_TOO_LARGE"});
}else if(xhr.status === 502){
err({message: message || "The destination is acting weird", code: "BAD_GATEWAY"});
}else if(xhr.status === 409){
if(response["error_summary"]){ // dropbox way to say doesn't exist
err({message: "Doesn\'t exist", code: "UNKNOWN_PATH"})
}else{
err({message: message || "Oups you just ran into a conflict", code: "CONFLICT"});
if (navigator.onLine === false) {
err({ message: "Connection Lost", code: "NO_INTERNET" });
} else if (xhr.status === 0 && xhr.responseText === "") {
err({
message: "Service unavailable, if the problem persist, contact your administrator",
code: "INTERNAL_SERVER_ERROR",
});
} else if (xhr.status === 500) {
err({
message: message || "Oups something went wrong with our servers",
code: "INTERNAL_SERVER_ERROR",
});
} else if (xhr.status === 401) {
err({ message: message || "Authentication error", code: "Unauthorized" });
} else if (xhr.status === 403) {
err({ message: message || "You can\'t do that", code: "Forbidden" });
} else if (xhr.status === 413) {
err({ message: message || "Payload too large", code: "PAYLOAD_TOO_LARGE" });
} else if (xhr.status === 502) {
err({ message: message || "The destination is acting weird", code: "BAD_GATEWAY" });
} else if (xhr.status === 409) {
if (response["error_summary"]) { // dropbox way to say doesn't exist
err({ message: "Doesn\'t exist", code: "UNKNOWN_PATH" });
} else {
err({ message: message || "Oups you just ran into a conflict", code: "CONFLICT" });
}
}else{
err({message: message || 'Oups something went wrong'});
} else {
err({ message: message || "Oups something went wrong" });
}
}

View file

@ -1,17 +1,19 @@
/* eslint-disable */
export function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
export function throttle(func, wait, options) {
var context, args, result;
@ -43,4 +45,4 @@ export function throttle(func, wait, options) {
}
return result;
};
};
}

View file

@ -1,10 +1,10 @@
import bcrypt from 'bcryptjs';
import bcrypt from "bcryptjs";
export function bcrypt_password(password){
export function bcrypt_password(password) {
return new Promise((done, error) => {
bcrypt.hash(password, 10, (err, hash) => {
if(err) return error(err)
if (err) return error(err);
done(hash);
})
});
});
}

View file

@ -1,34 +1,34 @@
"use strict";
const DB_VERSION = 3,
FILE_PATH = "file_path",
FILE_CONTENT = "file_content";
const DB_VERSION = 3;
const FILE_PATH = "file_path";
const FILE_CONTENT = "file_content";
function DataFromIndexedDB(){
function DataFromIndexedDB() {
this.db = null;
this.FILE_PATH = FILE_PATH;
this.FILE_CONTENT = FILE_CONTENT;
return this._init();
}
function DataFromMemory(){
function DataFromMemory() {
this.data = {};
this.FILE_PATH = FILE_PATH;
this.FILE_CONTENT = FILE_CONTENT;
return this._init();
}
DataFromIndexedDB.prototype._init = function(){
DataFromIndexedDB.prototype._init = function() {
const request = indexedDB.open("filestash", DB_VERSION);
request.onupgradeneeded = function(event){
request.onupgradeneeded = function(event) {
let store;
let db = event.target.result;
const db = event.target.result;
if(event.oldVersion == 1) {
if (event.oldVersion == 1) {
// we've change the schema on v2 adding an index, let's flush
// to make sure everything will be fine
db.deleteObjectStore(FILE_PATH);
db.deleteObjectStore(FILE_CONTENT);
}else if(event.oldVersion == 2){
} else if (event.oldVersion == 2) {
// we've change the primary key to be a (path,share)
db.deleteObjectStore(FILE_PATH);
db.deleteObjectStore(FILE_CONTENT);
@ -37,7 +37,7 @@ DataFromIndexedDB.prototype._init = function(){
store = db.createObjectStore(FILE_PATH, { keyPath: ["share", "path"] });
store.createIndex("idx_path", ["share", "path"], { unique: true });
store = db.createObjectStore(FILE_CONTENT, {keyPath: ["share", "path"]});
store = db.createObjectStore(FILE_CONTENT, { keyPath: ["share", "path"] });
store.createIndex("idx_path", ["share", "path"], { unique: true });
};
@ -48,14 +48,15 @@ DataFromIndexedDB.prototype._init = function(){
request.onerror = (e) => err("INDEXEDDB_NOT_SUPPORTED");
});
};
DataFromMemory.prototype._init = function(){
DataFromMemory.prototype._init = function() {
};
/*
* Fetch a record using its path, can be either a file path or content
*/
DataFromIndexedDB.prototype.get = function(type, key){
if(type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
DataFromIndexedDB.prototype.get = function(type, key) {
if (type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
return this.db.then((db) => {
const tx = db.transaction(type, "readonly");
@ -63,22 +64,21 @@ DataFromIndexedDB.prototype.get = function(type, key){
const query = store.get(key);
return new Promise((done, error) => {
query.onsuccess = (e) => {
let data = query.result;
done(query.result || null);
};
query.onerror = () => done()
query.onerror = () => done();
});
});
};
DataFromMemory.prototype.get = function(type, key){
if(type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
DataFromMemory.prototype.get = function(type, key) {
if (type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
const data = this.data[type] || null;
if(data === null){
if (data === null) {
return Promise.resolve(null);
}
const value = data[key.join("_")] || null;
if(value === null){
if (value === null) {
return Promise.resolve(null);
}
return new Promise((done) => {
@ -86,23 +86,23 @@ DataFromMemory.prototype.get = function(type, key){
});
};
DataFromIndexedDB.prototype.update = function(type, key, fn, exact = true){
if(type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
DataFromIndexedDB.prototype.update = function(type, key, fn, exact = true) {
if (type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
return this.db.then((db) => {
const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type);
const range = exact === true? IDBKeyRange.only(key) : IDBKeyRange.bound(
[key[0], key[1]],
[key[0], key[1]+'\uFFFF'],
false, true
[key[0], key[1]+"\uFFFF"],
false, true,
);
const request = store.openCursor(range);
let new_data = null;
return new Promise((done, err) => {
request.onsuccess = function(event) {
const cursor = event.target.result;
if(!cursor) return done(new_data);
if (!cursor) return done(new_data);
new_data = fn(cursor.value || null);
cursor.delete([key[0], cursor.value.path]);
store.put(new_data);
@ -111,20 +111,21 @@ DataFromIndexedDB.prototype.update = function(type, key, fn, exact = true){
});
}).catch(() => Promise.resolve());
};
DataFromMemory.prototype.update = function(type, key, fn, exact = true){
if(type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
DataFromMemory.prototype.update = function(type, key, fn, exact = true) {
if (type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
const data = this.data[type];
if(data === undefined){
if (data === undefined) {
return Promise.resolve(null);
}
const k = key.join("_");
if(exact === true){
if(this.data[type][k] !== undefined) this.data[type][k] = fn(data[k]);
}else{
for(let _k in data){
if(_k.indexOf(k) === 0){
if (exact === true) {
if (this.data[type][k] !== undefined) this.data[type][k] = fn(data[k]);
} else {
for (const _k in data) {
if (_k.indexOf(k) === 0) {
this.data[type][_k] = fn(data[_k]);
}
}
@ -132,8 +133,8 @@ DataFromMemory.prototype.update = function(type, key, fn, exact = true){
return Promise.resolve();
};
DataFromIndexedDB.prototype.upsert = function(type, key, fn){
if(type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
DataFromIndexedDB.prototype.upsert = function(type, key, fn) {
if (type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
return this.db.then((db) => {
const tx = db.transaction(type, "readwrite");
@ -142,7 +143,7 @@ DataFromIndexedDB.prototype.upsert = function(type, key, fn){
return new Promise((done, error) => {
query.onsuccess = (e) => {
const new_data = fn(query.result || null);
if(!new_data) return done(query.result || null);
if (!new_data) return done(query.result || null);
const request = store.put(new_data);
request.onsuccess = () => done(new_data);
@ -152,11 +153,11 @@ DataFromIndexedDB.prototype.upsert = function(type, key, fn){
});
});
};
DataFromMemory.prototype.upsert = function(type, key, fn){
if(type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
DataFromMemory.prototype.upsert = function(type, key, fn) {
if (type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
const db = this.data[type] || null;
if(db === null){
if (db === null) {
this.data[type] = {};
}
const k = key.join("_");
@ -165,8 +166,8 @@ DataFromMemory.prototype.upsert = function(type, key, fn){
return Promise.resolve(new_data);
};
DataFromIndexedDB.prototype.add = function(type, key, data){
if(type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
DataFromIndexedDB.prototype.add = function(type, key, data) {
if (type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
return this.db.then((db) => {
return new Promise((done, error) => {
@ -178,42 +179,42 @@ DataFromIndexedDB.prototype.add = function(type, key, data){
});
}).catch(() => Promise.resolve());
};
DataFromMemory.prototype.add = function(type, key, data){
if(type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
DataFromMemory.prototype.add = function(type, key, data) {
if (type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
if(this.data[type] === undefined){
if (this.data[type] === undefined) {
this.data[type] = {};
}
this.data[type][key.join("_")] = data;
return Promise.resolve(data);
};
DataFromIndexedDB.prototype.remove = function(type, key, exact = true){
if(type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
DataFromIndexedDB.prototype.remove = function(type, key, exact = true) {
if (type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
return this.db.then((db) => {
const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type);
if(exact === true){
if (exact === true) {
const req = store.delete(key);
return new Promise((done, err) => {
req.onsuccess = () => done();
req.onerror = err;
});
}else{
} else {
const request = store.openCursor(IDBKeyRange.bound(
[key[0], key[1]],
[key[0], key[1]+'\uFFFF'],
true, true
[key[0], key[1]+"\uFFFF"],
true, true,
));
return new Promise((done, err) => {
request.onsuccess = function(event) {
const cursor = event.target.result;
if(cursor){
if (cursor) {
cursor.delete([key[0], cursor.value.path]);
cursor.continue();
}else{
} else {
done();
}
};
@ -221,27 +222,27 @@ DataFromIndexedDB.prototype.remove = function(type, key, exact = true){
}
}).catch(() => Promise.resolve());
};
DataFromMemory.prototype.remove = function(type, key, exact = true){
if(type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
DataFromMemory.prototype.remove = function(type, key, exact = true) {
if (type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
const data = this.data[type] || null;
if(data === null){
if (data === null) {
return Promise.resolve();
}
const k = key.join("_");
if(exact === true){
if (exact === true) {
delete data[k];
}
for(let _k in data){
if(_k.indexOf(k) === 0){
for (const _k in data) {
if (_k.indexOf(k) === 0) {
delete data[_k];
}
}
return Promise.resolve();
};
DataFromIndexedDB.prototype.fetchAll = function(fn, type = FILE_PATH, key){
if(type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
DataFromIndexedDB.prototype.fetchAll = function(fn, type = FILE_PATH, key) {
if (type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
return this.db.then((db) => {
const tx = db.transaction([type], "readonly");
@ -249,17 +250,17 @@ DataFromIndexedDB.prototype.fetchAll = function(fn, type = FILE_PATH, key){
const index = store.index("idx_path");
const request = index.openCursor(IDBKeyRange.bound(
[key[0], key[1]],
[key[0], key[1]+("z".repeat(5000))]
[key[0], key[1]+("z".repeat(5000))],
));
return new Promise((done, error) => {
request.onsuccess = function(event) {
const cursor = event.target.result;
if(!cursor){
if (!cursor) {
return done();
}
const ret = fn(cursor.value);
if(ret === false){
if (ret === false) {
return done();
}
cursor.continue();
@ -270,18 +271,18 @@ DataFromIndexedDB.prototype.fetchAll = function(fn, type = FILE_PATH, key){
});
}).catch(() => Promise.resolve());
};
DataFromMemory.prototype.fetchAll = function(fn, type = FILE_PATH, key){
if(type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
DataFromMemory.prototype.fetchAll = function(fn, type = FILE_PATH, key) {
if (type !== FILE_PATH && type !== FILE_CONTENT) return Promise.reject();
const data = this.data[type] || null;
if(data === null){
if (data === null) {
return Promise.resolve();
}
const k = key.join("_");
for(let _k in data){
if(_k.indexOf(k) === 0){
for (const _k in data) {
if (_k.indexOf(k) === 0) {
const ret = fn(data[_k]);
if(ret === false){
if (ret === false) {
return Promise.resolve();
}
}
@ -289,7 +290,7 @@ DataFromMemory.prototype.fetchAll = function(fn, type = FILE_PATH, key){
return Promise.resolve();
};
DataFromIndexedDB.prototype.destroy = function(){
DataFromIndexedDB.prototype.destroy = function() {
return new Promise((done, err) => {
this.db.then((db) => {
purgeAll(db, FILE_PATH);
@ -297,21 +298,22 @@ DataFromIndexedDB.prototype.destroy = function(){
});
done();
function purgeAll(db, type){
function purgeAll(db, type) {
const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type);
store.clear();
}
});
};
DataFromMemory.prototype.destroy = function(){
DataFromMemory.prototype.destroy = function() {
this.data = {};
return Promise.resolve();
};
export let cache = new DataFromMemory();
if("indexedDB" in window && window.indexedDB !== null){
var request = indexedDB.open("_indexedDB", 1);
if ("indexedDB" in window && window.indexedDB !== null) {
const request = indexedDB.open("_indexedDB", 1);
request.onsuccess = (e) => {
cache = new DataFromIndexedDB();
indexedDB.deleteDatabase("_indexedDB");

View file

@ -1,13 +1,16 @@
export function leftPad(str, length, pad = "0"){
if(typeof str !== 'string' || typeof pad !== 'string' || str.length >= length || !pad.length > 0) return str;
export function leftPad(str, length, pad = "0") {
if (typeof str !== "string" || typeof pad !== "string" || str.length >= length ||
!pad.length > 0) {
return str;
}
return leftPad(pad + str, length, pad);
}
export function copyToClipboard (str){
if(!str) return
let $input = document.createElement("input");
export function copyToClipboard(str) {
if (!str) return;
const $input = document.createElement("input");
$input.setAttribute("type", "text");
$input.setAttribute("style", "position: absolute; top:0;left:0;background:red")
$input.setAttribute("style", "position: absolute; top:0;left:0;background:red");
$input.setAttribute("display", "none");
document.body.appendChild($input);
$input.value = str;
@ -16,11 +19,11 @@ export function copyToClipboard (str){
$input.remove();
}
export function format(str = ""){
if(str.length === 0) return str;
export function format(str = "") {
if (str.length === 0) return str;
return str.split("_")
.map((word, index) => {
if(index != 0) return word;
if (index != 0) return word;
return word[0].toUpperCase() + word.substring(1);
})
.join(" ");

View file

@ -1,18 +1,17 @@
import aesjs from "aes-js";
import Aesjs from "aes-js";
export function encrypt(obj, key){
const textBytes = aesjs.utils.utf8.toBytes(JSON.stringify(obj));
const keyBytes = aesjs.padding.pkcs7.pad(aesjs.utils.utf8.toBytes(key));
return aesjs.utils.hex.fromBytes(
new aesjs.ModeOfOperation.ctr(keyBytes, new aesjs.Counter(5)).encrypt(textBytes)
export function encrypt(obj, key) {
const textBytes = Aesjs.utils.utf8.toBytes(JSON.stringify(obj));
const keyBytes = Aesjs.padding.pkcs7.pad(Aesjs.utils.utf8.toBytes(key));
return Aesjs.utils.hex.fromBytes(
new Aesjs.ModeOfOperation.ctr(keyBytes, new Aesjs.Counter(5)).encrypt(textBytes),
);
}
export function decrypt(text, key){
const textBytes = aesjs.utils.hex.toBytes(text);
const keyBytes = aesjs.padding.pkcs7.pad(aesjs.utils.utf8.toBytes(key));
return JSON.parse(aesjs.utils.utf8.fromBytes(
new aesjs.ModeOfOperation.ctr(keyBytes, new aesjs.Counter(5)).decrypt(textBytes)
export function decrypt(text, key) {
const textBytes = Aesjs.utils.hex.toBytes(text);
const keyBytes = Aesjs.padding.pkcs7.pad(Aesjs.utils.utf8.toBytes(key));
return JSON.parse(Aesjs.utils.utf8.fromBytes(
new Aesjs.ModeOfOperation.ctr(keyBytes, new Aesjs.Counter(5)).decrypt(textBytes),
));
}

View file

@ -1,19 +1,19 @@
function Event(){
function Event() {
this.fns = [];
}
Event.prototype.subscribe = function(name, fn){
if(!name || typeof fn !== 'function') return;
this.fns.push({key: name, fn: fn});
}
Event.prototype.unsubscribe = function(name){
Event.prototype.subscribe = function(name, fn) {
if (!name || typeof fn !== "function") return;
this.fns.push({ key: name, fn: fn });
};
Event.prototype.unsubscribe = function(name) {
this.fns = this.fns.filter((data) => {
return data.key === name ? false : true;
});
}
Event.prototype.emit = function(name, payload){
};
Event.prototype.emit = function(name, payload) {
this.fns.map((data) => {
if(data.key === name) data.fn(payload);
if (data.key === name) data.fn(payload);
});
}
};
export const event = new Event();

View file

@ -1,10 +1,10 @@
export const FormObjToJSON = function(o, fn, i = 0){
if(i === 0) delete o["constants"];
let obj = Object.assign({}, o);
export const FormObjToJSON = function(o, fn, i = 0) {
if (i === 0) delete o["constants"];
const obj = Object.assign({}, o);
Object.keys(obj).map((key) => {
let t = obj[key];
if("label" in t && "type" in t && "default" in t && "value" in t){
if(typeof fn === "function"){
const t = obj[key];
if ("label" in t && "type" in t && "default" in t && "value" in t) {
if (typeof fn === "function") {
fn(obj, key);
} else {
obj[key] = obj[key].value;
@ -13,44 +13,44 @@ export const FormObjToJSON = function(o, fn, i = 0){
obj[key] = FormObjToJSON(obj[key], fn, i+1);
}
});
return obj
return obj;
};
export function createFormBackend(backend_available, backend_data){
if(!backend_available) return {};
else if(!backend_data) return {};
else if(!backend_available[backend_data.type]) return {};
export function createFormBackend(backend_available, backend_data) {
if (!backend_available) return {};
else if (!backend_data) return {};
else if (!backend_available[backend_data.type]) return {};
let template = JSON.parse(JSON.stringify(backend_available[backend_data.type]));
for(let key in backend_data){
if(key in template){
for (const key in backend_data) {
if (key in template) {
template[key].value = backend_data[key];
template[key].enabled = true;
} else {
// create a form object if data isn't available in the template
let obj = {};
const obj = {};
obj[key] = {
label: key,
type: "text",
value: null,
default: backend_data[key]
default: backend_data[key],
};
template = Object.assign(obj, template);
}
if(key === "label"){
if (key === "label") {
template[key].placeholder = "Name as shown on the login screen.";
template[key].value = backend_data[key];
template[key].enabled = true;
} else if(key === "type"){
} else if (key === "type") {
template[key].enabled = true;
} else if(key === "advanced"){
} else if (key === "advanced") {
template[key].enabled = true;
}
}
let obj = {};
const obj = {};
obj[backend_data.type] = template;
return obj;
}
@ -59,35 +59,35 @@ export function createFormBackend(backend_available, backend_data){
* return a new list of autocompletion candidates considering the current input
*/
export function autocomplete(values, list) {
if(values.length === 0) return list;
let candidates_input = [],
candidates_output = [];
if (values.length === 0) return list;
let candidates_input = [];
let candidates_output = [];
for(let i=0; i<list.length; i++){
for (let i=0; i<list.length; i++) {
const last_value = values[values.length - 1];
if(list[i].indexOf(last_value) === 0){
let tmp = JSON.parse(JSON.stringify(values))
if (list[i].indexOf(last_value) === 0) {
const tmp = JSON.parse(JSON.stringify(values));
tmp[values.length - 1] = list[i];
if(list[i] === last_value){
if (list[i] === last_value) {
candidates_input = [tmp];
} else {
candidates_input.push(tmp)
candidates_input.push(tmp);
}
continue
continue;
}
if(values.indexOf(list[i]) === -1){
if (values.indexOf(list[i]) === -1) {
candidates_output.push(list[i]);
}
}
if(candidates_input.length === 0){
candidates_input = [values]
if (candidates_input.length === 0) {
candidates_input = [values];
}
candidates_output = [""].concat(candidates_output);
if(candidates_input.length > 1) {
if (candidates_input.length > 1) {
return candidates_input.map((candidate) => {
return candidate.join(", ");
});

View file

@ -1,18 +1,24 @@
export { URL_HOME, goToHome, URL_FILES, goToFiles, URL_VIEWER, goToViewer, URL_LOGIN, goToLogin, URL_LOGOUT, goToLogout, urlParams, URL_ADMIN, URL_SHARE } from './navigate';
export { opener } from './mimetype';
export { debounce, throttle } from './backpressure';
export { event } from './events';
export { cache } from './cache';
export { pathBuilder, basename, dirname, absoluteToRelative, filetype, currentShare, findParams, appendShareToUrl } from './path';
export { memory } from './memory';
export { prepare } from './navigate';
export { invalidate, http_get, http_post, http_delete, http_options } from './ajax';
export { prompt, alert, confirm } from './popup';
export { notify } from './notify';
export { gid, randomString } from './random';
export { leftPad, format, copyToClipboard } from './common';
export { getMimeType } from './mimetype';
export { settings_get, settings_put } from './settings';
export { FormObjToJSON, createFormBackend, autocomplete } from './form';
export { upload } from './upload';
export {
URL_HOME, goToHome, URL_FILES, goToFiles, URL_VIEWER, goToViewer, URL_LOGIN,
goToLogin, URL_LOGOUT, goToLogout, urlParams, URL_ADMIN, URL_SHARE,
} from "./navigate";
export { opener } from "./mimetype";
export { debounce, throttle } from "./backpressure";
export { event } from "./events";
export { cache } from "./cache";
export {
pathBuilder, basename, dirname, absoluteToRelative, filetype, currentShare,
findParams, appendShareToUrl,
} from "./path";
export { memory } from "./memory";
export { prepare } from "./navigate";
export { invalidate, http_get, http_post, http_delete, http_options } from "./ajax";
export { prompt, alert, confirm } from "./popup";
export { notify } from "./notify";
export { gid, randomString } from "./random";
export { leftPad, format, copyToClipboard } from "./common";
export { getMimeType } from "./mimetype";
export { settings_get, settings_put } from "./settings";
export { FormObjToJSON, createFormBackend, autocomplete } from "./form";
export { upload } from "./upload";
export function nop() {}

View file

@ -1,18 +1,18 @@
function Memory(){
let data = {};
function Memory() {
const data = {};
return {
get: function(key){
if(data[key] === undefined) return null;
get: function(key) {
if (data[key] === undefined) return null;
return data[key];
},
set: function(key, value){
set: function(key, value) {
data[key] = value;
},
all: function(){
all: function() {
return data;
}
}
},
};
}

View file

@ -1,38 +1,36 @@
import Path from 'path';
import Path from "path";
export function getMimeType(file){
let ext = Path.extname(file).replace(/^\./, "").toLowerCase();
let mime = CONFIG.mime[ext];
if(mime){
return mime;
}else{
export function getMimeType(file) {
const ext = Path.extname(file).replace(/^\./, "").toLowerCase();
if (!window.CONFIG.mime[ext]) {
return "text/plain";
}
return window.CONFIG.mime[ext];
}
export function opener(file){
let mime = getMimeType(file);
let openerFromPlugin = window.overrides["xdg-open"](mime);
if(openerFromPlugin !== null){
export function opener(file) {
const mime = getMimeType(file);
const openerFromPlugin = window.overrides["xdg-open"](mime);
if (openerFromPlugin !== null) {
return openerFromPlugin;
}else if(mime.split("/")[0] === "text"){
} else if (mime.split("/")[0] === "text") {
return ["editor", null];
}else if(mime === "application/pdf"){
} else if (mime === "application/pdf") {
return ["pdf", null];
}else if(mime.split("/")[0] === "image"){
} else if (mime.split("/")[0] === "image") {
return ["image", null];
}else if(["application/javascript", "application/xml", "application/json", "application/x-perl"].indexOf(mime) !== -1){
} else if (["application/javascript", "application/xml", "application/json",
"application/x-perl"].indexOf(mime) !== -1) {
return ["editor", null];
}else if(["audio/wave", "audio/mp3", "audio/flac", "audio/ogg"].indexOf(mime) !== -1){
} else if (["audio/wave", "audio/mp3", "audio/flac", "audio/ogg"].indexOf(mime) !== -1) {
return ["audio", null];
}else if(mime === "application/x-form"){
} else if (mime === "application/x-form") {
return ["form", null];
}else if(mime.split("/")[0] === "video" || mime === "application/ogg"){
} else if (mime.split("/")[0] === "video" || mime === "application/ogg") {
return ["video", null];
}else if(mime.split("/")[0] === "application"){
} else if (mime.split("/")[0] === "application") {
return ["download", null];
}else{
} else {
return ["editor", null];
}
}

View file

@ -1,30 +1,30 @@
export const URL_HOME = "/";
export function goToHome(history){
export function goToHome(history) {
history.push(URL_HOME);
return Promise.resolve("ok");
}
export const URL_FILES = "/files";
export function goToFiles(history, path, state){
export function goToFiles(history, path, state) {
history.push(URL_FILES+"?path="+encode_path(path), state);
return Promise.resolve("ok");
}
export const URL_VIEWER = "/view";
export function goToViewer(history, path, state){
export function goToViewer(history, path, state) {
history.push(URL_VIEWER+"?path="+encode_path(path), state);
return Promise.resolve("ok");
}
export const URL_LOGIN = "/login";
export function goToLogin(history){
export function goToLogin(history) {
history.push(URL_EDIT);
return Promise.resolve("ok");
}
export const URL_LOGOUT = "/logout";
export function goToLogout(history){
export function goToLogout(history) {
history.push(URL_LOGOUT);
return Promise.resolve("ok");
}
@ -33,31 +33,31 @@ export const URL_ADMIN = "/admin";
export const URL_SHARE = "/s";
function encode_path(path){
if(/%2F/.test(path) === false){
return encodeURIComponent(path).replace(/%2F/g, "/"); // replace slash to make url more friendly
}else{
return encodeURIComponent(path) // in case you got a %2F folder somewhere ...
function encode_path(path) {
if (/%2F/.test(path) === false) { // replace slash to make url more friendly
return encodeURIComponent(path).replace(/%2F/g, "/");
} else { // in case you got a %2F folder somewhere ...
return encodeURIComponent(path);
}
}
export function prepare(path){
export function prepare(path) {
return encodeURIComponent(decodeURIComponent(path.replace(/%/g, "%25")));
}
export function urlParams() {
let p = "";
if(window.location.hash){
if (window.location.hash) {
p += window.location.hash.replace(/^\#/, "");
}
if(window.location.search){
if(p !== "") p += "&";
if (window.location.search) {
if (p !== "") p += "&";
p += window.location.search.replace(/^\?/, "");
}
return p.split("&").reduce((mem, chunk) => {
const d = chunk.split("=");
if(d.length !== 2) return mem;
if (d.length !== 2) return mem;
mem[decodeURIComponent(d[0])] = decodeURIComponent(d[1]);
return mem;
}, {})
}, {});
}

View file

@ -1,16 +1,20 @@
const Message = function (){
const Message = function() {
let fn = null;
return {
send: function(text, type){
if(['info', 'success', 'error'].indexOf(type) === -1){ type = 'info'; }
if(!fn){ return window.setTimeout(() => this.send(text,type), 50); }
send: function(text, type) {
if (["info", "success", "error"].indexOf(type) === -1) {
type = "info";
}
if (!fn) {
return window.setTimeout(() => this.send(text, type), 50);
}
fn(text, type);
return Promise.resolve();
},
subscribe: function(_fn){
subscribe: function(_fn) {
fn = _fn;
}
},
};
};

View file

@ -1,34 +1,39 @@
import { gid, leftPad } from "./";
export function extractTodos(text){
export function extractTodos(text) {
const headlines = parse(text);
let todos = [];
for(let i=0; i < headlines.length; i++){
let todo = formatTodo(headlines[i]);
if(todo.status){
const todos = [];
for (let i=0; i < headlines.length; i++) {
const todo = formatTodo(headlines[i]);
if (todo.status) {
todos.push(todo);
}
}
return todos
.sort((a,b) => {
if(a.status === "NEXT" && b.status !== "NEXT") return -1;
else if(b.status === "NEXT" && a.status !== "NEXT") return +1;
else if(a.status === "TODO" && b.status !== "TODO") return -1;
else if(b.status === "TODO" && a.status !== "TODO") return +1;
else if(a.status === "DONE" && b.status !== "DONE" && b.todo_status === "done") return -1;
else if(b.status === "DONE" && a.status !== "DONE" && a.todo_status === "done") return +1;
else if(a.todo_status === "todo" && b.todo_status !== "todo") return -1;
else if(a.todo_status === "done" && b.todo_status !== "done") return +1;
else if(a.priority !== null && b.priority === null) return -1;
else if(a.priority === null && b.priority !== null) return +1;
else if(a.priority !== null && b.priority !== null && a.priority !== b.priority) return a.priority > b.priority? +1 : -1;
else if(a.is_overdue === true && b.is_overdue === false) return -1;
else if(a.is_overdue === false && b.is_overdue === true) return +1;
else if(a.status === b.status) return a.id < b.id ? -1 : +1;
.sort((a, b) => {
if (a.status === "NEXT" && b.status !== "NEXT") return -1;
else if (b.status === "NEXT" && a.status !== "NEXT") return +1;
else if (a.status === "TODO" && b.status !== "TODO") return -1;
else if (b.status === "TODO" && a.status !== "TODO") return +1;
else if (a.status === "DONE" && b.status !== "DONE" &&
b.todo_status === "done") return -1;
else if (b.status === "DONE" && a.status !== "DONE" &&
a.todo_status === "done") return +1;
else if (a.todo_status === "todo" && b.todo_status !== "todo") return -1;
else if (a.todo_status === "done" && b.todo_status !== "done") return +1;
else if (a.priority !== null && b.priority === null) return -1;
else if (a.priority === null && b.priority !== null) return +1;
else if (a.priority !== null && b.priority !== null &&
a.priority !== b.priority) return a.priority > b.priority? +1 : -1;
else if (a.is_overdue === true && b.is_overdue === false) return -1;
else if (a.is_overdue === false && b.is_overdue === true) return +1;
else if (a.status === b.status) return a.id < b.id ? -1 : +1;
});
function formatTodo(thing){
const todo_status = ["TODO", "NEXT", "DOING", "WAITING", "PENDING"].indexOf(thing.header.todo_keyword) !== -1 ? 'todo' : 'done';
function formatTodo(thing) {
const todo_status = ["TODO", "NEXT", "DOING", "WAITING", "PENDING"].indexOf(
thing.header.todo_keyword,
) !== -1 ? "todo" : "done";
return {
key: thing.header.todo_keyword,
id: thing.id,
@ -41,31 +46,33 @@ export function extractTodos(text){
scheduled: _find_scheduled(thing.timestamps),
deadline: _find_deadline(thing.timestamps),
tasks: thing.subtasks,
tags: thing.header.tags
tags: thing.header.tags,
};
}
}
export function extractEvents(text){
export function extractEvents(text) {
const headlines = parse(text);
let events = [];
for(let i=0; i < headlines.length; i++){
for (let i=0; i < headlines.length; i++) {
events = events.concat(
formatEvents(headlines[i])
formatEvents(headlines[i]),
);
}
return events.sort((a, b) => a.date - b.date);
function formatEvents(thing){
let events = [];
for(let i=0; i < thing.timestamps.length; i++){
let timestamp = thing.timestamps[i];
if(timestamp.active === false) continue;
const todo_status = function(keyword){
if(!keyword) return null;
return ["TODO", "NEXT", "DOING", "WAITING", "PENDING"].indexOf(keyword) !== -1 ? 'todo' : 'done';
function formatEvents(thing) {
const events = [];
for (let i=0; i < thing.timestamps.length; i++) {
const timestamp = thing.timestamps[i];
if (timestamp.active === false) continue;
const todo_status = function(keyword) {
if (!keyword) return null;
return ["TODO", "NEXT", "DOING", "WAITING", "PENDING"].indexOf(
keyword,
) !== -1 ? "todo" : "done";
}(thing.header.todo_keyword);
let event = {
const event = {
id: thing.id,
line: thing.header.line,
title: thing.header.title,
@ -76,9 +83,9 @@ export function extractEvents(text){
is_overdue: _is_overdue(todo_status, thing.timestamps),
priority: thing.header.priority,
tasks: [],
tags: thing.header.tags
tags: thing.header.tags,
};
if(event.todo_status === 'done') continue;
if (event.todo_status === "done") continue;
event.date = new Date(timestamp.timestamp);
const today = new Date();
@ -86,25 +93,25 @@ export function extractEvents(text){
today.setMinutes(59);
today.setSeconds(59);
today.setMilliseconds(999);
if(event.date < today){
if (event.date < today) {
event.date = today;
}
event.key = Intl.DateTimeFormat().format(event.date);
event.date = event.date.toISOString();
if(timestamp.repeat){
if(timestamp.repeat.interval === "m"){
if (timestamp.repeat) {
if (timestamp.repeat.interval === "m") {
events.push(event);
}else{
if(timestamp.repeat.interval === "y"){
} else {
if (timestamp.repeat.interval === "y") {
timestamp.repeat.n *= 365;
}else if(timestamp.repeat.interval === "w"){
} else if (timestamp.repeat.interval === "w") {
timestamp.repeat.n *= 7;
}
const n_days = timestamp.repeat.n;
let today = _normalise(new Date());
for(let j=0;j<30;j++){
if(((today - _normalise(new Date(timestamp.timestamp))) / 1000*60*60*24) % n_days === 0){
const today = _normalise(new Date());
for (let j=0; j<30; j++) {
if (((today - _normalise(new Date(timestamp.timestamp))) / 1000*60*60*24) % n_days === 0) {/* eslint-disable-line max-len */
event.date = today.getTime();
event.key = Intl.DateTimeFormat().format(today);
events.push(JSON.parse(JSON.stringify((event))));
@ -112,7 +119,7 @@ export function extractEvents(text){
today.setDate(today.getDate() + 1);
}
}
}else{
} else {
events.push(event);
}
}
@ -121,45 +128,49 @@ export function extractEvents(text){
}
export function parse(content){
let todos = [], todo = reset(0), data, text, tags = [];
export function parse(content) {
const todos = [];
let todo = reset(0);
let data;
let text;
let tags = [];
const lines = content.split("\n");
for(let i = 0; i<lines.length; i++){
for (let i = 0; i<lines.length; i++) {
text = lines[i];
if(data = parse_header(text, i)){
tags = tags.filter(e => e.level < data.level);
if (data = parse_header(text, i)) {
tags = tags.filter((e) => e.level < data.level);
tags.push({ level: data.level, tags: data.tags });
data.tags = tags.reduce((acc, el) => {
return acc.concat(el.tags);
}, []);
if(todo.header){
if (todo.header) {
todos.push(todo);
todo = reset(i);
}
todo.header = data;
}else if(data = parse_timestamp(text, i)){
} else if (data = parse_timestamp(text, i)) {
todo.timestamps = todo.timestamps.concat(data);
}else if(data = parse_subtask(text, i)){
} else if (data = parse_subtask(text, i)) {
todo.subtasks.push(data);
}
if(i === lines.length - 1 && todo.header){
if (i === lines.length - 1 && todo.header) {
todos.push(todo);
}
}
return todos;
function reset(i){
return {id: leftPad(i.toString(), 5) + gid(i), timestamps: [], subtasks: []};
function reset(i) {
return { id: leftPad(i.toString(), 5) + gid(i), timestamps: [], subtasks: [] };
}
}
function parse_header(text, line){
const match = text.match(/^(\*+)\s(?:([A-Z]{4,})\s){0,1}(?:\[\#([A-C])\]\s){0,1}(.*?)(?:\s+\:((?:[a-z]+\:){1,})){0,1}$/);
if(!match) return null;
function parse_header(text, line) {
const match = text.match(/^(\*+)\s(?:([A-Z]{4,})\s){0,1}(?:\[\#([A-C])\]\s){0,1}(.*?)(?:\s+\:((?:[a-z]+\:){1,})){0,1}$/); /* eslint-disable-line max-len */
if (!match) return null;
return {
line: line,
level: RegExp.$1.length,
@ -170,30 +181,30 @@ function parse_header(text, line){
.replace(/:/g, " ")
.trim()
.split(" ")
.filter((e) => e)
.filter((e) => e),
};
}
function parse_subtask(text, line){
function parse_subtask(text, line) {
const match = text.match(/(?:-|\d+[\.\)])\s\[([X\s-])\]\s(.*)/);
if(!match) return null;
if (!match) return null;
return {
line: line,
status: function(state){
if(state === "X") return "DONE";
else if(state === " ") return "TODO";
status: function(state) {
if (state === "X") return "DONE";
else if (state === " ") return "TODO";
return null;
}(match[1]),
title: match[2] || "Empty task",
}
};
}
function parse_timestamp(text, line, _memory){
const reg = /(?:([A-Z]+)\:\s){0,1}([<\[])(\d{4}-\d{2}-\d{2})[^>](?:[A-Z][a-z]{1,2})(?:\s([0-9]{2}\:[0-9]{2})){0,1}(?:\-([0-9]{2}\:[0-9]{2})){0,1}(?:\s(\+{1,2}[0-9]+[dwmy])){0,1}[\>\]](?:--[<\[](\d{4}-\d{2}-\d{2})\s[A-Z][a-z]{1,2}\s(\d{2}:\d{2}){0,1}[>\]]){0,1}/;
function parse_timestamp(text, line, _memory) {
const reg = /(?:([A-Z]+)\:\s){0,1}([<\[])(\d{4}-\d{2}-\d{2})[^>](?:[A-Z][a-z]{1,2})(?:\s([0-9]{2}\:[0-9]{2})){0,1}(?:\-([0-9]{2}\:[0-9]{2})){0,1}(?:\s(\+{1,2}[0-9]+[dwmy])){0,1}[\>\]](?:--[<\[](\d{4}-\d{2}-\d{2})\s[A-Z][a-z]{1,2}\s(\d{2}:\d{2}){0,1}[>\]]){0,1}/; /* eslint-disable-line max-len */
const match = text.match(reg);
if(!match) return _memory || null;
if (!match) return _memory || null;
// https://orgmode.org/manual/Timestamps.html
const timestamp = {
@ -201,53 +212,46 @@ function parse_timestamp(text, line, _memory){
keyword: match[1],
active: match[2] === "<" ? true : false,
timestamp: new Date(match[3] + (match[4] ? " "+match[4] : "")).toISOString(),
range: function(start_date, start_time = "", start_time_end, end_date = "", end_time = ""){
if(start_time_end && !end_date){
return new Date(start_date+" "+start_time_end) - new Date(start_date+" "+start_time);
range: function(start_date, start_time = "", start_time_end, end_date = "", end_time = "") {
if (start_time_end && !end_date) {
return new Date(start_date+" "+start_time_end) -
new Date(start_date+" "+start_time);
}
if(end_date){
if (end_date) {
return new Date(end_date+" "+end_time) - new Date(start_date+" "+start_time);
}
return null;
}(match[3], match[4], match[5], match[7], match[8]),
repeat: function(keyword){
if(!keyword) return null;
repeat: function(keyword) {
if (!keyword) return null;
return {
n: parseInt(keyword.replace(/^.*([0-9]+).*$/, "$1")),
interval: keyword.replace(/^.*([dwmy])$/, "$1")
interval: keyword.replace(/^.*([dwmy])$/, "$1"),
};
}(match[6])
}(match[6]),
};
if(!_memory) _memory = [];
if (!_memory) _memory = [];
_memory.push(timestamp);
return parse_timestamp(text.replace(reg, ""), line, _memory);
}
function _find_deadline(timestamps){
function _find_deadline(timestamps) {
return timestamps.filter((e) => e.keyword === "DEADLINE")[0] || null;
}
function _find_scheduled(timestamps){
function _find_scheduled(timestamps) {
return timestamps.filter((e) => e.keyword === "SCHEDULED")[0] || null;
}
function _is_overdue(status, timestamp){
if(status !== "todo") return false;
function _is_overdue(status, timestamp) {
if (status !== "todo") return false;
return timestamp.filter((timeObj) => {
if(_normalise(new Date()) < _normalise(new Date(timeObj.timestamp))) return false;
if(timeObj.keyword === "DEADLINE" || timeObj.keyword === "SCHEDULED") return true;
if (_normalise(new Date()) < _normalise(new Date(timeObj.timestamp))) return false;
if (timeObj.keyword === "DEADLINE" || timeObj.keyword === "SCHEDULED") return true;
return false;
}).length > 0 ? true : false;
}
function _date_label(date){
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return window.Intl.DateTimeFormat().format(date);
}
function _normalise(date){
function _normalise(date) {
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);

View file

@ -1,21 +1,21 @@
import Path from 'path';
import Path from "path";
export function pathBuilder(path, filename, type = 'file'){
let tmp = Path.resolve(path, filename)
if(type === 'file'){
export function pathBuilder(path, filename, type = "file") {
const tmp = Path.resolve(path, filename);
if (type === "file") {
return tmp;
}else{
return tmp + '/';
} else {
return tmp + "/";
}
}
export function basename(path){
export function basename(path) {
return Path.basename(path);
}
export function dirname(path){
export function dirname(path) {
const dir = Path.dirname(path);
if(dir === '/') return dir;
if (dir === "/") return dir;
return dir + "/";
}
@ -23,35 +23,35 @@ export function filetype(path) {
return path.slice(-1) === "/" ? "directory" : "file";
}
export function absoluteToRelative(from, to){
export function absoluteToRelative(from, to) {
// remove any trace of file that would be interpreted by the path lib as a folder
from = from.replace(/\/[^\/]+$/, "/");
let r = Path.relative(from, to);
if(r.substring(0,3) !== "../"){
r = "./"+r
if (r.substring(0, 3) !== "../") {
r = "./" + r;
}
if(/\/$/.test(to) === true && r !== "./"){
r += "/"
if (/\/$/.test(to) === true && r !== "./") {
r += "/";
}
return r;
}
export function currentShare(){
export function currentShare() {
return findParams("share");
}
export function findParams(p){
return new window.URL(location.href).searchParams.get(p) || ""
export function findParams(p) {
return new window.URL(location.href).searchParams.get(p) || "";
}
export function appendShareToUrl(link) {
let url = new window.URL(location.href);
let share = url.searchParams.get("share");
const share = url.searchParams.get("share");
if(share){
url = new window.URL(location.origin + link)
url.searchParams.set("share", share)
return url.pathname + url.search
if (share) {
url = new window.URL(location.origin + link);
url.searchParams.set("share", share);
return url.pathname + url.search;
}
return link;
}

View file

@ -1,46 +1,56 @@
const Alert = function (){
const Alert = function() {
let fn = null;
return {
now: function(Component, okCallback){
if(!fn){ return window.setTimeout(() => this.now(Component, okCallback), 50); }
now: function(Component, okCallback) {
if (!fn) {
return window.setTimeout(() => this.now(Component, okCallback), 50);
}
fn(Component, okCallback);
},
subscribe: function(_fn){
subscribe: function(_fn) {
fn = _fn;
}
},
};
};
export const alert = new Alert();
const Prompt = function (){
const Prompt = function() {
let fn = null;
return {
now: function(text, okCallback, cancelCallback, type){
if(!fn){ return window.setTimeout(() => this.now(text, okCallback, cancelCallback, type), 50); }
now: function(text, okCallback, cancelCallback, type) {
if (!fn) {
return window.setTimeout(() => {
this.now(text, okCallback, cancelCallback, type);
}, 50);
}
fn(text, okCallback, cancelCallback, type);
},
subscribe: function(_fn){
subscribe: function(_fn) {
fn = _fn;
}
},
};
};
export const prompt = new Prompt();
const Confirm = function (){
const Confirm = function() {
let fn = null;
return {
now: function(comp, okCallback, cancelCallback){
if(!fn){ return window.setTimeout(() => this.now(comp, okCallback, cancelCallback), 50); }
now: function(comp, okCallback, cancelCallback) {
if (!fn) {
return window.setTimeout(() => {
this.now(comp, okCallback, cancelCallback);
}, 50);
}
fn(comp, okCallback, cancelCallback);
},
subscribe: function(_fn){
subscribe: function(_fn) {
fn = _fn;
}
},
};
};
export const confirm = new Confirm();

View file

@ -1,20 +1,22 @@
export function gid(prefix){
let id = prefix !== undefined ? prefix : '';
export function gid(prefix) {
let id = prefix !== undefined ? prefix : "";
id += new Date().getTime().toString(32);
id += parseInt(Math.random()*Math.pow(10,16)).toString(32);
id += parseInt(Math.random()*Math.pow(10, 16)).toString(32);
return id;
}
const alphabet = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p",
"q","r","s","t","u","v","x","y","z","A","B","C","D","E","F","G",
"H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W",
"X","Y","Z","0","1","2","3","4","5","6","7","8","9"];
const alphabet = [
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p",
"q", "r", "s", "t", "u", "v", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G",
"H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W",
"X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
];
const alphabet_size = alphabet.length;
export function randomString(size = 16){
export function randomString(size = 16) {
let str = "";
for(let i=0; i<size; i++){
str += alphabet[Math.floor(Math.random()*alphabet_size)]
for (let i=0; i<size; i++) {
str += alphabet[Math.floor(Math.random()*alphabet_size)];
}
return str;
}

View file

@ -1,18 +1,18 @@
let settings = JSON.parse(window.localStorage.getItem("settings")) || {};
const settings = JSON.parse(window.localStorage.getItem("settings")) || {};
export function settings_get(key){
if(settings[key] === undefined){
export function settings_get(key) {
if (settings[key] === undefined) {
return null;
}
return settings[key];
}
export function settings_put(key, value){
export function settings_put(key, value) {
settings[key] = value;
save(settings);
}
function save(d){
function save(d) {
setInterval(() => {
window.localStorage.setItem("settings", JSON.stringify(d));
}, 500);

View file

@ -1,15 +1,17 @@
const Upload = function () {
const Upload = function() {
let fn = null;
return {
add: function (path, files) {
if (!fn) { return window.setTimeout(() => this.add(path, files), 50); }
add: function(path, files) {
if (!fn) {
return window.setTimeout(() => this.add(path, files), 50);
}
fn(path, files);
return Promise.resolve();
},
subscribe: function (_fn) {
subscribe: function(_fn) {
fn = _fn;
}
},
};
};

View file

@ -1,46 +1,49 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Router from './router';
import React from "react";
import ReactDOM from "react-dom";
import Router from "./router";
import { Config, Log } from "./model/";
import { http_get } from "./helpers/ajax";
import load from "little-loader";
import './assets/css/reset.scss';
import "./assets/css/reset.scss";
window.addEventListener("DOMContentLoaded", () => {
const className = 'ontouchstart' in window ? 'touch-yes' : 'touch-no';
const className = "ontouchstart" in window ? "touch-yes" : "touch-no";
document.body.classList.add(className);
const $loader = document.querySelector("#n-lder");
function render(){
ReactDOM.render(<Router/>, document.querySelector("div[role='main']"));
function render() {
ReactDOM.render(
<React.StrictMode><Router/></React.StrictMode>,
document.querySelector("div[role='main']"),
);
return Promise.resolve();
};
function waitFor(n){
}
function waitFor(n) {
return new Promise((done) => {
window.setTimeout(() => {
window.requestAnimationFrame(() => done());
}, n);
});
}
function removeLoaderWithAnimation(){
if(!$loader) return Promise.resolve();
function removeLoaderWithAnimation() {
if (!$loader) return Promise.resolve();
$loader.classList.add("done");
return new Promise((done) => {
window.setTimeout(() => requestAnimationFrame(done), 500);
});
}
function removeLoader(){
if($loader) $loader.remove();
function removeLoader() {
if ($loader) $loader.remove();
return Promise.resolve();
}
Promise.all([Config.refresh(), setup_xdg_open(), translation()]).then(() => {
const timeSinceBoot = new Date() - window.initTime;
if(window.CONFIG.name) document.title = window.CONFIG.name;
if(timeSinceBoot >= 1500){
if (window.CONFIG.name) document.title = window.CONFIG.name;
if (timeSinceBoot >= 1500) {
const timeoutToAvoidFlickering = timeSinceBoot > 2500 ? 0 : 500;
return waitFor(timeoutToAvoidFlickering)
.then(removeLoaderWithAnimation)
@ -49,20 +52,20 @@ window.addEventListener("DOMContentLoaded", () => {
return removeLoader().then(render);
}).catch((e) => {
const msg = navigator.onLine === false ? "OFFLINE" : "CAN'T LOAD FILESTASH";
Log.report(msg, location.href);
Log.report(msg + " - " + (e && e.message), location.href);
return removeLoaderWithAnimation().then(() => {
$error(msg);
});
});
});
window.onerror = function (msg, url, lineNo, colNo, error) {
window.onerror = function(msg, url, lineNo, colNo, error) {
Log.report(msg, url, lineNo, colNo, error);
$error(msg);
}
};
function $error(msg){
let $code = document.createElement("code");
function $error(msg) {
const $code = document.createElement("code");
$code.style.textAlign = "center";
$code.style.display = "block";
$code.style.margin = "50px 0";
@ -74,12 +77,14 @@ function $error(msg){
if ("serviceWorker" in navigator) {
window.addEventListener("load", function() {
if(navigator.userAgent.indexOf("Mozilla/") !== -1 && navigator.userAgent.indexOf("Firefox/") !== -1 && navigator.userAgent.indexOf("Gecko/") !== -1){
if (navigator.userAgent.indexOf("Mozilla/") !== -1 &&
navigator.userAgent.indexOf("Firefox/") !== -1 &&
navigator.userAgent.indexOf("Gecko/") !== -1) {
// Firefox was acting weird with service worker so we disabled it
// see: https://github.com/mickael-kerjean/filestash/issues/255
return
return;
}
navigator.serviceWorker.register("/sw_cache.js").catch(function(err){
navigator.serviceWorker.register("/sw_cache.js").catch(function(err) {
console.error("ServiceWorker registration failed:", err);
});
});
@ -87,25 +92,25 @@ if ("serviceWorker" in navigator) {
// server generated frontend overrides
window.overrides = {};
function setup_xdg_open(){
function setup_xdg_open() {
return new Promise((done, err) => {
load("/overrides/xdg-open.js", function(error) {
if(error) return err(error);
done()
if (error) return err(error);
done();
});
});
}
function translation(){
function translation() {
const userLanguage = navigator.language.split("-")[0];
const selectedLanguage = [
"az", "be", "bg", "ca", "cs", "da", "de", "el", "es", "et",
"eu", "fi", "fr", "gl", "hr", "hu", "id", "is", "it", "ja",
"ka", "ko", "lt", "lv", "mn", "nb", "nl", "pl", "pt", "ro",
"ru", "sk", "sl", "sr", "sv", "th", "tr", "uk", "vi", "zh"
"ru", "sk", "sl", "sr", "sv", "th", "tr", "uk", "vi", "zh",
].indexOf(userLanguage) === -1 ? "en" : userLanguage;
if(selectedLanguage === "en"){
if (selectedLanguage === "en") {
return Promise.resolve();
}
return http_get("/assets/locales/"+selectedLanguage+".json").then((d) => {

View file

@ -1,14 +1,19 @@
export function t(str = "", replacementString, requestedKey){
const calculatedKey = str.toUpperCase().replace(/ /g, "_").replace(/[^a-zA-Z0-9\-\_\*\{\}\?]/g, "").replace(/\_+$/, "");
const value = requestedKey === undefined ? window.LNG && window.LNG[calculatedKey] : window.LNG && window.LNG[requestedKey];
export function t(str = "", replacementString, requestedKey) {
const calculatedKey = str.toUpperCase()
.replace(/ /g, "_")
.replace(/[^a-zA-Z0-9\-\_\*\{\}\?]/g, "")
.replace(/\_+$/, "");
const value = requestedKey === undefined ?
window.LNG && window.LNG[calculatedKey] :
window.LNG && window.LNG[requestedKey];
return reformat(
value || str || "",
str
str,
).replace("{{VALUE}}", replacementString);
}
function reformat(translated, initial){
if(initial[0] && initial[0].toLowerCase() === initial[0]){
function reformat(translated, initial) {
if (initial[0] && initial[0].toLowerCase() === initial[0]) {
return translated || "";
}
return (translated[0] && translated[0].toUpperCase() + translated.substring(1)) || "";

View file

@ -1,10 +1,10 @@
import { http_post, http_get } from '../helpers';
import { http_post, http_get } from "../helpers";
export const Admin = {
login: function(password = ""){
return http_post("/admin/api/session", {password: password});
login: function(password = "") {
return http_post("/admin/api/session", { password: password });
},
isAdmin: function(){
isAdmin: function() {
return http_get("/admin/api/session").then((res) => res.result);
}
},
};

View file

@ -1,39 +1,40 @@
import { http_get, http_post, http_delete, debounce } from '../helpers/';
import { http_get, http_post, debounce } from "../helpers/";
class ConfigModel {
constructor(){}
constructor() {
}
all(){
all() {
return http_get("/admin/api/config").then((d) => d.result);
}
save(config, debounced = true, fn_ok, fn_err){
let url = "/admin/api/config";
save(config, debounced = true, fn_ok, fn_err) {
const url = "/admin/api/config";
if(debounced){
if(!this.debounced_post){
if (debounced) {
if (!this.debounced_post) {
this.debounced_post = debounce((url, config) => {
return http_post(url, config).then(this.refresh).then((a) => {
if(typeof fn_ok === "function") return fn_ok();
return Promise.resolve(a)
if (typeof fn_ok === "function") return fn_ok();
return Promise.resolve(a);
}).catch((err) => {
if(typeof fn_err === "function") return fn_err();
return Promise.reject(err)
if (typeof fn_err === "function") return fn_err();
return Promise.reject(err);
});
}, 1000);
}
return this.debounced_post(url, config)
return this.debounced_post(url, config);
}
return http_post(url, config).then(this.refresh).then((a) => {
if(typeof fn_ok === "function") return fn_ok();
return Promise.resolve(a)
if (typeof fn_ok === "function") return fn_ok();
return Promise.resolve(a);
}).catch((err) => {
if(typeof fn_err === "function") return fn_err();
return Promise.reject(err)
if (typeof fn_err === "function") return fn_err();
return Promise.reject(err);
});
}
refresh(){
refresh() {
return http_get("/api/config").then((config) => {
window.CONFIG = config.result;
});
@ -41,9 +42,10 @@ class ConfigModel {
}
class BackendModel {
constructor(){}
constructor() {
}
all(){
all() {
return http_get("/api/backend").then((r) => r.result);
}
}

View file

@ -1,18 +1,20 @@
"use strict";
import { http_get, http_post, http_options, prepare, basename, dirname, pathBuilder } from '../helpers/';
import { filetype, currentShare, appendShareToUrl } from '../helpers/';
import {
http_get, http_post, http_options, prepare, basename, dirname, pathBuilder,
currentShare, appendShareToUrl,
} from "../helpers/";
import { Observable } from 'rxjs/Observable';
import { cache } from '../helpers/';
import { Observable } from "rxjs/Observable";
import { cache } from "../helpers/";
class FileSystem{
constructor(){
class FileSystem {
constructor() {
this.obs = null;
this.current_path = null;
}
ls(path, show_hidden = false){
ls(path, show_hidden = false) {
this.current_path = path;
this.obs && this.obs.complete();
return Observable.create((obs) => {
@ -20,19 +22,20 @@ class FileSystem{
let keep_pulling_from_http = false;
this._ls_from_cache(path, true).then((cache) => {
const fetch_from_http = (_path) => {
return this._ls_from_http(_path, show_hidden).then(() => new Promise((done, err) => {
window.setTimeout(() => done(), 2000);
})).then(() => {
if(keep_pulling_from_http === false) return Promise.resolve();
return fetch_from_http(_path);
}).catch((err) => {
if(cache === null){
this.obs && this.obs.error({message: "Unknown Path"});
}
});
return this._ls_from_http(_path, show_hidden)
.then(() => new Promise((done, err) => {
window.setTimeout(() => done(), 2000);
})).then(() => {
if (keep_pulling_from_http === false) return Promise.resolve();
return fetch_from_http(_path);
}).catch((err) => {
if (cache === null) {
this.obs && this.obs.error({ message: "Unknown Path" });
}
});
};
fetch_from_http(path);
}).catch((err) => this.obs.error({message: err && err.message}));
}).catch((err) => this.obs.error({ message: err && err.message }));
return () => {
keep_pulling_from_http = false;
@ -40,33 +43,33 @@ class FileSystem{
});
}
_ls_from_http(path, show_hidden){
const url = appendShareToUrl("/api/files/ls?path="+prepare(path));
_ls_from_http(path, show_hidden) {
const url = appendShareToUrl("/api/files/ls?path=" + prepare(path));
return http_get(url).then((response) => {
response = fileMiddleware(response, path, show_hidden);
return cache.upsert(cache.FILE_PATH, [currentShare(), path], (_files) => {
let store = Object.assign({
const store = Object.assign({
share: currentShare(),
status: "ok",
path: path,
results: null,
access_count: 0,
metadata: null
metadata: null,
}, _files);
store.metadata = response.metadata;
store.results = response.results;
if(_files && _files.results){
if (_files && _files.results) {
store.access_count = _files.access_count;
// find out which entry we want to keep from the cache
let _files_virtual_to_keep = _files.results.filter((file) => {
return file.icon === 'loading';
const _files_virtual_to_keep = _files.results.filter((file) => {
return file.icon === "loading";
});
// update file results when something is going on
for(let i=0; i<_files_virtual_to_keep.length; i++){
for(let j=0; j<store.results.length; j++){
if(store.results[j].name === _files_virtual_to_keep[i].name){
for (let i=0; i<_files_virtual_to_keep.length; i++) {
for (let j=0; j<store.results.length; j++) {
if (store.results[j].name === _files_virtual_to_keep[i].name) {
store.results[j] = Object.assign({}, _files_virtual_to_keep[i]);
_files_virtual_to_keep.splice(i, 1);
i -= 1;
@ -81,41 +84,41 @@ class FileSystem{
store.last_access = new Date();
return store;
}).catch(() => Promise.resolve(response)).then((data) => {
if(this.current_path === path){
if (this.current_path === path) {
this.obs && this.obs.next(data);
}
return Promise.resolve(null);
});
}).catch((_err) => {
if(_err.code === "Unauthorized"){
location = "/login?next="+location.pathname;
if (_err.code === "Unauthorized") {
location = "/login?next=" + location.pathname;
}
this.obs.next(_err);
return Promise.reject(_err);
})
});
}
_ls_from_cache(path, _record_access = false){
_ls_from_cache(path, _record_access = false) {
return cache.get(cache.FILE_PATH, [currentShare(), path]).then((response) => {
if(!response || !response.results) return null;
if(this.current_path === path){
if (!response || !response.results) return null;
if (this.current_path === path) {
this.obs && this.obs.next({
status: 'ok',
status: "ok",
results: response.results,
metadata: response.metadata
metadata: response.metadata,
});
}
return response;
}).then((e) => {
requestAnimationFrame(() => {
if(_record_access === true){
if (_record_access === true) {
cache.upsert(cache.FILE_PATH, [currentShare(), path], (response) => {
if(!response || !response.results) return null;
if(this.current_path === path){
if (!response || !response.results) return null;
if (this.current_path === path) {
this.obs && this.obs.next({
status: 'ok',
status: "ok",
results: response.results,
metadata: response.metadata
metadata: response.metadata,
});
}
response.last_access = new Date();
@ -128,40 +131,43 @@ class FileSystem{
});
}
rm(path){
const url = appendShareToUrl('/api/files/rm?path='+prepare(path));
return this._replace(path, 'loading')
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
rm(path) {
const url = appendShareToUrl("/api/files/rm?path=" + prepare(path));
return this._replace(path, "loading")
.then((res) => this.current_path === dirname(path) ?
this._ls_from_cache(dirname(path)) : Promise.resolve(res))
.then(() => http_get(url))
.then((res) => {
return cache.remove(cache.FILE_CONTENT, [currentShare(), path])
.then(cache.remove(cache.FILE_CONTENT, [currentShare(), path], false))
.then(cache.remove(cache.FILE_PATH, [currentShare(), dirname(path)], false))
.then(this._remove(path, 'loading'))
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
.then(this._remove(path, "loading"))
.then((res) => this.current_path === dirname(path) ?
this._ls_from_cache(dirname(path)) : Promise.resolve(res));
})
.catch((err) => {
return this._replace(path, 'error', 'loading')
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
return this._replace(path, "error", "loading")
.then((res) => this.current_path === dirname(path) ?
this._ls_from_cache(dirname(path)) : Promise.resolve(res))
.then(() => Promise.reject(err));
});
}
cat(path){
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
return http_get(url, 'raw')
cat(path) {
const url = appendShareToUrl("/api/files/cat?path=" + prepare(path));
return http_get(url, "raw")
.then((res) => {
if(this.is_binary(res) === true){
return Promise.reject({code: 'BINARY_FILE'});
if (this.is_binary(res) === true) {
return Promise.reject({ code: "BINARY_FILE" });
}
return cache.upsert(cache.FILE_CONTENT, [currentShare(), path], (response) => {
let file = response? response : {
const file = response ? response : {
share: currentShare(),
path: path,
last_update: null,
last_access: null,
access_count: -1,
result: null
result: null,
};
file.result = res;
file.access_count += 1;
@ -171,63 +177,66 @@ class FileSystem{
});
}
zip(paths){
const url = appendShareToUrl('/api/files/zip?'+paths.map((p)=> "path=" +prepare(p)).join("&"))
zip(paths) {
const url = appendShareToUrl(
"/api/files/zip?" + paths.map((p) => "path=" + prepare(p)).join("&"),
);
window.open(url);
return Promise.resolve();
}
options(path){
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
options(path) {
const url = appendShareToUrl("/api/files/cat?path=" + prepare(path));
return http_options(url);
}
url(path){
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
url(path) {
const url = appendShareToUrl("/api/files/cat?path=" + prepare(path));
return Promise.resolve(url);
}
save(path, file){
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
return this._replace(path, 'loading')
.then(() => http_post(url, file, 'blob'))
save(path, file) {
const url = appendShareToUrl("/api/files/cat?path=" + prepare(path));
return this._replace(path, "loading")
.then(() => http_post(url, file, "blob"))
.then(() => {
return this._saveFileToCache(path, file)
.then(() => this._replace(path, null, 'loading'))
.then(() => this._replace(path, null, "loading"))
.then(() => this._refresh(path));
})
.catch((err) => {
return this._replace(path, 'error', 'loading')
return this._replace(path, "error", "loading")
.then(() => this._refresh(path))
.then(() => Promise.reject(err));
});
}
mkdir(path, step){
const url = appendShareToUrl('/api/files/mkdir?path='+prepare(path)),
origin_path = pathBuilder(this.current_path, basename(path), 'directoy'),
destination_path = path;
mkdir(path, step) {
const url = appendShareToUrl("/api/files/mkdir?path=" + prepare(path));
const origin_path = pathBuilder(this.current_path, basename(path), "directoy");
const destination_path = path;
const action_prepare = (part_of_a_batch_operation = false) => {
if(part_of_a_batch_operation === true){
return this._add(destination_path, 'loading')
if (part_of_a_batch_operation === true) {
return this._add(destination_path, "loading")
.then(() => this._refresh(destination_path));
}
return this._add(destination_path, 'loading')
.then(() => origin_path !== destination_path ? this._add(origin_path, 'loading') : Promise.resolve())
return this._add(destination_path, "loading")
.then(() => origin_path !== destination_path ?
this._add(origin_path, "loading") : Promise.resolve())
.then(() => this._refresh(origin_path, destination_path));
};
const action_execute = (part_of_a_batch_operation = false) => {
if(part_of_a_batch_operation === true){
if (part_of_a_batch_operation === true) {
return http_get(url)
.then(() => {
return this._replace(destination_path, null, 'loading')
return this._replace(destination_path, null, "loading")
.then(() => this._refresh(destination_path));
})
.catch((err) => {
this._replace(destination_path, 'error', 'loading')
this._replace(destination_path, "error", "loading")
.then(() => this._refresh(origin_path, destination_path));
return Promise.reject(err);
});
@ -235,60 +244,63 @@ class FileSystem{
return http_get(url)
.then(() => {
return this._replace(destination_path, null, 'loading')
.then(() => origin_path !== destination_path ? this._remove(origin_path, 'loading') : Promise.resolve())
return this._replace(destination_path, null, "loading")
.then(() => origin_path !== destination_path ?
this._remove(origin_path, "loading") : Promise.resolve())
.then(() => cache.add(cache.FILE_PATH, [currentShare(), destination_path], {
path: destination_path,
share: currentShare(),
results: [],
access_count: 0,
last_access: null,
last_update: new Date()
last_update: new Date(),
}))
.then(() => this._refresh(origin_path, destination_path));
})
.catch((err) => {
this._replace(origin_path, 'error', 'loading')
.then(() => origin_path !== destination_path ? this._remove(destination_path, 'loading') : Promise.resolve())
this._replace(origin_path, "error", "loading")
.then(() => origin_path !== destination_path ?
this._remove(destination_path, "loading") : Promise.resolve())
.then(() => this._refresh(origin_path, destination_path));
return Promise.reject(err);
});
};
if(step === 'prepare_only'){
if (step === "prepare_only") {
return action_prepare(true);
}else if(step === 'execute_only'){
} else if (step === "execute_only") {
return action_execute(true);
}else{
} else {
return action_prepare().then(action_execute);
}
}
touch(path, file, step, params){
const origin_path = pathBuilder(this.current_path, basename(path), 'file'),
destination_path = path;
touch(path, file, step, params) {
const origin_path = pathBuilder(this.current_path, basename(path), "file");
const destination_path = path;
const action_prepare = (part_of_a_batch_operation = false) => {
if(part_of_a_batch_operation === true){
return this._add(destination_path, 'loading')
if (part_of_a_batch_operation === true) {
return this._add(destination_path, "loading")
.then(() => this._refresh(destination_path));
}else{
return this._add(destination_path, 'loading')
.then(() => origin_path !== destination_path ? this._add(origin_path, 'loading') : Promise.resolve())
} else {
return this._add(destination_path, "loading")
.then(() => origin_path !== destination_path ?
this._add(origin_path, "loading") : Promise.resolve())
.then(() => this._refresh(origin_path, destination_path));
}
};
const action_execute = (part_of_a_batch_operation = false) => {
if(part_of_a_batch_operation === true){
if (part_of_a_batch_operation === true) {
return query()
.then(() => {
return this._replace(destination_path, null, 'loading')
return this._replace(destination_path, null, "loading")
.then(() => this._refresh(destination_path));
})
.catch((err) => {
this._replace(destination_path, null, 'error')
.then(() => this._replace(destination_path, null, 'loading'))
this._replace(destination_path, null, "error")
.then(() => this._replace(destination_path, null, "loading"))
.then(() => this._refresh(destination_path));
return Promise.reject(err);
});
@ -296,49 +308,51 @@ class FileSystem{
return query()
.then(() => {
return this._saveFileToCache(path, file)
.then(() => this._replace(destination_path, null, 'loading'))
.then(() => origin_path !== destination_path ? this._remove(origin_path, 'loading') : Promise.resolve())
.then(() => this._replace(destination_path, null, "loading"))
.then(() => origin_path !== destination_path ?
this._remove(origin_path, "loading") : Promise.resolve())
.then(() => this._refresh(origin_path, destination_path));
})
.catch((err) => {
this._replace(origin_path, 'error', 'loading')
.then(() => origin_path !== destination_path ? this._remove(destination_path, 'loading') : Promise.resolve())
this._replace(origin_path, "error", "loading")
.then(() => origin_path !== destination_path ?
this._remove(destination_path, "loading") : Promise.resolve())
.then(() => this._refresh(origin_path, destination_path));
return Promise.reject(err);
});
function query(){
if(file){
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
return http_post(url, file, 'blob', params);
}else{
const url = appendShareToUrl('/api/files/touch?path='+prepare(path));
function query() {
if (file) {
const url = appendShareToUrl("/api/files/cat?path=" + prepare(path));
return http_post(url, file, "blob", params);
} else {
const url = appendShareToUrl("/api/files/touch?path=" + prepare(path));
return http_get(url);
}
}
};
if(step === 'prepare_only'){
if (step === "prepare_only") {
return action_prepare(true);
}else if(step === 'execute_only'){
} else if (step === "execute_only") {
return action_execute(true);
}else{
} else {
return action_prepare().then(action_execute);
}
}
mv(from, to){
const url = appendShareToUrl('/api/files/mv?from='+prepare(from)+"&to="+prepare(to)),
origin_path = from,
destination_path = to;
mv(from, to) {
const url = appendShareToUrl("/api/files/mv?from=" + prepare(from) + "&to=" + prepare(to));
const origin_path = from;
const destination_path = to;
return this._replace(origin_path, 'loading')
.then(this._add(destination_path, 'loading'))
return this._replace(origin_path, "loading")
.then(this._add(destination_path, "loading"))
.then(() => this._refresh(origin_path, destination_path))
.then(() => http_get(url))
.then((res) => {
return this._remove(origin_path, 'loading')
.then(() => this._replace(destination_path, null, 'loading'))
return this._remove(origin_path, "loading")
.then(() => this._replace(destination_path, null, "loading"))
.then(() => this._refresh(origin_path, destination_path))
.then(() => {
cache.update(cache.FILE_PATH, [currentShare(), origin_path], (data) => {
@ -353,56 +367,62 @@ class FileSystem{
});
})
.catch((err) => {
this._replace(origin_path, 'error', 'loading')
.then(() => this._remove(destination_path, 'loading'))
.then(() => this._refresh(origin_path, destination_path))
this._replace(origin_path, "error", "loading")
.then(() => this._remove(destination_path, "loading"))
.then(() => this._refresh(origin_path, destination_path));
return Promise.reject(err);
});
}
search(keyword, path = "/", show_hidden){
const url = appendShareToUrl("/api/files/search?path="+prepare(path)+"&q="+encodeURIComponent(keyword))
search(keyword, path = "/", show_hidden) {
const url = appendShareToUrl(
"/api/files/search?path=" + prepare(path) +
"&q="+encodeURIComponent(keyword),
);
return http_get(url).then((response) => {
response = fileMiddleware(response, path, show_hidden);
return response.results;
});
}
frequents(){
let data = [];
frequents() {
const data = [];
return cache.fetchAll((value) => {
if(value.access_count >= 1 && value.path !== "/"){
if (value.access_count >= 1 && value.path !== "/") {
data.push(value);
}
}, cache.FILE_PATH, [currentShare(), "/"]).then(() => {
return Promise.resolve(
data
.sort((a,b) => a.access_count > b.access_count? -1 : 1)
.sort((a, b) => a.access_count > b.access_count? -1 : 1)
.map((a) => a.path)
.slice(0,6)
.slice(0, 6),
);
});
}
_saveFileToCache(path, file){
if(!file) return update_cache("");
_saveFileToCache(path, file) {
if (!file) return update_cache("");
return new Promise((done, err) => {
const reader = new FileReader();
reader.readAsText(file);
reader.onload = () => this.is_binary(reader.result) === false? update_cache(reader.result).then(done) : done();
reader.onload = () => this.is_binary(reader.result) === false ?
update_cache(reader.result).then(done) : done();
reader.onerror = (_err) => err(_err);
});
function update_cache(result){
function update_cache(result) {
return cache.upsert(cache.FILE_CONTENT, [currentShare(), path], (response) => {
if(!response) response = {
share: currentShare(),
path: path,
last_access: null,
last_update: null,
result: null,
access_count: 0
};
if (!response) {
response = {
share: currentShare(),
path: path,
last_access: null,
last_update: null,
result: null,
access_count: 0,
};
}
response.last_update = new Date();
response.result = result;
return response;
@ -410,51 +430,55 @@ class FileSystem{
}
}
_refresh(origin_path, destination_path){
if(this.current_path === dirname(origin_path) ||
this.current_path === dirname(destination_path)){
_refresh(origin_path, destination_path) {
if (this.current_path === dirname(origin_path) ||
this.current_path === dirname(destination_path)) {
return this._ls_from_cache(this.current_path);
}
return Promise.resolve();
}
_replace(path, icon, icon_previous){
return cache.update(cache.FILE_PATH, [currentShare(), dirname(path)], function(res){
_replace(path, icon, icon_previous) {
return cache.update(cache.FILE_PATH, [currentShare(), dirname(path)], function(res) {
res.results = res.results.map((file) => {
if(file.name === basename(path) && file.icon == icon_previous){
if(!icon){ delete file.icon; }
if(icon){ file.icon = icon; }
if (file.name === basename(path) && file.icon == icon_previous) {
if (!icon) {
delete file.icon;
}
if (icon) {
file.icon = icon;
}
}
return file;
});
return res;
});
}
_add(path, icon){
_add(path, icon) {
return cache.upsert(cache.FILE_PATH, [currentShare(), dirname(path)], (res) => {
if(!res || !res.results){
if (!res || !res.results) {
res = {
path: path,
share: currentShare(),
results: [],
access_count: 0,
last_access: null,
last_update: new Date()
last_update: new Date(),
};
}
let file = mutateFile({
const file = mutateFile({
path: path,
name: basename(path),
type: /\/$/.test(path) ? 'directory' : 'file'
type: /\/$/.test(path) ? "directory" : "file",
}, path);
if(icon) file.icon = icon;
if (icon) file.icon = icon;
res.results.push(file);
return res;
});
}
_remove(path, previous_icon){
return cache.update(cache.FILE_PATH, [currentShare(), dirname(path)], function(res){
if(!res) return null;
_remove(path, previous_icon) {
return cache.update(cache.FILE_PATH, [currentShare(), dirname(path)], function(res) {
if (!res) return null;
res.results = res.results.filter((file) => {
return file.name === basename(path) && file.icon == previous_icon ? false : true;
});
@ -463,7 +487,7 @@ class FileSystem{
}
is_binary(str){
is_binary(str) {
// Reference: https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character
return /\ufffd/.test(str);
}
@ -475,9 +499,9 @@ const createLink = (type, path) => {
};
const fileMiddleware = (response, path, show_hidden) => {
for(let i=0; i<response.results.length; i++){
let f = mutateFile(response.results[i], path);
if(show_hidden === false && f.path.indexOf("/.") !== -1){
for (let i=0; i<response.results.length; i++) {
const f = mutateFile(response.results[i], path);
if (show_hidden === false && f.path.indexOf("/.") !== -1) {
response.results.splice(i, 1);
i -= 1;
}
@ -486,7 +510,7 @@ const fileMiddleware = (response, path, show_hidden) => {
};
const mutateFile = (file, path) => {
if(file.path === undefined) {
if (file.path === undefined) {
file.path = pathBuilder(path, file.name, file.type);
}
file.link = createLink(file.type, file.path);

View file

@ -3,4 +3,4 @@ export { Session } from "./session";
export { Share } from "./share";
export { Config, Backend } from "./config";
export { Log } from "./log";
export { Admin } from "./admin"
export { Admin } from "./admin";

View file

@ -1,35 +1,36 @@
import { http_get, http_post } from '../helpers/';
import { http_get, http_post } from "../helpers/";
class LogManager{
constructor(){}
get(maxSize = -1){
let url = this.url();
if(maxSize > 0){
url += "?maxSize="+maxSize
}
return http_get(url, 'text');
class LogManager {
constructor() {
}
url(){
get(maxSize = -1) {
let url = this.url();
if (maxSize > 0) {
url += "?maxSize=" + maxSize;
}
return http_get(url, "text");
}
url() {
return "/admin/api/logs";
}
send(msg) {
let url = "/report?";
url += "message="+encodeURIComponent(msg)
url += "message=" + encodeURIComponent(msg);
return http_post(url).catch();
}
report(msg, link, lineNo, columnNo, error){
if(navigator.onLine === false) return Promise.resolve();
report(msg, link, lineNo, columnNo, error) {
if (navigator.onLine === false) return Promise.resolve();
let url = "/report?";
url += "url="+encodeURIComponent(location.href)+"&";
url += "msg="+encodeURIComponent(msg)+"&";
url += "from="+encodeURIComponent(link)+"&";
url += "from.lineNo="+lineNo+"&";
url += "from.columnNo="+columnNo;
if(error) url += "error="+encodeURIComponent(error.message)+"&";
if (error) url += "error="+encodeURIComponent(error.message)+"&";
return http_post(url).catch();
}
}

View file

@ -1,27 +1,27 @@
import { http_get, http_post, http_delete } from '../helpers/';
import { http_get, http_post, http_delete } from "../helpers/";
class SessionManager{
currentUser(){
let url = '/api/session'
class SessionManager {
currentUser() {
const url = "/api/session";
return http_get(url)
.then(data => data.result);
.then((data) => data.result);
}
oauth2(url){
oauth2(url) {
return http_get(url)
.then(data => data.result);
.then((data) => data.result);
}
authenticate(params){
let url = '/api/session';
authenticate(params) {
const url = "/api/session";
return http_post(url, params)
.then(data => data.result);
.then((data) => data.result);
}
logout(){
let url = '/api/session';
logout() {
const url = "/api/session";
return http_delete(url)
.then(data => data.result);
.then((data) => data.result);
}
}

View file

@ -1,45 +1,46 @@
import { http_get, http_post, http_delete, appendShareToUrl } from '../helpers/';
import { http_get, http_post, http_delete, appendShareToUrl } from "../helpers/";
class ShareModel {
constructor(){}
constructor() {
}
all(path = "/"){
all(path = "/") {
const url = `/api/share?path=${path}`;
return http_get(url).then((res) => res.results.map((el) => {
if(el.can_read === true && el.can_write === false && el.can_upload === false){
if (el.can_read === true && el.can_write === false && el.can_upload === false) {
el.role = "viewer";
}else if(el.can_read === false && el.can_write === false && el.can_upload === true){
} else if (el.can_read === false && el.can_write === false && el.can_upload === true) {
el.role = "uploader";
}else if(el.can_read === true && el.can_write === true && el.can_upload === true){
} else if (el.can_read === true && el.can_write === true && el.can_upload === true) {
el.role = "editor";
}else{
} else {
el.role = "n/a";
}
return el;
}));
}
get(id){
get(id) {
const url = `/api/share/${id}`;
return http_get(url).then((res) => res.result);
}
upsert(obj){
const url = appendShareToUrl(`/api/share/${obj.id}`)
upsert(obj) {
const url = appendShareToUrl(`/api/share/${obj.id}`);
const data = Object.assign({}, obj);
delete data.role;
return http_post(url, data);
}
remove(id){
remove(id) {
const url = appendShareToUrl(`/api/share/${id}`);
return http_delete(url);
}
proof(id, data){
proof(id, data) {
const url = `/api/share/${id}/proof`;
return http_post(url, data).then((res) => res.result);
}
}
export const Share = new ShareModel()
export const Share = new ShareModel();

View file

@ -1,42 +1,44 @@
import React, { useState, useEffect } from "react";
import Path from "path";
import { Route, Switch, Link, NavLink, useRouteMatch } from "react-router-dom";
import { Route, Switch, NavLink, useRouteMatch } from "react-router-dom";
import "./error.scss";
import "./adminpage.scss";
import { Icon, LoadingPage, CSSTransition } from "../components/";
import { Config, Admin } from "../model";
import { Admin } from "../model";
import { notify } from "../helpers/";
import { HomePage, BackendPage, SettingsPage, LogPage, SetupPage, LoginPage } from "./adminpage/";
import {
HomePage, BackendPage, SettingsPage, LogPage, SetupPage, LoginPage,
} from "./adminpage/";
import { t } from "../locales/";
function AdminOnly(WrappedComponent){
function AdminOnly(WrappedComponent) {
let initIsAdmin = null;
return function(props) {
const [isAdmin, setIsAdmin] = useState(initIsAdmin);
const refresh = () => {
Admin.isAdmin().then((t) => {
initIsAdmin = t
setIsAdmin(t)
initIsAdmin = t;
setIsAdmin(t);
}).catch((err) => {
notify.send("Error: " + (err && err.message) , "error");
notify.send("Error: " + (err && err.message), "error");
});
}
};
useEffect(() => {
refresh()
refresh();
const timeout = window.setInterval(refresh, 5 * 1000);
return () => clearInterval(timeout);
}, []);
if(isAdmin === true) {
if (isAdmin === true) {
return ( <WrappedComponent {...props} /> );
} else if(isAdmin === false) {
} else if (isAdmin === false) {
return ( <LoginPage reload={refresh} /> );
}
return ( <LoadingPage /> );
}
};
}
export default AdminOnly((props) => {
@ -46,11 +48,20 @@ export default AdminOnly((props) => {
<div className="component_page_admin">
<SideMenu url={match.url} isLoading={isSaving}/>
<div className="page_container scroll-y">
<CSSTransition key={location.pathname} transitionName="adminpage" transitionAppearTimeout={30000}>
<CSSTransition key={location.pathname} transitionName="adminpage"
transitionAppearTimeout={30000}>
<Switch>
<Route path={match.url + "/backend"} render={()=><BackendPage isSaving={setIsSaving}/>} />
<Route path={match.url + "/settings"} render={()=><SettingsPage isSaving={setIsSaving}/>} />
<Route path={match.url + "/logs"} render={() =><LogPage isSaving={setIsSaving}/>} />
<Route
path={match.url + "/backend"}
render={()=> <BackendPage isSaving={setIsSaving}/>}
/>
<Route
path={match.url + "/settings"}
render={()=> <SettingsPage isSaving={setIsSaving}/>}
/>
<Route
path={match.url + "/logs"}
render={() =><LogPage isSaving={setIsSaving}/>} />
<Route path={match.url + "/setup"} component={SetupPage} />
<Route path={match.url} component={HomePage} />
</Switch>
@ -63,15 +74,18 @@ export default AdminOnly((props) => {
function SideMenu(props) {
return (
<div className="component_menu_sidebar no-select">
{ props.isLoading ?
<div className="header">
<Icon name="arrow_left" style={{"opacity": 0}}/>
<Icon name="loading" />
</div> :
<NavLink to="/" className="header">
<Icon name="arrow_left" />
<img src="/assets/logo/android-chrome-512x512.png" />
</NavLink>
{
props.isLoading ? (
<div className="header">
<Icon name="arrow_left" style={{ "opacity": 0 }}/>
<Icon name="loading" />
</div>
) : (
<NavLink to="/" className="header">
<Icon name="arrow_left" />
<img src="/assets/logo/android-chrome-512x512.png" />
</NavLink>
)
}
<h2>{ t("Admin console") }</h2>
<ul>

View file

@ -1,82 +1,85 @@
import React from 'react';
import { Files } from '../model/';
import { notify, upload } from '../helpers/';
import Path from 'path';
import { Files } from "../model/";
import { notify, upload } from "../helpers/";
import Path from "path";
import { Observable } from "rxjs/Observable";
import { t } from '../locales/';
import { t } from "../locales/";
export const sort = function(files, type){
if(type === 'name'){
export const sort = function(files, type) {
if (type === "name") {
return sortByName(files);
}else if(type === 'date'){
} else if (type === "date") {
return sortByDate(files);
}else{
} else {
return sortByType(files);
}
function _moveLoadingDownward(fileA, fileB){
if(fileA.icon === 'loading' && fileB.icon !== 'loading'){return +1;}
else if(fileA.icon !== 'loading' && fileB.icon === 'loading'){return -1;}
return 0;
};
function _moveFolderUpward(fileA, fileB){
if(['directory', 'link'].indexOf(fileA.type) === -1 && ['directory', 'link'].indexOf(fileB.type) !== -1){
function _moveLoadingDownward(fileA, fileB) {
if (fileA.icon === "loading" && fileB.icon !== "loading") {
return +1;
}else if(['directory', 'link'].indexOf(fileA.type) !== -1 && ['directory', 'link'].indexOf(fileB.type) === -1){
} else if (fileA.icon !== "loading" && fileB.icon === "loading") {
return -1;
}
return 0;
};
function _moveHiddenFilesDownward(fileA, fileB){
if(fileA.name[0] === "." && fileB.name[0] !== ".") return +1;
else if(fileA.name[0] !== "." && fileB.name[0] === ".") return -1;
return 0
function _moveFolderUpward(fileA, fileB) {
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;
}
return 0;
};
function sortByType(files){
function _moveHiddenFilesDownward(fileA, fileB) {
if (fileA.name[0] === "." && fileB.name[0] !== ".") return +1;
else if (fileA.name[0] !== "." && fileB.name[0] === ".") return -1;
return 0;
};
function sortByType(files) {
return files.sort((fileA, fileB) => {
let tmp = _moveLoadingDownward(fileA, fileB);
if(tmp !== 0) return tmp;
if (tmp !== 0) return tmp;
tmp = _moveFolderUpward(fileA, fileB);
if(tmp !== 0) return tmp;
if (tmp !== 0) return tmp;
tmp = _moveHiddenFilesDownward(fileA, fileB);
if(tmp !== 0) return tmp;
if (tmp !== 0) return tmp;
const aExt = Path.extname(fileA.name.toLowerCase()),
bExt = Path.extname(fileB.name.toLowerCase());
const aExt = Path.extname(fileA.name.toLowerCase());
const bExt = Path.extname(fileB.name.toLowerCase());
if(fileA.name.toLowerCase() === fileB.name.toLowerCase()){
if (fileA.name.toLowerCase() === fileB.name.toLowerCase()) {
return fileA.name > fileB.name ? +1 : -1;
}else{
if(aExt !== bExt) return aExt > bExt ? +1 : -1;
} else {
if (aExt !== bExt) return aExt > bExt ? +1 : -1;
else return fileA.name.toLowerCase() > fileB.name.toLowerCase() ? +1 : -1;
}
});
}
function sortByName(files){
function sortByName(files) {
return files.sort((fileA, fileB) => {
let tmp = _moveLoadingDownward(fileA, fileB);
if(tmp !== 0) return tmp;
if (tmp !== 0) return tmp;
tmp = _moveFolderUpward(fileA, fileB);
if(tmp !== 0) return tmp;
if (tmp !== 0) return tmp;
tmp = _moveHiddenFilesDownward(fileA, fileB);
if(tmp !== 0) return tmp;
if (tmp !== 0) return tmp;
if(fileA.name.toLowerCase() === fileB.name.toLowerCase()){
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){
function sortByDate(files) {
return files.sort((fileA, fileB) => {
let tmp = _moveLoadingDownward(fileA, fileB);
if(tmp !== 0) return tmp;
const tmp = _moveLoadingDownward(fileA, fileB);
if (tmp !== 0) return tmp;
if(fileB.time === fileA.time){
if (fileB.time === fileA.time) {
return fileA.name > fileB.name ? +1 : -1;
}
return fileB.time - fileA.time;
@ -84,54 +87,69 @@ export const sort = function(files, type){
}
};
export const onCreate = function(path, type, file){
if(type === 'file'){
export const onCreate = function(path, type, file) {
if (type === "file") {
return Files.touch(path, file)
.then(() => {
notify.send(t('A file named "{{VALUE}}" was created', Path.basename(path)), 'success');
notify.send(
t("A file named '{{VALUE}}' was created", Path.basename(path)),
"success",
);
return Promise.resolve();
})
.catch((err) => {
notify.send(err, 'error');
notify.send(err, "error");
return Promise.reject(err);
});
}else if(type === 'directory'){
} else if (type === "directory") {
return Files.mkdir(path)
.then(() => notify.send(t('A folder named "{{VALUE}}" was created"', Path.basename(path)), 'success'))
.catch((err) => notify.send(err, 'error'));
}else{
return Promise.reject({message: t('internal error: can\'t create a {{VALUE}}', type.toString()), code: 'UNKNOWN_TYPE'});
.then(() => notify.send(
t("A folder named '{{VALUE}}' was created", Path.basename(path)),
"success",
))
.catch((err) => notify.send(err, "error"));
} else {
return Promise.reject({
message: t("internal error: can't create a {{VALUE}}", type.toString()),
code: "UNKNOWN_TYPE",
});
}
};
export const onRename = function(from, to, type){
export const onRename = function(from, to, type) {
return Files.mv(from, to, type)
.then(() => notify.send(t('The file "{{VALUE}}" was renamed', Path.basename(from)), 'success'))
.catch((err) => notify.send(err, 'error'));
.then(() => notify.send(
t("The file '{{VALUE}}' was renamed", Path.basename(from)),
"success",
))
.catch((err) => notify.send(err, "error"));
};
export const onDelete = function(path, type){
export const onDelete = function(path, type) {
return Files.rm(path, type)
.then(() => notify.send(t('The file {{VALUE}} was deleted"', Path.basename(path)), 'success'))
.catch((err) => notify.send(err, 'error'));
.then(() => notify.send(
t("The file {{VALUE}} was deleted", Path.basename(path)),
"success",
))
.catch((err) => notify.send(err, "error"));
};
export const onMultiDelete = function(arrOfPath){
export const onMultiDelete = function(arrOfPath) {
return Promise.all(arrOfPath.map((p) => Files.rm(p)))
.then(() => notify.send(t('All done!'), 'success'))
.catch((err) => notify.send(err, 'error'));
}
.then(() => notify.send(t("All done!"), "success"))
.catch((err) => notify.send(err, "error"));
};
export const onMultiDownload = function(arr){
export const onMultiDownload = function(arr) {
return Files.zip(arr)
.catch((err) => notify.send(err, 'error'));
}
.catch((err) => notify.send(err, "error"));
};
export const onMultiRename = function(arrOfPath){
export const onMultiRename = function(arrOfPath) {
return Promise.all(arrOfPath.map((p) => Files.mv(p[0], p[1])))
.then(() => notify.send(t('All done!'), 'success'))
.catch((err) => notify.send(err, 'error'));
}
.then(() => notify.send(t("All done!"), "success"))
.catch((err) => notify.send(err, "error"));
};
/*
* The upload method has a few strategies:
@ -139,29 +157,31 @@ export const onMultiRename = function(arrOfPath){
* 2. user is coming from drag and drop + browser DOES NOT provides support to read entire folders
* 3. user is coming from a upload form button as he doesn't have drag and drop with files
*/
export const onUpload = function(path, e){
export const onUpload = function(path, e) {
let extractFiles = null;
if(e.dataTransfer === undefined){ // case 3
if (e.dataTransfer === undefined) { // case 3
extractFiles = extract_upload_crappy_hack_but_official_way(e.target);
} else {
if(e.dataTransfer.types && e.dataTransfer.types.length >= 0){
if(e.dataTransfer.types[0] === "text/uri-list"){
return
if (e.dataTransfer.types && e.dataTransfer.types.length >= 0) {
if (e.dataTransfer.types[0] === "text/uri-list") {
return;
}
}
extractFiles = extract_upload_directory_the_way_that_works_but_non_official(e.dataTransfer.items || [], []) // case 1
extractFiles = extract_upload_directory_the_way_that_works_but_non_official(
e.dataTransfer.items || [], [],
) // case 1
.then((files) => {
if(files.length === 0){ // case 2
if (files.length === 0) { // case 2
return extract_upload_crappy_hack_but_official_way(e.dataTransfer);
}
return Promise.resolve(files);
})
});
}
extractFiles.then((files) => upload.add(path, files));
// adapted from: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
function _rand_id(){
function _rand_id() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
@ -170,100 +190,102 @@ export const onUpload = function(path, e){
return s4() + s4() + s4() + s4();
}
function extract_upload_directory_the_way_that_works_but_non_official(items, files = []){
function extract_upload_directory_the_way_that_works_but_non_official(items, files = []) {
const traverseDirectory = (item, _files, parent_id) => {
let file = {
path: item.fullPath
const file = {
path: item.fullPath,
};
if(item.isFile){
if (item.isFile) {
return new Promise((done, err) => {
file.type = "file";
item.file((_file, _err) => {
if(!_err){
if (!_err) {
file.file = _file;
if(parent_id) file._prior = parent_id;
if (parent_id) file._prior = parent_id;
_files.push(file);
}
done(_files);
});
});
}else if(item.isDirectory){
} else if (item.isDirectory) {
file.type = "directory";
file.path += "/";
file._id = _rand_id();
if(parent_id) file._prior = parent_id;
if (parent_id) file._prior = parent_id;
_files.push(file);
let reader = item.createReader();
const filereader = function(r){
const reader = item.createReader();
const filereader = function(r) {
return new Promise((done) => {
r.readEntries(function(entries){
r.readEntries(function(entries) {
Promise.all(entries.map((entry) => {
return traverseDirectory(entry, _files, file._id);
})).then((e) => {
if(entries.length > 0){
if (entries.length > 0) {
return filereader(r).then(done);
}
return done(e);
})
});
});
});
}
};
return filereader(reader).then(() => {
return Promise.resolve(_files)
return Promise.resolve(_files);
});
}else{
} else {
return Promise.resolve();
}
};
return Promise.all(
Array.prototype.slice.call(items).map((item) => {
if(typeof item.webkitGetAsEntry === 'function'){
if (typeof item.webkitGetAsEntry === "function") {
return traverseDirectory(item.webkitGetAsEntry(), files.slice(0));
}
}).filter((e) => e)
}).filter((e) => e),
).then((res) => Promise.resolve([].concat.apply([], res)));
}
function extract_upload_crappy_hack_but_official_way(data){
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');
function detectType(_f) {
// the 4096 is an heuristic I've observed and taken from:
// https://stackoverflow.com/questions/25016442
// 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();
const reader = new window.FileReader();
reader.onload = function() {
done('file');
done("file");
};
reader.onerror = function() {
done('directory');
done("directory");
};
reader.readAsText(_f);
});
}
function transform(_type){
let file = {
function transform(_type) {
const file = {
type: _type,
path: _file.name
path: _file.name,
};
if(file.type === 'file'){
if (file.type === "file") {
file.file = _file;
}else{
} else {
file.path += "/";
}
return Promise.resolve(file);
}
})
}),
);
}
};

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect } from "react";
import { Redirect } from "react-router";
import { Session } from "../model/";
@ -9,15 +9,15 @@ export function HomePage() {
useEffect(() => {
Session.currentUser().then((res) => {
if(res && res.is_authenticated === true){
if (res && res.is_authenticated === true) {
setRedirection(res.home ? "/files" + res.home : "/files");
}else{
} else {
setRedirection("/login");
}
}).catch((err) => setRedirection("/login"));
}, []);
if(!redirection) {
if (!redirection) {
return ( <div> <Loader /> </div> );
}
return ( <Redirect to={redirection} /> );

View file

@ -1,7 +1,7 @@
export { HomePage } from './homepage';
export { SharePage } from './sharepage';
export { ConnectPage } from './connectpage';
export { LogoutPage } from './logout';
export { NotFoundPage } from './notfoundpage';
export { FilesPage } from './filespage';
export { ViewerPage } from './viewerpage';
export { HomePage } from "./homepage";
export { SharePage } from "./sharepage";
export { ConnectPage } from "./connectpage";
export { LogoutPage } from "./logout";
export { NotFoundPage } from "./notfoundpage";
export { FilesPage } from "./filespage";
export { ViewerPage } from "./viewerpage";

View file

@ -1,17 +1,15 @@
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { Button } from "../components/";
import "./error.scss";
export function NotFoundPage({ history }) {
const [t, setTimer] = useState(10);
useEffect(() => {
const timeout = window.setTimeout(() => {
if (t == 0) {
history.push("/");
return
return;
}
setTimer(t - 1);
}, 1000);
@ -21,11 +19,10 @@ export function NotFoundPage({ history }) {
};
}, [t]);
return (
<div className="component_page_notfound error-page">
<h1>Oops!</h1>
<h2>We can"t seem to find the page you"re looking for.</h2>
<h2>We can&apos;t seem to find the page you&apos;re looking for.</h2>
<p>
You will be redirected to the <Link to="/">homepage</Link> in {t} seconds
</p>

View file

@ -1,8 +1,17 @@
import React, { Suspense } from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { NotFoundPage, ConnectPage, HomePage, SharePage, LogoutPage, FilesPage, ViewerPage } from "./pages/";
import { URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT, URL_ADMIN, URL_SHARE } from "./helpers/";
import { ModalPrompt, ModalAlert, ModalConfirm, Notification, UploadQueue, LoadingPage } from "./components/";
import {
NotFoundPage, ConnectPage, HomePage, SharePage, LogoutPage,
FilesPage, ViewerPage,
} from "./pages/";
import {
URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT,
URL_ADMIN, URL_SHARE,
} from "./helpers/";
import {
ModalPrompt, ModalAlert, ModalConfirm, Notification, UploadQueue,
LoadingPage,
} from "./components/";
const LazyAdminPage = React.lazy(() => import(/* webpackChunkName: "admin" */"./pages/adminpage"));
@ -12,25 +21,23 @@ const AdminPage = () => (
</Suspense>
);
export default class AppRouter extends React.Component {
render() {
return (
<div style={{height: "100%"}}>
<BrowserRouter>
export default function AppRouter() {
return (
<div style={{ height: "100%" }}>
<BrowserRouter>
<Switch>
<Route exact path={URL_HOME} component={HomePage} />
<Route path={`${URL_SHARE}/:id*`} component={SharePage} />
<Route path={URL_LOGIN} component={ConnectPage} />
<Route path={`${URL_FILES}/:path*`} component={FilesPage} />
<Route path={`${URL_VIEWER}/:path*`} component={ViewerPage} />
<Route path={URL_LOGOUT} component={LogoutPage} />
<Route path={URL_ADMIN} component={AdminPage} />
<Route component={NotFoundPage} />
<Route exact path={URL_HOME} component={HomePage} />
<Route path={`${URL_SHARE}/:id*`} component={SharePage} />
<Route path={URL_LOGIN} component={ConnectPage} />
<Route path={`${URL_FILES}/:path*`} component={FilesPage} />
<Route path={`${URL_VIEWER}/:path*`} component={ViewerPage} />
<Route path={URL_LOGOUT} component={LogoutPage} />
<Route path={URL_ADMIN} component={AdminPage} />
<Route component={NotFoundPage} />
</Switch>
</BrowserRouter>
<ModalPrompt /> <ModalAlert /> <ModalConfirm />
<Notification /> <UploadQueue/>
</div>
);
}
</BrowserRouter>
<ModalPrompt /> <ModalAlert /> <ModalConfirm />
<Notification /> <UploadQueue/>
</div>
);
}

View file

@ -4,14 +4,14 @@ const CACHE_NAME = "v0.3";
* Control everything going through the wire, applying different
* strategy for caching, fetching resources
*/
self.addEventListener("fetch", function(event){
if(is_a_ressource(event.request)){
self.addEventListener("fetch", function(event) {
if (is_a_ressource(event.request)) {
return event.respondWith(cacheFirstStrategy(event));
}else if(is_an_api_call(event.request)){
} else if (is_an_api_call(event.request)) {
return event;
}else if(is_an_index(event.request)){
} else if (is_an_index(event.request)) {
return event.respondWith(cacheFirstStrategy(event));
}else{
} else {
return event;
}
});
@ -20,11 +20,11 @@ self.addEventListener("fetch", function(event){
* When a new service worker is coming in, we need to do a bit of
* cleanup to get rid of the rotten cache
*/
self.addEventListener("activate", function(event){
self.addEventListener("activate", function(event) {
vacuum(event);
});
self.addEventListener("error", function(err){
self.addEventListener("error", function(err) {
console.error(err);
});
@ -32,55 +32,56 @@ self.addEventListener("error", function(err){
* When a newly installed service worker is coming in, we want to use it
* straight away (make it active). By default it would be in a "waiting state"
*/
self.addEventListener("install", function(){
self.addEventListener("install", function() {
caches.open(CACHE_NAME).then(function(cache) {
return cache.addAll([
"/",
"/api/config"
"/api/config",
]);
});
if (self.skipWaiting) { self.skipWaiting(); }
if (self.skipWaiting) {
self.skipWaiting();
}
});
function is_a_ressource(request){
function is_a_ressource(request) {
const p = _pathname(request);
if(["assets", "manifest.json", "favicon.ico"].indexOf(p[0]) !== -1){
if (["assets", "manifest.json", "favicon.ico"].indexOf(p[0]) !== -1) {
return true;
} else if(p[0] === "api" && (p[1] === "config")){
} else if (p[0] === "api" && (p[1] === "config")) {
return true;
}
return false;
}
function is_an_api_call(request){
function is_an_api_call(request) {
return _pathname(request)[0] === "api" ? true : false;
}
function is_an_index(request){
return ["files", "view", "login", "logout", ""].indexOf(_pathname(request)[0]) >= 0? true : false;
function is_an_index(request) {
return ["files", "view", "login", "logout", ""]
.indexOf(_pathname(request)[0]) >= 0? true : false;
}
////////////////////////////////////////
// //////////////////////////////////////
// HELPERS
////////////////////////////////////////
// //////////////////////////////////////
function vacuum(event){
function vacuum(event) {
return event.waitUntil(
caches.keys().then(function(cachesName){
return Promise.all(cachesName.map(function(cacheName){
if(cacheName !== CACHE_NAME){
caches.keys().then(function(cachesName) {
return Promise.all(cachesName.map(function(cacheName) {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
}));
})
}),
);
}
function _pathname(request){
//eslint-disable-next-line no-useless-escape
function _pathname(request) {
return request.url.replace(/^http[s]?:\/\/[^\/]*\//, "").split("/");
}
@ -89,34 +90,36 @@ function _pathname(request){
* 1. use whatever is in the cache
* 2. perform the network call to update the cache
*/
function cacheFirstStrategy(event){
return caches.open(CACHE_NAME).then(function(cache){
return cache.match(event.request).then(function(response){
if(!response){
function cacheFirstStrategy(event) {
return caches.open(CACHE_NAME).then(function(cache) {
return cache.match(event.request).then(function(response) {
if (!response) {
return fetchAndCache(event);
}
fetchAndCache(event).catch(nil);
return response;
});
});
function fetchAndCache(event){
function fetchAndCache(event) {
// A request is a stream and can only be consumed once. Since we are consuming this
// once by cache and once by the browser for fetch, we need to clone the response as
// seen on https://developers.google.com/web/fundamentals/getting-started/primers/service-workers
// seen on:
// https://developers.google.com/web/fundamentals/getting-started/primers/service-workers
return fetch(event.request)
.then(function(response){
if(!response || response.status !== 200){ return response; }
.then(function(response) {
if (!response || response.status !== 200) {
return response;
}
// A response is a stream and can only because we want the browser to consume the
// response as well as the cache consuming the response, we need to clone it
const responseClone = response.clone();
caches.open(CACHE_NAME).then(function(cache){
caches.open(CACHE_NAME).then(function(cache) {
cache.put(event.request, responseClone);
});
return response;
});
}
function nil(){}
function nil() {}
}

View file

@ -38,6 +38,7 @@
"wavesurfer.js": "^1.4.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.16.5",
"babel-core": "^6.13.2",
"babel-loader": "^6.2.10",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
@ -50,8 +51,9 @@
"compression-webpack-plugin": "^1.1.11",
"copy-webpack-plugin": "^4.5.2",
"css-loader": "^0.28.10",
"eslint": "^7.16.0",
"eslint-plugin-react": "^7.21.5",
"eslint": "^8.5.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-react": "^7.27.1",
"html-loader": "^0.4.5",
"html-webpack-plugin": "^3.2.0",
"node-sass": "^4.10.0",

View file

@ -1,69 +1,69 @@
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
// const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const CopyWebpackPlugin = require("copy-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
let config = {
const config = {
entry: {
app: path.join(__dirname, 'client', 'index.js')
app: path.join(__dirname, "client", "index.js"),
},
output: {
path: path.join(__dirname, 'dist', 'data', 'public'),
publicPath: '/',
filename: 'assets/js/[name]_[chunkhash].js',
chunkFilename: "assets/js/chunk_[name]_[id]_[chunkhash].js"
path: path.join(__dirname, "dist", "data", "public"),
publicPath: "/",
filename: "assets/js/[name]_[chunkhash].js",
chunkFilename: "assets/js/chunk_[name]_[id]_[chunkhash].js",
},
module: {
rules: [
{
test: path.join(__dirname, 'client'),
use: ['babel-loader'],
exclude: /node_modules/
test: path.join(__dirname, "client"),
use: ["babel-loader"],
exclude: /node_modules/,
},
{
test: /\.html$/,
loader: 'html-loader'
loader: "html-loader",
},
{
test: /\.woff2$/,
loader: 'woff-loader'
loader: "woff-loader",
},
{
test: /\.scss$/,
loaders: ['style-loader', 'css-loader', 'sass-loader']
loaders: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.css$/,
loaders: ['style-loader', 'css-loader']
loaders: ["style-loader", "css-loader"],
},
{
test: /\.(pdf|jpg|png|gif|svg|ico|woff|woff2|eot|ttf)$/,
loader: "url-loader"
loader: "url-loader",
},
{
test: /[a-z]+\.worker\.js$/,
loader: "worker-loader",
options: { name: 'assets/js/[name]_[hash].js' }
}
]
options: { name: "assets/js/[name]_[hash].js" },
},
],
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
}),
new webpack.optimize.OccurrenceOrderPlugin(),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'client', 'index.html'),
template: path.join(__dirname, "client", "index.html"),
inject: true,
minify: {
collapseWhitespace: true,
removeComments: true,
minifyJS: true,
minifyCSS: true,
}
},
}),
new CopyWebpackPlugin([
{ from: "locales/*.json", to: "assets/" },
@ -72,16 +72,16 @@ let config = {
{ from: "assets/logo/*" },
{ from: "assets/icons/*" },
{ from: "assets/fonts/*" },
], { context: path.join(__dirname, 'client') }),
], { context: path.join(__dirname, "client") }),
new CopyWebpackPlugin([
{ from: "node_modules/pdfjs-dist/", to: "assets/vendor/pdfjs/2.6.347/"}
{ from: "node_modules/pdfjs-dist/", to: "assets/vendor/pdfjs/2.6.347/" },
]),
//new BundleAnalyzerPlugin()
]
// new BundleAnalyzerPlugin()
],
};
if (process.env.NODE_ENV === 'production') {
if (process.env.NODE_ENV === "production") {
config.plugins.push(new UglifyJSPlugin({
sourceMap: false,
extractComments: true,
@ -91,17 +91,17 @@ if (process.env.NODE_ENV === 'production') {
algorithm: "gzip",
test: /\.js$|\.json$|\.html$|\.svg|\.ico$/,
threshold: 0,
minRatio: 0.8
minRatio: 0.8,
}));
config.plugins.push(new CompressionPlugin({
asset: "[path].br[query]",
algorithm: "brotliCompress",
test: /\.js$|\.json$|\.html$|\.svg|\.ico$/,
threshold: 0,
minRatio: 0.8
minRatio: 0.8,
}));
} else {
config.devtool = '#inline-source-map';
config.devtool = "#inline-source-map";
}
module.exports = config;