From db05496dcaf119de063e08f43f3f91dac0a3a81e Mon Sep 17 00:00:00 2001 From: bbedward Date: Sat, 12 Jul 2025 14:33:50 -0400 Subject: [PATCH] Settings globally, refactor some menus --- Common/Prefs.qml | 32 +- Services/UserInfoService.qml | 148 ++++++ Services/qmldir | 3 +- .../CenterCommandCenter.qml | 2 - Widgets/CenterCommandCenter/EventsWidget.qml | 5 +- Widgets/CenterCommandCenter/WeatherWidget.qml | 5 +- Widgets/ControlCenter/ControlCenterPopup.qml | 432 +++++++++++++++++- Widgets/ControlCenter/DisplayTab.qml | 41 +- Widgets/PowerConfirmDialog.qml | 3 +- Widgets/SettingsPopup.qml | 314 +++++++++++++ Widgets/SettingsSection.qml | 48 ++ Widgets/SettingsToggle.qml | 112 +++++ Widgets/TopBar/ClockWidget.qml | 2 +- Widgets/TopBar/TopBar.qml | 7 - Widgets/TopBar/WeatherWidget.qml | 3 +- Widgets/qmldir | 5 +- shell.qml | 8 +- 17 files changed, 1100 insertions(+), 70 deletions(-) create mode 100644 Services/UserInfoService.qml create mode 100644 Widgets/SettingsPopup.qml create mode 100644 Widgets/SettingsSection.qml create mode 100644 Widgets/SettingsToggle.qml diff --git a/Common/Prefs.qml b/Common/Prefs.qml index 1b201976..9c6fbd2a 100644 --- a/Common/Prefs.qml +++ b/Common/Prefs.qml @@ -13,6 +13,11 @@ Singleton { property real topBarTransparency: 0.75 property var recentlyUsedApps: [] + // New global preferences + property bool use24HourClock: true + property bool useFahrenheit: false + property bool nightModeEnabled: false + Component.onCompleted: loadSettings() @@ -49,6 +54,9 @@ Singleton { topBarTransparency = settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100.0 : settings.topBarTransparency) : 0.75 recentlyUsedApps = settings.recentlyUsedApps || [] + use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true + useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false + nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false console.log("Loaded settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length) applyStoredTheme() @@ -68,7 +76,10 @@ Singleton { themeIsDynamic, isLightMode, topBarTransparency, - recentlyUsedApps + recentlyUsedApps, + use24HourClock, + useFahrenheit, + nightModeEnabled }, null, 2)) console.log("Saving settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length) } @@ -133,4 +144,23 @@ Singleton { function getRecentApps() { return recentlyUsedApps } + + // New preference setters + function setClockFormat(use24Hour) { + console.log("Prefs setClockFormat called - use24HourClock:", use24Hour) + use24HourClock = use24Hour + saveSettings() + } + + function setTemperatureUnit(fahrenheit) { + console.log("Prefs setTemperatureUnit called - useFahrenheit:", fahrenheit) + useFahrenheit = fahrenheit + saveSettings() + } + + function setNightModeEnabled(enabled) { + console.log("Prefs setNightModeEnabled called - nightModeEnabled:", enabled) + nightModeEnabled = enabled + saveSettings() + } } \ No newline at end of file diff --git a/Services/UserInfoService.qml b/Services/UserInfoService.qml new file mode 100644 index 00000000..cc38bc20 --- /dev/null +++ b/Services/UserInfoService.qml @@ -0,0 +1,148 @@ +import QtQuick +import Quickshell +import Quickshell.Io +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + id: root + + property string username: "" + property string fullName: "" + property string profilePicture: "" + property string uptime: "" + property string hostname: "" + property bool profileAvailable: false + + Component.onCompleted: { + getUserInfo() + getUptime() + + // Update uptime every minute + uptimeTimer.start() + } + + Timer { + id: uptimeTimer + interval: 60000 // 1 minute + running: false + repeat: true + onTriggered: getUptime() + } + + // Get username and full name + Process { + id: userInfoProcess + command: ["bash", "-c", "echo \"$USER|$(getent passwd $USER | cut -d: -f5 | cut -d, -f1)|$(hostname)\""] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const parts = text.trim().split("|") + if (parts.length >= 3) { + root.username = parts[0] || "" + root.fullName = parts[1] || parts[0] || "" + root.hostname = parts[2] || "" + console.log("UserInfoService: User info loaded -", root.username, root.fullName, root.hostname) + + // Try to find profile picture + getProfilePicture() + } + } + } + + onExited: (exitCode) => { + if (exitCode !== 0) { + console.warn("UserInfoService: Failed to get user info") + root.username = "User" + root.fullName = "User" + root.hostname = "System" + } + } + } + + // Get system uptime + Process { + id: uptimeProcess + command: ["bash", "-c", "uptime -p | sed 's/up //'"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + root.uptime = text.trim() || "Unknown" + console.log("UserInfoService: Uptime updated -", root.uptime) + } + } + + onExited: (exitCode) => { + if (exitCode !== 0) { + console.warn("UserInfoService: Failed to get uptime") + root.uptime = "Unknown" + } + } + } + + // Look for profile picture in common locations + Process { + id: profilePictureProcess + command: ["bash", "-c", ` + # Try common profile picture locations + for path in \ + "$HOME/.face" \ + "$HOME/.face.icon" \ + "/var/lib/AccountsService/icons/$USER" \ + "/usr/share/pixmaps/faces/$USER" \ + "/usr/share/pixmaps/faces/$USER.png" \ + "/usr/share/pixmaps/faces/$USER.jpg"; do + if [ -f "$path" ]; then + echo "$path" + exit 0 + fi + done + # Fallback to generic user icon + echo "" + `] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const path = text.trim() + if (path && path.length > 0) { + root.profilePicture = "file://" + path + root.profileAvailable = true + console.log("UserInfoService: Profile picture found at", path) + } else { + root.profilePicture = "" + root.profileAvailable = false + console.log("UserInfoService: No profile picture found, using default avatar") + } + } + } + + onExited: (exitCode) => { + if (exitCode !== 0) { + console.warn("UserInfoService: Failed to find profile picture") + root.profilePicture = "" + root.profileAvailable = false + } + } + } + + function getUserInfo() { + userInfoProcess.running = true + } + + function getUptime() { + uptimeProcess.running = true + } + + function getProfilePicture() { + profilePictureProcess.running = true + } + + function refreshUserInfo() { + getUserInfo() + getUptime() + getProfilePicture() + } +} \ No newline at end of file diff --git a/Services/qmldir b/Services/qmldir index 9d84c080..e1a6ccc8 100644 --- a/Services/qmldir +++ b/Services/qmldir @@ -12,4 +12,5 @@ singleton AppSearchService 1.0 AppSearchService.qml singleton PreferencesService 1.0 PreferencesService.qml singleton LauncherService 1.0 LauncherService.qml singleton NiriWorkspaceService 1.0 NiriWorkspaceService.qml -singleton CalendarService 1.0 CalendarService.qml \ No newline at end of file +singleton CalendarService 1.0 CalendarService.qml +singleton UserInfoService 1.0 UserInfoService.qml \ No newline at end of file diff --git a/Widgets/CenterCommandCenter/CenterCommandCenter.qml b/Widgets/CenterCommandCenter/CenterCommandCenter.qml index 53ede5cf..9b7f0b01 100644 --- a/Widgets/CenterCommandCenter/CenterCommandCenter.qml +++ b/Widgets/CenterCommandCenter/CenterCommandCenter.qml @@ -14,7 +14,6 @@ PanelWindow { property var theme: Theme property bool hasActiveMedia: root.hasActiveMedia property var weather: root.weather - property bool useFahrenheit: false property bool showMediaPlayer: hasActiveMedia || hideMediaTimer.running @@ -202,7 +201,6 @@ PanelWindow { height: weather ? 140 : 80 theme: centerCommandCenter.theme weather: centerCommandCenter.weather - useFahrenheit: centerCommandCenter.useFahrenheit } } diff --git a/Widgets/CenterCommandCenter/EventsWidget.qml b/Widgets/CenterCommandCenter/EventsWidget.qml index 6b106fea..8c6426a8 100644 --- a/Widgets/CenterCommandCenter/EventsWidget.qml +++ b/Widgets/CenterCommandCenter/EventsWidget.qml @@ -237,10 +237,11 @@ Rectangle { if (modelData.allDay) { return "All day" } else { - let startTime = Qt.formatTime(modelData.start, "h:mm AP") + let timeFormat = Prefs.use24HourClock ? "H:mm" : "h:mm AP" + let startTime = Qt.formatTime(modelData.start, timeFormat) if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime()) { - return startTime + " – " + Qt.formatTime(modelData.end, "h:mm AP") + return startTime + " – " + Qt.formatTime(modelData.end, timeFormat) } return startTime } diff --git a/Widgets/CenterCommandCenter/WeatherWidget.qml b/Widgets/CenterCommandCenter/WeatherWidget.qml index 53ac27f6..e6e6a00d 100644 --- a/Widgets/CenterCommandCenter/WeatherWidget.qml +++ b/Widgets/CenterCommandCenter/WeatherWidget.qml @@ -9,7 +9,6 @@ Rectangle { property var theme: Theme property var weather - property bool useFahrenheit: false width: parent.width height: parent.height @@ -80,7 +79,7 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter Text { - text: weather ? ((useFahrenheit ? weather.tempF : weather.temp) + "°" + (useFahrenheit ? "F" : "C")) : "" + text: weather ? ((Prefs.useFahrenheit ? weather.tempF : weather.temp) + "°" + (Prefs.useFahrenheit ? "F" : "C")) : "" font.pixelSize: theme.fontSizeXLarge color: theme.surfaceText font.weight: Font.Light @@ -89,7 +88,7 @@ Rectangle { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: if (weather) useFahrenheit = !useFahrenheit + onClicked: if (weather) Prefs.setTemperatureUnit(!Prefs.useFahrenheit) enabled: weather !== null } } diff --git a/Widgets/ControlCenter/ControlCenterPopup.qml b/Widgets/ControlCenter/ControlCenterPopup.qml index 23fc6bd0..7e01670e 100644 --- a/Widgets/ControlCenter/ControlCenterPopup.qml +++ b/Widgets/ControlCenter/ControlCenterPopup.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import QtQuick.Effects import Quickshell import Quickshell.Widgets import Quickshell.Wayland @@ -29,7 +30,7 @@ PanelWindow { } property int currentTab: 0 // 0: Network, 1: Audio, 2: Bluetooth, 3: Display - property bool nightModeEnabled: false + property bool powerOptionsExpanded: false Rectangle { width: Math.min(600, parent.width - Theme.spacingL * 2) @@ -63,23 +64,403 @@ PanelWindow { anchors.margins: Theme.spacingL spacing: Theme.spacingM - // Header with tabs + // Elegant User Header Column { width: parent.width - spacing: Theme.spacingM + spacing: Theme.spacingL - Row { + // User Info Section - Jony Ive inspired + Rectangle { width: parent.width - height: 32 + height: 90 + radius: Theme.cornerRadiusLarge + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 - Text { - text: "Control Center" - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceText - font.weight: Font.Medium + Row { + anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + spacing: Theme.spacingL + + // Profile Picture + Rectangle { + width: 54 + height: 54 + radius: 27 + color: Theme.primary + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: 2 + + Image { + anchors.fill: parent + anchors.margins: 2 + source: UserInfoService.profilePicture + fillMode: Image.PreserveAspectCrop + visible: UserInfoService.profileAvailable + smooth: true + + layer.enabled: true + layer.effect: MultiEffect { + maskEnabled: true + maskSource: Rectangle { + width: parent.width + height: parent.height + radius: width / 2 + visible: false + } + } + } + + // Fallback icon when no profile picture + Text { + anchors.centerIn: parent + text: "person" + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize + 4 + color: Theme.onPrimary + visible: !UserInfoService.profileAvailable + } + } + + // User Info Text + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Text { + text: UserInfoService.fullName || UserInfoService.username || "User" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + Text { + text: "Uptime: " + (UserInfoService.uptime || "Unknown") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + font.weight: Font.Normal + } + } } + // Action Buttons - Power and Settings + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: Theme.spacingL + spacing: Theme.spacingS + + // Power Button + Rectangle { + width: 40 + height: 40 + radius: 20 + color: powerButton.containsMouse || controlCenterPopup.powerOptionsExpanded ? + Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : + Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + + Rectangle { + anchors.centerIn: parent + width: parent.width + height: parent.height + radius: parent.radius + color: "transparent" + clip: true + + Text { + anchors.centerIn: parent + text: controlCenterPopup.powerOptionsExpanded ? "expand_less" : "power_settings_new" + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize - 2 + color: powerButton.containsMouse || controlCenterPopup.powerOptionsExpanded ? Theme.error : Theme.surfaceText + + Behavior on text { + // Smooth icon transition + SequentialAnimation { + NumberAnimation { + target: parent + property: "opacity" + to: 0.0 + duration: Theme.shortDuration / 2 + easing.type: Theme.standardEasing + } + PropertyAction { target: parent; property: "text" } + NumberAnimation { + target: parent + property: "opacity" + to: 1.0 + duration: Theme.shortDuration / 2 + easing.type: Theme.standardEasing + } + } + } + } + } + + MouseArea { + id: powerButton + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + controlCenterPopup.powerOptionsExpanded = !controlCenterPopup.powerOptionsExpanded + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + // Settings Button + Rectangle { + width: 40 + height: 40 + radius: 20 + color: settingsButton.containsMouse ? + Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : + Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + + Text { + anchors.centerIn: parent + text: "settings" + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize - 2 + color: Theme.surfaceText + } + + MouseArea { + id: settingsButton + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.controlCenterVisible = false + root.settingsVisible = true + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + + // Animated Collapsible Power Options (moved here for better integration) + Rectangle { + width: parent.width + height: controlCenterPopup.powerOptionsExpanded ? 60 : 0 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: controlCenterPopup.powerOptionsExpanded ? 1 : 0 + opacity: controlCenterPopup.powerOptionsExpanded ? 1.0 : 0.0 + clip: true + + Behavior on height { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Row { + anchors.centerIn: parent + spacing: Theme.spacingL + opacity: controlCenterPopup.powerOptionsExpanded ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + // Logout + Rectangle { + width: 100 + height: 34 + radius: Theme.cornerRadius + color: logoutButton.containsMouse ? + Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) : + Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Text { + text: "logout" + font.family: Theme.iconFont + font.pixelSize: Theme.fontSizeSmall + color: logoutButton.containsMouse ? Theme.warning : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: "Logout" + font.pixelSize: Theme.fontSizeSmall + color: logoutButton.containsMouse ? Theme.warning : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: logoutButton + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + controlCenterPopup.powerOptionsExpanded = false + root.powerConfirmAction = "logout" + root.powerConfirmTitle = "Logout" + root.powerConfirmMessage = "Are you sure you want to logout?" + root.powerConfirmVisible = true + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + // Reboot + Rectangle { + width: 100 + height: 34 + radius: Theme.cornerRadius + color: rebootButton.containsMouse ? + Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) : + Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Text { + text: "restart_alt" + font.family: Theme.iconFont + font.pixelSize: Theme.fontSizeSmall + color: rebootButton.containsMouse ? Theme.warning : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: "Restart" + font.pixelSize: Theme.fontSizeSmall + color: rebootButton.containsMouse ? Theme.warning : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: rebootButton + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + controlCenterPopup.powerOptionsExpanded = false + root.powerConfirmAction = "reboot" + root.powerConfirmTitle = "Restart" + root.powerConfirmMessage = "Are you sure you want to restart?" + root.powerConfirmVisible = true + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + // Shutdown + Rectangle { + width: 100 + height: 34 + radius: Theme.cornerRadius + color: shutdownButton.containsMouse ? + Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : + Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Text { + text: "power_settings_new" + font.family: Theme.iconFont + font.pixelSize: Theme.fontSizeSmall + color: shutdownButton.containsMouse ? Theme.error : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: "Shutdown" + font.pixelSize: Theme.fontSizeSmall + color: shutdownButton.containsMouse ? Theme.error : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: shutdownButton + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + controlCenterPopup.powerOptionsExpanded = false + root.powerConfirmAction = "poweroff" + root.powerConfirmTitle = "Shutdown" + root.powerConfirmMessage = "Are you sure you want to shutdown?" + root.powerConfirmVisible = true + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } } // Tab buttons @@ -166,10 +547,32 @@ PanelWindow { // Tab content area Rectangle { width: parent.width - height: parent.height - 120 + height: { + // More generous height calculation - use most of the available space + let baseHeight = parent.height + + // Subtract only the essential fixed elements + baseHeight -= 90 + Theme.spacingL // User header + spacing + baseHeight -= 40 + Theme.spacingM // Tab buttons + spacing + baseHeight -= Theme.spacingM // Bottom spacing + + // Subtract power options height when expanded + if (controlCenterPopup.powerOptionsExpanded) { + baseHeight -= 60 + Theme.spacingL + } + + return Math.max(300, baseHeight) // Higher minimum height for better content display + } radius: Theme.cornerRadius color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) + Behavior on height { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + // Network Tab NetworkTab { anchors.fill: parent @@ -228,13 +631,6 @@ PanelWindow { anchors.margins: Theme.spacingM visible: controlCenterPopup.currentTab === 3 - // Bind properties from parent - nightModeEnabled: controlCenterPopup.nightModeEnabled - - // Sync night mode state back to parent - onNightModeEnabledChanged: { - controlCenterPopup.nightModeEnabled = nightModeEnabled - } } } } diff --git a/Widgets/ControlCenter/DisplayTab.qml b/Widgets/ControlCenter/DisplayTab.qml index 10285d6c..d2589cae 100644 --- a/Widgets/ControlCenter/DisplayTab.qml +++ b/Widgets/ControlCenter/DisplayTab.qml @@ -11,21 +11,6 @@ ScrollView { id: displayTab clip: true - // These should be bound from parent - property bool nightModeEnabled: false - property real topBarTransparency: Prefs.topBarTransparency // Default transparency value - - Component.onCompleted: { - // Sync with stored transparency value on startup - topBarTransparency = Prefs.topBarTransparency - } - - Connections { - target: Prefs - function onTopBarTransparencyChanged() { - displayTab.topBarTransparency = Prefs.topBarTransparency - } - } Column { width: parent.width @@ -79,28 +64,28 @@ ScrollView { width: (parent.width - Theme.spacingM) / 2 height: 50 radius: Theme.cornerRadius - color: displayTab.nightModeEnabled ? + color: Prefs.nightModeEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) - border.color: displayTab.nightModeEnabled ? Theme.primary : "transparent" - border.width: displayTab.nightModeEnabled ? 1 : 0 + border.color: Prefs.nightModeEnabled ? Theme.primary : "transparent" + border.width: Prefs.nightModeEnabled ? 1 : 0 Column { anchors.centerIn: parent spacing: Theme.spacingXS Text { - text: displayTab.nightModeEnabled ? "nightlight" : "dark_mode" + text: Prefs.nightModeEnabled ? "nightlight" : "dark_mode" font.family: Theme.iconFont font.pixelSize: Theme.iconSize - color: displayTab.nightModeEnabled ? Theme.primary : Theme.surfaceText + color: Prefs.nightModeEnabled ? Theme.primary : Theme.surfaceText anchors.horizontalCenter: parent.horizontalCenter } Text { text: "Night Mode" font.pixelSize: Theme.fontSizeSmall - color: displayTab.nightModeEnabled ? Theme.primary : Theme.surfaceText + color: Prefs.nightModeEnabled ? Theme.primary : Theme.surfaceText font.weight: Font.Medium anchors.horizontalCenter: parent.horizontalCenter } @@ -113,14 +98,14 @@ ScrollView { cursorShape: Qt.PointingHandCursor onClicked: { - if (displayTab.nightModeEnabled) { + if (Prefs.nightModeEnabled) { // Disable night mode - kill any running color temperature processes nightModeDisableProcess.running = true - displayTab.nightModeEnabled = false + Prefs.setNightModeEnabled(false) } else { // Enable night mode using wlsunset or redshift nightModeEnableProcess.running = true - displayTab.nightModeEnabled = true + Prefs.setNightModeEnabled(true) } } } @@ -194,7 +179,7 @@ ScrollView { CustomSlider { width: parent.width - (Theme.spacingM * 2) anchors.horizontalCenter: parent.horizontalCenter - value: Math.round(displayTab.topBarTransparency * 100) + value: Math.round(Prefs.topBarTransparency * 100) minimum: 0 maximum: 100 leftIcon: "opacity" @@ -204,7 +189,7 @@ ScrollView { onSliderValueChanged: (newValue) => { let transparencyValue = newValue / 100.0 - displayTab.topBarTransparency = transparencyValue + // Update live preview } onSliderDragFinished: (finalValue) => { @@ -229,7 +214,7 @@ ScrollView { spacing: Theme.spacingS Text { - text: "Theme" + text: "Theme Color" font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText font.weight: Font.Medium @@ -251,7 +236,7 @@ ScrollView { onExited: (exitCode) => { if (exitCode !== 0) { console.warn("Failed to enable night mode") - displayTab.nightModeEnabled = false + Prefs.setNightModeEnabled(false) } } } diff --git a/Widgets/PowerConfirmDialog.qml b/Widgets/PowerConfirmDialog.qml index 58c133b4..aa4e8b58 100644 --- a/Widgets/PowerConfirmDialog.qml +++ b/Widgets/PowerConfirmDialog.qml @@ -170,8 +170,7 @@ PanelWindow { let command = [] switch(action) { case "logout": - // Try multiple logout commands for different environments - command = ["bash", "-c", "loginctl terminate-user $USER || pkill -KILL -u $USER || gnome-session-quit --force || xfce4-session-logout --logout || i3-msg exit || swaymsg exit || niri msg quit"] + command = ["niri", "msg", "action", "quit", "-s"] break case "suspend": command = ["systemctl", "suspend"] diff --git a/Widgets/SettingsPopup.qml b/Widgets/SettingsPopup.qml new file mode 100644 index 00000000..5713f8ba --- /dev/null +++ b/Widgets/SettingsPopup.qml @@ -0,0 +1,314 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Io +import "../Common" + +PanelWindow { + id: settingsPopup + + property bool settingsVisible: false + + visible: settingsVisible + + implicitWidth: 600 + implicitHeight: 700 + + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + // Darkened background + Rectangle { + anchors.fill: parent + color: "black" + opacity: 0.5 + + MouseArea { + anchors.fill: parent + onClicked: settingsPopup.settingsVisible = false + } + } + + // Main settings panel - spotlight-like centered appearance + Rectangle { + id: mainPanel + width: Math.min(600, parent.width - Theme.spacingXL * 2) + height: Math.min(700, parent.height - Theme.spacingXL * 2) + anchors.centerIn: parent + color: Theme.surfaceContainer + radius: Theme.cornerRadiusLarge + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: 1 + + opacity: settingsPopup.settingsVisible ? 1.0 : 0.0 + scale: settingsPopup.settingsVisible ? 1.0 : 0.95 + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + // Header + Row { + width: parent.width + spacing: Theme.spacingM + + Text { + text: "settings" + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: "Settings" + font.pixelSize: Theme.fontSizeXLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - 200 // Spacer to push close button to the right + height: 1 + } + + // Close button + Rectangle { + width: 32 + height: 32 + radius: Theme.cornerRadius + color: closeButton.containsMouse ? + Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : + "transparent" + + Text { + text: "close" + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize - 4 + color: Theme.surfaceText + anchors.centerIn: parent + } + + MouseArea { + id: closeButton + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: settingsPopup.settingsVisible = false + } + } + } + + // Settings sections + ScrollView { + width: parent.width + height: parent.height - 80 + clip: true + + Column { + width: parent.width + spacing: Theme.spacingL + + // Clock Settings + SettingsSection { + title: "Clock & Time" + iconName: "schedule" + + content: Column { + width: parent.width + spacing: Theme.spacingM + + SettingsToggle { + text: "24-Hour Format" + description: "Use 24-hour time format instead of 12-hour AM/PM" + checked: Prefs.use24HourClock + onToggled: (checked) => Prefs.setClockFormat(checked) + } + } + } + + // Weather Settings + SettingsSection { + title: "Weather" + iconName: "wb_sunny" + + content: Column { + width: parent.width + spacing: Theme.spacingM + + SettingsToggle { + text: "Fahrenheit" + description: "Use Fahrenheit instead of Celsius for temperature" + checked: Prefs.useFahrenheit + onToggled: (checked) => Prefs.setTemperatureUnit(checked) + } + } + } + + // Display Settings + SettingsSection { + title: "Display & Appearance" + iconName: "palette" + + content: Column { + width: parent.width + spacing: Theme.spacingL + + SettingsToggle { + text: "Night Mode" + description: "Apply warm color temperature to reduce eye strain" + checked: Prefs.nightModeEnabled + onToggled: (checked) => { + Prefs.setNightModeEnabled(checked) + if (checked) { + nightModeEnableProcess.running = true + } else { + nightModeDisableProcess.running = true + } + } + } + + SettingsToggle { + text: "Light Mode" + description: "Use light theme instead of dark theme" + checked: Prefs.isLightMode + onToggled: (checked) => { + Prefs.setLightMode(checked) + Theme.isLightMode = checked + } + } + + // Top Bar Transparency + Column { + width: parent.width + spacing: Theme.spacingS + + Text { + text: "Top Bar Transparency" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + CustomSlider { + width: parent.width + value: Math.round(Prefs.topBarTransparency * 100) + minimum: 0 + maximum: 100 + leftIcon: "opacity" + rightIcon: "circle" + unit: "%" + showValue: true + + onSliderDragFinished: (finalValue) => { + let transparencyValue = finalValue / 100.0 + Prefs.setTopBarTransparency(transparencyValue) + } + } + + Text { + text: "Adjust the transparency of the top bar background" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + } + + // Theme Picker + Column { + width: parent.width + spacing: Theme.spacingS + + Text { + text: "Theme Color" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + ThemePicker { + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + } + } + } + + // Add shadow effect + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 8 + shadowBlur: 1.0 + shadowColor: Qt.rgba(0, 0, 0, 0.3) + shadowOpacity: 0.3 + } + } + + // Night mode processes + Process { + id: nightModeEnableProcess + command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"] + running: false + + onExited: (exitCode) => { + if (exitCode !== 0) { + console.warn("Failed to enable night mode") + Prefs.setNightModeEnabled(false) + } + } + } + + Process { + id: nightModeDisableProcess + command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"] + running: false + + onExited: (exitCode) => { + if (exitCode !== 0) { + console.warn("Failed to disable night mode") + } + } + } + + // Keyboard focus and shortcuts + FocusScope { + anchors.fill: parent + focus: settingsPopup.settingsVisible + + Keys.onEscapePressed: settingsPopup.settingsVisible = false + } +} \ No newline at end of file diff --git a/Widgets/SettingsSection.qml b/Widgets/SettingsSection.qml new file mode 100644 index 00000000..f0e74373 --- /dev/null +++ b/Widgets/SettingsSection.qml @@ -0,0 +1,48 @@ +import QtQuick +import "../Common" + +Column { + id: root + + property string title: "" + property string iconName: "" + property alias content: contentLoader.sourceComponent + + width: parent.width + spacing: Theme.spacingM + + // Section header + Row { + width: parent.width + spacing: Theme.spacingS + + Text { + text: iconName + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize - 2 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: title + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + // Divider + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + // Content + Loader { + id: contentLoader + width: parent.width + } +} \ No newline at end of file diff --git a/Widgets/SettingsToggle.qml b/Widgets/SettingsToggle.qml new file mode 100644 index 00000000..683fac63 --- /dev/null +++ b/Widgets/SettingsToggle.qml @@ -0,0 +1,112 @@ +import QtQuick +import "../Common" + +Rectangle { + id: root + + property string text: "" + property string description: "" + property bool checked: false + + signal toggled(bool checked) + + width: parent.width + height: 60 + radius: Theme.cornerRadius + color: toggleArea.containsMouse ? + Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : + Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Row { + anchors.left: parent.left + anchors.right: toggle.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingXS + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Text { + text: root.text + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + Text { + text: root.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: Math.min(implicitWidth, root.width - 120) + visible: root.description.length > 0 + } + } + } + + // Toggle switch + Rectangle { + id: toggle + width: 48 + height: 24 + radius: 12 + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + + color: root.checked ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Rectangle { + id: toggleHandle + width: 20 + height: 20 + radius: 10 + anchors.verticalCenter: parent.verticalCenter + x: root.checked ? parent.width - width - 2 : 2 + color: root.checked ? Theme.onPrimary : Theme.surfaceText + + Behavior on x { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + MouseArea { + id: toggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.checked = !root.checked + root.toggled(root.checked) + } + } +} \ No newline at end of file diff --git a/Widgets/TopBar/ClockWidget.qml b/Widgets/TopBar/ClockWidget.qml index 7c4b2f56..9838cd47 100644 --- a/Widgets/TopBar/ClockWidget.qml +++ b/Widgets/TopBar/ClockWidget.qml @@ -28,7 +28,7 @@ Rectangle { spacing: Theme.spacingS Text { - text: Qt.formatTime(root.currentDate, "h:mm AP") + text: Prefs.use24HourClock ? Qt.formatTime(root.currentDate, "H:mm") : Qt.formatTime(root.currentDate, "h:mm AP") font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText font.weight: Font.Medium diff --git a/Widgets/TopBar/TopBar.qml b/Widgets/TopBar/TopBar.qml index 33141f31..f7e97577 100644 --- a/Widgets/TopBar/TopBar.qml +++ b/Widgets/TopBar/TopBar.qml @@ -36,7 +36,6 @@ PanelWindow { property string weatherCode: "" property int weatherTemp: 0 property int weatherTempF: 0 - property bool useFahrenheit: false property string osLogo: "" property string networkStatus: "disconnected" property string wifiSignalStrength: "good" @@ -204,7 +203,6 @@ PanelWindow { weatherCode: topBar.weatherCode weatherTemp: topBar.weatherTemp weatherTempF: topBar.weatherTempF - useFahrenheit: topBar.useFahrenheit onClicked: { if (topBar.shellRoot) { @@ -312,11 +310,6 @@ PanelWindow { } } } - - // Power Button - PowerButton { - anchors.verticalCenter: parent.verticalCenter - } } } } diff --git a/Widgets/TopBar/WeatherWidget.qml b/Widgets/TopBar/WeatherWidget.qml index 6b9486d4..ab91b229 100644 --- a/Widgets/TopBar/WeatherWidget.qml +++ b/Widgets/TopBar/WeatherWidget.qml @@ -9,7 +9,6 @@ Rectangle { property string weatherCode: "" property int weatherTemp: 0 property int weatherTempF: 0 - property bool useFahrenheit: false signal clicked() @@ -49,7 +48,7 @@ Rectangle { } Text { - text: (useFahrenheit ? weatherTempF : weatherTemp) + "°" + text: (Prefs.useFahrenheit ? weatherTempF : weatherTemp) + "°" + (Prefs.useFahrenheit ? "F" : "C") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText font.weight: Font.Medium diff --git a/Widgets/qmldir b/Widgets/qmldir index 60dee1f0..5e6e5ba6 100644 --- a/Widgets/qmldir +++ b/Widgets/qmldir @@ -15,4 +15,7 @@ PowerConfirmDialog 1.0 PowerConfirmDialog.qml ThemePicker 1.0 ThemePicker.qml CpuMonitorWidget 1.0 CpuMonitorWidget.qml RamMonitorWidget 1.0 RamMonitorWidget.qml -SpotlightLauncher 1.0 SpotlightLauncher.qml \ No newline at end of file +SpotlightLauncher 1.0 SpotlightLauncher.qml +SettingsPopup 1.0 SettingsPopup.qml +SettingsSection 1.0 SettingsSection.qml +SettingsToggle 1.0 SettingsToggle.qml \ No newline at end of file diff --git a/shell.qml b/shell.qml index 2a1583be..34cdb97e 100644 --- a/shell.qml +++ b/shell.qml @@ -46,6 +46,7 @@ ShellRoot { property string powerConfirmAction: "" property string powerConfirmTitle: "" property string powerConfirmMessage: "" + property bool settingsVisible: false // Network properties from NetworkService property string networkStatus: NetworkService.networkStatus @@ -161,7 +162,6 @@ ShellRoot { property var weather: WeatherService.weather // Weather configuration - property bool useFahrenheit: true // Default to Fahrenheit // WiFi Auto-refresh Timer @@ -295,7 +295,6 @@ ShellRoot { weatherCode: root.weather.wCode weatherTemp: root.weather.temp weatherTempF: root.weather.tempF - useFahrenheit: root.useFahrenheit osLogo: root.osLogo networkStatus: root.networkStatus wifiSignalStrength: root.wifiSignalStrength @@ -340,6 +339,11 @@ ShellRoot { PowerMenuPopup {} PowerConfirmDialog {} + SettingsPopup { + id: settingsPopup + settingsVisible: root.settingsVisible + } + // Application and clipboard components AppLauncher { id: appLauncher