todos.mjs 27.9 KB
Newer Older
1
"use strict";
2
3
import { userData, appData, handleError, translations, setUserData, startBuilding } from "../render.js";
import { _paq } from "./matomo.mjs";
4
5
6
7
import { categories } from "./filters.mjs";
import { generateRecurrence } from "./recurrences.mjs";
import { convertDate, isToday, isTomorrow, isPast } from "./date.mjs";
import { show } from "./form.mjs";
8
import { RecExtension, SugarDueExtension } from "./todotxtExtensions.mjs";
9
10
import "../../node_modules/marked/marked.min.js";

11
12
import "../../node_modules/jstodotxt/jsTodoExtensions.js";
import "../../node_modules/jstodotxt/jsTodoTxt.js";
13

14
const modalForm = document.getElementById("modalForm");
15
const todoContext = document.getElementById("todoContext");
16
const todoContextDelete = document.getElementById("todoContextDelete");
17
18
19
20
const todoContextEdit = document.getElementById("todoContextEdit");
const todoContextUseAsTemplate = document.getElementById("todoContextUseAsTemplate");
const todoTableContainer = document.getElementById("todoTableContainer");
const todoTableWrapper = document.getElementById("todoTableWrapper");
21
22
23
24

todoContextUseAsTemplate.innerHTML = translations.useAsTemplate;
todoContextEdit.innerHTML = translations.edit;
todoContextDelete.innerHTML = translations.delete;
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ########################################################################################################################
// 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) + " [...] ";
43
    return `${text} <a href="${href}" target="_blank"><i class="fas fa-external-link-alt"></i></a>`;
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  }
};
marked.use({ renderer });
// ########################################################################################################################
// PREPARE TABLE
// ########################################################################################################################
const tableContainerContent = document.createDocumentFragment();
const todoTableBodyRowTemplate = document.createElement("div");
const todoTableBodyCellCheckboxTemplate  = document.createElement("div");
const todoTableBodyCellTextTemplate = document.createElement("a");
const tableContainerCategoriesTemplate = document.createElement("span");
const todoTableBodyCellPriorityTemplate = document.createElement("div");
const todoTableBodyCellSpacerTemplate = document.createElement("div");
const todoTableBodyCellDueDateTemplate = document.createElement("span");
const todoTableBodyCellRecurrenceTemplate = document.createElement("span");
const item = { previous: "" }
60
61
62
let
  items,
  clusterCounter,
63
  clusterSize = Math.ceil(window.innerHeight/30), // 35 being the pixel height of one todo in compact mode
64
65
66
  clusterThreshold = 0,
  stopBuilding = false,
  visibleRows = 0;
67

68
69
70
todoTableWrapper.addEventListener("scroll", function(event) {
  if(Math.floor(event.target.scrollHeight - event.target.scrollTop) <= event.target.clientHeight && visibleRows<items.filtered.length) {
    stopBuilding = false;
71
    startBuilding(true);
72
73
  }
});
74
75
76
todoContext.addEventListener("keyup", function(event) {
  if(event.key==="Escape") this.classList.remove("is-active");
});
77
78
79
80
81
82
83
84
85

