todos.mjs 29.2 KB
Newer Older
1
"use strict";
ransome1's avatar
ransome1 committed
2
import "../../node_modules/jstodotxt/jsTodoExtensions.js";
3
import { getActiveFile, userData, appData, handleError, translations, setUserData, startBuilding, getConfirmation, resetModal } from "../render.js";
4
import { _paq } from "./matomo.mjs";
5
6
7
8
import { categories } from "./filters.mjs";
import { generateRecurrence } from "./recurrences.mjs";
import { convertDate, isToday, isTomorrow, isPast } from "./date.mjs";
import { show } from "./form.mjs";
9
import { SugarDueExtension, RecExtension, ThresholdExtension } from "./todotxtExtensions.mjs";
10

11
const modalForm = document.getElementById("modalForm");
12
const todoContext = document.getElementById("todoContext");
13
const todoContextDelete = document.getElementById("todoContextDelete");
14
15
16
const todoContextEdit = document.getElementById("todoContextEdit");
const todoContextUseAsTemplate = document.getElementById("todoContextUseAsTemplate");
const todoTableWrapper = document.getElementById("todoTableWrapper");
17
18
19
20

todoContextUseAsTemplate.innerHTML = translations.useAsTemplate;
todoContextEdit.innerHTML = translations.edit;
todoContextDelete.innerHTML = translations.delete;
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// ########################################################################################################################
// CONFIGURE MARKDOWN PARSER
// ########################################################################################################################
marked.setOptions({
  pedantic: false,
  gfm: true,
  breaks: false,
  sanitize: false,
  sanitizer: false,
  smartLists: false,
  smartypants: false,
  xhtml: false,
  baseUrl: "https://"
});
const renderer = {
  link(href, title, text) {
    // truncate the url
    if(text.length > 40) text = text.slice(0, 40) + " [...] ";
39
    return `${text} <a href="${href}" target="_blank"><i class="fas fa-external-link-alt"></i></a>`;
40
41
42
43
44
45
46
47
48
  }
};
marked.use({ renderer });
// ########################################################################################################################
// PREPARE TABLE
// ########################################################################################################################
const tableContainerContent = document.createDocumentFragment();
const todoTableBodyRowTemplate = document.createElement("div");
const todoTableBodyCellCheckboxTemplate  = document.createElement("div");
49
const todoTableBodyCellTextTemplate = document.createElement("a");
50
51
52
53
const tableContainerCategoriesTemplate = document.createElement("span");
const todoTableBodyCellPriorityTemplate = document.createElement("div");
const todoTableBodyCellDueDateTemplate = document.createElement("span");
const todoTableBodyCellRecurrenceTemplate = document.createElement("span");
ransome1's avatar
ransome1 committed
54
const todoTableBodyCellArchiveTemplate = document.createElement("span");
ransome1's avatar
ransome1 committed
55
const todoTableBodyCellHiddenTemplate = document.createElement("span");
56
const item = { previous: "" }
57
58
let
  items,
59
60
61
62
63
  clusterCounter = 0,
  clusterSize = Math.ceil(window.innerHeight/32), // 32 being the pixel height of one todo in compact mode
  clusterThreshold = clusterSize,
  visibleRows,
  todoRows;
64

65
todoTableWrapper.addEventListener("scroll", function(event) {
66
  if(visibleRows>=items.filtered.length) return false;
ransome1's avatar
ransome1 committed
67
  if(Math.floor(event.target.scrollHeight - event.target.scrollTop) <= event.target.clientHeight) {
68
    startBuilding(true);
69
70
  }
});
71
72
73
todoContext.addEventListener("keyup", function(event) {
  if(event.key==="Escape") this.classList.remove("is-active");
});
74

