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