diff --git a/quickshell/agility/Core/Components/SidebarNavRow.qml b/quickshell/agility/Core/Components/SidebarNavRow.qml index 9b1418c..eed7220 100644 --- a/quickshell/agility/Core/Components/SidebarNavRow.qml +++ b/quickshell/agility/Core/Components/SidebarNavRow.qml @@ -10,8 +10,11 @@ Rectangle { property string labelText: "" property string statusText: "" property bool isActive: false + property bool hasChevron: false + property bool isExpanded: false signal rowClicked() + signal chevronClicked() Layout.fillWidth: true Layout.preferredHeight: Constants.size3Xl @@ -78,6 +81,24 @@ Rectangle { } + SvgIcon { + visible: rowRoot.hasChevron + icon: "chevron-down" + iconSize: 12 + flat: true + rotation: rowRoot.isExpanded ? 0 : -90 + iconColor: rowRoot.isActive ? Theme.fg : (hoverHandler.hovered ? Theme.fg : Theme.muted) + Layout.alignment: Qt.AlignVCenter + Layout.rightMargin: Constants.size3Xs + + Behavior on rotation { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + } + } HoverHandler { diff --git a/quickshell/agility/Core/Services/AppState.qml b/quickshell/agility/Core/Services/AppState.qml index c2b70f6..8467fd8 100644 --- a/quickshell/agility/Core/Services/AppState.qml +++ b/quickshell/agility/Core/Services/AppState.qml @@ -29,6 +29,10 @@ Item { property real activeNotificationHeight: 0 readonly property bool isNiri: Quickshell.env("NIRI_SOCKET") !== undefined || Quickshell.env("XDG_CURRENT_DESKTOP") === "niri" property bool hasActiveWindow: false + property string niriActiveTitle: "" + property string niriActiveAppId: "" + readonly property string activeWindowTitle: isNiri ? niriActiveTitle : (Hyprland.activeWindow ? (Hyprland.activeWindow.title || "") : "") + readonly property string activeWindowClass: isNiri ? niriActiveAppId : (Hyprland.activeWindow ? (Hyprland.activeWindow.class || Hyprland.activeWindow.initialClass || "") : "") readonly property bool hasWindowOnScreen: isNiri ? hasActiveWindow : Boolean(Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.toplevelCount > 0) readonly property var focusWidgets: ["launcher", "clipboard", "wallpaper", "screenshot", "powerMenu"] readonly property var standaloneWindows: ["minflair_settings", "minflair_keybinds", "packagemanager"] @@ -40,7 +44,7 @@ Item { if (!id) return false; - return id === "dashboard" || id === "controlCenter" || id === "notificationsCenter" || id === "music" || id === "clock" || id.startsWith("systemTray_"); + return id === "dashboard" || id === "controlCenter" || id === "notificationsCenter" || id === "music" || id === "clock" || id === "weather" || id.startsWith("systemTray_"); } function isFocusWidget(id) { @@ -231,14 +235,44 @@ Item { } } + Process { + id: niriFocusedWindowProc + + command: ["niri", "msg", "-j", "focused-window"] + + stdout: SplitParser { + onRead: (data) => { + if (!data) + return; + try { + let win = JSON.parse(data.trim()); + if (win && win.id !== undefined) { + appState.niriActiveTitle = win.title || ""; + appState.niriActiveAppId = win.app_id || ""; + } else { + appState.niriActiveTitle = ""; + appState.niriActiveAppId = ""; + } + } catch (e) { + appState.niriActiveTitle = ""; + appState.niriActiveAppId = ""; + } + } + } + } + Timer { id: niriDebounceTimer interval: 50 repeat: false onTriggered: { - if (appState.isNiri && !niriQueryProc.running) - niriQueryProc.running = true; + if (appState.isNiri) { + if (!niriQueryProc.running) + niriQueryProc.running = true; + if (!niriFocusedWindowProc.running) + niriFocusedWindowProc.running = true; + } } } @@ -252,12 +286,15 @@ Item { onTriggered: { if (!niriQueryProc.running) niriQueryProc.running = true; + if (!niriFocusedWindowProc.running) + niriFocusedWindowProc.running = true; } } Component.onCompleted: { if (appState.isNiri) { niriQueryProc.running = true; + niriFocusedWindowProc.running = true; niriEventStreamProc.running = true; } } diff --git a/quickshell/agility/Core/Services/NotificationService.qml b/quickshell/agility/Core/Services/NotificationService.qml index e0c51f3..55ad751 100644 --- a/quickshell/agility/Core/Services/NotificationService.qml +++ b/quickshell/agility/Core/Services/NotificationService.qml @@ -10,7 +10,7 @@ Item { property alias server: notificationServer property alias activeList: activeListModel property alias historyList: historyListModel - property bool dndEnabled: false + property bool dndEnabled: SettingsService.dndEnabled property int unreadCount: historyListModel.count property var notificationObjects: ({ }) diff --git a/quickshell/agility/Core/Services/SettingsService.qml b/quickshell/agility/Core/Services/SettingsService.qml index 2f63b68..cd74539 100644 --- a/quickshell/agility/Core/Services/SettingsService.qml +++ b/quickshell/agility/Core/Services/SettingsService.qml @@ -34,6 +34,19 @@ Item { property string pillPopupStyle: "center" property string pillWidgetLayout: "distributed" property bool barAutoHide: false + property string barGlassMode: "solid" + property int barGlassOpacity: 85 + property bool appletActiveWindow: true + property string appletActiveWindowAction: "maximize" + property bool appletDynamicMedia: true + property bool appletMediaVisualizer: true + property bool appletWeather: true + property string appletWeatherUnit: "C" + property bool appletBattery: true + property bool appletBatteryShowTime: true + property bool appletNetwork: false + property bool appletDnd: false + property bool dndEnabled: false readonly property int barWidgetHeight: 28 property bool edgeLightingEnabled: true property string edgeLightingColorMode: "album_art" @@ -205,6 +218,19 @@ Item { saveSettings(); } + onBarGlassModeChanged: { if (settingsLoaded) saveSettings(); } + onBarGlassOpacityChanged: { if (settingsLoaded) saveSettings(); } + onAppletActiveWindowChanged: { if (settingsLoaded) saveSettings(); } + onAppletActiveWindowActionChanged: { if (settingsLoaded) saveSettings(); } + onAppletDynamicMediaChanged: { if (settingsLoaded) saveSettings(); } + onAppletMediaVisualizerChanged: { if (settingsLoaded) saveSettings(); } + onAppletWeatherChanged: { if (settingsLoaded) saveSettings(); } + onAppletWeatherUnitChanged: { if (settingsLoaded) saveSettings(); } + onAppletBatteryChanged: { if (settingsLoaded) saveSettings(); } + onAppletBatteryShowTimeChanged: { if (settingsLoaded) saveSettings(); } + onAppletNetworkChanged: { if (settingsLoaded) saveSettings(); } + onAppletDndChanged: { if (settingsLoaded) saveSettings(); } + onDndEnabledChanged: { if (settingsLoaded) saveSettings(); } onEdgeLightingEnabledChanged: { if (settingsLoaded) saveSettings(); } onEdgeLightingColorModeChanged: { if (settingsLoaded) saveSettings(); } onEdgeLightingCustomColorChanged: { if (settingsLoaded) saveSettings(); } @@ -265,6 +291,19 @@ Item { "pillPopupStyle": settingsService.pillPopupStyle, "pillWidgetLayout": settingsService.pillWidgetLayout, "barAutoHide": settingsService.barAutoHide, + "barGlassMode": settingsService.barGlassMode, + "barGlassOpacity": settingsService.barGlassOpacity, + "appletActiveWindow": settingsService.appletActiveWindow, + "appletActiveWindowAction": settingsService.appletActiveWindowAction, + "appletDynamicMedia": settingsService.appletDynamicMedia, + "appletMediaVisualizer": settingsService.appletMediaVisualizer, + "appletWeather": settingsService.appletWeather, + "appletWeatherUnit": settingsService.appletWeatherUnit, + "appletBattery": settingsService.appletBattery, + "appletBatteryShowTime": settingsService.appletBatteryShowTime, + "appletNetwork": settingsService.appletNetwork, + "appletDnd": settingsService.appletDnd, + "dndEnabled": settingsService.dndEnabled, "actionShortcuts": settingsService.actionShortcuts, "customShortcuts": settingsService.customShortcuts, "edgeLightingEnabled": settingsService.edgeLightingEnabled, @@ -451,6 +490,33 @@ cp ~/.cache/quickshell/settings_prefs.json /var/tmp/sddm-theme/settings_prefs.js if (prefs.barAutoHide !== undefined) settingsService.barAutoHide = Boolean(prefs.barAutoHide); + if (prefs.barGlassMode !== undefined) + settingsService.barGlassMode = prefs.barGlassMode; + if (prefs.barGlassOpacity !== undefined) + settingsService.barGlassOpacity = prefs.barGlassOpacity; + if (prefs.appletActiveWindow !== undefined) + settingsService.appletActiveWindow = prefs.appletActiveWindow; + if (prefs.appletActiveWindowAction !== undefined) + settingsService.appletActiveWindowAction = prefs.appletActiveWindowAction; + if (prefs.appletDynamicMedia !== undefined) + settingsService.appletDynamicMedia = prefs.appletDynamicMedia; + if (prefs.appletMediaVisualizer !== undefined) + settingsService.appletMediaVisualizer = prefs.appletMediaVisualizer; + if (prefs.appletWeather !== undefined) + settingsService.appletWeather = prefs.appletWeather; + if (prefs.appletWeatherUnit !== undefined) + settingsService.appletWeatherUnit = prefs.appletWeatherUnit; + if (prefs.appletBattery !== undefined) + settingsService.appletBattery = prefs.appletBattery; + if (prefs.appletBatteryShowTime !== undefined) + settingsService.appletBatteryShowTime = prefs.appletBatteryShowTime; + if (prefs.appletNetwork !== undefined) + settingsService.appletNetwork = prefs.appletNetwork; + if (prefs.appletDnd !== undefined) + settingsService.appletDnd = prefs.appletDnd; + if (prefs.dndEnabled !== undefined) + settingsService.dndEnabled = prefs.dndEnabled; + if (prefs.actionShortcuts !== undefined) settingsService.actionShortcuts = prefs.actionShortcuts; diff --git a/quickshell/agility/Core/Services/WeatherService.qml b/quickshell/agility/Core/Services/WeatherService.qml new file mode 100644 index 0000000..ec9118e --- /dev/null +++ b/quickshell/agility/Core/Services/WeatherService.qml @@ -0,0 +1,155 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Core +import qs.Core.Services +pragma Singleton + +Item { + id: weatherService + + property string city: "Loading..." + property int temperatureC: 22 + property int apparentTemperatureC: 22 + property int humidity: 50 + property int windSpeed: 5 + property int weatherCode: 0 + property var dailyForecast: [] + property bool isFetching: false + + readonly property string displayTemp: { + let isF = SettingsService.appletWeatherUnit === "F"; + let val = isF ? Math.round(temperatureC * 9 / 5 + 32) : temperatureC; + return val + (isF ? "°F" : "°C"); + } + + readonly property string displayFeelsLike: { + let isF = SettingsService.appletWeatherUnit === "F"; + let val = isF ? Math.round(apparentTemperatureC * 9 / 5 + 32) : apparentTemperatureC; + return val + (isF ? "°F" : "°C"); + } + + function wmoToIcon(code) { + if (code === 0) + return "sun-duotone"; + if (code === 1 || code === 2) + return "cloud-sun-duotone"; + if (code === 3) + return "cloud-duotone"; + if (code === 45 || code === 48) + return "cloud-fog-duotone"; + if ((code >= 51 && code <= 67) || (code >= 80 && code <= 82)) + return "cloud-rain-duotone"; + if ((code >= 71 && code <= 77) || code === 85 || code === 86) + return "cloud-snow-duotone"; + if (code >= 95) + return "cloud-lightning-duotone"; + return "sun-duotone"; + } + + function wmoToText(code) { + if (code === 0) + return "Clear sky"; + if (code === 1) + return "Mainly clear"; + if (code === 2) + return "Partly cloudy"; + if (code === 3) + return "Overcast"; + if (code === 45 || code === 48) + return "Foggy"; + if (code >= 51 && code <= 55) + return "Drizzle"; + if (code >= 61 && code <= 65) + return "Rain"; + if (code >= 71 && code <= 77) + return "Snow"; + if (code >= 80 && code <= 82) + return "Rain showers"; + if (code >= 95) + return "Thunderstorm"; + return "Fair"; + } + + readonly property string conditionText: wmoToText(weatherCode) + readonly property string conditionIcon: wmoToIcon(weatherCode) + + function formatDaily(dailyList) { + if (!dailyList || !Array.isArray(dailyList)) + return []; + + let days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + let isF = SettingsService.appletWeatherUnit === "F"; + let res = []; + + for (let i = 0; i < dailyList.length; i++) { + let item = dailyList[i]; + let d = new Date(item.date); + let dayName = (i === 0) ? "Today" : ((i === 1) ? "Tomorrow" : days[d.getDay()]); + let maxVal = isF ? Math.round(item.max * 9 / 5 + 32) : item.max; + let minVal = isF ? Math.round(item.min * 9 / 5 + 32) : item.min; + + res.push({ + "day": dayName, + "icon": wmoToIcon(item.code), + "condition": wmoToText(item.code), + "max": maxVal + "°", + "min": minVal + "°" + }); + } + return res; + } + + function refreshWeather(force) { + if (isFetching) + return; + isFetching = true; + let scriptPath = Quickshell.shellDir + "/Scripts/system/fetch_weather.sh"; + weatherProc.command = ["bash", scriptPath, force ? "--force" : ""]; + weatherProc.running = true; + } + + Process { + id: weatherProc + + stdout: StdioCollector { + id: weatherOutput + } + + onExited: function(exitCode) { + weatherService.isFetching = false; + if (exitCode === 0 && weatherOutput.text) { + try { + let data = JSON.parse(weatherOutput.text.trim()); + if (data && !data.error) { + weatherService.city = data.city || "Local"; + weatherService.temperatureC = data.temperature !== undefined ? data.temperature : 22; + weatherService.apparentTemperatureC = data.apparent_temperature !== undefined ? data.apparent_temperature : 22; + weatherService.humidity = data.humidity !== undefined ? data.humidity : 50; + weatherService.windSpeed = data.wind_speed !== undefined ? data.wind_speed : 5; + weatherService.weatherCode = data.weather_code !== undefined ? data.weather_code : 0; + weatherService.dailyForecast = weatherService.formatDaily(data.daily); + } + } catch (e) { + console.error("Error parsing weather JSON: " + e); + } + } + } + } + + Timer { + id: weatherPollTimer + + interval: 900000 // 15 mins + repeat: true + running: SettingsService.appletWeather + triggeredOnStart: true + onTriggered: { + weatherService.refreshWeather(false); + } + } + + Component.onCompleted: { + weatherService.refreshWeather(false); + } +} diff --git a/quickshell/agility/Modules/Bar/BarOverlayWindow.qml b/quickshell/agility/Modules/Bar/BarOverlayWindow.qml index 8a0be88..dde3009 100644 --- a/quickshell/agility/Modules/Bar/BarOverlayWindow.qml +++ b/quickshell/agility/Modules/Bar/BarOverlayWindow.qml @@ -429,7 +429,7 @@ PanelWindow { anchors.fill: parent acceptedButtons: Qt.AllButtons - enabled: root.hasExpandableHost && root.activeHost && root.activeHost.isOpen + enabled: (root.hasExpandableHost && root.activeHost && root.activeHost.isOpen) || AppState.isWidgetOpen("weather") onClicked: { if (root.activeHost && typeof root.activeHost.close === "function") root.activeHost.close(); @@ -545,6 +545,16 @@ PanelWindow { height: isActive ? root.height : 0 } + // Weather Card Popout Overlay + Region { + property bool isActive: AppState.isWidgetOpen("weather") + + x: 0 + y: 0 + width: isActive ? root.width : 0 + height: isActive ? root.height : 0 + } + } } diff --git a/quickshell/agility/Modules/Bar/Components/ActiveWindowPill.qml b/quickshell/agility/Modules/Bar/Components/ActiveWindowPill.qml new file mode 100644 index 0000000..298d18a --- /dev/null +++ b/quickshell/agility/Modules/Bar/Components/ActiveWindowPill.qml @@ -0,0 +1,162 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Core +import qs.Core.Components +import qs.Core.Services + +Rectangle { + id: root + + readonly property string rawTitle: AppState.activeWindowTitle + readonly property string rawClass: AppState.activeWindowClass + readonly property bool hasWindow: SettingsService.appletActiveWindow && rawTitle !== "" + property bool isHovered: mouseArea.containsMouse + property bool isPressed: mouseArea.pressed + + function resolveAppIcon(appId) { + if (!appId) + return "app-window-duotone"; + let lower = appId.toLowerCase(); + if (lower.includes("brave")) + return "monitor-filled"; + if (lower.includes("firefox")) + return "monitor"; + if (lower.includes("code") || lower.includes("nvim") || lower.includes("neovim")) + return "code"; + if (lower.includes("terminal") || lower.includes("alacritty") || lower.includes("kitty") || lower.includes("foot")) + return "terminal-window-duotone"; + if (lower.includes("spotify") || lower.includes("music") || lower.includes("cider")) + return "music"; + if (lower.includes("files") || lower.includes("thunar") || lower.includes("nemo") || lower.includes("dolphin")) + return "box"; + if (lower.includes("bubble")) + return "sparkles"; + return "app-window-duotone"; + } + + function handlePrimaryClick() { + if (SettingsService.appletActiveWindowAction === "switcher") { + AppState.toggleWidget("launcher"); + return; + } + + // Default: Maximize / Minimize toggle + if (AppState.isNiri) { + maximizeNiriProc.running = true; + } else { + Quickshell.exec(["hyprctl", "dispatch", "fullscreen", "1"]); + } + } + + Process { + id: maximizeNiriProc + command: ["niri", "msg", "action", "maximize-column"] + } + + implicitHeight: SettingsService.barWidgetHeight + height: parent && parent.height > 0 ? parent.height : implicitHeight + implicitWidth: hasWindow ? Math.min(220, contentRow.implicitWidth + Constants.sizeSm * 2) : 0 + width: implicitWidth + opacity: hasWindow ? 1 : 0 + visible: hasWindow || opacity > 0.001 + clip: true + + radius: height / 2 + color: isHovered ? Theme.bgSecondary : "transparent" + border.color: isHovered ? Theme.borderSecondary : "transparent" + border.width: 1 + scale: isPressed ? 0.96 : (isHovered ? 1.02 : 1) + + Behavior on width { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + Behavior on opacity { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + Behavior on color { + ColorAnimation { + duration: Constants.animFast + } + } + + Behavior on scale { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: (mouse) => { + if (mouse.button === Qt.RightButton) { + AppState.toggleWidget("launcher"); + } else { + root.handlePrimaryClick(); + } + } + + onDoubleClicked: { + AppState.toggleWidget("launcher"); + } + } + + RowLayout { + id: contentRow + anchors.centerIn: parent + spacing: Constants.size2Xs + + SvgIcon { + icon: root.resolveAppIcon(root.rawClass) + iconSize: 14 + flat: true + iconColor: root.isHovered ? Theme.accent : Theme.fg + Layout.alignment: Qt.AlignVCenter + + Behavior on iconColor { + ColorAnimation { + duration: Constants.animFast + } + } + } + + ThemedText { + text: { + let t = root.rawTitle.trim(); + let c = root.rawClass.trim(); + if (c !== "" && !t.toLowerCase().startsWith(c.toLowerCase())) { + let shortClass = c.split('.').pop(); + return "[" + shortClass + "] " + t; + } + return t; + } + customSize: 11 + font.weight: Font.Medium + color: root.isHovered ? Theme.fg : Theme.muted + elide: Text.ElideRight + Layout.maximumWidth: 160 + Layout.alignment: Qt.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Constants.animFast + } + } + } + } +} diff --git a/quickshell/agility/Modules/Bar/Components/BatteryPill.qml b/quickshell/agility/Modules/Bar/Components/BatteryPill.qml new file mode 100644 index 0000000..2c9323d --- /dev/null +++ b/quickshell/agility/Modules/Bar/Components/BatteryPill.qml @@ -0,0 +1,144 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Core +import qs.Core.Components +import qs.Core.Services + +Rectangle { + id: root + + readonly property bool isCharging: SystemInfoService.batteryStatus === "Charging" + readonly property int level: SystemInfoService.batteryLevel + readonly property bool hasBattery: SystemInfoService.hasBattery + readonly property bool isVisible: SettingsService.appletBattery && hasBattery + property bool isHovered: mouseArea.containsMouse + property bool isPressed: mouseArea.pressed + + function getBatteryIcon() { + if (root.isCharging) + return "battery-charging-duotone"; + if (root.level >= 80) + return "battery-full-duotone"; + if (root.level >= 50) + return "battery-high-duotone"; + if (root.level >= 20) + return "battery-medium-duotone"; + return "battery-low-duotone"; + } + + implicitHeight: SettingsService.barWidgetHeight + height: parent && parent.height > 0 ? parent.height : implicitHeight + implicitWidth: isVisible ? (contentRow.implicitWidth + Constants.sizeSm * 2) : 0 + width: implicitWidth + opacity: isVisible ? 1 : 0 + visible: isVisible || opacity > 0.001 + clip: true + + radius: height / 2 + color: isHovered ? Theme.bgSecondary : (isCharging ? Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.12) : "transparent") + border.color: isCharging ? Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.4) : (isHovered ? Theme.borderSecondary : "transparent") + border.width: 1 + scale: isPressed ? 0.96 : (isHovered ? 1.02 : 1) + + Behavior on width { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + Behavior on opacity { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + Behavior on color { + ColorAnimation { + duration: Constants.animFast + } + } + + // Charging Pulse Glow Animation + SequentialAnimation on opacity { + running: root.isCharging && root.isVisible + loops: Animation.Infinite + + NumberAnimation { + from: 1.0 + to: 0.65 + duration: 1000 + easing.type: Easing.InOutSine + } + NumberAnimation { + from: 0.65 + to: 1.0 + duration: 1000 + easing.type: Easing.InOutSine + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + AppState.toggleWidget("controlCenter"); + } + } + + RowLayout { + id: contentRow + anchors.centerIn: parent + spacing: Constants.size2Xs + + SvgIcon { + icon: root.getBatteryIcon() + iconSize: 14 + flat: true + iconColor: { + if (root.isCharging) + return Theme.accent; + if (root.level <= 15) + return Theme.error; + if (root.level <= 30) + return Theme.warning; + return root.isHovered ? Theme.fg : Theme.muted; + } + Layout.alignment: Qt.AlignVCenter + + Behavior on iconColor { + ColorAnimation { + duration: Constants.animFast + } + } + } + + ThemedText { + text: { + let txt = root.level + "%"; + if (SettingsService.appletBatteryShowTime && SystemInfoService.batteryEstimation !== "") { + let est = SystemInfoService.batteryEstimation.trim(); + if (est !== "" && est !== "Unknown" && est !== "Estimating...") { + txt += " • " + est; + } + } + return txt; + } + customSize: 11 + font.weight: Font.Medium + color: root.isHovered ? Theme.fg : Theme.muted + Layout.alignment: Qt.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Constants.animFast + } + } + } + } +} diff --git a/quickshell/agility/Modules/Bar/Components/DndPill.qml b/quickshell/agility/Modules/Bar/Components/DndPill.qml new file mode 100644 index 0000000..002b529 --- /dev/null +++ b/quickshell/agility/Modules/Bar/Components/DndPill.qml @@ -0,0 +1,79 @@ +import QtQuick +import Quickshell +import qs.Core +import qs.Core.Components +import qs.Core.Services + +Rectangle { + id: root + + readonly property bool isDnd: SettingsService.dndEnabled + readonly property bool isVisible: SettingsService.appletDnd + property bool isHovered: mouseArea.containsMouse + property bool isPressed: mouseArea.pressed + + implicitHeight: SettingsService.barWidgetHeight + height: parent && parent.height > 0 ? parent.height : implicitHeight + implicitWidth: isVisible ? height : 0 + width: implicitWidth + opacity: isVisible ? 1 : 0 + visible: isVisible || opacity > 0.001 + clip: true + + radius: height / 2 + color: isDnd ? Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.2) : (isHovered ? Theme.bgSecondary : "transparent") + border.color: isDnd ? Theme.accent : (isHovered ? Theme.borderSecondary : "transparent") + border.width: 1 + scale: isPressed ? 0.94 : (isHovered ? 1.05 : 1) + + Behavior on width { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + Behavior on opacity { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + Behavior on color { + ColorAnimation { + duration: Constants.animFast + } + } + + Behavior on scale { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + SettingsService.dndEnabled = !SettingsService.dndEnabled; + } + } + + SvgIcon { + anchors.centerIn: parent + icon: root.isDnd ? "bell-slash-duotone" : "bell" + iconSize: 14 + flat: true + iconColor: root.isDnd ? Theme.accent : (root.isHovered ? Theme.fg : Theme.muted) + + Behavior on iconColor { + ColorAnimation { + duration: Constants.animFast + } + } + } +} diff --git a/quickshell/agility/Modules/Bar/Components/MediaButton.qml b/quickshell/agility/Modules/Bar/Components/MediaButton.qml index 95749aa..fa8c37f 100644 --- a/quickshell/agility/Modules/Bar/Components/MediaButton.qml +++ b/quickshell/agility/Modules/Bar/Components/MediaButton.qml @@ -13,7 +13,7 @@ Rectangle { readonly property bool isPlaying: MprisService.isPlaying readonly property string trackTitle: hasPlayer ? (MprisService.activePlayer.trackTitle || "").trim() : "" readonly property string trackArtist: hasPlayer ? (MprisService.activePlayer.trackArtist || "").trim() : "" - readonly property bool rawHasMedia: hasPlayer && (trackTitle !== "" || trackArtist !== "") + readonly property bool rawHasMedia: SettingsService.appletDynamicMedia && hasPlayer && (trackTitle !== "" || trackArtist !== "") property bool hasMedia: rawHasMedia property string lastTrackTitle: "" property string lastTrackArtist: "" @@ -22,12 +22,33 @@ Rectangle { property bool isHovered: mouseArea.containsMouse property bool isPressed: mouseArea.pressed readonly property bool isCenteredBar: SettingsService.barIslandMode - readonly property real fullWidth: Math.min(300, contentRow.implicitWidth) + readonly property real fullWidth: Math.min(320, contentRow.implicitWidth + (isHovered && hasPlayer ? 54 : 0)) + property real dynamicExpansionScale: 1.0 onTrackTitleChanged: { - if (trackTitle !== "") + if (trackTitle !== "") { lastTrackTitle = trackTitle; + trackChangeAnim.restart(); + } + } + + SequentialAnimation { + id: trackChangeAnim + NumberAnimation { + target: root + property: "dynamicExpansionScale" + to: 1.08 + duration: 180 + easing.type: Easing.OutBack + } + NumberAnimation { + target: root + property: "dynamicExpansionScale" + to: 1.0 + duration: 250 + easing.type: Easing.OutCubic + } } onTrackArtistChanged: { if (trackArtist !== "") @@ -50,7 +71,7 @@ Rectangle { visible: hasMedia || opacity > 0.001 clip: true color: "transparent" - scale: DisplayProfileService.gameModeActive ? 1 : (isPressed ? 0.95 : (isHovered ? 1.02 : 1)) + scale: (DisplayProfileService.gameModeActive ? 1 : (isPressed ? 0.95 : (isHovered ? 1.02 : 1))) * dynamicExpansionScale Timer { id: hideDebounceTimer @@ -92,7 +113,7 @@ Rectangle { MiniCavaBars { id: visualizerRow - visible: root.hasMedia + visible: root.hasMedia && SettingsService.appletMediaVisualizer Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: visible ? implicitWidth : 0 Layout.rightMargin: Constants.size2Xs @@ -138,6 +159,71 @@ Rectangle { Layout.preferredWidth: Math.min(implicitWidth, Layout.maximumWidth) } + RowLayout { + id: hoverControls + visible: root.isHovered && root.hasPlayer + spacing: 2 + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Constants.size2Xs + + Rectangle { + width: 18 + height: 18 + radius: 9 + color: prevHover.hovered ? Theme.bgSecondary : "transparent" + Layout.alignment: Qt.AlignVCenter + + SvgIcon { + anchors.centerIn: parent + icon: "player-previous" + iconSize: 10 + flat: true + iconColor: prevHover.hovered ? Theme.accent : Theme.fg + } + + HoverHandler { id: prevHover; cursorShape: Qt.PointingHandCursor } + TapHandler { onTapped: MprisService.activePlayer && MprisService.activePlayer.previous() } + } + + Rectangle { + width: 18 + height: 18 + radius: 9 + color: playHover.hovered ? Theme.bgSecondary : "transparent" + Layout.alignment: Qt.AlignVCenter + + SvgIcon { + anchors.centerIn: parent + icon: MprisService.isPlaying ? "player-pause" : "player-play" + iconSize: 10 + flat: true + iconColor: playHover.hovered ? Theme.accent : Theme.fg + } + + HoverHandler { id: playHover; cursorShape: Qt.PointingHandCursor } + TapHandler { onTapped: MprisService.activePlayer && MprisService.activePlayer.togglePlaying() } + } + + Rectangle { + width: 18 + height: 18 + radius: 9 + color: nextHover.hovered ? Theme.bgSecondary : "transparent" + Layout.alignment: Qt.AlignVCenter + + SvgIcon { + anchors.centerIn: parent + icon: "player-next" + iconSize: 10 + flat: true + iconColor: nextHover.hovered ? Theme.accent : Theme.fg + } + + HoverHandler { id: nextHover; cursorShape: Qt.PointingHandCursor } + TapHandler { onTapped: MprisService.activePlayer && MprisService.activePlayer.next() } + } + } + } Behavior on opacity { diff --git a/quickshell/agility/Modules/Bar/Components/NetworkPill.qml b/quickshell/agility/Modules/Bar/Components/NetworkPill.qml new file mode 100644 index 0000000..4a1730b --- /dev/null +++ b/quickshell/agility/Modules/Bar/Components/NetworkPill.qml @@ -0,0 +1,142 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Core +import qs.Core.Components +import qs.Core.Services + +Rectangle { + id: root + + property real prevRxBytes: 0 + property real prevTxBytes: 0 + property real prevTimestamp: 0 + property string downSpeedText: "0K" + property string upSpeedText: "0K" + property bool isHovered: mouseArea.containsMouse + property bool isPressed: mouseArea.pressed + readonly property bool isVisible: SettingsService.appletNetwork + + function formatSpeed(bytesPerSec) { + if (bytesPerSec < 1024) + return Math.round(bytesPerSec) + "B"; + if (bytesPerSec < 1024 * 1024) + return Math.round(bytesPerSec / 1024) + "K"; + return (bytesPerSec / (1024 * 1024)).toFixed(1) + "M"; + } + + implicitHeight: SettingsService.barWidgetHeight + height: parent && parent.height > 0 ? parent.height : implicitHeight + implicitWidth: isVisible ? (contentRow.implicitWidth + Constants.sizeSm * 2) : 0 + width: implicitWidth + opacity: isVisible ? 1 : 0 + visible: isVisible || opacity > 0.001 + clip: true + + radius: height / 2 + color: isHovered ? Theme.bgSecondary : "transparent" + border.color: isHovered ? Theme.borderSecondary : "transparent" + border.width: 1 + scale: isPressed ? 0.96 : (isHovered ? 1.02 : 1) + + Behavior on width { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + Behavior on opacity { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + Process { + id: netProc + command: ["sh", "-c", "awk 'NR>2 {rx+=$2; tx+=$10} END {print rx, tx}' /proc/net/dev"] + + stdout: SplitParser { + onRead: (line) => { + if (!line) return; + let parts = line.trim().split(/\s+/); + if (parts.length >= 2) { + let rx = parseFloat(parts[0]) || 0; + let tx = parseFloat(parts[1]) || 0; + let now = Date.now(); + + if (root.prevTimestamp > 0 && now > root.prevTimestamp) { + let dt = (now - root.prevTimestamp) / 1000; + let rxRate = Math.max(0, (rx - root.prevRxBytes) / dt); + let txRate = Math.max(0, (tx - root.prevTxBytes) / dt); + root.downSpeedText = root.formatSpeed(rxRate); + root.upSpeedText = root.formatSpeed(txRate); + } + + root.prevRxBytes = rx; + root.prevTxBytes = tx; + root.prevTimestamp = now; + } + } + } + } + + Timer { + interval: 2000 + repeat: true + running: root.isVisible + triggeredOnStart: true + onTriggered: { + if (!netProc.running) + netProc.running = true; + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: AppState.toggleWidget("controlCenter") + } + + RowLayout { + id: contentRow + anchors.centerIn: parent + spacing: Constants.size3Xs + + SvgIcon { + icon: "download" + iconSize: 12 + flat: true + iconColor: Theme.accent + Layout.alignment: Qt.AlignVCenter + } + + ThemedText { + text: root.downSpeedText + customSize: 10 + font.weight: Font.Medium + color: root.isHovered ? Theme.fg : Theme.muted + Layout.alignment: Qt.AlignVCenter + } + + SvgIcon { + icon: "arrow-up-duotone" + iconSize: 11 + flat: true + iconColor: Theme.muted + Layout.alignment: Qt.AlignVCenter + } + + ThemedText { + text: root.upSpeedText + customSize: 10 + font.weight: Font.Medium + color: root.isHovered ? Theme.fg : Theme.muted + Layout.alignment: Qt.AlignVCenter + } + } +} diff --git a/quickshell/agility/Modules/Bar/Components/WeatherCard.qml b/quickshell/agility/Modules/Bar/Components/WeatherCard.qml new file mode 100644 index 0000000..5c9366c --- /dev/null +++ b/quickshell/agility/Modules/Bar/Components/WeatherCard.qml @@ -0,0 +1,257 @@ +import Qt5Compat.GraphicalEffects +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Core +import qs.Core.Components +import qs.Core.Services + +Rectangle { + id: root + + property bool isOpen: false + + signal closeRequested() + + width: 270 + implicitHeight: mainLayout.implicitHeight + Constants.sizeLg * 2 + radius: Constants.sizeMd + color: Theme.bg + border.color: Theme.borderSecondary + border.width: 1 + + opacity: isOpen ? 1 : 0 + scale: isOpen ? 1 : 0.94 + visible: opacity > 0.001 + + Behavior on opacity { + NumberAnimation { + duration: Constants.animNormal + easing.type: Easing.OutCubic + } + } + + Behavior on scale { + NumberAnimation { + duration: Constants.animNormal + easing.type: Easing.OutCubic + } + } + + layer.enabled: true + layer.effect: DropShadow { + transparentBorder: true + color: Theme.shadow + radius: Constants.sizeLg + samples: 16 + verticalOffset: 6 + horizontalOffset: 0 + } + + ColumnLayout { + id: mainLayout + anchors.fill: parent + anchors.margins: Constants.sizeMd + spacing: Constants.sizeSm + + // Header: Location & Refresh Button + RowLayout { + Layout.fillWidth: true + + SvgIcon { + icon: "sparkles" + iconSize: 13 + flat: true + iconColor: Theme.accent + Layout.alignment: Qt.AlignVCenter + } + + ThemedText { + text: WeatherService.city + customSize: 13 + font.weight: Font.Bold + color: Theme.fg + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + + Rectangle { + width: 24 + height: 24 + radius: 12 + color: refreshHover.hovered ? Theme.bgSecondary : "transparent" + Layout.alignment: Qt.AlignVCenter + + SvgIcon { + anchors.centerIn: parent + icon: "refresh" + iconSize: 12 + flat: true + iconColor: WeatherService.isFetching ? Theme.accent : Theme.muted + rotation: WeatherService.isFetching ? 360 : 0 + + Behavior on rotation { + NumberAnimation { + duration: 800 + loops: Animation.Infinite + } + } + } + + HoverHandler { + id: refreshHover + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onTapped: WeatherService.refreshWeather(true) + } + } + } + + // Current Weather Hero + RowLayout { + Layout.fillWidth: true + spacing: Constants.sizeMd + + SvgIcon { + icon: WeatherService.conditionIcon + iconSize: 38 + flat: true + iconColor: Theme.accent + Layout.alignment: Qt.AlignVCenter + } + + ColumnLayout { + spacing: 0 + Layout.fillWidth: true + + ThemedText { + text: WeatherService.displayTemp + customSize: 26 + font.weight: Font.Bold + color: Theme.fg + } + + ThemedText { + text: WeatherService.conditionText + " • Feels " + WeatherService.displayFeelsLike + customSize: 11 + color: Theme.muted + elide: Text.ElideRight + Layout.fillWidth: true + } + } + } + + // Quick Stats: Humidity & Wind + RowLayout { + Layout.fillWidth: true + spacing: Constants.sizeSm + + Rectangle { + Layout.fillWidth: true + height: 32 + radius: Constants.sizeXs + color: Theme.bgSecondary + + RowLayout { + anchors.centerIn: parent + spacing: 4 + + ThemedText { + text: "💧" + customSize: 10 + } + ThemedText { + text: WeatherService.humidity + "% Humidity" + customSize: 10 + color: Theme.muted + } + } + } + + Rectangle { + Layout.fillWidth: true + height: 32 + radius: Constants.sizeXs + color: Theme.bgSecondary + + RowLayout { + anchors.centerIn: parent + spacing: 4 + + ThemedText { + text: "💨" + customSize: 10 + } + ThemedText { + text: WeatherService.windSpeed + " km/h" + customSize: 10 + color: Theme.muted + } + } + } + } + + // Divider + Rectangle { + Layout.fillWidth: true + height: 1 + color: Theme.borderSecondary + opacity: 0.5 + } + + // 3-Day Forecast Section Header + ThemedText { + text: "3-DAY FORECAST" + customSize: 9 + font.weight: Font.Bold + font.letterSpacing: 0.8 + color: Theme.muted + Layout.topMargin: 2 + } + + // 3-Day Forecast Rows + Repeater { + model: WeatherService.dailyForecast + + delegate: RowLayout { + required property var modelData + Layout.fillWidth: true + spacing: Constants.sizeSm + + ThemedText { + text: modelData.day + customSize: 11 + font.weight: Font.Medium + color: Theme.fg + Layout.preferredWidth: 60 + } + + SvgIcon { + icon: modelData.icon + iconSize: 14 + flat: true + iconColor: Theme.accent + Layout.alignment: Qt.AlignVCenter + } + + ThemedText { + text: modelData.condition + customSize: 10 + color: Theme.muted + elide: Text.ElideRight + Layout.fillWidth: true + } + + ThemedText { + text: modelData.max + " / " + modelData.min + customSize: 11 + font.weight: Font.DemiBold + color: Theme.fg + Layout.alignment: Qt.AlignRight + } + } + } + } +} diff --git a/quickshell/agility/Modules/Bar/Components/WeatherPill.qml b/quickshell/agility/Modules/Bar/Components/WeatherPill.qml new file mode 100644 index 0000000..3e82137 --- /dev/null +++ b/quickshell/agility/Modules/Bar/Components/WeatherPill.qml @@ -0,0 +1,115 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Core +import qs.Core.Components +import qs.Core.Services + +Item { + id: root + + readonly property bool isCardOpen: AppState.isWidgetOpen("weather") + property bool isHovered: mouseArea.containsMouse + property bool isPressed: mouseArea.pressed + readonly property bool enabledInSettings: SettingsService.appletWeather + + implicitHeight: SettingsService.barWidgetHeight + height: parent && parent.height > 0 ? parent.height : implicitHeight + implicitWidth: enabledInSettings ? (contentPill.implicitWidth) : 0 + width: implicitWidth + opacity: enabledInSettings ? 1 : 0 + visible: enabledInSettings || opacity > 0.001 + + Behavior on width { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + Behavior on opacity { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + Rectangle { + id: contentPill + anchors.fill: parent + radius: height / 2 + color: root.isCardOpen ? Theme.bgSecondary : (root.isHovered ? Theme.bgSecondary : "transparent") + border.color: root.isCardOpen ? Theme.accent : (root.isHovered ? Theme.borderSecondary : "transparent") + border.width: 1 + scale: root.isPressed ? 0.96 : (root.isHovered ? 1.02 : 1) + + Behavior on color { + ColorAnimation { + duration: Constants.animFast + } + } + + Behavior on scale { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + AppState.toggleWidget("weather"); + } + } + + RowLayout { + anchors.centerIn: parent + anchors.leftMargin: Constants.sizeSm + anchors.rightMargin: Constants.sizeSm + spacing: Constants.size2Xs + + SvgIcon { + icon: WeatherService.conditionIcon + iconSize: 14 + flat: true + iconColor: (root.isCardOpen || root.isHovered) ? Theme.accent : Theme.fg + Layout.alignment: Qt.AlignVCenter + + Behavior on iconColor { + ColorAnimation { + duration: Constants.animFast + } + } + } + + ThemedText { + text: WeatherService.displayTemp + customSize: 11 + font.weight: Font.Medium + color: (root.isCardOpen || root.isHovered) ? Theme.fg : Theme.muted + Layout.alignment: Qt.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Constants.animFast + } + } + } + } + } + + // Floating 3-Day Weather Card Popout + WeatherCard { + id: weatherCard + anchors.top: root.bottom + anchors.topMargin: Constants.sizeSm + anchors.horizontalCenter: root.horizontalCenter + isOpen: root.isCardOpen + onCloseRequested: root.isCardOpen = false + } +} diff --git a/quickshell/agility/Modules/Bar/Styles/Pill/PillBar.qml b/quickshell/agility/Modules/Bar/Styles/Pill/PillBar.qml index ba45322..85f1ffc 100644 --- a/quickshell/agility/Modules/Bar/Styles/Pill/PillBar.qml +++ b/quickshell/agility/Modules/Bar/Styles/Pill/PillBar.qml @@ -48,16 +48,31 @@ Item { readonly property real targetPillWidth: Math.round(root.width * (SettingsService.pillBarWidthPercent / 100)) readonly property bool isFullWidth: SettingsService.pillBarWidthPercent >= 100 + readonly property real bgAlpha: (SettingsService.barGlassMode === "solid") ? 1.0 : (SettingsService.barGlassOpacity / 100) + readonly property color baseBgColor: { + if (SettingsService.barGlassMode === "adaptive") + return Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, bgAlpha * 0.65); + return Qt.rgba(Theme.bg.r, Theme.bg.g, Theme.bg.b, bgAlpha); + } + anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter height: root.baseBarHeight width: Math.max(minPillWidth, Math.min(root.width - (isFullWidth ? 0 : Constants.sizeMd * 2), targetPillWidth)) implicitWidth: width radius: isFullWidth ? Constants.sizeSm : height / 2 - color: Theme.bg - border.color: Theme.borderSecondary + color: baseBgColor + border.color: SettingsService.barGlassMode === "adaptive" ? Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.4) : Theme.borderSecondary border.width: 1 + gradient: SettingsService.barGlassMode === "gradient" ? gradientDef : null + + Gradient { + id: gradientDef + GradientStop { position: 0.0; color: Qt.rgba(Theme.bg.r, Theme.bg.g, Theme.bg.b, pillContainer.bgAlpha * 0.95) } + GradientStop { position: 1.0; color: Qt.rgba(Theme.bgSecondary.r, Theme.bgSecondary.g, Theme.bgSecondary.b, pillContainer.bgAlpha * 0.7) } + } + Behavior on width { NumberAnimation { duration: Constants.animFast @@ -65,6 +80,12 @@ Item { } } + Behavior on color { + ColorAnimation { + duration: Constants.animFast + } + } + layer.enabled: true layer.effect: DropShadow { transparentBorder: true @@ -86,7 +107,7 @@ Item { anchors.rightMargin: isDistributed ? Constants.sizeLg : 0 spacing: Constants.sizeSm - // Left Navigation (Material 3 Dashboard, Compositor Workspaces) + // Left Navigation (Material 3 Dashboard, Compositor Workspaces, Active Window) RowLayout { id: leftGroup spacing: Constants.sizeSm @@ -111,6 +132,11 @@ Item { Layout.alignment: Qt.AlignVCenter opacity: 0.6 } + + ActiveWindowPill { + id: activeWindowPill + Layout.alignment: Qt.AlignVCenter + } } // Expanding spacer between Left and Center @@ -120,7 +146,7 @@ Item { visible: contentRow.isDistributed } - // Center (Clock & Live Playing Media) + // Center (Clock, Weather & Live Playing Media) RowLayout { id: centerGroup spacing: Constants.sizeSm @@ -131,6 +157,11 @@ Item { Layout.alignment: Qt.AlignVCenter } + WeatherPill { + id: weatherPill + Layout.alignment: Qt.AlignVCenter + } + MediaButton { id: mediaBtn Layout.alignment: Qt.AlignVCenter @@ -144,7 +175,7 @@ Item { visible: contentRow.isDistributed } - // Right Quick Controls (Recording, Tray, QuickSettings, Notifications, Power) + // Right Quick Controls (Battery, Network, DND, Recording, Tray, QuickSettings, Notifications, Power) RowLayout { id: rightGroup spacing: Constants.sizeSm @@ -159,6 +190,21 @@ Item { opacity: 0.6 } + NetworkPill { + id: netPill + Layout.alignment: Qt.AlignVCenter + } + + DndPill { + id: dndPill + Layout.alignment: Qt.AlignVCenter + } + + BatteryPill { + id: batteryPill + Layout.alignment: Qt.AlignVCenter + } + RecordingIndicator { id: recIndicator Layout.alignment: Qt.AlignVCenter diff --git a/quickshell/agility/Modules/Settings/BarSettings/BarAppletsSettings.qml b/quickshell/agility/Modules/Settings/BarSettings/BarAppletsSettings.qml new file mode 100644 index 0000000..f9f9614 --- /dev/null +++ b/quickshell/agility/Modules/Settings/BarSettings/BarAppletsSettings.qml @@ -0,0 +1,215 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Core +import qs.Core.Components +import qs.Core.Services +import qs.Core.Utils +import qs.Modules.Settings.Components + +AppContainer { + id: barAppletsRoot + + // ACTIVE WINDOW TITLE & ICON PILL + AppGroup { + title: "Active Window Pill" + icon: "window" + + SettingToggle { + label: "Enable Active Window Pill" + description: "Display the focused application's icon and truncated window title in a compact pill" + checked: SettingsService.appletActiveWindow + onCheckedChanged: { + SettingsService.appletActiveWindow = checked; + } + } + + Divider { + visible: SettingsService.appletActiveWindow + } + + SettingSegmented { + visible: SettingsService.appletActiveWindow + label: "Primary Click Action" + description: "Choose what happens when clicking the window pill (Right-click always opens App Switcher)" + model: [{ + "text": "Maximize / Restore", + "value": "maximize" + }, { + "text": "App Switcher", + "value": "switcher" + }] + currentValue: SettingsService.appletActiveWindowAction + onActivated: (value) => { + SettingsService.appletActiveWindowAction = value; + } + } + } + + // DYNAMIC AUDIO VISUALIZER & MEDIA PILL + AppGroup { + title: "Dynamic Media & Visualizer" + icon: "music" + + SettingToggle { + label: "Enable Dynamic Media Pill" + description: "Display playing song and artist inside a dynamic capsule with hover playback controls" + checked: SettingsService.appletDynamicMedia + onCheckedChanged: { + SettingsService.appletDynamicMedia = checked; + } + } + + Divider { + visible: SettingsService.appletDynamicMedia + } + + SettingToggle { + visible: SettingsService.appletDynamicMedia + label: "Live Cava Audio Visualizer" + description: "Show 4-bar bouncing visualizer bars synchronized with audio output" + checked: SettingsService.appletMediaVisualizer + onCheckedChanged: { + SettingsService.appletMediaVisualizer = checked; + } + } + } + + // WEATHER PILL & 3-DAY FORECAST + AppGroup { + title: "Weather Pill & Forecast" + icon: "sun" + + SettingToggle { + label: "Enable Weather Pill" + description: "Display live weather condition icon and temperature beside the clock that expands into a 3-day card" + checked: SettingsService.appletWeather + onCheckedChanged: { + SettingsService.appletWeather = checked; + } + } + + Divider { + visible: SettingsService.appletWeather + } + + SettingSegmented { + visible: SettingsService.appletWeather + label: "Temperature Unit" + description: "Choose between Celsius (°C) and Fahrenheit (°F)" + model: [{ + "text": "Celsius (°C)", + "value": "C" + }, { + "text": "Fahrenheit (°F)", + "value": "F" + }] + currentValue: SettingsService.appletWeatherUnit + onActivated: (value) => { + SettingsService.appletWeatherUnit = value; + } + } + } + + // BATTERY HEALTH & CHARGE ESTIMATOR + AppGroup { + title: "Battery Health & Estimator" + icon: "charger" + + SettingToggle { + label: "Enable Battery Pill" + description: "Display battery capsule with pulse charging animation and level indicator" + checked: SettingsService.appletBattery + onCheckedChanged: { + SettingsService.appletBattery = checked; + } + } + + Divider { + visible: SettingsService.appletBattery + } + + SettingToggle { + visible: SettingsService.appletBattery + label: "Show Discharge / Charge Time" + description: "Display estimated remaining hours and minutes (e.g. ⚡ 85% • 3h 15m)" + checked: SettingsService.appletBatteryShowTime + onCheckedChanged: { + SettingsService.appletBatteryShowTime = checked; + } + } + } + + // NETWORK & STATUS INDICATORS + AppGroup { + title: "Network & Status Indicators" + icon: "wifi" + + SettingToggle { + label: "Network Traffic Monitor" + description: "Display real-time upload and download speeds on the bar" + checked: SettingsService.appletNetwork + onCheckedChanged: { + SettingsService.appletNetwork = checked; + } + } + + Divider { + } + + SettingToggle { + label: "Do Not Disturb (DND) Pill" + description: "Quick 1-click toggle on the bar to suppress notification popups and banners" + checked: SettingsService.appletDnd + onCheckedChanged: { + SettingsService.appletDnd = checked; + } + } + } + + // BAR TRANSLUCENCY & GLASSMORPHISM + AppGroup { + title: "Bar Translucency & Glassmorphism" + icon: "sparkles" + + SettingSegmented { + label: "Background Mode" + description: "Choose between Solid, Frosted Glass, Translucent Gradient, or Wallpaper-adaptive tint" + model: [{ + "text": "Solid", + "value": "solid" + }, { + "text": "Frosted Glass", + "value": "blur" + }, { + "text": "Gradient", + "value": "gradient" + }, { + "text": "Adaptive Tint", + "value": "adaptive" + }] + currentValue: SettingsService.barGlassMode + onActivated: (value) => { + SettingsService.barGlassMode = value; + } + } + + Divider { + visible: SettingsService.barGlassMode !== "solid" + } + + ThemedSlider { + visible: SettingsService.barGlassMode !== "solid" + label: "Bar Opacity" + from: 20 + to: 100 + stepSize: 1 + value: SettingsService.barGlassOpacity + suffix: "%" + decimals: 0 + onMoved: (val) => { + SettingsService.barGlassOpacity = Math.round(val); + } + } + } +} diff --git a/quickshell/agility/Modules/Settings/Components/SettingsSidebar.qml b/quickshell/agility/Modules/Settings/Components/SettingsSidebar.qml index 708b047..7715721 100644 --- a/quickshell/agility/Modules/Settings/Components/SettingsSidebar.qml +++ b/quickshell/agility/Modules/Settings/Components/SettingsSidebar.qml @@ -10,6 +10,7 @@ AppSidebar { id: sidebarRoot property int activeTab: 0 + property bool isDesktopBarExpanded: activeTab === 1 || activeTab === 9 signal tabClicked(int index) @@ -73,8 +74,61 @@ AppSidebar { anchors.fill: parent iconName: "bar" labelText: "Desktop Bar" + hasChevron: true + isExpanded: sidebarRoot.isDesktopBarExpanded isActive: sidebarRoot.activeTab === 1 - onRowClicked: sidebarRoot.tabClicked(1) + onRowClicked: { + if (sidebarRoot.activeTab === 1) { + sidebarRoot.isDesktopBarExpanded = !sidebarRoot.isDesktopBarExpanded; + } else { + sidebarRoot.isDesktopBarExpanded = true; + sidebarRoot.tabClicked(1); + } + } + onChevronClicked: { + sidebarRoot.isDesktopBarExpanded = !sidebarRoot.isDesktopBarExpanded; + } + } + + } + + // Applets Dropdown Submenu (Tab 9) + ColumnLayout { + id: appletsSubmenuLayout + Layout.fillWidth: true + spacing: Constants.size2Xs + clip: true + visible: Layout.preferredHeight > 0 || sidebarRoot.isDesktopBarExpanded + Layout.preferredHeight: sidebarRoot.isDesktopBarExpanded ? 34 : 0 + opacity: sidebarRoot.isDesktopBarExpanded ? 1 : 0 + + Behavior on Layout.preferredHeight { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutCubic + } + } + + Behavior on opacity { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + + Item { + Layout.fillWidth: true + Layout.leftMargin: Constants.sizeSm + 16 + Layout.rightMargin: Constants.sizeSm + Layout.preferredHeight: 34 + + SidebarNavRow { + anchors.fill: parent + iconName: "widgets" + labelText: "Applets" + isActive: sidebarRoot.activeTab === 9 + onRowClicked: sidebarRoot.tabClicked(9) + } } } diff --git a/quickshell/agility/Modules/Settings/Settings.qml b/quickshell/agility/Modules/Settings/Settings.qml index ddeb44a..146c8eb 100644 --- a/quickshell/agility/Modules/Settings/Settings.qml +++ b/quickshell/agility/Modules/Settings/Settings.qml @@ -20,9 +20,9 @@ import qs.Modules.Settings.WindowSettings AppWindow { id: root - property var pageComponents: [personalizationComp, barSettingsComp, effectsComp, windowComp, integrationsComp, mouseComp, inputAndClipboardComp, systemInfoComp, wallpaperComp] + property var pageComponents: [personalizationComp, barSettingsComp, effectsComp, windowComp, integrationsComp, mouseComp, inputAndClipboardComp, systemInfoComp, wallpaperComp, barAppletsComp] property int activeTab: 0 - readonly property var navOrder: [0, 8, 1, 2, 3, 5, 6, 4, 7] + readonly property var navOrder: [0, 8, 1, 9, 2, 3, 5, 6, 4, 7] readonly property var tabMetadata: ({ "0": { "title": "Appearance", @@ -68,6 +68,11 @@ AppWindow { "title": "Wallpaper", "subtitle": "Screen crop preview, scaling modes, transitions, and shuffle", "category": "DESKTOP" + }, + "9": { + "title": "Bar Applets", + "subtitle": "Active window, media visualizer, weather, battery, and status pills", + "category": "DESKTOP" } }) readonly property var currentTabMeta: tabMetadata[activeTab] || tabMetadata[0] @@ -249,4 +254,13 @@ AppWindow { } + Component { + id: barAppletsComp + + BarAppletsSettings { + anchors.fill: parent + } + + } + } diff --git a/quickshell/agility/Scripts/system/fetch_weather.sh b/quickshell/agility/Scripts/system/fetch_weather.sh new file mode 100644 index 0000000..fed68c9 --- /dev/null +++ b/quickshell/agility/Scripts/system/fetch_weather.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -euo pipefail + +CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/agility-shell/weather" +CACHE_FILE="$CACHE_DIR/weather_data.json" +mkdir -p "$CACHE_DIR" + +FORCE_REFRESH="${1:-}" + +# Return cache if fresher than 15 minutes (900 seconds) +if [[ "$FORCE_REFRESH" != "--force" && -f "$CACHE_FILE" ]]; then + now=$(date +%s) + mod=$(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0) + if (( now - mod < 900 )); then + cat "$CACHE_FILE" + exit 0 + fi +fi + +# Geolocation lookup with timeout +GEO_JSON=$(curl -s --max-time 3 "https://ipwho.is/" 2>/dev/null || true) +CITY="" +LAT="" +LON="" + +if [[ -n "$GEO_JSON" ]]; then + CITY=$(echo "$GEO_JSON" | jq -r '.city // empty' 2>/dev/null || true) + LAT=$(echo "$GEO_JSON" | jq -r '.latitude // empty' 2>/dev/null || true) + LON=$(echo "$GEO_JSON" | jq -r '.longitude // empty' 2>/dev/null || true) +fi + +if [[ -z "$LAT" || -z "$LON" ]]; then + GEO_FALLBACK=$(curl -s --max-time 3 "https://ipapi.co/json/" 2>/dev/null || true) + if [[ -n "$GEO_FALLBACK" ]]; then + CITY=$(echo "$GEO_FALLBACK" | jq -r '.city // empty' 2>/dev/null || true) + LAT=$(echo "$GEO_FALLBACK" | jq -r '.latitude // empty' 2>/dev/null || true) + LON=$(echo "$GEO_FALLBACK" | jq -r '.longitude // empty' 2>/dev/null || true) + fi +fi + +if [[ -z "$LAT" || -z "$LON" ]]; then + if [[ -f "$CACHE_FILE" ]]; then + cat "$CACHE_FILE" + exit 0 + fi + echo '{"error": "geolocation_failed", "city": "Unknown", "temperature": 22, "apparent_temperature": 22, "humidity": 50, "wind_speed": 5, "weather_code": 0, "daily": []}' + exit 0 +fi + +[[ -z "$CITY" ]] && CITY="Local" + +FORECAST_JSON=$(curl -s --max-time 4 "https://api.open-meteo.com/v1/forecast?latitude=${LAT}&longitude=${LON}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto" 2>/dev/null || true) + +if [[ -z "$FORECAST_JSON" || "$(echo "$FORECAST_JSON" | jq -r '.error // empty' 2>/dev/null)" == "true" ]]; then + if [[ -f "$CACHE_FILE" ]]; then + cat "$CACHE_FILE" + exit 0 + fi + echo '{"error": "forecast_failed", "city": "'"$CITY"'", "temperature": 22, "apparent_temperature": 22, "humidity": 50, "wind_speed": 5, "weather_code": 0, "daily": []}' + exit 0 +fi + +OUTPUT=$(echo "$FORECAST_JSON" | jq --arg city "$CITY" '. as $root | { + city: $city, + temperature: ($root.current.temperature_2m | round), + apparent_temperature: ($root.current.apparent_temperature | round), + humidity: $root.current.relative_humidity_2m, + wind_speed: ($root.current.wind_speed_10m | round), + weather_code: $root.current.weather_code, + daily: [ + range(0; 3) | { + date: $root.daily.time[.], + code: $root.daily.weather_code[.], + max: ($root.daily.temperature_2m_max[.] | round), + min: ($root.daily.temperature_2m_min[.] | round) + } + ] +}' 2>/dev/null || true) + +if [[ -n "$OUTPUT" ]]; then + echo "$OUTPUT" > "$CACHE_FILE" + cat "$CACHE_FILE" +elif [[ -f "$CACHE_FILE" ]]; then + cat "$CACHE_FILE" +else + echo '{"error": "parse_failed", "city": "'"$CITY"'", "temperature": 22, "apparent_temperature": 22, "humidity": 50, "wind_speed": 5, "weather_code": 0, "daily": []}' +fi diff --git a/quickshell/agility/shell.qml b/quickshell/agility/shell.qml index 3903a96..7376afc 100644 --- a/quickshell/agility/shell.qml +++ b/quickshell/agility/shell.qml @@ -71,6 +71,7 @@ ShellRoot { readonly property int barMarginSide: BarStyleConfig.barMarginSide(SettingsService.barStyle) WlrLayershell.layer: WlrLayer.Top + WlrLayershell.exclusionMode: (SettingsService.barAutoHide && (SettingsService.barIslandMode || SettingsService.barPillMode)) ? ExclusionMode.Ignore : ExclusionMode.Auto color: "transparent" focusable: false implicitHeight: !SettingsService.settingsLoaded ? 0 : (barMarginTop + barHeight)