Commit 8128fef1 authored by ransome1's avatar ransome1
Browse files

Adding modules, added new line char after todo, optimized window state and size

parent efeac2bc
......@@ -18,3 +18,5 @@ flatpak/generated-sources.json
flatpak/com.github.ransome1.sleek.yml
assets/icons/bak
squashfs-root/
test/
src/__tests__
......@@ -6,19 +6,19 @@
+ [Get it from Snap Store](#get-sleek-from-snap-store)
+ [Get it from Flathub](#get-sleek-from-flathub)
+ [Get it from Arch User Repository](#get-sleek-from-arch-user-repository)
+ [Download it on Github](#download-sleek-on-github)
+ [Download it](#download-sleek)
+ [Build sleek from source code](#build-sleek-from-source-code)
+ [sleeks Roadmap 2021](#sleeks-roadmap-2021)
+ [Features](#features)
+ [Used libraries](#used-libraries)
sleek is an open-source todo app that makes use of the todo.txt format. sleeks GUI is modern and clean yet offers a decent set of functions which help users getting things done. sleek is available as a client for Windows, MacOS and Linux.
sleek is an open-source todo app that makes use of the todo.txt format. sleeks GUI is modern and simple but still offers a decent set of functions which help users getting things done. sleek is available as a client for Windows, MacOS and Linux.
By using sleeks GUI or simply writing in plain text todo.txt format, users can add contexts, projects, priorities, due dates or recurrences to their todos and use these todo.txt attributes as filters or search for them by full text search.
Users can add contexts, projects, priorities, due dates or recurrences to their todos. These todo.txt attributes can then be used as filters or to group and sort the todo list.
sleek watches todo.txt files continuously for changes so it can be used with other todo.txt apps. Users can switch between bright and dark mode, choose several languages and manage multiple todo.txt files.
sleek manages and watches multiple todo.txt files continuously for changes, which makes it easy to integrate sleek with other todo.txt apps. Also users can switch between bright and dark mode and choose from multiple languages.
The todo list can be sorted and grouped by priorities or due dates. Todos with due date or repeating todos will trigger alarms with thresholds of 1 or 2 days before the due date. Completed todos can be hidden or archived into separate done.txt files and if users have tons of todos, a compact view can come in handy.
Todos with due date or repeating todos will trigger notifications and completed todos can be hidden or archived into separate done.txt files. If users have tons of todos, a compact view can come in handy.
### Screenshots
......
{
"name": "sleek",
"productName": "sleek",
"version": "1.0.2",
"version": "1.0.3",
"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",
......@@ -106,7 +106,9 @@
"bulma": "^0.9.2",
"i18next": "^20.2.2",
"i18next-browser-languagedetector": "^6.1.0",
"i18next-fs-backend": "^1.1.1"
"i18next-fs-backend": "^1.1.1",
"marked": "^2.0.3",
"vanillajs-datepicker": "^1.1.4"
},
"devDependencies": {
"chai": "^4.3.4",
......
......@@ -303,8 +303,8 @@ nav {
background: #2d2d2d !important; }
.dueDate svg,
.dueDate #dueDatePickerInput,
.dueDate #dueDatePickerInput::placeholder {
.dueDate #datePickerInput,
.dueDate #datePickerInput::placeholder {
color: white !important;
background: transparent !important;
cursor: pointer; }
......
......@@ -345,7 +345,7 @@ nav {
#zoomUndo {
cursor: pointer; }
#dueDatePickerInput {
#datePickerInput {
cursor: pointer; }
#todoTableSearchContainer {
......@@ -671,7 +671,7 @@ nav {
color: white;
background: #2a8971; }
.contexts .button.is-dark {
.contexts .button.is-dark, .contexts .button.is-dark:hover, .contexts .button.is-dark.is-hovered {
background: #184e41;
color: white; }
......@@ -682,25 +682,26 @@ nav {
color: white;
background: #953395; }
.projects .button.is-dark {
.projects .button.is-dark, .projects .button.is-dark:hover, .projects .button.is-dark.is-hovered {
background: #5c1f5c;
color: white; }
.priority .button {
.priority .button, .priority .button:hover {
font-size: 1.35em;
font-family: FreeSansBold;
background: #ccc;
color: #666666;
padding: .25em .7em;
height: auto; }
.priority .button span.tag {
.priority .button span.tag, .priority .button:hover span.tag {
color: inherit; }
.priority .button.is-dark {
.priority .button.is-dark span.tag {
background: #4d4d4d; }
.priority .button.is-dark, .priority .button.is-dark:hover, .priority .button.is-dark.is-hovered {
background: #737373;
color: white; }
.priority .button.is-dark span.tag {
background: #4d4d4d; }
.priority .button.A {
background: #ff3860;
......@@ -4431,7 +4432,7 @@ button.dropdown-item {
.modal-card {
margin: 0 auto;
max-height: calc(100vh - 40px);
width: 800px; } }
width: 640px; } }
.modal-close {
background: none;
......
......@@ -256,7 +256,7 @@
</div>
<div class="field dueDate">
<div class="control has-icons-left">
<input id="dueDatePickerInput" class="input" tabindex="310" readonly>
<input id="datePickerInput" class="input" tabindex="310" readonly>
<a href="#" class="icon is-left" tabindex="-1">
<i class="far fa-clock is-left"></i>
</a>
......@@ -637,12 +637,7 @@
<script defer src="js/jsTodoExtensions.js"></script>
<script defer src="js/jsTodoTxt.js"></script>
<script defer src="js/marked.min.js"></script>
<script defer src="js/datepicker.min.js"></script>
<script defer src="locales/de/datepicker.js"></script>
<script defer src="locales/es/datepicker.js"></script>
<script defer src="locales/it/datepicker.js"></script>
<script defer src="locales/fr/datepicker.js"></script>
<script defer src="render.js"></script>
<script defer type="module" src="render.js"></script>
<script defer src="js/fontawesome.js"></script>
<script defer src="js/fontawesome.solid.min.js"></script>
<script defer src="js/fontawesome.regular.min.js"></script>
......
export function showForm(todo, templated) {
try {
// in case a content window was open, it will be closed
modal.forEach(function(el) {
el.classList.remove("is-active");
});
// in case the more toggle menu is open we close it!3KL7jeuduikbngbkfuvgflctnfguvjfdtgdnngbbfnfbkhnbtetllfhndlknfnfj
showMore(false);
// clear the input value in case there was an old one
modalFormInput.value = null;
modalForm.classList.toggle("is-active");
// clean up the alert box first
modalFormAlert.innerHTML = null;
modalFormAlert.parentElement.classList.remove("is-active", 'is-warning', 'is-danger');
// here we configure the headline and the footer buttons
if(todo) {
// replace invisible multiline ascii character with new line
todo = todo.replaceAll(String.fromCharCode(16),"\r\n");
// we need to check if there already is a due date in the object
todo = new TodoTxtItem(todo, [ new DueExtension(), new RecExtension() ]);
// set the priority
setPriorityInput(todo.priority);
//
if(templated === true) {
// this is a new templated todo task
// erase the original creation date and description
todo.date = null;
todo.text = "____________";
modalFormInput.value = todo;
modalTitle.innerHTML = window.translations.addTodo;
// automatically select the placeholder description
let selectStart = modalFormInput.value.indexOf(todo.text);
let selectEnd = selectStart + todo.text.length;
modalFormInput.setSelectionRange(selectStart, selectEnd);
btnItemStatus.classList.remove("is-active");
} else {
// this is an existing todo task to be edited
// put the initially passed todo to the modal data field
modalForm.setAttribute("data-item", todo.toString());
modalFormInput.value = todo;
modalTitle.innerHTML = window.translations.editTodo;
btnItemStatus.classList.add("is-active");
}
//btnItemStatus.classList.add("is-active");
// only show the complete button on open items
if(todo.complete === false) {
btnItemStatus.innerHTML = window.translations.done;
} else {
btnItemStatus.innerHTML = window.translations.inProgress;
}
// if there is a recurrence
if(todo.rec) {
setRecurrenceInput(todo.rec).then(function(result) {
console.log(result);
}).catch(function(error) {
handleError(error);
});
}
// if so we paste it into the input field
if(todo.dueString) {
dueDatePicker.setDate(todo.dueString);
dueDatePickerInput.value = todo.dueString;
// only show the recurrence picker when a due date is set
recurrencePicker.classList.add("is-active");
} else {
// hide the recurrence picker when a due date is not set
recurrencePicker.classList.remove("is-active");
// if not we clean it up
dueDatePicker.setDate({
clear: true
});
dueDatePickerInput.value = null;
}
} else {
// hide the recurrence picker when a due date is not set
recurrencePicker.classList.remove("is-active");
// if not we clean it up
dueDatePicker.setDate({
clear: true
});
dueDatePickerInput.value = null;
modalTitle.innerHTML = window.translations.addTodo;
btnItemStatus.classList.remove("is-active");
}
// adjust size of recurrence picker input field
resizeInput(recurrencePickerInput);
// adjust size of due date picker input field
resizeInput(dueDatePickerInput);
//resizeInput(recurrencePickerInput);
// in any case put focus into the input field
modalFormInput.focus();
// if textarea, resize to content length
if(modalFormInput.tagName==="TEXTAREA") {
modalFormInput.style.height="auto";
modalFormInput.style.height= modalFormInput.scrollHeight+"px";
}
positionAutoCompleteContainer();
return Promise.resolve("Info: Show/Edit todo window opened");
} catch (error) {
error.functionName = arguments.callee.name;
return Promise.reject(error);
}
}
"use strict";
import { modal } from "../../render.js";
function showTab(tab) {
contentTabsCards.forEach(function(el) {
el.classList.remove("is-active");
});
document.getElementById(tab).classList.add("is-active");
}
function showContent(section) {
try {
// in case a content window was open, it will be closed
modal.forEach(function(el) {
el.classList.remove("is-active");
});
contentTabs.forEach(function(el) {
el.classList.remove("is-active");
});
contentTabsCards.forEach(function(el) {
el.classList.remove("is-active");
});
let firstTab = section.querySelector(".tabs");
firstTab.firstElementChild.firstElementChild.classList.add("is-active");
let firstSection = section.querySelector("section");
firstSection.classList.add("is-active");
section.classList.add("is-active");
section.focus();
return Promise.resolve("Info: Content is shown");
} catch(error) {
error.functionName = showContent.name;
return Promise.reject(error);
}
}
const contentTabs = document.querySelectorAll('.modal.content ul li');
const contentTabsCards = document.querySelectorAll('.modal.content section');
contentTabs.forEach(el => el.addEventListener("click", function(el) {
contentTabs.forEach(function(el) {
el.classList.remove("is-active");
});
this.classList.add("is-active");
showTab(this.classList[0]);
// trigger matomo event
if(window.consent) _paq.push(["trackEvent", "Content", "Click on " + this.firstElementChild.innerHTML, this.classList[0]]);
}));
export { showContent };
"use strict";
function convertDate(date) {
//https://stackoverflow.com/a/6040556
let day = ("0" + (date.getDate())).slice(-2)
let month = ("0" + (date.getMonth() + 1)).slice(-2);
let year = date.getFullYear();
return year + "-" + month + "-" + day;
};
function isToday(date) {
const today = new Date()
return date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
};
function isTomorrow(date) {
const today = new Date()
return date.getDate() === today.getDate()+1 &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
};
function isPast(date) {
const today = new Date();
if (date.setHours(0, 0, 0, 0) < today.setHours(0, 0, 0, 0)) {
return true;
}
return false;
};
/*Date.prototype.isFuture = function () {
const today = new Date();
if (date.setHours(0, 0, 0, 0) > today.setHours(0, 0, 0, 0)) return true
return false;
};*/
function isFuture(date) {
const today = new Date();
if (date.setHours(0, 0, 0, 0) > today.setHours(0, 0, 0, 0)) return true
return false;
};
export { convertDate, isToday, isTomorrow, isPast, isFuture };
"use strict";
import { translations, userData, resizeInput } from "../../render.js";
import Datepicker from "../../../node_modules/vanillajs-datepicker/js/Datepicker.js";
import de from "../../../node_modules/vanillajs-datepicker/js/i18n/locales/de.js";
import it from "../../../node_modules/vanillajs-datepicker/js/i18n/locales/it.js";
import es from "../../../node_modules/vanillajs-datepicker/js/i18n/locales/es.js";
import fr from "../../../node_modules/vanillajs-datepicker/js/i18n/locales/fr.js";
const datePickerInput = document.getElementById("datePickerInput");
datePickerInput.onfocus = function () {
datePicker.update();
autoCompleteContainer.classList.remove("is-active");
resizeInput(datePickerInput);
};
datePickerInput.addEventListener("changeDate", function (e, detail) {
//let caretPosition = getCaretPosition(modalFormInput);
// we only update the object if there is a date selected. In case of a refresh it would throw an error otherwise
if(e.detail.date) {
// generate the object on what is written into input, so we don't overwrite previous inputs of user
let todo = new TodoTxtItem(modalFormInput.value, [ new DueExtension(), new HiddenExtension(), new RecExtension() ]);
todo.due = new Date(e.detail.date);
todo.dueString = new Date(e.detail.date.getTime() - (e.detail.date.getTimezoneOffset() * 60000 )).toISOString().split("T")[0];
// if suggestion box was open, it needs to be closed
autoCompleteContainer.classList.remove("is-active");
autoCompleteContainer.blur();
// if a due date is set, the recurrence picker will be shown);
modalFormInput.value = todo.toString();
modalFormInput.focus();
resizeInput(datePickerInput);
datePicker.hide();
// trigger matomo event
if(window.consent) _paq.push(["trackEvent", "Form", "Datepicker used to add date to input"]);
}
});
datePickerInput.placeholder = translations.formSelectDueDate;
Object.assign(Datepicker.locales, de, it, es, fr);
const datePicker = new Datepicker(datePickerInput, {
autohide: true,
language: userData.language,
format: "yyyy-mm-dd",
clearBtn: true,
beforeShowDay: function(date) {
let today = new Date();
if (date.getDate() == today.getDate() &&
date.getMonth() == today.getMonth() &&
date.getFullYear() == today.getFullYear()) {
return { classes: 'today'};
}
}
});
document.querySelector(".datepicker .clear-btn").onclick = function() {
let todo = new TodoTxtItem(modalFormInput.value, [ new DueExtension(), new HiddenExtension(), new RecExtension() ]);
todo.due = undefined;
todo.dueString = undefined;
modalFormInput.value = todo.toString();
resizeInput(datePickerInput);
datePicker.hide();
}
export { datePickerInput, datePicker};
"use strict";
import { setUserData, showMore, userData, handleError, navBtns } from "../../render.js";
// ########################################################################################################################
// RESIZEABLE FILTER DRAWER
// https://spin.atomicobject.com/2019/11/21/creating-a-resizable-html-element/
// ########################################################################################################################
const getResizeableElement = () => { return document.getElementById("drawerContainer"); };
const getHandleElement = () => { return document.getElementById("handle"); };
const minPaneSize = 400;
const maxPaneSize = document.body.clientWidth * .75
const setPaneWidth = (width) => {
getResizeableElement().style.setProperty("--resizeable-width", `${width}px`);
setUserData("drawerWidth", `${width}`);
};
const getPaneWidth = () => {
const pxWidth = getComputedStyle(getResizeableElement())
.getPropertyValue("--resizeable-width");
return parseInt(pxWidth, 10);
};
const startDragging = (event) => {
event.preventDefault();
const host = getResizeableElement();
const startingPaneWidth = getPaneWidth();
const xOffset = event.pageX;
const mouseDragHandler = (moveEvent) => {
moveEvent.preventDefault();
const primaryButtonPressed = moveEvent.buttons === 1;
if (!primaryButtonPressed) {
setPaneWidth(Math.min(Math.max(getPaneWidth(), minPaneSize), maxPaneSize));
document.body.removeEventListener("pointermove", mouseDragHandler);
return;
}
const paneOriginAdjustment = "left" === "right" ? 1 : -1;
setPaneWidth((xOffset - moveEvent.pageX ) * paneOriginAdjustment + startingPaneWidth);
};
const remove = document.body.addEventListener("pointermove", mouseDragHandler);
};
getResizeableElement().style.setProperty("--max-width", `${maxPaneSize}px`);
getResizeableElement().style.setProperty("--min-width", `${minPaneSize}px`);
getHandleElement().addEventListener("mousedown", startDragging);
document.getElementById("filterDrawer").addEventListener ("keydown", function () {
if(event.key === "Escape") {
showDrawer(false, navBtnFilter.id, filterDrawer.id).then(function(result) {
console.log(result);
}).catch(function(error) {
handleError(error);
});
}
});
document.getElementById("viewDrawer").addEventListener ("keydown", function () {
if(event.key === "Escape") {
showDrawer(false, navBtnView.id, viewDrawer.id).then(function(result) {
console.log(result);
}).catch(function(error) {
handleError(error);
});
}
});
const drawers = document.querySelectorAll(".drawer");
const drawerCloser = document.querySelectorAll(".drawerClose").forEach(function(drawerClose) {
drawerClose.onclick = function() {
showDrawer(false).then(function(result) {
console.log(result);
}).catch(function(error) {
handleError(error);
});
// trigger matomo event
if(window.consent) _paq.push(["trackEvent", "Drawer", "Click on close button"])
}
})
function showDrawer(variable, buttonId, drawerId) {
try {
switch (drawerId) {
case "viewDrawer":
if(userData.showCompleted) {
viewToggleSortCompletedLast.parentElement.classList.remove("is-hidden");
} else {
viewToggleSortCompletedLast.parentElement.classList.add("is-hidden");
}
// set viewContainer sort select
Array.from(viewSelectSortBy.options).forEach(function(item) {
if(item.value===userData.sortBy) item.selected = true
});
break;
}
buttonId = document.getElementById(buttonId);
drawerId = document.getElementById(drawerId);
// always hide the drawer container first
drawerContainer.classList.remove("is-active");
// next show or hide the single drawers
switch(variable) {
case true:
buttonId.classList.add("is-highlighted");
drawerId.classList.add("is-active");
break;
case false:
drawers.forEach(function(drawer) {
drawer.classList.remove("is-active");
});
navBtns.forEach(function(navBtn) {
navBtn.classList.remove("is-highlighted");
});
drawerContainer.classList.remove("is-active");
break;
case "toggle":
buttonId.classList.toggle("is-highlighted");
drawerId.classList.toggle("is-active");
break;
}
// if any of the drawers is active now, also show the container
drawers.forEach(function(drawer) {
if(drawer.classList.contains("is-active")) {
drawerContainer.classList.add("is-active");
setUserData(drawer.id, true);
} else {
setUserData(drawer.id, false);
}
});
// persist filter drawer state
if(drawerId && drawerId.classList.contains("is-active")) {
// if the drawer is open the table needs a fixed width to overlap the viewport
todoTable.style.minWidth = "45em";
todoTableSearchContainer.style.minWidth = "45em";
} else {
// undo what has been done
todoTable.style.minWidth = "auto";
todoTableSearchContainer.style.minWidth = "auto";
}
// if more toggle is open we close it as user doesn't need it anymore
showMore(false);
return Promise.resolve("Success: Drawer toggled");
} catch(error) {
error.functionName = showDrawer.name;
return Promise.reject(error);
}
}
export { setPaneWidth, showDrawer };
"use strict";
import { userData, handleError, translations, setUserData, startBuilding } from "../../render.js";
import { items, generateGroups, generateTable } from "./todos.mjs";
let categories, filtersCounted, selectedFilters;
function filterItems(items, searchString) {
try {
// selected filters are empty, unless they were persisted
if(userData.selectedFilters && userData.selectedFilters.length>0) {
var selectedFilters = JSON.parse(userData.selectedFilters);
} else {
var selectedFilters = new Array;
userData.selectedFilters = selectedFilters;
}
// apply persisted contexts and projects
if(selectedFilters.length > 0) {
// we iterate through the filters in the order they got selected
selectedFilters.forEach(filter => {
if(filter[1]=="projects") {
items = items.filter(function(item) {
if(item.projects) return item.projects.includes(filter[0]);
});
} else if(filter[1]=="contexts") {
items = items.filter(function(item) {
if(item.contexts) {
return item.contexts.includes(filter[0]);
}
});
} else if(filter[1]=="priority") {
items = items.filter(function(item) {
if(item.priority) {
return item.priority.includes(filter[0]);
}
});
}
});
}
// apply filters or filter by search string
items = items.filter(function(item) {
if(todoTableSearch.value) searchString = todoTableSearch.value;
if((searchString || todoTableSearch.value) && item.toString().toLowerCase().indexOf(searchString.toLowerCase()) === -1) return false;
if(!userData.showCompleted && item.complete) return false;
if(!userData.showDueIsToday && item.due && item.due.isToday()) return false;
if(!userData.showDueIsPast && item.due && item.due.isPast()) return false;
if(!userData.showDueIsFuture && item.due && item.due.isFuture()) return false;
if(item.text==="") return false;
return true;
});
return Promise.resolve(items);
} catch(error) {
error.functionName = filterItems.name;
return Promise.reject(error);