function configureTodoTableTemplate(append) {
  try {
    if(!append) {
      todoTableContainer.innerHTML = "";
      visibleRows = 0;
      clusterThreshold = 0;
      stopBuilding = false;
    }
86
87
88
    todoTableBodyRowTemplate.setAttribute("class", "todo");
    todoTableBodyCellCheckboxTemplate.setAttribute("class", "cell checkbox");
    todoTableBodyCellTextTemplate.setAttribute("class", "cell text");
89
90
91
92
    todoTableBodyCellTextTemplate.setAttribute("tabindex", 0);
    todoTableBodyCellTextTemplate.setAttribute("href", "#");
    todoTableBodyCellTextTemplate.setAttribute("title", translations.editTodo);
    tableContainerCategoriesTemplate.setAttribute("class", "categories");
93
94
    todoTableBodyCellDueDateTemplate.setAttribute("class", "cell itemDueDate");
    todoTableBodyCellRecurrenceTemplate.setAttribute("class", "cell recurrence");
95
96
97
98
99
100
    return Promise.resolve("Success: Table templates set up");
  } catch(error) {
    error.functionName = configureTodoTableTemplate.name;
    return Promise.reject(error);
  }
}
101
102
function generateItems(content) {
  try {
Daniel Weisser's avatar
Daniel Weisser committed
103
    items = { objects: TodoTxt.parse(content, [ new SugarDueExtension(), new RecExtension(), new HiddenExtension() ]) }
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
    items.objects = items.objects.filter(function(item) {
      if(!item.text) return false;
      return true;
    });
    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) {
  // after filters have been built a last selection has to be made including the previous filter choices
  items = items.filter(function(item) {
    if(!checkIsTodoVisible(item)) return false;
    return true;
  });
  // build object according to sorting method
  items = items.reduce((object, a) => {
    if(userData.sortCompletedLast && a.complete) {
      object["completed"] = [...object["completed"] || [], a];
    } else if(userData.sortBy==="dueString" && !a.due) {
      object["noDueDate"] = [...object["noDueDate"] || [], a];
    } else {
      object[a[userData.sortBy]] = [...object[a[userData.sortBy]] || [], a];
    }
    //object[a[userData.sortBy]] = [...object[a[userData.sortBy]] || [], a];
    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;
    });
  }
154
155
156
157
  // sort the items within the groups
  items.forEach((group) => {
    group[1] = sortTodoData(group[1]);
  });
158
159
  return Promise.resolve(items)
}
160
function generateTable(groups, append) {
161
  // prepare the templates for the table
162
163
  return configureTodoTableTemplate(append).then(function(response) {
    clusterCounter = 0;
164
    console.info(response);
165
    for (let group in groups) {
166
167
168
169
      if(stopBuilding) {
        stopBuilding = false;
        break;
      }
170
      // create a divider row
171
      let dividerRow;
172
173
      // completed todos
      if(userData.sortCompletedLast && groups[group][0]==="completed") {
174
        dividerRow = document.createRange().createContextualFragment("<div id=\"" + userData.sortBy + groups[group][0] + "\" class=\"group " + userData.sortBy + " " + groups[group][0] + "\"><div class=\"cell\"></div></div>")
175
176
      // for priority, context and project
      } else if(groups[group][0]!="null" && userData.sortBy!="dueString") {
177
        dividerRow = document.createRange().createContextualFragment("<div id=\"" + userData.sortBy + groups[group][0] + "\" class=\"group " + userData.sortBy + " " + groups[group][0] + "\"><div class=\"cell\"><span class=\"button " + groups[group][0] + "\">" + groups[group][0].replace(/,/g, ', ') + "</span></div></div>")
178
179
180
      // if sorting is by due date
      } else if(userData.sortBy==="dueString" && groups[group][1][0].due) {
        if(isToday(groups[group][1][0].due)) {
181
          dividerRow= document.createRange().createContextualFragment("<div id=\"" + userData.sortBy + groups[group][0] + "\" class=\"group due\"><div class=\"cell isToday\"><span class=\"button\">" + translations.today + "</span></div></div>")
182
        } else if(isTomorrow(groups[group][1][0].due)) {
183
          dividerRow = document.createRange().createContextualFragment("<div id=\"" + userData.sortBy + groups[group][0] + "\" class=\"group due\"><div class=\"cell isTomorrow\"><span class=\"button\">" + translations.tomorrow + "</span></div></div>")
184
        } else if(isPast(groups[group][1][0].due)) {
185
          dividerRow = document.createRange().createContextualFragment("<div id=\"" + userData.sortBy + groups[group][0] + "\" class=\"group due\"><div class=\"cell isPast\"><span class=\"button\">" + groups[group][0] + "</span></div></div>")
186
        } else {
187
          dividerRow = document.createRange().createContextualFragment("<div id=\"" + userData.sortBy + groups[group][0] + "\" class=\"group due\"><div class=\"cell\"><span class=\"button\">" + groups[group][0] + "</span></div></div>")
188
189
        }
      // create an empty divider row
190
      } else {
191
        dividerRow = document.createRange().createContextualFragment("<div class=\"group\"></div>")
192
      }
193
      // add divider row only if it doesn't exist yet
194
      if(!append && !document.getElementById(userData.sortBy + groups[group][0]) && dividerRow) tableContainerContent.appendChild(dividerRow);
195
196
      for (let item in groups[group][1]) {
        let todo = groups[group][1][item];
197
        // if this todo is not a recurring one the rec value will be set to null
198
        if(!todo.rec) todo.rec = null;
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
        // 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);
            });
          }
        }
216
        //
217
218
219
220
221
222
223
224
        if(clusterCounter<clusterThreshold) {
          clusterCounter++;
          continue;
        } else if((visibleRows===clusterSize+clusterThreshold) || visibleRows===items.filtered.length ) {
          clusterThreshold = visibleRows;
          stopBuilding = true;
          break;
        }
225
226
227
228
229
        tableContainerContent.appendChild(generateTableRow(todo));
      }
      // TODO add a catch
    }
    todoTableContainer.appendChild(tableContainerContent);