75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
function showResultStats() {
  try {
    // we show some information on filters if any are set
    if(items.filtered.length!=items.objects.length) {
      resultStats.classList.add("is-active");
      resultStats.firstElementChild.innerHTML = translations.visibleTodos + "&nbsp;<strong>" + items.filtered.length + " </strong>&nbsp;" + translations.of + "&nbsp;<strong>" + items.objects.length + "</strong>";
      return Promise.resolve("Info: Result box is shown");
    } else {
      resultStats.classList.remove("is-active");
      return Promise.resolve("Info: Result box is hidden");
    }
  } catch(error) {
    error.functionName = showResultStats.name;
    return Promise.reject(error);
  }
}
91
function configureTodoTableTemplate() {
92
  try {
93
94
95
    todoTableBodyRowTemplate.setAttribute("class", "todo");
    todoTableBodyCellCheckboxTemplate.setAttribute("class", "cell checkbox");
    todoTableBodyCellTextTemplate.setAttribute("class", "cell text");
96
    todoTableBodyCellTextTemplate.setAttribute("tabindex", 0);
97
    todoTableBodyCellTextTemplate.setAttribute("href", "#");
98
    tableContainerCategoriesTemplate.setAttribute("class", "categories");
99
100
    todoTableBodyCellDueDateTemplate.setAttribute("class", "cell itemDueDate");
    todoTableBodyCellRecurrenceTemplate.setAttribute("class", "cell recurrence");
101
102
103
104
105
106
    return Promise.resolve("Success: Table templates set up");
  } catch(error) {
    error.functionName = configureTodoTableTemplate.name;
    return Promise.reject(error);
  }
}
107
108
function generateItems(content) {
  try {
109
    items = { objects: TodoTxt.parse(content, [ new DueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]) }
110
111
112
113
114
115
116
117
118
119
    items.complete = items.objects.filter(function(item) { return item.complete === true });
    items.incomplete = items.objects.filter(function(item) { return item.complete === false });
    items.objects = items.objects.filter(function(item) { return item.toString() != "" });
    return Promise.resolve(items);
  } catch(error) {
    error.functionName = generateItems.name;
    return Promise.reject(error);
  }
}
function generateGroups(items) {
ransome1's avatar
ransome1 committed
120
  const sortBy = userData.sortBy[0];
121
122
123
124
  // build object according to sorting method
  items = items.reduce((object, a) => {
    if(userData.sortCompletedLast && a.complete) {
      object["completed"] = [...object["completed"] || [], a];
ransome1's avatar
ransome1 committed
125
    } else if(sortBy==="dueString" && !a.due) {
126
127
      object["noDueDate"] = [...object["noDueDate"] || [], a];
    } else {
ransome1's avatar
ransome1 committed
128
      object[a[sortBy]] = [...object[a[sortBy]] || [], a];
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
    }
    return object;
  }, {});
  // object is converted to a sorted array
  items = Object.entries(items).sort(function(a,b) {
    // when a is null sort it after b
    if(a[0]==="null") return 1;
    // when b is null sort it after a
    if(b[0]==="null") return -1;
    // sort alphabetically
    if(a < b) return -1;
  });
  //
  if(userData.sortCompletedLast) {
    items.sort(function(a,b) {
      // when a is null sort it after b
      if(a[0]==="completed") return 1;
      // when b is null sort it after a
      if(b[0]==="completed") return -1;
      return 0;
    });
  }
151
152
153
154
  // sort the items within the groups
  items.forEach((group) => {
    group[1] = sortTodoData(group[1]);
  });
155
156
  return Promise.resolve(items)
}
157
async function generateTable(groups, loadAll) {
ransome1's avatar
ransome1 committed
158
  try {
159
160
161
    todoRows = new Array;
    // TODO Overthink due to performance reasons
    todoTable.textContent = "";
162
163
    // configure stats
    showResultStats();
ransome1's avatar
ransome1 committed
164
    // prepare the templates for the table
165
    await configureTodoTableTemplate();
ransome1's avatar
ransome1 committed
166
    // reset cluster count for this run
167
168
    for (let group in groups) {
      // create a divider row
169
      let dividerRow;
170
171
      // completed todos
      if(userData.sortCompletedLast && groups[group][0]==="completed") {
ransome1's avatar
ransome1 committed
172
        dividerRow = document.createRange().createContextualFragment("<div id=\"" + userData.sortBy[0] + groups[group][0] + "\" class=\"group " + userData.sortBy[0] + " " + groups[group][0] + "\"><div class=\"cell\"></div></div>")
173
      // for priority, context and project
ransome1's avatar
ransome1 committed
174
      } else if(groups[group][0]!="null" && userData.sortBy[0]!="dueString") {
ransome1's avatar
ransome1 committed
175
        dividerRow = document.createRange().createContextualFragment("<div id=\"" + userData.sortBy[0] + groups[group][0] + "\" class=\"group " + userData.sortBy[0] + " " + groups[group][0] + "\"><div class=\"cell\"><button tabindex=\"-1\" class=\"" + groups[group][0] + "\">" + groups[group][0].replace(/,/g, ', ') + "</button></div></div>")
176
      // if sorting is by due date
ransome1's avatar
ransome1 committed
177
      } else if(userData.sortBy[0]==="dueString" && groups[group][1][0].due) {
178
        if(isToday(groups[group][1][0].due)) {
ransome1's avatar
ransome1 committed
179
          dividerRow= document.createRange().createContextualFragment("<div id=\"" + userData.sortBy[0] + groups[group][0] + "\" class=\"group due\"><div class=\"cell isToday\">" + translations.today + "</button></div></div>")
180
        } else if(isTomorrow(groups[group][1][0].due)) {
ransome1's avatar
ransome1 committed
181
          dividerRow = document.createRange().createContextualFragment("<div id=\"" + userData.sortBy[0] + groups[group][0] + "\" class=\"group due\"><div class=\"cell isTomorrow\">" + translations.tomorrow + "</button></div></div>")
182
        } else if(isPast(groups[group][1][0].due)) {
ransome1's avatar
ransome1 committed
183
          dividerRow = document.createRange().createContextualFragment("<div id=\"" + userData.sortBy[0] + groups[group][0] + "\" class=\"group due\"><div class=\"cell isPast\">" + groups[group][0] + "</button></div></div>")
184
        } else {
ransome1's avatar
ransome1 committed
185
          dividerRow = document.createRange().createContextualFragment("<div id=\"" + userData.sortBy[0] + groups[group][0] + "\" class=\"group due\"><div class=\"cell\">" + groups[group][0] + "</div></div>")
186
187
        }
      // create an empty divider row
188
      } else {
189
        dividerRow = document.createRange().createContextualFragment("<div class=\"group\"></div>")
190
      }
191
      if(!document.getElementById(userData.sortBy[0] + groups[group][0]) && dividerRow) todoRows.push(dividerRow);
192
      for (let item in groups[group][1]) {
193
        let todo = groups[group][1][item];
194
        // if this todo is not a recurring one the rec value will be set to null
195
        if(!todo.rec) todo.rec = null;
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
        // incompleted todos with due date
        if (todo.due && !todo.complete) {
          // create notification
          if(isToday(todo.due)) {
            generateNotification(todo, 0).then(response => {
              console.log(response);
            }).catch(error => {
              handleError(error);
            });
          } else if(isTomorrow(todo.due)) {
            generateNotification(todo, 1).then(response => {
              console.log(response);
            }).catch(error => {
              handleError(error);
            });
          }
        }
213
214
215
216
217
218
219
220
221
222
223
224
        todoRows.push(generateTableRow(todo));
      }
    }
    for (let row in todoRows) {
      clusterCounter++;
      visibleRows++;
      if(clusterCounter === clusterThreshold) {
        clusterThreshold = clusterThreshold + clusterCounter;
        clusterCounter = 0;
        break;
      } else if(visibleRows < clusterThreshold) {
        continue;
225
      }
226
      tableContainerContent.appendChild(todoRows[row]);
227
    }
ransome1's avatar
ransome1 committed
228
    todoTable.appendChild(tableContainerContent);
ransome1's avatar
ransome1 committed
229
230
231
232
233
    return Promise.resolve("Success: Todo table generated");
  } catch(error) {
    error.functionName = archiveTodos.name;
    return Promise.reject(error);
  }
234
235
236
237
238
239
240
241
242
243
244
}
function generateTableRow(todo) {
  try {
    // create nodes from templates
    let todoTableBodyRow = todoTableBodyRowTemplate.cloneNode(true);
    let todoTableBodyCellCheckbox = todoTableBodyCellCheckboxTemplate.cloneNode(true);
    let todoTableBodyCellText = todoTableBodyCellTextTemplate.cloneNode(true);
    let tableContainerCategories = tableContainerCategoriesTemplate.cloneNode(true);
    let todoTableBodyCellPriority = todoTableBodyCellPriorityTemplate.cloneNode(true);
    let todoTableBodyCellDueDate = todoTableBodyCellDueDateTemplate.cloneNode(true);
    let todoTableBodyCellRecurrence = todoTableBodyCellRecurrenceTemplate.cloneNode(true);
ransome1's avatar
ransome1 committed
245
    let todoTableBodyCellArchive = todoTableBodyCellArchiveTemplate.cloneNode(true);
ransome1's avatar
ransome1 committed
246
    let todoTableBodyCellHidden = todoTableBodyCellHiddenTemplate.cloneNode(true);
247
248
249
250
251
252
253
    // if new item was saved, row is being marked
    if(todo.toString()==item.previous) {
      todoTableBodyRow.setAttribute("id", "previousItem");
      item.previous = null;
    }
    // start with the individual config of the items
    if(todo.complete==true) {
254
      todoTableBodyRow.setAttribute("class", "todo completed");
255
256
257
    }
    todoTableBodyRow.setAttribute("data-item", todo.toString());
    // add the priority marker or a white spacer
ransome1's avatar
ransome1 committed
258
    if(todo.priority && userData.sortBy[0]==="priority") {
259
      todoTableBodyCellPriority.setAttribute("class", "cell priority " + todo.priority);
260
      todoTableBodyRow.appendChild(todoTableBodyCellPriority);
ransome1's avatar
CI AUR    
ransome1 committed
261
    }
262
263
264
265
266
267
268
269
270
271
272
    // add the checkbox
    if(todo.complete==true) {
      todoTableBodyCellCheckbox.setAttribute("title", translations.inProgress);
      todoTableBodyCellCheckbox.innerHTML = "<a href=\"#\"><i class=\"fas fa-check-circle\"></i></a>";
    } else {
      todoTableBodyCellCheckbox.setAttribute("title", translations.done);
      todoTableBodyCellCheckbox.innerHTML = "<a href=\"#\"><i class=\"far fa-circle\"></i></a>";
    }
    // add a listener on the checkbox to call the completeItem function
    todoTableBodyCellCheckbox.onclick = function() {
      // passing the data-item attribute of the parent tag to complete function
273
      setTodoComplete(this.parentElement.getAttribute("data-item")).then(response => {
274
275
276
277
278
         console.log(response);
      }).catch(error => {
        handleError(error);
      });
      // trigger matomo event
279
      if(userData.matomoEvents) _paq.push(["trackEvent", "Todo-Table", "Click on Checkbox"]);
280
281
    }
    todoTableBodyRow.appendChild(todoTableBodyCellCheckbox);
ransome1's avatar
ransome1 committed
282
283
284
    // add archiving icon
    if(todo.complete) {
      todoTableBodyCellArchive.setAttribute("class", "cell archive");
ransome1's avatar
ransome1 committed
285
      todoTableBodyCellArchive.innerHTML = "<a href=\"#\"><i class=\"fas fa-archive\"></i></a>";
ransome1's avatar
ransome1 committed
286
287
288
289
290
291
292
293
      todoTableBodyCellArchive.onclick = function() {
        getConfirmation(archiveTodos, translations.archivingPrompt);
        // trigger matomo event
        if(userData.matomoEvents) _paq.push(["trackEvent", "Todo-Table", "Click on Archive button"]);
      }
      // append the due date to the text item
      todoTableBodyRow.appendChild(todoTableBodyCellArchive);
    }
ransome1's avatar
ransome1 committed
294
295
296
297
298
299
300
301
    // add hidden icon
    if(todo.h) {
      todoTableBodyRow.setAttribute("class", "todo is-greyed-out");
      todoTableBodyCellHidden.setAttribute("class", "cell");
      todoTableBodyCellHidden.innerHTML = "<i class=\"far fa-eye-slash\"></i>";
      // append the due date to the text item
      todoTableBodyRow.appendChild(todoTableBodyCellHidden);
    }
302
303
    // creates cell for the text
    if(todo.text) {
ransome1's avatar
ransome1 committed
304
      if(todo.priority && userData.sortBy[0]!="priority") todoTableBodyCellText.innerHTML = "<span class=\"priority\"><button class=\"" + todo.priority + "\">" + todo.priority + "</button></span>";
305
      // parse text string through markdown parser
306
      todoTableBodyCellText.innerHTML +=  "<span class=\"text\">" + marked.parseInline(todo.text) + "</span>";
307
      // replace line feed character with a space
308
309
310
311
      todoTableBodyCellText.innerHTML = todoTableBodyCellText.innerHTML.replaceAll(String.fromCharCode(16)," ");
      // add a spacer to divide text (and link) and categories
      todoTableBodyCellText.innerHTML += " ";
    }
312
    // click on the text
313
314
315
    todoTableBodyCellText.onclick = function() {
      // if the clicked item is not the external link icon, show(true) will be called
      if(!event.target.classList.contains('fa-external-link-alt')) {
316
        show(this.parentElement.getAttribute("data-item"));
317
        // trigger matomo event
318
        if(userData.matomoEvents) _paq.push(["trackEvent", "Todo-Table", "Click on Todo item"]);
319
320
      }
    }
321
322
323
    // cell for the categories
    categories.forEach(category => {
      if(todo[category] && category!="priority") {
ransome1's avatar
ransome1 committed
324
        todo[category].forEach(element => {
325
326
          let todoTableBodyCellCategory = document.createElement("span");
          todoTableBodyCellCategory.setAttribute("class", "tag " + category);
ransome1's avatar
ransome1 committed
327
          todoTableBodyCellCategory.innerHTML = element;
328
329
330
331
332
333
          tableContainerCategories.appendChild(todoTableBodyCellCategory);
        });
      }
    });
    // only add the categories to text cell if it has child nodes
    if(tableContainerCategories.hasChildNodes()) todoTableBodyCellText.appendChild(tableContainerCategories);
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
    // check for and add a given due date
    if(todo.due) {
      var tag = convertDate(todo.due);
      if(isToday(todo.due)) {
        todoTableBodyCellDueDate.classList.add("isToday");
        tag = translations.today;
      } else if(isTomorrow(todo.due)) {
        todoTableBodyCellDueDate.classList.add("isTomorrow");
        tag = translations.tomorrow;
      } else if(isPast(todo.due)) {
        todoTableBodyCellDueDate.classList.add("isPast");
      }
      todoTableBodyCellDueDate.innerHTML = `
        <i class="far fa-clock"></i>
        <div class="tags has-addons">
          <span class="tag">` + translations.due + `</span><span class="tag is-dark">` + tag + `</span>
        </div>
ransome1's avatar
ransome1 committed
351
        <i class="fas fa-sort-down"></i>`;
352
353
354
355
356
357
358
359
360
361
362
      // append the due date to the text item
      todoTableBodyCellText.appendChild(todoTableBodyCellDueDate);
    }
    // add recurrence icon
    if(todo.rec) {
      todoTableBodyCellRecurrence.innerHTML = "<i class=\"fas fa-redo\"></i>";
      // append the due date to the text item
      todoTableBodyCellText.appendChild(todoTableBodyCellRecurrence);
    }
    // add the text cell to the row
    todoTableBodyRow.appendChild(todoTableBodyCellText);
363
    todoTableBodyRow.addEventListener("contextmenu", event => {
ransome1's avatar
ransome1 committed
364
      //todoContextUseAsTemplate.focus();
365
366
367
368
369
      todoContext.style.left = event.x + "px";
      todoContext.style.top = event.y + "px";
      todoContext.classList.toggle("is-active");
      todoContext.setAttribute("data-item", todo.toString())
      // click on use as template option
ransome1's avatar
ransome1 committed
370
371
      todoContextUseAsTemplate.onclick = function() {
        show(todoContext.getAttribute('data-item'), true);
372
373
374
375
376
        todoContext.classList.toggle("is-active");
        todoContext.removeAttribute("data-item");
        // trigger matomo event
        if(userData.matomoEvents) _paq.push(["trackEvent", "Todo-Table-Context", "Click on Use as template"]);
      }
ransome1's avatar
ransome1 committed
377
378
      todoContextEdit.onclick = function() {
        show(todoContext.getAttribute('data-item'));
379
380
381
382
383
384
        todoContext.classList.toggle("is-active");
        todoContext.removeAttribute("data-item");
        // trigger matomo event
        if(userData.matomoEvents) _paq.push(["trackEvent", "Todo-Table-Context", "Click on Edit"]);
      }
      // click on delete
ransome1's avatar
ransome1 committed
385
      todoContextDelete.onclick = function() {
386
        // passing the data-item attribute of the parent tag to complete function
ransome1's avatar
ransome1 committed
387
        setTodoDelete(todoContext.getAttribute('data-item')).then(response => {
388
389
390
391
392
          console.log(response);
          todoContext.classList.toggle("is-active");
          todoContext.removeAttribute("data-item");
        }).catch(error => {
          handleError(error);
393
394
        });
        // trigger matomo event
395
        if(userData.matomoEvents) _paq.push(["trackEvent", "Todo-Table-Context", "Click on Delete"]);
396
      }
397
    });
398
399
400
401
402
403
404
405
406
    // return the fully built row
    return todoTableBodyRow;
  } catch(error) {
    error.functionName = generateTableRow.name;
    return Promise.reject(error);
  }
}
function sortTodoData(group) {
  try {
ransome1's avatar
ransome1 committed
407
    for(let i = 1; i < userData.sortBy.length; i++) {
ransome1's avatar
ransome1 committed
408
409
      group.sort(function(a, b) {
        // only continue if the two items have the same filters from the previous iteration
ransome1's avatar
ransome1 committed
410
411
        if(i>1 && JSON.stringify(a[userData.sortBy[i-2]]) !== JSON.stringify(b[userData.sortBy[i-2]]) ) return;
        if(i>1 &&  JSON.stringify(a[userData.sortBy[i-1]]) !== JSON.stringify(b[userData.sortBy[i-1]]) ) return;
ransome1's avatar
ransome1 committed
412
        let
ransome1's avatar
ransome1 committed
413
414
          item1 = a[userData.sortBy[i]],
          item2 = b[userData.sortBy[i]];
ransome1's avatar
ransome1 committed
415
416
417
418
419
420
421
422
423
424
425
        // when item1 is empty or bigger than item2 it will be sorted after item2
        if(!item1 && item2 || item1 > item2) {
          return 1;
        // when item2 is empty or bigger than item1, item 1 will be sorted before item2
        } else if(item1 && !item2 || item1 < item2) {
          return -1;
        }
        // no change to sorting
        return;
      });
    }
426
427
428
429
430
431
432
433
434
    return group;
  } catch(error) {
    error.functionName = sortTodoData.name;
    return Promise.reject(error);
  }
}
function setTodoComplete(todo) {
  try {
    // first convert the string to a todo.txt object
435
    todo = new TodoTxtItem(todo, [ new DueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]);
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
    // get index of todo
    const index = items.objects.map(function(item) {return item.toString(); }).indexOf(todo.toString());
    // mark item as in progress
    if(todo.complete) {
      // if item was already completed we set complete to false and the date to null
      todo.complete = false;
      todo.completed = null;
      // delete old item from array and add the new one at it's position
      items.objects.splice(index, 1, todo);
    // Mark item as complete
    } else if(!todo.complete) {
      if(todo.due) {
        const date = convertDate(todo.due);
        // if set to complete it will be removed from persisted notifcations
        if(userData.dismissedNotifications) {
          // the one set for today
          userData.dismissedNotifications = userData.dismissedNotifications.filter(e => e !== generateHash(date + todo.text)+0);
          // the one set for tomorrow
          userData.dismissedNotifications = userData.dismissedNotifications.filter(e => e !== generateHash(date + todo.text)+1);
          setUserData("dismissedNotifications", userData.dismissedNotifications);
        }
      }
      todo.complete = true;
      todo.completed = new Date();
      // delete old todo from array and add the new one at it's position
      items.objects.splice(index, 1, todo);
      // if recurrence is set start generating the recurring todo
      if(todo.rec) generateRecurrence(todo)
464
465
      // finally remove priority
      todo.priority = null;
466
467
    }
    //write the data to the file
468
469
    window.api.send("writeToFile", [items.objects.join("\n").toString() + "\n"]);
    return Promise.resolve("Success: Changes written to file: " + getActiveFile());
470
471
472
473
474
475
476
477
  } catch(error) {
    error.functionName = setTodoComplete.name;
    return Promise.reject(error);
  }
}
function setTodoDelete(todo) {
  try {
    // in case edit form is open, text has changed and complete button is pressed, we do not fall back to the initial value of todo but instead choose input value
ransome1's avatar
ransome1 committed
478
    if(document.getElementById("modalFormInput").value) todo = document.getElementById("modalFormInput").value;
479
    // first convert the string to a todo.txt object
480
    todo = new TodoTxtItem(todo, [ new DueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]);
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
    // get index of todo
    const index = items.objects.map(function(item) {return item.toString(); }).indexOf(todo.toString());
    // Delete item
    if(todo.due) {
      var date = convertDate(todo.due);
      // if deleted it will be removed from persisted notifcations
      if(userData.dismissedNotifications) {
        // the one set for today
        userData.dismissedNotifications = userData.dismissedNotifications.filter(e => e !== generateHash(date + todo.text)+0);
        // the one set for tomorrow
        userData.dismissedNotifications = userData.dismissedNotifications.filter(e => e !== generateHash(date + todo.text)+1);
        setUserData("dismissedNotifications", userData.dismissedNotifications);
      }
    }
    items.objects.splice(index, 1);
    //write the data to the file
497
498
    window.api.send("writeToFile", [items.objects.join("\n").toString() + "\n"]);
    return Promise.resolve("Success: Changes written to file: " + getActiveFile());
499
500
501
502
503
  } catch(error) {
    error.functionName = setTodoDelete.name;
    return Promise.reject(error);
  }
}
ransome1's avatar
ransome1 committed
504
505
function addTodo(todo) {
  try {
506
    todo = new TodoTxtItem(todo, [ new SugarDueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]);
ransome1's avatar
ransome1 committed
507
    // abort if there is no text
508
    if(!todo.text && !todo.h) return Promise.resolve("Info: Text is missing, no todo is written");
ransome1's avatar
ransome1 committed
509
510
511
512
513
514
515
516
517
    // we add the current date to the start date attribute of the todo.txt object
    todo.date = new Date();
    // get index of todo
    const index = items.objects.map(function(item) { return item.toString(); }).indexOf(todo.toString());
    if(index===-1) {
      // we build the array
      items.objects.push(todo);
      //write the data to the file
      // a newline character is added to prevent other todo.txt apps to append new todos to the last line
518
519
      window.api.send("writeToFile", [items.objects.join("\n").toString() + "\n"]);
      return Promise.resolve("Success: New todo added to file: " + getActiveFile());
ransome1's avatar
ransome1 committed
520
521
522
523
524
525
526
    } else {
      return Promise.resolve("Info: Todo already in file, nothing will be written");
    }
  } catch (error) {
    return Promise.reject(error);
  }
}
527
async function archiveTodos() {
528
  try {
529
530
    const index = userData.files.findIndex(file => file[0]===1);
    const file = userData.files[index][1];
531
532
533
    // cancel operation if there are no completed todos
    if(items.complete.length===0) return Promise.resolve("Info: No completed todos found, nothing will be archived")
    // if user archives within done.txt file, operating is canceled
534
    if(file.includes("_done.")) return Promise.resolve("Info: Current file seems to be a done.txt file, won't archive")
535
    // define path to done.txt
536
537
    let doneFile = function() {
      if(appData.os==="windows") {
538
        return file.replace(file.split("\\").pop(), file.substr(0, file.lastIndexOf(".")).split("\\").pop() + "_done.txt");
539
      } else {
540
        return file.replace(file.split("/").pop(), file.substr(0, file.lastIndexOf(".")).split("/").pop() + "_done.txt");
541
      }
542
    }
543
    const getContentFromDoneFile = new Promise(function(resolve) {
544
545
546
547
      window.api.send("getContent", doneFile());
      return window.api.receive("getContent", (content) => {
        resolve(content);
      });
548
    });
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
    let contentFromDoneFile = await getContentFromDoneFile;
    let contentForDoneFile = items.complete;
    if(contentFromDoneFile) {
      // create array from done file
      contentFromDoneFile = contentFromDoneFile.split("\n");
      //combine the two arrays
      contentForDoneFile  = contentFromDoneFile.concat(items.complete.toString().split(","));
      // use Set function to remove the duplicates: https://www.javascripttutorial.net/array/javascript-remove-duplicates-from-array/
      contentForDoneFile= [...new Set(contentForDoneFile)];
      // remove empty entries
      contentForDoneFile = contentForDoneFile.filter(function(element) {
        return element;
      });
    }
    //write completed items to done file
    window.api.send("writeToFile", [contentForDoneFile.join("\n").toString() + "\n", doneFile()]);
    // write incompleted items to todo file
566
    window.api.send("writeToFile", [items.incomplete.join("\n").toString() + "\n", file]);
567
568
569
    // send notifcation on success
    generateNotification(null, null, translations.archivingCompletedTitle, translations.archivingCompletedBody + doneFile());

570
    return Promise.resolve("Success: Completed todos appended to: " + doneFile())
571
572
573
574
575
  } catch(error) {
    error.functionName = archiveTodos.name;
    return Promise.reject(error);
  }
}
576
function generateNotification(todo, offset, customTitle, customBody) {
577
578
  try {
    // abort if user didn't permit notifications within sleek
579
    if(!userData.notifications) return Promise.resolve("Info: Notification surpressed (turned off in sleek's settings)");
580
    // check for necessary permissions
581
    return navigator.permissions.query({name: "notifications"}).then(function(result) {
582
583
      // abort if user didn't permit notifications
      if(result.state!="granted") return Promise.resolve("Info: Notification surpressed (not permitted by OS)");
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
      let notification;
      if(todo) {
        // add the offset so a notification shown today with "due tomorrow", will be shown again tomorrow but with "due today"
        const hash = generateHash(todo.toString()) + offset;
        let title;
        switch (offset) {
          case 0:
            title = translations.dueToday;
            break;
          case 1:
            title = translations.dueTomorrow;
            break;
        }
        // if notification already has been triggered once it will be discarded
        if(userData.dismissedNotifications.includes(hash)) return Promise.resolve("Info: Notification skipped (has already been sent)");
        // set options for notifcation
        notification = {
          title: title,
          body: todo.text,
          string: todo.toString(),
          timeoutType: "never",
          silent: false,
          actions: [{
            type: "button",
            text: "Show Button"
          }]
        }
        // once shown, it will be persisted as hash to it won't be shown a second time
        userData.dismissedNotifications.push(hash);
        setUserData("dismissedNotifications", userData.dismissedNotifications);
      } else {
        notification = {
          title: customTitle,
          body: customBody,
          timeoutType: "default",
          silent: true
        }
621
622
      }
      // send notification object to main process for execution
623
      window.api.send("showNotification", notification);
624
      // trigger matomo event
625
      if(userData.matomoEvents) _paq.push(["trackEvent", "Notification", "Shown"]);
626
627
628
629
630
631
632
      return Promise.resolve("Info: Notification successfully sent");
    });
  } catch(error) {
    error.functionName = generateNotification.name;
    return Promise.reject(error);
  }
}
633
634
function generateHash(string) {
  return string.split('').reduce((prevHash, currVal) =>
635
636
637
    (((prevHash << 5) - prevHash) + currVal.charCodeAt(0))|0, 0);
}

638
export { generateItems, generateGroups, generateTable, items, item, setTodoComplete, archiveTodos, addTodo };