From 1846c7af2e3b842ce33d43bcfbb8c42f894dc5cf Mon Sep 17 00:00:00 2001 From: Corey Woodworth Date: Fri, 24 Oct 2025 23:34:58 -0400 Subject: [PATCH] Added option for an analog style clock on the calendar screen. --- Assets/Translations/de.json | 4 + Assets/Translations/en.json | 4 + Assets/Translations/es.json | 4 + Assets/Translations/fr.json | 4 + Assets/Translations/pt.json | 4 + Assets/Translations/zh-CN.json | 4 + Commons/Settings.qml | 1 + Modules/Bar/Calendar/CalendarPanel.qml | 207 +++++++++++++++++++------ Modules/Settings/Tabs/LocationTab.qml | 7 + 9 files changed, 195 insertions(+), 44 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 65904b16..9343f16c 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -630,6 +630,10 @@ "show-events": { "label": "Kalenderereignisse anzeigen", "description": "Ereignisse im Kalender-Panel anzeigen." + }, + "use-analog": { + "description": "Eine Analoguhr auf dem Kalenderbildschirm anzeigen.", + "label": "Analoge Uhr verwenden" } } }, diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 9b590938..42b86d9d 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -630,6 +630,10 @@ "show-events": { "label": "Show calendar events", "description": "Display events in the calendar panel." + }, + "use-analog": { + "label": "Use analog style clock", + "description": "Show an analog style clock on the calendar screen." } } }, diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 64f84c66..ea84a65b 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -630,6 +630,10 @@ "show-events": { "label": "Mostrar eventos del calendario", "description": "Mostrar eventos en el panel del calendario." + }, + "use-analog": { + "description": "Mostrar un reloj de estilo analógico en la pantalla del calendario.", + "label": "Usar reloj de estilo analógico" } } }, diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index c57e4938..abbacdc0 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -630,6 +630,10 @@ "show-events": { "label": "Afficher les événements du calendrier", "description": "Afficher les événements dans le panneau du calendrier." + }, + "use-analog": { + "description": "Afficher une horloge de style analogique sur l'écran du calendrier.", + "label": "Utiliser une horloge de style analogique." } } }, diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index ad6f88f4..78885367 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -592,6 +592,10 @@ "show-events": { "label": "Mostrar eventos do calendário", "description": "Exibir eventos no painel do calendário." + }, + "use-analog": { + "description": "Mostrar um relógio estilo analógico na tela do calendário.", + "label": "Use um relógio de estilo analógico." } } }, diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index d8cc9406..31064e39 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -630,6 +630,10 @@ "show-events": { "label": "显示日历事件", "description": "在日历面板中显示事件。" + }, + "use-analog": { + "description": "在日历屏幕上显示一个模拟时钟。", + "label": "使用模拟时钟样式" } } }, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 709cebba..2268cec3 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -203,6 +203,7 @@ Singleton { property bool use12hourFormat: false property bool showWeekNumberInCalendar: false property bool showCalendarEvents: true + property bool analogClockInCalendar: false } // screen recorder diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index b873f706..3accb309 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -95,16 +95,16 @@ NPanel { text: { if (!Settings.data.location.weatherEnabled) return "" - if (!weatherReady) - return "" - var temp = LocationService.data.weather.current_weather.temperature - var suffix = "C" - if (Settings.data.location.useFahrenheit) { - temp = LocationService.celsiusToFahrenheit(temp) - suffix = "F" - } - temp = Math.round(temp) - return `${temp}°${suffix}` + if (!weatherReady) + return "" + var temp = LocationService.data.weather.current_weather.temperature + var suffix = "C" + if (Settings.data.location.useFahrenheit) { + temp = LocationService.celsiusToFahrenheit(temp) + suffix = "F" + } + temp = Math.round(temp) + return `${temp}°${suffix}` } pointSize: Style.fontSizeM font.weight: Style.fontWeightBold @@ -174,10 +174,10 @@ NPanel { text: { if (!Settings.data.location.weatherEnabled) return "" - if (!weatherReady) - return I18n.tr("calendar.weather.loading") - const chunks = Settings.data.location.name.split(",") - return chunks[0] + if (!weatherReady) + return I18n.tr("calendar.weather.loading") + const chunks = Settings.data.location.name.split(",") + return chunks[0] } pointSize: Style.fontSizeM font.weight: Style.fontWeightMedium @@ -202,19 +202,147 @@ NPanel { } } - // Digital clock with circular progress + // Analog clock Item { id: clockItem - Layout.alignment: Qt.AlignVCenter anchors.right: parent.right anchors.rightMargin: Style.marginM anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter height: Math.round((Style.fontSizeXXXL * 1.9) / 2 * Style.uiScaleRatio) * 2 width: clockItem.height - // Seconds circular progress + Canvas { + id: clockCanvas + visible: Settings.data.location.analogClockInCalendar + anchors.fill: parent + + property int hours: now.getHours() + property int minutes: now.getMinutes() + property int seconds: now.getSeconds() + property real markAlpha: 0.7 + property color secondHandColor: { + var defaultColor = Color.mError + var backgroundL = Color.mPrimary.hslLightness + var hourMarkL = (Color.mOnPrimary.hslLightness * markAlpha) + (backgroundL *(1.0-markAlpha)) + + var bestWorstContrast = -1 + var bestColor = defaultColor + + var candidates = [ + Color.mSecondary, + Color.mTertiary, + Color.mError, + ] + + for (var i = 0; i < candidates.length; i++) { + var candidateColor = candidates[i] + var candidateL = candidateColor.hslLightness + + var diffBackground = Math.abs(backgroundL - candidateL) + var diffHourMark = Math.abs(hourMarkL - candidateL) + + var currentWorstContrast = Math.min(diffBackground, diffHourMark) + + if (currentWorstContrast > bestWorstContrast) { + bestWorstContrast = currentWorstContrast + bestColor = candidateColor + } + } + + return bestColor + } + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + ctx.translate(width / 2, height / 2) + var radius = Math.min(width, height) / 2 + + // Hour marks + ctx.strokeStyle = Qt.alpha(Color.mOnPrimary, markAlpha) + ctx.lineWidth = 2 * Style.uiScaleRatio + var scaleFactor = 0.7 + + for (var i = 0; i < 12; i++) { + var scaleFactor = 0.8 + if (i % 3 === 0) { + scaleFactor = 0.65 + } + ctx.save() + ctx.rotate(i * Math.PI / 6) + ctx.beginPath() + ctx.moveTo(0, -radius * scaleFactor) + ctx.lineTo(0, -radius) + ctx.stroke() + ctx.restore() + } + + // Hour hand + ctx.save() + var hourAngle = (hours % 12 + minutes / 60) * Math.PI / 6 + ctx.rotate(hourAngle) + ctx.strokeStyle = Color.mOnPrimary + ctx.lineWidth = 3 * Style.uiScaleRatio + ctx.lineCap = "round" + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(0, -radius * 0.6) + ctx.stroke() + ctx.restore() + + // Minute hand + ctx.save() + var minuteAngle = (minutes + seconds / 60) * Math.PI / 30 + ctx.rotate(minuteAngle) + ctx.strokeStyle = Color.mOnPrimary + ctx.lineWidth = 2 * Style.uiScaleRatio + ctx.lineCap = "round" + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(0, -radius * 0.9) + ctx.stroke() + ctx.restore() + + // Second hand + ctx.save() + var secondAngle = seconds * Math.PI / 30 + ctx.rotate(secondAngle) + ctx.strokeStyle = secondHandColor + ctx.lineWidth = 1.6 * Style.uiScaleRatio + ctx.lineCap = "round" + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(0, -radius) + ctx.stroke() + ctx.restore() + + // Center dot + ctx.beginPath() + ctx.arc(0, 0, 3 * Style.uiScaleRatio, 0, 2 * Math.PI) + ctx.fillStyle = Color.mOnPrimary + ctx.fill() + } + + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: { + clockCanvas.hours = now.getHours() + clockCanvas.minutes = now.getMinutes() + clockCanvas.seconds = now.getSeconds() + clockCanvas.requestPaint() + } + } + + Component.onCompleted: requestPaint() + } + + // Digital clock's seconds circular progress Canvas { id: secondsProgress + visible: !Settings.data.location.analogClockInCalendar anchors.fill: parent property real progress: now.getSeconds() / 60 onProgressChanged: requestPaint() @@ -251,6 +379,7 @@ NPanel { // Digital clock ColumnLayout { + visible: !Settings.data.location.analogClockInCalendar anchors.centerIn: parent spacing: -Style.marginXXS @@ -279,7 +408,6 @@ NPanel { } } - // ... (rest of the file is unchanged) ... RowLayout { visible: weatherReady Layout.fillWidth: true @@ -429,14 +557,14 @@ NPanel { if (!CalendarService.available || CalendarService.events.length === 0) return false - const targetDate = new Date(year, month, day) - const targetStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000 - const targetEnd = targetStart + 86400 // +24 hours + const targetDate = new Date(year, month, day) + const targetStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000 + const targetEnd = targetStart + 86400 // +24 hours - return CalendarService.events.some(event => { - // Check if event starts or overlaps with this day - return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd) - }) + return CalendarService.events.some(event => { + // Check if event starts or overlaps with this day + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd) + }) } // Helper function to get events for a specific date @@ -444,13 +572,13 @@ NPanel { if (!CalendarService.available || CalendarService.events.length === 0) return [] - const targetDate = new Date(year, month, day) - const targetStart = Math.floor(new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000) - const targetEnd = targetStart + 86400 // +24 hours + const targetDate = new Date(year, month, day) + const targetStart = Math.floor(new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000) + const targetEnd = targetStart + 86400 // +24 hours - return CalendarService.events.filter(event => { - return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd) - }) + return CalendarService.events.filter(event => { + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd) + }) } // Helper function to check if an event is all-day @@ -550,9 +678,9 @@ NPanel { color: { if (model.today) return Color.mOnSecondary - if (model.month === grid.month) - return Color.mOnSurface - return Color.mOnSurfaceVariant + if (model.month === grid.month) + return Color.mOnSurface + return Color.mOnSurfaceVariant } opacity: model.month === grid.month ? 1.0 : 0.4 pointSize: Style.fontSizeM @@ -617,15 +745,6 @@ NPanel { } } } - - function getISOWeekNumber(date) { - const target = new Date(date.getTime()) - target.setHours(0, 0, 0, 0) - const dayOfWeek = target.getDay() || 7 - target.setDate(target.getDate() + 4 - dayOfWeek) - const yearStart = new Date(target.getFullYear(), 0, 1) - const weekNumber = Math.ceil(((target - yearStart) / 86400000 + 1) / 7) - return weekNumber - } } } + diff --git a/Modules/Settings/Tabs/LocationTab.qml b/Modules/Settings/Tabs/LocationTab.qml index 77048745..f257f3ff 100644 --- a/Modules/Settings/Tabs/LocationTab.qml +++ b/Modules/Settings/Tabs/LocationTab.qml @@ -124,6 +124,13 @@ ColumnLayout { checked: Settings.data.location.showCalendarEvents onToggled: checked => Settings.data.location.showCalendarEvents = checked } + + NToggle { + label: I18n.tr("settings.location.date-time.use-analog.label") + description: I18n.tr("settings.location.date-time.use-analog.description") + checked: Settings.data.location.analogClockInCalendar + onToggled: checked => Settings.data.location.analogClockInCalendar = checked + } } NDivider {