From e972e1f7aa9a79714c720909ec5c2fe0739ccda5 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Sun, 30 Nov 2025 14:26:09 -0500 Subject: [PATCH] Cards & Settings refactoring - All cards now live in Modules/Cards - CalendarPanel is now called ClockPanel - Added a way to ease settings migration in separate QML files --- Commons/Migrations/Migration26.qml | 44 ++ Commons/Migrations/MigrationRegistry.qml | 15 + Commons/Settings.qml | 76 ++- Modules/Bar/Widgets/Clock.qml | 6 +- .../ControlCenter => }/Cards/AudioCard.qml | 0 Modules/Cards/CalendarHeaderCard.qml | 132 ++++ Modules/Cards/CalendarMonthCard.qml | 396 +++++++++++ .../ControlCenter => }/Cards/MediaCard.qml | 0 .../ControlCenter => }/Cards/ProfileCard.qml | 1 - .../Cards/ShortcutsCard.qml | 1 - .../Cards/SystemMonitorCard.qml | 0 .../{Panels/Calendar => Cards}/TimerCard.qml | 0 .../ControlCenter => }/Cards/WeatherCard.qml | 0 .../MainScreen/Backgrounds/AllBackgrounds.qml | 4 +- Modules/MainScreen/MainScreen.qml | 12 +- Modules/Panels/Calendar/CalendarPanel.qml | 636 ------------------ Modules/Panels/Clock/ClockPanel.qml | 87 +++ .../ControlCenter/ControlCenterPanel.qml | 2 +- Modules/Panels/Settings/Tabs/LocationTab.qml | 8 +- Services/Control/IPCService.qml | 4 +- 20 files changed, 738 insertions(+), 686 deletions(-) create mode 100644 Commons/Migrations/Migration26.qml create mode 100644 Commons/Migrations/MigrationRegistry.qml rename Modules/{Panels/ControlCenter => }/Cards/AudioCard.qml (100%) create mode 100644 Modules/Cards/CalendarHeaderCard.qml create mode 100644 Modules/Cards/CalendarMonthCard.qml rename Modules/{Panels/ControlCenter => }/Cards/MediaCard.qml (100%) rename Modules/{Panels/ControlCenter => }/Cards/ProfileCard.qml (98%) rename Modules/{Panels/ControlCenter => }/Cards/ShortcutsCard.qml (98%) rename Modules/{Panels/ControlCenter => }/Cards/SystemMonitorCard.qml (100%) rename Modules/{Panels/Calendar => Cards}/TimerCard.qml (100%) rename Modules/{Panels/ControlCenter => }/Cards/WeatherCard.qml (100%) delete mode 100644 Modules/Panels/Calendar/CalendarPanel.qml create mode 100644 Modules/Panels/Clock/ClockPanel.qml diff --git a/Commons/Migrations/Migration26.qml b/Commons/Migrations/Migration26.qml new file mode 100644 index 00000000..42448fb8 --- /dev/null +++ b/Commons/Migrations/Migration26.qml @@ -0,0 +1,44 @@ +import QtQuick + +QtObject { + id: root + + // Migrate from version < 26 to version 26 + // Replaces old calendar-card and banner-card with calendar-header-card and calendar-month-card + function migrate(adapter, logger) { + logger.i("Settings", "Migrating settings to v26"); + + // Replace old calendar-card and banner-card with calendar-header-card and calendar-month-card + if (adapter.calendar !== undefined && adapter.calendar.cards !== undefined) { + const oldCards = adapter.calendar.cards; + const newCards = []; + let anyCalendarEnabled = false; + + // Check if any calendar-related card was enabled + for (var i = 0; i < oldCards.length; i++) { + const card = oldCards[i]; + if ((card.id === "banner-card" || card.id === "calendar-card") && card.enabled) { + anyCalendarEnabled = true; + } else if (card.id !== "banner-card" && card.id !== "calendar-card") { + // Keep other cards as-is (timer, weather) + newCards.push(card); + } + } + + // Add new split cards at the beginning (enabled if any old calendar card was enabled) + newCards.unshift({ + "id": "calendar-month-card", + "enabled": anyCalendarEnabled + }); + newCards.unshift({ + "id": "calendar-header-card", + "enabled": anyCalendarEnabled + }); + + adapter.calendar.cards = newCards; + logger.i("Settings", "Replaced old calendar cards with calendar-header-card + calendar-month-card"); + } + + return true; + } +} diff --git a/Commons/Migrations/MigrationRegistry.qml b/Commons/Migrations/MigrationRegistry.qml new file mode 100644 index 00000000..8ef1fb8b --- /dev/null +++ b/Commons/Migrations/MigrationRegistry.qml @@ -0,0 +1,15 @@ +pragma Singleton + +import QtQuick + +QtObject { + id: root + + // Map of version number to migration component + readonly property var migrations: ({ + 26: migration26Component + }) + + // Migration components + property Component migration26Component: Migration26 {} +} diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 33b69c54..29cc6dd6 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -5,6 +5,7 @@ import Quickshell import Quickshell.Io import "../Helpers/QtObj2JS.js" as QtObj2JS import qs.Commons +import qs.Commons.Migrations import qs.Modules.OSD import qs.Services.UI @@ -21,7 +22,7 @@ Singleton { - Default cache directory: ~/.cache/noctalia */ readonly property alias data: adapter // Used to access via Settings.data.xxx.yyy - readonly property int settingsVersion: 25 + readonly property int settingsVersion: 26 readonly property bool isDebug: Quickshell.env("NOCTALIA_DEBUG") === "1" readonly property string shellName: "noctalia" readonly property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/" @@ -100,6 +101,10 @@ Singleton { if (!isLoaded) { Logger.i("Settings", "Settings loaded"); + // ----------------- + // Run versioned migrations from MigrationRegistry + runVersionedMigrations(); + upgradeSettingsData(); root.isLoaded = true; @@ -267,11 +272,11 @@ Singleton { property JsonObject calendar: JsonObject { property list cards: [ { - "id": "banner-card", + "id": "calendar-header-card", "enabled": true }, { - "id": "calendar-card", + "id": "calendar-month-card", "enabled": true }, { @@ -627,6 +632,41 @@ Singleton { } } + // ----------------------------------------------------- + // Run versioned migrations using MigrationRegistry + function runVersionedMigrations() { + const currentVersion = adapter.settingsVersion; + const migrations = MigrationRegistry.migrations; + + // Get all migration versions and sort them + const versions = Object.keys(migrations).map(v => parseInt(v)).sort((a, b) => a - b); + + // Run migrations in order for versions newer than current + for (var i = 0; i < versions.length; i++) { + const version = versions[i]; + + if (currentVersion < version) { + // Create migration instance and run it + const migrationComponent = migrations[version]; + const migration = migrationComponent.createObject(root); + + if (migration && typeof migration.migrate === "function") { + const success = migration.migrate(adapter, Logger); + if (!success) { + Logger.e("Settings", "Migration to v" + version + " failed"); + } + } else { + Logger.e("Settings", "Invalid migration for v" + version); + } + + // Clean up migration instance + if (migration) { + migration.destroy(); + } + } + } + } + // ----------------------------------------------------- // Function to clean up deprecated user/custom bar widgets settings function upgradeWidget(widget) { @@ -677,31 +717,7 @@ Singleton { const sections = ["left", "center", "right"]; // ----------------- - // 1st. convert old widget id to new id - for (var s = 0; s < sections.length; s++) { - const sectionName = sections[s]; - for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) { - var widget = adapter.bar.widgets[sectionName][i]; - - switch (widget.id) { - case "DarkModeToggle": - widget.id = "DarkMode"; - break; - case "PowerToggle": - widget.id = "SessionMenu"; - break; - case "ScreenRecorderIndicator": - widget.id = "ScreenRecorder"; - break; - case "SidePanelToggle": - widget.id = "ControlCenter"; - break; - } - } - } - - // ----------------- - // 2nd. remove any non existing widget type + // 1. remove any non existing widget type var removedWidget = false; for (var s = 0; s < sections.length; s++) { const sectionName = sections[s]; @@ -718,7 +734,7 @@ Singleton { } // ----------------- - // 3nd. upgrade widget settings + // 2. upgrade user widget settings for (var s = 0; s < sections.length; s++) { const sectionName = sections[s]; for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) { @@ -737,7 +753,7 @@ Singleton { } // ----------------- - // 4th. safety check + // 3. safety check // if a widget was deleted, ensure we still have a control center if (removedWidget) { var gotControlCenter = false; diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 43e276e0..bb0483f3 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -140,7 +140,7 @@ Rectangle { } if (action === "open-calendar") { - PanelService.getPanel("calendarPanel", screen)?.toggle(root); + PanelService.getPanel("clockPanel", screen)?.toggle(root); } else if (action === "widget-settings") { BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); } @@ -154,7 +154,7 @@ Rectangle { hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton onEntered: { - if (!PanelService.getPanel("calendarPanel", screen)?.active) { + if (!PanelService.getPanel("clockPanel", screen)?.active) { TooltipService.show(root, I18n.tr("clock.tooltip"), BarService.getTooltipDirection()); } } @@ -171,7 +171,7 @@ Rectangle { contextMenu.openAtItem(root, pos.x, pos.y); } } else { - PanelService.getPanel("calendarPanel", screen)?.toggle(this); + PanelService.getPanel("clockPanel", screen)?.toggle(this); } } } diff --git a/Modules/Panels/ControlCenter/Cards/AudioCard.qml b/Modules/Cards/AudioCard.qml similarity index 100% rename from Modules/Panels/ControlCenter/Cards/AudioCard.qml rename to Modules/Cards/AudioCard.qml diff --git a/Modules/Cards/CalendarHeaderCard.qml b/Modules/Cards/CalendarHeaderCard.qml new file mode 100644 index 00000000..7dbb5041 --- /dev/null +++ b/Modules/Cards/CalendarHeaderCard.qml @@ -0,0 +1,132 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Location +import qs.Widgets + +// Calendar header with date, month/year, location, and clock +Rectangle { + id: root + Layout.fillWidth: true + Layout.minimumHeight: (60 * Style.uiScaleRatio) + (Style.marginM * 2) + Layout.preferredHeight: (60 * Style.uiScaleRatio) + (Style.marginM * 2) + implicitHeight: (60 * Style.uiScaleRatio) + (Style.marginM * 2) + radius: Style.radiusL + color: Color.mPrimary + + // Internal state + readonly property var now: Time.now + readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null) + + // Expose current month/year for potential synchronization with CalendarMonthCard + readonly property int currentMonth: now.getMonth() + readonly property int currentYear: now.getFullYear() + + ColumnLayout { + id: capsuleColumn + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + anchors.rightMargin: clockLoader.width + (Style.marginXL * 2) + anchors.leftMargin: Style.marginXL + spacing: 0 + + // Combined layout for date, month year, location and time-zone + RowLayout { + Layout.fillWidth: true + height: 60 * Style.uiScaleRatio + clip: true + spacing: Style.marginS + + // Today day number + NText { + Layout.preferredWidth: implicitWidth + elide: Text.ElideNone + clip: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + text: root.now.getDate() + pointSize: Style.fontSizeXXXL * 1.5 + font.weight: Style.fontWeightBold + color: Color.mOnPrimary + } + + // Month, year, location + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.bottomMargin: Style.marginXXS + Layout.topMargin: -Style.marginXXS + spacing: -Style.marginXS + + RowLayout { + spacing: Style.marginS + + NText { + text: I18n.locale.monthName(root.currentMonth, Locale.LongFormat).toUpperCase() + pointSize: Style.fontSizeXL * 1.1 + font.weight: Style.fontWeightBold + color: Color.mOnPrimary + Layout.alignment: Qt.AlignBaseline + elide: Text.ElideRight + } + + NText { + text: `${root.currentYear}` + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + color: Qt.alpha(Color.mOnPrimary, 0.7) + Layout.alignment: Qt.AlignBaseline + } + } + + RowLayout { + spacing: 0 + + NText { + text: { + if (!Settings.data.location.weatherEnabled) + return ""; + if (!root.weatherReady) + return I18n.tr("calendar.weather.loading"); + const chunks = Settings.data.location.name.split(","); + return chunks[0]; + } + pointSize: Style.fontSizeM + font.weight: Style.fontWeightMedium + color: Color.mOnPrimary + Layout.maximumWidth: 150 + elide: Text.ElideRight + } + + NText { + text: root.weatherReady ? ` (${LocationService.data.weather.timezone_abbreviation})` : "" + pointSize: Style.fontSizeXS + font.weight: Style.fontWeightMedium + color: Qt.alpha(Color.mOnPrimary, 0.7) + } + } + } + + // Spacer + Item { + Layout.fillWidth: true + } + } + } + + // Analog/Digital clock + NClock { + id: clockLoader + anchors.right: parent.right + anchors.rightMargin: Style.marginXL + anchors.verticalCenter: parent.verticalCenter + clockStyle: Settings.data.location.analogClockInCalendar ? "analog" : "digital" + progressColor: Color.mOnPrimary + Layout.alignment: Qt.AlignVCenter + now: root.now + } +} diff --git a/Modules/Cards/CalendarMonthCard.qml b/Modules/Cards/CalendarMonthCard.qml new file mode 100644 index 00000000..d32f1b28 --- /dev/null +++ b/Modules/Cards/CalendarMonthCard.qml @@ -0,0 +1,396 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Location +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +// Calendar month grid with navigation +NBox { + id: root + Layout.fillWidth: true + implicitHeight: calendarContent.implicitHeight + Style.marginM * 2 + + // Internal state - independent from header + readonly property var now: Time.now + property int calendarMonth: now.getMonth() + property int calendarYear: now.getFullYear() + readonly property int firstDayOfWeek: Settings.data.location.firstDayOfWeek === -1 ? I18n.locale.firstDayOfWeek : Settings.data.location.firstDayOfWeek + + // Helper function to calculate ISO week number + function getISOWeekNumber(date) { + const target = new Date(date.valueOf()); + const dayNr = (date.getDay() + 6) % 7; + target.setDate(target.getDate() - dayNr + 3); + const firstThursday = new Date(target.getFullYear(), 0, 4); + const diff = target - firstThursday; + const oneWeek = 1000 * 60 * 60 * 24 * 7; + const weekNumber = 1 + Math.round(diff / oneWeek); + return weekNumber; + } + + // Helper function to check if an event is all-day + function isAllDayEvent(event) { + const duration = event.end - event.start; + const startDate = new Date(event.start * 1000); + const isAtMidnight = startDate.getHours() === 0 && startDate.getMinutes() === 0; + return duration === 86400 && isAtMidnight; + } + + ColumnLayout { + id: calendarContent + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + // Navigation row + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NDivider { + Layout.fillWidth: true + } + + NIconButton { + icon: "chevron-left" + onClicked: { + let newDate = new Date(root.calendarYear, root.calendarMonth - 1, 1); + root.calendarYear = newDate.getFullYear(); + root.calendarMonth = newDate.getMonth(); + const now = new Date(); + const monthStart = new Date(root.calendarYear, root.calendarMonth, 1); + const monthEnd = new Date(root.calendarYear, root.calendarMonth + 1, 0); + const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000))); + const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000))); + CalendarService.loadEvents(daysAhead + 30, daysBehind + 30); + } + } + + NIconButton { + icon: "calendar" + onClicked: { + root.calendarMonth = root.now.getMonth(); + root.calendarYear = root.now.getFullYear(); + CalendarService.loadEvents(); + } + } + + NIconButton { + icon: "chevron-right" + onClicked: { + let newDate = new Date(root.calendarYear, root.calendarMonth + 1, 1); + root.calendarYear = newDate.getFullYear(); + root.calendarMonth = newDate.getMonth(); + const now = new Date(); + const monthStart = new Date(root.calendarYear, root.calendarMonth, 1); + const monthEnd = new Date(root.calendarYear, root.calendarMonth + 1, 0); + const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000))); + const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000))); + CalendarService.loadEvents(daysAhead + 30, daysBehind + 30); + } + } + } + + // Day names header + RowLayout { + Layout.fillWidth: true + spacing: 0 + + Item { + visible: Settings.data.location.showWeekNumberInCalendar + Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0 + } + + GridLayout { + Layout.fillWidth: true + columns: 7 + rows: 1 + columnSpacing: 0 + rowSpacing: 0 + + Repeater { + model: 7 + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.fontSizeS * 2 + + NText { + anchors.centerIn: parent + text: { + let dayIndex = (root.firstDayOfWeek + index) % 7; + const dayName = I18n.locale.dayName(dayIndex, Locale.ShortFormat); + return dayName.substring(0, 2).toUpperCase(); + } + color: Color.mPrimary + pointSize: Style.fontSizeS + font.weight: Style.fontWeightBold + horizontalAlignment: Text.AlignHCenter + } + } + } + } + } + + // Calendar grid with week numbers + RowLayout { + Layout.fillWidth: true + spacing: 0 + + // Helper functions + function hasEventsOnDate(year, month, day) { + 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; + return CalendarService.events.some(event => { + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd); + }); + } + + function getEventsForDate(year, month, day) { + 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; + return CalendarService.events.filter(event => { + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd); + }); + } + + function isMultiDayEvent(event) { + if (root.isAllDayEvent(event)) { + return false; + } + const startDate = new Date(event.start * 1000); + const endDate = new Date(event.end * 1000); + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); + return startDateOnly.getTime() !== endDateOnly.getTime(); + } + + function getEventColor(event, isToday) { + if (isMultiDayEvent(event)) { + return isToday ? Color.mOnSecondary : Color.mTertiary; + } else if (root.isAllDayEvent(event)) { + return isToday ? Color.mOnSecondary : Color.mSecondary; + } else { + return isToday ? Color.mOnSecondary : Color.mPrimary; + } + } + + // Week numbers column + ColumnLayout { + visible: Settings.data.location.showWeekNumberInCalendar + Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0 + Layout.alignment: Qt.AlignTop + spacing: Style.marginXXS + + property var weekNumbers: { + if (!grid.daysModel || grid.daysModel.length === 0) + return []; + const weeks = []; + const numWeeks = Math.ceil(grid.daysModel.length / 7); + for (var i = 0; i < numWeeks; i++) { + const dayIndex = i * 7; + if (dayIndex < grid.daysModel.length) { + const weekDay = grid.daysModel[dayIndex]; + const date = new Date(weekDay.year, weekDay.month, weekDay.day); + let thursday = new Date(date); + if (root.firstDayOfWeek === 0) { + thursday.setDate(date.getDate() + 4); + } else if (root.firstDayOfWeek === 1) { + thursday.setDate(date.getDate() + 3); + } else { + let daysToThursday = (4 - root.firstDayOfWeek + 7) % 7; + thursday.setDate(date.getDate() + daysToThursday); + } + weeks.push(root.getISOWeekNumber(thursday)); + } + } + return weeks; + } + + Repeater { + model: parent.weekNumbers + Item { + Layout.preferredWidth: Style.baseWidgetSize * 0.7 + Layout.preferredHeight: Style.baseWidgetSize * 0.9 + + NText { + anchors.centerIn: parent + color: Qt.alpha(Color.mPrimary, 0.7) + pointSize: Style.fontSizeXXS + font.weight: Style.fontWeightMedium + text: modelData + } + } + } + } + + // Calendar grid + GridLayout { + id: grid + Layout.fillWidth: true + columns: 7 + columnSpacing: Style.marginXXS + rowSpacing: Style.marginXXS + + property int month: root.calendarMonth + property int year: root.calendarYear + + property var daysModel: { + const firstOfMonth = new Date(year, month, 1); + const lastOfMonth = new Date(year, month + 1, 0); + const daysInMonth = lastOfMonth.getDate(); + const firstDayOfWeek = root.firstDayOfWeek; + const firstOfMonthDayOfWeek = firstOfMonth.getDay(); + let daysBefore = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7; + const lastOfMonthDayOfWeek = lastOfMonth.getDay(); + const daysAfter = (firstDayOfWeek - lastOfMonthDayOfWeek - 1 + 7) % 7; + const days = []; + const today = new Date(); + + // Previous month days + const prevMonth = new Date(year, month, 0); + const prevMonthDays = prevMonth.getDate(); + for (var i = daysBefore - 1; i >= 0; i--) { + const day = prevMonthDays - i; + days.push({ + "day": day, + "month": month - 1, + "year": month === 0 ? year - 1 : year, + "today": false, + "currentMonth": false + }); + } + + // Current month days + for (var day = 1; day <= daysInMonth; day++) { + const date = new Date(year, month, day); + const isToday = date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate(); + days.push({ + "day": day, + "month": month, + "year": year, + "today": isToday, + "currentMonth": true + }); + } + + // Next month days + for (var i = 1; i <= daysAfter; i++) { + days.push({ + "day": i, + "month": month + 1, + "year": month === 11 ? year + 1 : year, + "today": false, + "currentMonth": false + }); + } + + return days; + } + + Repeater { + model: grid.daysModel + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 0.9 + + Rectangle { + width: Style.baseWidgetSize * 0.9 + height: Style.baseWidgetSize * 0.9 + anchors.centerIn: parent + radius: Style.radiusM + color: modelData.today ? Color.mSecondary : Color.transparent + + NText { + anchors.centerIn: parent + text: modelData.day + color: { + if (modelData.today) + return Color.mOnSecondary; + if (modelData.currentMonth) + return Color.mOnSurface; + return Color.mOnSurfaceVariant; + } + opacity: modelData.currentMonth ? 1.0 : 0.4 + pointSize: Style.fontSizeM + font.weight: modelData.today ? Style.fontWeightBold : Style.fontWeightMedium + } + + // Event indicator dots + Row { + visible: Settings.data.location.showCalendarEvents && parent.parent.parent.parent.hasEventsOnDate(modelData.year, modelData.month, modelData.day) + spacing: 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.marginXS + + Repeater { + model: parent.parent.parent.parent.parent.getEventsForDate(modelData.year, modelData.month, modelData.day) + + Rectangle { + width: 4 + height: width + radius: width / 2 + color: parent.parent.parent.parent.parent.getEventColor(modelData, modelData.today) + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + enabled: Settings.data.location.showCalendarEvents + + onEntered: { + const events = parent.parent.parent.parent.getEventsForDate(modelData.year, modelData.month, modelData.day); + if (events.length > 0) { + const summaries = events.map(event => { + if (root.isAllDayEvent(event)) { + return event.summary; + } else { + const timeFormat = Settings.data.location.use12hourFormat ? "hh:mm AP" : "HH:mm"; + const start = new Date(event.start * 1000); + const startFormatted = I18n.locale.toString(start, timeFormat); + const end = new Date(event.end * 1000); + const endFormatted = I18n.locale.toString(end, timeFormat); + return `${startFormatted}-${endFormatted} ${event.summary}`; + } + }).join('\n'); + TooltipService.show(parent, summaries, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed); + } + } + + onClicked: { + const dateWithSlashes = `${(modelData.month + 1).toString().padStart(2, '0')}/${modelData.day.toString().padStart(2, '0')}/${modelData.year.toString().substring(2)}`; + if (ProgramCheckerService.gnomeCalendarAvailable) { + Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes]); + } + } + + onExited: { + TooltipService.hide(); + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + } + } + } +} diff --git a/Modules/Panels/ControlCenter/Cards/MediaCard.qml b/Modules/Cards/MediaCard.qml similarity index 100% rename from Modules/Panels/ControlCenter/Cards/MediaCard.qml rename to Modules/Cards/MediaCard.qml diff --git a/Modules/Panels/ControlCenter/Cards/ProfileCard.qml b/Modules/Cards/ProfileCard.qml similarity index 98% rename from Modules/Panels/ControlCenter/Cards/ProfileCard.qml rename to Modules/Cards/ProfileCard.qml index 62ca83e1..7a483347 100644 --- a/Modules/Panels/ControlCenter/Cards/ProfileCard.qml +++ b/Modules/Cards/ProfileCard.qml @@ -5,7 +5,6 @@ import Quickshell import Quickshell.Io import Quickshell.Widgets import qs.Commons -import qs.Modules.Panels.ControlCenter.Cards import qs.Modules.Panels.Settings import qs.Services.System import qs.Services.UI diff --git a/Modules/Panels/ControlCenter/Cards/ShortcutsCard.qml b/Modules/Cards/ShortcutsCard.qml similarity index 98% rename from Modules/Panels/ControlCenter/Cards/ShortcutsCard.qml rename to Modules/Cards/ShortcutsCard.qml index 37f80164..104569c3 100644 --- a/Modules/Panels/ControlCenter/Cards/ShortcutsCard.qml +++ b/Modules/Cards/ShortcutsCard.qml @@ -4,7 +4,6 @@ import QtQuick.Layouts import Quickshell import qs.Commons import qs.Modules.Panels.ControlCenter -import qs.Modules.Panels.ControlCenter.Cards import qs.Widgets RowLayout { diff --git a/Modules/Panels/ControlCenter/Cards/SystemMonitorCard.qml b/Modules/Cards/SystemMonitorCard.qml similarity index 100% rename from Modules/Panels/ControlCenter/Cards/SystemMonitorCard.qml rename to Modules/Cards/SystemMonitorCard.qml diff --git a/Modules/Panels/Calendar/TimerCard.qml b/Modules/Cards/TimerCard.qml similarity index 100% rename from Modules/Panels/Calendar/TimerCard.qml rename to Modules/Cards/TimerCard.qml diff --git a/Modules/Panels/ControlCenter/Cards/WeatherCard.qml b/Modules/Cards/WeatherCard.qml similarity index 100% rename from Modules/Panels/ControlCenter/Cards/WeatherCard.qml rename to Modules/Cards/WeatherCard.qml diff --git a/Modules/MainScreen/Backgrounds/AllBackgrounds.qml b/Modules/MainScreen/Backgrounds/AllBackgrounds.qml index c43aadef..58dfcdf9 100644 --- a/Modules/MainScreen/Backgrounds/AllBackgrounds.qml +++ b/Modules/MainScreen/Backgrounds/AllBackgrounds.qml @@ -90,9 +90,9 @@ Item { backgroundColor: panelBackgroundColor } - // Calendar + // Clock PanelBackground { - panel: root.windowRoot.calendarPanelPlaceholder + panel: root.windowRoot.clockPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: panelBackgroundColor } diff --git a/Modules/MainScreen/MainScreen.qml b/Modules/MainScreen/MainScreen.qml index 26fdbd47..25cebebd 100644 --- a/Modules/MainScreen/MainScreen.qml +++ b/Modules/MainScreen/MainScreen.qml @@ -14,8 +14,8 @@ import qs.Modules.Panels.Audio import qs.Modules.Panels.Battery import qs.Modules.Panels.Bluetooth import qs.Modules.Panels.Brightness -import qs.Modules.Panels.Calendar import qs.Modules.Panels.Changelog +import qs.Modules.Panels.Clock import qs.Modules.Panels.ControlCenter import qs.Modules.Panels.Launcher import qs.Modules.Panels.NotificationHistory @@ -39,7 +39,7 @@ PanelWindow { readonly property alias batteryPanel: batteryPanel readonly property alias bluetoothPanel: bluetoothPanel readonly property alias brightnessPanel: brightnessPanel - readonly property alias calendarPanel: calendarPanel + readonly property alias clockPanel: clockPanel readonly property alias changelogPanel: changelogPanel readonly property alias controlCenterPanel: controlCenterPanel readonly property alias launcherPanel: launcherPanel @@ -56,7 +56,7 @@ PanelWindow { readonly property var batteryPanelPlaceholder: batteryPanel.panelRegion readonly property var bluetoothPanelPlaceholder: bluetoothPanel.panelRegion readonly property var brightnessPanelPlaceholder: brightnessPanel.panelRegion - readonly property var calendarPanelPlaceholder: calendarPanel.panelRegion + readonly property var clockPanelPlaceholder: clockPanel.panelRegion readonly property var changelogPanelPlaceholder: changelogPanel.panelRegion readonly property var controlCenterPanelPlaceholder: controlCenterPanel.panelRegion readonly property var launcherPanelPlaceholder: launcherPanel.panelRegion @@ -240,9 +240,9 @@ PanelWindow { z: 50 } - CalendarPanel { - id: calendarPanel - objectName: "calendarPanel-" + (root.screen?.name || "unknown") + ClockPanel { + id: clockPanel + objectName: "clockPanel-" + (root.screen?.name || "unknown") screen: root.screen z: 50 } diff --git a/Modules/Panels/Calendar/CalendarPanel.qml b/Modules/Panels/Calendar/CalendarPanel.qml deleted file mode 100644 index 2ca8d5ae..00000000 --- a/Modules/Panels/Calendar/CalendarPanel.qml +++ /dev/null @@ -1,636 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Wayland -import "." -import qs.Commons -import qs.Modules.MainScreen -import qs.Modules.Panels.ControlCenter.Cards -import qs.Services.Location -import qs.Services.System -import qs.Services.UI -import qs.Widgets - -SmartPanel { - id: root - readonly property var now: Time.now - - // Calculate width based on settings - preferredWidth: Math.round((Settings.data.location.showWeekNumberInCalendar ? 460 : 440) * Style.uiScaleRatio) - - // Use a reasonable fixed height that accommodates most layouts - preferredHeight: Math.round(700 * Style.uiScaleRatio) - - // Helper function to calculate ISO week number - function getISOWeekNumber(date) { - const target = new Date(date.valueOf()); - const dayNr = (date.getDay() + 6) % 7; - target.setDate(target.getDate() - dayNr + 3); - const firstThursday = new Date(target.getFullYear(), 0, 4); - const diff = target - firstThursday; - const oneWeek = 1000 * 60 * 60 * 24 * 7; - const weekNumber = 1 + Math.round(diff / oneWeek); - return weekNumber; - } - - // Helper function to check if an event is all-day - function isAllDayEvent(event) { - const duration = event.end - event.start; - const startDate = new Date(event.start * 1000); - const isAtMidnight = startDate.getHours() === 0 && startDate.getMinutes() === 0; - return duration === 86400 && isAtMidnight; - } - - // Shared calendar state (month/year) accessible by all components - property int calendarMonth: now.getMonth() - property int calendarYear: now.getFullYear() - - panelContent: Item { - anchors.fill: parent - - // Dynamic height based on actual content height - property real contentPreferredHeight: content.implicitHeight + Style.marginL * 2 - - ColumnLayout { - id: content - x: Style.marginL - y: Style.marginL - width: parent.width - (Style.marginL * 2) - spacing: Style.marginL - - readonly property int firstDayOfWeek: Settings.data.location.firstDayOfWeek === -1 ? I18n.locale.firstDayOfWeek : Settings.data.location.firstDayOfWeek - property bool isCurrentMonth: true - readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null) - - function checkIsCurrentMonth() { - return (now.getMonth() === root.calendarMonth) && (now.getFullYear() === root.calendarYear); - } - - Component.onCompleted: { - isCurrentMonth = checkIsCurrentMonth(); - } - - Connections { - target: Time - function onNowChanged() { - content.isCurrentMonth = content.checkIsCurrentMonth(); - } - } - - Connections { - target: I18n - function onLanguageChanged() { - // Force update by toggling month - root.calendarMonth = root.calendarMonth; - } - } - - // All calendar items (Banner, Calendar, Timer, Weather, etc.) - Repeater { - model: Settings.data.calendar.cards - Loader { - active: modelData.enabled && (modelData.id !== "weather-card" || Settings.data.location.weatherEnabled) - visible: active - Layout.fillWidth: true - Layout.topMargin: 0 - Layout.bottomMargin: 0 - sourceComponent: { - switch (modelData.id) { - case "banner-card": - return bannerCard; - case "calendar-card": - return calendarCard; - case "timer-card": - return timerCard; - case "weather-card": - return weatherCard; - default: - return null; - } - } - } - } - } - - Component { - id: bannerCard - Rectangle { - id: banner - Layout.fillWidth: true - Layout.minimumHeight: (60 * Style.uiScaleRatio) + (Style.marginM * 2) - Layout.preferredHeight: (60 * Style.uiScaleRatio) + (Style.marginM * 2) - implicitHeight: (60 * Style.uiScaleRatio) + (Style.marginM * 2) - radius: Style.radiusL - color: Color.mPrimary - - // Access parent properties - readonly property var now: root.now - readonly property bool isCurrentMonth: content.isCurrentMonth - readonly property bool weatherReady: content.weatherReady - - ColumnLayout { - id: capsuleColumn - anchors.top: parent.top - anchors.left: parent.left - anchors.bottom: parent.bottom - anchors.topMargin: Style.marginM - anchors.bottomMargin: Style.marginM - anchors.rightMargin: clockLoader.width + (Style.marginXL * 2) - anchors.leftMargin: Style.marginXL - spacing: 0 - - // Combined layout for date, month year, location and time-zone - RowLayout { - Layout.fillWidth: true - height: 60 * Style.uiScaleRatio - clip: true - spacing: Style.marginS - - // Today day number - NText { - opacity: banner.isCurrentMonth ? 1.0 : 0.0 - Layout.preferredWidth: banner.isCurrentMonth ? implicitWidth : 0 - elide: Text.ElideNone - clip: true - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - text: banner.now.getDate() - pointSize: Style.fontSizeXXXL * 1.5 - font.weight: Style.fontWeightBold - color: Color.mOnPrimary - - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - } - } - - Behavior on Layout.preferredWidth { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.InOutQuad - } - } - } - - // Month, year, location - ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - Layout.bottomMargin: Style.marginXXS - Layout.topMargin: -Style.marginXXS - spacing: -Style.marginXS - - RowLayout { - spacing: Style.marginS - - NText { - text: I18n.locale.monthName(root.calendarMonth, Locale.LongFormat).toUpperCase() - pointSize: Style.fontSizeXL * 1.1 - font.weight: Style.fontWeightBold - color: Color.mOnPrimary - Layout.alignment: Qt.AlignBaseline - elide: Text.ElideRight - } - - NText { - text: `${root.calendarYear}` - pointSize: Style.fontSizeM - font.weight: Style.fontWeightBold - color: Qt.alpha(Color.mOnPrimary, 0.7) - Layout.alignment: Qt.AlignBaseline - } - } - - RowLayout { - spacing: 0 - - NText { - text: { - if (!Settings.data.location.weatherEnabled) - return ""; - if (!banner.weatherReady) - return I18n.tr("calendar.weather.loading"); - const chunks = Settings.data.location.name.split(","); - return chunks[0]; - } - pointSize: Style.fontSizeM - font.weight: Style.fontWeightMedium - color: Color.mOnPrimary - Layout.maximumWidth: 150 - elide: Text.ElideRight - } - - NText { - text: banner.weatherReady ? ` (${LocationService.data.weather.timezone_abbreviation})` : "" - pointSize: Style.fontSizeXS - font.weight: Style.fontWeightMedium - color: Qt.alpha(Color.mOnPrimary, 0.7) - } - } - } - - // Spacer - Item { - Layout.fillWidth: true - } - } - } - - // Analog clock - NClock { - id: clockLoader - anchors.right: parent.right - anchors.rightMargin: Style.marginXL - anchors.verticalCenter: parent.verticalCenter - clockStyle: Settings.data.location.analogClockInCalendar ? "analog" : "digital" - progressColor: Color.mOnPrimary - Layout.alignment: Qt.AlignVCenter - now: parent.now - } - } - } - - Component { - id: calendarCard - NBox { - Layout.fillWidth: true - implicitHeight: calendarContent.implicitHeight + Style.marginM * 2 - - ColumnLayout { - id: calendarContent - anchors.fill: parent - anchors.margins: Style.marginM - spacing: Style.marginS - - // Navigation row - RowLayout { - Layout.fillWidth: true - spacing: Style.marginS - - NDivider { - Layout.fillWidth: true - } - - NIconButton { - icon: "chevron-left" - onClicked: { - let newDate = new Date(root.calendarYear, root.calendarMonth - 1, 1); - root.calendarYear = newDate.getFullYear(); - root.calendarMonth = newDate.getMonth(); - content.isCurrentMonth = content.checkIsCurrentMonth(); - const now = new Date(); - const monthStart = new Date(root.calendarYear, root.calendarMonth, 1); - const monthEnd = new Date(root.calendarYear, root.calendarMonth + 1, 0); - const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000))); - const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000))); - CalendarService.loadEvents(daysAhead + 30, daysBehind + 30); - } - } - - NIconButton { - icon: "calendar" - onClicked: { - root.calendarMonth = now.getMonth(); - root.calendarYear = now.getFullYear(); - content.isCurrentMonth = true; - CalendarService.loadEvents(); - } - } - - NIconButton { - icon: "chevron-right" - onClicked: { - let newDate = new Date(root.calendarYear, root.calendarMonth + 1, 1); - root.calendarYear = newDate.getFullYear(); - root.calendarMonth = newDate.getMonth(); - content.isCurrentMonth = content.checkIsCurrentMonth(); - const now = new Date(); - const monthStart = new Date(root.calendarYear, root.calendarMonth, 1); - const monthEnd = new Date(root.calendarYear, root.calendarMonth + 1, 0); - const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000))); - const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000))); - CalendarService.loadEvents(daysAhead + 30, daysBehind + 30); - } - } - } - - // Day names header - RowLayout { - Layout.fillWidth: true - spacing: 0 - - Item { - visible: Settings.data.location.showWeekNumberInCalendar - Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0 - } - - GridLayout { - Layout.fillWidth: true - columns: 7 - rows: 1 - columnSpacing: 0 - rowSpacing: 0 - - Repeater { - model: 7 - Item { - Layout.fillWidth: true - Layout.preferredHeight: Style.fontSizeS * 2 - - NText { - anchors.centerIn: parent - text: { - let dayIndex = (content.firstDayOfWeek + index) % 7; - const dayName = I18n.locale.dayName(dayIndex, Locale.ShortFormat); - return dayName.substring(0, 2).toUpperCase(); - } - color: Color.mPrimary - pointSize: Style.fontSizeS - font.weight: Style.fontWeightBold - horizontalAlignment: Text.AlignHCenter - } - } - } - } - } - - // Calendar grid with week numbers - RowLayout { - Layout.fillWidth: true - spacing: 0 - - // Helper functions - function hasEventsOnDate(year, month, day) { - 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; - return CalendarService.events.some(event => { - return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd); - }); - } - - function getEventsForDate(year, month, day) { - 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; - return CalendarService.events.filter(event => { - return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd); - }); - } - - function isMultiDayEvent(event) { - if (root.isAllDayEvent(event)) { - return false; - } - const startDate = new Date(event.start * 1000); - const endDate = new Date(event.end * 1000); - const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); - const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); - return startDateOnly.getTime() !== endDateOnly.getTime(); - } - - function getEventColor(event, isToday) { - if (isMultiDayEvent(event)) { - return isToday ? Color.mOnSecondary : Color.mTertiary; - } else if (root.isAllDayEvent(event)) { - return isToday ? Color.mOnSecondary : Color.mSecondary; - } else { - return isToday ? Color.mOnSecondary : Color.mPrimary; - } - } - - // Week numbers column - ColumnLayout { - visible: Settings.data.location.showWeekNumberInCalendar - Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0 - Layout.alignment: Qt.AlignTop - spacing: Style.marginXXS - - property var weekNumbers: { - if (!grid.daysModel || grid.daysModel.length === 0) - return []; - const weeks = []; - const numWeeks = Math.ceil(grid.daysModel.length / 7); - for (var i = 0; i < numWeeks; i++) { - const dayIndex = i * 7; - if (dayIndex < grid.daysModel.length) { - const weekDay = grid.daysModel[dayIndex]; - const date = new Date(weekDay.year, weekDay.month, weekDay.day); - let thursday = new Date(date); - if (content.firstDayOfWeek === 0) { - thursday.setDate(date.getDate() + 4); - } else if (content.firstDayOfWeek === 1) { - thursday.setDate(date.getDate() + 3); - } else { - let daysToThursday = (4 - content.firstDayOfWeek + 7) % 7; - thursday.setDate(date.getDate() + daysToThursday); - } - weeks.push(root.getISOWeekNumber(thursday)); - } - } - return weeks; - } - - Repeater { - model: parent.weekNumbers - Item { - Layout.preferredWidth: Style.baseWidgetSize * 0.7 - Layout.preferredHeight: Style.baseWidgetSize * 0.9 - - NText { - anchors.centerIn: parent - color: Qt.alpha(Color.mPrimary, 0.7) - pointSize: Style.fontSizeXXS - font.weight: Style.fontWeightMedium - text: modelData - } - } - } - } - - // Calendar grid - GridLayout { - id: grid - Layout.fillWidth: true - columns: 7 - columnSpacing: Style.marginXXS - rowSpacing: Style.marginXXS - - property int month: root.calendarMonth - property int year: root.calendarYear - - property var daysModel: { - const firstOfMonth = new Date(year, month, 1); - const lastOfMonth = new Date(year, month + 1, 0); - const daysInMonth = lastOfMonth.getDate(); - const firstDayOfWeek = content.firstDayOfWeek; - const firstOfMonthDayOfWeek = firstOfMonth.getDay(); - let daysBefore = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7; - const lastOfMonthDayOfWeek = lastOfMonth.getDay(); - const daysAfter = (firstDayOfWeek - lastOfMonthDayOfWeek - 1 + 7) % 7; - const days = []; - const today = new Date(); - - // Previous month days - const prevMonth = new Date(year, month, 0); - const prevMonthDays = prevMonth.getDate(); - for (var i = daysBefore - 1; i >= 0; i--) { - const day = prevMonthDays - i; - days.push({ - "day": day, - "month": month - 1, - "year": month === 0 ? year - 1 : year, - "today": false, - "currentMonth": false - }); - } - - // Current month days - for (var day = 1; day <= daysInMonth; day++) { - const date = new Date(year, month, day); - const isToday = date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate(); - days.push({ - "day": day, - "month": month, - "year": year, - "today": isToday, - "currentMonth": true - }); - } - - // Next month days - for (var i = 1; i <= daysAfter; i++) { - days.push({ - "day": i, - "month": month + 1, - "year": month === 11 ? year + 1 : year, - "today": false, - "currentMonth": false - }); - } - - return days; - } - - Repeater { - model: grid.daysModel - - Item { - Layout.fillWidth: true - Layout.preferredHeight: Style.baseWidgetSize * 0.9 - - Rectangle { - width: Style.baseWidgetSize * 0.9 - height: Style.baseWidgetSize * 0.9 - anchors.centerIn: parent - radius: Style.radiusM - color: modelData.today ? Color.mSecondary : Color.transparent - - NText { - anchors.centerIn: parent - text: modelData.day - color: { - if (modelData.today) - return Color.mOnSecondary; - if (modelData.currentMonth) - return Color.mOnSurface; - return Color.mOnSurfaceVariant; - } - opacity: modelData.currentMonth ? 1.0 : 0.4 - pointSize: Style.fontSizeM - font.weight: modelData.today ? Style.fontWeightBold : Style.fontWeightMedium - } - - // Event indicator dots - Row { - visible: Settings.data.location.showCalendarEvents && parent.parent.parent.parent.hasEventsOnDate(modelData.year, modelData.month, modelData.day) - spacing: 2 - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: Style.marginXS - - Repeater { - model: parent.parent.parent.parent.parent.getEventsForDate(modelData.year, modelData.month, modelData.day) - - Rectangle { - width: 4 - height: width - radius: width / 2 - color: parent.parent.parent.parent.parent.getEventColor(modelData, modelData.today) - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - enabled: Settings.data.location.showCalendarEvents - - onEntered: { - const events = parent.parent.parent.parent.getEventsForDate(modelData.year, modelData.month, modelData.day); - if (events.length > 0) { - const summaries = events.map(event => { - if (root.isAllDayEvent(event)) { - return event.summary; - } else { - const timeFormat = Settings.data.location.use12hourFormat ? "hh:mm AP" : "HH:mm"; - const start = new Date(event.start * 1000); - const startFormatted = I18n.locale.toString(start, timeFormat); - const end = new Date(event.end * 1000); - const endFormatted = I18n.locale.toString(end, timeFormat); - return `${startFormatted}-${endFormatted} ${event.summary}`; - } - }).join('\n'); - TooltipService.show(parent, summaries, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed); - } - } - - onClicked: { - const dateWithSlashes = `${(modelData.month + 1).toString().padStart(2, '0')}/${modelData.day.toString().padStart(2, '0')}/${modelData.year.toString().substring(2)}`; - if (ProgramCheckerService.gnomeCalendarAvailable) { - Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes]); - root.close(); - } - } - - onExited: { - TooltipService.hide(); - } - } - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - } - } - } - } - } - } - } - } - - Component { - id: timerCard - TimerCard { - Layout.fillWidth: true - } - } - - Component { - id: weatherCard - WeatherCard { - Layout.fillWidth: true - forecastDays: 5 - showLocation: false - } - } - } -} diff --git a/Modules/Panels/Clock/ClockPanel.qml b/Modules/Panels/Clock/ClockPanel.qml new file mode 100644 index 00000000..16ae804e --- /dev/null +++ b/Modules/Panels/Clock/ClockPanel.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Modules.Cards +import qs.Modules.MainScreen +import qs.Services.Location +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + // Calculate width based on settings + preferredWidth: Math.round((Settings.data.location.showWeekNumberInCalendar ? 460 : 440) * Style.uiScaleRatio) + + panelContent: Item { + anchors.fill: parent + + // SmartPanel uses this to calculate panel height dynamically + readonly property real contentPreferredHeight: content.implicitHeight + (Style.marginL * 2) + + ColumnLayout { + id: content + x: Style.marginL + y: Style.marginL + width: parent.width - (Style.marginL * 2) + spacing: Style.marginL + + // All clock panel cards + Repeater { + model: Settings.data.calendar.cards + Loader { + active: modelData.enabled && (modelData.id !== "weather-card" || Settings.data.location.weatherEnabled) + visible: active + Layout.fillWidth: true + sourceComponent: { + switch (modelData.id) { + case "calendar-header-card": + return calendarHeaderCard; + case "calendar-month-card": + return calendarMonthCard; + case "timer-card": + return timerCard; + case "weather-card": + return weatherCard; + default: + return null; + } + } + } + } + } + } + + Component { + id: calendarHeaderCard + CalendarHeaderCard { + Layout.fillWidth: true + } + } + + Component { + id: calendarMonthCard + CalendarMonthCard { + Layout.fillWidth: true + } + } + + Component { + id: timerCard + TimerCard { + Layout.fillWidth: true + } + } + + Component { + id: weatherCard + WeatherCard { + Layout.fillWidth: true + forecastDays: 5 + showLocation: false + } + } +} diff --git a/Modules/Panels/ControlCenter/ControlCenterPanel.qml b/Modules/Panels/ControlCenter/ControlCenterPanel.qml index dab8e821..c65f0196 100644 --- a/Modules/Panels/ControlCenter/ControlCenterPanel.qml +++ b/Modules/Panels/ControlCenter/ControlCenterPanel.qml @@ -3,8 +3,8 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons +import qs.Modules.Cards import qs.Modules.MainScreen -import qs.Modules.Panels.ControlCenter.Cards import qs.Services.Media import qs.Services.UI import qs.Widgets diff --git a/Modules/Panels/Settings/Tabs/LocationTab.qml b/Modules/Panels/Settings/Tabs/LocationTab.qml index da93d66a..38f2fc1c 100644 --- a/Modules/Panels/Settings/Tabs/LocationTab.qml +++ b/Modules/Panels/Settings/Tabs/LocationTab.qml @@ -12,14 +12,14 @@ ColumnLayout { property list cardsModel: [] property list cardsDefault: [ { - "id": "banner-card", - "text": I18n.tr("settings.location.calendar.banner.label"), + "id": "calendar-header-card", + "text": I18n.tr("settings.location.calendar.header.label"), "enabled": true, "required": false }, { - "id": "calendar-card", - "text": I18n.tr("settings.location.calendar.calendar.label"), + "id": "calendar-month-card", + "text": I18n.tr("settings.location.calendar.month.label"), "enabled": true, "required": true }, diff --git a/Services/Control/IPCService.qml b/Services/Control/IPCService.qml index 5c676ed8..850fc8d7 100644 --- a/Services/Control/IPCService.qml +++ b/Services/Control/IPCService.qml @@ -46,8 +46,8 @@ Item { target: "calendar" function toggle() { root.withTargetScreen(screen => { - var calendarPanel = PanelService.getPanel("calendarPanel", screen); - calendarPanel?.toggle(null, "Clock"); + var clockPanel = PanelService.getPanel("clockPanel", screen); + clockPanel?.toggle(null, "Clock"); }); } }