230

231
    return new Promise(function(resolve) {
232
      resolve("Success: Todo table generated");
233
234
235
236
237
238
239
    });
  }).catch(error => {
    handleError(error);
  });
}
function generateTableRow(todo) {
  try {
240
    clusterCounter++;
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
    visibleRows++;
    // 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 todoTableBodyCellSpacer = todoTableBodyCellSpacerTemplate.cloneNode(true);
    let todoTableBodyCellDueDate = todoTableBodyCellDueDateTemplate.cloneNode(true);
    let todoTableBodyCellRecurrence = todoTableBodyCellRecurrenceTemplate.cloneNode(true);
    // 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) {
258
      todoTableBodyRow.setAttribute("class", "todo completed");
259
260
261
262
    }
    todoTableBodyRow.setAttribute("data-item", todo.toString());
    // add the priority marker or a white spacer
    if(todo.priority && userData.sortBy==="priority") {
263
      todoTableBodyCellPriority.setAttribute("class", "cell priority " + todo.priority);
264
265
      todoTableBodyRow.appendChild(todoTableBodyCellPriority);
    } else if(!todo.priority && userData.sortBy==="priority") {
266
      todoTableBodyCellSpacer.setAttribute("class", "cell spacer");
267
268
269
270
271
272
273
274
275
276
277
278
279
      todoTableBodyRow.appendChild(todoTableBodyCellSpacer);
    }
    // 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
280
      setTodoComplete(this.parentElement.getAttribute("data-item")).then(response => {
281
282
283
284
285
         console.log(response);
      }).catch(error => {
        handleError(error);
      });
      // trigger matomo event
286
      if(userData.matomoEvents) _paq.push(["trackEvent", "Todo-Table", "Click on Checkbox"]);
287
288
289
290
291
292
    }
    todoTableBodyRow.appendChild(todoTableBodyCellCheckbox);
    // creates cell for the text
    if(todo.text) {
      if(todo.priority && userData.sortBy!="priority") todoTableBodyCellText.innerHTML = "<span class=\"priority\"><span class=\"button " + todo.priority + "\">" + todo.priority + "</span></span>";
      // parse text string through markdown parser
293
      todoTableBodyCellText.innerHTML +=  "<span class=\"text\">" + marked.parseInline(todo.text) + "</span>";
294
      // replace line feed character with a space
295
296
297
298
      todoTableBodyCellText.innerHTML = todoTableBodyCellText.innerHTML.replaceAll(String.fromCharCode(16)," ");
      // add a spacer to divide text (and link) and categories
      todoTableBodyCellText.innerHTML += " ";
    }
