Compare commits
26 Commits
v0.5
...
01adec7db5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01adec7db5 | ||
|
|
95b10fd9f6 | ||
|
|
fc088ec240 | ||
|
|
2763b68bc4 | ||
|
|
f84614a8da | ||
|
|
c2ed541604 | ||
|
|
8c7fe2e9ef | ||
|
|
9301ddd467 | ||
|
|
d5ec073f75 | ||
|
|
b8f0efc01c | ||
|
|
81c8743de7 | ||
|
|
6abfab229a | ||
|
|
ec780f7d9b | ||
|
|
94a14f8b9d | ||
|
|
90dbfd6f5a | ||
|
|
de5f17e2c9 | ||
|
|
0e22586e12 | ||
|
|
80becbb7a7 | ||
|
|
2e98bdff2d | ||
|
|
8f966c98a4 | ||
|
|
a0f336ca67 | ||
|
|
f8b5eb71e0 | ||
|
|
7c70ba27cb | ||
|
|
683b7a5f2d | ||
|
|
a77f28fbcf | ||
|
|
6291742690 |
@@ -5,16 +5,18 @@ import (
|
|||||||
"local/args"
|
"local/args"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Port string
|
Port string
|
||||||
StoreType string
|
StoreType string
|
||||||
StoreAddr string
|
StoreAddr string
|
||||||
StoreUser string
|
StoreUser string
|
||||||
StorePass string
|
StorePass string
|
||||||
Root string
|
Root string
|
||||||
MyTinyTodo string
|
OAuth string
|
||||||
|
Loop time.Duration
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -32,8 +34,9 @@ func Refresh() {
|
|||||||
as.Append(args.STRING, "storeaddr", "addr of store", "")
|
as.Append(args.STRING, "storeaddr", "addr of store", "")
|
||||||
as.Append(args.STRING, "storeuser", "user of store", "")
|
as.Append(args.STRING, "storeuser", "user of store", "")
|
||||||
as.Append(args.STRING, "storepass", "pass of store", "")
|
as.Append(args.STRING, "storepass", "pass of store", "")
|
||||||
as.Append(args.STRING, "mtt", "url of php server", "http://localhost:38808")
|
as.Append(args.STRING, "oauth", "url for boauthz", "")
|
||||||
as.Append(args.STRING, "root", "root of static files", "./public")
|
as.Append(args.STRING, "root", "root of static files", "./public")
|
||||||
|
as.Append(args.DURATION, "loop", "loop duration for refreshing completed tasks", time.Minute)
|
||||||
if err := as.Parse(); err != nil {
|
if err := as.Parse(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -44,5 +47,6 @@ func Refresh() {
|
|||||||
StoreUser = as.Get("storeuser").GetString()
|
StoreUser = as.Get("storeuser").GetString()
|
||||||
StorePass = as.Get("storepass").GetString()
|
StorePass = as.Get("storepass").GetString()
|
||||||
Root = as.Get("root").GetString()
|
Root = as.Get("root").GetString()
|
||||||
MyTinyTodo = as.Get("mtt").GetString()
|
Loop = as.Get("loop").GetDuration()
|
||||||
|
OAuth = as.Get("oauth").GetString()
|
||||||
}
|
}
|
||||||
|
|||||||
1
main.go
1
main.go
@@ -12,6 +12,7 @@ func main() {
|
|||||||
if err := s.Routes(); err != nil {
|
if err := s.Routes(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
go s.Async()
|
||||||
log.Println("listening on", config.Port)
|
log.Println("listening on", config.Port)
|
||||||
if err := http.ListenAndServe(config.Port, s); err != nil {
|
if err := http.ListenAndServe(config.Port, s); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
<title>todo breel</title>
|
<title>todo breel</title>
|
||||||
<link rel="stylesheet" type="text/css" href="/themes/default/style.css?v=1.4.3" media="all"/>
|
<link rel="stylesheet" type="text/css" href="/themes/default/style.css?v=1.4.3" media="all"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/themes/default/print.css?v=1.4.3" media="print"/>
|
<link rel="stylesheet" type="text/css" href="/themes/default/print.css?v=1.4.3" media="print"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/themes/default/pda.css?v=1.4.3" media="only screen and (max-device-width: 720px)"/>
|
||||||
|
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -23,7 +25,7 @@
|
|||||||
window.location.href += '/?pda';
|
window.location.href += '/?pda';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
mytinytodo.mttUrl = "/";
|
mytinytodo.mttUrl = "/";
|
||||||
mytinytodo.templateUrl = "/themes/default/";
|
mytinytodo.templateUrl = "/themes/default/";
|
||||||
mytinytodo.db = new mytinytodoStorageAjax(mytinytodo);
|
mytinytodo.db = new mytinytodoStorageAjax(mytinytodo);
|
||||||
@@ -166,6 +168,14 @@
|
|||||||
<span class="h">Due </span>
|
<span class="h">Due </span>
|
||||||
<input name="duedate" id="duedate" value="" class="in100" title="Y-M-D, M/D/Y, D.M.Y, M/D, D.M" autocomplete="off"/>
|
<input name="duedate" id="duedate" value="" class="in100" title="Y-M-D, M/D/Y, D.M.Y, M/D, D.M" autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-row form-row-short">
|
||||||
|
<span class="h">Loop </span>
|
||||||
|
<input type="text" name="loop" value="" class="in100" maxlength="30"/>
|
||||||
|
</div>
|
||||||
|
<div class="form-row form-row-short">
|
||||||
|
<span class="h">Cron </span>
|
||||||
|
<input type="text" name="cron" value="" class="in100" maxlength="30"/>
|
||||||
|
</div>
|
||||||
<div class="form-row-short-end"></div>
|
<div class="form-row-short-end"></div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="h">Task</div>
|
<div class="h">Task</div>
|
||||||
@@ -255,6 +265,8 @@
|
|||||||
<li class="mtt-menu-delimiter"></li>
|
<li class="mtt-menu-delimiter"></li>
|
||||||
<li class="mtt-need-list" id="btnShowCompleted">
|
<li class="mtt-need-list" id="btnShowCompleted">
|
||||||
<div class="menu-icon"></div>Show completed tasks</li>
|
<div class="menu-icon"></div>Show completed tasks</li>
|
||||||
|
<li class="mtt-need-list" id="btnShowLooping">
|
||||||
|
<div class="menu-icon"></div>Show looping tasks</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function getGetParamValue(url, paramName) {
|
|||||||
this._lists = {};
|
this._lists = {};
|
||||||
this._length = 0;
|
this._length = 0;
|
||||||
this._order = [];
|
this._order = [];
|
||||||
this._alltasks = {id: -1, showCompl: 0, sort: 3};
|
this._alltasks = {id: -1, showCompl: 0, showLooping: 0, sort: 3};
|
||||||
},
|
},
|
||||||
length: function () {
|
length: function () {
|
||||||
return this._length;
|
return this._length;
|
||||||
@@ -851,6 +851,7 @@ function getGetParamValue(url, paramName) {
|
|||||||
_mtt.db.request('loadTasks', {
|
_mtt.db.request('loadTasks', {
|
||||||
list: curList.id,
|
list: curList.id,
|
||||||
compl: curList.showCompl,
|
compl: curList.showCompl,
|
||||||
|
looping: curList.showLooping,
|
||||||
sort: curList.sort,
|
sort: curList.sort,
|
||||||
search: filter.search,
|
search: filter.search,
|
||||||
tag: _mtt.filter.getTags(true),
|
tag: _mtt.filter.getTags(true),
|
||||||
@@ -1233,6 +1234,9 @@ function getGetParamValue(url, paramName) {
|
|||||||
case 'btnRssFeed':
|
case 'btnRssFeed':
|
||||||
feedCurList();
|
feedCurList();
|
||||||
break;
|
break;
|
||||||
|
case 'btnShowLooping':
|
||||||
|
showLoopingToggle();
|
||||||
|
break;
|
||||||
case 'btnShowCompleted':
|
case 'btnShowCompleted':
|
||||||
showCompletedToggle();
|
showCompletedToggle();
|
||||||
break;
|
break;
|
||||||
@@ -1354,6 +1358,8 @@ function getGetParamValue(url, paramName) {
|
|||||||
form.tags.value = item.tags.split(',').join(', ');
|
form.tags.value = item.tags.split(',').join(', ');
|
||||||
form.duedate.value = item.duedate;
|
form.duedate.value = item.duedate;
|
||||||
form.prio.value = item.prio;
|
form.prio.value = item.prio;
|
||||||
|
form.loop.value = item.loop;
|
||||||
|
form.cron.value = item.cron;
|
||||||
$('#taskedit-date .date-created>span').text(item.date);
|
$('#taskedit-date .date-created>span').text(item.date);
|
||||||
if (item.compl) $('#taskedit-date .date-completed').show().find('span').text(item.dateCompleted);
|
if (item.compl) $('#taskedit-date .date-completed').show().find('span').text(item.dateCompleted);
|
||||||
else $('#taskedit-date .date-completed').hide();
|
else $('#taskedit-date .date-completed').hide();
|
||||||
@@ -1370,6 +1376,8 @@ function getGetParamValue(url, paramName) {
|
|||||||
form.duedate.value = '';
|
form.duedate.value = '';
|
||||||
form.prio.value = '0';
|
form.prio.value = '0';
|
||||||
form.id.value = '';
|
form.id.value = '';
|
||||||
|
form.loop.value = '';
|
||||||
|
form.cron.value = '';
|
||||||
toggleEditAllTags(0);
|
toggleEditAllTags(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1408,11 +1416,17 @@ function getGetParamValue(url, paramName) {
|
|||||||
if (flag.readOnly) return false;
|
if (flag.readOnly) return false;
|
||||||
if (form.isadd.value != 0)
|
if (form.isadd.value != 0)
|
||||||
return submitFullTask(form);
|
return submitFullTask(form);
|
||||||
|
var requestBody = {
|
||||||
_mtt.db.request('editTask', {
|
id: form.id.value,
|
||||||
id: form.id.value, title: form.task.value, note: form.note.value,
|
title: form.task.value,
|
||||||
prio: form.prio.value, tags: form.tags.value, duedate: form.duedate.value
|
note: form.note.value,
|
||||||
},
|
prio: form.prio.value,
|
||||||
|
tags: form.tags.value,
|
||||||
|
duedate: form.duedate.value,
|
||||||
|
loop: form.loop.value,
|
||||||
|
cron: form.cron.value,
|
||||||
|
}
|
||||||
|
_mtt.db.request('editTask', requestBody,
|
||||||
function (json) {
|
function (json) {
|
||||||
if (!parseInt(json.total)) return;
|
if (!parseInt(json.total)) return;
|
||||||
var item = json.list[0];
|
var item = json.list[0];
|
||||||
@@ -1531,7 +1545,9 @@ function getGetParamValue(url, paramName) {
|
|||||||
note: form.note.value,
|
note: form.note.value,
|
||||||
prio: form.prio.value,
|
prio: form.prio.value,
|
||||||
tags: form.tags.value,
|
tags: form.tags.value,
|
||||||
duedate: form.duedate.value
|
duedate: form.duedate.value,
|
||||||
|
loop: form.loop.value,
|
||||||
|
cron: form.cron.value,
|
||||||
}, function (json) {
|
}, function (json) {
|
||||||
if (!parseInt(json.total)) return;
|
if (!parseInt(json.total)) return;
|
||||||
form.task.value = '';
|
form.task.value = '';
|
||||||
@@ -1880,6 +1896,8 @@ function getGetParamValue(url, paramName) {
|
|||||||
}
|
}
|
||||||
if (list.showCompl) $('#btnShowCompleted').addClass('mtt-item-checked');
|
if (list.showCompl) $('#btnShowCompleted').addClass('mtt-item-checked');
|
||||||
else $('#btnShowCompleted').removeClass('mtt-item-checked');
|
else $('#btnShowCompleted').removeClass('mtt-item-checked');
|
||||||
|
if (list.showLooping) $('#btnShowLooping').addClass('mtt-item-checked');
|
||||||
|
else $('#btnShowLooping').removeClass('mtt-item-checked');
|
||||||
}
|
}
|
||||||
|
|
||||||
function listOrderChanged(event, ui) {
|
function listOrderChanged(event, ui) {
|
||||||
@@ -1893,6 +1911,14 @@ function getGetParamValue(url, paramName) {
|
|||||||
_mtt.doAction('listOrderChanged', {order: order});
|
_mtt.doAction('listOrderChanged', {order: order});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showLoopingToggle() { // todo
|
||||||
|
var act = curList.showLooping ? 0 : 1;
|
||||||
|
curList.showLooping = tabLists.get(curList.id).showLooping = act;
|
||||||
|
if (act) $('#btnShowLooping').addClass('mtt-item-checked');
|
||||||
|
else $('#btnShowLooping').removeClass('mtt-item-checked');
|
||||||
|
loadTasks({setLooping: 1});
|
||||||
|
}
|
||||||
|
|
||||||
function showCompletedToggle() {
|
function showCompletedToggle() {
|
||||||
var act = curList.showCompl ? 0 : 1;
|
var act = curList.showCompl ? 0 : 1;
|
||||||
curList.showCompl = tabLists.get(curList.id).showCompl = act;
|
curList.showCompl = tabLists.get(curList.id).showCompl = act;
|
||||||
@@ -2196,4 +2222,4 @@ function getGetParamValue(url, paramName) {
|
|||||||
}, 'json');
|
}, 'json');
|
||||||
}
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
})
|
})
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$.getJSON(this.mtt.mttUrl + 'ajax.php?loadTasks&list=' + params.list + '&compl=' + params.compl + '&sort=' + params.sort + q, callback);
|
$.getJSON(this.mtt.mttUrl + 'ajax.php?loadTasks&list=' + params.list + '&compl=' + params.compl + '&looping=' + params.looping + '&sort=' + params.sort + q, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +64,9 @@
|
|||||||
note: params.note,
|
note: params.note,
|
||||||
prio: params.prio,
|
prio: params.prio,
|
||||||
tags: params.tags,
|
tags: params.tags,
|
||||||
duedate: params.duedate
|
duedate: params.duedate,
|
||||||
|
loop: params.loop,
|
||||||
|
cron: params.cron,
|
||||||
},
|
},
|
||||||
callback, 'json');
|
callback, 'json');
|
||||||
},
|
},
|
||||||
@@ -78,7 +80,9 @@
|
|||||||
note: params.note,
|
note: params.note,
|
||||||
prio: params.prio,
|
prio: params.prio,
|
||||||
tags: params.tags,
|
tags: params.tags,
|
||||||
duedate: params.duedate
|
duedate: params.duedate,
|
||||||
|
loop: params.loop,
|
||||||
|
cron: params.cron,
|
||||||
},
|
},
|
||||||
callback, 'json');
|
callback, 'json');
|
||||||
},
|
},
|
||||||
@@ -194,4 +198,4 @@
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -188,3 +188,34 @@ h3 {
|
|||||||
.mtt-notes-showhide {
|
.mtt-notes-showhide {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mtt-tab {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mtt-tabs-selected {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BEL */
|
||||||
|
|
||||||
|
#taskview ,
|
||||||
|
#bar ,
|
||||||
|
br[clear="all"] ,
|
||||||
|
#task_placeholder > span ,
|
||||||
|
#mtt_body > h2:first-child {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
#toolbar ,
|
||||||
|
#taskcontainer {
|
||||||
|
z-index: 15;
|
||||||
|
}
|
||||||
|
#taskcontainer {
|
||||||
|
position: fixed;
|
||||||
|
top: 6em;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -605,6 +605,7 @@ li.task-expanded .task-toggle {
|
|||||||
.task-middle {
|
.task-middle {
|
||||||
margin-left: 40px;
|
margin-left: 40px;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
|
padding-right: 2.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tasklist {
|
#tasklist {
|
||||||
@@ -793,6 +794,7 @@ li:hover a.taskactionbtn, a.taskactionbtn.mtt-menu-button-active {
|
|||||||
min-height: 16px;
|
min-height: 16px;
|
||||||
display: none;
|
display: none;
|
||||||
margin: .7em .5em 0 0;
|
margin: .7em .5em 0 0;
|
||||||
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.task-expanded .task-note-block {
|
li.task-expanded .task-note-block {
|
||||||
@@ -1366,3 +1368,8 @@ li.mtt-item-hidden {
|
|||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
}
|
}
|
||||||
body { filter: invert(80%); background-color: #222; }
|
body { filter: invert(80%); background-color: #222; }
|
||||||
|
#newtask_adv ,
|
||||||
|
#tagcloudbtn ,
|
||||||
|
#settings {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ package ajax
|
|||||||
import (
|
import (
|
||||||
"local/storage"
|
"local/storage"
|
||||||
"local/todo-server/config"
|
"local/todo-server/config"
|
||||||
|
"local/todo-server/server/ajax/task"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Ajax struct {
|
type Ajax struct {
|
||||||
@@ -82,3 +85,78 @@ func has(params url.Values, k string) bool {
|
|||||||
_, ok := params[k]
|
_, ok := params[k]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Ajax) Async() {
|
||||||
|
var err error
|
||||||
|
nextDue := time.Now().Add(config.Loop)
|
||||||
|
c := time.NewTicker(config.Loop)
|
||||||
|
c2 := time.NewTicker(config.Loop)
|
||||||
|
for {
|
||||||
|
c.Stop()
|
||||||
|
c.Reset(config.Loop)
|
||||||
|
c2.Stop()
|
||||||
|
c2.Reset(time.Until(nextDue))
|
||||||
|
log.Println("next loop at", time.Until(nextDue), "or", config.Loop)
|
||||||
|
select {
|
||||||
|
case <-c.C:
|
||||||
|
case <-c2.C:
|
||||||
|
}
|
||||||
|
nextDue, err = a.loopTasks()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("failed to loop tasks", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Ajax) loopTasks() (time.Time, error) {
|
||||||
|
nextDue := time.Now().Add(time.Hour * 240)
|
||||||
|
lists, err := a.storageListLists()
|
||||||
|
if err != nil {
|
||||||
|
return nextDue, err
|
||||||
|
}
|
||||||
|
for _, list := range lists {
|
||||||
|
tasks, err := a.storageListTasks(list.UUID, func(t *task.Task) bool {
|
||||||
|
return t.Complete
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nextDue, err
|
||||||
|
}
|
||||||
|
for _, task := range tasks {
|
||||||
|
if !task.Complete {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if task.Loop == 0 && task.Cron == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var nextTask time.Time
|
||||||
|
if task.Loop > 0 {
|
||||||
|
nextTask = task.Completed.Add(task.Loop)
|
||||||
|
}
|
||||||
|
if string(task.Cron) != "" {
|
||||||
|
nextTask2 := task.Cron.Next(task.Completed)
|
||||||
|
if nextTask2 != (time.Time{}) && (nextTask == (time.Time{}) || nextTask2.Before(nextTask)) {
|
||||||
|
nextTask = nextTask2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if time.Now().After(nextTask) {
|
||||||
|
task.Complete = false
|
||||||
|
task.Completed = time.Time{}
|
||||||
|
if nextTask == (time.Time{}) {
|
||||||
|
task.Title += " !!!INVALID CRON/LOOP!!!"
|
||||||
|
}
|
||||||
|
task.Index = list.NextIndex()
|
||||||
|
if err := a.storageSetTask(list.UUID, task); err != nil {
|
||||||
|
return nextDue, err
|
||||||
|
}
|
||||||
|
if err := a.storageSetList(list); err != nil {
|
||||||
|
return nextDue, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if nextTask.Before(nextDue) {
|
||||||
|
nextDue = nextTask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextDue, nil
|
||||||
|
}
|
||||||
|
|||||||
19
server/ajax/form/cron.go
Normal file
19
server/ajax/form/cron.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package form
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cron string
|
||||||
|
|
||||||
|
func (c Cron) Next(since time.Time) time.Time {
|
||||||
|
schedule, err := cron.ParseStandard(string(c))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to parse cron %q: %v", string(c), err)
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return schedule.Next(since)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -60,6 +61,38 @@ func ToStrArr(k string) []string {
|
|||||||
return outArr
|
return outArr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToDuration(s string) time.Duration {
|
||||||
|
var d time.Duration
|
||||||
|
for _, c := range []struct {
|
||||||
|
key string
|
||||||
|
d time.Duration
|
||||||
|
}{
|
||||||
|
{"d", time.Hour * 24},
|
||||||
|
{"w", time.Hour * 24 * 7},
|
||||||
|
} {
|
||||||
|
daysPattern := regexp.MustCompile(`(^|[a-z])[0-9]+` + c.key + `($|[0-9])`)
|
||||||
|
idxes := daysPattern.FindAllStringIndex(s, -1)
|
||||||
|
if len(idxes) > 1 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for _, idx := range idxes {
|
||||||
|
substr := s[idx[0]:idx[1]]
|
||||||
|
for len(substr) > 0 && (substr[0] < '0' || substr[0] > '9') {
|
||||||
|
substr = substr[1:]
|
||||||
|
}
|
||||||
|
for len(substr) > 0 && (substr[len(substr)-1] >= '0' && substr[len(substr)-1] <= '9') {
|
||||||
|
substr = substr[:len(substr)-1]
|
||||||
|
}
|
||||||
|
s = strings.ReplaceAll(s, substr, "")
|
||||||
|
substr = strings.TrimSuffix(substr, c.key)
|
||||||
|
n, _ := strconv.Atoi(substr)
|
||||||
|
d += c.d * time.Duration(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d2, _ := time.ParseDuration(s)
|
||||||
|
return d2 + d
|
||||||
|
}
|
||||||
|
|
||||||
func ToTime(s string) time.Time {
|
func ToTime(s string) time.Time {
|
||||||
v, err := time.Parse("2006-01-02 15:04:05", s)
|
v, err := time.Parse("2006-01-02 15:04:05", s)
|
||||||
if err != nil || v.IsZero() {
|
if err != nil || v.IsZero() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
func TestGet(t *testing.T) {
|
||||||
@@ -142,3 +143,29 @@ func testReq() *http.Request {
|
|||||||
"d": "e, f,g"
|
"d": "e, f,g"
|
||||||
}`))
|
}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToDuration(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
input string
|
||||||
|
want time.Duration
|
||||||
|
}{
|
||||||
|
"invalid": {},
|
||||||
|
"simple": {input: "1s", want: time.Second},
|
||||||
|
"compound": {input: "1m1s", want: time.Minute + time.Second},
|
||||||
|
"compound:unsorted": {input: "1s1m", want: time.Minute + time.Second},
|
||||||
|
"compound:extension": {input: "1d1m", want: time.Minute + time.Hour*24},
|
||||||
|
"extension:day": {input: "1d", want: 24 * time.Hour},
|
||||||
|
"extension:week": {input: "1w", want: 24 * 7 * time.Hour},
|
||||||
|
"extension:week,day": {input: "1w1d", want: 24 * 8 * time.Hour},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got := ToDuration(c.input)
|
||||||
|
if got != c.want {
|
||||||
|
t.Fatal(c.input, c.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,15 +7,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type List struct {
|
type List struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
UUID string `json:"id"`
|
UUID string `json:"id"`
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
Published int `json:"published"`
|
Published int `json:"published"`
|
||||||
ShowCompl int `json:"showCompl"`
|
ShowCompl int `json:"showCompl"`
|
||||||
ShowNotes int `json:"showNotes"`
|
ShowLooping int `json:"showLooping"`
|
||||||
Hidden int `json:"hidden"`
|
ShowNotes int `json:"showNotes"`
|
||||||
Max int `json:"max"`
|
Hidden int `json:"hidden"`
|
||||||
Index int `json:"index"`
|
Max int `json:"max"`
|
||||||
|
Index int `json:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(r *http.Request) (*List, error) {
|
func New(r *http.Request) (*List, error) {
|
||||||
@@ -41,6 +42,10 @@ func (l *List) SetShowCompl(state bool) {
|
|||||||
set(state, &l.ShowCompl)
|
set(state, &l.ShowCompl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *List) SetShowLooping(state bool) {
|
||||||
|
set(state, &l.ShowLooping)
|
||||||
|
}
|
||||||
|
|
||||||
func (l *List) SetShowNotes(state bool) {
|
func (l *List) SetShowNotes(state bool) {
|
||||||
set(state, &l.ShowNotes)
|
set(state, &l.ShowNotes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,11 +62,11 @@ func (a *Ajax) storageListTasks(listID string, filters ...func(t *task.Task) boo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
includeComplete := true
|
includeComplete := false
|
||||||
for _, f := range filters {
|
for _, f := range filters {
|
||||||
t := &task.Task{Complete: true}
|
t := &task.Task{Complete: true}
|
||||||
if !f(t) {
|
if f(t) {
|
||||||
includeComplete = false
|
includeComplete = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if includeComplete {
|
if includeComplete {
|
||||||
@@ -77,11 +77,16 @@ func (a *Ajax) storageListTasks(listID string, filters ...func(t *task.Task) boo
|
|||||||
results = append(results, completeResults...)
|
results = append(results, completeResults...)
|
||||||
}
|
}
|
||||||
tasks := []*task.Task{}
|
tasks := []*task.Task{}
|
||||||
|
uuids := map[string]struct{}{}
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
taskID := path.Base(result)
|
taskID := path.Base(result)
|
||||||
if taskID == "" {
|
if taskID == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if _, ok := uuids[taskID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uuids[taskID] = struct{}{}
|
||||||
task, err := a.storageGetTask(taskID)
|
task, err := a.storageGetTask(taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -20,15 +20,24 @@ type taskWithDelta struct {
|
|||||||
func (a *Ajax) loadTasks(w http.ResponseWriter, r *http.Request) error {
|
func (a *Ajax) loadTasks(w http.ResponseWriter, r *http.Request) error {
|
||||||
listID, _, _ := a.Cur(r)
|
listID, _, _ := a.Cur(r)
|
||||||
filterComplete := filterComplete(form.Get(r, "compl"))
|
filterComplete := filterComplete(form.Get(r, "compl"))
|
||||||
|
filterLooping := filterLooping(form.Get(r, "looping"))
|
||||||
filterTags := filterTags(form.ToStrArr(form.Get(r, "t")))
|
filterTags := filterTags(form.ToStrArr(form.Get(r, "t")))
|
||||||
filterSubstr := filterSubstr(form.Get(r, "s"))
|
filterSubstr := filterSubstr(form.Get(r, "s"))
|
||||||
tasks, err := a.storageListTasks(listID, filterComplete, filterTags, filterSubstr)
|
tasks, err := a.storageListTasks(listID, filterComplete, filterTags, filterSubstr, filterLooping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return json.NewEncoder(w).Encode(map[string]interface{}{"list": tasks})
|
return json.NewEncoder(w).Encode(map[string]interface{}{"list": tasks})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filterLooping(looping string) func(t *task.Task) bool {
|
||||||
|
return func(t *task.Task) bool {
|
||||||
|
hasLoop := t.Loop > 0 || t.Cron != ""
|
||||||
|
onlyLooping := looping == "1"
|
||||||
|
return !onlyLooping || hasLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func filterComplete(compl string) func(t *task.Task) bool {
|
func filterComplete(compl string) func(t *task.Task) bool {
|
||||||
return func(t *task.Task) bool {
|
return func(t *task.Task) bool {
|
||||||
return compl == "" || !t.Complete || (compl == "1" && t.Complete)
|
return compl == "" || !t.Complete || (compl == "1" && t.Complete)
|
||||||
@@ -160,16 +169,27 @@ func (a *Ajax) setPrio(w http.ResponseWriter, r *http.Request) error {
|
|||||||
func (a *Ajax) moveTask(w http.ResponseWriter, r *http.Request) error {
|
func (a *Ajax) moveTask(w http.ResponseWriter, r *http.Request) error {
|
||||||
_, taskID, _ := a.Cur(r)
|
_, taskID, _ := a.Cur(r)
|
||||||
toList := form.Get(r, "to")
|
toList := form.Get(r, "to")
|
||||||
|
|
||||||
|
list, err := a.storageGetList(toList)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
movedTask, err := a.storageGetTask(taskID)
|
movedTask, err := a.storageGetTask(taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.storageDelTask(taskID); err != nil {
|
if err := a.storageDelTask(taskID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
movedTask.Index = list.NextIndex()
|
||||||
if err := a.storageSetTask(toList, movedTask); err != nil {
|
if err := a.storageSetTask(toList, movedTask); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := a.storageSetList(list); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return json.NewEncoder(w).Encode(map[string]interface{}{"total": 1, "list": []*task.Task{movedTask}})
|
return json.NewEncoder(w).Encode(map[string]interface{}{"total": 1, "list": []*task.Task{movedTask}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ type Task struct {
|
|||||||
Complete bool
|
Complete bool
|
||||||
Note []string
|
Note []string
|
||||||
Due time.Time
|
Due time.Time
|
||||||
|
Loop time.Duration
|
||||||
|
Cron form.Cron
|
||||||
|
|
||||||
Index int
|
Index int
|
||||||
}
|
}
|
||||||
@@ -42,8 +44,9 @@ func New(r *http.Request) (*Task, error) {
|
|||||||
Tags: append(StrList(form.ToStrArr(form.Get(r, "tag"))), StrList(form.ToStrArr(form.Get(r, "tags")))...),
|
Tags: append(StrList(form.ToStrArr(form.Get(r, "tag"))), StrList(form.ToStrArr(form.Get(r, "tags")))...),
|
||||||
Created: time.Now(),
|
Created: time.Now(),
|
||||||
Edited: time.Now(),
|
Edited: time.Now(),
|
||||||
|
Due: form.ToTime(form.Get(r, "duedate")),
|
||||||
Due: form.ToTime(form.Get(r, "duedate")),
|
Loop: form.ToDuration(form.Get(r, "loop")),
|
||||||
|
Cron: form.Cron(form.Get(r, "cron")),
|
||||||
}
|
}
|
||||||
task.SetNote(form.Get(r, "note"))
|
task.SetNote(form.Get(r, "note"))
|
||||||
return task, task.validate()
|
return task, task.validate()
|
||||||
@@ -73,7 +76,9 @@ func (t *Task) MarshalJSON() ([]byte, error) {
|
|||||||
// "dueClass":"",
|
// "dueClass":"",
|
||||||
// "dueStr":"",
|
// "dueStr":"",
|
||||||
// "dueInt":33330000,
|
// "dueInt":33330000,
|
||||||
// "dueTitle":"Due "}
|
// "dueTitle":"Due ",
|
||||||
|
// "loop": "1m",
|
||||||
|
// "cron": "* * * * *"}
|
||||||
// ]}
|
// ]}
|
||||||
fullFormat := "02 Jan 2006 03:04 PM"
|
fullFormat := "02 Jan 2006 03:04 PM"
|
||||||
shortFormat := "02 Jan"
|
shortFormat := "02 Jan"
|
||||||
@@ -109,6 +114,8 @@ func (t *Task) MarshalJSON() ([]byte, error) {
|
|||||||
"dueStr": t.Due.Format(shortFormat),
|
"dueStr": t.Due.Format(shortFormat),
|
||||||
"dueInt": t.Due.Unix(),
|
"dueInt": t.Due.Unix(),
|
||||||
"dueTitle": "Due ",
|
"dueTitle": "Due ",
|
||||||
|
"loop": t.Loop.String(),
|
||||||
|
"cron": t.Cron,
|
||||||
}
|
}
|
||||||
if t.Due.IsZero() {
|
if t.Due.IsZero() {
|
||||||
for k := range m {
|
for k := range m {
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"local/gziphttp"
|
"local/gziphttp"
|
||||||
|
"local/oauth2/oauth2client"
|
||||||
"local/router"
|
"local/router"
|
||||||
|
"local/simpleserve/simpleserve"
|
||||||
"local/todo-server/config"
|
"local/todo-server/config"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -12,6 +14,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) Routes() error {
|
func (s *Server) Routes() error {
|
||||||
@@ -21,24 +24,31 @@ func (s *Server) Routes() error {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
handler: s.gzip(s.index),
|
handler: s.index,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/mytinytodo_lang.php",
|
path: "/mytinytodo_lang.php",
|
||||||
handler: s.gzip(s.lang),
|
handler: s.lang,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: fmt.Sprintf("/themes/%s%s", router.Wildcard, router.Wildcard),
|
||||||
|
handler: s.handleDeviceCSS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: fmt.Sprintf("%s%s", router.Wildcard, router.Wildcard),
|
path: fmt.Sprintf("%s%s", router.Wildcard, router.Wildcard),
|
||||||
handler: s.gzip(s.phpProxy),
|
handler: s.phpProxy,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/ajax.php",
|
path: "/ajax.php",
|
||||||
handler: s.gzip(s.HandleAjax),
|
handler: s.HandleAjax,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
if err := s.Add(route.path, route.handler); err != nil {
|
handler := route.handler
|
||||||
|
handler = s.gzip(handler)
|
||||||
|
handler = s.oauth(handler)
|
||||||
|
if err := s.Add(route.path, handler); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,6 +84,10 @@ func (s *Server) lang(w http.ResponseWriter, r *http.Request) {
|
|||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) toindex(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) index(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) index(w http.ResponseWriter, r *http.Request) {
|
||||||
f, err := os.Open(path.Join(config.Root, "index.html"))
|
f, err := os.Open(path.Join(config.Root, "index.html"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -91,19 +105,61 @@ func (s *Server) phpProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.static(w, r)
|
s.static(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
url, err := url.Parse(config.MyTinyTodo)
|
url, err := url.Parse("http://127.0.0.1:64123")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
} else {
|
} else {
|
||||||
log.Println("WOULD proxy", url.String(), r.URL.Path)
|
log.Println("WOULD proxy", url.String(), r.URL.Path)
|
||||||
s.index(w, r)
|
s.toindex(w, r)
|
||||||
//proxy := httputil.NewSingleHostReverseProxy(url)
|
//proxy := httputil.NewSingleHostReverseProxy(url)
|
||||||
//proxy.ServeHTTP(w, r)
|
//proxy.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) static(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) static(w http.ResponseWriter, r *http.Request) {
|
||||||
s.fileServer.ServeHTTP(w, r)
|
if err := s._static(w, r); err != nil {
|
||||||
|
s.toindex(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) _static(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.FileServer(s.fileDir).ServeHTTP(w, r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f, err := s.fileDir.Open(path.Clean(r.URL.Path))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
info, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
r.URL.Path = path.Join(r.URL.Path, "index.html")
|
||||||
|
f, err = s.fileDir.Open(r.URL.Path)
|
||||||
|
defer f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
simpleserve.SetContentTypeIfMedia(w, r)
|
||||||
|
_, err = io.Copy(w, f)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) oauth(h http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if config.OAuth != "" {
|
||||||
|
err := oauth2client.Authenticate(config.OAuth, r.Host, w, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("oauth failure", r.Host, ":", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h(w, r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) gzip(h http.HandlerFunc) http.HandlerFunc {
|
func (s *Server) gzip(h http.HandlerFunc) http.HandlerFunc {
|
||||||
@@ -119,3 +175,14 @@ func (s *Server) gzip(h http.HandlerFunc) http.HandlerFunc {
|
|||||||
h(w, r)
|
h(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeviceCSS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if _, ok := r.URL.Query()["pda"]; ok || strings.Contains(r.Header.Get("User-Agent"), "Android") || strings.Contains(r.Header.Get("User-Agent"), "Mobile") {
|
||||||
|
if path.Base(r.URL.Path) == "print.css" {
|
||||||
|
r.URL.Path = path.Join(path.Dir(r.URL.Path), "pda.css")
|
||||||
|
http.Redirect(w, r, r.URL.String(), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.static(w, r)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
*ajax.Ajax
|
*ajax.Ajax
|
||||||
*router.Router
|
*router.Router
|
||||||
fileServer http.Handler
|
fileDir http.Dir
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Server {
|
func New() *Server {
|
||||||
@@ -18,10 +18,10 @@ func New() *Server {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
fileServer := http.FileServer(http.Dir(config.Root))
|
fileDir := http.Dir(config.Root)
|
||||||
return &Server{
|
return &Server{
|
||||||
Ajax: ajax,
|
Ajax: ajax,
|
||||||
Router: router.New(),
|
Router: router.New(),
|
||||||
fileServer: fileServer,
|
fileDir: fileDir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user