24 Commits

Author SHA1 Message Date
Bel LaPointe
01adec7db5 set looped task to latest idx 2021-08-30 09:37:40 -06:00
Bel LaPointe
95b10fd9f6 support only looping 2021-08-17 11:47:10 -06:00
bel
fc088ec240 haflways 2021-08-05 23:45:11 -06:00
bel
2763b68bc4 either of cron or loop can be set and unset, mark invalids as invalid and unfinish 2021-07-17 23:58:51 -06:00
bel
f84614a8da add cron to loop 2021-07-17 23:35:24 -06:00
bel
c2ed541604 add cron to ui 2021-07-17 23:17:09 -06:00
bel
8c7fe2e9ef async triggers for either interval or next due, accept cron 2021-07-17 23:14:54 -06:00
bel
9301ddd467 support combo new and old 2021-07-17 22:51:54 -06:00
bel
d5ec073f75 support 1w, 1d for loop 2021-07-17 22:49:10 -06:00
Bel LaPointe
b8f0efc01c every -loop duration, look for completed tasks with loop set and incomplete them 2021-07-17 11:04:52 -06:00
Bel LaPointe
81c8743de7 accept loop param on task 2021-07-17 10:34:22 -06:00
Bel LaPointe
6abfab229a log 2021-04-20 07:55:49 -05:00
Bel LaPointe
ec780f7d9b actually do handler 2021-04-20 07:54:33 -05:00
Bel LaPointe
94a14f8b9d set content type 2021-04-20 07:40:13 -05:00
Bel LaPointe
90dbfd6f5a gr 2021-04-20 07:32:46 -05:00
Bel LaPointe
de5f17e2c9 impl oauth 2021-04-20 07:04:05 -05:00
Bel LaPointe
0e22586e12 fix method 2021-04-20 06:27:57 -05:00
Bel LaPointe
80becbb7a7 weird paths just redir to root 2021-04-20 06:09:02 -05:00
bel
2e98bdff2d Oh hey html has a solution for my css bullshit 2020-03-17 03:28:39 +00:00
bel
8f966c98a4 whoops on css 2020-03-12 04:41:31 +00:00
bel
a0f336ca67 CSS but not lighter for mobile 2020-03-12 04:36:36 +00:00
bel
f8b5eb71e0 remove unused param 2020-03-12 00:57:58 +00:00
bel
7c70ba27cb fix multi-line task note preview 2020-02-02 06:13:27 +00:00
bel
683b7a5f2d Fix pda technically 2020-02-02 05:49:17 +00:00
17 changed files with 386 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -794,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 {
@@ -1367,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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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