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}