1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-10 13:13:29 -04:00

add local to-do planner / tasks to calendar overvie… (#2583)

* feat(quickshell): add local to-do planner / tasks to calendar overview card

* feat(quickshell): add auto-focus and task reordering support in calendar planner

* feat(quickshell): implement smooth drag-and-drop task reordering and inline editing

* fix(quickshell): resolve overlap and jitter in task drag-and-drop

* fix(quickshell): fix boundary swaps and prevent task list scrambling on reload

* fix(quickshell): resolve race, fix qml error, simplify dragging, and remove python dependency

* fix(quickshell): use Log service instead of console.warn in CalendarService

* style: format QML files w/qmlformat-qt6

---------
This commit is contained in:
goatnath
2026-06-09 10:13:41 +05:30
committed by GitHub
parent 38af56c6fd
commit 8856d45887
2 changed files with 770 additions and 107 deletions
@@ -105,6 +105,13 @@ Rectangle {
}
onSelectedDateChanged: updateSelectedDateEvents()
onShowEventDetailsChanged: {
if (showEventDetails) {
taskInput.forceActiveFocus();
}
}
Component.onCompleted: {
loadEventsForMonth();
updateSelectedDateEvents();
@@ -176,7 +183,7 @@ Rectangle {
text: {
const dateStr = Qt.formatDate(selectedDate, "MMM d");
if (selectedDateEvents && selectedDateEvents.length > 0) {
const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 event") : selectedDateEvents.length + " " + I18n.tr("events");
const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 task") : selectedDateEvents.length + " " + I18n.tr("tasks");
return dateStr + " • " + eventCount;
}
return dateStr;
@@ -416,7 +423,6 @@ Rectangle {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) {
root.selectedDate = dayDate;
root.showEventDetails = true;
}
@@ -426,38 +432,233 @@ Rectangle {
}
}
}
}
DankListView {
Flickable {
id: flickableArea
width: parent.width - Theme.spacingS * 2
height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS
height: parent.height - (showEventDetails ? 40 + 42 : 28 + 18) - Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
model: selectedDateEvents
visible: showEventDetails
clip: true
spacing: Theme.spacingXS
contentWidth: width
contentHeight: listViewContainer.height
interactive: listViewContainer.draggedItem === null
Item {
id: listViewContainer
width: parent.width
height: 100
property var draggedItem: null
property bool orderChanged: false
function resetAndLayout() {
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
item.visualIndex = i;
item.isDragging = false;
item.isEditing = false;
}
}
updateLayout();
}
function updateLayout() {
let items = [];
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
items.push(item);
}
}
items.sort((a, b) => a.visualIndex - b.visualIndex);
let currentY = 0;
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (item && !item.isDragging) {
item.y = currentY;
}
if (item) {
currentY += item.height + Theme.spacingXS;
}
}
listViewContainer.height = Math.max(0, currentY - Theme.spacingXS);
}
function checkAndReorder(dragged) {
let items = [];
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
items.push(item);
}
}
items.sort((a, b) => a.visualIndex - b.visualIndex);
let swapped = false;
// Helper to get target Y position without animation offsets
function getTargetY(index) {
let y = 0;
for (let i = 0; i < index; i++) {
y += items[i].height + Theme.spacingXS;
}
return y;
}
while (true) {
let draggedIdx = items.indexOf(dragged);
if (draggedIdx === -1)
break;
let didSwap = false;
// Check item above
if (draggedIdx > 0) {
let above = items[draggedIdx - 1];
let targetYAbove = getTargetY(draggedIdx - 1);
if (above && dragged.y < (targetYAbove + above.height / 2)) {
// Swap visualIndex
let temp = dragged.visualIndex;
dragged.visualIndex = above.visualIndex;
above.visualIndex = temp;
// Swap in local array
items[draggedIdx] = above;
items[draggedIdx - 1] = dragged;
listViewContainer.orderChanged = true;
swapped = true;
didSwap = true;
}
}
// Check item below
if (!didSwap && draggedIdx < items.length - 1) {
let below = items[draggedIdx + 1];
let targetYBelow = getTargetY(draggedIdx + 1);
if (below && (dragged.y + dragged.height) > (targetYBelow + below.height / 2)) {
// Swap visualIndex
let temp = dragged.visualIndex;
dragged.visualIndex = below.visualIndex;
below.visualIndex = temp;
// Swap in local array
items[draggedIdx] = below;
items[draggedIdx + 1] = dragged;
listViewContainer.orderChanged = true;
swapped = true;
didSwap = true;
}
}
if (!didSwap) {
break;
}
}
if (swapped) {
updateLayout();
}
}
function saveNewOrder() {
if (!orderChanged)
return;
let items = [];
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
items.push(item);
}
}
items.sort((a, b) => a.visualIndex - b.visualIndex);
let orderedIds = [];
for (let i = 0; i < items.length; i++) {
let tid = items[i].taskId;
if (tid && tid.startsWith("task_")) {
orderedIds.push(tid.replace("task_", ""));
}
}
if (orderedIds.length > 0) {
CalendarService.reorderTasksForDate(root.selectedDate, orderedIds);
}
orderChanged = false;
}
Repeater {
id: repeater
model: selectedDateEvents
onModelChanged: {
Qt.callLater(listViewContainer.resetAndLayout);
}
delegate: Rectangle {
id: taskItem
width: parent ? parent.width : 0
height: eventContent.implicitHeight + Theme.spacingS
height: isEditing ? 34 : (eventContent.implicitHeight + Theme.spacingS)
radius: Theme.cornerRadius
color: {
if (modelData.url && eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
} else if (eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06);
property int modelIndex: index
property int visualIndex: index
property string taskId: (modelData && modelData.id) ? modelData.id : ""
property bool isDragging: false
property bool isEditing: false
property real dragMouseOffsetY: 0
onModelIndexChanged: {
visualIndex = modelIndex;
}
return Theme.nestedSurface;
onYChanged: {
if (isDragging) {
listViewContainer.checkAndReorder(taskItem);
}
border.color: {
if (modelData.url && eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
} else if (eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
}
return Theme.outlineMedium;
color: isDragging ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : Theme.nestedSurface)
border.color: isDragging ? Theme.primary : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : Theme.outlineMedium)
border.width: (isDragging || eventMouseArea.containsMouse) ? 1 : Theme.layerOutlineWidth
scale: isDragging ? 1.02 : 1.0
z: isDragging ? 100 : visualIndex
Behavior on scale {
NumberAnimation {
duration: 100
}
}
Behavior on y {
id: yBehavior
enabled: !taskItem.isDragging
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
Component.onCompleted: {
visualIndex = index;
listViewContainer.updateLayout();
}
onHeightChanged: {
listViewContainer.updateLayout();
}
onIsEditingChanged: {
if (isEditing) {
editInput.forceActiveFocus();
editInput.selectAll();
}
}
border.width: eventMouseArea.containsMouse ? 1 : Theme.layerOutlineWidth
Rectangle {
width: 3
@@ -466,25 +667,105 @@ Rectangle {
anchors.leftMargin: 3
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: Theme.primary
color: (modelData && modelData.id && modelData.id.startsWith("task_")) ? (modelData.completed ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Theme.primary) : Theme.primary
opacity: 0.8
}
// Drag Handle
Rectangle {
id: dragHandle
width: 24
height: 24
anchors.left: parent.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: "transparent"
visible: modelData && modelData.id && modelData.id.startsWith("task_") && !taskItem.isEditing
DankIcon {
anchors.centerIn: parent
name: "drag_indicator"
size: 14
color: dragMouseArea.containsMouse ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
}
MouseArea {
id: dragMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.SizeAllCursor
preventStealing: true
drag.target: taskItem
drag.axis: Drag.YAxis
drag.minimumY: 0
drag.maximumY: listViewContainer.height - taskItem.height
onPressed: {
taskItem.isDragging = true;
listViewContainer.orderChanged = false;
listViewContainer.draggedItem = taskItem;
}
onPositionChanged: {
// Handled natively by MouseArea.drag
}
onReleased: {
taskItem.isDragging = false;
listViewContainer.draggedItem = null;
if (listViewContainer.orderChanged) {
listViewContainer.saveNewOrder();
} else {
listViewContainer.updateLayout();
}
}
onCanceled: {
taskItem.isDragging = false;
listViewContainer.draggedItem = null;
listViewContainer.resetAndLayout();
}
}
}
// Checkbox status icon
Rectangle {
id: checkboxContainer
width: 24
height: 24
anchors.left: parent.left
anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? (taskItem.isEditing ? 8 : 32) : 8
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: "transparent"
visible: modelData && modelData.id && modelData.id.startsWith("task_")
DankIcon {
anchors.centerIn: parent
name: (modelData && modelData.completed) ? "check_box" : "check_box_outline_blank"
size: 16
color: (modelData && modelData.completed) ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
}
}
Column {
id: eventContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS + 6
anchors.rightMargin: Theme.spacingXS
anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 60 : (Theme.spacingS + 6)
anchors.rightMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 64 : Theme.spacingXS
spacing: 2
visible: !taskItem.isEditing
StyledText {
width: parent.width
text: modelData.title
text: modelData ? modelData.title : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
color: (modelData && modelData.id && modelData.id.startsWith("task_") && modelData.completed) ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) : Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
@@ -508,19 +789,61 @@ Rectangle {
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
visible: text !== ""
visible: text !== "" && modelData && modelData.id && !modelData.id.startsWith("task_")
}
}
// Inline Edit Input Box
Rectangle {
id: editInputContainer
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 36
anchors.rightMargin: 64
anchors.verticalCenter: parent.verticalCenter
height: 28
visible: taskItem.isEditing
color: "transparent"
TextInput {
id: editInput
anchors.fill: parent
verticalAlignment: TextInput.AlignVCenter
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall
selectByMouse: true
clip: true
text: modelData ? modelData.title : ""
onAccepted: {
let txt = text.trim();
if (txt !== "" && modelData && modelData.id) {
CalendarService.editTask(modelData.id, txt);
}
taskItem.isEditing = false;
}
Keys.onEscapePressed: {
taskItem.isEditing = false;
}
}
}
// Main body MouseArea (declared before the delete/edit buttons so they sit on top)
MouseArea {
id: eventMouseArea
anchors.fill: parent
anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 32 : 6
anchors.rightMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 64 : 0
hoverEnabled: true
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.url !== ""
cursorShape: (modelData && (modelData.url || (modelData.id && modelData.id.startsWith("task_")))) ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData && (modelData.url !== "" || (modelData.id && modelData.id.startsWith("task_"))) && !taskItem.isEditing
onClicked: {
if (modelData.url && modelData.url !== "") {
if (modelData && modelData.id && modelData.id.startsWith("task_")) {
CalendarService.toggleTask(modelData.id);
} else if (modelData && modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) {
log.warn("Failed to open URL: " + modelData.url);
} else {
@@ -529,6 +852,120 @@ Rectangle {
}
}
}
// Delete / Cancel Button
Rectangle {
id: deleteButton
width: 24
height: 24
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: deleteMouseArea.containsMouse ? (taskItem.isEditing ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(0.9, 0.2, 0.2, 0.15)) : "transparent"
visible: modelData && modelData.id && modelData.id.startsWith("task_")
DankIcon {
anchors.centerIn: parent
name: taskItem.isEditing ? "close" : "delete"
size: 14
color: deleteMouseArea.containsMouse ? (taskItem.isEditing ? Theme.primary : Qt.rgba(0.9, 0.2, 0.2, 1.0)) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
}
MouseArea {
id: deleteMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (taskItem.isEditing) {
taskItem.isEditing = false;
} else if (modelData && modelData.id) {
CalendarService.removeTask(modelData.id);
}
}
}
}
// Edit / Save Button
Rectangle {
id: editButton
width: 24
height: 24
anchors.right: deleteButton.left
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: editMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
visible: modelData && modelData.id && modelData.id.startsWith("task_")
DankIcon {
anchors.centerIn: parent
name: taskItem.isEditing ? "check" : "edit"
size: 14
color: editMouseArea.containsMouse ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
}
MouseArea {
id: editMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (taskItem.isEditing) {
let txt = editInput.text.trim();
if (txt !== "" && modelData && modelData.id) {
CalendarService.editTask(modelData.id, txt);
}
taskItem.isEditing = false;
} else {
taskItem.isEditing = true;
}
}
}
}
}
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 34
anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius
color: Theme.nestedSurface
border.color: Theme.outlineMedium
border.width: 1
visible: showEventDetails
TextInput {
id: taskInput
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
verticalAlignment: TextInput.AlignVCenter
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall
selectByMouse: true
clip: true
// Hint placeholder text
Text {
text: I18n.tr("Add a task...")
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
visible: !taskInput.text && !taskInput.activeFocus
font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter
}
onAccepted: {
let txt = text.trim();
if (txt !== "") {
CalendarService.addTaskForDate(root.selectedDate, txt);
text = "";
}
}
}
}
}
+238 -12
View File
@@ -5,18 +5,27 @@ import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
Singleton {
id: root
readonly property var log: Log.scoped("CalendarService")
property bool khalAvailable: false
property bool khalAvailable: true // Always true to enable DMS calendar card UI
property bool khalInstalled: false // Tracks if khal is actually on the system
property var eventsByDate: ({})
property var khalEventsByDate: ({})
property var taskEventsByDate: ({})
property var localTasks: ({})
property bool isLoading: false
property string lastError: ""
property date lastStartDate
property date lastEndDate
property string khalDateFormat: "MM/dd/yyyy"
onKhalEventsByDateChanged: mergeEvents()
onTaskEventsByDateChanged: mergeEvents()
function checkKhalAvailability() {
if (!khalCheckProcess.running)
khalCheckProcess.running = true;
@@ -50,7 +59,7 @@ Singleton {
}
function loadEvents(startDate, endDate) {
if (!root.khalAvailable) {
if (!root.khalInstalled) {
return;
}
if (eventsProcess.running) {
@@ -79,11 +88,232 @@ Singleton {
return events.length > 0;
}
// In-memory Task CRUD methods
function loadTasks(text) {
if (!text || text.trim() === "") {
root.localTasks = {};
root.taskEventsByDate = {};
return;
}
try {
root.localTasks = JSON.parse(text);
updateTaskEvents();
} catch (error) {
log.warn("Failed to parse local tasks JSON: " + error.toString());
}
}
function saveTasks() {
let dir = Quickshell.env("HOME") + "/.config/niri-calendar-todo";
Quickshell.execDetached(["mkdir", "-p", dir]);
tasksFileView.setText(JSON.stringify(root.localTasks, null, 2));
}
function updateTaskEvents() {
let newTaskEvents = {};
for (let dateKey in root.localTasks) {
let taskList = root.localTasks[dateKey] || [];
newTaskEvents[dateKey] = [];
for (let task of taskList) {
let eventId = "task_" + task.id;
let parts = dateKey.split("-");
let taskDate = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
newTaskEvents[dateKey].push({
"id": eventId,
"title": task.text,
"completed": !!task.completed,
"start": taskDate,
"end": taskDate,
"location": "",
"description": "Task from your Planner",
"url": "",
"calendar": "Todo Planner",
"color": "#10B981" // Pastel Green
,
"allDay": true,
"isMultiDay": false
});
}
}
root.taskEventsByDate = newTaskEvents;
}
function addTaskForDate(date, text) {
let dateKey = Qt.formatDate(date, "yyyy-MM-dd");
let tasks = Object.assign({}, root.localTasks);
if (!tasks[dateKey]) {
tasks[dateKey] = [];
}
let taskId = (new Date().getTime()) + "-dms";
tasks[dateKey].push({
"id": taskId,
"text": text,
"completed": false
});
root.localTasks = tasks;
updateTaskEvents();
saveTasks();
}
function toggleTask(taskId) {
let cleanId = taskId.replace("task_", "");
let tasks = Object.assign({}, root.localTasks);
let updated = false;
for (let dateKey in tasks) {
let list = tasks[dateKey];
for (let item of list) {
if (item.id === cleanId) {
item.completed = !item.completed;
updated = true;
break;
}
}
if (updated)
break;
}
if (updated) {
root.localTasks = tasks;
updateTaskEvents();
saveTasks();
}
}
function removeTask(taskId) {
let cleanId = taskId.replace("task_", "");
let tasks = Object.assign({}, root.localTasks);
let updated = false;
for (let dateKey in tasks) {
let list = tasks[dateKey];
let filtered = list.filter(item => item.id !== cleanId);
if (filtered.length !== list.length) {
if (filtered.length === 0) {
delete tasks[dateKey];
} else {
tasks[dateKey] = filtered;
}
updated = true;
break;
}
}
if (updated) {
root.localTasks = tasks;
updateTaskEvents();
saveTasks();
}
}
function reorderTasksForDate(date, orderedIds) {
let dateKey = Qt.formatDate(date, "yyyy-MM-dd");
let tasks = Object.assign({}, root.localTasks);
let v = tasks[dateKey] || [];
let idToItem = {};
for (let item of v) {
idToItem[item.id] = item;
}
let newV = [];
for (let tid of orderedIds) {
if (idToItem[tid]) {
newV.push(idToItem[tid]);
}
}
let orderedSet = new Set(orderedIds);
for (let item of v) {
if (!orderedSet.has(item.id)) {
newV.push(item);
}
}
tasks[dateKey] = newV;
root.localTasks = tasks;
updateTaskEvents();
saveTasks();
}
function editTask(taskId, newText) {
let cleanId = taskId.replace("task_", "");
let tasks = Object.assign({}, root.localTasks);
let updated = false;
for (let dateKey in tasks) {
let list = tasks[dateKey];
for (let item of list) {
if (item.id === cleanId) {
item.text = newText;
updated = true;
break;
}
}
if (updated)
break;
}
if (updated) {
root.localTasks = tasks;
updateTaskEvents();
saveTasks();
}
}
function mergeEvents() {
let merged = {};
// Merge khal events
for (let dateKey in root.khalEventsByDate) {
merged[dateKey] = [].concat(root.khalEventsByDate[dateKey]);
}
// Merge task events
for (let dateKey in root.taskEventsByDate) {
if (!merged[dateKey]) {
merged[dateKey] = [];
}
for (let event of root.taskEventsByDate[dateKey]) {
if (!merged[dateKey].some(e => e.id === event.id)) {
merged[dateKey].push(event);
}
}
}
// Sort events within each date
for (let dateKey in merged) {
let list = merged[dateKey];
for (let idx = 0; idx < list.length; idx++) {
list[idx]._origIdx = idx;
}
list.sort((a, b) => {
let diff = a.start.getTime() - b.start.getTime();
if (diff !== 0)
return diff;
return a._origIdx - b._origIdx;
});
}
root.eventsByDate = merged;
}
// Initialize on component completion
Component.onCompleted: {
detectKhalDateFormat();
}
// Atomic file view for tasks
FileView {
id: tasksFileView
path: Quickshell.env("HOME") + "/.config/niri-calendar-todo/tasks.json"
blockLoading: false
blockWrites: false
atomicWrites: true
watchChanges: true
printErrors: false
onLoaded: {
loadTasks(tasksFileView.text());
}
onLoadFailed: {
root.localTasks = {};
root.taskEventsByDate = {};
}
}
// Process for detecting khal date format
Process {
id: khalFormatProcess
@@ -119,9 +349,11 @@ Singleton {
command: ["khal", "list", "today"]
running: false
onExited: exitCode => {
root.khalAvailable = (exitCode === 0);
if (exitCode === 0) {
root.khalInstalled = (exitCode === 0);
if (root.khalInstalled) {
loadCurrentMonth();
} else {
loadEvents(root.lastStartDate || new Date(), root.lastEndDate || new Date());
}
}
}
@@ -284,17 +516,11 @@ Singleton {
}
}
}
// Sort events by start time within each date
for (let dateKey in newEventsByDate) {
newEventsByDate[dateKey].sort((a, b) => {
return a.start.getTime() - b.start.getTime();
});
}
root.eventsByDate = newEventsByDate;
root.khalEventsByDate = newEventsByDate;
root.lastError = "";
} catch (error) {
root.lastError = "Failed to parse events JSON: " + error.toString();
root.eventsByDate = {};
root.khalEventsByDate = {};
}
// Reset for next run
eventsProcess.rawOutput = "";