299
    // click on the text
300
301
302
    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')) {
303
        show(this.parentElement.getAttribute("data-item"));
304
        // trigger matomo event
305
        if(userData.matomoEvents) _paq.push(["trackEvent", "Todo-Table", "Click on Todo item"]);
306
307
      }
    }
308
309
310
311
312
313
314
315
316
317
318
319
320
    // cell for the categories
    categories.forEach(category => {
      if(todo[category] && category!="priority") {
        todo[category].forEach(el => {
          let todoTableBodyCellCategory = document.createElement("span");
          todoTableBodyCellCategory.setAttribute("class", "tag " + category);
          todoTableBodyCellCategory.innerHTML = el;
          tableContainerCategories.appendChild(todoTableBodyCellCategory);
        });
      }
    });
    // only add the categories to text cell if it has child nodes
    if(tableContainerCategories.hasChildNodes()) todoTableBodyCellText.appendChild(tableContainerCategories);
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
    // 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>
        <i class="fas fa-sort-down"></i>
      `;
      // 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);

352
    todoTableBodyRow.addEventListener("contextmenu", event => {
353
      todoContext.focus();
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
      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
      todoContext.firstElementChild.children[0].onclick = function() {
        show(this.parentElement.parentElement.getAttribute('data-item'), true);
        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"]);
      }
      todoContext.firstElementChild.children[1].onclick = function() {
        show(this.parentElement.parentElement.getAttribute('data-item'));
        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
      todoContext.firstElementChild.children[2].onclick = function() {
        // passing the data-item attribute of the parent tag to complete function
        setTodoDelete(this.parentElement.parentElement.getAttribute('data-item')).then(response => {
          console.log(response);
          todoContext.classList.toggle("is-active");
          todoContext.removeAttribute("data-item");
        }).catch(error => {
          handleError(error);
384
385
        });
        // trigger matomo event
386
        if(userData.matomoEvents) _paq.push(["trackEvent", "Todo-Table-Context", "Click on Delete"]);
387
      }
388
389
    });

390
391
392
393
394
395
396
397
398
399
400
401
    // return the fully built row
    return todoTableBodyRow;
  } catch(error) {
    error.functionName = generateTableRow.name;
    return Promise.reject(error);
  }
}
function sortTodoData(group) {
  try {
    // first sort by priority
    group = group.sort(function(a, b) {
      // most recent or already past todo will be sorted to the top
402
403
404
405
406
      if(a.priority===null && b.priority===null) {
        return 0;
      } else if(a.priority===null) {
        return 1;
      } else if(a.priority!==null && b.priority===null) {
407
        return -1;
408
      } else if(a.priority > b.priority) {
409
        return 1;
410
411
      } else if(a.priority < b.priority) {
        return -1;
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
      } else {
        // if all fail, no change to sort order
        return 0;
      }
    });
    // second sort by due date
    group = group.sort(function(a, b) {
      // when a is smaller than b it, a is put after b
      if(a.priority===b.priority && a.due < b.due) return -1
      // when a is is undefined but b is not, b is put before a
      if(a.priority===b.priority && !a.due && b.due) return 1
      // when b is is undefined but a is not, a is put before b
      if(a.priority===b.priority && a.due && !b.due) return -1
      // if all fail, no change to sort order
      return 0;
    });
    return group;
  } catch(error) {
    error.functionName = sortTodoData.name;
    return Promise.reject(error);
  }
}
function setTodoComplete(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
437
    //if(modalForm.elements[0].value) todo = modalForm.elements[0].value;
438
    // first convert the string to a todo.txt object
Daniel Weisser's avatar
Daniel Weisser committed
439
    todo = new TodoTxtItem(todo, [ new SugarDueExtension(), new RecExtension(), new HiddenExtension() ]);
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
    // 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)
    }
    //write the data to the file
470
    window.api.send("writeToFile", [items.objects.join("\n").toString() + "\n", userData.file]);
471
472
473
474
475
476
477
478
479
480
481
    return Promise.resolve("Success: Changes written to file: " + userData.file);
  } 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
    if(modalForm.elements[0].value) todo = modalForm.elements[0].value;
    // first convert the string to a todo.txt object
Daniel Weisser's avatar
Daniel Weisser committed
482
    todo = new TodoTxtItem(todo, [ new SugarDueExtension(), new RecExtension(), new HiddenExtension() ]);
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
    // 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
499
    window.api.send("writeToFile", [items.objects.join("\n").toString() + "\n", userData.file]);
500
501
502
503
504
505
    return Promise.resolve("Success: Changes written to file: " + userData.file);
  } catch(error) {
    error.functionName = setTodoDelete.name;
    return Promise.reject(error);
  }
}
506
async function archiveTodos() {
507
508
509
510
  try {
    // 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
511
    if(userData.file.includes("_done.")) return Promise.resolve("Info: Current file seems to be a done.txt file, won't archive")
512
    // define path to done.txt
513
514
515
    let doneFile = function() {
      if(appData.os==="windows") {
        return userData.file.replace(userData.file.split("\\").pop(), userData.file.substr(0, userData.file.lastIndexOf(".")).split("\\").pop() + "_done.txt");
516
      } else {
517
        return userData.file.replace(userData.file.split("/").pop(), userData.file.substr(0, userData.file.lastIndexOf(".")).split("/").pop() + "_done.txt");
518
      }
519
    }
520
    const getContentFromDoneFile = new Promise(function(resolve) {
521
522
523
524
      window.api.send("getContent", doneFile());
      return window.api.receive("getContent", (content) => {
        resolve(content);
      });
525
    });
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
    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
    window.api.send("writeToFile", [items.incomplete.join("\n").toString() + "\n", userData.file]);
544
545
546
    // send notifcation on success
    generateNotification(null, null, translations.archivingCompletedTitle, translations.archivingCompletedBody + doneFile());

547
    return Promise.resolve("Success: Completed todos appended to: " + doneFile())
548
549
550
551
552
553
554
555
556
557
  } catch(error) {
    error.functionName = archiveTodos.name;
    return Promise.reject(error);
  }
}
function checkIsTodoVisible(todo) {
  if(!userData.showHidden && todo.h) return false
  if(!todo.text) return false
  return true;
}
558
function generateNotification(todo, offset, customTitle, customBody) {
559
560
  try {
    // abort if user didn't permit notifications within sleek
561
    if(!userData.notifications) return Promise.resolve("Info: Notification surpressed (turned off in sleek's settings)");
562
    // check for necessary permissions
563
    return navigator.permissions.query({name: "notifications"}).then(function(result) {
564
565
      // abort if user didn't permit notifications
      if(result.state!="granted") return Promise.resolve("Info: Notification surpressed (not permitted by OS)");
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
      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.due.toISOString().slice(0, 10) + todo.text) + offset;
        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
        }
604
605
      }
      // send notification object to main process for execution
606
      window.api.send("showNotification", notification);
607
      // trigger matomo event
608
      if(userData.matomoEvents) _paq.push(["trackEvent", "Notification", "Shown"]);
609
610
611
612
613
614
615
      return Promise.resolve("Info: Notification successfully sent");
    });
  } catch(error) {
    error.functionName = generateNotification.name;
    return Promise.reject(error);
  }
}
616
617
function generateHash(string) {
  return string.split('').reduce((prevHash, currVal) =>
618
619
620
    (((prevHash << 5) - prevHash) + currVal.charCodeAt(0))|0, 0);
}

621
export { generateItems, generateGroups, generateTable, items, item, visibleRows, setTodoComplete, archiveTodos };