diff --git a/src/api.php b/src/api.php index 2d6c717..5eae879 100644 --- a/src/api.php +++ b/src/api.php @@ -45,6 +45,9 @@ $endpoints = array( '/tasks/parseTitle' => [ 'POST' => [ TasksController::class , 'postTitleParse' ], ], + '/tasks/newCounter' => [ + 'POST' => [ TasksController::class , 'postNewCounter' ], + ], '/tagCloud/(-?\d+)' => [ 'GET' => [ TagsController::class , 'getCloud' ], ], diff --git a/src/content/mytinytodo.js b/src/content/mytinytodo.js index 6eaed67..e17e3c9 100644 --- a/src/content/mytinytodo.js +++ b/src/content/mytinytodo.js @@ -30,6 +30,7 @@ var tabLists = { _length: 0, _order: [], _alltasks: {}, + lastTime: 0, clear: function(){ this._lists = {}; this._length = 0; this._order = []; this._alltasks = { id:-1, showCompl:0, sort:3, name:_mtt.lang.get('alltasks') }; @@ -76,11 +77,13 @@ var mytinytodo = window.mytinytodo = _mtt = { history: true, markdown: true, viewTaskOnClick: false, + newTaskCounter: true, }, timers: { previewtag: 0, ajaxAnimation: 0, + newTaskCounter: 0, }, lang: { @@ -719,6 +722,11 @@ var mytinytodo = window.mytinytodo = _mtt = { }); } + // Counter + if (this.options.newTaskCounter) { + this.addAction('listsLoaded', newTaskCounterStart); + } + this.doAction( 'init' ); return this; @@ -804,8 +812,10 @@ var mytinytodo = window.mytinytodo = _mtt = { if (list) { openListId = list.id; } + tabLists.lastTime = res.time; res.list.forEach( (item) => { + item.lastTime = res.time; if ( item.id == -1 ) { tabLists._alltasks = item; ti += prepareListHtml(item); @@ -1070,7 +1080,7 @@ function renameCurList() var item = json.list[0]; curList = item; tabLists.replace(item); - $('#lists ul>.mtt-tab-selected>a').attr('title', item.name).find('span').html(item.name); //FIXME: wft? + $('#list_'+curList.id).replaceWith(prepareListHtml(curList, true)); mytinytodo.doAction('listRenamed', item); }); }); @@ -1164,6 +1174,8 @@ function loadTasks(opts) taskOrder.push(parseInt(item.id)); changeTaskCnt(item, 1); }); + curList.lastTime = json.time; + $('#list_'+curList.id).find('.counter').text('').addClass('hidden'); if(opts.beforeShow && opts.beforeShow.call) { opts.beforeShow(); } @@ -1172,18 +1184,22 @@ function loadTasks(opts) }); }; -function prepareListHtml(list) +function prepareListHtml(list, isSelected) { - var opentag = ''; + const selected = isSelected ? ' mtt-tab-selected' : ''; + let opentag = ''; if (list.id == -1) { - opentag = '
  • '; + opentag = '
  • '; } else { - opentag = '
  • '; + opentag = '
  • '; } return opentag + - '' + list.name + '' + - '
  • '; + ''+ + ''+ + '' + list.name + '' + + '
    '+ + '
    '; } function prepareTaskStr(item, noteExp) @@ -2680,6 +2696,63 @@ function setLocalStorageItem(key, value) } } +function newTaskCounterStart() +{ + clearInterval(_mtt.timers.newTaskCounter); + _mtt.timers.newTaskCounter = setInterval(newTaskCounter, 60*1000); //every 60 sec +} + +function newTaskCounter() +{ + const params = { + list: curList.id, + later: curList.lastTime, + lists: [], + }; + tabLists.getAll().forEach( (list) => { + if (list.hidden || list.id == -1 || list.id == curList.id) { + return; + } + params.lists.push({ + listId: list.id, + later: list.lastTime + }); + }); + + fetch(_mtt.apiUrl + 'tasks/newCounter', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'MTT-Token': _mtt.options.token, + }, + body: JSON.stringify(params) + }) + .then(response => response.json()) + .then(json => { + if (json && json.total) { + if (Array.isArray(json.tasks)) { + let curCounter = 0; + json.tasks.forEach((id) => { + if (taskList[id]) { + console.log('skip existing id ', id); + } + else { + curCounter++; + } + }); + if (curCounter > 0) { + $('#list_'+curList.id).find('.counter').text(curCounter).removeClass('hidden'); + } + } + if (Array.isArray(json.lists)) { + json.lists.forEach((item) => { + $('#list_'+item.listId).find('.counter').text(item.counter).removeClass('hidden'); + }); + } + } + }); +} + /* Errors and Info messages */ diff --git a/src/content/theme/style.css b/src/content/theme/style.css index 9b599ae..400c75e 100644 --- a/src/content/theme/style.css +++ b/src/content/theme/style.css @@ -286,25 +286,30 @@ select.form-input { border-bottom:none; border-top-right-radius:8px; transition:background-color 0.1s ease-in; + max-width:230px; + min-width:95px; } .mtt-tab a { margin:0; text-decoration:none; white-space:nowrap; color: var(--color-tab-text); display:inline-block; outline:none; - box-sizing:border-box; height:2.2rem; padding:1px 0.3rem 0 0.3rem; display:flex; align-items:center; + box-sizing:border-box; + height:2.2rem; + padding:1px 0.3rem 0 0.3rem; + display:flex; + align-items:center; } .mtt-tab a:active { outline:0; text-decoration:none; } -.mtt-tab a>span { +.mtt-tab a > .title { display:inline-block; text-align:center; - min-width:75px; - max-width:195px; cursor:pointer; padding:0; overflow:hidden; text-overflow: ellipsis; margin-left:0.3rem; margin-right:0.3rem; + flex-grow:1; } .mtt-tab .list-action { display:none; } .mtt-tab .mtt-img-button:hover, @@ -331,6 +336,19 @@ select.form-input { background-color: var(--color-placeholder); border-color: var(--color-placeholder-border); } +.mtt-tab .counter { + display: inline-block; + min-width: 1.2rem; + height: 1rem; + border-radius: 1rem; + font-size: 0.8rem; + text-align: center; + background-color: #686868; /* #de5141 */ + color: white; +} +.mtt-tab .counter.hidden { + display: none; +} #tabs_buttons { padding-left:2px; padding-right:2px; diff --git a/src/includes/api/ListsController.php b/src/includes/api/ListsController.php index 012737f..6440a7e 100644 --- a/src/includes/api/ListsController.php +++ b/src/includes/api/ListsController.php @@ -28,6 +28,7 @@ class ListsController extends ApiController { $t['list'][] = $this->prepareAllTasksList(); // show alltasks lists only for authorized user $t['total'] = 1; } + $t['time'] = time(); $q = $db->dq("SELECT * FROM {$db->prefix}lists $sqlWhere ORDER BY ow ASC, id ASC"); while ($r = $q->fetchAssoc()) { diff --git a/src/includes/api/TasksController.php b/src/includes/api/TasksController.php index c285a22..32e0f1a 100644 --- a/src/includes/api/TasksController.php +++ b/src/includes/api/TasksController.php @@ -98,6 +98,7 @@ class TasksController extends ApiController { $t = array(); $t['total'] = 0; $t['list'] = array(); + $t['time'] = time(); $groupConcat = ''; if ($db::DBTYPE == DBConnection::DBTYPE_POSTGRES) { @@ -106,6 +107,7 @@ class TasksController extends ApiController { else { $groupConcat = "GROUP_CONCAT(tags.id) AS tags_ids, GROUP_CONCAT(tags.name) AS tags"; } + $q = $db->dq(" SELECT todo.*, todo.duedate IS NULL AS ddn, $groupConcat FROM {$db->prefix}todolist AS todo @@ -246,6 +248,65 @@ class TasksController extends ApiController { $this->response->data = $t; } + + function postNewCounter() + { + checkReadAccess(); + $lists = $this->req->jsonBody['lists'] ?? false; + if (!is_array($lists)) { + $this->response->data = [ 'total' => 0 ]; + return; + } + $userLists = []; + if (!haveWriteAccess()) { + $userLists = $this->getUserListsSimple(true); + if ($userLists) { + $sqlWhereList = "AND list_id IN (". implode(',', $userLists). ")"; + // remove lists without access granted + $lists = array_filter($lists, function($item) use ($userLists) { + return in_array( (string)($item['listId'] ?? ''), $userLists ); + }); + } + } + $sqlWhereList = []; + foreach ($lists as $item) { + if (!isset($item['later'])) continue; + $later = (int)$item['later']; + if ($later <= 0) continue; + $sqlWhereList[] = "(list_id = ". (int)$item['listId']. " AND d_created > $later)"; + } + $sqlWhere = implode(' OR ', $sqlWhereList); + + + $db = DBConnection::instance(); + $a = []; + $q = $db->dq("SELECT list_id, COUNT(id) c FROM {$db->prefix}todolist + WHERE $sqlWhere GROUP BY list_id"); + while ($r = $q->fetchAssoc()) { + $a[] = [ + 'listId' => (int)$r['list_id'], + 'counter' => (int)$r['c'], + ]; + } + + $b = []; + $list = (int) ($this->req->jsonBody['list'] ?? 0); + $later = (int) ($this->req->jsonBody['later'] ?? 0); + if ($list > 0 && $later > 0 && (!$userLists || in_array((string)$list, $userLists))) { + $q = $db->dq("SELECT id FROM {$db->prefix}todolist + WHERE list_id = $list AND d_created > $later"); + while ($r = $q->fetchAssoc()) { + $b[] = (int)$r['id']; + } + } + + $this->response->data = [ + 'total' => count($b) + count($a), + 'tasks' => $b, + 'lists' => $a + ]; + } + /* Private Functions */ private function newTaskInList(int $listId): ?array @@ -488,13 +549,17 @@ class TasksController extends ApiController { return $t; } - private function getUserListsSimple(): array + private function getUserListsSimple(bool $readOnly = false): array { $db = DBConnection::instance(); + $sqlWhere = ''; + if ($readOnly) { + $sqlWhere = "WHERE published=1"; + } $a = array(); - $q = $db->dq("SELECT id,name FROM {$db->prefix}lists ORDER BY id ASC"); + $q = $db->dq("SELECT id,name FROM {$db->prefix}lists $sqlWhere ORDER BY id ASC"); while($r = $q->fetchRow()) { - $a[$r[0]] = $r[1]; + $a[ (string)$r[0] ] = (string)$r[1]; } return $a; }