feature (orgmode): making org mode awesome from a browser

This commit is contained in:
Mickael KERJEAN 2018-05-02 20:55:43 +10:00
parent db44e0b7e3
commit 8b313c6bef
39 changed files with 866 additions and 255 deletions

View file

@ -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

View file

@ -1,3 +1,5 @@
@import url('https://fonts.googleapis.com/css?family=Source+Code+Pro:400,600');
:root {
--bg-color: #f2f2f2;
--color: #626469;

View 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

View file

@ -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

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 512 512"
version="1.1"
id="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

View file

@ -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 {

View file

@ -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{

View file

@ -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');
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -24,5 +24,5 @@ export class NgIf extends React.Component {
}
NgIf.propTypes = {
cond: PropTypes.bool.isRequired
cond: PropTypes.bool.isRequired
};

View file

@ -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);
}
}

View file

@ -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'});
}

View file

@ -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
View 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);
}

View file

@ -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';

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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) => {

View file

@ -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>

View file

@ -1,4 +1,5 @@
.component_page_notfound{
.error{
width: 80%;
max-width: 600px;
margin: 50px auto 0 auto;

View file

@ -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>

View file

@ -22,4 +22,7 @@
overflow-y: scroll!important;
overflow-x: hidden!important;
-webkit-overflow-scrolling: touch;
> .container{
height: 100%;
}
}

View file

@ -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){

View file

@ -20,6 +20,7 @@
}
.component_thing{
clear: both;
&:hover .box, .highlight.box{
background: var(--super-light);
border-color: var(--super-light);

View file

@ -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>

View file

@ -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>
);

View file

@ -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){

View 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;
}

View file

@ -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++){

View file

@ -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;

View file

@ -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>
);
}

View file

@ -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;

View file

@ -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>

View file

@ -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);

View file

@ -1,6 +1,6 @@
module.exports = {
god_editor_mode: true,
fork_button: false,
fork_button: true,
search: {
enable: true,
max_depth: 15,

View file

@ -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);
})

View file

@ -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'});

View file

@ -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); }
});
});
});
},