mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-27 10:42:36 +01:00
feature (orgmode): making org mode awesome from a browser
This commit is contained in:
parent
db44e0b7e3
commit
8b313c6bef
39 changed files with 866 additions and 255 deletions
11
README.md
11
README.md
|
|
@ -18,10 +18,11 @@
|
|||
- Upload files and folders
|
||||
- Works great on mobile
|
||||
- Multiple cloud providers and protocols, easily extensible
|
||||
- [Org Mode](https://orgmode.org/) friendly: see [org features](https://github.com/mickael-kerjean/nuage/wiki/Org-Mode)
|
||||
- Audio player
|
||||
- Video player
|
||||
- Image viewer
|
||||
- Emacs keybindings + [org mode](https://orgmode.org/) friendly `;)`
|
||||
- Emacs keybindings `;)`
|
||||
- Frequently access folders are pin to the homepage for quick access
|
||||
- Customise the connection page so that your users don't even have to know what protocol to use and where it is located ([example](http://files.kerjean.me))
|
||||
- Stateless (perfect candidate for AWS lamdba if that's your thing)
|
||||
|
|
@ -39,13 +40,7 @@ node server/index.js
|
|||
Or with [docker](https://hub.docker.com/r/machines/nuage/) and [Docker compose](https://github.com/mickael-kerjean/nuage/blob/master/docker/docker-compose.yml)
|
||||
|
||||
# What about my credentials?
|
||||
Credentials are stored in your browser in a http only cookie encrypted using aes-256-cbc and aren't persistent in the server disk at all.
|
||||
The "remember me" feature relies on localstorage to store your credentials encrypted using aes-256-cbc.
|
||||
|
||||
Note that on the FTP and sFTP, sessions connections aren't destroyed on every request but are reused and killed after 2 minutes. The reasoning is connections are expensive to create and this trick make the entire application feel much much faster for users who tries to navigate.
|
||||
|
||||
# Licensing
|
||||
Nuage is an open source software with its source code available under the AGPL license. Commercial license and support is available upon request, contact me for details: mickael@kerjean.me
|
||||
Nuage is stateless, nothing is kept server side. Credentials are simply stored in an http only cookie encrypted using aes-256-cbc only the server has the key (in config_server.js).
|
||||
|
||||
# Credits
|
||||
- Iconography: www.flaticon.com, fontawesome.com and material.io
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@import url('https://fonts.googleapis.com/css?family=Source+Code+Pro:400,600');
|
||||
|
||||
:root {
|
||||
--bg-color: #f2f2f2;
|
||||
--color: #626469;
|
||||
|
|
|
|||
59
client/assets/img/alarm.svg
Normal file
59
client/assets/img/alarm.svg
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
aria-hidden="true"
|
||||
data-prefix="far"
|
||||
data-icon="alarm-clock"
|
||||
role="img"
|
||||
viewBox="0 0 512 512"
|
||||
class="svg-inline--fa fa-alarm-clock fa-w-16 fa-7x"
|
||||
version="1.1"
|
||||
id="svg1553"
|
||||
sodipodi:docname="alarm.svg"
|
||||
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||
<metadata
|
||||
id="metadata1559">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1557" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="940"
|
||||
inkscape:window-height="1027"
|
||||
id="namedview1555"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.4609375"
|
||||
inkscape:cx="315.66101"
|
||||
inkscape:cy="256"
|
||||
inkscape:window-x="10"
|
||||
inkscape:window-y="37"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg1553" />
|
||||
<path
|
||||
d="m 459.7,194.55 c 22.3,-20.5 36.3,-49.9 36.3,-82.6 0,-61.9 -50.1,-112 -112,-112 -45.3,0 -84.3,26.8 -101.9,65.5 -17.3,-2 -34.9,-2 -52.2,0 -17.6,-38.7 -56.6,-65.5 -101.9,-65.5 -61.9,0 -112,50.1 -112,112 0,32.7 14,62.1 36.3,82.6 -13,28.4 -20.3,60.1 -20.3,93.4 0,53.2 18.6,102.1 49.5,140.5 L 39,470.95 c -9.4,9.4 -9.4,24.6 0,33.9 9.4,9.4 24.6,9.4 33.9,0 l 42.5,-42.5 c 81.5,65.7 198.7,66.4 281,0 l 42.6,42.6 c 9.4,9.4 24.6,9.4 33.9,0 9.4,-9.4 9.4,-24.6 0,-33.9 l -42.5,-42.5 c 31,-38.4 49.5,-87.3 49.5,-140.5 0.1,-33.4 -7.2,-65.1 -20.2,-93.5 z M 384,47.95 c 35.3,0 64,28.7 64,64 0,15.1 -5.3,29 -14,39.9 -26.2,-34.2 -62,-60.6 -103.3,-75.2 11.4,-17.3 31,-28.7 53.3,-28.7 z m -320,64 c 0,-35.3 28.7,-64 64,-64 22.3,0 41.9,11.4 53.4,28.7 -41.4,14.6 -77.2,41 -103.3,75.2 C 69.3,140.95 64,127.05 64,111.95 Z m 192,352 c -97.3,0 -176,-78.7 -176,-176 0,-97 78.4,-176 176,-176 97.4,0 176,78.8 176,176 0,97.3 -78.7,176 -176,176 z m 46.2,-95.7 -69,-47.5 c -3.3,-2.2 -5.2,-5.9 -5.2,-9.9 v -130.9 c 0,-6.6 5.4,-12 12,-12 h 32 c 6.6,0 12,5.4 12,12 v 107.7 l 50,34.4 c 5.5,3.8 6.8,11.2 3.1,16.7 l -18.1,26.4 c -3.8,5.4 -11.3,6.8 -16.8,3.1 z"
|
||||
class=""
|
||||
id="path1551"
|
||||
style="fill:#000000;fill-opacity:0.2"
|
||||
inkscape:connector-curvature="0" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -20,7 +20,7 @@
|
|||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1894"
|
||||
inkscape:window-width="940"
|
||||
inkscape:window-height="1027"
|
||||
id="namedview5214"
|
||||
showgrid="false"
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
<path
|
||||
d="M 400,64 H 352 V 12 C 352,5.4 346.6,0 340,0 h -40 c -6.6,0 -12,5.4 -12,12 V 64 H 160 V 12 C 160,5.4 154.6,0 148,0 H 108 C 101.4,0 96,5.4 96,12 V 64 H 48 C 21.5,64 0,85.5 0,112 v 352 c 0,26.5 21.5,48 48,48 h 352 c 26.5,0 48,-21.5 48,-48 V 112 C 448,85.5 426.5,64 400,64 Z m -2,404 H 50 c -3.3,0 -6.022147,-2.70007 -6,-6 V 154 h 360 v 308 c 0,3.3 -2.7,6 -6,6 z"
|
||||
id="path5210"
|
||||
style="fill:#000000;fill-opacity:0.53333336"
|
||||
style="fill:#000000;fill-opacity:0.2"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="scssssccsssscsssssssssssccss" />
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 1.9 KiB |
52
client/assets/img/search.svg
Normal file
52
client/assets/img/search.svg
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1399"
|
||||
sodipodi:docname="search.svg"
|
||||
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||
<metadata
|
||||
id="metadata1405">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1403" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="940"
|
||||
inkscape:window-height="1027"
|
||||
id="namedview1401"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.4609375"
|
||||
inkscape:cx="285.28814"
|
||||
inkscape:cy="256"
|
||||
inkscape:window-x="964"
|
||||
inkscape:window-y="37"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg1399" />
|
||||
<path
|
||||
d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"
|
||||
id="path1397"
|
||||
style="fill:#000000;fill-opacity:0.2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -8,7 +8,6 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { Icon, NgIf } from "./";
|
||||
import { debounce } from "../helpers/";
|
||||
import './dropdown.scss';
|
||||
|
||||
export class Dropdown extends React.Component {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
.dropdown_container{display: none; position: absolute; right: 0;}
|
||||
|
||||
.dropdown_button{
|
||||
border: 1px solid white;
|
||||
border: 1px solid rgba(0,0,0,0);
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
min-width: 20px;
|
||||
|
|
@ -37,8 +37,12 @@
|
|||
&.active{
|
||||
.dropdown_container{
|
||||
display: block;
|
||||
li:hover{
|
||||
background: var(--bg-color);
|
||||
li{
|
||||
background: white;
|
||||
transition: background 0.1s ease-out;
|
||||
&:hover{
|
||||
background: var(--bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.dropdown_button{
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import img_loading_white from "../assets/img/loader_white.svg";
|
|||
import img_download_white from "../assets/img/download_white.svg";
|
||||
import img_todo_white from '../assets/img/todo_white.svg';
|
||||
import img_calendar_white from '../assets/img/calendar_white.svg';
|
||||
import img_calendar from '../assets/img/calendar.svg';
|
||||
import img_alarm from '../assets/img/alarm.svg';
|
||||
import img_arrow_right from '../assets/img/arrow_right.svg';
|
||||
import img_more from '../assets/img/more.svg';
|
||||
import img_close from '../assets/img/close.svg';
|
||||
|
|
@ -26,6 +28,7 @@ import img_deadline from '../assets/img/deadline.svg';
|
|||
import img_arrow_down from '../assets/img/arrow-down.svg';
|
||||
import img_arrow_up_double from '../assets/img/arrow-up-double.svg';
|
||||
import img_arrow_down_double from '../assets/img/arrow-down-double.svg';
|
||||
import img_search from '../assets/img/search.svg';
|
||||
|
||||
export const Icon = (props) => {
|
||||
let img;
|
||||
|
|
@ -62,9 +65,9 @@ export const Icon = (props) => {
|
|||
}else if(props.name === 'calendar_white'){
|
||||
img = img_calendar_white;
|
||||
}else if(props.name === 'schedule'){
|
||||
img = img_schedule;
|
||||
img = img_calendar;
|
||||
}else if(props.name === 'deadline'){
|
||||
img = img_deadline;
|
||||
img = img_alarm;
|
||||
}else if(props.name === 'todo_white'){
|
||||
img = img_todo_white;
|
||||
}else if(props.name === 'arrow_right'){
|
||||
|
|
@ -79,6 +82,8 @@ export const Icon = (props) => {
|
|||
img = img_arrow_down_double;
|
||||
}else if(props.name === 'arrow_down'){
|
||||
img = img_arrow_down;
|
||||
}else if(props.name === 'search'){
|
||||
img = img_search;
|
||||
}else{
|
||||
throw('unknown icon');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
background: inherit;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
border-bottom: 2px solid rgba(70, 99, 114, 0.1);
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
font-size: inherit;
|
||||
|
|
@ -11,4 +10,11 @@
|
|||
outline: none;
|
||||
box-sizing: border-box;
|
||||
color: inherit;
|
||||
|
||||
|
||||
border-bottom: 2px solid rgba(70, 99, 114, 0.1);
|
||||
transition: border-color 0.2s ease-out;
|
||||
&:focus{
|
||||
border-color: var(--emphasis-primary);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,14 @@ export class Modal extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(){
|
||||
// that's quite a bad hack but well it will do for now
|
||||
requestAnimationFrame(() => {
|
||||
this.setState({marginTop: this._marginTop()});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
this._resetMargin();
|
||||
window.addEventListener("resize", this._resetMargin);
|
||||
window.addEventListener('keydown', this._onEscapeKeyPress);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,5 +24,5 @@ export class NgIf extends React.Component {
|
|||
}
|
||||
|
||||
NgIf.propTypes = {
|
||||
cond: PropTypes.bool.isRequired
|
||||
cond: PropTypes.bool.isRequired
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
background: inherit;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
border-bottom: 2px solid rgba(70, 99, 114, 0.1);
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
font-size: inherit;
|
||||
|
|
@ -12,4 +11,17 @@
|
|||
outline: none;
|
||||
box-sizing: border-box;
|
||||
color: inherit;
|
||||
vertical-align: top;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
&[name="password"]{
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
|
||||
border-bottom: 2px solid rgba(70, 99, 114, 0.1);
|
||||
transition: border-color 0.2s ease-out;
|
||||
&:focus{
|
||||
border-color: var(--emphasis-primary);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,14 +90,16 @@ export function http_delete(url){
|
|||
|
||||
|
||||
function handle_error_response(xhr, err){
|
||||
let message = (function(content){
|
||||
const response = (function(content){
|
||||
let message = content;
|
||||
try{
|
||||
message = JSON.parse(content)['message'];
|
||||
message = JSON.parse(content);
|
||||
}catch(err){}
|
||||
return message;
|
||||
return message || {};
|
||||
})(xhr.responseText);
|
||||
|
||||
const message = response.message || null;
|
||||
|
||||
if(navigator.onLine === false){
|
||||
err({message: 'Connection Lost', code: "NO_INTERNET"});
|
||||
}else if(xhr.status === 500){
|
||||
|
|
@ -112,7 +114,11 @@ function handle_error_response(xhr, err){
|
|||
}else if(xhr.status === 502){
|
||||
err({message: message || "The destination is acting weird", code: "BAD_GATEWAY"});
|
||||
}else if(xhr.status === 409){
|
||||
err({message: message || "Oups you just ran into a conflict", code: "CONFLICT"});
|
||||
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'});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ Data.prototype.upsert = function(type, path, fn){
|
|||
return new Promise((done, error) => {
|
||||
query.onsuccess = (e) => {
|
||||
const new_data = fn(query.result || null);
|
||||
if(!new_data) return done(query.result);
|
||||
if(!new_data) return done(query.result || null);
|
||||
|
||||
const request = store.put(new_data);
|
||||
request.onsuccess = () => done(new_data);
|
||||
|
|
|
|||
4
client/helpers/common.js
Normal file
4
client/helpers/common.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -10,4 +10,5 @@ export { prepare } from './navigate';
|
|||
export { invalidate, http_get, http_post, http_delete } from './ajax';
|
||||
export { prompt } from './prompt';
|
||||
export { notify } from './notify';
|
||||
export { guid } from './random';
|
||||
export { gid } from './random';
|
||||
export { leftPad } from './common';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { guid } from "./";
|
||||
import { gid, leftPad } from "./";
|
||||
|
||||
export function extractTodos(text){
|
||||
const headlines = parse(text);
|
||||
|
|
@ -11,20 +11,33 @@ export function extractTodos(text){
|
|||
}
|
||||
return todos
|
||||
.sort((a,b) => {
|
||||
if(a.status === "DONE") return +1;
|
||||
else if(a.todo_status === "todo") return -1;
|
||||
return 0;
|
||||
if(a.status === "TODO" && b.status !== "TODO" && b.todo_status === "todo") return -1;
|
||||
else if(b.status === "TODO" && a.status !== "TODO" && a.todo_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"].indexOf(thing.header.todo_keyword) !== -1 ? 'todo' : 'done';
|
||||
return {
|
||||
key: thing.header.todo_keyword,
|
||||
id: thing.id,
|
||||
line: thing.header.line,
|
||||
title: thing.header.title,
|
||||
status: thing.header.todo_keyword,
|
||||
todo_status: ["TODO", "NEXT", "DOING", "WAITING"].indexOf(thing.header.todo_keyword) !== -1 ? 'todo' : 'done',
|
||||
is_overdue: _is_overdue(thing.header.todo_keyword, thing.timestamps),
|
||||
todo_status: todo_status,
|
||||
is_overdue: _is_overdue(todo_status, thing.timestamps),
|
||||
priority: thing.header.priority,
|
||||
scheduled: _find_scheduled(thing.timestamps),
|
||||
deadline: _find_deadline(thing.timestamps),
|
||||
tasks: thing.subtasks,
|
||||
tags: thing.header.tags
|
||||
};
|
||||
|
|
@ -46,22 +59,26 @@ export function extractEvents(text){
|
|||
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"].indexOf(keyword) !== -1 ? 'todo' : 'done';
|
||||
}(thing.header.todo_keyword);
|
||||
let event = {
|
||||
id: thing.id,
|
||||
line: thing.header.line,
|
||||
title: thing.header.title,
|
||||
status: thing.header.todo_keyword,
|
||||
todo_status: function(keyword){
|
||||
if(!keyword) return null;
|
||||
return ["TODO", "NEXT", "DOING", "WAITING"].indexOf(keyword) !== -1 ? 'todo' : 'done';
|
||||
}(thing.header.todo_keyword),
|
||||
is_overdue: _is_overdue(thing.header.todo_keyword, thing.timestamps),
|
||||
todo_status: todo_status,
|
||||
scheduled: _find_scheduled(thing.timestamps),
|
||||
deadline: _find_deadline(thing.timestamps),
|
||||
is_overdue: _is_overdue(todo_status, thing.timestamps),
|
||||
priority: thing.header.priority,
|
||||
tasks: [],
|
||||
tags: thing.header.tags
|
||||
};
|
||||
if(event.todo_status === 'done') continue;
|
||||
|
||||
event.date = timestamp.timestamp;
|
||||
event.date = new Date(timestamp.timestamp);
|
||||
const today = new Date();
|
||||
today.setHours(23);
|
||||
today.setMinutes(59);
|
||||
|
|
@ -70,8 +87,9 @@ export function extractEvents(text){
|
|||
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"){
|
||||
events.push(event);
|
||||
|
|
@ -82,9 +100,9 @@ export function extractEvents(text){
|
|||
timestamp.repeat.n *= 7;
|
||||
}
|
||||
const n_days = timestamp.repeat.n;
|
||||
let today = normalise(new Date());
|
||||
let today = _normalise(new Date());
|
||||
for(let j=0;j<30;j++){
|
||||
if(((today - normalise(timestamp.timestamp)) / 1000*60*60*24) % n_days === 0){
|
||||
if(((today - _normalise(new Date(timestamp.timestamp))) / 1000*60*60*24) % n_days === 0){
|
||||
event.date = today.getTime();
|
||||
event.key = Intl.DateTimeFormat().format(today);
|
||||
events.push(JSON.parse(JSON.stringify((event))));
|
||||
|
|
@ -97,32 +115,28 @@ export function extractEvents(text){
|
|||
}
|
||||
}
|
||||
return events;
|
||||
|
||||
function normalise(date){
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
return date;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function parse(content){
|
||||
let todos = [], todo = reset(), data, text;
|
||||
let todos = [], todo = reset(0), data, text, tags = [];
|
||||
|
||||
const lines = content.split("\n");
|
||||
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);
|
||||
tags.push({ level: data.level, tags: data.tags });
|
||||
data.tags = Array.concat.apply(null, tags.map((e) => e.tags));
|
||||
|
||||
if(todo.header){
|
||||
todos.push(todo);
|
||||
todo = reset();
|
||||
todo = reset(i);
|
||||
}
|
||||
todo.header = data;
|
||||
}else if(data = parse_timestamp(text, i)){
|
||||
todo.timestamps.push(data);
|
||||
todo.timestamps = todo.timestamps.concat(data);
|
||||
}else if(data = parse_subtask(text, i)){
|
||||
todo.subtasks.push(data);
|
||||
}
|
||||
|
|
@ -130,10 +144,11 @@ export function parse(content){
|
|||
todos.push(todo);
|
||||
}
|
||||
}
|
||||
|
||||
return todos;
|
||||
|
||||
function reset(){
|
||||
return {id: guid(), timestamps: [], subtasks: []};
|
||||
function reset(i){
|
||||
return {id: leftPad(i.toString(), 5) + gid(i), timestamps: [], subtasks: []};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -171,16 +186,17 @@ function parse_subtask(text, line){
|
|||
}
|
||||
|
||||
|
||||
function parse_timestamp(text, line){
|
||||
const match = text.match(/(?:([A-Z]+)\:\s){0,1}([<\[])(\d{4}-\d{2}-\d{2})[^>](?:[A-Z][a-z]{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]{2}\s(\d{2}:\d{2}){0,1}[>\]]){0,1}/);
|
||||
if(!match) return null;
|
||||
function parse_timestamp(text, line, _memory){
|
||||
const reg = /(?:([A-Z]+)\:\s){0,1}([<\[])(\d{4}-\d{2}-\d{2})[^>](?:[A-Z][a-z]{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]{2}\s(\d{2}:\d{2}){0,1}[>\]]){0,1}/;
|
||||
const match = text.match(reg);
|
||||
if(!match) return _memory || null;
|
||||
|
||||
// https://orgmode.org/manual/Timestamps.html
|
||||
return {
|
||||
const timestamp = {
|
||||
line: line,
|
||||
keyword: match[1],
|
||||
active: match[2] === "<" ? true : false,
|
||||
timestamp: new Date(match[3] + (match[4] ? " "+match[4] : "")),
|
||||
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);
|
||||
|
|
@ -191,22 +207,30 @@ function parse_timestamp(text, line){
|
|||
return null;
|
||||
}(match[3], match[4], match[5], match[7], match[8]),
|
||||
repeat: function(keyword){
|
||||
if(!keyword) return;
|
||||
if(!keyword) return null;
|
||||
return {
|
||||
n: parseInt(keyword.replace(/^.*([0-9]+).*$/, "$1")),
|
||||
interval: keyword.replace(/^.*([dwmy])$/, "$1")
|
||||
};
|
||||
}(match[6])
|
||||
};
|
||||
if(!_memory) _memory = [];
|
||||
_memory.push(timestamp);
|
||||
return parse_timestamp(text.replace(reg, ""), line, _memory);
|
||||
}
|
||||
|
||||
|
||||
function _find_deadline(timestamps){
|
||||
return timestamps.filter((e) => e.keyword === "DEADLINE")[0] || null;
|
||||
}
|
||||
function _find_scheduled(timestamps){
|
||||
return timestamps.filter((e) => e.keyword === "SCHEDULED")[0] || null;
|
||||
}
|
||||
|
||||
function _is_overdue(status, timestamp){
|
||||
if(status !== "TODO") return false;
|
||||
if(status !== "todo") return false;
|
||||
return timestamp.filter((timeObj) => {
|
||||
if(new Date() < timeObj.date) return false;
|
||||
if(timeObj.keyword === "DEADLINE" || timeObj.keyword === "SCHEDULE") 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;
|
||||
}
|
||||
|
|
@ -218,3 +242,11 @@ function _date_label(date){
|
|||
date.setMilliseconds(0);
|
||||
return window.Intl.DateTimeFormat().format(date);
|
||||
}
|
||||
|
||||
function _normalise(date){
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
return date;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
export function guid(){
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
||||
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);
|
||||
return id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class FileSystem{
|
|||
this.obs = obs;
|
||||
let keep_pulling_from_http = false;
|
||||
this._ls_from_cache(path, true)
|
||||
.then(() => {
|
||||
.then((cache) => {
|
||||
const fetch_from_http = (_path) => {
|
||||
return this._ls_from_http(_path)
|
||||
.then(() => new Promise((done, err) => {
|
||||
|
|
@ -30,7 +30,11 @@ class FileSystem{
|
|||
if(keep_pulling_from_http === false) return Promise.resolve();
|
||||
return fetch_from_http(_path);
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch((e) => {
|
||||
if(cache === null){
|
||||
this.obs && this.obs.error({message: "Unknown Path"});
|
||||
}
|
||||
});
|
||||
};
|
||||
fetch_from_http(path);
|
||||
});
|
||||
|
|
@ -81,7 +85,7 @@ class FileSystem{
|
|||
});
|
||||
}).catch((_err) => {
|
||||
this.obs.next(_err);
|
||||
return Promise.reject();
|
||||
return Promise.reject(null);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +96,7 @@ class FileSystem{
|
|||
if(this.current_path === path){
|
||||
this.obs && this.obs.next({status: 'ok', results: response.results});
|
||||
}
|
||||
return Promise.resolve();
|
||||
return response;
|
||||
});
|
||||
}else{
|
||||
return cache.upsert(cache.FILE_PATH, path, (response) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Container, Card, NgIf, Input, Button, Textarea, Loader, Notification, Prompt } from '../../components/';
|
||||
import { invalidate, encrypt, decrypt } from '../../helpers/';
|
||||
import { invalidate, encrypt, decrypt, gid } from '../../helpers/';
|
||||
import { Session } from '../../model/';
|
||||
import config from '../../../config_client';
|
||||
import './form.scss';
|
||||
|
|
@ -190,7 +190,6 @@ export class Form extends React.Component {
|
|||
<Input type={this.input_type('webdav', 'path')} name="path" placeholder="Path" ref={(input) => {this.state.refs.webdav_path = input; }} autoComplete="new-password" />
|
||||
</NgIf>
|
||||
</NgIf>
|
||||
<Input type="hidden" name="type" value="webdav"/>
|
||||
<Button theme="emphasis">CONNECT</Button>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.type === 'ftp'}>
|
||||
|
|
@ -203,7 +202,6 @@ export class Form extends React.Component {
|
|||
<NgIf cond={this.should_appear('ftp', 'password')}>
|
||||
<Input type={this.input_type('ftp', 'password')} name="password" placeholder="Password" ref={(input) => {this.state.refs.ftp_password = input; }} autoComplete="new-password" />
|
||||
</NgIf>
|
||||
<Input type="hidden" name="type" value="ftp"/>
|
||||
<NgIf cond={this.should_appear('ftp', 'advanced')}>
|
||||
<label>
|
||||
<input checked={this.state.advanced_ftp} onChange={e => { this.setState({advanced_ftp: e.target.checked}); }} type="checkbox" autoComplete="new-password"/> Advanced
|
||||
|
|
@ -229,7 +227,6 @@ export class Form extends React.Component {
|
|||
<NgIf cond={this.should_appear('sftp', 'password')}>
|
||||
<Input type={this.input_type('sftp', 'password')} name="password" placeholder="Password" ref={(input) => {this.state.refs.sftp_password = input; }} autoComplete="new-password" />
|
||||
</NgIf>
|
||||
<Input type="hidden" name="type" value="sftp"/>
|
||||
<NgIf cond={this.should_appear('sftp', 'advanced')}>
|
||||
<label>
|
||||
<input checked={this.state.advanced_sftp} onChange={e => { this.setState({advanced_sftp: JSON.parse(e.target.checked)}); }} type="checkbox" autoComplete="new-password"/> Advanced
|
||||
|
|
@ -256,13 +253,13 @@ export class Form extends React.Component {
|
|||
<Input type={this.input_type('git', 'username')} name="username" placeholder="Username" ref={(input) => {this.state.refs.git_username = input; }} autoComplete="new-password" />
|
||||
</NgIf>
|
||||
<NgIf cond={this.should_appear('git', 'password')}>
|
||||
<Textarea type="text" style={this.input_type('git', 'password') === 'hidden' ? {visibility: 'hidden', position: 'absolute'} : {} } rows="1" name="password" placeholder="Password" ref={(input) => {this.state.refs.git_password = input; }} autoComplete="new-password" />
|
||||
<Textarea type="text" style={this.input_type('git', 'password') === 'hidden' ? {visibility: 'hidden', position: 'absolute'} : {} } rows="1" name="password" placeholder="Password" ref={(input) => {this.state.refs.git_password = input; }} autoComplete="new-password" />
|
||||
</NgIf>
|
||||
<Input type="hidden" name="type" value="git"/>
|
||||
<Input type="hidden" name="uid" value={gid()} ref={(input) => { this.state.refs.git_uid = input; }} />
|
||||
<NgIf cond={this.should_appear('git', 'advanced')}>
|
||||
<label>
|
||||
<input checked={this.state.advanced_git} onChange={e => { this.setState({advanced_git: JSON.parse(e.target.checked)}); }} type="checkbox" autoComplete="new-password"/> Advanced
|
||||
</label>
|
||||
<label>
|
||||
<input checked={this.state.advanced_git} onChange={e => { this.setState({advanced_git: JSON.parse(e.target.checked)}); }} type="checkbox" autoComplete="new-password"/> Advanced
|
||||
</label>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.advanced_git === true} className="advanced_form">
|
||||
<NgIf cond={this.should_appear('git', 'passphrase')}>
|
||||
|
|
@ -296,7 +293,6 @@ export class Form extends React.Component {
|
|||
<NgIf cond={this.should_appear('s3', 'secret_access_key')}>
|
||||
<Input type={this.input_type('s3', 'secret_access_key')} name="secret_access_key" placeholder="Secret Access Key*" ref={(input) => {this.state.refs.s3_secret_access_key = input; }} autoComplete="new-password" />
|
||||
</NgIf>
|
||||
<Input type="hidden" name="type" value="s3"/>
|
||||
<NgIf cond={this.should_appear('s3', 'advanced')}>
|
||||
<label>
|
||||
<input checked={this.state.advanced_s3} onChange={e => { this.setState({advanced_s3: JSON.parse(e.target.checked)}); }} type="checkbox" autoComplete="new-password"/> Advanced
|
||||
|
|
@ -314,7 +310,6 @@ export class Form extends React.Component {
|
|||
<div onClick={this.redirect_dropbox.bind(this)}>
|
||||
<img src={img_dropbox} />
|
||||
</div>
|
||||
<Input type="hidden" name="type" value="dropbox"/>
|
||||
<Button type="button" onClick={this.redirect_dropbox.bind(this)} theme="emphasis">LOGIN WITH DROPBOX</Button>
|
||||
</a>
|
||||
</NgIf>
|
||||
|
|
@ -322,7 +317,6 @@ export class Form extends React.Component {
|
|||
<div onClick={this.redirect_google.bind(this)}>
|
||||
<img src={img_drive}/>
|
||||
</div>
|
||||
<Input type="hidden" name="type" value="gdrive"/>
|
||||
<Button type="button" onClick={this.redirect_google.bind(this)} theme="emphasis">LOGIN WITH GOOGLE</Button>
|
||||
</NgIf>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
.component_page_notfound{
|
||||
.error{
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
margin: 50px auto 0 auto;
|
||||
|
||||
|
|
@ -4,6 +4,7 @@ import HTML5Backend from 'react-dnd-html5-backend-filedrop';
|
|||
import Path from 'path';
|
||||
|
||||
import './filespage.scss';
|
||||
import './error.scss';
|
||||
import { Files } from '../model/';
|
||||
import { NgIf, Loader, Uploader, EventReceiver } from '../components/';
|
||||
import { notify, debounce, goToFiles, goToViewer, event } from '../helpers/';
|
||||
|
|
@ -19,7 +20,7 @@ export class FilesPage extends React.Component {
|
|||
files: [],
|
||||
frequents: [],
|
||||
loading: true,
|
||||
error: false,
|
||||
error: null,
|
||||
height: null
|
||||
};
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ export class FilesPage extends React.Component {
|
|||
}
|
||||
|
||||
hideError(){
|
||||
this.setState({error: false});
|
||||
this.setState({error: null});
|
||||
}
|
||||
|
||||
onRefresh(path = this.state.path){
|
||||
|
|
@ -83,11 +84,10 @@ export class FilesPage extends React.Component {
|
|||
notify.send(res, 'error');
|
||||
}
|
||||
}, (error) => {
|
||||
notify.send(error, 'error');
|
||||
this.setState({error: error});
|
||||
});
|
||||
this.observers.push(observer);
|
||||
this.setState({error: false});
|
||||
this.setState({error: null});
|
||||
Files.frequents().then((s) => this.setState({frequents: s}));
|
||||
}
|
||||
|
||||
|
|
@ -310,16 +310,21 @@ export class FilesPage extends React.Component {
|
|||
<BreadCrumb className="breadcrumb" path={this.state.path} />
|
||||
<div className="page_container">
|
||||
<div className="scroll-y">
|
||||
<NgIf className="container" cond={this.state.loading === false}>
|
||||
<NgIf className="container" cond={this.state.loading === false && this.state.error === null}>
|
||||
<NgIf cond={this.state.path === '/'}>
|
||||
<FrequentlyAccess files={this.state.frequents}/>
|
||||
</NgIf>
|
||||
<FileSystem path={this.state.path} files={this.state.files} />
|
||||
<Uploader path={this.state.path} />
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.loading}>
|
||||
<NgIf cond={this.state.loading && this.state.error === null}>
|
||||
<Loader/>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.error !== null} className="error">
|
||||
<h1>Oops!</h1>
|
||||
<h2>It seems this directory doesn't exist</h2>
|
||||
<p>{JSON.stringify(this.state.error)}</p>
|
||||
</NgIf>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,4 +22,7 @@
|
|||
overflow-y: scroll!important;
|
||||
overflow-x: hidden!important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
> .container{
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,17 @@ export class NewThing extends React.Component {
|
|||
message: null,
|
||||
icon: null
|
||||
};
|
||||
|
||||
this._onEscapeKeyPress = (e) => {
|
||||
if(e.keyCode === 27) this.onDelete();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
window.addEventListener('keydown', this._onEscapeKeyPress);
|
||||
}
|
||||
componentWillUnmount(){
|
||||
window.removeEventListener('keydown', this._onEscapeKeyPress);
|
||||
}
|
||||
|
||||
onNew(type){
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
}
|
||||
|
||||
.component_thing{
|
||||
clear: both;
|
||||
&:hover .box, .highlight.box{
|
||||
background: var(--super-light);
|
||||
border-color: var(--super-light);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Button } from '../components/';
|
||||
import './notfoundpage.scss';
|
||||
import './error.scss';
|
||||
|
||||
export class NotFoundPage extends React.Component {
|
||||
constructor(props){
|
||||
|
|
@ -29,7 +29,7 @@ export class NotFoundPage extends React.Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<div className="component_page_notfound">
|
||||
<div className="component_page_notfound error">
|
||||
<h1>Oops!</h1>
|
||||
<h2>We can't seem to find the page you're looking for.</h2>
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import Path from 'path';
|
||||
|
||||
import './viewerpage.scss';
|
||||
import './error.scss';
|
||||
import { Files } from '../model/';
|
||||
import { BreadCrumb, Bundle, NgIf, Loader, Container, EventReceiver, EventEmitter } from '../components/';
|
||||
import { debounce, opener, notify } from '../helpers/';
|
||||
|
|
@ -31,6 +32,7 @@ export class ViewerPage extends React.Component {
|
|||
needSaving: false,
|
||||
isSaving: false,
|
||||
loading: true,
|
||||
error: null
|
||||
};
|
||||
this.props.subscribe('file.select', this.onPathUpdate.bind(this));
|
||||
}
|
||||
|
|
@ -58,7 +60,7 @@ export class ViewerPage extends React.Component {
|
|||
if(err && err.code === 'BINARY_FILE'){
|
||||
this.setState({opener: 'download', loading: false});
|
||||
}else{
|
||||
notify.send(err, 'error');
|
||||
this.setState({error: err});
|
||||
}
|
||||
});
|
||||
}else{
|
||||
|
|
@ -114,7 +116,7 @@ export class ViewerPage extends React.Component {
|
|||
<div className="component_page_viewerpage">
|
||||
<BreadCrumb needSaving={this.state.needSaving} className="breadcrumb" path={this.state.path} />
|
||||
<div className="page_container">
|
||||
<NgIf cond={this.state.loading === false}>
|
||||
<NgIf cond={this.state.loading === false && this.state.error === null}>
|
||||
<NgIf cond={this.state.opener === 'editor'}>
|
||||
<IDE needSavingUpdate={this.needSaving.bind(this)}
|
||||
needSaving={this.state.needSaving}
|
||||
|
|
@ -140,9 +142,14 @@ export class ViewerPage extends React.Component {
|
|||
<FileDownloader data={this.state.url} filename={this.state.filename} />
|
||||
</NgIf>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.loading === true}>
|
||||
<NgIf cond={this.state.loading === true && this.state.error === null}>
|
||||
<Loader/>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.error !== null} className="error">
|
||||
<h1>Oops!</h1>
|
||||
<h2>There is nothing in here</h2>
|
||||
<p>{JSON.stringify(this.state.error)}</p>
|
||||
</NgIf>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -30,9 +30,11 @@ export class Editor extends React.Component {
|
|||
this.state = {
|
||||
loading: null,
|
||||
editor: null,
|
||||
filename: this.props.filename
|
||||
filename: this.props.filename,
|
||||
listeners: []
|
||||
};
|
||||
this._refresh = this._refresh.bind(this);
|
||||
this.onEdit = this.onEdit.bind(this);
|
||||
}
|
||||
|
||||
_refresh(){
|
||||
|
|
@ -56,12 +58,18 @@ export class Editor extends React.Component {
|
|||
const [type, value] = data;
|
||||
if(type === "goTo"){
|
||||
const pY = this.state.editor.charCoords({line: value, ch: 0}, "local").top;
|
||||
this.state.editor.scrollTo(null, pY);
|
||||
//this.state.editor.setCursor({line: new_props.currentLine, ch: 2});
|
||||
this.state.editor.operation((cm) => {
|
||||
this.state.editor.scrollTo(null, pY);
|
||||
this.state.editor.setSelection({line: value, ch: 0}, {line: value, ch: this.state.editor.getLine(value).length});
|
||||
});
|
||||
}else if(type === "refresh"){
|
||||
const cursor = this.state.editor.getCursor();
|
||||
const selections = this.state.editor.listSelections();
|
||||
this.state.editor.setValue(this.props.content);
|
||||
this.state.editor.setCursor(cursor);
|
||||
if(selections.length > 0){
|
||||
this.state.editor.setSelection(selections[0].anchor, selections[0].head);
|
||||
}
|
||||
}else if(type === "fold"){
|
||||
this.props.onFoldChange(
|
||||
org_shifttab(this.state.editor)
|
||||
|
|
@ -74,7 +82,7 @@ export class Editor extends React.Component {
|
|||
function loadCodeMirror(data){
|
||||
const [CodeMirror, mode] = data;
|
||||
|
||||
const size_small = 500;
|
||||
let listeners = [];
|
||||
let editor = CodeMirror(document.getElementById('editor'), {
|
||||
value: this.props.content,
|
||||
lineNumbers: true,
|
||||
|
|
@ -85,26 +93,20 @@ export class Editor extends React.Component {
|
|||
widget: "..."
|
||||
}
|
||||
});
|
||||
|
||||
if(!('ontouchstart' in window)) editor.focus();
|
||||
editor.getWrapperElement().setAttribute("mode", mode);
|
||||
this.props.onModeChange(mode);
|
||||
|
||||
editor.on('change', this.onEdit);
|
||||
|
||||
if(mode === "orgmode"){
|
||||
CodeMirror.orgmode.init(editor, (key, value) => {
|
||||
listeners.push(CodeMirror.orgmode.init(editor, (key, value) => {
|
||||
if(key === "shifttab"){
|
||||
this.props.onFoldChange(value);
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
this.setState({editor: editor});
|
||||
this.props.onModeChange(mode);
|
||||
|
||||
editor.on('change', (edit) => {
|
||||
if(this.props.onChange){
|
||||
this.props.onChange(edit.getValue());
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.commands.save = () => {
|
||||
this.props.onSave && this.props.onSave();
|
||||
|
|
@ -114,12 +116,24 @@ export class Editor extends React.Component {
|
|||
window.history.back();
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise((done) => {
|
||||
this.setState({editor: editor, listeners: listeners}, done);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onEdit(cm){
|
||||
if(this.props.onChange){
|
||||
this.props.onChange(cm.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
window.removeEventListener('resize', this._refresh);
|
||||
this.state.editor.off('change', this.onEdit);
|
||||
this.state.editor.clearHistory();
|
||||
this.state.listeners.map((fn) => fn());
|
||||
}
|
||||
|
||||
loadMode(file){
|
||||
|
|
|
|||
|
|
@ -19,10 +19,8 @@
|
|||
height: 100%;
|
||||
color: #3b4045;
|
||||
background: var(--bg-color);
|
||||
font-size: 16px;
|
||||
font-family: 'Inconsolata', monospace;
|
||||
|
||||
}
|
||||
|
||||
.CodeMirror-sizer{
|
||||
> div{
|
||||
padding-top: 4px;
|
||||
|
|
@ -36,12 +34,14 @@
|
|||
}
|
||||
|
||||
/* HIDE LINE NUMBERS ON MOBILE */
|
||||
// this hack is important as we rely on the dom to provide code folding for org mode
|
||||
@media screen and (max-width: 400px) {
|
||||
.CodeMirror-sizer{ margin-left: 0!important; }
|
||||
.CodeMirror-gutters{ display: none; }
|
||||
.CodeMirror-gutter-wrapper{ display: none; }
|
||||
}
|
||||
.CodeMirror-linenumber{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* SEARCH */
|
||||
|
|
@ -71,44 +71,82 @@
|
|||
background: transparent;
|
||||
width: 20em;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog button {
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
/* Highlight Theme */
|
||||
/* Font stuff */
|
||||
.CodeMirror {
|
||||
font-size: 15px;
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
}
|
||||
.cm-s-default .cm-header { font-size: 17px; }
|
||||
.cm-s-default .cm-header.cm-level1{ font-size: 18px;}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.CodeMirror { font-size: 14px; }
|
||||
.cm-s-default .cm-header { font-size: 15px; }
|
||||
.cm-s-default .cm-header.cm-level1{ font-size: 16px;}
|
||||
}
|
||||
|
||||
/* Make things more confy */
|
||||
.CodeMirror{
|
||||
.CodeMirror-code{ line-height: 1.3em; }
|
||||
&[mode="orgmode"] .CodeMirror-code, &[mode="yaml-frontmatter"] .CodeMirror-code{
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Widget stuff */
|
||||
.CodeMirror-linewidget{
|
||||
img{
|
||||
margin: 10px;
|
||||
height: 300px;
|
||||
max-width: 80%;
|
||||
text-align: center;
|
||||
box-shadow: 1px 1px 5px rgba(0,0,0,0.5);
|
||||
background: var(--dark);
|
||||
}
|
||||
}
|
||||
|
||||
/* Code Highlight Theme */
|
||||
.cm-s-default .cm-header {
|
||||
color: #3E7AA6;
|
||||
font-size: 18px;
|
||||
margin: 1px 0 1px 0;
|
||||
display: inline-block;
|
||||
font-kerning: normal;
|
||||
line-height: 1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cm-s-default .cm-keyword {color: #3E7AA6;}
|
||||
.cm-header.cm-level1{ color: #376e95; }
|
||||
.cm-s-default .cm-keyword { color: var(--emphasis-secondary); }
|
||||
.cm-s-default .cm-header.cm-org-level-star{
|
||||
color: #6f6f6f;
|
||||
vertical-align: text-bottom;
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
padding-left: 4px;
|
||||
margin-left: -4px;
|
||||
padding-left: 5px;
|
||||
margin-left: -5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.cm-s-default .cm-header.cm-org-todo{color: #FF8355; font-weight: normal;}
|
||||
.cm-s-default .cm-header.cm-org-done{color: #3BB27C; font-weight: normal;}
|
||||
.cm-s-default .cm-header.cm-org-todo{ color: #FF8355; font-weight: normal; cursor: pointer; }
|
||||
.cm-s-default .cm-header.cm-org-done{ color: #3BB27C; font-weight: normal; cursor: pointer; }
|
||||
.cm-s-default .cm-header.cm-org-priority{ cursor: pointer; font-weight: normal; }
|
||||
.cm-s-default .cm-org-toggle{ cursor: pointer; }
|
||||
.cm-s-default .cm-void {
|
||||
display: inline-block;
|
||||
max-width: 20px;
|
||||
max-width: 10px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cm-s-default .cm-header.cm-comment{font-weight: normal;}
|
||||
.cm-s-default .cm-header.cm-comment{font-weight: normal; font-size: 0.9em!important; float: right; display: inline-block; color: var(--secondary);}
|
||||
pre.CodeMirror-line{clear: right;}
|
||||
|
||||
.cm-s-default .cm-link{color: #555!important;}
|
||||
.cm-s-default .cm-url{color: #555!important;}
|
||||
|
||||
.cm-s-default .cm-link{color: var(--secondary)}
|
||||
.cm-s-default .cm-strong{color: var(--secondary)}
|
||||
.cm-s-default .cm-org-url, .cm-s-default .cm-org-image{color: var(--secondary)!important; border-bottom: 1px dashed var(--light); cursor: pointer;}
|
||||
.cm-s-default .cm-variable-3 {color: #085;}
|
||||
.cm-s-default .cm-comment {color: #6f6f6f;}
|
||||
.cm-s-default .cm-comment {color: var(--secondary);}
|
||||
.cm-s-default .cm-string, .cm-s-default .cm-string-2{ color: #c41a16; }
|
||||
.cm-s-default .cm-def { color: rgb(68, 85, 136); }
|
||||
|
||||
|
|
@ -121,11 +159,13 @@
|
|||
color: var(--color);
|
||||
text-shadow: 1px 1px 10px var(--color);
|
||||
}
|
||||
|
||||
span.CodeMirror-matchingbracket {color: #0f0;}
|
||||
span.CodeMirror-nonmatchingbracket {color: #f22;}
|
||||
|
||||
|
||||
/* BUGFIX */
|
||||
// https://github.com/codemirror/CodeMirror/issues/5056
|
||||
.CodeMirror-cursor {
|
||||
width: 1px !important;
|
||||
min-width: 1px !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ function set_folding_mode(cm, mode){
|
|||
}else if(mode === "CONTENT"){
|
||||
folding_mode_content(cm);
|
||||
}
|
||||
cm.refresh();
|
||||
|
||||
function folding_mode_overview(cm){
|
||||
cm.operation(function() {
|
||||
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ import {
|
|||
org_metadown, org_insert_todo_heading, org_shiftleft, org_shiftright, fold, unfold,
|
||||
isFold, org_set_fold, org_shiftmetaleft, org_shiftmetaright
|
||||
} from './emacs-org';
|
||||
import { pathBuilder, dirname } from '../../../helpers/';
|
||||
let CodeMirror = window.CodeMirror;
|
||||
|
||||
CodeMirror.__mode = 'orgmode';
|
||||
|
||||
CodeMirror.defineSimpleMode("orgmode", {
|
||||
start: [
|
||||
{regex: /^(\*\s)(TODO|DOING|WAITING|NEXT|)(CANCELLED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, token: ["header level1 org-level-star","header level1 org-todo","header level1 org-done", "header level1 org-priority", "header level1", "header level1 void", "header level1 comment"]},
|
||||
{regex: /^(\*{1,}\s)(TODO|DOING|WAITING|NEXT|)(CANCELLED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, token: ["header org-level-star","header org-todo","header org-done", "header org-priority", "header", "header void", "header comment"]},
|
||||
{regex: /(\+[^\+]+\+)/, token: ["strikethrough"]},
|
||||
{regex: /(\*[^\*]+\*)/, token: ["strong"]},
|
||||
|
|
@ -16,8 +19,9 @@ CodeMirror.defineSimpleMode("orgmode", {
|
|||
{regex: /(\_[^\_]+\_)/, token: ["link"]},
|
||||
{regex: /(\~[^\~]+\~)/, token: ["comment"]},
|
||||
{regex: /(\=[^\=]+\=)/, token: ["comment"]},
|
||||
{regex: /\[\[[^\[\]]*\]\[[^\[\]]*\]\]/, token: "url"}, // links
|
||||
{regex: /\[[xX\s\-\_]?\]/, token: 'qualifier org-toggle'}, // checkbox
|
||||
{regex: /\[\[[^\[\]]+\]\[[^\[\]]+\]\]/, token: "org-url"}, // links
|
||||
{regex: /\!\[\[[^\[\]]+\]\]/, token: "org-image"}, // image
|
||||
{regex: /\[[xX\s\-\_]\]/, token: 'qualifier org-toggle'}, // checkbox
|
||||
{regex: /\#\+BEGIN_[A-Z]*/, token: "comment", next: "env"}, // comments
|
||||
{regex: /:?[A-Z_]+\:.*/, token: "comment"}, // property drawers
|
||||
{regex: /(\#\+[A-Z_]*)(\:.*)/, token: ["keyword", 'qualifier']}, // environments
|
||||
|
|
@ -73,9 +77,10 @@ CodeMirror.registerGlobalHelper("fold", "drawer", function(mode) {
|
|||
while(end < lastLine){
|
||||
end += 1;
|
||||
if(isEndOfADrawer(end)){
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
from: CodeMirror.Pos(start.line, cm.getLine(start.line).length),
|
||||
to: CodeMirror.Pos(end, cm.getLine(end).length)
|
||||
|
|
@ -96,7 +101,6 @@ CodeMirror.registerGlobalHelper("fold", "drawer", function(mode) {
|
|||
});
|
||||
|
||||
|
||||
|
||||
CodeMirror.registerHelper("orgmode", "init", (editor, fn) => {
|
||||
editor.setOption("extraKeys", {
|
||||
"Tab": function(cm) { org_cycle(cm); },
|
||||
|
|
@ -110,82 +114,13 @@ CodeMirror.registerHelper("orgmode", "init", (editor, fn) => {
|
|||
"Shift-Alt-Right": function(cm){ org_shiftmetaright(cm); },
|
||||
"Shift-Alt-Enter": function(cm){ org_insert_todo_heading(cm); },
|
||||
"Shift-Left": function(cm){ org_shiftleft(cm); },
|
||||
"Shift-Right": function(cm){ org_shiftright(cm); },
|
||||
"Shift-Right": function(cm){ org_shiftright(cm); }
|
||||
});
|
||||
fn('shifttab', org_set_fold(editor));
|
||||
|
||||
editor.addKeyMap({
|
||||
"Ctrl-X Ctrl-C": function(cm){
|
||||
cm.execCommand('quit');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Toggle headline on org mode by clicking on the heading ;)
|
||||
editor.on('mousedown', toggleHandler);
|
||||
editor.on('touchstart', toggleHandler);
|
||||
function toggleHandler(cm, e){
|
||||
const position = cm.coordsChar({
|
||||
left: e.clientX || (e.targetTouches && e.targetTouches[0].clientX),
|
||||
top: e.clientY || (e.targetTouches && e.targetTouches[0].clientY)
|
||||
}, "page"),
|
||||
token = cm.getTokenAt(position);
|
||||
|
||||
if(/org-level-star/.test(token.type)){
|
||||
_preventIfShould();
|
||||
_foldHeadline();
|
||||
}else if(/org-toggle/.test(token.type)){
|
||||
_preventIfShould();
|
||||
_toggleCheckbox();
|
||||
}else if(/org-todo/.test(token.type)){
|
||||
_preventIfShould();
|
||||
_toggleTodo();
|
||||
}else if(/org-done/.test(token.type)){
|
||||
_preventIfShould();
|
||||
_toggleDone();
|
||||
}else if(/org-priority/.test(token.type)){
|
||||
_preventIfShould();
|
||||
_togglePriority();
|
||||
}
|
||||
|
||||
|
||||
function _preventIfShould(){
|
||||
if('ontouchstart' in window) e.preventDefault();
|
||||
}
|
||||
|
||||
function _foldHeadline(){
|
||||
const line = position.line;
|
||||
if(line >= 0){
|
||||
const cursor = {line: line, ch: 0};
|
||||
isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor);
|
||||
}
|
||||
}
|
||||
function _toggleCheckbox(){
|
||||
const line = position.line;
|
||||
const content = cm.getRange({line: line, ch: token.start}, {line: line, ch: token.end});
|
||||
let new_content = content === "[X]" || content === "[x]" ? "[ ]" : "[X]";
|
||||
cm.replaceRange(new_content, {line: line, ch: token.start}, {line: line, ch: token.end});
|
||||
}
|
||||
function _toggleTodo(){
|
||||
const line = position.line;
|
||||
cm.replaceRange("DONE", {line: line, ch: token.start}, {line: line, ch: token.end});
|
||||
}
|
||||
function _toggleDone(){
|
||||
const line = position.line;
|
||||
cm.replaceRange("TODO", {line: line, ch: token.start}, {line: line, ch: token.end});
|
||||
}
|
||||
function _togglePriority(){
|
||||
const PRIORITIES = [" [#A] ", " [#B] ", " [#C] ", " [#A] "];
|
||||
const line = position.line;
|
||||
const content = cm.getRange({line: line, ch: token.start}, {line: line, ch: token.end});
|
||||
let new_content = PRIORITIES[PRIORITIES.indexOf(content) + 1];
|
||||
cm.replaceRange(new_content, {line: line, ch: token.start}, {line: line, ch: token.end});
|
||||
}
|
||||
}
|
||||
editor.on('gutterClick', function(cm, line){
|
||||
const cursor = {line: line, ch: 0};
|
||||
isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor);
|
||||
});
|
||||
editor.on('gutterClick', foldLine);
|
||||
|
||||
// fold everything except headers by default
|
||||
editor.operation(function() {
|
||||
|
|
@ -195,6 +130,153 @@ CodeMirror.registerHelper("orgmode", "init", (editor, fn) => {
|
|||
}
|
||||
}
|
||||
});
|
||||
return CodeMirror.orgmode.destroy.bind(this, editor);
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("orgmode", "destroy", (editor) => {
|
||||
editor.off('mousedown', toggleHandler);
|
||||
editor.off('touchstart', toggleHandler);
|
||||
editor.off('gutterClick', foldLine);
|
||||
});
|
||||
|
||||
function foldLine(cm, line){
|
||||
const cursor = {line: line, ch: 0};
|
||||
isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor);
|
||||
}
|
||||
|
||||
|
||||
let widgets = [];
|
||||
function toggleHandler(cm, e){
|
||||
const position = cm.coordsChar({
|
||||
left: e.clientX || (e.targetTouches && e.targetTouches[0].clientX),
|
||||
top: e.clientY || (e.targetTouches && e.targetTouches[0].clientY)
|
||||
}, "page"),
|
||||
token = cm.getTokenAt(position);
|
||||
|
||||
_disableSelection();
|
||||
if(/org-level-star/.test(token.type)){
|
||||
_preventIfShould();
|
||||
_foldHeadline();
|
||||
_disableSelection();
|
||||
}else if(/org-toggle/.test(token.type)){
|
||||
_preventIfShould();
|
||||
_toggleCheckbox();
|
||||
_disableSelection();
|
||||
}else if(/org-todo/.test(token.type)){
|
||||
_preventIfShould();
|
||||
_toggleTodo();
|
||||
_disableSelection();
|
||||
}else if(/org-done/.test(token.type)){
|
||||
_preventIfShould();
|
||||
_toggleDone();
|
||||
_disableSelection();
|
||||
}else if(/org-priority/.test(token.type)){
|
||||
_preventIfShould();
|
||||
_togglePriority();
|
||||
_disableSelection();
|
||||
}else if(/org-url/.test(token.type)){
|
||||
_disableSelection();
|
||||
_navigateLink();
|
||||
}else if(/org-image/.test(token.type)){
|
||||
_disableSelection();
|
||||
_toggleImageWidget();
|
||||
}
|
||||
|
||||
function _preventIfShould(){
|
||||
if('ontouchstart' in window) e.preventDefault();
|
||||
}
|
||||
function _disableSelection(){
|
||||
cm.on('beforeSelectionChange', _onSelectionChangeHandler);
|
||||
function _onSelectionChangeHandler(cm, obj){
|
||||
obj.update([{
|
||||
anchor: position,
|
||||
head: position
|
||||
}]);
|
||||
cm.off('beforeSelectionChange', _onSelectionChangeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function _foldHeadline(){
|
||||
const line = position.line;
|
||||
if(line >= 0){
|
||||
const cursor = {line: line, ch: 0};
|
||||
isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor);
|
||||
}
|
||||
}
|
||||
|
||||
function _toggleCheckbox(){
|
||||
const line = position.line;
|
||||
const content = cm.getRange({line: line, ch: token.start}, {line: line, ch: token.end});
|
||||
let new_content = content === "[X]" || content === "[x]" ? "[ ]" : "[X]";
|
||||
cm.replaceRange(new_content, {line: line, ch: token.start}, {line: line, ch: token.end});
|
||||
}
|
||||
|
||||
function _toggleTodo(){
|
||||
const line = position.line;
|
||||
cm.replaceRange("DONE", {line: line, ch: token.start}, {line: line, ch: token.end});
|
||||
}
|
||||
|
||||
function _toggleDone(){
|
||||
const line = position.line;
|
||||
cm.replaceRange("TODO", {line: line, ch: token.start}, {line: line, ch: token.end});
|
||||
}
|
||||
|
||||
function _togglePriority(){
|
||||
const PRIORITIES = [" [#A] ", " [#B] ", " [#C] ", " [#A] "];
|
||||
const line = position.line;
|
||||
const content = cm.getRange({line: line, ch: token.start}, {line: line, ch: token.end});
|
||||
let new_content = PRIORITIES[PRIORITIES.indexOf(content) + 1];
|
||||
cm.replaceRange(new_content, {line: line, ch: token.start}, {line: line, ch: token.end});
|
||||
}
|
||||
|
||||
function _toggleImageWidget(){
|
||||
let exist = !!widgets
|
||||
.filter((line) => line === position.line)[0];
|
||||
|
||||
if(exist === false){
|
||||
if(!token.string.match(/\!\[\[(.*)\]\]/)) return null;
|
||||
let $node = _buildImage(RegExp.$1);
|
||||
const widget = cm.addLineWidget(position.line, $node, {coverGutter: false});
|
||||
widgets.push(position.line);
|
||||
$node.addEventListener('click', closeWidget);
|
||||
|
||||
function closeWidget(){
|
||||
widget.clear();
|
||||
$node.removeEventListener('click', closeWidget);
|
||||
widgets = widgets.filter((line) => line !== position.line);
|
||||
}
|
||||
}
|
||||
function _buildImage(src){
|
||||
let $el = document.createElement("div");
|
||||
let $img = document.createElement("img");
|
||||
|
||||
if(/^https?\:\/\//.test(src)){
|
||||
$img.src = src;
|
||||
}else{
|
||||
const root_path = dirname(window.location.pathname.replace(/^\/view/, ''));
|
||||
const img_path = src;
|
||||
$img.src = "/api/files/cat?path="+encodeURIComponent(pathBuilder(root_path, img_path));
|
||||
}
|
||||
$el.appendChild($img);
|
||||
return $el;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _navigateLink(){
|
||||
token.string.match(/\[\[(.*?)\]\[/);
|
||||
const link = RegExp.$1;
|
||||
if(!link) return;
|
||||
|
||||
if(/^https?\:\/\//.test(link)){
|
||||
window.open(link);
|
||||
}else{
|
||||
const root_path = dirname(window.location.pathname.replace(/^\/view/, ''));
|
||||
const link_path = link;
|
||||
window.open("/view"+pathBuilder(root_path, link_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default CodeMirror;
|
||||
|
|
|
|||
|
|
@ -54,11 +54,11 @@ export class IDE extends React.Component {
|
|||
|
||||
|
||||
/* Org Viewer specific stuff */
|
||||
toggleAgenda(){
|
||||
this.setState({appear_agenda: !this.state.appear_agenda});
|
||||
toggleAgenda(force = null){
|
||||
this.setState({appear_agenda: force === null ? !this.state.appear_agenda : !!force});
|
||||
}
|
||||
toggleTodo(){
|
||||
this.setState({appear_todo: !this.state.appear_todo});
|
||||
toggleTodo(force = null){
|
||||
this.setState({appear_todo: force === null ? !this.state.appear_todo : !!force});
|
||||
}
|
||||
onModeChange(){
|
||||
this.state.event.next(["fold"]);
|
||||
|
|
@ -115,10 +115,10 @@ export class IDE extends React.Component {
|
|||
|
||||
<OrgEventsViewer isActive={this.state.appear_agenda} content={this.state.contentToSave}
|
||||
onUpdate={this.onUpdate.bind(this, "contentToSave", true)} goTo={this.goTo.bind(this)}
|
||||
onQuit={this.toggleAgenda.bind(this)} />
|
||||
onQuit={this.toggleAgenda.bind(this, false)} />
|
||||
<OrgTodosViewer isActive={this.state.appear_todo} content={this.state.contentToSave}
|
||||
onUpdate={this.onUpdate.bind(this, "contentToSave", true)} goTo={this.goTo.bind(this)}
|
||||
onQuit={this.toggleTodo.bind(this)} />
|
||||
onQuit={this.toggleTodo.bind(this, false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
height: 19px;
|
||||
width: 19px;
|
||||
cursor: pointer;
|
||||
padding: 6px 7px;
|
||||
padding: 7px 7px 5px 7px
|
||||
}
|
||||
.download-button .component_icon{
|
||||
padding-right: 1px;
|
||||
|
|
|
|||
|
|
@ -3,27 +3,40 @@ import { StickyContainer, Sticky } from 'react-sticky';
|
|||
|
||||
import { Modal, Container, NgIf, Icon, Dropdown, DropdownButton, DropdownList, DropdownItem } from '../../components/';
|
||||
import { extractEvents, extractTodos } from '../../helpers/org';
|
||||
import { leftPad } from '../../helpers/common';
|
||||
import { debounce } from '../../helpers/';
|
||||
import './org_viewer.scss';
|
||||
|
||||
export const OrgEventsViewer = (props) => {
|
||||
if(props.isActive !== true) return null;
|
||||
const headlines = extractEvents(props.content);
|
||||
export class OrgEventsViewer extends React.Component {
|
||||
shouldComponentUpdate(nextProps){
|
||||
if(this.props.content !== nextProps.content) return true;
|
||||
if(this.props.isActive !== nextProps.isActive) return true;
|
||||
return false;
|
||||
}
|
||||
render(){
|
||||
const headlines = this.props.isActive ? extractEvents(this.props.content) : [];
|
||||
return (
|
||||
<OrgViewer title="Agenda" headlines={headlines} content={this.props.content} isActive={this.props.isActive}
|
||||
onQuit={this.props.onQuit} goTo={this.props.goTo} onUpdate={this.props.onUpdate} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<OrgViewer title="Agenda" headlines={headlines} content={props.content} isActive={props.isActive}
|
||||
onQuit={props.onQuit} goTo={props.goTo} onUpdate={props.onUpdate} />
|
||||
);
|
||||
};
|
||||
export class OrgTodosViewer extends React.Component {
|
||||
shouldComponentUpdate(nextProps){
|
||||
if(this.props.content !== nextProps.content) return true;
|
||||
if(this.props.isActive !== nextProps.isActive) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export const OrgTodosViewer = (props) => {
|
||||
if(props.isActive !== true) return null;
|
||||
const headlines = extractTodos(props.content);
|
||||
|
||||
return (
|
||||
<OrgViewer title="Todos" headlines={headlines} content={props.content} isActive={props.isActive}
|
||||
onQuit={props.onQuit} goTo={props.goTo} onUpdate={props.onUpdate} />
|
||||
);
|
||||
};
|
||||
render(){
|
||||
const headlines = this.props.isActive ? extractTodos(this.props.content) : [];
|
||||
return (
|
||||
<OrgViewer title="Todos" headlines={headlines} content={this.props.content} isActive={this.props.isActive}
|
||||
onQuit={this.props.onQuit} goTo={this.props.goTo} onUpdate={this.props.onUpdate} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -32,13 +45,18 @@ class OrgViewer extends React.Component {
|
|||
super(props);
|
||||
this.state = {
|
||||
headlines: this.buildHeadlines(props.headlines),
|
||||
content: props.content
|
||||
content: props.content,
|
||||
search: '',
|
||||
_: null
|
||||
};
|
||||
this.rerender = () => {this.setState({_: Math.random()});};
|
||||
this.findResults = debounce(this.findResults.bind(this), 150);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props){
|
||||
this.setState({
|
||||
headlines: this.buildHeadlines(props.headlines)
|
||||
headlines: this.buildHeadlines(props.headlines),
|
||||
content: props.content
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +80,7 @@ class OrgViewer extends React.Component {
|
|||
|
||||
onTaskUpdate(type, line, value){
|
||||
const content = this.state.content.split("\n");
|
||||
let head_line, item_line, head_status, deadline_line, scheduled_line, insertion_line;
|
||||
switch(type){
|
||||
case "status":
|
||||
content[line] = content[line].replace(/^(\*+\s)[A-Z]{3,}(\s.*)$/, "$1"+value+"$2");
|
||||
|
|
@ -73,12 +92,109 @@ class OrgViewer extends React.Component {
|
|||
content[line] = content[line].replace(/\[.\]/, '[ ]');
|
||||
}
|
||||
break;
|
||||
case "schedule":
|
||||
case "existing_scheduled":
|
||||
[head_line, head_status, item_line] = line;
|
||||
content[item_line] = content[item_line].replace(/SCHEDULED\: \<.*?\>\s*/, value ? "SCHEDULED: "+orgdate(value)+" " : "");
|
||||
this.state.headlines[head_status] = this.state.headlines[head_status]
|
||||
.map((todo) => {
|
||||
if(todo.line === head_line){
|
||||
if(value) todo.scheduled.timestamp = new Date(value).toISOString();
|
||||
else todo.scheduled = null;
|
||||
}
|
||||
return todo;
|
||||
});
|
||||
this.setState({headlines: this.state.headlines});
|
||||
break;
|
||||
case "deadline":
|
||||
case "existing_deadline":
|
||||
[head_line, head_status, item_line] = line;
|
||||
content[item_line] = content[item_line].replace(/DEADLINE\: \<.*?\>\s*/, value ? "DEADLINE: "+orgdate(value) : "");
|
||||
this.state.headlines[head_status] = this.state.headlines[head_status]
|
||||
.map((todo) => {
|
||||
if(todo.line === head_line){
|
||||
if(value) todo.deadline.timestamp = new Date(value).toISOString();
|
||||
else todo.deadline = null;
|
||||
}
|
||||
return todo;
|
||||
});
|
||||
this.setState({headlines: this.state.headlines});
|
||||
break;
|
||||
case "new_scheduled":
|
||||
[head_line, head_status, deadline_line] = line;
|
||||
if(deadline_line !== null){
|
||||
insertion_line = deadline_line;
|
||||
content[deadline_line] = "SCHEDULED: "+orgdate(value)+" "+content[deadline_line];
|
||||
}else{
|
||||
insertion_line = head_line + 1;
|
||||
if(content[insertion_line] === "" && content[insertion_line + 1] === ""){
|
||||
content[insertion_line] = "SCHEDULED: "+orgdate(value);
|
||||
}else{
|
||||
content.splice(
|
||||
insertion_line,
|
||||
0,
|
||||
"SCHEDULED: "+orgdate(value)
|
||||
);
|
||||
}
|
||||
}
|
||||
this.state.headlines[head_status] = this.state.headlines[head_status]
|
||||
.map((todo) => {
|
||||
if(todo.line === head_line){
|
||||
todo.scheduled = {
|
||||
line: insertion_line,
|
||||
keyword: "SCHEDULED",
|
||||
active: true,
|
||||
range: null,
|
||||
repeat: null,
|
||||
timestamp: new Date(value).toISOString()
|
||||
};
|
||||
}
|
||||
return todo;
|
||||
});
|
||||
this.setState({headlines: this.state.headlines});
|
||||
break;
|
||||
case "new_deadline":
|
||||
[head_line, head_status, scheduled_line] = line;
|
||||
if(scheduled_line !== null){
|
||||
insertion_line = scheduled_line;
|
||||
content[scheduled_line] = content[scheduled_line]+" DEADLINE: "+orgdate(value);
|
||||
}else{
|
||||
insertion_line = head_line + 1;
|
||||
if(content[insertion_line] === "" && content[insertion_line + 1] === ""){
|
||||
content[insertion_line] = "DEADLINE: "+orgdate(value);
|
||||
}else{
|
||||
content.splice(
|
||||
insertion_line,
|
||||
0,
|
||||
"DEADLINE: "+orgdate(value)
|
||||
);
|
||||
}
|
||||
this.state.headlines[head_status] = this.state.headlines[head_status]
|
||||
.map((todo) => {
|
||||
if(todo.line === head_line){
|
||||
todo.deadline = {
|
||||
line: insertion_line,
|
||||
keyword: "DEADLINE",
|
||||
active: true,
|
||||
range: null,
|
||||
repeat: null,
|
||||
timestamp: new Date(value).toISOString()
|
||||
};
|
||||
}
|
||||
return todo;
|
||||
});
|
||||
this.setState({headlines: this.state.headlines});
|
||||
}
|
||||
break;
|
||||
};
|
||||
this.setState({content: content.join("\n")});
|
||||
|
||||
function orgdate(_date){
|
||||
const date = new Date(_date);
|
||||
return "<"+date.getFullYear()+"-"+leftPad((date.getMonth() + 1).toString(), 2)+"-"+leftPad(date.getDate().toString(), 2)+" "+day(date.getDay())+">";
|
||||
|
||||
function day(n){
|
||||
return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][n];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
navigate(line){
|
||||
|
|
@ -91,6 +207,41 @@ class OrgViewer extends React.Component {
|
|||
this.props.onQuit();
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
window.addEventListener('resize', this.rerender);
|
||||
}
|
||||
componentWillUnmount(){
|
||||
window.removeEventListener('resize', this.rerender);
|
||||
}
|
||||
|
||||
search(terms){
|
||||
this.setState({search: terms}, () => {
|
||||
this.findResults(terms);
|
||||
});
|
||||
}
|
||||
|
||||
findResults(terms){
|
||||
let headlines = this.props.headlines;
|
||||
if(terms){
|
||||
headlines = this.props.headlines.filter((headline) => {
|
||||
const keywords = terms.split(" ");
|
||||
const head = function(){
|
||||
let str = " ";
|
||||
str += headline['status'] + " ";
|
||||
str += headline['title'] + " ";
|
||||
str += headline.tags.map((tag) => "#"+tag).join(" ") + " ";
|
||||
str += headline.scheduled ? "scheduled "+headline.scheduled.timestamp + " ": "";
|
||||
str += headline.deadline ? "deadline "+headline.deadline.timestamp + " ": "";
|
||||
str += headline.priority ? "priority #"+headline.priority+" " : "";
|
||||
str += headline.is_overdue ? "overdue " : "";
|
||||
str += headline.tasks.map((task) => task.title).join(" ")+ " ";
|
||||
return str;
|
||||
}(headline);
|
||||
return keywords.filter((keyword) => new RegExp(" "+keyword, "i").test(head)).length === keywords.length ? true : false;
|
||||
});
|
||||
}
|
||||
this.setState({headlines: this.buildHeadlines(headlines)});
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
|
|
@ -100,6 +251,12 @@ class OrgViewer extends React.Component {
|
|||
<Icon name="close"/>
|
||||
</span>
|
||||
<h1>{this.props.title}</h1>
|
||||
<NgIf className="search" cond={this.props.headlines.length > 0}>
|
||||
<label className={this.state.search.length > 0 ? "active" : ""}>
|
||||
<input type="text" onChange={(e) => this.search(e.target.value)} placeholder="Search ..."/>
|
||||
<Icon name="search" />
|
||||
</label>
|
||||
</NgIf>
|
||||
</div>
|
||||
<NgIf cond={this.props.headlines.length === 0} className="nothing">
|
||||
Nothing
|
||||
|
|
@ -133,6 +290,11 @@ class OrgViewer extends React.Component {
|
|||
key={j} title={headline.title}
|
||||
tags={headline.tags}
|
||||
line={headline.line}
|
||||
date={headline.date}
|
||||
overdue={headline.is_overdue}
|
||||
scheduled={headline.scheduled}
|
||||
deadline={headline.deadline}
|
||||
sortKey={headline.key}
|
||||
status={headline.status || null}
|
||||
todo_status={headline.todo_status}
|
||||
todo_priority={headline.priority}
|
||||
|
|
@ -183,13 +345,49 @@ class Headline extends React.Component {
|
|||
this.props.onTaskUpdate('status', this.props.line, new_status_label);
|
||||
}
|
||||
|
||||
onTimeSet(keyword, existing, value){
|
||||
if(existing === true){
|
||||
this.props.onTaskUpdate(
|
||||
"existing_"+keyword,
|
||||
[
|
||||
this.props.line,
|
||||
this.props.sortKey,
|
||||
this.props[keyword].line,
|
||||
],
|
||||
value
|
||||
);
|
||||
}else{
|
||||
const opposite_keyword = keyword === "scheduled" ? "deadline" : "scheduled";
|
||||
this.props.onTaskUpdate(
|
||||
"new_"+keyword,
|
||||
[
|
||||
this.props.line,
|
||||
this.props.sortKey,
|
||||
this.props[opposite_keyword] && this.props[opposite_keyword].line || null
|
||||
],
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
const dateInput = (obj) => {
|
||||
if(!obj || !obj.timestamp) return "";
|
||||
const d = new Date(obj.timestamp);
|
||||
return d.getFullYear()+"-"+leftPad((d.getMonth() + 1).toString(), 2)+"-"+leftPad(d.getDate().toString(), 2);
|
||||
};
|
||||
return (
|
||||
<div className="component_headline">
|
||||
<div className={"no-select headline-main "+this.state.status + " " +(this.props.is_overdue === true ? "overdue" : "")}>
|
||||
<div className={"no-select headline-main "+(this.state.status || "")}>
|
||||
<div className="title" onClick={this.onStatusToggle.bind(this)}>
|
||||
<div>
|
||||
<span>{this.props.title}</span>
|
||||
<div className={(this.props.todo_priority? this.props.todo_priority + " " : "")+(this.props.overdue === true ? "overdue" : "")}>
|
||||
<span className="label">{this.props.title}</span>
|
||||
<NgIf cond={this.props.scheduled !== null && this.props.scheduled.timestamp === this.props.date} type="inline">
|
||||
<Icon name="schedule" />
|
||||
</NgIf>
|
||||
<NgIf cond={this.props.deadline !== null && this.props.deadline.timestamp === this.props.date} type="inline">
|
||||
<Icon name="deadline" />
|
||||
</NgIf>
|
||||
<div className="tags">
|
||||
{
|
||||
this.props.tags.map((tag, i) => {
|
||||
|
|
@ -207,18 +405,29 @@ class Headline extends React.Component {
|
|||
</DropdownButton>
|
||||
<DropdownList>
|
||||
<DropdownItem name="navigate" icon="arrow_right"> Navigate </DropdownItem>
|
||||
<DropdownItem name="properties"> Properties </DropdownItem>
|
||||
</DropdownList>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<NgIf className="headline-properties" cond={this.state.properties}>
|
||||
<div>
|
||||
<label> <Icon name="schedule" />
|
||||
<input type="date" onChange={(e) => this.props.onTaskUpdate.bind(this, 'schedule', this.props.line, e.target.value)}/>
|
||||
<NgIf cond={this.props.scheduled !== null}>
|
||||
<input type="date" value={dateInput(this.props.scheduled)} onChange={(e) => this.onTimeSet('scheduled', true, e.target.value)}/>
|
||||
</NgIf>
|
||||
<NgIf cond={this.props.scheduled === null}>
|
||||
<input type="date" onChange={(e) => this.onTimeSet('scheduled', false, e.target.value)}/>
|
||||
</NgIf>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label> <Icon name="deadline" />
|
||||
<input type="date" onChange={(e) => this.props.onTaskUpdate.bind(this, 'deadline', this.props.line, e.target.value)}/>
|
||||
<NgIf cond={this.props.deadline !== null}>
|
||||
<input type="date" value={dateInput(this.props.deadline)} onChange={(e) => this.onTimeSet('deadline', true, e.target.value)}/>
|
||||
</NgIf>
|
||||
<NgIf cond={this.props.deadline === null}>
|
||||
<input type="date" onChange={(e) => this.onTimeSet('deadline', false, e.target.value)}/>
|
||||
</NgIf>
|
||||
</label>
|
||||
</div>
|
||||
</NgIf>
|
||||
|
|
|
|||
|
|
@ -11,20 +11,47 @@
|
|||
}
|
||||
.modal-top{
|
||||
margin: -5px -5px 0 -5px;
|
||||
padding: 30px 20px;
|
||||
padding: 30px 0px 0 30px;
|
||||
background: var(--primary);
|
||||
border-bottom: 1px solid var(--emphasis-primary);
|
||||
color: rgba(0,0,0,0.5);
|
||||
min-height: 65px;
|
||||
h1{margin: 0;}
|
||||
span{
|
||||
float: right;
|
||||
margin-top: -10px;
|
||||
.component_icon{
|
||||
padding: 2px 0;
|
||||
margin-top: -10px;
|
||||
padding: 10px 15px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.search{
|
||||
text-align: right;
|
||||
label{
|
||||
background: white;
|
||||
margin-bottom: -1px;
|
||||
border-top-left-radius: 7px;
|
||||
&.active input, &:hover input{width: 150px; padding: 2px 7px; transition: width 0.2s ease-out;}
|
||||
display: inline-block;
|
||||
input{
|
||||
transition: width 0.1s ease-out;
|
||||
width: 0px;
|
||||
background: inherit;
|
||||
border: none;
|
||||
border-top-left-radius: 7px;
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
color: var(--emphasis-secondary);
|
||||
line-height: 25px;
|
||||
&::-webkit-input-placeholder{ color: var(--emphasis-secondary); }
|
||||
&:-ms-input-placeholder{ color: var(--emphasis-secondary); }
|
||||
&::-moz-input-placeholder{ color: var(--emphasis-secondary); }
|
||||
}
|
||||
.component_icon{ height: 20px; padding: 5px; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nothing{
|
||||
|
|
@ -40,7 +67,7 @@
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 0 10px;
|
||||
padding: 0 10px 70px 10px;
|
||||
|
||||
.sticky_header{
|
||||
z-index: 2;
|
||||
|
|
@ -95,17 +122,30 @@
|
|||
color: var(--light);
|
||||
&:hover{background: inherit;}
|
||||
transition: color 0.35s ease-out;
|
||||
.title{ > div{border-color: rgba(0,0,0,0)!important;}}
|
||||
}
|
||||
|
||||
.title{
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
line-height: 32px;
|
||||
.label{margin-right: 10px;}
|
||||
> div{
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: border-color 0.2s ease-out;
|
||||
|
||||
border-left: 5px solid rgba(0,0,0,0);
|
||||
padding-left: 5px;
|
||||
&.A, &.overdue{border-color: var(--error);}
|
||||
&.B:not(.overdue){border-color: #FF8355;}
|
||||
&.C:not(.overdue){border-color: var(--bg-color);}
|
||||
}
|
||||
.component_icon{
|
||||
height: 17px;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
.tags{
|
||||
display: inline-block;
|
||||
|
|
@ -156,8 +196,16 @@
|
|||
}
|
||||
|
||||
.headline-properties{
|
||||
> div{display: inline-block;}
|
||||
> div{
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
@media screen and (max-width: 440px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="date"] {
|
||||
width: calc(100% - 26px);
|
||||
background: inherit;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
|
|
@ -165,9 +213,10 @@
|
|||
color: inherit;
|
||||
}
|
||||
.component_icon{
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
background: var(--super-light);
|
||||
color: var(--light);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
module.exports = {
|
||||
god_editor_mode: true,
|
||||
fork_button: false,
|
||||
fork_button: true,
|
||||
search: {
|
||||
enable: true,
|
||||
max_depth: 15,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,14 @@ app.get('/cat', function(req, res){
|
|||
if(path){
|
||||
Files.cat(path, req.cookies.auth, res)
|
||||
.then(function(stream){
|
||||
stream = stream.on('error', function (error) {
|
||||
let status = 404;
|
||||
if(typeof (error && error.status === "number")){
|
||||
status = error.status;
|
||||
}
|
||||
res.status(status).send({status: status, message: "There's nothing here"});
|
||||
this.end();
|
||||
});
|
||||
res.set('Content-Type', mime.getMimeType(path));
|
||||
stream.pipe(res);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ app.post('/', function(req, res){
|
|||
};
|
||||
const cookie = crypto.encrypt(persist);
|
||||
if(Buffer.byteLength(cookie, 'utf-8') > 4096){
|
||||
res.status(413).send({status: 'error', message: 'we can\'t authenticate you', })
|
||||
res.status(413).send({status: 'error', message: 'we can\'t authenticate you'})
|
||||
}else{
|
||||
res.cookie('auth', crypto.encrypt(persist), { maxAge: 365*24*60*60*1000, httpOnly: true, path: "/api/" });
|
||||
res.send({status: 'ok'});
|
||||
|
|
|
|||
|
|
@ -49,13 +49,13 @@ module.exports = {
|
|||
.then(() => Promise.resolve(params))
|
||||
},
|
||||
cat: function(path, params){
|
||||
return connect(params)
|
||||
return connect(params)
|
||||
.then((c) => {
|
||||
return new Promise((done, err) => {
|
||||
c.get(path, function(error, stream) {
|
||||
if (error){ err(error); }
|
||||
else{ done(stream); }
|
||||
});
|
||||
else{ done(stream); }
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue