Commit 38bf7348 authored by ransome1's avatar ransome1
Browse files

Preserve file history, enhanced hidden todos function

parent abf595b2
......@@ -18,8 +18,7 @@ flatpak/com.github.ransome1.sleek.yml
assets/icons/bak
squashfs-root/
.eslintrc.json
.stylelintrc.json
package-lock.json
.vs/
.vscode/
.stylelintrc.json
src/css/
{
"name": "sleek",
"productName": "sleek",
"version": "1.1.0-rc.4",
"version": "1.1.0-rc.5",
"description": "Todo app based on todo.txt for Linux, Windows and MacOS, free and open-source",
"synopsis": "Todo app based on todo.txt for Linux, Windows and MacOS, free and open-source",
"category": "ProjectManagement",
......@@ -102,7 +102,7 @@
"pack": "yarn build:css && yarn build:pegjs && electron-builder --dir",
"lint": "eslint --ext .js, src --ext .mjs, src",
"test": "mocha --timeout 10000",
"test1": "mocha ./test/createTodos.js --timeout 10000",
"test1": "mocha ./test/onboarding.js --timeout 10000",
"sass": "sass -w src/scss/style.scss:src/css/style.css",
"start": "yarn sass & electron ."
},
......
This diff is collapsed.
This diff is collapsed.
"use strict";
import { resetFilters, resetModal, handleError, userData, appData, setUserData, translations } from "../render.js";
import { resetFilters, resetModal, handleError, userData, appData, setUserData, translations, showOnboarding } from "../render.js";
import { _paq } from "./matomo.mjs";
import { createModalJail } from "../configs/modal.config.mjs";
......@@ -8,22 +8,39 @@ const modalChangeFile = document.getElementById("modalChangeFile");
const modalChangeFileTable = document.getElementById("modalChangeFileTable");
const fileTabBarList = document.querySelector("#fileTabBar ul");
function removeFileFromList(isActive, index) {
function removeFileFromList(index, isTabItem) {
try {
if(isActive && index-1 === -1) {
userData.files[index+1][0] = 1;
} else if(isActive && index-1 >= 0) {
userData.files[index-1][0] = 1;
if(isTabItem) {
let newItemIndex;
userData.files[index][0] = 0;
userData.files[index][2] = 0;
newItemIndex = userData.files.findIndex(file => {
return file[2] === 1;
});
if(newItemIndex >= 0) {
userData.files[newItemIndex][0] = 1;
userData.files[newItemIndex][2] = 1;
resetFilters(true).then(function(response) {
console.log(response);
setUserData("files", userData.files);
window.api.send("startFileWatcher", [userData.files[newItemIndex][1], 1]);
}).catch(function(error) {
handleError(error);
});
} else {
userData.files[index][0] = 0;
userData.files[index][2] = 0;
setUserData("files", userData.files);
if(userData.files.length>0) generateFileList();
showOnboarding(true);
}
} else {
if(userData.files[index][0]) {
showOnboarding(true);
}
userData.files.splice(index, 1);
setUserData("files", userData.files);
}
userData.files.splice(index, 1);
setUserData("files", userData.files);
resetFilters(true).then(function(response) {
console.info(response);
index = userData.files.findIndex(file => file[0] === 1);
window.api.send("startFileWatcher", userData.files[index][1]);
}).catch(function(error) {
handleError(error);
});
return Promise.resolve("Success: File removed from list");
} catch (error) {
return Promise.reject(error);
......@@ -38,7 +55,7 @@ function selectFileFromList(index) {
handleError(error);
});
resetModal().then(response => {
window.api.send("startFileWatcher", userData.files[index][1]);
window.api.send("startFileWatcher", [userData.files[index][1], 1]);
console.info(response);
}).catch(error => {
handleError(error);
......@@ -51,40 +68,43 @@ function selectFileFromList(index) {
function generateFileList() {
try {
if(userData.files.length>1 && userData.fileTabs) {
fileTabBar.classList.add("is-active");
} else {
fileTabBar.classList.remove("is-active");
}
fileTabBar.classList.remove("is-active");
fileTabBarList.innerHTML = null;
modalChangeFileTable.innerHTML = null;
modalChangeFileTable.classList.add("files");
let j = 0;
for (let i = 0; i < userData.files.length; i++) {
let isActive = userData.files[i][0];
let fileName = userData.files[i][1].split("/").pop();
if(appData.os === "windows") fileName = userData.files[i][1].split("\\").pop();
let listItem = document.createElement("li");
listItem.innerHTML = fileName;
listItem.innerHTML += "<i class=\"fas fa-minus-circle\"></i>";
if(isActive===1) listItem.classList.add("is-highlighted");
listItem.querySelector("i").onclick = function() {
removeFileFromList(isActive, i);
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "File-Tab", "Click on remove icon"]);
}
if(!isActive) {
listItem.onclick = function(event) {
if(event.target.classList.contains("fas")) return false;
selectFileFromList(i).then(function(response) {
console.info(response);
}).catch(function(error) {
handleError(error);
});
let isTabItem = userData.files[i][2];
if(isTabItem) {
j++;
let fileName = userData.files[i][1].split("/").pop();
if(j > 1 && userData.fileTabs) fileTabBar.classList.add("is-active");
if(appData.os === "windows") fileName = userData.files[i][1].split("\\").pop();
let listItem = document.createElement("li");
listItem.setAttribute("title", userData.files[i][1]);
listItem.innerHTML = fileName;
listItem.innerHTML += "<i class=\"fas fa-times\"></i>";
if(isActive===1) listItem.classList.add("is-highlighted");
listItem.querySelector("i").onclick = function() {
removeFileFromList(i, isTabItem);
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "File-Tab", "Click on tab"]);
if(userData.matomoEvents) _paq.push(["trackEvent", "File-Tab", "Click on remove icon"]);
}
if(!isActive) {
listItem.onclick = function(event) {
if(event.target.classList.contains("fas")) return false;
selectFileFromList(i).then(function(response) {
console.info(response);
}).catch(function(error) {
handleError(error);
});
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "File-Tab", "Click on tab"]);
}
}
fileTabBarList.appendChild(listItem);
}
fileTabBarList.appendChild(listItem);
let row = modalChangeFileTable.insertRow(-1);
let cell1 = row.insertCell(0);
let cell2 = row.insertCell(1);
......@@ -107,7 +127,7 @@ function generateFileList() {
cell3.innerHTML = "<a href=\"#\" tabindex=\"0\"><i class=\"fas fa-minus-circle\"></i></a>";
cell3.title = translations.delete;
cell3.onclick = function() {
removeFileFromList(isActive, i);
removeFileFromList(i);
generateFileList().then(response => {
modalChangeFile.classList.add("is-active");
modalChangeFile.focus();
......
......@@ -155,7 +155,7 @@ function filterItems(items) {
}
// apply filters
items = items.filter(function(item) {
if(!item.text && !item.h) return false;
//if(!item.text && !item.h) return false;
if(!userData.showHidden && item.h) return false;
if(!userData.showCompleted && item.complete) return false;
if(!userData.showDueIsToday && item.due && isToday(item.due)) return false;
......
......@@ -370,12 +370,12 @@ function show(todo, templated) {
}
function submitForm() {
try {
if(userData.file === undefined) {
modalFormAlert.innerHTML = translations.formErrorWritingFile;
modalFormAlert.parentElement.classList.remove("is-active", 'is-danger');
modalFormAlert.parentElement.classList.add("is-active", 'is-warning');
return Promise.resolve("Info: No todo.txt defined yet");
}
// if(userData.file === undefined) {
// modalFormAlert.innerHTML = translations.formErrorWritingFile;
// modalFormAlert.parentElement.classList.remove("is-active", 'is-danger');
// modalFormAlert.parentElement.classList.add("is-active", 'is-warning');
// return Promise.resolve("Info: No todo.txt defined yet");
// }
// check if there is an input in the text field, otherwise indicate it to the user
// input value and data item are the same, nothing has changed, nothing will be written
if(modalForm.getAttribute("data-item") === document.getElementById("modalFormInput").value) {
......@@ -440,7 +440,7 @@ function submitForm() {
}
//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
window.api.send("writeToFile", [items.objects.join("\n").toString() + "\n", userData.file]);
window.api.send("writeToFile", [items.objects.join("\n").toString() + "\n"]);
// close and reset any modal
resetModal().then(function(result) {
console.log(result);
......@@ -449,11 +449,11 @@ function submitForm() {
});
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "Form", "Submit"]);
return Promise.resolve("Success: Changes written to file: " + userData.file);
return Promise.resolve("Success: Changes written to file");
// if the input field is empty, let users know
} catch (error) {
// if writing into file is denied throw alert
modalFormAlert.innerHTML = translations.formErrorWritingFile + userData.file;
modalFormAlert.innerHTML = translations.formErrorWritingFile;
modalFormAlert.parentElement.classList.add("is-active", 'is-danger');
error.functionName = submitForm.name;
return Promise.reject(error);
......
......@@ -115,10 +115,10 @@ function configureTodoTableTemplate(append) {
function generateItems(content) {
try {
items = { objects: TodoTxt.parse(content, [ new DueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]) }
items.objects = items.objects.filter(function(item) {
if(!item.text && !item.h) return false;
return true;
});
// items.objects = items.objects.filter(function(item) {
// if(!item.text && !item.h) 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() != "" });
......@@ -538,16 +538,18 @@ function addTodo(todo) {
}
async function archiveTodos() {
try {
const index = userData.files.findIndex(file => file[0]===1);
const file = userData.files[index][1];
// 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
if(userData.file.includes("_done.")) return Promise.resolve("Info: Current file seems to be a done.txt file, won't archive")
if(file.includes("_done.")) return Promise.resolve("Info: Current file seems to be a done.txt file, won't archive")
// define path to done.txt
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");
return file.replace(file.split("\\").pop(), file.substr(0, file.lastIndexOf(".")).split("\\").pop() + "_done.txt");
} else {
return userData.file.replace(userData.file.split("/").pop(), userData.file.substr(0, userData.file.lastIndexOf(".")).split("/").pop() + "_done.txt");
return file.replace(file.split("/").pop(), file.substr(0, file.lastIndexOf(".")).split("/").pop() + "_done.txt");
}
}
const getContentFromDoneFile = new Promise(function(resolve) {
......@@ -573,7 +575,7 @@ async function archiveTodos() {
//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]);
window.api.send("writeToFile", [items.incomplete.join("\n").toString() + "\n", file]);
// send notifcation on success
generateNotification(null, null, translations.archivingCompletedTitle, translations.archivingCompletedBody + doneFile());
......
......@@ -123,7 +123,7 @@ const createWindow = async function() {
userData.data.path = path.dirname(file);
userData.set("path", userData.data.path);
console.info("Success: Opened file: " + file);
startFileWatcher(file).then(response => {
startFileWatcher(file, 1).then(response => {
console.info(response);
mainWindow.webContents.send("triggerFunction", "resetModal")
}).catch(error => {
......@@ -153,7 +153,7 @@ const createWindow = async function() {
userData.data.path = path.dirname(file.filePath);
userData.set("path", userData.data.path);
console.info("Success: New file created: " + file.filePath);
startFileWatcher(file.filePath).then(response => {
startFileWatcher(file.filePath, 1).then(response => {
console.info(response);
mainWindow.webContents.send("triggerFunction", "resetModal")
}).catch(error => {
......@@ -167,14 +167,13 @@ const createWindow = async function() {
break;
}
}
const startFileWatcher = function(file) {
const startFileWatcher = function(file, isTabItem) {
try {
if(!fs.existsSync(file)) throw("Error: File not found on disk")
// skip persisted files and go with ENV if set
if(process.env.SLEEK_CUSTOM_FILE && fs.existsSync(process.env.SLEEK_CUSTOM_FILE)) {
file = process.env.SLEEK_CUSTOM_FILE;
}
let args;
if (process.defaultApp) {
// electron "unbundled" app -- have to skip "electron" and script name arg eg: "."
args = process.argv.slice(2);
......@@ -192,6 +191,7 @@ const createWindow = async function() {
// if path is found it is set active
if(element[1]===file) {
element[0] = 1
if(isTabItem) element[2] = 1;
fileFound = true;
// if this entry is not equal to the new path it is set 0
} else {
......@@ -202,10 +202,10 @@ const createWindow = async function() {
userData.data.files = new Array;
}
// only push new path if it is not already in the user data
if((!fileFound || !userData.data.files) && file) userData.data.files.push([1, file]);
if((!fileFound || !userData.data.files) && file) userData.data.files.push([1, file, 1]);
userData.set("files", userData.data.files);
userData.data.file = file;
userData.set("file", file);
//userData.data.file = file;
//userData.set("file", file);
// TODO describe
if(fileWatcher) fileWatcher.close();
fileWatcher = chokidar.watch(file);
......@@ -589,7 +589,7 @@ const createWindow = async function() {
type: "radio",
checked: false,
click: function() {
startFileWatcher(file[1]);
startFileWatcher(file[1], 1);
mainWindow.show();
mainWindow.setSkipTaskbar(true);
}
......@@ -694,8 +694,15 @@ const createWindow = async function() {
})
.on("writeToFile", function(event, args) {
try {
let file;
if(!args[1]) {
const index = userData.data.files.findIndex(file => file[0] ===1 );
file = userData.data.files[index][1];
} else {
file = args[1];
}
// Write content to file
fs.writeFileSync(args[1], args[0], {encoding: "utf-8"});
if(file) fs.writeFileSync(file, args [0], {encoding: "utf-8"});
} catch(error) {
console.error(error);
error.functionName = "fs.writeFileSync";
......@@ -708,8 +715,8 @@ const createWindow = async function() {
.on("openOrCreateFile", (event, args) => {
openDialog(args);
})
.on("startFileWatcher", (event, file) => {
startFileWatcher(file).then(response => {
.on("startFileWatcher", (event, data) => {
startFileWatcher(data[0], data[1]).then(response => {
console.info(response);
}).catch(error => {
console.error(error);
......@@ -763,8 +770,9 @@ const createWindow = async function() {
// REFRESH WHEN IN BACKGROUND
// ########################################################################################################################
setInterval(() => {
if(userData.data.file && !mainWindow.isFocused()) {
getContent(userData.data.file).then(content => {
if(userData.data.files.length > 0 && !mainWindow.isFocused()) {
const index = userData.data.files.findIndex(file => file[0] ===1 );
getContent(userData.data.files[index][1]).then(content => {
mainWindow.webContents.send("refresh", [content])
}).catch(error => {
console.error(error);
......
......@@ -134,19 +134,19 @@ function configureMainView() {
handleError(error);
});
// configure table view
if(userData.file && todos.items.objects.length===0) {
if(todos.items.objects.length===0) {
addTodoContainer.classList.add("is-active");
todoTableSearchContainer.classList.remove("is-active");
todoTable.classList.remove("is-active");
noResultContainer.classList.remove("is-active");
return Promise.resolve("Info: File is empty");
} else if(userData.file && todos.items.filtered.length===0) {
} else if(todos.items.filtered.length===0) {
addTodoContainer.classList.remove("is-active");
todoTableSearchContainer.classList.add("is-active");
noResultContainer.classList.add("is-active");
return Promise.resolve("Info: No results");
// TODO explain
} else if(userData.file && todos.items.filtered.length>0) {
} else if(todos.items.filtered.length>0) {
todoTableSearchContainer.classList.add("is-active");
addTodoContainer.classList.remove("is-active");
noResultContainer.classList.remove("is-active");
......@@ -318,7 +318,19 @@ function registerEvents() {
if(userData.matomoEvents) matomo._paq.push(["trackEvent", "Onboarding", "Click on Create file"]);
}
btnOnboardingOpenTodoFile.onclick = function() {
window.api.send("openOrCreateFile", "open");
//TODO: thhis is a duplicate
if(typeof userData.files === "object" && userData.files.length>0) {
files.generateFileList().then(response => {
console.info(response);
modalChangeFile.classList.add("is-active");
modalChangeFile.focus();
createModalJail(modalChangeFile);
}).catch(error => {
handleError(error);
});
} else {
window.api.send("openOrCreateFile", "open");
}
// trigger matomo event
if(userData.matomoEvents) matomo._paq.push(["trackEvent", "Onboarding", "Click on Open file"]);
}
......@@ -386,37 +398,40 @@ function registerKeyboardShortcuts() {
}
// close tab or window
if((event.ctrlKey || event.metaKey) && event.key === "w") {
if(userData.files.length > 1) {
const isTabFound = userData.files.findIndex(file => {
return file[2] === 1;
});
if(isTabFound >= 0) {
let index = userData.files.findIndex(file => file[0] === 1);
files.removeFileFromList(1, index);
files.removeFileFromList(index, 1);
} else {
window.api.send("closeWindow");
}
}
}, true)
window.addEventListener("keyup", function(event) {
// switch files
const regex=/^[1-9]+$/;
if(event.key.match(regex) && userData.files.length > 1 && userData.files[event.key-1] && !modalForm.classList.contains("is-active") && (document.activeElement.id!="todoTableSearch" && document.activeElement.id!="filterContextInput" && document.activeElement.id!="modalFormInput")) {
if(userData.files[event.key-1][1]) window.api.send("startFileWatcher", userData.files[event.key-1][1]);
}
// cycle through tabs
if(event.ctrlKey && !event.shiftKey && event.keyCode === 9) {
let index = userData.files.findIndex(file => file[0] === 1);
if(!userData.files[index+1]) {
window.api.send("startFileWatcher", userData.files[0][1]);
window.api.send("startFileWatcher", [userData.files[0][1], 1]);
} else {
window.api.send("startFileWatcher", userData.files[index+1][1]);
window.api.send("startFileWatcher", [userData.files[index+1][1], 1]);
}
}
if(event.ctrlKey && event.shiftKey && event.keyCode === 9) {
let index = userData.files.findIndex(file => file[0] === 1);
if(!userData.files[index-1]) {
window.api.send("startFileWatcher", userData.files[userData.files.length-1][1]);
window.api.send("startFileWatcher", [userData.files[userData.files.length-1][1], 1]);
} else {
window.api.send("startFileWatcher", userData.files[index-1][1]);
window.api.send("startFileWatcher", [userData.files[index-1][1], 1]);
}
}
}, true)
window.addEventListener("keyup", function(event) {
// switch files
const regex=/^[1-9]+$/;
if(event.key.match(regex) && userData.files.length > 1 && userData.files[event.key-1] && !modalForm.classList.contains("is-active") && (document.activeElement.id!="todoTableSearch" && document.activeElement.id!="filterContextInput" && document.activeElement.id!="modalFormInput")) {
if(userData.files[event.key-1][1]) window.api.send("startFileWatcher", [userData.files[event.key-1][1]]);
}
// open settings
if(event.key === "," && !modalForm.classList.contains("is-active") && (document.activeElement.id!="todoTableSearch" && document.activeElement.id!="filterContextInput" && document.activeElement.id!="modalFormInput")) {
content.showContent("modalSettings").then(function(response) {
......@@ -435,9 +450,7 @@ function registerKeyboardShortcuts() {
}
// create new todo
if(event.key==="n" && !modalForm.classList.contains("is-active") && (document.activeElement.id!="todoTableSearch" && document.activeElement.id!="filterContextInput" && document.activeElement.id!="modalFormInput")) {
// abort when onboarding is shown
if(onboarding) return false;
form.show().then(function(response) {
console.info(response);
}).catch(function(error) {
......@@ -446,9 +459,7 @@ function registerKeyboardShortcuts() {
}
// reset filters
if(event.key==="0" && !modalForm.classList.contains("is-active") && (document.activeElement.id!="todoTableSearch" && document.activeElement.id!="filterContextInput" && document.activeElement.id!="modalFormInput")) {
// abort when onboarding is shown
if(onboarding) return false;
resetFilters(true).then(function(response) {
console.info(response);
}).catch(function(error) {
......@@ -457,15 +468,22 @@ function registerKeyboardShortcuts() {
}
// toggle completed todos
if(event.key==="h" && !modalForm.classList.contains("is-active") && (document.activeElement.id!="todoTableSearch" && document.activeElement.id!="filterContextInput" && document.activeElement.id!="modalFormInput")) {
// abort when onboarding is shown
if(onboarding) return false;
view.toggle("showCompleted").then(function(response) {
console.info(response);
}).catch(function(error) {
handleError(error);
});
}
// toggle deferred todos
if(event.key==="t" && !modalForm.classList.contains("is-active") && (document.activeElement.id!="todoTableSearch" && document.activeElement.id!="filterContextInput" && document.activeElement.id!="modalFormInput")) {
if(onboarding) return false;
view.toggle("deferredTodos").then(function(response) {
console.info(response);
}).catch(function(error) {
handleError(error);
});
}
// archive todos
if(event.key==="a" && !modalForm.classList.contains("is-active") && (document.activeElement.id!="todoTableSearch" && document.activeElement.id!="filterContextInput" && document.activeElement.id!="modalFormInput")) {
// abort when onboarding is shown
......@@ -497,7 +515,7 @@ function registerKeyboardShortcuts() {
}
}, true)
// shortcuts for modal form
modalForm.addEventListener ("keyup", function(event) {
modalForm.addEventListener ("keydown", function(event) {
// priority up
if(!(event.ctrlKey || event.metaKey) && event.altKey && event.key === "ArrowUp") {
form.setPriority("up");
......@@ -768,11 +786,10 @@ window.onload = async function () {
filters = await import("./js/filters.mjs");
drawer = await import("./js/drawer.mjs");
files = await import("./js/files.mjs");
if(userData.file) {
window.api.send("startFileWatcher", userData.file);
// for users who upgrade from very old versions
} else if(userData.pathToFile) {
window.api.send("startFileWatcher", userData.pathToFile);
//TODO: Refactoring
if(userData.files) {
const index = userData.files.findIndex(file => file[0] ===1 );
window.api.send("startFileWatcher", [userData.files[index][1], 0]);
} else {
showOnboarding(true).then(function(response) {
console.info(response);
......@@ -899,4 +916,4 @@ window.api.receive("refresh", async (args) => {
});
});
export { resetFilters, resetModal, setUserData, startBuilding, handleError, userData, appData, translations, modal, setTheme, getConfirmation };
export { showOnboarding, resetFilters, resetModal, setUserData, startBuilding, handleError, userData, appData, translations, modal, setTheme, getConfirmation };
......@@ -28,7 +28,7 @@
i {
position: absolute;
top: 0.65em;
right: 0.5em;
right: 0.85em;
color: $light-grey;
padding-left: 0.5em;
}
......
......@@ -6,3 +6,4 @@ x 2021-06-19 2021-06-19 test todo
x 2021-07-05 2021-07-05 test todo
x 2021-07-15 2021-07-15 test todo
x 2021-07-17 2021-07-17 test todo
x 2021-07-25 2021-07-25 test todo
{"theme":"light","width":1538,"height":863,"horizontal":2450,"vertical":280,"maximizeWindow":false,"notifications":true,"useTextarea":false,"compactView":false,"matomoEvents":false,"drawerWidth":"500","showDueIsPast":true,"showDueIsFuture":true,"showDueIsToday":true,"showHidden":false,"showCompleted":true,"sortCompletedLast":false,"sortBy":["priority","dueString","contexts","projects"],"zoom":"100","tray":false,"showEmptyFilters":true,"dismissedNotifications":[-1319247018],"dismissedMessages":[],"hideFilterCategories":[],"language":"en","uid":"TESTING","path":"test/preferences_empty/todo.txt","files":[[1,"test/preferences_empty/todo.txt"]],"file":"test/preferences_empty/todo.txt","filterDrawer":false,"selectedFilters":[],"viewDrawer":false,"sortByLevel":["priority","dueString","contexts","projects"],"deferredTodos":true,"fileTabs":true}
\ No newline at end of file
{"theme":"light","width":1538,"height":863,"horizontal":46,"vertical":217,"maximizeWindow":false,"notifications":true,"useTextarea":false,"compactView":false,"matomoEvents":false,"drawerWidth":"500","showDueIsPast":true,"showDueIsFuture":true,"showDueIsToday":true,"showHidden":false,"showCompleted":true,"sortCompletedLast":false,"sortBy":["priority","dueString","contexts","projects"],"zoom":"100","tray":false,"showEmptyFilters":true,"dismissedNotifications":[-1319247018],"dismissedMessages":[],"hideFilterCategories":[],"language":"en","uid":"TESTING","path":"test/preferences_empty/todo.txt","files":[[1,"test/preferences_empty/todo.txt"]],"file":"test/preferences_empty/todo.txt","filterDrawer":false,"selectedFilters":[],"viewDrawer":false,"sortByLevel":["priority","dueString","contexts","projects"],"deferredTodos":true,"fileTabs":true}
\ No newline at end of file
{"theme":"light","width":1538,"height":863,"horizontal":2181,"vertical":577,"maximizeWindow":false,"notifications":true,"useTextarea":false,"compactView":false,"matomoEvents":false,"drawerWidth":"500","showDueIsPast":true,"showDueIsFuture":true,"showDueIsToday":true,"showHidden":false,"showCompleted":true,"sortCompletedLast":false,"sortBy":["priority","dueString","contexts","projects"],"zoom":"100","tray":false,"showEmptyFilters":true,"dismissedNotifications":[-1319247018],"dismissedMessages":[],"hideFilterCategories":[],"language":"en","uid":"TESTING","path":"test/preferences_existent/todo.txt","files":[[1,"test/preferences_existent/todo.txt"]],"file":"test/preferences_existent/todo.txt","filterDrawer":false,"selectedFilters":[],"viewDrawer":false,"sortByLevel":["priority","dueString","contexts","projects"],"deferredTodos":true,"fileTabs":true}
\ No newline at end of file
{"theme":"light","width":1538,"height":863,"horizontal":382,"vertical":577,"maximizeWindow":false,"notifications":true,"useTextarea":false,"compactView":false,"matomoEvents":false,"drawerWidth":"500","showDueIsP