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}