614 lines
No EOL
25 KiB
QML
614 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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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: 100 // Debounce time between scrolls
|
|
onTriggered: parent.canScroll = true
|
|
}
|
|
|
|
// Configurable scroll direction
|
|
property bool invertScrollDirection: false // Set to true to invert the scroll direction
|
|
|
|
onWheel: {
|
|
if (!canScroll) return
|
|
|
|
// Handle two-finger scroll (horizontal)
|
|
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 vertical scroll (optional, can be removed if not needed)
|
|
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.1 // Adjust this to control how fast months change (lower = slower)
|
|
|
|
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.5 // Half the width for full month transition
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add Today button
|
|
Rectangle {
|
|
id: todayButton
|
|
visible: root.currentYear !== new Date().getFullYear() || monthView.currentIndex !== new Date().getMonth()
|
|
anchors {
|
|
top: parent.top
|
|
horizontalCenter: parent.horizontalCenter
|
|
topMargin: -24
|
|
}
|
|
width: todayText.width + 20
|
|
height: 25
|
|
color: mouseArea.containsMouse ? "#3d3d3d" : "#2d2d2d"
|
|
radius: 4
|
|
|
|
Text {
|
|
id: todayText
|
|
anchors.centerIn: parent
|
|
text: "Today"
|
|
color: "#ffffff"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
MouseArea {
|
|
id: mouseArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: {
|
|
root.currentYear = new Date().getFullYear()
|
|
monthView.currentIndex = new Date().getMonth()
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
// 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()
|
|
}
|
|
}
|
|
}
|
|
} |