quickshell/modules/bar/popouts/Calendar.qml
2025-06-16 21:23:08 +02:00

617 lines
No EOL
25 KiB
QML

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
// Show dates in chronological order
Row {
spacing: Appearance.spacing.small
StyledText {
text: {
if (!root.hasSelection) return Qt.formatDateTime(new Date(), "dd MMM yyyy")
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))
// For past dates, show selected date first
if (diffDays < 0) {
return Qt.formatDateTime(root.selectedStartDate, "dd MMM yyyy")
}
// For future dates, show today first
return Qt.formatDateTime(new Date(), "dd MMM yyyy")
}
font.pointSize: Appearance.font.size.small
font.family: Appearance.font.family.mono
color: {
if (!root.hasSelection) return Colours.palette.m3onSurface
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))
return diffDays < 0 ? Colours.palette.m3primary : Colours.palette.m3onSurface
}
}
// Arrow and second 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: {
if (!root.hasSelection) return ""
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))
// For past dates, show today second
if (diffDays < 0) {
return Qt.formatDateTime(new Date(), "dd MMM yyyy")
}
// For future dates, show selected date second
return Qt.formatDateTime(root.selectedStartDate, "dd MMM yyyy")
}
font.pointSize: Appearance.font.size.small
font.family: Appearance.font.family.mono
color: {
if (!root.hasSelection) return Colours.palette.m3onSurface
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))
return diffDays < 0 ? Colours.palette.m3onSurface : 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()
}
}
}
}
}
// Today button between controls and calendar
Rectangle {
id: todayButton
visible: root.currentYear !== new Date().getFullYear() || monthView.currentIndex !== new Date().getMonth()
anchors.horizontalCenter: parent.horizontalCenter
width: todayText.width + 20
height: 25
color: mouseArea.containsMouse ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3surfaceContainer
radius: Appearance.rounding.small
Text {
id: todayText
anchors.centerIn: parent
text: "Today"
color: Colours.palette.m3onSurface
font.pixelSize: 12
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.currentYear = new Date().getFullYear()
monthView.currentIndex = new Date().getMonth()
}
}
}
// 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
property bool canScroll: true
property Timer scrollTimer: Timer {
interval: 200 // Increased debounce time for smoother scrolling
onTriggered: parent.canScroll = true
}
// Configurable scroll direction
property bool invertScrollDirection: false // Set to true to invert the scroll direction
onWheel: {
if (!canScroll) return
// Handle trackpad horizontal scroll
if (wheel.angleDelta.x !== 0) {
const scrollRight = wheel.angleDelta.x > 0
const shouldGoBack = invertScrollDirection ? !scrollRight : scrollRight
if (shouldGoBack) {
// 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()
}
}
}
// Handle mouse wheel vertical scroll
else if (wheel.angleDelta.y !== 0) {
const scrollDown = wheel.angleDelta.y < 0
const shouldGoBack = invertScrollDirection ? !scrollDown : scrollDown
if (shouldGoBack) {
// 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()
}
}
}
// Prevent rapid scrolling
canScroll = false
scrollTimer.restart()
}
}
// Add touch gesture support with percentage-based scrolling
MouseArea {
anchors.fill: parent
property real startX: 0
property bool isDragging: false
property real scrollPercentage: 0.0 // 0.0 to 1.0, represents progress to next/prev month
property real scrollSpeed: 0.15 // Increased threshold for more deliberate scrolling
onPressed: (mouse) => {
startX = mouse.x
isDragging = true
scrollPercentage = 0.0
}
onReleased: {
isDragging = false
scrollPercentage = 0.0
}
onPositionChanged: (mouse) => {
if (!isDragging) return
const dragDistance = mouse.x - startX
const maxDragDistance = monthView.width * 0.4 // Reduced to 40% of width for more responsive feel
// Calculate scroll percentage (-1.0 to 1.0)
scrollPercentage = Math.max(-1.0, Math.min(1.0, dragDistance / maxDragDistance))
// Only trigger month change when we've scrolled enough
if (Math.abs(scrollPercentage) >= scrollSpeed) {
if (scrollPercentage > 0) {
// Scrolling right - previous month
if (monthView.currentIndex === 0) {
root.currentYear--
monthView.currentIndex = 11
} else {
monthView.decrementCurrentIndex()
}
} else {
// Scrolling left - next month
if (monthView.currentIndex === 11) {
root.currentYear++
monthView.currentIndex = 0
} else {
monthView.incrementCurrentIndex()
}
}
// Reset scroll percentage after month change
scrollPercentage = 0.0
startX = mouse.x
}
}
}
model: 12 // Show all months
currentIndex: new Date().getMonth()
// Ensure current month is active and fully visible when calendar opens
Component.onCompleted: {
currentIndex = new Date().getMonth()
// Force immediate opacity update for current month
for (let i = 0; i < count; i++) {
const item = itemAt(i)
if (item) {
item.opacity = i === currentIndex ? 1 : 0.2
}
}
}
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 {
// English notation
// model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
// German notation
model: ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
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()
}
}
}
}