diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 417e2410..434f4931 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -316,6 +316,7 @@ Singleton { property bool useFahrenheit: false property bool use12hourFormat: false property bool monthBeforeDay: false + property bool showWeekNumberInCalendar: false } // screen recorder diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 80b994ad..39695dcd 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -10,7 +10,7 @@ import qs.Widgets NPanel { id: root - preferredWidth: 340 + preferredWidth: Settings.data.location.showWeekNumberInCalendar ? 350 : 330 preferredHeight: 320 // Main Column @@ -60,7 +60,7 @@ NPanel { NDivider { Layout.fillWidth: true Layout.topMargin: Style.marginS * scaling - Layout.bottomMargin: Style.marginM * scaling + Layout.bottomMargin: Style.marginL * scaling } // Columns label (respects locale's first day of week) @@ -68,62 +68,172 @@ NPanel { Layout.fillWidth: true Layout.leftMargin: Style.marginS * scaling // Align with grid Layout.rightMargin: Style.marginS * scaling + Layout.bottomMargin: Style.marginM * scaling spacing: 0 - Repeater { - model: 7 + // Week header spacer or label (same width as week number column) + Item { + visible: Settings.data.location.showWeekNumberInCalendar + Layout.preferredWidth: visible ? Style.baseWidgetSize * scaling : 0 NText { - text: { - // Use the locale's first day of week setting - let firstDay = Qt.locale().firstDayOfWeek - let dayIndex = (firstDay + index) % 7 - return Qt.locale().dayName(dayIndex, Locale.ShortFormat) - } - color: Color.mSecondary - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold + anchors.centerIn: parent + text: "Week" + color: Color.mOutline + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightRegular horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true - Layout.preferredWidth: Style.baseWidgetSize * scaling + } + } + + // Day name headers - now properly aligned with calendar grid + GridLayout { + Layout.fillWidth: true + Layout.fillHeight: true + columns: 7 + rows: 1 + columnSpacing: 0 + rowSpacing: 0 + + Repeater { + model: 7 + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: Style.baseWidgetSize * scaling + + NText { + anchors.centerIn: parent + text: { + // Use the locale's first day of week setting + let firstDay = Qt.locale().firstDayOfWeek + let dayIndex = (firstDay + index) % 7 + return Qt.locale().dayName(dayIndex, Locale.ShortFormat) + } + color: Color.mSecondary + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + horizontalAlignment: Text.AlignHCenter + } + } } } } - // Grids: days - MonthGrid { - id: grid - + // Grids: days with optional week numbers + RowLayout { Layout.fillWidth: true - Layout.fillHeight: true // Take remaining space + Layout.fillHeight: true Layout.leftMargin: Style.marginS * scaling Layout.rightMargin: Style.marginS * scaling spacing: 0 - month: Time.date.getMonth() - year: Time.date.getFullYear() - locale: Qt.locale() // Use system locale - delegate: Rectangle { - width: (Style.baseWidgetSize * scaling) - height: (Style.baseWidgetSize * scaling) - radius: Style.radiusS * scaling - color: model.today ? Color.mPrimary : Color.transparent + // Week numbers column (only visible when enabled) + GridLayout { + visible: Settings.data.location.showWeekNumberInCalendar + Layout.preferredWidth: visible ? Style.baseWidgetSize * scaling : 0 + Layout.fillHeight: true + columns: 1 + rows: 6 + columnSpacing: 0 + rowSpacing: 0 - NText { - anchors.centerIn: parent - text: model.day - color: model.today ? Color.mOnPrimary : Color.mOnSurface - opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight - font.pointSize: (Style.fontSizeM * scaling) - font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular + Repeater { + model: 6 // Maximum 6 weeks in a month view + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.transparent + + NText { + anchors.centerIn: parent + color: Color.mOutline + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightBold + text: { + // Calculate the first day shown in the calendar grid + let firstDay = new Date(grid.year, grid.month, 1) + let firstDayOfWeek = Qt.locale().firstDayOfWeek + let startOffset = (firstDay.getDay() - firstDayOfWeek + 7) % 7 + let gridStartDate = new Date(grid.year, grid.month, 1 - startOffset) + + // Get the date for the start of this specific row + let rowDate = new Date(gridStartDate) + rowDate.setDate(gridStartDate.getDate() + (index * 7)) + + // Calculate week number based on the Thursday of the visual row + // This correctly handles rows that span two different ISO weeks. + let thursdayOfRow = new Date(rowDate) + let offsetToThursday = (4 - thursdayOfRow.getDay() + 7) % 7 + thursdayOfRow.setDate(thursdayOfRow.getDate() + offsetToThursday) + + // Check if this row is visible (contains days from current month) + let rowEndDate = new Date(rowDate) + rowEndDate.setDate(rowDate.getDate() + 6) + + if (rowDate.getMonth() === grid.month || rowEndDate.getMonth() === grid.month || (rowDate.getMonth() < grid.month && rowEndDate.getMonth() > grid.month)) { + return `${getISOWeekNumber(thursdayOfRow)}` + } + return "" + } + } + } } + } - Behavior on color { - ColorAnimation { - duration: Style.animationFast + // The actual calendar grid + MonthGrid { + id: grid + + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 0 + month: Time.date.getMonth() + year: Time.date.getFullYear() + locale: Qt.locale() + + delegate: Rectangle { + width: Style.baseWidgetSize * scaling + height: Style.baseWidgetSize * scaling + radius: Style.radiusS * scaling + color: model.today ? Color.mPrimary : Color.transparent + + NText { + anchors.centerIn: parent + text: model.day + color: model.today ? Color.mOnPrimary : Color.mOnSurface + opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight + font.pointSize: Style.fontSizeM * scaling + font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } } } } } } + + function getISOWeekNumber(date) { + // Create a copy of the date and normalize to noon to prevent DST issues + const targetDate = new Date(date.getTime()) + targetDate.setHours(12, 0, 0, 0) + + // Roll the date to the Thursday of the week. + // getDay() is 0 for Sunday, we want Monday to be 1 and Sunday to be 7. + const dayOfWeek = targetDate.getDay() || 7 + targetDate.setDate(targetDate.getDate() - dayOfWeek + 4) + + // Get the first day of that Thursday's year + const yearStart = new Date(targetDate.getFullYear(), 0, 1) + + // Calculate the difference in days and find the week number + const dayOfYear = ((targetDate - yearStart) / 86400000) + 1 + return Math.ceil(dayOfYear / 7) + } } diff --git a/Modules/SettingsPanel/Tabs/LocationTab.qml b/Modules/SettingsPanel/Tabs/LocationTab.qml index 0b82cba2..ca8dbd05 100644 --- a/Modules/SettingsPanel/Tabs/LocationTab.qml +++ b/Modules/SettingsPanel/Tabs/LocationTab.qml @@ -100,11 +100,18 @@ ColumnLayout { } NToggle { - label: "Show Month Before Day" + label: "Show month before day" description: "Organize your dates. On for 09/17/2025, off for 17/09/2025." checked: Settings.data.location.monthBeforeDay onToggled: checked => Settings.data.location.monthBeforeDay = checked } + + NToggle { + label: "Show week number in calendar" + description: "Displays the week number of the year in calendar view." + checked: Settings.data.location.showWeekNumberInCalendar + onToggled: checked => Settings.data.location.showWeekNumberInCalendar = checked + } } NDivider {