Personal noctalia plugins collection
1import QtQuick
2import Quickshell
3import qs.Commons
4import qs.Services.Location
5import qs.Services.UI
6import Quickshell.Io
7import qs.Services.System
8
9Item {
10 id: root
11 property var pluginApi: null
12 property date currentDate: new Date()
13 property ListModel eventsModel: ListModel {}
14 property ListModel allDayEventsModel: ListModel {}
15 property var overlappingEventsData: ({})
16 property bool isLoading: false
17 property bool hasLoadedOnce: false
18 property string syncStatus: ""
19 property int lastKnownEventCount: 0
20
21 // Todo support
22 property ListModel todosModel: ListModel {}
23 property var taskLists: []
24 property bool todosLoading: false
25 property string todoSyncStatus: ""
26 property bool showCompletedTodos: false
27
28 property real dayColumnWidth: 120 * Style.uiScaleRatio
29 property real allDaySectionHeight: 0 * Style.uiScaleRatio
30 property var allDayEventsWithLayout: []
31
32 property date weekStart: calculateWeekStart(currentDate, firstDayOfWeek)
33 property date weekEnd: calculateWeekEnd(weekStart)
34 property var weekDates: calculateWeekDates(weekStart)
35 property string monthRangeText: formatMonthRangeText(weekDates)
36
37 // Settings
38 property string panelModeSetting: pluginApi?.pluginSettings?.panelMode || "attached"
39 property string weekStartSetting: pluginApi?.pluginSettings?.weekStart || "1"
40 property string timeFormatSetting: pluginApi?.pluginSettings?.timeFormat || "24h"
41 property string lineColorTypeSetting: pluginApi?.pluginSettings?.lineColorType || "mOutline"
42 property real hourLineOpacitySetting: pluginApi?.pluginSettings?.hourLineOpacity ?? 0.5
43 property real dayLineOpacitySetting: pluginApi?.pluginSettings?.dayLineOpacity ?? 0.9
44
45 readonly property int firstDayOfWeek: weekStartSetting === "0" ? 0 :
46 weekStartSetting === "1" ? 1 :
47 weekStartSetting === "6" ? 6 : I18n.locale.firstDayOfWeek
48
49 readonly property bool use12hourFormat: timeFormatSetting === "12h" ? true :
50 timeFormatSetting === "24h" ? false :
51 Settings.data.location.use12hourFormat
52
53 readonly property color lineColor: lineColorTypeSetting === "mOnSurfaceVariant" ? Color.mOnSurfaceVariant : Color.mOutline
54
55 onWeekStartSettingChanged: if (hasLoadedOnce) Qt.callLater(refreshView)
56 onTimeFormatSettingChanged: eventsModelChanged()
57 onCurrentDateChanged: Qt.callLater(refreshView)
58 onLineColorTypeSettingChanged: eventsModelChanged()
59 onHourLineOpacitySettingChanged: eventsModelChanged()
60 onDayLineOpacitySettingChanged: eventsModelChanged()
61
62 // React to CalendarService signals (async event delivery)
63 Connections {
64 target: CalendarService
65 function onAvailableChanged() {
66 if (CalendarService.available) {
67 Qt.callLater(loadEvents)
68 Qt.callLater(loadTaskLists)
69 Qt.callLater(loadTodos)
70 } else {
71 isLoading = false
72 if (pluginApi) syncStatus = pluginApi.tr("panel.no_service")
73 }
74 }
75 function onEventsChanged() {
76 var count = CalendarService.events ? CalendarService.events.length : 0
77 // If auto-refresh dropped event count significantly, re-request wide range
78 if (hasLoadedOnce && !isLoading && lastKnownEventCount > 10 && count < lastKnownEventCount * 0.5) {
79 console.log("[weekly-calendar] Auto-refresh narrowed events (" + count + " vs " + lastKnownEventCount + "), re-requesting wide range")
80 Qt.callLater(loadEvents)
81 return
82 }
83 lastKnownEventCount = Math.max(lastKnownEventCount, count)
84 Qt.callLater(updateEventsFromService)
85 }
86 function onLoadingChanged() {
87 if (!CalendarService.loading && isLoading) {
88 Qt.callLater(updateEventsFromService)
89 }
90 }
91 }
92
93 // Safety timeout: if CalendarService never signals back, stop spinning
94 Timer {
95 id: loadingTimeout
96 interval: 15000
97 repeat: false
98 onTriggered: {
99 if (isLoading) {
100 console.warn("[weekly-calendar] loading timeout, forcing update")
101 updateEventsFromService()
102 }
103 }
104 }
105
106 // IPC
107 IpcHandler {
108 target: "plugin:weekly-calendar"
109 function togglePanel() { pluginApi?.withCurrentScreen(s => pluginApi.togglePanel(s)) }
110 }
111
112 Component.onCompleted: {
113 initializePluginSettings()
114 // Process any cached events immediately
115 if (CalendarService.events && CalendarService.events.length > 0) {
116 Qt.callLater(updateEventsFromService)
117 }
118 if (CalendarService.available) {
119 Qt.callLater(loadEvents)
120 Qt.callLater(loadTaskLists)
121 Qt.callLater(loadTodos)
122 }
123 }
124
125 onPluginApiChanged: {
126 initializePluginSettings()
127 if (CalendarService.events && CalendarService.events.length > 0) {
128 Qt.callLater(updateEventsFromService)
129 }
130 if (CalendarService.available) {
131 Qt.callLater(loadEvents)
132 Qt.callLater(loadTaskLists)
133 Qt.callLater(loadTodos)
134 }
135 }
136
137 function initializePluginSettings() {
138 if (!pluginApi) return
139 if (!pluginApi.pluginSettings.weekStart) {
140 pluginApi.pluginSettings = {
141 weekStart: "1",
142 timeFormat: "24h",
143 lineColorType: "mOutline",
144 hourLineOpacity: 0.5,
145 dayLineOpacity: 1.0
146 }
147 pluginApi.saveSettings()
148 }
149 }
150
151 function initializePlugin() {
152 console.log("[weekly-calendar] initializePlugin called, CalendarService.available=" + CalendarService.available)
153 if (!hasLoadedOnce && CalendarService.available) {
154 loadEvents()
155 } else {
156 refreshView()
157 }
158 }
159
160 // Re-filter existing events for the current week view (no new fetch)
161 function refreshView() {
162 if (!pluginApi) return
163 if (CalendarService.events && CalendarService.events.length > 0) {
164 updateEventsFromService()
165 } else if (hasLoadedOnce) {
166 clearEventModels()
167 syncStatus = pluginApi.tr("panel.no_events")
168 }
169 }
170
171 // Fetch events from EDS - requests a wide date range to cover past/future navigation
172 function loadEvents() {
173 if (!pluginApi) return
174 if (!CalendarService.available) {
175 syncStatus = pluginApi.tr("panel.no_service")
176 console.log("[weekly-calendar] loadEvents: service not available")
177 return
178 }
179
180 isLoading = true
181 syncStatus = pluginApi.tr("panel.loading")
182
183 // Request a wide range: 180 days behind, 60 days ahead
184 // This covers ~6 months of past events and ~2 months of future events
185 var daysAhead = 60
186 var daysBehind = 180
187
188 CalendarService.loadEvents(daysAhead, daysBehind)
189
190 hasLoadedOnce = true
191 loadingTimeout.restart()
192
193 // If CalendarService already has events (cached), display them now
194 if (CalendarService.events && CalendarService.events.length > 0) {
195 Qt.callLater(updateEventsFromService)
196 }
197 }
198
199 function updateEventsFromService() {
200 if (!pluginApi) return
201 loadingTimeout.stop()
202 clearEventModels()
203
204 if (!CalendarService.available) {
205 syncStatus = pluginApi.tr("panel.no_service")
206 } else if (!CalendarService.events?.length) {
207 syncStatus = pluginApi.tr("panel.no_events")
208 } else {
209 var stats = processCalendarEvents(CalendarService.events)
210 syncStatus = stats.timedCount === 1
211 ? `${stats.timedCount} ${pluginApi.tr("panel.event")}, ${stats.allDayCount} ${pluginApi.tr("panel.allday")}`
212 : `${stats.timedCount} ${pluginApi.tr("panel.events")}, ${stats.allDayCount} ${pluginApi.tr("panel.allday")}`
213 }
214
215 isLoading = false
216 }
217
218 // Events generation & layout
219 function processCalendarEvents(events) {
220 var uniqueEvents = {}, uniqueAllDayEvents = {}
221 var timedCount = 0, allDayCount = 0
222 var newEvents = [], newAllDayEvents = []
223 var weekStartDate = new Date(weekStart), weekEndDate = new Date(weekEnd)
224
225 for (var i = 0; i < events.length; i++) {
226 var event = events[i], eventObj = createEventObject(event, i)
227 var eventStart = new Date(eventObj.startTime), eventEnd = new Date(eventObj.endTime)
228 var overlapsWeek = eventStart < weekEndDate && eventEnd > weekStartDate
229
230 if (overlapsWeek) {
231 var key = event.uid + "-" + event.start + "-" + event.end + i
232 if (eventObj.allDay) {
233 if (!uniqueAllDayEvents[key]) {
234 uniqueAllDayEvents[key] = true
235 allDayCount++
236 newAllDayEvents.push(eventObj)
237 }
238 } else if (!uniqueEvents[key]) {
239 uniqueEvents[key] = true
240 timedCount++
241 processTimedEventIntoArray(eventObj, newEvents)
242 }
243 }
244 }
245
246 eventsModel.clear()
247 allDayEventsModel.clear()
248 newEvents.forEach(e => eventsModel.append(e))
249 newAllDayEvents.forEach(e => allDayEventsModel.append(e))
250
251 calculateAllDayEventLayout()
252 updateOverlappingEvents()
253 eventsModel.layoutChanged()
254 allDayEventsModel.layoutChanged()
255
256 return {timedCount: timedCount, allDayCount: allDayCount}
257 }
258
259 function clearEventModels() { eventsModel.clear(); allDayEventsModel.clear() }
260
261 function processTimedEventIntoArray(eventObj, target) {
262 var start = new Date(eventObj.startTime), end = new Date(eventObj.endTime)
263 var startDay = new Date(start.getFullYear(), start.getMonth(), start.getDate())
264 var endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate())
265
266 if (startDay.getTime() === endDay.getTime()) {
267 if (start < weekEnd && end > weekStart) target.push(createEventPart(eventObj, 0, start, end, startDay, 0, 1))
268 } else {
269 var firstEnd = new Date(startDay); firstEnd.setHours(24, 0, 0, 0)
270 var secondStart = new Date(endDay); secondStart.setHours(0, 0, 0, 0)
271 if (start < weekEnd && firstEnd > weekStart) target.push(createEventPart(eventObj, 0, start, firstEnd, startDay, 0, 2))
272 if (secondStart < weekEnd && end > weekStart) target.push(createEventPart(eventObj, 1, secondStart, end, endDay, 1, 2))
273 }
274 }
275
276 function createEventObject(event, idx) {
277 var start = new Date(event.start * 1000), end = new Date(event.end * 1000)
278 var allDay = isAllDayEvent(event), multiDay = isMultiDayEvent(event)
279 var daySpan = calculateDaySpan(start, end, multiDay || allDay)
280 var endsMidnight = end.getHours() === 0 && end.getMinutes() === 0 && end.getSeconds() === 0
281 var id = event.uid + "-" + event.start + "-" + event.end + idx
282
283 return {
284 id: id, title: event.summary || "Untitled Event", description: event.description || "",
285 location: event.location || "", startTime: start, endTime: end, allDay: allDay, multiDay: multiDay,
286 daySpan: daySpan, rawStart: event.start, rawEnd: event.end, duration: (event.end - event.start) / 3600,
287 endsAtMidnight: endsMidnight
288 }
289 }
290
291 function createEventPart(event, partIdx, start, end, day, partNum, total) {
292 return {
293 id: event.id + "-part-" + partIdx, title: event.title, description: event.description,
294 location: event.location, startTime: start, endTime: end, allDay: false, multiDay: true,
295 daySpan: 1, fullStartTime: event.startTime, fullEndTime: event.endTime, isPart: true,
296 partDay: new Date(day), partIndex: partNum, totalParts: total
297 }
298 }
299
300 function getDayIndexForDate(date) {
301 if (!date || isNaN(date.getTime())) return -1
302 var diff = Math.floor((date - weekStart) / 86400000)
303 return diff >= 0 && diff < 7 ? diff : -1
304 }
305 function getDisplayDayIndexForDate(date) { return getDayIndexForDate(date) }
306
307 function calculateAllDaySpanForWeek(event) {
308 var start = new Date(event.startTime), end = new Date(event.endTime)
309 var endsMidnight = end.getHours() === 0 && end.getMinutes() === 0 && end.getSeconds() === 0
310 var adjEnd = endsMidnight ? new Date(end.getTime() - 1) : end
311 var startIdx = Math.max(0, getDayIndexForDate(start))
312 var endIdx = Math.min(6, Math.floor((Math.min(adjEnd, weekEnd) - weekStart) / 86400000))
313 return Math.max(1, endIdx - startIdx + 1)
314 }
315
316 function findAvailableLane(occupied, start, end) {
317 var lane = 0, found = false
318 while (!found) {
319 var conflict = false
320 for (var d = start; d <= end; d++) {
321 if (occupied[d]?.includes(lane)) { conflict = true; break }
322 }
323 if (!conflict) found = true
324 else lane++
325 }
326 return lane
327 }
328
329 function calculateAllDayEventLayout() {
330
331 var occupied = [[], [], [], [], [], [], []]
332 var eventsWithLayout = [], maxLanes = 0
333 var weekStartDate = new Date(weekStart), weekEndDate = new Date(weekEnd)
334
335 for (var i = 0; i < allDayEventsModel.count; i++) {
336 var event = allDayEventsModel.get(i)
337 var start = new Date(event.startTime), end = new Date(event.endTime)
338 var startDay = new Date(start.getFullYear(), start.getMonth(), start.getDate())
339 var endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate())
340
341 if (startDay < weekStartDate && endDay >= weekStartDate) {
342 var span = calculateAllDaySpanForWeek(event)
343 if (span > 0) {
344 var lane = findAvailableLane(occupied, 0, span - 1)
345 for (var d = 0; d < span && d < 7; d++) { if (!occupied[d]) occupied[d] = []; occupied[d].push(lane) }
346 maxLanes = Math.max(maxLanes, lane + 1)
347 eventsWithLayout.push(createLayoutEvent(event, 0, span, lane, true))
348 }
349 } else if (startDay >= weekStartDate && startDay < weekEndDate) {
350 var startIdx = getDayIndexForDate(start)
351 var span = calculateAllDaySpanForWeek(event)
352 if (span > 0) {
353 var lane = findAvailableLane(occupied, startIdx, startIdx + span - 1)
354 for (var d = startIdx; d < startIdx + span && d < 7; d++) { if (!occupied[d]) occupied[d] = []; occupied[d].push(lane) }
355 maxLanes = Math.max(maxLanes, lane + 1)
356 eventsWithLayout.push(createLayoutEvent(event, startIdx, span, lane, false))
357 }
358 }
359 }
360
361 eventsWithLayout.sort((a,b) => a.lane !== b.lane ? a.lane - b.lane : a.startDay - b.startDay)
362 allDayEventsWithLayout = eventsWithLayout
363 allDaySectionHeight = maxLanes === 0 ? 0 : maxLanes === 1 ? 25 : Math.max(30, maxLanes * 25)
364
365 return maxLanes
366 }
367
368 function createLayoutEvent(event, startDay, spanDays, lane, isCont) {
369 return {
370 id: event.id, title: event.title, description: event.description, location: event.location,
371 startTime: event.startTime, endTime: event.endTime, allDay: event.allDay, multiDay: event.multiDay,
372 daySpan: event.daySpan, rawStart: event.rawStart, rawEnd: event.rawEnd, duration: event.duration,
373 endsAtMidnight: event.endsAtMidnight, fullStartTime: event.fullStartTime, fullEndTime: event.fullEndTime,
374 startDay: startDay, spanDays: spanDays, lane: lane, isContinuation: isCont
375 }
376 }
377
378 function updateOverlappingEvents() {
379 var overlapData = {}
380 for (var day = 0; day < 7; day++) processDayEventsWithLanes(day, overlapData)
381 overlappingEventsData = overlapData
382 }
383
384 function processDayEventsWithLanes(day, data) {
385 var events = []
386 for (var i = 0; i < eventsModel.count; i++) {
387 var e = eventsModel.get(i)
388 if (getDisplayDayIndexForDate(e.startTime) === day) {
389 events.push({index: i, start: e.startTime.getTime(), end: e.endTime.getTime()})
390 }
391 }
392 if (events.length === 0) return
393
394 events.sort((a,b) => a.start === b.start ? (b.end - b.start) - (a.end - a.start) : a.start - b.start)
395 var groups = [], current = [], endTime = -1
396
397 events.forEach(e => {
398 if (e.start >= endTime) {
399 if (current.length > 0) groups.push({events: current, endTime: endTime})
400 current = [e]; endTime = e.end
401 } else {
402 current.push(e)
403 if (e.end > endTime) endTime = e.end
404 }
405 })
406 if (current.length > 0) groups.push({events: current, endTime: endTime})
407 groups.forEach(g => assignLanesToGroup(g.events, data))
408 }
409
410 function assignLanesToGroup(group, data) {
411 if (group.length === 0) return
412 var laneEnds = []
413 group.forEach(e => {
414 var placed = false
415 for (var lane = 0; lane < laneEnds.length; lane++) {
416 if (e.start >= laneEnds[lane]) {
417 laneEnds[lane] = e.end
418 e.lane = lane
419 placed = true
420 break
421 }
422 }
423 if (!placed) { e.lane = laneEnds.length; laneEnds.push(e.end) }
424 })
425
426 var total = laneEnds.length
427 group.forEach(e => {
428 data[e.index] = {
429 xOffset: (e.lane / total) * (dayColumnWidth +1),
430 width: (dayColumnWidth+1) / total,
431 lane: e.lane,
432 totalLanes: total
433 }
434 })
435 }
436
437 // Range & formatting of calendar
438 function calculateWeekStart(date, firstDay) {
439 var d = new Date(date)
440 var day = d.getDay()
441 var diff = (day - firstDay + 7) % 7
442 d.setDate(d.getDate() - diff)
443 d.setHours(0, 0, 0, 0)
444 return d
445 }
446
447 function calculateWeekDates(startDate) {
448 var dates = []
449 var start = new Date(startDate)
450
451 for (var i = 0; i < 7; i++) {
452 var d = new Date(start)
453 d.setDate(start.getDate() + i)
454 dates.push(d)
455 }
456
457 return dates
458 }
459
460 function calculateWeekEnd(startDate) {
461 var end = new Date(startDate)
462 end.setDate(end.getDate() + 7)
463 end.setHours(0, 0, 0, 0)
464 return end
465 }
466
467 function isSameDay(date1, date2) {
468 return date1.getDate() === date2.getDate() &&
469 date1.getMonth() === date2.getMonth() &&
470 date1.getFullYear() === date2.getFullYear()
471 }
472
473 function isToday(date) {
474 var today = new Date()
475 return isSameDay(date, today)
476 }
477
478 function isDateInRange(date, startDate, endDate) {
479 return date >= startDate && date < endDate
480 }
481
482 function formatMonthRangeText(dates) {
483 if (!dates || dates.length === 0) return ""
484 var start = dates[0], end = dates[6], locale = I18n.locale
485 return locale.toString(start, "yyyy-MM") === locale.toString(end, "yyyy-MM")
486 ? locale.toString(start, "MMM yyyy")
487 : start.getFullYear() === end.getFullYear()
488 ? locale.toString(start, "MMM") + " – " + locale.toString(end, "MMM") + " " + start.getFullYear()
489 : locale.toString(start, "MMM yyyy") + " – " + locale.toString(end, "MMM yyyy")
490 }
491
492 function isAllDayEvent(event) {
493 var dur = event.end - event.start
494 var start = new Date(event.start * 1000), end = new Date(event.end * 1000)
495 var startsMidnight = start.getHours() === 0 && start.getMinutes() === 0 && start.getSeconds() === 0
496 var endsMidnight = end.getHours() === 0 && end.getMinutes() === 0 && end.getSeconds() === 0
497 return (dur === 86400 && startsMidnight) || (dur >= 86400 && endsMidnight) || dur >= 86400
498 }
499
500 function isMultiDayEvent(event) {
501 var start = new Date(event.start * 1000), end = new Date(event.end * 1000)
502 var endsMidnight = end.getHours() === 0 && end.getMinutes() === 0 && end.getSeconds() === 0
503 var startDay = new Date(start.getFullYear(), start.getMonth(), start.getDate())
504 var endDay = endsMidnight ? new Date(end.getFullYear(), end.getMonth(), end.getDate() - 1) :
505 new Date(end.getFullYear(), end.getMonth(), end.getDate())
506 return startDay.getTime() !== endDay.getTime()
507 }
508
509 function calculateDaySpan(start, end, isMultiDay) {
510 if (!isMultiDay) return 1
511 var startDay = new Date(start.getFullYear(), start.getMonth(), start.getDate())
512 var endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate())
513 var diff = Math.floor((endDay - startDay) / 86400000)
514 var endsMidnight = end.getHours() === 0 && end.getMinutes() === 0 && end.getSeconds() === 0
515 return Math.max(1, endsMidnight ? diff : diff + 1)
516 }
517
518 function formatTime(date) {
519 if (!date || isNaN(date.getTime())) return ""
520 return use12hourFormat ? I18n.locale.toString(date, "h:mm AP") : I18n.locale.toString(date, "HH:mm")
521 }
522
523 function formatDateTime(date) {
524 if (!date || isNaN(date.getTime())) return ""
525 return I18n.locale.monthName(date.getMonth(), Locale.ShortFormat) + ' ' +
526 date.getDate() + ', ' + date.getFullYear() + ' ' + formatTime(date)
527 }
528
529 function formatTimeRangeForDisplay(event) {
530 var start = event.fullStartTime || event.startTime
531 var end = event.fullEndTime || event.endTime
532 return formatTime(start) + " - " + formatTime(end)
533 }
534
535 // Interaction functions
536 function getEventTooltip(event) {
537 var start = event.fullStartTime || event.startTime
538 var end = event.fullEndTime || event.endTime
539 var tip = event.title + "\n" + formatDateTime(start) + " - " + formatDateTime(end)
540 if (event.location) tip += "\n⚲ " + event.location
541 if (event.description) tip += "\n🛈 " + event.description
542 return tip
543 }
544
545 function navigateWeek(days) {
546 var d = new Date(currentDate)
547 d.setDate(d.getDate() + days)
548 currentDate = d
549 }
550
551 function handleEventClick(event) {
552 const date = event.startTime || new Date();
553 const month = date.getMonth() + 1;
554 const day = date.getDate();
555 const year = date.getFullYear();
556 const dateWithSlashes = `${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/${year.toString().substring(2)}`;
557 if (ProgramCheckerService.gnomeCalendarAvailable) {
558 Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes]);
559 }
560 }
561
562 function goToToday() { currentDate = new Date() }
563
564 // Event creation via EDS Python script
565 property string createEventStdout: ""
566 property string createEventStderr: ""
567
568 Process {
569 id: createEventProcess
570 onExited: function(exitCode, exitStatus) {
571 if (exitCode === 0) {
572 try {
573 var result = JSON.parse(createEventStdout)
574 if (result.success) {
575 console.log("Event created: " + result.uid)
576 Qt.callLater(loadEvents)
577 }
578 } catch(e) {
579 console.error("Failed to parse create-event output: " + createEventStdout)
580 }
581 } else {
582 console.error("create-event.py failed: " + createEventStderr)
583 }
584 createEventStdout = ""
585 createEventStderr = ""
586 }
587 stdout: SplitParser {
588 onRead: data => createEventStdout += data
589 }
590 stderr: SplitParser {
591 onRead: data => createEventStderr += data
592 }
593 }
594
595 function createEvent(calendarUid, summary, startTimestamp, endTimestamp, location, description) {
596 var scriptPath = pluginApi.pluginDir + "/scripts/create-event.py"
597 var args = ["python3", scriptPath,
598 "--calendar", calendarUid,
599 "--summary", summary,
600 "--start", String(startTimestamp),
601 "--end", String(endTimestamp)]
602 if (location) { args.push("--location"); args.push(location) }
603 if (description) { args.push("--description"); args.push(description) }
604 createEventProcess.command = args
605 createEventProcess.running = true
606 }
607
608 // === Todo support ===
609
610 property string listTaskListsStdout: ""
611 property string listTaskListsStderr: ""
612
613 Process {
614 id: listTaskListsProcess
615 onExited: function(exitCode, exitStatus) {
616 if (exitCode === 0) {
617 try {
618 var result = JSON.parse(listTaskListsStdout)
619 if (Array.isArray(result)) {
620 taskLists = result.filter(function(tl) { return tl.enabled })
621 }
622 } catch(e) {
623 console.error("[weekly-calendar] Failed to parse task lists: " + listTaskListsStdout)
624 }
625 } else {
626 console.error("[weekly-calendar] list-task-lists.py failed: " + listTaskListsStderr)
627 }
628 listTaskListsStdout = ""
629 listTaskListsStderr = ""
630 }
631 stdout: SplitParser { onRead: data => listTaskListsStdout += data }
632 stderr: SplitParser { onRead: data => listTaskListsStderr += data }
633 }
634
635 property string listTodosStdout: ""
636 property string listTodosStderr: ""
637
638 Process {
639 id: listTodosProcess
640 onExited: function(exitCode, exitStatus) {
641 todosLoading = false
642 if (exitCode === 0) {
643 try {
644 var result = JSON.parse(listTodosStdout)
645 if (Array.isArray(result)) {
646 todosModel.clear()
647 for (var i = 0; i < result.length; i++) {
648 todosModel.append(result[i])
649 }
650 todoSyncStatus = result.length + (result.length === 1 ? " task" : " tasks")
651 }
652 } catch(e) {
653 console.error("[weekly-calendar] Failed to parse todos: " + listTodosStdout)
654 todoSyncStatus = pluginApi ? pluginApi.tr("panel.task_error") : "Error"
655 }
656 } else {
657 console.error("[weekly-calendar] list-todos.py failed: " + listTodosStderr)
658 todoSyncStatus = pluginApi ? pluginApi.tr("panel.task_error") : "Error"
659 }
660 listTodosStdout = ""
661 listTodosStderr = ""
662 }
663 stdout: SplitParser { onRead: data => listTodosStdout += data }
664 stderr: SplitParser { onRead: data => listTodosStderr += data }
665 }
666
667 property string createTodoStdout: ""
668 property string createTodoStderr: ""
669
670 Process {
671 id: createTodoProcess
672 onExited: function(exitCode, exitStatus) {
673 if (exitCode === 0) {
674 try {
675 var result = JSON.parse(createTodoStdout)
676 if (result.success) {
677 console.log("[weekly-calendar] Todo created: " + result.uid)
678 Qt.callLater(loadTodos)
679 }
680 } catch(e) {
681 console.error("[weekly-calendar] Failed to parse create-todo output: " + createTodoStdout)
682 }
683 } else {
684 console.error("[weekly-calendar] create-todo.py failed: " + createTodoStderr)
685 }
686 createTodoStdout = ""
687 createTodoStderr = ""
688 }
689 stdout: SplitParser { onRead: data => createTodoStdout += data }
690 stderr: SplitParser { onRead: data => createTodoStderr += data }
691 }
692
693 property string updateTodoStdout: ""
694 property string updateTodoStderr: ""
695
696 Process {
697 id: updateTodoProcess
698 onExited: function(exitCode, exitStatus) {
699 if (exitCode === 0) {
700 try {
701 var result = JSON.parse(updateTodoStdout)
702 if (result.success) {
703 console.log("[weekly-calendar] Todo updated")
704 Qt.callLater(loadTodos)
705 }
706 } catch(e) {
707 console.error("[weekly-calendar] Failed to parse update-todo output: " + updateTodoStdout)
708 }
709 } else {
710 console.error("[weekly-calendar] update-todo.py failed: " + updateTodoStderr)
711 }
712 updateTodoStdout = ""
713 updateTodoStderr = ""
714 }
715 stdout: SplitParser { onRead: data => updateTodoStdout += data }
716 stderr: SplitParser { onRead: data => updateTodoStderr += data }
717 }
718
719 function loadTaskLists() {
720 if (!pluginApi) return
721 var scriptPath = pluginApi.pluginDir + "/scripts/list-task-lists.py"
722 listTaskListsProcess.command = ["python3", scriptPath]
723 listTaskListsProcess.running = true
724 }
725
726 function loadTodos() {
727 if (!pluginApi) return
728 todosLoading = true
729 todoSyncStatus = pluginApi.tr("panel.loading")
730 var scriptPath = pluginApi.pluginDir + "/scripts/list-todos.py"
731 var args = ["python3", scriptPath]
732 if (showCompletedTodos) args.push("--include-completed")
733 listTodosProcess.command = args
734 listTodosProcess.running = true
735 }
736
737 function createTodo(taskListUid, summary, due, priority, description) {
738 if (!pluginApi) return
739 var scriptPath = pluginApi.pluginDir + "/scripts/create-todo.py"
740 var args = ["python3", scriptPath,
741 "--task-list", taskListUid,
742 "--summary", summary]
743 if (due > 0) { args.push("--due"); args.push(String(due)) }
744 if (priority > 0) { args.push("--priority"); args.push(String(priority)) }
745 if (description) { args.push("--description"); args.push(description) }
746 createTodoProcess.command = args
747 createTodoProcess.running = true
748 }
749
750 function completeTodo(taskListUid, todoUid) {
751 if (!pluginApi) return
752 var scriptPath = pluginApi.pluginDir + "/scripts/update-todo.py"
753 updateTodoProcess.command = ["python3", scriptPath,
754 "--task-list", taskListUid, "--uid", todoUid, "--action", "complete"]
755 updateTodoProcess.running = true
756 }
757
758 function uncompleteTodo(taskListUid, todoUid) {
759 if (!pluginApi) return
760 var scriptPath = pluginApi.pluginDir + "/scripts/update-todo.py"
761 updateTodoProcess.command = ["python3", scriptPath,
762 "--task-list", taskListUid, "--uid", todoUid, "--action", "uncomplete"]
763 updateTodoProcess.running = true
764 }
765
766 function deleteTodo(taskListUid, todoUid) {
767 if (!pluginApi) return
768 var scriptPath = pluginApi.pluginDir + "/scripts/update-todo.py"
769 updateTodoProcess.command = ["python3", scriptPath,
770 "--task-list", taskListUid, "--uid", todoUid, "--action", "delete"]
771 updateTodoProcess.running = true
772 }
773}