import "root:/widgets" import "root:/services" import "root:/config" import QtQuick Column { id: root spacing: Appearance.spacing.normal width: 280 property date currentDate: new Date() property bool isCurrentMonth: true property string currentTime: Qt.formatDateTime(new Date(), "HH:mm:ss") property int currentYear: new Date().getFullYear() property date selectedStartDate: new Date() property date selectedEndDate: new Date() property bool isSelectingRange: false property string dateCalculation: "" property bool hasSelection: false property bool hasValidSelection: root.selectedStartDate !== undefined function calculateDays() { if (!root.selectedStartDate) return if (root.selectedStartDate && root.selectedEndDate) { // For single date selection, compare with today const today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate()) const selected = new Date(root.selectedStartDate.getFullYear(), root.selectedStartDate.getMonth(), root.selectedStartDate.getDate()) const diffDays = Math.round((selected - today) / (1000 * 60 * 60 * 24)) if (diffDays === 0) { root.dateCalculation = "0 days" } else if (diffDays > 0) { root.dateCalculation = diffDays + " days until" } else { root.dateCalculation = Math.abs(diffDays) + " days since" } } } function resetSelection() { root.selectedStartDate = new Date() root.selectedEndDate = new Date() root.isSelectingRange = false root.dateCalculation = "" root.hasSelection = false } // Update time every second Timer { interval: 1000 running: true repeat: true onTriggered: { root.currentTime = Qt.formatDateTime(new Date(), "HH:mm:ss") } } // Date calculation display Row { visible: root.hasSelection anchors.horizontalCenter: parent.horizontalCenter spacing: Appearance.spacing.small StyledText { text: root.dateCalculation font.pointSize: Appearance.font.size.small font.family: Appearance.font.family.mono color: Colours.palette.m3onSurfaceVariant } MaterialIcon { text: "close" color: Colours.palette.m3onSurfaceVariant MouseArea { anchors.fill: parent onClicked: root.resetSelection() } } } // Date display section Row { anchors.left: parent.left anchors.leftMargin: 12 spacing: Appearance.spacing.normal // Today's date StyledText { text: Qt.formatDateTime(new Date(), "dd MMM yyyy") font.pointSize: Appearance.font.size.small font.family: Appearance.font.family.mono } // Arrow and selected date (when applicable) Row { visible: root.hasSelection spacing: Appearance.spacing.small StyledText { text: "→" font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } StyledText { text: Qt.formatDateTime(root.selectedStartDate, "dd MMM yyyy") font.pointSize: Appearance.font.size.small font.family: Appearance.font.family.mono color: Colours.palette.m3primary } } } // Time display StyledText { anchors.horizontalCenter: parent.horizontalCenter text: root.currentTime font.pointSize: Appearance.font.size.larger font.family: Appearance.font.family.mono } Row { anchors.horizontalCenter: parent.horizontalCenter spacing: Appearance.spacing.small Row { spacing: Appearance.spacing.small MaterialIcon { text: "chevron_left" color: Colours.palette.m3onSurfaceVariant MouseArea { anchors.fill: parent onClicked: { root.currentYear-- root.currentDate = new Date(root.currentYear, monthView.currentIndex, 1) } } } StyledText { text: root.currentYear font.pointSize: Appearance.font.size.large font.weight: 700 } MaterialIcon { text: "chevron_right" color: Colours.palette.m3onSurfaceVariant MouseArea { anchors.fill: parent onClicked: { root.currentYear++ root.currentDate = new Date(root.currentYear, monthView.currentIndex, 1) } } } } Row { spacing: Appearance.spacing.small MaterialIcon { text: "chevron_left" color: Colours.palette.m3onSurfaceVariant MouseArea { anchors.fill: parent onClicked: { monthView.decrementCurrentIndex() } } } StyledText { text: Qt.formatDateTime(root.currentDate, "MMMM") font.pointSize: Appearance.font.size.large font.weight: 400 color: Colours.palette.m3onSurfaceVariant } MaterialIcon { text: "chevron_right" color: Colours.palette.m3onSurfaceVariant MouseArea { anchors.fill: parent onClicked: { monthView.incrementCurrentIndex() } } } } } // Calendar grid Rectangle { width: 280 height: 250 anchors.horizontalCenter: parent.horizontalCenter color: "transparent" border.color: Colours.palette.m3outline border.width: 1 radius: 8 ListView { id: monthView anchors.fill: parent anchors.margins: 12 anchors.rightMargin: 0 // Reduced right margin orientation: ListView.Horizontal snapMode: ListView.SnapOneItem highlightRangeMode: ListView.StrictlyEnforceRange highlightMoveDuration: Appearance.anim.durations.normal highlightMoveVelocity: -1 highlightResizeDuration: Appearance.anim.durations.normal highlightResizeVelocity: -1 // Add mouse wheel scrolling MouseArea { anchors.fill: parent onWheel: { if (wheel.angleDelta.y > 0) { // Going to previous month if (monthView.currentIndex === 0) { root.currentYear-- monthView.currentIndex = 11 } else { monthView.decrementCurrentIndex() } } else { // Going to next month if (monthView.currentIndex === 11) { root.currentYear++ monthView.currentIndex = 0 } else { monthView.incrementCurrentIndex() } } } } model: 12 // Show all months currentIndex: new Date().getMonth() // Ensure current month is active when calendar opens Component.onCompleted: { currentIndex = new Date().getMonth() } delegate: Grid { width: monthView.width height: monthView.height opacity: monthView.currentIndex === index ? 1 : 0.2 // Dim non-current months Behavior on opacity { NumberAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standard } } columns: 7 spacing: Appearance.spacing.small // Day headers Repeater { model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] StyledText { width: 30 horizontalAlignment: Text.AlignHCenter text: modelData font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } } // Calendar days Repeater { model: { const firstDay = new Date(root.currentYear, monthView.currentIndex, 1); const lastDay = new Date(root.currentYear, monthView.currentIndex + 1, 0); const daysInMonth = lastDay.getDate(); const firstDayOfWeek = firstDay.getDay() || 7; // Convert Sunday (0) to 7 const today = new Date(); let days = []; // Add previous month's days const prevMonthLastDay = new Date(root.currentYear, monthView.currentIndex, 0).getDate(); for (let i = firstDayOfWeek - 1; i > 0; i--) { const date = new Date(root.currentYear, monthView.currentIndex - 1, prevMonthLastDay - i + 1); days.push({ day: prevMonthLastDay - i + 1, isCurrentMonth: false, isNextMonth: false, date: date }); } // Add days of the current month for (let i = 1; i <= daysInMonth; i++) { const date = new Date(root.currentYear, monthView.currentIndex, i); const isToday = i === today.getDate() && monthView.currentIndex === today.getMonth() && root.currentYear === today.getFullYear(); days.push({ day: i, isCurrentMonth: true, isToday: isToday, isNextMonth: false, date: date }); } // Add next month's days const remainingCells = Math.ceil((firstDayOfWeek - 1 + daysInMonth) / 7) * 7 - days.length; for (let i = 1; i <= remainingCells; i++) { const date = new Date(root.currentYear, monthView.currentIndex + 1, i); days.push({ day: i, isCurrentMonth: false, isNextMonth: true, date: date }); } return days; } Rectangle { width: 30 height: 30 radius: Appearance.rounding.full color: { if (modelData.date.getTime() === root.selectedStartDate.getTime()) { return Colours.palette.m3primary } else if (modelData.date.getTime() === root.selectedEndDate.getTime()) { return Colours.palette.m3secondary } else if (modelData.isToday) { return Colours.palette.m3tertiary } return "transparent" } border.width: modelData.isToday ? 2 : 0 border.color: Colours.palette.m3outline MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: function(mouse) { if (!modelData.date) return if (mouse.button === Qt.RightButton) { root.selectedStartDate = modelData.date root.hasSelection = true root.calculateDays() } else if (mouse.button === Qt.LeftButton) { if (root.selectedStartDate) { root.selectedEndDate = modelData.date root.hasSelection = true root.calculateDays() } } } } StyledText { anchors.centerIn: parent text: modelData.day font.pointSize: Appearance.font.size.small font.family: Appearance.font.family.mono color: { if (modelData.date.getTime() === root.selectedStartDate.getTime() || modelData.date.getTime() === root.selectedEndDate.getTime()) { return Colours.palette.m3onPrimary } else if (modelData.isToday) { return Colours.palette.m3onTertiary } else if (modelData.isCurrentMonth) { return Colours.palette.m3onSurface } else if (modelData.isNextMonth) { return "#404040" } else { return "#505050" } } } } } } onCurrentIndexChanged: { root.currentDate = new Date(root.currentYear, currentIndex, 1) root.isCurrentMonth = currentIndex === new Date().getMonth() && root.currentYear === new Date().getFullYear() } } } // Usage instructions Column { width: 280 anchors.horizontalCenter: parent.horizontalCenter spacing: Appearance.spacing.small StyledText { text: " Usage:" font.pointSize: Appearance.font.size.tiny color: "#505050" } StyledText { text: " • Scroll to navigate" font.pointSize: Appearance.font.size.tiny color: "#505050" } StyledText { text: " • Right-click calculates days from/till today" font.pointSize: Appearance.font.size.tiny color: "#505050" } } // Reset to current month when popout closes Connections { target: root.parent function onVisibleChanged() { if (!root.parent.visible) { monthView.currentIndex = new Date().getMonth() root.currentYear = new Date().getFullYear() root.currentDate = new Date() root.isCurrentMonth = true root.resetSelection() } } } }