Personal noctalia plugins collection
1import QtQuick 2import QtQuick.Controls 3import QtQuick.Layouts 4import Quickshell 5import qs.Commons 6import qs.Services.Location 7import qs.Services.UI 8import qs.Widgets 9 10Item { 11 id: root 12 property var pluginApi: null 13 readonly property var mainInstance: pluginApi?.mainInstance 14 readonly property var geometryPlaceholder: panelContainer 15 property real contentPreferredWidth: 950 * Style.uiScaleRatio 16 property real contentPreferredHeight: 700 * Style.uiScaleRatio 17 property real topHeaderHeight: 60 * Style.uiScaleRatio 18 readonly property bool allowAttach: mainInstance ? mainInstance.panelModeSetting === "attached" : false 19 readonly property bool panelAnchorHorizontalCenter: mainInstance ? mainInstance.panelModeSetting === "centered" : false 20 readonly property bool panelAnchorVerticalCenter: mainInstance ? mainInstance.panelModeSetting === "centered" : false 21 anchors.fill: parent 22 23 property real hourHeight: 50 * Style.uiScaleRatio 24 property real timeColumnWidth: 65 * Style.uiScaleRatio 25 property real daySpacing: 1 * Style.uiScaleRatio 26 27 // Attempt at live syncing 28 Connections { 29 target: CalendarService 30 enabled: root.visible 31 function onEventsChanged() { 32 if (mainInstance) { 33 Qt.callLater(() => { 34 mainInstance.updateEventsFromService() 35 mainInstance.calculateAllDayEventLayout() 36 }) 37 } 38 } 39 } 40 41 Component.onCompleted: mainInstance?.initializePlugin() 42 onVisibleChanged: if (visible && mainInstance) { 43 mainInstance.updateEventsFromService() 44 mainInstance.goToToday() 45 Qt.callLater(root.scrollToCurrentTime) 46 } 47 48 // Scroll to time indicator position 49 function scrollToCurrentTime() { 50 if (!mainInstance || !calendarFlickable) return 51 var now = new Date(), today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) 52 var weekStart = new Date(mainInstance.weekStart) 53 var weekEnd = new Date(weekStart.getFullYear(), weekStart.getMonth(), weekStart.getDate() + 7) 54 55 if (today >= weekStart && today < weekEnd) { 56 var currentHour = now.getHours() + now.getMinutes() / 60 57 var scrollPos = (currentHour * hourHeight) - (calendarFlickable.height / 2) 58 var maxScroll = Math.max(0, (24 * hourHeight) - calendarFlickable.height) 59 scrollAnim.targetY = Math.max(0, Math.min(scrollPos, maxScroll)) 60 scrollAnim.start() 61 } 62 } 63 64 // UI 65 Rectangle { 66 id: panelContainer 67 anchors.fill: parent 68 color: "transparent" 69 70 ColumnLayout { 71 anchors.fill: parent 72 anchors.margins: Style.marginM 73 spacing: Style.marginM 74 75 //Header Section 76 Rectangle { 77 id: header 78 Layout.fillWidth: true 79 Layout.preferredHeight: topHeaderHeight 80 color: Color.mSurfaceVariant 81 radius: Style.radiusM 82 83 RowLayout { 84 anchors.margins: Style.marginM 85 anchors.fill: parent 86 87 NIcon { icon: "calendar-week"; pointSize: Style.fontSizeXXL; color: Color.mPrimary } 88 89 ColumnLayout { 90 Layout.fillHeight: true 91 spacing: 0 92 NText { 93 text: pluginApi.tr("panel.header") 94 font.pointSize: Style.fontSizeL; font.weight: Font.Bold; color: Color.mOnSurface 95 } 96 RowLayout { 97 spacing: Style.marginS 98 NText { 99 text: mainInstance?.monthRangeText || "" 100 font.pointSize: Style.fontSizeS; font.weight: Font.Medium; color: Color.mOnSurfaceVariant 101 } 102 Rectangle { 103 Layout.preferredWidth: 8; Layout.preferredHeight: 8; radius: 4 104 color: mainInstance?.isLoading ? Color.mError : 105 mainInstance?.syncStatus?.includes("No") ? Color.mError : Color.mOnSurfaceVariant 106 } 107 NText { 108 text: mainInstance?.syncStatus || "" 109 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 110 } 111 } 112 } 113 114 Item { Layout.fillWidth: true } 115 116 RowLayout { 117 spacing: Style.marginS 118 NIconButton { 119 icon: "chevron-left" 120 onClicked: { mainInstance?.navigateWeek(-7); mainInstance?.loadEvents() } 121 } 122 NIconButton { 123 icon: "calendar"; tooltipText: pluginApi.tr("panel.today") 124 onClicked: { mainInstance?.goToToday(); mainInstance?.loadEvents(); Qt.callLater(root.scrollToCurrentTime) } 125 } 126 NIconButton { 127 icon: "chevron-right" 128 onClicked: { mainInstance?.navigateWeek(7); mainInstance?.loadEvents() } 129 } 130 NIconButton { 131 icon: "refresh"; tooltipText: I18n.tr("common.refresh") 132 onClicked: mainInstance?.loadEvents() 133 enabled: mainInstance ? !mainInstance.isLoading : false 134 } 135 NIconButton { 136 icon: "close"; tooltipText: I18n.tr("common.close") 137 onClicked: pluginApi.closePanel(pluginApi.panelOpenScreen) 138 } 139 } 140 } 141 } 142 143 // Calendar 144 Rectangle { 145 Layout.fillWidth: true 146 Layout.fillHeight: true 147 color: Color.mSurfaceVariant 148 radius: Style.radiusM 149 clip: true 150 151 Column { 152 anchors.fill: parent 153 spacing: 0 154 155 //Day Headers 156 Rectangle { 157 id: dayHeaders 158 width: parent.width 159 height: 56 160 color: Color.mSurfaceVariant 161 radius: Style.radiusM 162 163 Row { 164 anchors.fill: parent 165 anchors.leftMargin: root.timeColumnWidth 166 spacing: root.daySpacing 167 168 Repeater { 169 model: 7 170 Rectangle { 171 width: mainInstance?.dayColumnWidth 172 height: parent.height 173 color: "transparent" 174 property date dayDate: mainInstance?.weekDates?.[index] || new Date() 175 property bool isToday: { 176 var today = new Date() 177 return dayDate.getDate() === today.getDate() && 178 dayDate.getMonth() === today.getMonth() && 179 dayDate.getFullYear() === today.getFullYear() 180 } 181 Rectangle { 182 anchors.fill: parent 183 anchors.margins: 4 184 color: Color.mSurfaceVariant 185 border.color: isToday ? Color.mPrimary : "transparent" 186 border.width: 2 187 radius: Style.radiusM 188 Column { 189 anchors.centerIn: parent 190 spacing: 2 191 NText { 192 anchors.horizontalCenter: parent.horizontalCenter 193 text: dayDate ? I18n.locale.dayName(dayDate.getDay(), Locale.ShortFormat).toUpperCase() : "" 194 color: isToday ? Color.mPrimary : Color.mOnSurface 195 font.pointSize: Style.fontSizeS; font.weight: Font.Medium 196 } 197 NText { 198 anchors.horizontalCenter: parent.horizontalCenter 199 text: dayDate ? ((dayDate.getDate() < 10 ? "0" : "") + dayDate.getDate()) : "" 200 color: isToday ? Color.mPrimary : Color.mOnSurface 201 font.pointSize: Style.fontSizeM; font.weight: Font.Bold 202 } 203 } 204 } 205 } 206 } 207 } 208 } 209 // All-day row 210 Rectangle { 211 id: allDayEventsSection 212 width: parent.width 213 height: mainInstance ? Math.round(mainInstance.allDaySectionHeight * Style.uiScaleRatio) : 0 214 color: Color.mSurfaceVariant 215 visible: height > 0 216 217 Item { 218 id: allDayEventsContainer 219 anchors.fill: parent 220 anchors.leftMargin: root.timeColumnWidth 221 222 Repeater { 223 model: 6 224 delegate: Rectangle { 225 width: 1; height: parent.height 226 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2) 227 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9) 228 } 229 } 230 231 Repeater { 232 model: mainInstance?.allDayEventsWithLayout || [] 233 delegate: Item { 234 property var eventData: modelData 235 x: eventData.startDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 236 y: eventData.lane * 25 237 width: (eventData.spanDays * ((mainInstance?.dayColumnWidth) + (root.daySpacing))) - (root.daySpacing) 238 height: 24 239 240 Rectangle { 241 anchors.fill: parent 242 color: Color.mTertiary 243 radius: Style.radiusS 244 NText { 245 anchors.fill: parent; anchors.margins: 4 246 text: eventData.title 247 color: Color.mOnTertiary 248 font.pointSize: Style.fontSizeXXS; font.weight: Font.Medium 249 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter 250 } 251 } 252 MouseArea { 253 anchors.fill: parent 254 hoverEnabled: true 255 cursorShape: Qt.PointingHandCursor 256 onEntered: { 257 var tip = mainInstance?.getEventTooltip(eventData) || "" 258 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed) 259 } 260 onClicked: mainInstance?.handleEventClick(eventData) 261 onExited: TooltipService.hide() 262 } 263 } 264 } 265 } 266 } 267 // Calendar flickable 268 Rectangle { 269 width: parent.width 270 height: parent.height - dayHeaders.height - (allDayEventsSection.visible ? allDayEventsSection.height : 0) 271 color: Color.mSurfaceVariant 272 radius: Style.radiusM 273 clip: true 274 275 Flickable { 276 id: calendarFlickable 277 anchors.fill: parent 278 clip: true 279 contentHeight: 24 * (root.hourHeight) 280 boundsBehavior: Flickable.DragOverBounds 281 282 Component.onCompleted: { 283 calendarFlickable.forceActiveFocus() 284 } 285 286 // Keyboard interaction 287 Keys.onPressed: function(event) { 288 if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) { 289 var step = root.hourHeight 290 var targetY = event.key === Qt.Key_Up ? Math.max(0, contentY - step) : 291 Math.min(Math.max(0, contentHeight - height), contentY + step) 292 scrollAnim.targetY = targetY 293 scrollAnim.start() 294 event.accepted = true 295 } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) { 296 if (mainInstance) { 297 mainInstance.navigateWeek(event.key === Qt.Key_Left ? -7 : 7) 298 mainInstance.loadEvents() 299 } 300 event.accepted = true 301 } 302 } 303 304 NumberAnimation { 305 id: scrollAnim 306 target: calendarFlickable; property: "contentY"; duration: 100 307 easing.type: Easing.OutCubic; property real targetY: 0; to: targetY 308 } 309 310 ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } 311 312 Row { 313 width: parent.width 314 height: parent.height 315 316 // Time Column 317 Column { 318 width: root.timeColumnWidth 319 height: parent.height 320 Repeater { 321 model: 23 322 Rectangle { 323 width: root.timeColumnWidth 324 height: root.hourHeight 325 color: "transparent" 326 NText { 327 text: { 328 var hour = index + 1 329 if (mainInstance?.use12hourFormat) { 330 var d = new Date(); d.setHours(hour, 0, 0, 0) 331 return mainInstance.formatTime(d) 332 } 333 return (hour < 10 ? "0" : "") + hour + ':00' 334 } 335 anchors.right: parent.right 336 anchors.rightMargin: Style.marginS 337 anchors.verticalCenter: parent.top 338 anchors.verticalCenterOffset: root.hourHeight 339 font.pointSize: Style.fontSizeXS; color: Color.mOnSurfaceVariant 340 } 341 } 342 } 343 } 344 345 // Hour Rectangles 346 Item { 347 width: 7 * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 348 height: parent.height 349 350 Row { 351 anchors.fill: parent 352 spacing: root.daySpacing 353 Repeater { 354 model: 7 355 Column { 356 width: mainInstance?.dayColumnWidth 357 height: parent.height 358 Repeater { 359 model: 24 360 Rectangle { width: parent.width; height: 1; color: Color.mSurfaceVariant } 361 } 362 } 363 } 364 } 365 // Hour Lines 366 Repeater { 367 model: 24 368 Rectangle { 369 width: parent.width; height: 1 370 y: index * (root.hourHeight) 371 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.hourLineOpacitySetting || 0.5) 372 } 373 } 374 // Day Lines 375 Repeater { 376 model: 6 377 Rectangle { 378 width: 1; height: parent.height 379 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2) 380 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9) 381 } 382 } 383 384 // Event positioning 385 Repeater { 386 model: mainInstance?.eventsModel 387 delegate: Item { 388 property var eventData: model 389 property int dayIndex: mainInstance?.getDisplayDayIndexForDate(model.startTime) ?? -1 390 property real startHour: model.startTime.getHours() + model.startTime.getMinutes() / 60 391 property real endHour: model.endTime.getHours() + model.endTime.getMinutes() / 60 392 property real duration: Math.max(0, (model.endTime - model.startTime) / 3600000) 393 394 property real exactHeight: Math.max(1, duration * (mainInstance?.hourHeight || 50) - 1) 395 property bool isCompact: exactHeight < 40 396 property var overlapInfo: mainInstance?.overlappingEventsData?.[index] ?? { 397 xOffset: 0, width: (mainInstance?.dayColumnWidth) - 8, lane: 0, totalLanes: 1 398 } 399 property real eventWidth: overlapInfo.width - 1 400 property real eventXOffset: overlapInfo.xOffset 401 402 visible: dayIndex >= 0 && dayIndex < 7 && duration > 0 403 width: eventWidth 404 height: exactHeight 405 x: dayIndex * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) + eventXOffset 406 y: startHour * (mainInstance?.hourHeight || 50) 407 z: 100 + overlapInfo.lane 408 409 Rectangle { 410 anchors.fill: parent 411 color: Color.mPrimary 412 radius: Style.radiusS 413 opacity: 0.9 414 clip: true 415 Rectangle { 416 visible: exactHeight < 5 && overlapInfo.lane > 0 417 anchors.fill: parent 418 color: "transparent" 419 radius: parent.radius 420 border.width: 1 421 border.color: Color.mPrimary 422 } 423 Loader { 424 anchors.fill: parent 425 anchors.margins: exactHeight < 10 ? 1 : Style.marginS 426 anchors.leftMargin: exactHeight < 10 ? 1 : Style.marginS + 3 427 sourceComponent: isCompact ? compactLayout : normalLayout 428 } 429 } 430 431 Component { 432 id: normalLayout 433 Column { 434 spacing: 2 435 width: parent.width - 3 436 NText { 437 visible: exactHeight >= 20 438 text: model.title 439 color: Color.mOnPrimary 440 font.pointSize: Style.fontSizeXS; font.weight: Font.Medium 441 elide: Text.ElideRight; width: parent.width 442 } 443 NText { 444 visible: exactHeight >= 30 445 text: mainInstance?.formatTimeRangeForDisplay(model) || "" 446 color: Color.mOnPrimary 447 font.pointSize: Style.fontSizeXXS; opacity: 0.9 448 elide: Text.ElideRight; width: parent.width 449 } 450 } 451 } 452 453 Component { 454 id: compactLayout 455 NText { 456 text: exactHeight < 15 ? model.title : 457 model.title + " • " + (mainInstance?.formatTimeRangeForDisplay(model) || "") 458 color: Color.mOnPrimary 459 font.pointSize: exactHeight < 15 ? Style.fontSizeXXS : Style.fontSizeXS 460 font.weight: Font.Medium 461 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter 462 width: parent.width - 3 463 } 464 } 465 466 MouseArea { 467 anchors.fill: parent 468 hoverEnabled: true 469 cursorShape: Qt.PointingHandCursor 470 onEntered: { 471 var tip = mainInstance?.getEventTooltip(model) || "" 472 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed) 473 } 474 onClicked: mainInstance?.handleEventClick(eventData) 475 onExited: TooltipService.hide() 476 } 477 } 478 } 479 480 // Time Indicator 481 Rectangle { 482 property var now: new Date() 483 property date today: new Date(now.getFullYear(), now.getMonth(), now.getDate()) 484 property date weekStartDate: mainInstance?.weekStart ?? new Date() 485 property date weekEndDate: mainInstance ? 486 new Date(mainInstance.weekStart.getFullYear(), mainInstance.weekStart.getMonth(), mainInstance.weekStart.getDate() + 7) : new Date() 487 property bool inCurrentWeek: today >= weekStartDate && today < weekEndDate 488 property int currentDay: mainInstance?.getDayIndexForDate(now) ?? -1 489 property real currentHour: now.getHours() + now.getMinutes() / 60 490 491 visible: inCurrentWeek && currentDay >= 0 492 width: mainInstance?.dayColumnWidth 493 height: 2 494 x: currentDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 495 y: currentHour * (root.hourHeight) 496 color: Color.mError 497 radius: 1 498 z: 1000 499 Rectangle { 500 width: 8; height: 8; radius: 4; color: Color.mError 501 anchors.verticalCenter: parent.verticalCenter; x: -4 502 } 503 Timer { 504 interval: 60000; running: true; repeat: true 505 onTriggered: parent.now = new Date() 506 } 507 } 508 } 509 } 510 } 511 } 512 } 513 } 514 } 515 } 516}