From fb68300746829574e682d39acfc8840e725449a2 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 5 Aug 2025 17:41:08 +0200 Subject: [PATCH] Settings rework... --- Bar/Bar.qml | 35 +- Bar/Modules/ActiveWindow.qml | 7 +- Bar/Modules/Applauncher.qml | 893 ++++++++++++------ Bar/Modules/Battery.qml | 42 +- Bar/Modules/Bluetooth.qml | 272 ++++++ Bar/Modules/Brightness.qml | 1 + Bar/Modules/ClockWidget.qml | 1 + Bar/Modules/CustomTrayMenu.qml | 474 ++++++++-- Bar/Modules/Media.qml | 10 +- Bar/Modules/SettingsButton.qml | 80 ++ Bar/Modules/SystemInfo.qml | 2 + Bar/Modules/SystemTray.qml | 68 +- Bar/Modules/Taskbar.qml | 5 +- Bar/Modules/Volume.qml | 3 +- Bar/Modules/Wifi.qml | 370 ++++++++ Bar/Modules/Workspace.qml | 16 +- Components/Avatar.qml | 51 + Components/CircularProgressBar.qml | 22 +- Components/StyledTooltip.qml | 45 +- Helpers/IPCHandlers.qml | 16 +- Helpers/IdleInhibitor.qml | 6 +- Helpers/Time.js | 18 + README.md | 8 + Services/MusicManager.qml | 33 +- Services/Network.qml | 348 +++++++ Services/WallpaperManager.qml | 13 +- Settings/Settings.qml | 17 + Widgets/Background.qml | 1 + Widgets/Dock.qml | 350 +++++++ Widgets/LockScreen/BatteryCharge.qml | 6 +- Widgets/LockScreen/LockScreen.qml | 68 +- Widgets/Notification/NotificationHistory.qml | 2 +- Widgets/Notification/NotificationIcon.qml | 9 +- Widgets/Notification/NotificationManager.qml | 4 +- Widgets/Notification/NotificationPopup.qml | 22 +- Widgets/Overview.qml | 10 +- Widgets/SettingsWindow/SettingsWindow.qml | 369 ++++++++ Widgets/SettingsWindow/Tabs/About.qml | 405 ++++++++ Widgets/SettingsWindow/Tabs/Bar.qml | 380 ++++++++ .../Tabs/Components/UnitSelector.qml | 97 ++ Widgets/SettingsWindow/Tabs/Display.qml | 354 +++++++ Widgets/SettingsWindow/Tabs/General.qml | 339 +++++++ Widgets/SettingsWindow/Tabs/Misc.qml | 137 +++ Widgets/SettingsWindow/Tabs/Network.qml | 193 ++++ Widgets/SettingsWindow/Tabs/Record.qml | 19 + Widgets/SettingsWindow/Tabs/Recording.qml | 812 ++++++++++++++++ Widgets/SettingsWindow/Tabs/TimeWeather.qml | 283 ++++++ Widgets/Sidebar/Config/ProfileSettings.qml | 127 ++- Widgets/Sidebar/Config/SettingsModal.qml | 82 +- Widgets/Sidebar/Config/WallpaperSettings.qml | 271 ++++-- Widgets/Sidebar/Config/WeatherSettings.qml | 10 +- Widgets/Sidebar/Panel/BluetoothPanel.qml | 6 +- Widgets/Sidebar/Panel/Music.qml | 165 +++- Widgets/Sidebar/Panel/PanelPopup.qml | 41 +- Widgets/Sidebar/Panel/PowerProfile.qml | 6 +- Widgets/Sidebar/Panel/QuickAccess.qml | 14 +- Widgets/Sidebar/Panel/System.qml | 87 +- Widgets/Sidebar/Panel/SystemMonitor.qml | 13 +- Widgets/Sidebar/Panel/WallpaperPanel.qml | 29 +- Widgets/Sidebar/Panel/Weather.qml | 38 +- Widgets/Sidebar/Panel/WifiPanel.qml | 505 +++++++--- qmlls.ini | 0 shell.qml | 55 +- 63 files changed, 7139 insertions(+), 1026 deletions(-) create mode 100644 Bar/Modules/Bluetooth.qml create mode 100644 Bar/Modules/SettingsButton.qml create mode 100644 Bar/Modules/Wifi.qml create mode 100644 Components/Avatar.qml create mode 100644 Helpers/Time.js create mode 100644 Services/Network.qml create mode 100644 Widgets/Dock.qml create mode 100644 Widgets/SettingsWindow/SettingsWindow.qml create mode 100644 Widgets/SettingsWindow/Tabs/About.qml create mode 100644 Widgets/SettingsWindow/Tabs/Bar.qml create mode 100644 Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml create mode 100644 Widgets/SettingsWindow/Tabs/Display.qml create mode 100644 Widgets/SettingsWindow/Tabs/General.qml create mode 100644 Widgets/SettingsWindow/Tabs/Misc.qml create mode 100644 Widgets/SettingsWindow/Tabs/Network.qml create mode 100644 Widgets/SettingsWindow/Tabs/Record.qml create mode 100644 Widgets/SettingsWindow/Tabs/Recording.qml create mode 100644 Widgets/SettingsWindow/Tabs/TimeWeather.qml create mode 100644 qmlls.ini diff --git a/Bar/Bar.qml b/Bar/Bar.qml index 986bacc..8d355ba 100644 --- a/Bar/Bar.qml +++ b/Bar/Bar.qml @@ -4,7 +4,7 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Wayland -import Qt5Compat.GraphicalEffects +import QtQuick.Effects import qs.Bar.Modules import qs.Settings import qs.Services @@ -15,6 +15,7 @@ import qs.Widgets.Sidebar import qs.Widgets.Sidebar.Panel import qs.Widgets.Notification +// Main bar component - creates panels on selected monitors with widgets and corners Scope { id: rootScope property var shell @@ -38,7 +39,8 @@ Scope { anchors.left: true anchors.right: true - visible: true + visible: Settings.settings.barMonitors.includes(modelData.name) || + (Settings.settings.barMonitors.length === 0) Rectangle { id: barBackground @@ -103,6 +105,14 @@ Scope { anchors.verticalCenter: parent.verticalCenter } + Wifi { + anchors.verticalCenter: parent.verticalCenter + } + + Bluetooth { + anchors.verticalCenter: parent.verticalCenter + } + Battery { id: widgetsBattery anchors.verticalCenter: parent.verticalCenter @@ -110,6 +120,7 @@ Scope { Brightness { id: widgetsBrightness + screen: modelData anchors.verticalCenter: parent.verticalCenter } @@ -124,6 +135,10 @@ Scope { anchors.verticalCenter: parent.verticalCenter } + SettingsButton { + anchors.verticalCenter: parent.verticalCenter + } + PanelPopup { id: sidebarPopup } @@ -149,7 +164,8 @@ Scope { screen: modelData margins.top: 36 WlrLayershell.exclusionMode: ExclusionMode.Ignore - visible: true + visible: Settings.settings.barMonitors.includes(modelData.name) || + (Settings.settings.barMonitors.length === 0) WlrLayershell.layer: WlrLayer.Background aboveWindows: false WlrLayershell.namespace: "swww-daemon" @@ -175,7 +191,8 @@ Scope { screen: modelData margins.top: 36 WlrLayershell.exclusionMode: ExclusionMode.Ignore - visible: true + visible: Settings.settings.barMonitors.includes(modelData.name) || + (Settings.settings.barMonitors.length === 0) WlrLayershell.layer: WlrLayer.Background aboveWindows: false WlrLayershell.namespace: "swww-daemon" @@ -201,7 +218,8 @@ Scope { color: "transparent" screen: modelData WlrLayershell.exclusionMode: ExclusionMode.Ignore - visible: true + visible: Settings.settings.barMonitors.includes(modelData.name) || + (Settings.settings.barMonitors.length === 0) WlrLayershell.layer: WlrLayer.Background aboveWindows: false WlrLayershell.namespace: "swww-daemon" @@ -227,7 +245,8 @@ Scope { color: "transparent" screen: modelData WlrLayershell.exclusionMode: ExclusionMode.Ignore - visible: true + visible: Settings.settings.barMonitors.includes(modelData.name) || + (Settings.settings.barMonitors.length === 0) WlrLayershell.layer: WlrLayer.Background aboveWindows: false WlrLayershell.namespace: "swww-daemon" @@ -249,6 +268,6 @@ Scope { } } - // This alias exposes the visual bar's visibility to the outside world + property alias visible: barRootItem.visible -} \ No newline at end of file +} diff --git a/Bar/Modules/ActiveWindow.qml b/Bar/Modules/ActiveWindow.qml index c01d58d..cd6ab68 100644 --- a/Bar/Modules/ActiveWindow.qml +++ b/Bar/Modules/ActiveWindow.qml @@ -1,9 +1,9 @@ import QtQuick import Quickshell -import qs.Components -import qs.Settings import Quickshell.Wayland import Quickshell.Widgets +import qs.Components +import qs.Settings PanelWindow { id: activeWindowPanel @@ -14,7 +14,7 @@ PanelWindow { anchors.right: true focusable: false margins.top: barHeight - visible: !activeWindowWrapper.finallyHidden + visible: Settings.settings.showActiveWindow && !activeWindowWrapper.finallyHidden implicitHeight: activeWindowTitleContainer.height implicitWidth: 0 property int barHeight: 36 @@ -121,6 +121,7 @@ PanelWindow { source: ToplevelManager?.activeToplevel ? getIcon() : "" visible: Settings.settings.showActiveWindowIcon anchors.verticalCenterOffset: -3 + } Text { diff --git a/Bar/Modules/Applauncher.qml b/Bar/Modules/Applauncher.qml index 4536c15..a02ee37 100644 --- a/Bar/Modules/Applauncher.qml +++ b/Bar/Modules/Applauncher.qml @@ -3,18 +3,159 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets import qs.Components import qs.Settings -import Quickshell.Wayland + import "../../Helpers/Fuzzysort.js" as Fuzzysort PanelWithOverlay { + Timer { + id: clipboardTimer + interval: 1000 + repeat: true + running: appLauncherPanel.visible && searchField.text.startsWith(">clip") + onTriggered: { + updateClipboardHistory(); + } + } + + property var clipboardHistory: [] + property bool clipboardInitialized: false + + Process { + id: clipboardTypeProcess + property bool isLoading: false + property var currentTypes: [] + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + currentTypes = String(stdout.text).trim().split('\n').filter(t => t); + + const imageType = currentTypes.find(t => t.startsWith('image/')); + if (imageType) { + clipboardImageProcess.mimeType = imageType; + clipboardImageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`]; + clipboardImageProcess.running = true; + } else { + + clipboardHistoryProcess.command = ["wl-paste", "-n", "--type", "text/plain"]; + clipboardHistoryProcess.running = true; + } + } else { + + clipboardTypeProcess.isLoading = false; + } + } + + stdout: StdioCollector {} + } + + Process { + id: clipboardImageProcess + property string mimeType: "" + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const base64 = stdout.text.trim(); + if (base64) { + const entry = { + type: 'image', + mimeType: mimeType, + data: `data:${mimeType};base64,${base64}`, + timestamp: new Date().getTime() + }; + + + const exists = clipboardHistory.find(item => + item.type === 'image' && item.data === entry.data + ); + + if (!exists) { + clipboardHistory = [entry, ...clipboardHistory].slice(0, 20); + root.updateFilter(); + } + } + } + + if (!clipboardHistoryProcess.isLoading) { + clipboardInitialized = true; + } + clipboardTypeProcess.isLoading = false; + } + + stdout: StdioCollector {} + } + + Process { + id: clipboardHistoryProcess + property bool isLoading: false + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const content = String(stdout.text).trim(); + if (content && !content.startsWith("vscode-file://")) { + const entry = { + type: 'text', + content: content, + timestamp: new Date().getTime() + }; + + + const exists = clipboardHistory.find(item => { + if (item.type === 'text') { + return item.content === content; + } + + return item === content; + }); + + if (!exists) { + + const newHistory = clipboardHistory.map(item => { + if (typeof item === 'string') { + return { + type: 'text', + content: item, + timestamp: new Date().getTime() + }; + } + return item; + }); + + clipboardHistory = [entry, ...newHistory].slice(0, 20); + } + } + } else { + + clipboardHistoryProcess.isLoading = false; + } + clipboardInitialized = true; + clipboardTypeProcess.isLoading = false; + root.updateFilter(); + } + + stdout: StdioCollector {} + } + + + + function updateClipboardHistory() { + if (!clipboardTypeProcess.isLoading && !clipboardHistoryProcess.isLoading) { + clipboardTypeProcess.isLoading = true; + clipboardTypeProcess.command = ["wl-paste", "-l"]; + clipboardTypeProcess.running = true; + } + } + id: appLauncherPanel WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand function isPinned(app) { return app && app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1; } + function togglePin(app) { if (!app || !app.execString) return; var arr = Settings.settings.pinnedExecs ? Settings.settings.pinnedExecs.slice() : []; @@ -101,9 +242,11 @@ PanelWithOverlay { appLauncherPanel.visible = false; } } + function isMathExpression(str) { return /^[-+*/().0-9\s]+$/.test(str); } + function safeEval(expr) { try { return Function('return (' + expr + ')')(); @@ -111,13 +254,114 @@ PanelWithOverlay { return undefined; } } + function updateFilter() { var query = searchField.text ? searchField.text.toLowerCase() : ""; var apps = root.appModel.slice(); var results = []; - // Calculator mode: starts with '=' - if (query.startsWith("=")) { - var expr = searchField.text.slice(1).trim(); + + + if (query === ">") { + results.push({ + isCommand: true, + name: ">calc", + content: "Calculator - evaluate mathematical expressions", + icon: "calculate", + execute: function() { + searchField.text = ">calc "; + searchField.cursorPosition = searchField.text.length; + } + }); + + results.push({ + isCommand: true, + name: ">clip", + content: "Clipboard history - browse and restore clipboard items", + icon: "content_paste", + execute: function() { + searchField.text = ">clip "; + searchField.cursorPosition = searchField.text.length; + } + }); + + root.filteredApps = results; + return; + } + + + if (query.startsWith(">clip")) { + if (!clipboardInitialized) { + updateClipboardHistory(); + } + const searchTerm = query.slice(5).trim(); + + clipboardHistory.forEach(function(clip, index) { + let searchContent = clip.type === 'image' ? + clip.mimeType : + clip.content || clip; // Support both new object format and old string format + + if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) { + let entry; + if (clip.type === 'image') { + entry = { + isClipboard: true, + name: "Image from " + new Date(clip.timestamp).toLocaleTimeString(), + content: "Image: " + clip.mimeType, + icon: "image", + type: 'image', + data: clip.data, + execute: function() { + // Convert base64 image data back to binary and copy to clipboard + const base64Data = clip.data.split(',')[1]; + clipboardTypeProcess.command = ["sh", "-c", `echo '${base64Data}' | base64 -d | wl-copy -t '${clip.mimeType}'`]; + clipboardTypeProcess.running = true; + } + }; + } else { + const textContent = clip.content || clip; // Support both new object format and old string format + let displayContent = textContent; + let previewContent = ""; + + // Clean up whitespace for display + displayContent = displayContent.replace(/\s+/g, ' ').trim(); + + // Truncate long content and show preview + if (displayContent.length > 50) { + previewContent = displayContent; + // Show first line or first 50 characters as title + displayContent = displayContent.split('\n')[0].substring(0, 50) + "..."; + } + + entry = { + isClipboard: true, + name: displayContent, + content: previewContent || textContent, + icon: "content_paste", + execute: function() { + Quickshell.execDetached(["sh", "-c", "echo -n '" + textContent.replace(/'/g, "'\\''") + "' | wl-copy"]); + } + }; + } + results.push(entry); + } + }); + + if (results.length === 0) { + results.push({ + isClipboard: true, + name: "No clipboard history", + content: "No matching clipboard entries found", + icon: "content_paste_off" + }); + } + + root.filteredApps = results; + return; + } + + + if (query.startsWith(">calc")) { + var expr = searchField.text.slice(5).trim(); if (expr && isMathExpression(expr)) { var value = safeEval(expr); if (value !== undefined && value !== null && value !== "") { @@ -130,8 +374,27 @@ PanelWithOverlay { }); } } + + + var pinned = []; + var unpinned = []; + for (var i = 0; i < results.length; ++i) { + var app = results[i]; + if (app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1) { + pinned.push(app); + } else { + unpinned.push(app); + } + } + // Sort pinned apps alphabetically for consistent display + pinned.sort(function(a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); + root.filteredApps = pinned.concat(unpinned); + root.selectedIndex = 0; + return; } - if (!query || query.startsWith("=")) { + if (!query) { results = results.concat(apps.sort(function (a, b) { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); })); @@ -143,7 +406,7 @@ PanelWithOverlay { return r.obj; })); } - // Pinning logic: split into pinned and unpinned + var pinned = []; var unpinned = []; for (var i = 0; i < results.length; ++i) { @@ -161,10 +424,12 @@ PanelWithOverlay { root.filteredApps = pinned.concat(unpinned); root.selectedIndex = 0; } + function selectNext() { if (filteredApps.length > 0) selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1); } + function selectPrev() { if (filteredApps.length > 0) selectedIndex = Math.max(selectedIndex - 1, 0); @@ -182,6 +447,10 @@ PanelWithOverlay { Quickshell.clipboardText = String(modelData.result); Quickshell.execDetached(["notify-send", "Calculator Result", `${modelData.expr} = ${modelData.result} (copied to clipboard)`]); }); + } else if (modelData.isCommand) { + + modelData.execute(); + return; } else if (modelData.runInTerminal && termEmu){ Quickshell.execDetached([termEmu, "-e", modelData.execString.trim()]); } else if (modelData.execute) { @@ -200,310 +469,382 @@ PanelWithOverlay { Component.onCompleted: updateFilter() - ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom + RowLayout { + anchors.fill: parent anchors.margins: 32 spacing: 18 - // Search Bar + Rectangle { - id: searchBar - color: Theme.surfaceVariant - radius: 22 - height: 48 - Layout.fillWidth: true - border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: searchField.activeFocus ? 2 : 1 + id: previewPanel + Layout.preferredWidth: 200 + Layout.fillHeight: true + color: Theme.surface + radius: 20 + visible: false - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 14 - anchors.rightMargin: 14 - spacing: 10 - Text { - text: "search" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeHeader - color: searchField.activeFocus ? Theme.accentPrimary : Theme.textSecondary - verticalAlignment: Text.AlignVCenter - Layout.alignment: Qt.AlignVCenter - } - TextField { - id: searchField - placeholderText: "Search apps..." - color: Theme.textPrimary - placeholderTextColor: Theme.textSecondary - background: null - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeBody - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - onTextChanged: root.updateFilter() - selectedTextColor: Theme.onAccent - selectionColor: Theme.accentPrimary - padding: 0 - verticalAlignment: TextInput.AlignVCenter - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 - font.bold: true - Component.onCompleted: contentItem.cursorColor = Theme.textPrimary - onActiveFocusChanged: contentItem.cursorColor = Theme.textPrimary + Rectangle { + anchors.fill: parent + anchors.margins: 16 + color: "transparent" + clip: true - Keys.onDownPressed: root.selectNext() - Keys.onUpPressed: root.selectPrev() - Keys.onEnterPressed: root.activateSelected() - Keys.onReturnPressed: root.activateSelected() - Keys.onEscapePressed: appLauncherPanel.hidePanel() - } - } - Behavior on border.color { - ColorAnimation { - duration: 120 - } - } - Behavior on border.width { - NumberAnimation { - duration: 120 + Image { + id: previewImage + anchors.fill: parent + fillMode: Image.PreserveAspectFit + asynchronous: true + cache: true + smooth: true } } } - // App List Card - Rectangle { - color: Theme.surface - radius: 20 + + ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - clip: true - property int innerPadding: 16 + spacing: 18 - Item { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - height: parent.innerPadding - visible: false + + Rectangle { + id: searchBar + color: Theme.surfaceVariant + radius: 20 + height: 48 + Layout.fillWidth: true + border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: searchField.activeFocus ? 2 : 1 + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 14 + anchors.rightMargin: 14 + spacing: 10 + + Text { + text: "search" + font.family: "Material Symbols Outlined" + font.pixelSize: Theme.fontSizeHeader + color: searchField.activeFocus ? Theme.accentPrimary : Theme.textSecondary + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignVCenter + } + + TextField { + id: searchField + placeholderText: "Search apps..." + color: Theme.textPrimary + placeholderTextColor: Theme.textSecondary + background: null + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeBody + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + onTextChanged: root.updateFilter() + selectedTextColor: Theme.onAccent + selectionColor: Theme.accentPrimary + padding: 0 + verticalAlignment: TextInput.AlignVCenter + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + font.bold: true + Component.onCompleted: contentItem.cursorColor = Theme.textPrimary + onActiveFocusChanged: contentItem.cursorColor = Theme.textPrimary + + Keys.onDownPressed: root.selectNext() + Keys.onUpPressed: root.selectPrev() + Keys.onEnterPressed: root.activateSelected() + Keys.onReturnPressed: root.activateSelected() + Keys.onEscapePressed: appLauncherPanel.hidePanel() + } + } + + Behavior on border.color { + ColorAnimation { + duration: 120 + } + } + + Behavior on border.width { + NumberAnimation { + duration: 120 + } + } } - ListView { - id: appList - anchors.fill: parent - anchors.margins: parent.innerPadding - spacing: 2 - model: root.filteredApps - currentIndex: root.selectedIndex - delegate: Item { - id: appDelegate - width: appList.width - height: 48 - property bool hovered: mouseArea.containsMouse - property bool isSelected: index === root.selectedIndex + + Rectangle { + color: Theme.surface + radius: 20 + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + property int innerPadding: 16 - Rectangle { - anchors.fill: parent - color: (hovered || isSelected) - ? Theme.accentPrimary - : (appLauncherPanel.isPinned(modelData) ? Theme.surfaceVariant : "transparent") - radius: 12 - border.color: appLauncherPanel.isPinned(modelData) - ? "transparent" - : (hovered || isSelected ? Theme.accentPrimary : "transparent") - border.width: appLauncherPanel.isPinned(modelData) ? 0 : (hovered || isSelected ? 2 : 0) - Behavior on color { - ColorAnimation { - duration: 120 - } - } - Behavior on border.color { - ColorAnimation { - duration: 120 - } - } - Behavior on border.width { - NumberAnimation { - duration: 120 - } - } - } + ListView { + id: appList + anchors.fill: parent + anchors.margins: parent.innerPadding + spacing: 2 + model: root.filteredApps + currentIndex: root.selectedIndex + delegate: Item { + id: appDelegate + width: appList.width + height: (modelData.isClipboard || modelData.isCommand) ? 64 : 48 + property bool hovered: mouseArea.containsMouse + property bool isSelected: index === root.selectedIndex - RowLayout { - anchors.fill: parent - anchors.leftMargin: 10 - anchors.rightMargin: 10 - spacing: 10 + Rectangle { + anchors.fill: parent + color: (hovered || isSelected) + ? Theme.accentPrimary + : (appLauncherPanel.isPinned(modelData) ? Theme.surfaceVariant : "transparent") + radius: 12 + border.color: appLauncherPanel.isPinned(modelData) + ? "transparent" + : (hovered || isSelected ? Theme.accentPrimary : "transparent") + border.width: appLauncherPanel.isPinned(modelData) ? 0 : (hovered || isSelected ? 2 : 0) + + Behavior on color { + ColorAnimation { + duration: 120 + } + } + + Behavior on border.color { + ColorAnimation { + duration: 120 + } + } + + Behavior on border.width { + NumberAnimation { + duration: 120 + } + } + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + spacing: 10 + + Item { + width: 28 + height: 28 + property bool iconLoaded: !modelData.isCalculator && !modelData.isClipboard && !modelData.isCommand && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error + + Image { + id: clipboardImage + anchors.fill: parent + visible: modelData.type === 'image' + source: modelData.data || "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + + MouseArea { + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + onContainsMouseChanged: { + if (containsMouse && modelData.type === 'image') { + previewImage.source = modelData.data; + previewPanel.visible = true; + } else { + previewPanel.visible = false; + } + } + onMouseXChanged: mouse.accepted = false + onMouseYChanged: mouse.accepted = false + onClicked: mouse.accepted = false + } + } + + IconImage { + id: iconImg + anchors.fill: parent + asynchronous: true + source: modelData.isCalculator ? "qrc:/icons/calculate.svg" : + modelData.isClipboard ? "qrc:/icons/" + modelData.icon + ".svg" : + modelData.isCommand ? "qrc:/icons/" + modelData.icon + ".svg" : + Quickshell.iconPath(modelData.icon, "application-x-executable") + visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded) && modelData.type !== 'image' + } + + Text { + anchors.centerIn: parent + visible: !modelData.isCalculator && !modelData.isClipboard && !modelData.isCommand && !parent.iconLoaded && modelData.type !== 'image' + text: "broken_image" + font.family: "Material Symbols Outlined" + font.pixelSize: Theme.fontSizeHeader + color: Theme.accentPrimary + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 1 + + Text { + text: modelData.name + color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textPrimary) + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.bold: hovered || isSelected + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : + modelData.isClipboard ? modelData.content : + modelData.isCommand ? modelData.content : + (modelData.comment || modelData.genericName || "No description available") + color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textSecondary : Theme.textSecondary) + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeCaption + font.italic: !(modelData.comment || modelData.genericName) + opacity: modelData.isClipboard ? 0.8 : modelData.isCommand ? 0.9 : ((modelData.comment || modelData.genericName) ? 1.0 : 0.6) + elide: Text.ElideRight + maximumLineCount: (modelData.isClipboard || modelData.isCommand) ? 2 : 1 + wrapMode: (modelData.isClipboard || modelData.isCommand) ? Text.WordWrap : Text.NoWrap + Layout.fillWidth: true + Layout.preferredHeight: (modelData.isClipboard || modelData.isCommand) ? implicitHeight : contentHeight + } + } + + Item { + Layout.fillWidth: true + } + + Text { + text: modelData.isCalculator ? "content_copy" : "chevron_right" + font.family: "Material Symbols Outlined" + font.pixelSize: Theme.fontSizeBody + color: (hovered || isSelected) + ? Theme.onAccent + : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textSecondary) + verticalAlignment: Text.AlignVCenter + Layout.rightMargin: 8 + } + + + Item { width: 8; height: 1 } + } + + Rectangle { + id: ripple + anchors.fill: parent + color: Theme.onAccent + opacity: 0.0 + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: { + + if (pinArea.containsMouse) return; + if (mouse.button === Qt.RightButton) { + appLauncherPanel.togglePin(modelData); + return; + } + ripple.opacity = 0.18; + rippleNumberAnimation.start(); + root.selectedIndex = index; + root.activateSelected(); + } + cursorShape: Qt.PointingHandCursor + onPressed: ripple.opacity = 0.18 + onReleased: ripple.opacity = 0.0 + } + + NumberAnimation { + id: rippleNumberAnimation + target: ripple + property: "opacity" + to: 0.0 + duration: 320 + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 1 + color: Theme.outline + opacity: index === appList.count - 1 ? 0 : 0.10 + } + + Item { - width: 28 - height: 28 - property bool iconLoaded: !modelData.isCalculator && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error - Image { - id: iconImg + id: pinArea + width: 28; height: 28 + z: 100 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + MouseArea { anchors.fill: parent - fillMode: Image.PreserveAspectFit - smooth: true - cache: false - asynchronous: true - source: modelData.isCalculator ? "qrc:/icons/calculate.svg" : Quickshell.iconPath(modelData.icon, "application-x-executable") - visible: modelData.isCalculator || parent.iconLoaded + preventStealing: true + z: 100 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + propagateComposedEvents: false + onClicked: { + appLauncherPanel.togglePin(modelData); + event.accepted = true; + } } + Text { anchors.centerIn: parent - visible: !modelData.isCalculator && !parent.iconLoaded - text: "broken_image" + text: "star" font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeHeader - color: Theme.accentPrimary - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 1 - Text { - text: modelData.name - color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textPrimary) - font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall - font.bold: hovered || isSelected + color: (parent.MouseArea.containsMouse || hovered || isSelected) + ? Theme.onAccent + : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textDisabled) verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - Layout.fillWidth: true } - Text { - text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : (modelData.comment || modelData.genericName || "No description available") - color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textSecondary : Theme.textSecondary) - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeCaption - font.italic: !(modelData.comment || modelData.genericName) - opacity: (modelData.comment || modelData.genericName) ? 1.0 : 0.6 - elide: Text.ElideRight - Layout.fillWidth: true - } - } - - Item { - Layout.fillWidth: true - } - Text { - text: modelData.isCalculator ? "content_copy" : "chevron_right" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody - color: (hovered || isSelected) - ? Theme.onAccent - : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textSecondary) - verticalAlignment: Text.AlignVCenter - Layout.rightMargin: 8 // Add margin to separate from star - } - // Add a spacing item between chevron and star - Item { width: 8; height: 1 } - } - - Rectangle { - id: ripple - anchors.fill: parent - color: Theme.onAccent - opacity: 0.0 - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: { - // Prevent app launch if click is inside pinArea - if (pinArea.containsMouse) return; - if (mouse.button === Qt.RightButton) { - appLauncherPanel.togglePin(modelData); - return; - } - ripple.opacity = 0.18; - rippleNumberAnimation.start(); - root.selectedIndex = index; - root.activateSelected(); - } - cursorShape: Qt.PointingHandCursor - onPressed: ripple.opacity = 0.18 - onReleased: ripple.opacity = 0.0 - } - - NumberAnimation { - id: rippleNumberAnimation - target: ripple - property: "opacity" - to: 0.0 - duration: 320 - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 1 - color: Theme.outline - opacity: index === appList.count - 1 ? 0 : 0.10 - } - // Pin/Unpin button (move to last child for stacking) - Item { - id: pinArea - width: 28; height: 28 - z: 100 // Ensure above everything else - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - MouseArea { - anchors.fill: parent - preventStealing: true - z: 100 - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton - propagateComposedEvents: false - onClicked: { - appLauncherPanel.togglePin(modelData); - event.accepted = true; - } - } - Text { - anchors.centerIn: parent - text: "star" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeSmall - color: (parent.MouseArea.containsMouse || hovered || isSelected) - ? Theme.onAccent - : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textDisabled) - verticalAlignment: Text.AlignVCenter } } } } } } - } - Corners { - id: launcherCornerRight - position: "bottomleft" - size: 1.1 - fillColor: Theme.backgroundPrimary - anchors.top: root.top - offsetX: 416 - offsetY: 0 - } + Corners { + id: launcherCornerRight + position: "bottomleft" + size: 1.1 + fillColor: Theme.backgroundPrimary + anchors.top: root.top + offsetX: 416 + offsetY: 0 + } - Corners { - id: launcherCornerLeft - position: "bottomright" - size: 1.1 - fillColor: Theme.backgroundPrimary - anchors.top: root.top - offsetX: -416 - offsetY: 0 + Corners { + id: launcherCornerLeft + position: "bottomright" + size: 1.1 + fillColor: Theme.backgroundPrimary + anchors.top: root.top + offsetX: -416 + offsetY: 0 + } } } } \ No newline at end of file diff --git a/Bar/Modules/Battery.qml b/Bar/Modules/Battery.qml index af41d66..03af9c3 100644 --- a/Bar/Modules/Battery.qml +++ b/Bar/Modules/Battery.qml @@ -4,6 +4,7 @@ import Quickshell.Services.UPower import QtQuick.Layouts import qs.Components import qs.Settings +import "../../Helpers/Time.js" as Time Item { id: batteryWidget @@ -73,19 +74,40 @@ Item { } StyledTooltip { id: batteryTooltip + positionAbove: false text: { let lines = []; - if (batteryWidget.isReady) { + if (!batteryWidget.isReady) { + return ""; + } + + if (batteryWidget.battery.timeToEmpty > 0) { + lines.push("Time left: " + Time.formatVagueHumanReadableTime(batteryWidget.battery.timeToEmpty)); + } + + if (batteryWidget.battery.timeToFull > 0) { + lines.push("Time until full: " + Time.formatVagueHumanReadableTime(batteryWidget.battery.timeToFull)); + } + + if (batteryWidget.battery.changeRate !== undefined) { + const rate = batteryWidget.battery.changeRate; + if (rate > 0) { + lines.push(batteryWidget.charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(2) + " W"); + } + else if (rate < 0) { + lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W"); + } + else { + lines.push("Estimating..."); + } + } + else { lines.push(batteryWidget.charging ? "Charging" : "Discharging"); - lines.push(Math.round(batteryWidget.percent) + "%"); - if (batteryWidget.battery.changeRate !== undefined) - lines.push("Rate: " + batteryWidget.battery.changeRate.toFixed(2) + " W"); - if (batteryWidget.battery.timeToEmpty > 0) - lines.push("Time left: " + Math.floor(batteryWidget.battery.timeToEmpty / 60) + " min"); - if (batteryWidget.battery.timeToFull > 0) - lines.push("Time to full: " + Math.floor(batteryWidget.battery.timeToFull / 60) + " min"); - if (batteryWidget.battery.healthPercentage !== undefined) - lines.push("Health: " + Math.round(batteryWidget.battery.healthPercentage) + "%"); + } + + + if (batteryWidget.battery.healthPercentage !== undefined && batteryWidget.battery.healthPercentage > 0) { + lines.push("Health: " + Math.round(batteryWidget.battery.healthPercentage) + "%"); } return lines.join("\n"); } diff --git a/Bar/Modules/Bluetooth.qml b/Bar/Modules/Bluetooth.qml new file mode 100644 index 0000000..80beb61 --- /dev/null +++ b/Bar/Modules/Bluetooth.qml @@ -0,0 +1,272 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Bluetooth +import qs.Settings +import qs.Components + +Item { + id: root + width: Settings.settings.bluetoothEnabled ? 22 : 0 + height: Settings.settings.bluetoothEnabled ? 22 : 0 + + property bool menuVisible: false + + // Bluetooth icon/button + Item { + id: bluetoothIcon + width: 22; height: 22 + visible: Settings.settings.bluetoothEnabled + + // Check if any devices are currently connected + property bool hasConnectedDevices: { + if (!Bluetooth.defaultAdapter) return false; + + for (let i = 0; i < Bluetooth.defaultAdapter.devices.count; i++) { + if (Bluetooth.defaultAdapter.devices.valueAt(i).connected) { + return true; + } + } + return false; + } + + Text { + id: bluetoothText + anchors.centerIn: parent + text: { + if (!Bluetooth.defaultAdapter || !Bluetooth.defaultAdapter.enabled) { + return "bluetooth_disabled" + } else if (parent.hasConnectedDevices) { + return "bluetooth_connected" + } else { + return "bluetooth" + } + } + font.family: mouseAreaBluetooth.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" + font.pixelSize: 16 + color: mouseAreaBluetooth.containsMouse ? Theme.accentPrimary : Theme.textPrimary + } + + MouseArea { + id: mouseAreaBluetooth + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + bluetoothMenu.visible = !bluetoothMenu.visible; + // Enable adapter and start discovery when menu opens + if (bluetoothMenu.visible && Bluetooth.defaultAdapter) { + if (!Bluetooth.defaultAdapter.enabled) { + Bluetooth.defaultAdapter.enabled = true; + } + if (!Bluetooth.defaultAdapter.discovering) { + Bluetooth.defaultAdapter.discovering = true; + } + } + } + onEntered: bluetoothTooltip.tooltipVisible = true + onExited: bluetoothTooltip.tooltipVisible = false + } + } + + StyledTooltip { + id: bluetoothTooltip + text: "Bluetooth Devices" + positionAbove: false + tooltipVisible: false + targetItem: bluetoothIcon + delay: 200 + } + + PanelWindow { + id: bluetoothMenu + implicitWidth: 320 + implicitHeight: 480 + visible: false + color: "transparent" + anchors.top: true + anchors.right: true + margins.right: 0 + margins.top: 0 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + onVisibleChanged: { + // Stop discovery when menu closes to save battery + if (!visible && Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering) { + Bluetooth.defaultAdapter.discovering = false; + } + } + + Rectangle { + anchors.fill: parent + color: Theme.backgroundPrimary + radius: 12 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 16 + + RowLayout { + Layout.fillWidth: true + spacing: 12 + + Text { + text: "bluetooth" + font.family: "Material Symbols Outlined" + font.pixelSize: 24 + color: Theme.accentPrimary + } + + Text { + text: "Bluetooth Devices" + font.pixelSize: 18 + font.bold: true + color: Theme.textPrimary + Layout.fillWidth: true + } + + IconButton { + icon: "close" + onClicked: { + bluetoothMenu.visible = false; + if (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering) { + Bluetooth.defaultAdapter.discovering = false; + } + } + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: Theme.outline + opacity: 0.12 + } + + ListView { + id: deviceList + Layout.fillWidth: true + Layout.fillHeight: true + model: Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.devices : [] + spacing: 8 + clip: true + + delegate: Item { + width: parent.width + height: 48 + + Rectangle { + anchors.fill: parent + radius: 8 + color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.44) : (deviceMouseArea.containsMouse ? Theme.highlight : "transparent") + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 + + Text { + text: modelData.connected ? "bluetooth" : "bluetooth_disabled" + font.family: "Material Symbols Outlined" + font.pixelSize: 18 + color: deviceMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary) + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Text { + text: { + let deviceName = modelData.name || modelData.deviceName || "Unknown Device"; + // Hide MAC addresses and show "Unknown Device" instead + let macPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; + if (macPattern.test(deviceName)) { + return "Unknown Device"; + } + return deviceName; + } + color: deviceMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textPrimary) + font.pixelSize: 14 + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: { + let deviceName = modelData.name || modelData.deviceName || ""; + let macPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; + if (macPattern.test(deviceName)) { + // Show MAC address in subtitle for unnamed devices + return modelData.address + " • " + (modelData.paired ? "Paired" : "Available"); + } else { + // Show only status for named devices + return modelData.paired ? "Paired" : "Available"; + } + } + color: deviceMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary) + font.pixelSize: 11 + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + Item { + Layout.preferredWidth: 22 + Layout.preferredHeight: 22 + visible: modelData.pairing || modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting + + Spinner { + visible: parent.visible + running: parent.visible + color: Theme.accentPrimary + anchors.centerIn: parent + size: 22 + } + } + } + + MouseArea { + id: deviceMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + // Handle device actions: disconnect, pair, or connect + if (modelData.connected) { + modelData.disconnect(); + } else if (!modelData.paired) { + modelData.pair(); + } else { + modelData.connect(); + } + } + } + } + } + } + + // Discovering indicator + RowLayout { + Layout.fillWidth: true + spacing: 8 + visible: Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering + + Text { + text: "Scanning for devices..." + font.pixelSize: 12 + color: Theme.textSecondary + } + + Spinner { + running: true + color: Theme.accentPrimary + size: 16 + } + } + } + } + } +} \ No newline at end of file diff --git a/Bar/Modules/Brightness.qml b/Bar/Modules/Brightness.qml index be04cfd..6ce1158 100644 --- a/Bar/Modules/Brightness.qml +++ b/Bar/Modules/Brightness.qml @@ -128,6 +128,7 @@ Item { StyledTooltip { id: brightnessTooltip text: "Brightness: " + brightness + "%" + positionAbove: false tooltipVisible: false targetItem: pill delay: 1500 diff --git a/Bar/Modules/ClockWidget.qml b/Bar/Modules/ClockWidget.qml index f899d0d..eba2afb 100644 --- a/Bar/Modules/ClockWidget.qml +++ b/Bar/Modules/ClockWidget.qml @@ -41,6 +41,7 @@ Rectangle { StyledTooltip { id: dateTooltip text: Time.dateString + positionAbove: false tooltipVisible: showTooltip && !calendar.visible targetItem: clockWidget delay: 200 diff --git a/Bar/Modules/CustomTrayMenu.qml b/Bar/Modules/CustomTrayMenu.qml index 5366d7f..a64ac7b 100644 --- a/Bar/Modules/CustomTrayMenu.qml +++ b/Bar/Modules/CustomTrayMenu.qml @@ -1,7 +1,7 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 import Quickshell import qs.Settings @@ -21,120 +21,456 @@ PopupWindow { anchor.rect.x: anchorX anchor.rect.y: anchorY - 4 + // Recursive function to destroy all open submenus in delegate tree, safely avoiding infinite recursion + function destroySubmenusRecursively(item) { + if (!item || !item.contentItem) return; + var children = item.contentItem.children; + for (var i = 0; i < children.length; ++i) { + var child = children[i]; + if (child.subMenu) { + child.subMenu.hideMenu(); + child.subMenu.destroy(); + child.subMenu = null; + } + // Recursively destroy submenus only if the child has contentItem to prevent issues + if (child.contentItem) { + destroySubmenusRecursively(child); + } + } + } + function showAt(item, x, y) { if (!item) { - console.warn("CustomTrayMenu: anchorItem is undefined, not showing menu."); + console.warn("CustomTrayMenu: anchorItem is undefined, won't show menu."); return; } - anchorItem = item - anchorX = x - anchorY = y - visible = true - forceActiveFocus() - Qt.callLater(() => trayMenu.anchor.updateAnchor()) + anchorItem = item; + anchorX = x; + anchorY = y; + visible = true; + forceActiveFocus(); + Qt.callLater(() => trayMenu.anchor.updateAnchor()); } function hideMenu() { - visible = false + visible = false; + destroySubmenusRecursively(listView); } Item { - anchors.fill: parent - Keys.onEscapePressed: trayMenu.hideMenu() + anchors.fill: parent; + Keys.onEscapePressed: trayMenu.hideMenu(); } QsMenuOpener { - id: opener - menu: trayMenu.menu + id: opener; + menu: trayMenu.menu; } Rectangle { - id: bg - anchors.fill: parent - color: Theme.surfaceVariant || "#222" - border.color: Theme.outline || "#444" - border.width: 1 - radius: 12 - z: 0 + id: bg; + anchors.fill: parent; + color: Theme.backgroundPrimary || "#222"; + border.color: Theme.outline || "#444"; + border.width: 1; + radius: 12; + z: 0; } ListView { - id: listView - anchors.fill: parent - anchors.margins: 6 - spacing: 2 - interactive: false - enabled: trayMenu.visible - clip: true + id: listView; + anchors.fill: parent; + anchors.margins: 6; + spacing: 2; + interactive: false; + enabled: trayMenu.visible; + clip: true; model: ScriptModel { values: opener.children ? [...opener.children.values] : [] } delegate: Rectangle { - id: entry - required property var modelData + id: entry; + required property var modelData; - width: listView.width - height: (modelData?.isSeparator) ? 8 : 32 - color: "transparent" - radius: 12 + width: listView.width; + height: (modelData?.isSeparator) ? 8 : 32; + color: "transparent"; + radius: 12; + + property var subMenu: null; Rectangle { - anchors.centerIn: parent - width: parent.width - 20 - height: 1 - color: Qt.darker(Theme.surfaceVariant || "#222", 1.4) - visible: modelData?.isSeparator ?? false + anchors.centerIn: parent; + width: parent.width - 20; + height: 1; + color: Qt.darker(Theme.backgroundPrimary || "#222", 1.4); + visible: modelData?.isSeparator ?? false; } Rectangle { - id: bg - anchors.fill: parent - color: mouseArea.containsMouse ? Theme.highlight : "transparent" - radius: 8 - visible: !(modelData?.isSeparator ?? false) - property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary + id: bg; + anchors.fill: parent; + color: mouseArea.containsMouse ? Theme.highlight : "transparent"; + radius: 8; + visible: !(modelData?.isSeparator ?? false); + property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary; RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 8 + anchors.fill: parent; + anchors.leftMargin: 12; + anchors.rightMargin: 12; + spacing: 8; Text { - Layout.fillWidth: true - color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled - text: modelData?.text ?? "" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight + Layout.fillWidth: true; + color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled; + text: modelData?.text ?? ""; + font.family: Theme.fontFamily; + font.pixelSize: Theme.fontSizeSmall; + verticalAlignment: Text.AlignVCenter; + elide: Text.ElideRight; } Image { - Layout.preferredWidth: 16 - Layout.preferredHeight: 16 - source: modelData?.icon ?? "" - visible: (modelData?.icon ?? "") !== "" - fillMode: Image.PreserveAspectFit + Layout.preferredWidth: 16; + Layout.preferredHeight: 16; + source: modelData?.icon ?? ""; + visible: (modelData?.icon ?? "") !== ""; + fillMode: Image.PreserveAspectFit; + } + + Text { + // Material Symbols Outlined chevron right for submenu + text: modelData?.hasChildren ? "menu" : ""; + font.family: "Material Symbols Outlined"; + font.pixelSize: 18; + verticalAlignment: Text.AlignVCenter; + visible: modelData?.hasChildren ?? false; + color: Theme.textPrimary; } } MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible + id: mouseArea; + anchors.fill: parent; + hoverEnabled: true; + enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible; onClicked: { if (modelData && !modelData.isSeparator) { - modelData.triggered() - trayMenu.hideMenu() + if (modelData.hasChildren) { + // Submenus open on hover; ignore click here + return; + } + modelData.triggered(); + trayMenu.hideMenu(); + } + } + + onEntered: { + if (!trayMenu.visible) return; + + if (modelData?.hasChildren) { + // Close sibling submenus immediately + for (let i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i]; + if (sibling !== entry && sibling.subMenu) { + sibling.subMenu.hideMenu(); + sibling.subMenu.destroy(); + sibling.subMenu = null; + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } + var globalPos = entry.mapToGlobal(0, 0); + var submenuWidth = 180; + var gap = 12; + var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width); + var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap; + + entry.subMenu = subMenuComponent.createObject(trayMenu, { + menu: modelData, + anchorItem: entry, + anchorX: anchorX, + anchorY: 0 + }); + entry.subMenu.showAt(entry, anchorX, 0); + } else { + // Hovered item without submenu; close siblings + for (let i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i]; + if (sibling.subMenu) { + sibling.subMenu.hideMenu(); + sibling.subMenu.destroy(); + sibling.subMenu = null; + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } + } + } + + onExited: { + if (entry.subMenu && !entry.subMenu.containsMouse()) { + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } + } + } + } + + // Simplified containsMouse without recursive calls to avoid stack overflow + function containsMouse() { + return mouseArea.containsMouse; + } + + Component.onDestruction: { + if (subMenu) { + subMenu.destroy(); + subMenu = null; + } + } + } + } + + Component { + id: subMenuComponent; + + PopupWindow { + id: subMenu; + implicitWidth: 180; + implicitHeight: Math.max(40, listView.contentHeight + 12); + visible: false; + color: "transparent"; + + property QsMenuHandle menu; + property var anchorItem: null; + property real anchorX; + property real anchorY; + + anchor.item: anchorItem ? anchorItem : null; + anchor.rect.x: anchorX; + anchor.rect.y: anchorY; + + function showAt(item, x, y) { + if (!item) { + console.warn("subMenuComponent: anchorItem is undefined, not showing menu."); + return; + } + anchorItem = item; + anchorX = x; + anchorY = y; + visible = true; + Qt.callLater(() => subMenu.anchor.updateAnchor()); + } + + function hideMenu() { + visible = false; + // Close all submenus recursively in this submenu + for (let i = 0; i < listView.contentItem.children.length; i++) { + const child = listView.contentItem.children[i]; + if (child.subMenu) { + child.subMenu.hideMenu(); + child.subMenu.destroy(); + child.subMenu = null; + } + } + } + + // Simplified containsMouse avoiding recursive calls + function containsMouse() { + return subMenu.containsMouse; + } + + Item { + anchors.fill: parent; + Keys.onEscapePressed: subMenu.hideMenu(); + } + + QsMenuOpener { + id: opener; + menu: subMenu.menu; + } + + Rectangle { + id: bg; + anchors.fill: parent; + color: Theme.backgroundPrimary || "#222"; + border.color: Theme.outline || "#444"; + border.width: 1; + radius: 12; + z: 0; + } + + ListView { + id: listView; + anchors.fill: parent; + anchors.margins: 6; + spacing: 2; + interactive: false; + enabled: subMenu.visible; + clip: true; + + model: ScriptModel { + values: opener.children ? [...opener.children.values] : []; + } + + delegate: Rectangle { + id: entry; + required property var modelData; + + width: listView.width; + height: (modelData?.isSeparator) ? 8 : 32; + color: "transparent"; + radius: 12; + + property var subMenu: null; + + Rectangle { + anchors.centerIn: parent; + width: parent.width - 20; + height: 1; + color: Qt.darker(Theme.surfaceVariant || "#222", 1.4); + visible: modelData?.isSeparator ?? false; + } + + Rectangle { + id: bg; + anchors.fill: parent; + color: mouseArea.containsMouse ? Theme.highlight : "transparent"; + radius: 8; + visible: !(modelData?.isSeparator ?? false); + property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary; + + RowLayout { + anchors.fill: parent; + anchors.leftMargin: 12; + anchors.rightMargin: 12; + spacing: 8; + + Text { + Layout.fillWidth: true; + color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled; + text: modelData?.text ?? ""; + font.family: Theme.fontFamily; + font.pixelSize: Theme.fontSizeSmall; + verticalAlignment: Text.AlignVCenter; + elide: Text.ElideRight; + } + + Image { + Layout.preferredWidth: 16; + Layout.preferredHeight: 16; + source: modelData?.icon ?? ""; + visible: (modelData?.icon ?? "") !== ""; + fillMode: Image.PreserveAspectFit; + } + + Text { + text: modelData?.hasChildren ? "\uE5CC" : ""; + font.family: "Material Symbols Outlined"; + font.pixelSize: 18; + verticalAlignment: Text.AlignVCenter; + visible: modelData?.hasChildren ?? false; + color: Theme.textPrimary; + } + } + + MouseArea { + id: mouseArea; + anchors.fill: parent; + hoverEnabled: true; + enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && subMenu.visible; + + onClicked: { + if (modelData && !modelData.isSeparator) { + if (modelData.hasChildren) { + return; + } + modelData.triggered(); + trayMenu.hideMenu(); + } + } + + onEntered: { + if (!subMenu.visible) return; + + if (modelData?.hasChildren) { + for (let i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i]; + if (sibling !== entry && sibling.subMenu) { + sibling.subMenu.hideMenu(); + sibling.subMenu.destroy(); + sibling.subMenu = null; + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } + var globalPos = entry.mapToGlobal(0, 0); + var submenuWidth = 180; + var gap = 12; + var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width); + var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap; + + entry.subMenu = subMenuComponent.createObject(subMenu, { + menu: modelData, + anchorItem: entry, + anchorX: anchorX, + anchorY: 0 + }); + entry.subMenu.showAt(entry, anchorX, 0); + } else { + for (let i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i]; + if (sibling.subMenu) { + sibling.subMenu.hideMenu(); + sibling.subMenu.destroy(); + sibling.subMenu = null; + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } + } + } + + onExited: { + if (entry.subMenu && !entry.subMenu.containsMouse()) { + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } + } + } + } + + // Simplified & safe containsMouse avoiding recursion + function containsMouse() { + return mouseArea.containsMouse; + } + + Component.onDestruction: { + if (subMenu) { + subMenu.destroy(); + subMenu = null; } } } } } } -} \ No newline at end of file +} diff --git a/Bar/Modules/Media.qml b/Bar/Modules/Media.qml index ca30cd6..f2dbd1f 100644 --- a/Bar/Modules/Media.qml +++ b/Bar/Modules/Media.qml @@ -1,7 +1,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Qt5Compat.GraphicalEffects +import Quickshell.Widgets +import QtQuick.Effects import qs.Settings import qs.Services import qs.Components @@ -54,17 +55,16 @@ Item { anchors.margins: 1 fillMode: Image.PreserveAspectCrop smooth: true + mipmap: true cache: false asynchronous: true - sourceSize.width: 24 - sourceSize.height: 24 source: MusicManager.trackArtUrl visible: source.toString() !== "" // Rounded corners using layer layer.enabled: true - layer.effect: OpacityMask { - cached: true + layer.effect: MultiEffect { + maskEnabled: true maskSource: Rectangle { width: albumArt.width height: albumArt.height diff --git a/Bar/Modules/SettingsButton.qml b/Bar/Modules/SettingsButton.qml new file mode 100644 index 0000000..7a9bede --- /dev/null +++ b/Bar/Modules/SettingsButton.qml @@ -0,0 +1,80 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Settings +import qs.Components +import qs.Widgets.SettingsWindow + +Item { + id: root + width: 22 + height: 22 + + property var settingsWindow: null + + Rectangle { + id: button + anchors.fill: parent + color: "transparent" + radius: width / 2 + + Text { + anchors.centerIn: parent + text: "settings" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 + color: mouseArea.containsMouse ? Theme.accentPrimary : Theme.textPrimary + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (!settingsWindow) { + // Create new window + settingsWindow = settingsComponent.createObject(null); // No parent to avoid dependency issues + if (settingsWindow) { + settingsWindow.visible = true; + // Handle window closure + settingsWindow.visibleChanged.connect(function() { + if (settingsWindow && !settingsWindow.visible) { + var windowToDestroy = settingsWindow; + settingsWindow = null; + windowToDestroy.destroy(); + } + }); + } + } else if (settingsWindow.visible) { + // Close and destroy window + var windowToDestroy = settingsWindow; + settingsWindow = null; + windowToDestroy.visible = false; + windowToDestroy.destroy(); + } + } + } + + StyledTooltip { + text: "Settings" + targetItem: mouseArea + tooltipVisible: mouseArea.containsMouse + } + } + + Component { + id: settingsComponent + SettingsWindow {} + } + + // Clean up on destruction + Component.onDestruction: { + if (settingsWindow) { + var windowToDestroy = settingsWindow; + settingsWindow = null; + windowToDestroy.destroy(); + } + } +} \ No newline at end of file diff --git a/Bar/Modules/SystemInfo.qml b/Bar/Modules/SystemInfo.qml index 07c7144..110deb1 100644 --- a/Bar/Modules/SystemInfo.qml +++ b/Bar/Modules/SystemInfo.qml @@ -8,6 +8,8 @@ Row { spacing: 10 visible: Settings.settings.showSystemInfoInBar + width: Math.floor(cpuUsageLayout.width + cpuTempLayout.width + memoryUsageLayout.width + (2 * 10)) + Row { id: cpuUsageLayout spacing: 6 diff --git a/Bar/Modules/SystemTray.qml b/Bar/Modules/SystemTray.qml index 90d30fb..7db68bf 100644 --- a/Bar/Modules/SystemTray.qml +++ b/Bar/Modules/SystemTray.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import Quickshell -import Qt5Compat.GraphicalEffects +import QtQuick.Effects import Quickshell.Services.SystemTray import Quickshell.Widgets import qs.Settings @@ -14,10 +14,10 @@ Row { property var trayMenu spacing: 8 Layout.alignment: Qt.AlignVCenter - + property bool containsMouse: false property var systemTray: SystemTray - + Repeater { model: systemTray.items delegate: Item { @@ -26,7 +26,7 @@ Row { // Hide Spotify icon, or adjust to your liking visible: modelData && modelData.id !== "spotify" property bool isHovered: trayMouseArea.containsMouse - + // Hover scale animation scale: isHovered ? 1.15 : 1.0 Behavior on scale { @@ -35,7 +35,7 @@ Row { easing.type: Easing.OutCubic } } - + // Subtle rotation on hover rotation: isHovered ? 5 : 0 Behavior on rotation { @@ -44,7 +44,7 @@ Row { easing.type: Easing.OutCubic } } - + Rectangle { anchors.centerIn: parent width: 16 @@ -63,7 +63,8 @@ Row { backer.fillMode: Image.PreserveAspectFit source: { let icon = modelData?.icon || ""; - if (!icon) return ""; + if (!icon) + return ""; // Process icon path if (icon.includes("?path=")) { const [name, path] = icon.split("?path="); @@ -80,72 +81,71 @@ Row { easing.type: Easing.OutCubic } } - Component.onCompleted: { - - } + Component.onCompleted: {} } } - + MouseArea { id: trayMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: (mouse) => { - if (!modelData) return; - + onClicked: mouse => { + if (!modelData) + return; + if (mouse.button === Qt.LeftButton) { // Close any open menu first if (trayMenu && trayMenu.visible) { - trayMenu.hideMenu() + trayMenu.hideMenu(); } - + if (!modelData.onlyMenu) { - modelData.activate() + modelData.activate(); } } else if (mouse.button === Qt.MiddleButton) { // Close any open menu first if (trayMenu && trayMenu.visible) { - trayMenu.hideMenu() + trayMenu.hideMenu(); } - - modelData.secondaryActivate && modelData.secondaryActivate() + + modelData.secondaryActivate && modelData.secondaryActivate(); } else if (mouse.button === Qt.RightButton) { - trayTooltip.tooltipVisible = false - console.log("Right click on", modelData.id, "hasMenu:", modelData.hasMenu, "menu:", modelData.menu) + trayTooltip.tooltipVisible = false; // If menu is already visible, close it if (trayMenu && trayMenu.visible) { - trayMenu.hideMenu() - return + trayMenu.hideMenu(); + return; } - + if (modelData.hasMenu && modelData.menu && trayMenu) { // Anchor the menu to the tray icon item (parent) and position it below the icon const menuX = (width / 2) - (trayMenu.width / 2); const menuY = height + 20; trayMenu.menu = modelData.menu; trayMenu.showAt(parent, menuX, menuY); - } else { - // console.log("No menu available for", modelData.id, "or trayMenu not set") - } + } else + // console.log("No menu available for", modelData.id, "or trayMenu not set") + {} } } onEntered: trayTooltip.tooltipVisible = true onExited: trayTooltip.tooltipVisible = false } - + StyledTooltip { id: trayTooltip text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item" + positionAbove: false tooltipVisible: false targetItem: trayIcon delay: 200 } - - Component.onDestruction: { - // No cache cleanup needed - } + + Component.onDestruction: + // No cache cleanup needed + {} } } -} \ No newline at end of file +} diff --git a/Bar/Modules/Taskbar.qml b/Bar/Modules/Taskbar.qml index 46fd78b..792babc 100644 --- a/Bar/Modules/Taskbar.qml +++ b/Bar/Modules/Taskbar.qml @@ -15,6 +15,7 @@ Item { // Attach custom tooltip StyledTooltip { id: styledTooltip + positionAbove: false } function getAppIcon(toplevel: Toplevel): string { @@ -74,7 +75,6 @@ Item { height: Math.max(12, Settings.settings.taskbarIconSize * 0.625) anchors.centerIn: parent source: getAppIcon(modelData) - smooth: true visible: source.toString() !== "" } @@ -95,7 +95,8 @@ Item { cursorShape: Qt.PointingHandCursor onEntered: { - styledTooltip.text = appTitle || appId; + var text = appTitle || appId; + styledTooltip.text = text.length > 60 ? text.substring(0, 60) + "..." : text; styledTooltip.targetItem = appButton; styledTooltip.tooltipVisible = true; } diff --git a/Bar/Modules/Volume.qml b/Bar/Modules/Volume.qml index f6fb87e..36e2be6 100644 --- a/Bar/Modules/Volume.qml +++ b/Bar/Modules/Volume.qml @@ -47,7 +47,8 @@ Item { StyledTooltip { id: volumeTooltip - text: "Volume: " + volume + "%\nScroll up/down to change volume.\nLeft click to open the input/output selection." + text: "Volume: " + volume + "%\nLeft click for advanced settings.\nScroll up/down to change volume." + positionAbove: false tooltipVisible: !ioSelector.visible && volumeDisplay.containsMouse targetItem: pillIndicator delay: 1500 diff --git a/Bar/Modules/Wifi.qml b/Bar/Modules/Wifi.qml new file mode 100644 index 0000000..5c75ba1 --- /dev/null +++ b/Bar/Modules/Wifi.qml @@ -0,0 +1,370 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Settings +import qs.Components +import qs.Services + +Item { + id: root + width: Settings.settings.wifiEnabled ? 22 : 0 + height: Settings.settings.wifiEnabled ? 22 : 0 + + property bool menuVisible: false + property string passwordPromptSsid: "" + property string passwordInput: "" + property bool showPasswordPrompt: false + + Network { + id: network + } + + // WiFi icon/button + Item { + id: wifiIcon + width: 22; height: 22 + visible: Settings.settings.wifiEnabled + + property int currentSignal: { + let maxSignal = 0; + for (const net in network.networks) { + if (network.networks[net].connected && network.networks[net].signal > maxSignal) { + maxSignal = network.networks[net].signal; + } + } + return maxSignal; + } + + Text { + id: wifiText + anchors.centerIn: parent + text: { + let connected = false; + for (const net in network.networks) { + if (network.networks[net].connected) { + connected = true; + break; + } + } + return connected ? network.signalIcon(parent.currentSignal) : "wifi_off" + } + font.family: mouseAreaWifi.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" + font.pixelSize: 16 + color: mouseAreaWifi.containsMouse ? Theme.accentPrimary : Theme.textPrimary + } + + MouseArea { + id: mouseAreaWifi + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + wifiMenu.visible = !wifiMenu.visible; + if (wifiMenu.visible) { + network.onMenuOpened(); + } else { + network.onMenuClosed(); + } + } + onEntered: wifiTooltip.tooltipVisible = true + onExited: wifiTooltip.tooltipVisible = false + } + } + + StyledTooltip { + id: wifiTooltip + text: "WiFi Networks" + positionAbove: false + tooltipVisible: false + targetItem: wifiIcon + delay: 200 + } + + PanelWindow { + id: wifiMenu + implicitWidth: 320 + implicitHeight: 480 + visible: false + color: "transparent" + anchors.top: true + anchors.right: true + margins.right: 0 + margins.top: 0 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + Rectangle { + anchors.fill: parent + color: Theme.backgroundPrimary + radius: 12 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 16 + + RowLayout { + Layout.fillWidth: true + spacing: 12 + + Text { + text: "wifi" + font.family: "Material Symbols Outlined" + font.pixelSize: 24 + color: Theme.accentPrimary + } + + Text { + text: "WiFi Networks" + font.pixelSize: 18 + font.bold: true + color: Theme.textPrimary + Layout.fillWidth: true + } + + IconButton { + icon: "refresh" + onClicked: network.refreshNetworks() + } + + IconButton { + icon: "close" + onClicked: { + wifiMenu.visible = false; + network.onMenuClosed(); + } + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: Theme.outline + opacity: 0.12 + } + + ListView { + id: networkList + Layout.fillWidth: true + Layout.fillHeight: true + model: Object.values(network.networks) + spacing: 8 + clip: true + + delegate: Item { + width: parent.width + height: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 108 : 48 // 48 for network + 60 for password prompt + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 48 + radius: 8 + color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.44) : (networkMouseArea.containsMouse ? Theme.highlight : "transparent") + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 + + Text { + text: network.signalIcon(modelData.signal) + font.family: "Material Symbols Outlined" + font.pixelSize: 18 + color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary) + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Text { + text: modelData.ssid || "Unknown Network" + color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textPrimary) + font.pixelSize: 14 + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" + color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary) + font.pixelSize: 11 + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" && network.connectError.length > 0 + text: network.connectError + color: Theme.error + font.pixelSize: 11 + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + Item { + Layout.preferredWidth: 22 + Layout.preferredHeight: 22 + visible: network.connectStatusSsid === modelData.ssid && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) + + Spinner { + visible: network.connectingSsid === modelData.ssid + running: network.connectingSsid === modelData.ssid + color: Theme.accentPrimary + anchors.centerIn: parent + size: 22 + } + + Text { + visible: network.connectStatus === "success" && !network.connectingSsid + text: "check_circle" + font.family: "Material Symbols Outlined" + font.pixelSize: 18 + color: "#43a047" + anchors.centerIn: parent + } + + Text { + visible: network.connectStatus === "error" && !network.connectingSsid + text: "error" + font.family: "Material Symbols Outlined" + font.pixelSize: 18 + color: Theme.error + anchors.centerIn: parent + } + } + + Text { + visible: modelData.connected + text: "connected" + color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary + font.pixelSize: 11 + } + } + + MouseArea { + id: networkMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + if (modelData.connected) { + network.disconnectNetwork(modelData.ssid); + } else if (network.isSecured(modelData.security) && !modelData.existing) { + passwordPromptSsid = modelData.ssid; + showPasswordPrompt = true; + passwordInput = ""; // Clear previous input + Qt.callLater(function() { + passwordInputField.forceActiveFocus(); + }); + } else { + network.connectNetwork(modelData.ssid, modelData.security); + } + } + } + } + + // Password prompt section + Rectangle { + id: passwordPromptSection + Layout.fillWidth: true + Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 + Layout.margins: 8 + visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt + color: Theme.surfaceVariant + radius: 8 + + RowLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 10 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 36 + + Rectangle { + anchors.fill: parent + radius: 8 + color: "transparent" + border.color: passwordInputField.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + + TextInput { + id: passwordInputField + anchors.fill: parent + anchors.margins: 12 + text: passwordInput + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: TextInput.AlignVCenter + clip: true + focus: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhNone + echoMode: TextInput.Password + onTextChanged: passwordInput = text + onAccepted: { + network.submitPassword(passwordPromptSsid, passwordInput); + showPasswordPrompt = false; + } + + MouseArea { + id: passwordInputMouseArea + anchors.fill: parent + onClicked: passwordInputField.forceActiveFocus() + } + } + } + } + + Rectangle { + Layout.preferredWidth: 80 + Layout.preferredHeight: 36 + radius: 18 + color: Theme.accentPrimary + border.color: Theme.accentPrimary + border.width: 0 + opacity: 1.0 + + Behavior on color { + ColorAnimation { + duration: 100 + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + network.submitPassword(passwordPromptSsid, passwordInput); + showPasswordPrompt = false; + } + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = Qt.darker(Theme.accentPrimary, 1.1) + onExited: parent.color = Theme.accentPrimary + } + + Text { + anchors.centerIn: parent + text: "Connect" + color: Theme.backgroundPrimary + font.pixelSize: 14 + font.bold: true + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Bar/Modules/Workspace.qml b/Bar/Modules/Workspace.qml index 40a4023..a658a6a 100644 --- a/Bar/Modules/Workspace.qml +++ b/Bar/Modules/Workspace.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window -import Qt5Compat.GraphicalEffects +import QtQuick.Effects import Quickshell import Quickshell.Io import qs.Settings @@ -124,13 +124,13 @@ Item { border.color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.1) border.width: 1 layer.enabled: true - layer.effect: DropShadow { - color: "black" - radius: 12 - samples: 24 - verticalOffset: 0 - horizontalOffset: 0 - opacity: 0.10 + layer.effect: MultiEffect { + shadowColor: "black" + // radius: 12 + + shadowVerticalOffset: 0 + shadowHorizontalOffset: 0 + shadowOpacity: 0.10 } } diff --git a/Components/Avatar.qml b/Components/Avatar.qml new file mode 100644 index 0000000..3a369bd --- /dev/null +++ b/Components/Avatar.qml @@ -0,0 +1,51 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Settings +import QtQuick.Effects + +Item { + anchors.fill: parent + anchors.margins: 2 + + Image { + id: avatarImage + anchors.fill: parent + source: "file://" + Settings.settings.profileImage + visible: false + mipmap: true + smooth: true + asynchronous: true + fillMode: Image.PreserveAspectCrop + } + + MultiEffect { + anchors.fill: parent + source: avatarImage + maskEnabled: true + maskSource: mask + visible: Settings.settings.profileImage !== "" + } + + Item { + id: mask + anchors.fill: parent + layer.enabled: true + visible: false + Rectangle { + anchors.fill: parent + radius: avatarImage.width / 2 + } + } + + // Fallback icon + Text { + anchors.centerIn: parent + text: "person" + font.family: "Material Symbols Outlined" + font.pixelSize: 24 + color: Theme.onAccent + visible: Settings.settings.profileImage === undefined || Settings.settings.profileImage === "" + z: 0 + } +} diff --git a/Components/CircularProgressBar.qml b/Components/CircularProgressBar.qml index 075f201..da03bfa 100644 --- a/Components/CircularProgressBar.qml +++ b/Components/CircularProgressBar.qml @@ -5,8 +5,7 @@ Rectangle { id: circularProgressBar color: "transparent" - // Properties - property real progress: 0.0 // 0.0 to 1.0 + property real progress: 0.0 property int size: 80 property color backgroundColor: Theme.surfaceVariant property color progressColor: Theme.accentPrimary @@ -19,7 +18,7 @@ Rectangle { // Notch properties property bool hasNotch: false - property real notchSize: 0.25 // Size of the notch as a fraction of the circle + property real notchSize: 0.25 property string notchIcon: "" property int notchIconSize: 12 property color notchIconColor: Theme.accentPrimary @@ -32,6 +31,7 @@ Rectangle { anchors.fill: parent onPaint: { + // Setup canvas context and calculate dimensions var ctx = getContext("2d") var centerX = width / 2 var centerY = height / 2 @@ -41,25 +41,22 @@ Rectangle { var notchStartAngle = -notchAngle / 2 var notchEndAngle = notchAngle / 2 - // Clear canvas ctx.reset() - - // Background circle ctx.strokeStyle = backgroundColor ctx.lineWidth = strokeWidth ctx.lineCap = "round" ctx.beginPath() if (hasNotch) { - // Draw background circle with notch on the right side - // Draw the arc excluding the notch area (notch is at 0 radians, right side) + // Draw background arc with notch gap ctx.arc(centerX, centerY, radius, notchEndAngle, 2 * Math.PI + notchStartAngle) } else { + // Draw full background circle ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI) } ctx.stroke() - // Progress arc + // Draw progress arc if (progress > 0) { ctx.strokeStyle = progressColor ctx.lineWidth = strokeWidth @@ -67,15 +64,11 @@ Rectangle { ctx.beginPath() if (hasNotch) { - // Calculate progress with notch consideration + // Calculate progress arc with notch gap var availableAngle = 2 * Math.PI - notchAngle var progressAngle = availableAngle * progress - - // Start from where the notch cutout begins (top-right) and go clockwise var adjustedStartAngle = notchEndAngle var adjustedEndAngle = adjustedStartAngle + progressAngle - - // Ensure we don't exceed the available space if (adjustedEndAngle > 2 * Math.PI + notchStartAngle) { adjustedEndAngle = 2 * Math.PI + notchStartAngle } @@ -84,6 +77,7 @@ Rectangle { ctx.arc(centerX, centerY, radius, adjustedStartAngle, adjustedEndAngle) } } else { + // Draw full progress arc ctx.arc(centerX, centerY, radius, startAngle, startAngle + (2 * Math.PI * progress)) } ctx.stroke() diff --git a/Components/StyledTooltip.qml b/Components/StyledTooltip.qml index 3c63e93..ae444e7 100644 --- a/Components/StyledTooltip.qml +++ b/Components/StyledTooltip.qml @@ -8,18 +8,22 @@ Window { property bool tooltipVisible: false property Item targetItem: null property int delay: 300 + + property bool positionAbove: true + flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint color: "transparent" visible: false - minimumWidth: tooltipText.implicitWidth + 24 - minimumHeight: tooltipText.implicitHeight + 16 property var _timerObj: null + onTooltipVisibleChanged: { if (tooltipVisible) { if (delay > 0) { if (_timerObj) { _timerObj.destroy(); _timerObj = null; } - _timerObj = Qt.createQmlObject('import QtQuick 2.0; Timer { interval: ' + delay + '; running: true; repeat: false; onTriggered: tooltipWindow._showNow() }', tooltipWindow); + _timerObj = Qt.createQmlObject( + 'import QtQuick 2.0; Timer { interval: ' + delay + '; running: true; repeat: false; onTriggered: tooltipWindow._showNow() }', + tooltipWindow); } else { _showNow(); } @@ -27,30 +31,45 @@ Window { _hideNow(); } } + function _showNow() { + width = Math.max(50, tooltipText.implicitWidth + 24) + height = Math.max(50, tooltipText.implicitHeight + 16) + if (!targetItem) return; - var pos = targetItem.mapToGlobal(0, targetItem.height); - x = pos.x - width / 2 + targetItem.width / 2; - y = pos.y + 12; + + if (positionAbove) { + // Position tooltip above the target item + var pos = targetItem.mapToGlobal(0, 0); + x = pos.x - width / 2 + targetItem.width / 2; + y = pos.y - height - 12; // 12 px margin above + } else { + // Position tooltip below the target item + var pos = targetItem.mapToGlobal(0, targetItem.height); + x = pos.x - width / 2 + targetItem.width / 2; + y = pos.y + 12; // 12 px margin below + } visible = true; } + function _hideNow() { visible = false; if (_timerObj) { _timerObj.destroy(); _timerObj = null; } } + Connections { target: tooltipWindow.targetItem function onXChanged() { - if (tooltipWindow.visible) tooltipWindow._showNow() + if (tooltipWindow.visible) tooltipWindow._showNow(); } function onYChanged() { - if (tooltipWindow.visible) tooltipWindow._showNow() + if (tooltipWindow.visible) tooltipWindow._showNow(); } function onWidthChanged() { - if (tooltipWindow.visible) tooltipWindow._showNow() + if (tooltipWindow.visible) tooltipWindow._showNow(); } function onHeightChanged() { - if (tooltipWindow.visible) tooltipWindow._showNow() + if (tooltipWindow.visible) tooltipWindow._showNow(); } } @@ -63,6 +82,7 @@ Window { opacity: 0.97 z: 1 } + Text { id: tooltipText text: tooltipWindow.text @@ -76,15 +96,16 @@ Window { padding: 8 z: 2 } + MouseArea { anchors.fill: parent hoverEnabled: true onExited: tooltipWindow.tooltipVisible = false cursorShape: Qt.ArrowCursor } + onTextChanged: { width = Math.max(minimumWidth, tooltipText.implicitWidth + 24); height = Math.max(minimumHeight, tooltipText.implicitHeight + 16); } - -} \ No newline at end of file +} \ No newline at end of file diff --git a/Helpers/IPCHandlers.qml b/Helpers/IPCHandlers.qml index 6cc0aea..3da1fca 100644 --- a/Helpers/IPCHandlers.qml +++ b/Helpers/IPCHandlers.qml @@ -6,7 +6,7 @@ IpcHandler { property var appLauncherPanel property var lockScreen property IdleInhibitor idleInhibitor - property var notificationPopup + property var notificationPopupVariants target: "globalIPC" @@ -17,10 +17,18 @@ IpcHandler { function toggleNotificationPopup(): void { console.log("[IPC] NotificationPopup toggle() called") - notificationPopup.togglePopup(); + + if (notificationPopupVariants) { + for (let i = 0; i < notificationPopupVariants.count; i++) { + let popup = notificationPopupVariants.objectAt(i); + if (popup) { + popup.togglePopup(); + } + } + } } - // Toggle Applauncher visibility + function toggleLauncher(): void { if (!appLauncherPanel) { console.warn("AppLauncherIpcHandler: appLauncherPanel not set!"); @@ -34,7 +42,7 @@ IpcHandler { } } - // Toggle LockScreen + function toggleLock(): void { if (!lockScreen) { console.warn("LockScreenIpcHandler: lockScreen not set!"); diff --git a/Helpers/IdleInhibitor.qml b/Helpers/IdleInhibitor.qml index de78a99..d2d8166 100644 --- a/Helpers/IdleInhibitor.qml +++ b/Helpers/IdleInhibitor.qml @@ -3,10 +3,10 @@ import Quickshell.Io Process { id: idleRoot - // Example: systemd-inhibit to prevent idle/sleep + // Uses systemd-inhibit to prevent idle/sleep command: ["systemd-inhibit", "--what=idle:sleep", "--who=noctalia", "--why=User requested", "sleep", "infinity"] - // Keep process running in background + // Track background process state property bool isRunning: running onStarted: { @@ -17,7 +17,7 @@ Process { console.log("[IdleInhibitor] Process finished:", exitCode) } - // Control functions + function start() { if (!running) { console.log("[IdleInhibitor] Starting idle inhibitor...") diff --git a/Helpers/Time.js b/Helpers/Time.js new file mode 100644 index 0000000..7e1e316 --- /dev/null +++ b/Helpers/Time.js @@ -0,0 +1,18 @@ +function formatVagueHumanReadableTime(totalSeconds) { + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60); + const seconds = totalSeconds - (hours * 3600) - (minutes * 60); + + var str = ""; + if (hours) { + str += hours.toString() + "h"; + } + if (minutes) { + str += minutes.toString() + "m"; + } + if (!hours && !minutes) { + str += seconds.toString() + "s"; + } + return str; +} + diff --git a/README.md b/README.md index f682de1..9f9e6a6 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,14 @@ Contributions are welcome! Feel free to open issues or submit pull requests. While I actually didn't want to accept donations, more and more people are asking to donate so... I don't know, if you really feel like donating then I obviously highly appreciate it but **PLEASE** never feel forced to donate or anything. It won't change how I work on Noctalia, it's a project that I work on for fun in the end. [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R01IX85B) + +--- + +#### Special Thanks + +Thank you to everyone who supports me and this project 💜! +* Gohma + --- ## License diff --git a/Services/MusicManager.qml b/Services/MusicManager.qml index 27f0226..81236b1 100644 --- a/Services/MusicManager.qml +++ b/Services/MusicManager.qml @@ -8,7 +8,7 @@ import qs.Components Singleton { id: manager - // Properties + property var currentPlayer: null property real currentPosition: 0 property int selectedPlayerIndex: 0 @@ -25,14 +25,14 @@ Singleton { property bool canSeek: currentPlayer ? currentPlayer.canSeek : false property bool hasPlayer: getAvailablePlayers().length > 0 - // Initialize + Item { Component.onCompleted: { updateCurrentPlayer() } } - // Returns available MPRIS players + function getAvailablePlayers() { if (!Mpris.players || !Mpris.players.values) { return [] @@ -51,14 +51,14 @@ Singleton { return controllablePlayers } - // Returns active player or first available + function findActivePlayer() { let availablePlayers = getAvailablePlayers() if (availablePlayers.length === 0) { return null } - // Use selected player if valid, otherwise use first available + if (selectedPlayerIndex < availablePlayers.length) { return availablePlayers[selectedPlayerIndex] } else { @@ -67,7 +67,8 @@ Singleton { } } - // Updates currentPlayer and currentPosition + + // Switch to the most recently active player function updateCurrentPlayer() { let newPlayer = findActivePlayer() if (newPlayer !== currentPlayer) { @@ -76,7 +77,7 @@ Singleton { } } - // Player control functions + function playPause() { if (currentPlayer) { if (currentPlayer.isPlaying) { @@ -118,6 +119,7 @@ Singleton { } } + // Seek to position based on ratio (0.0 to 1.0) function seekByRatio(ratio) { if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) { let seekPosition = ratio * currentPlayer.length @@ -126,20 +128,29 @@ Singleton { } } - // Updates progress bar every second + // Update progress bar every second while playing Timer { id: positionTimer interval: 1000 - running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0 + running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0 && currentPlayer.playbackState === MprisPlaybackState.Playing repeat: true onTriggered: { - if (currentPlayer && currentPlayer.isPlaying) { + if (currentPlayer && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) { currentPosition = currentPlayer.position + } else { + running = false } } } - // Reacts to player list changes + // Reset position when switching to inactive player + onCurrentPlayerChanged: { + if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { + currentPosition = 0 + } + } + + // Update current player when available players change Connections { target: Mpris.players function onValuesChanged() { diff --git a/Services/Network.qml b/Services/Network.qml new file mode 100644 index 0000000..c1dd53a --- /dev/null +++ b/Services/Network.qml @@ -0,0 +1,348 @@ +import QtQuick +import Quickshell.Io + +QtObject { + id: root + + property var networks: ({}) + property string connectingSsid: "" + property string connectStatus: "" + property string connectStatusSsid: "" + property string connectError: "" + property string detectedInterface: "" + + function signalIcon(signal) { + if (signal >= 80) + return "network_wifi"; + if (signal >= 60) + return "network_wifi_3_bar"; + if (signal >= 40) + return "network_wifi_2_bar"; + if (signal >= 20) + return "network_wifi_1_bar"; + return "wifi_0_bar"; + } + + function isSecured(security) { + return security && security.trim() !== "" && security.trim() !== "--"; + } + + function refreshNetworks() { + existingNetwork.running = true; + } + + function connectNetwork(ssid, security) { + pendingConnect = { + ssid: ssid, + security: security, + password: "" + }; + doConnect(); + } + + function submitPassword(ssid, password) { + pendingConnect = { + ssid: ssid, + security: networks[ssid].security, + password: password + }; + doConnect(); + } + + function disconnectNetwork(ssid) { + disconnectProfileProcess.connectionName = ssid; + disconnectProfileProcess.running = true; + } + + property var pendingConnect: null + + function doConnect() { + const params = pendingConnect; + if (!params) + return; + + connectingSsid = params.ssid; + connectStatus = ""; + connectStatusSsid = params.ssid; + + + const targetNetwork = networks[params.ssid]; + + if (targetNetwork && targetNetwork.existing) { + upConnectionProcess.profileName = params.ssid; + upConnectionProcess.running = true; + pendingConnect = null; + return; + } + + + if (params.security && params.security !== "--") { + getInterfaceProcess.running = true; + return; + } + connectProcess.security = params.security; + connectProcess.ssid = params.ssid; + connectProcess.password = params.password; + connectProcess.running = true; + pendingConnect = null; + } + + property int refreshInterval: 25000 + + // Only refresh when we have an active connection + property bool hasActiveConnection: { + for (const net in networks) { + if (networks[net].connected) { + return true; + } + } + return false; + } + + property Timer refreshTimer: Timer { + interval: root.refreshInterval + // Only run timer when we're connected to a network + running: root.hasActiveConnection + repeat: true + onTriggered: root.refreshNetworks() + } + + // Force a refresh when menu is opened + function onMenuOpened() { + refreshNetworks(); + } + + function onMenuClosed() { + // No need to do anything special on close + } + + property Process disconnectProfileProcess: Process { + id: disconnectProfileProcess + property string connectionName: "" + running: false + command: ["nmcli", "connection", "down", connectionName] + onRunningChanged: { + if (!running) { + root.refreshNetworks(); + } + } + } + + property Process existingNetwork: Process { + id: existingNetwork + running: false + command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] + stdout: StdioCollector { + onStreamFinished: { + const lines = text.split("\n"); + const networksMap = {}; + + for (let i = 0; i < lines.length; ++i) { + const line = lines[i].trim(); + if (!line) + continue; + + const parts = line.split(":"); + if (parts.length < 2) { + console.warn("Malformed nmcli output line:", line); + continue; + } + + const ssid = parts[0]; + const type = parts[1]; + + if (ssid) { + networksMap[ssid] = { + ssid: ssid, + type: type + }; + } + } + scanProcess.existingNetwork = networksMap; + scanProcess.running = true; + } + } + } + + property Process scanProcess: Process { + id: scanProcess + running: false + command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] + + property var existingNetwork + + stdout: StdioCollector { + onStreamFinished: { + const lines = text.split("\n"); + const networksMap = {}; + + for (let i = 0; i < lines.length; ++i) { + const line = lines[i].trim(); + if (!line) + continue; + + const parts = line.split(":"); + if (parts.length < 4) { + console.warn("Malformed nmcli output line:", line); + continue; + } + const ssid = parts[0]; + const security = parts[1]; + const signal = parseInt(parts[2]); + const inUse = parts[3] === "*"; + + if (ssid) { + if (!networksMap[ssid]) { + networksMap[ssid] = { + ssid: ssid, + security: security, + signal: signal, + connected: inUse, + existing: ssid in scanProcess.existingNetwork + }; + } else { + const existingNet = networksMap[ssid]; + if (inUse) { + existingNet.connected = true; + } + if (signal > existingNet.signal) { + existingNet.signal = signal; + existingNet.security = security; + } + } + } + } + + root.networks = networksMap; + scanProcess.existingNetwork = {}; + } + } + } + + property Process connectProcess: Process { + id: connectProcess + property string ssid: "" + property string password: "" + property string security: "" + running: false + command: { + if (password) { + return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password]; + } else { + return ["nmcli", "device", "wifi", "connect", `'${ssid}'`]; + } + } + stdout: StdioCollector { + onStreamFinished: { + root.connectingSsid = ""; + root.connectStatus = "success"; + root.connectStatusSsid = connectProcess.ssid; + root.connectError = ""; + root.refreshNetworks(); + } + } + stderr: StdioCollector { + onStreamFinished: { + root.connectingSsid = ""; + root.connectStatus = "error"; + root.connectStatusSsid = connectProcess.ssid; + root.connectError = text; + } + } + } + + property Process getInterfaceProcess: Process { + id: getInterfaceProcess + running: false + command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] + stdout: StdioCollector { + onStreamFinished: { + var lines = text.split("\n"); + for (var i = 0; i < lines.length; ++i) { + var parts = lines[i].split(":"); + if (parts[1] === "wifi" && parts[2] !== "unavailable") { + root.detectedInterface = parts[0]; + break; + } + } + if (root.detectedInterface) { + var params = root.pendingConnect; + addConnectionProcess.ifname = root.detectedInterface; + addConnectionProcess.ssid = params.ssid; + addConnectionProcess.password = params.password; + addConnectionProcess.profileName = params.ssid; + addConnectionProcess.security = params.security; + addConnectionProcess.running = true; + } else { + root.connectStatus = "error"; + root.connectStatusSsid = root.pendingConnect.ssid; + root.connectError = "No Wi-Fi interface found."; + root.connectingSsid = ""; + root.pendingConnect = null; + } + } + } + } + + property Process addConnectionProcess: Process { + id: addConnectionProcess + property string ifname: "" + property string ssid: "" + property string password: "" + property string profileName: "" + property string security: "" + running: false + command: { + var cmd = ["nmcli", "connection", "add", "type", "wifi", "ifname", ifname, "con-name", profileName, "ssid", ssid]; + if (security && security !== "--") { + cmd.push("wifi-sec.key-mgmt"); + cmd.push("wpa-psk"); + cmd.push("wifi-sec.psk"); + cmd.push(password); + } + return cmd; + } + stdout: StdioCollector { + onStreamFinished: { + upConnectionProcess.profileName = addConnectionProcess.profileName; + upConnectionProcess.running = true; + } + } + stderr: StdioCollector { + onStreamFinished: { + upConnectionProcess.profileName = addConnectionProcess.profileName; + upConnectionProcess.running = true; + } + } + } + + property Process upConnectionProcess: Process { + id: upConnectionProcess + property string profileName: "" + running: false + command: ["nmcli", "connection", "up", "id", profileName] + stdout: StdioCollector { + onStreamFinished: { + root.connectingSsid = ""; + root.connectStatus = "success"; + root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : ""; + root.connectError = ""; + root.pendingConnect = null; + root.refreshNetworks(); + } + } + stderr: StdioCollector { + onStreamFinished: { + root.connectingSsid = ""; + root.connectStatus = "error"; + root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : ""; + root.connectError = text; + root.pendingConnect = null; + } + } + } + + Component.onCompleted: { + refreshNetworks(); + } +} \ No newline at end of file diff --git a/Services/WallpaperManager.qml b/Services/WallpaperManager.qml index 64d1cad..f22f0fb 100644 --- a/Services/WallpaperManager.qml +++ b/Services/WallpaperManager.qml @@ -15,7 +15,7 @@ Singleton { toggleRandomWallpaper(); } } - property string wallpaperDirectory: Settings.settings.wallpaperFolder + property var wallpaperList: [] property string currentWallpaper: Settings.settings.currentWallpaper property bool scanning: false @@ -46,6 +46,11 @@ Singleton { } changeWallpaperProcess.running = true; } + + if (randomWallpaperTimer.running) { + randomWallpaperTimer.restart(); + } + generateTheme(); } @@ -91,15 +96,17 @@ Singleton { FolderListModel { id: folderModel + // Swww supports many images format but Quickshell only support a subset of those. nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] showDirs: false sortField: FolderListModel.Name onStatusChanged: { if (status === FolderListModel.Ready) { var files = []; + var filesSwww = []; for (var i = 0; i < count; i++) { - var fileph = (Settings.settings.wallpaperFolder !== undefined ? Settings.settings.wallpaperFolder : "") + "/" + get(i, "fileName"); - files.push(fileph); + var filepath = (Settings.settings.wallpaperFolder !== undefined ? Settings.settings.wallpaperFolder : "") + "/" + get(i, "fileName"); + files.push(filepath); } wallpaperList = files; scanning = false; diff --git a/Settings/Settings.qml b/Settings/Settings.qml index 879abd2..23bb5f2 100644 --- a/Settings/Settings.qml +++ b/Settings/Settings.qml @@ -45,6 +45,7 @@ Singleton { property string wallpaperFolder: "/usr/share/wallpapers" property string currentWallpaper: "" property string videoPath: "~/Videos/" + property bool showActiveWindow: true property bool showActiveWindowIcon: false property bool showSystemInfoInBar: false property bool showCorners: true @@ -65,6 +66,22 @@ Singleton { property real fontSizeMultiplier: 1.0 // Font size multiplier (1.0 = normal, 1.2 = 20% larger, 0.8 = 20% smaller) property int taskbarIconSize: 24 // Taskbar icon button size in pixels (default: 32, smaller: 24, larger: 40) property var pinnedExecs: [] // Added for AppLauncher pinned apps + + property bool showDock: true + property bool dockExclusive: false + property bool wifiEnabled: false + property bool bluetoothEnabled: false + property int recordingFrameRate: 60 + property string recordingQuality: "very_high" + property string recordingCodec: "h264" + property string audioCodec: "opus" + property bool showCursor: true + property string colorRange: "limited" + + // Monitor/Display Settings + property var barMonitors: [] // Array of monitor names to show the bar on + property var dockMonitors: [] // Array of monitor names to show the dock on + property var notificationMonitors: [] // Array of monitor names to show notifications on } } diff --git a/Widgets/Background.qml b/Widgets/Background.qml index b8eb602..b4d5dca 100644 --- a/Widgets/Background.qml +++ b/Widgets/Background.qml @@ -35,6 +35,7 @@ ShellRoot { visible: wallpaperSource !== "" cache: true smooth: true + mipmap: false } } } diff --git a/Widgets/Dock.qml b/Widgets/Dock.qml new file mode 100644 index 0000000..94b69f3 --- /dev/null +++ b/Widgets/Dock.qml @@ -0,0 +1,350 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Settings +import qs.Components + +PanelWindow { + id: taskbarWindow + visible: Settings.settings.showDock && + (Settings.settings.dockMonitors.includes(modelData.name) || + (Settings.settings.dockMonitors.length === 0)) + screen: (typeof modelData !== 'undefined' ? modelData : null) + exclusionMode: ExclusionMode.Ignore + anchors.bottom: true + anchors.left: true + anchors.right: true + focusable: false + color: "transparent" + implicitHeight: 43 + + // Auto-hide properties + property bool autoHide: true + property bool hidden: true + property int hideDelay: 500 + property int showDelay: 100 + property int hideAnimationDuration: 200 + property int showAnimationDuration: 150 + property int peekHeight: 2 + property int fullHeight: taskbarContainer.height + + // Track hover state + property bool dockHovered: false + property bool anyAppHovered: false + + // Context menu properties + property bool contextMenuVisible: false + property var contextMenuTarget: null + property var contextMenuToplevel: null + + // Timer for auto-hide delay + Timer { + id: hideTimer + interval: hideDelay + onTriggered: if (autoHide && !dockHovered && !anyAppHovered && !contextMenuVisible) hidden = true + } + + // Timer for show delay + Timer { + id: showTimer + interval: showDelay + onTriggered: hidden = false + } + + // Behavior for smooth hide/show animations + Behavior on margins.bottom { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: Easing.InOutQuad + } + } + + // Mouse area at screen bottom to detect entry and keep dock visible + MouseArea { + id: screenEdgeMouseArea + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 10 + hoverEnabled: true + propagateComposedEvents: true + + onEntered: if (autoHide && hidden) showTimer.start() + onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered && !contextMenuVisible) hideTimer.start() + } + + margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 + + Rectangle { + id: taskbarContainer + width: taskbar.width + 40 + height: Settings.settings.taskbarIconSize + 20 + topLeftRadius: 16 + topRightRadius: 16 + color: Theme.backgroundSecondary + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + + MouseArea { + id: dockMouseArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + + onEntered: { + dockHovered = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + onExited: { + dockHovered = false + if (autoHide && !anyAppHovered && !contextMenuVisible) hideTimer.start() + } + } + + Item { + id: taskbar + width: runningAppsRow.width + height: parent.height - 10 + anchors.centerIn: parent + + StyledTooltip { id: styledTooltip } + + function getAppIcon(toplevel: Toplevel): string { + if (!toplevel) return ""; + let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true); + if (!icon) icon = Quickshell.iconPath(toplevel.appId, true); + if (!icon) icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true); + if (!icon) icon = Quickshell.iconPath(toplevel.title, true); + return icon || Quickshell.iconPath("application-x-executable", true); + } + + Row { + id: runningAppsRow + spacing: 12 + height: parent.height + anchors.centerIn: parent + + Repeater { + model: ToplevelManager ? ToplevelManager.toplevels : null + + delegate: Rectangle { + id: appButton + width: Settings.settings.taskbarIconSize + 8 + height: Settings.settings.taskbarIconSize + 8 + radius: Math.max(6, Settings.settings.taskbarIconSize * 0.3) + color: isActive ? Theme.accentPrimary : (hovered ? Theme.surfaceVariant : "transparent") + border.color: isActive ? Qt.darker(Theme.accentPrimary, 1.2) : "transparent" + border.width: 1 + + property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData + property bool hovered: appMouseArea.containsMouse + property string appId: modelData ? modelData.appId : "" + property string appTitle: modelData ? modelData.title : "" + + Behavior on color { ColorAnimation { duration: 150 } } + Behavior on border.color { ColorAnimation { duration: 150 } } + + IconImage { + id: appIcon + width: Math.max(20, Settings.settings.taskbarIconSize * 0.75) + height: Math.max(20, Settings.settings.taskbarIconSize * 0.75) + anchors.centerIn: parent + source: taskbar.getAppIcon(modelData) + visible: source.toString() !== "" + } + + Text { + anchors.centerIn: parent + visible: !appIcon.visible + text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" + font.family: Theme.fontFamily + font.pixelSize: Math.max(14, Settings.settings.taskbarIconSize * 0.5) + font.bold: true + color: appButton.isActive ? Theme.onAccent : Theme.textPrimary + } + + MouseArea { + id: appMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + onEntered: { + anyAppHovered = true + if (!contextMenuVisible) { + styledTooltip.text = appTitle || appId; + styledTooltip.targetItem = appButton; + styledTooltip.positionAbove = true; + styledTooltip.tooltipVisible = true; + } + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + onExited: { + anyAppHovered = false + if (!contextMenuVisible) { + styledTooltip.tooltipVisible = false; + } + if (autoHide && !dockHovered && !contextMenuVisible) hideTimer.start() + } + onClicked: function(mouse) { + if (mouse.button === Qt.MiddleButton && modelData?.close) { + modelData.close(); + } + if (mouse.button === Qt.LeftButton && modelData?.activate) { + modelData.activate(); + } + if (mouse.button === Qt.RightButton) { + styledTooltip.tooltipVisible = false; + contextMenuTarget = appButton; + contextMenuToplevel = modelData; + contextMenuVisible = true; + } + } + } + + Rectangle { + visible: isActive + width: 6 + height: 6 + radius: 3 + color: Theme.onAccent + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: -8 + } + } + } + } + } + } + + // Context Menu + PanelWindow { + id: contextMenuWindow + visible: contextMenuVisible + screen: taskbarWindow.screen + exclusionMode: ExclusionMode.Ignore + anchors.bottom: true + anchors.left: true + anchors.right: true + color: "transparent" + focusable: false + + MouseArea { + anchors.fill: parent + onClicked: { + contextMenuVisible = false; + contextMenuTarget = null; + contextMenuToplevel = null; + hidden = true; // Hide dock when context menu closes + } + } + + Rectangle { + id: contextMenuContainer + width: 80 + height: contextMenuColumn.height + 0 + radius: 16 + color: Theme.backgroundPrimary + border.color: Theme.outline + border.width: 1 + + x: { + if (!contextMenuTarget) return 0; + // Get position relative to screen + const pos = contextMenuTarget.mapToItem(null, 0, 0); + // Center horizontally above the icon + let xPos = pos.x + (contextMenuTarget.width - width) / 2; + // Constrain to screen edges + return Math.max(0, Math.min(xPos, taskbarWindow.width - width)); + } + + y: { + if (!contextMenuTarget) return 0; + // Position above the dock + const pos = contextMenuTarget.mapToItem(null, 0, 0); + return pos.y - height + 32; + } + + Column { + id: contextMenuColumn + anchors.centerIn: parent + spacing: 4 + width: parent.width + + + Rectangle { + width: parent.width + height: 32 + radius: 16 + color: closeMouseArea.containsMouse ? Theme.surfaceVariant : "transparent" + border.color: Theme.outline + border.width: 1 + + Row { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + Text { + anchors.verticalCenter: parent.verticalCenter + text: "close" + font.family: "Material Symbols Outlined" + font.pixelSize: 14 + color: Theme.textPrimary + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: "Close" + font.family: Theme.fontFamily + font.pixelSize: 14 + color: Theme.textPrimary + } + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (contextMenuToplevel?.close) contextMenuToplevel.close(); + contextMenuVisible = false; + hidden = true; + } + } + } + } + + // Animation + scale: contextMenuVisible ? 1 : 0.9 + opacity: contextMenuVisible ? 1 : 0 + transformOrigin: Item.Bottom + + Behavior on scale { + NumberAnimation { + duration: 150 + easing.type: Easing.OutBack + } + } + + Behavior on opacity { + NumberAnimation { duration: 100 } + } + } + } +} diff --git a/Widgets/LockScreen/BatteryCharge.qml b/Widgets/LockScreen/BatteryCharge.qml index 5e8b4c0..dd96c25 100644 --- a/Widgets/LockScreen/BatteryCharge.qml +++ b/Widgets/LockScreen/BatteryCharge.qml @@ -6,7 +6,7 @@ import qs.Components import qs.Settings Item { - // Test mode + property bool testMode: false property int testPercent: 49 property bool testCharging: true @@ -21,7 +21,7 @@ Item { height: row.height visible: testMode || (isReady && battery.isLaptopBattery) - // Choose icon based on charge and charging state + function batteryIcon() { if (!show) return ""; @@ -32,7 +32,7 @@ Item { if (percent >= 95) return "battery_android_full"; - // Hardcoded battery symbols + if (percent >= 85) return "battery_android_6"; if (percent >= 70) diff --git a/Widgets/LockScreen/LockScreen.qml b/Widgets/LockScreen/LockScreen.qml index 1097c29..2eaf479 100644 --- a/Widgets/LockScreen/LockScreen.qml +++ b/Widgets/LockScreen/LockScreen.qml @@ -2,11 +2,11 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import QtQuick.Effects -import Qt5Compat.GraphicalEffects -import Quickshell.Wayland import Quickshell +import Quickshell.Wayland import Quickshell.Services.Pam import Quickshell.Io +import Quickshell.Widgets import qs.Components import qs.Settings import qs.Services @@ -32,7 +32,7 @@ WlSessionLock { Component.onCompleted: { Qt.callLater(function () { fetchWeatherData(); - }) + }); } function fetchWeatherData() { @@ -135,8 +135,8 @@ WlSessionLock { fillMode: Image.PreserveAspectCrop source: WallpaperManager.currentWallpaper !== "" ? WallpaperManager.currentWallpaper : "" cache: true - smooth: false - visible: true // source for MultiEffect + smooth: true + mipmap: false } MultiEffect { @@ -146,6 +146,7 @@ WlSessionLock { blurEnabled: true blur: 0.48 // controls blur strength (0 to 1) blurMax: 128 // max blur radius in pixels + // transparentBorder: true } ColumnLayout { @@ -160,39 +161,21 @@ WlSessionLock { radius: 40 color: Theme.accentPrimary - Image { - id: avatarImage + Rectangle { anchors.fill: parent - anchors.margins: 4 - source: Settings.settings.profileImage - fillMode: Image.PreserveAspectCrop - visible: false - asynchronous: true - } - OpacityMask { - anchors.fill: avatarImage - source: avatarImage - maskSource: Rectangle { - width: avatarImage.width - height: avatarImage.height - radius: avatarImage.width / 2 - visible: false - } - visible: Settings.settings.profileImage !== "" - } - Text { - anchors.centerIn: parent - text: "person" - font.family: "Material Symbols Outlined" - font.pixelSize: 32 - color: Theme.onAccent - visible: Settings.settings.profileImage === "" + color: "transparent" + radius: 40 + border.color: Theme.accentPrimary + border.width: 3 + z: 2 } + + Avatar {} + layer.enabled: true - layer.effect: Glow { - color: Theme.accentPrimary - radius: 8 - samples: 16 + layer.effect: MultiEffect { + shadowEnabled: true + shadowColor: Theme.accentPrimary } } @@ -257,7 +240,7 @@ WlSessionLock { width: parent.width * 0.8 height: 44 color: Theme.overlay - radius: 22 + radius: 20 visible: lock.errorMessage !== "" Text { @@ -275,7 +258,7 @@ WlSessionLock { Layout.alignment: Qt.AlignHCenter width: 120 height: 44 - radius: 22 + radius: 20 opacity: unlockButtonArea.containsMouse ? 0.8 : 0.5 color: unlockButtonArea.containsMouse ? Theme.accentPrimary : Theme.surface border.color: Theme.accentPrimary @@ -328,7 +311,7 @@ WlSessionLock { position: "bottomright" size: 1.3 fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" - offsetX: - Screen.width / 2 - 38 + offsetX: -Screen.width / 2 - 38 offsetY: 0 anchors.top: parent.top visible: Settings.settings.showCorners @@ -336,7 +319,7 @@ WlSessionLock { } Rectangle { - width: infoColumn.width + 16 + width: infoColumn.width + 32 height: infoColumn.height + 8 color: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" anchors.horizontalCenter: parent.horizontalCenter @@ -404,7 +387,6 @@ WlSessionLock { horizontalAlignment: Text.AlignHCenter Layout.alignment: Qt.AlignHCenter } - } } @@ -431,13 +413,11 @@ WlSessionLock { anchors.left: parent.left anchors.bottom: parent.bottom anchors.margins: 32 - spacing: 12 + spacing: 12 - BatteryCharge { - } + BatteryCharge {} } - ColumnLayout { anchors.right: parent.right anchors.bottom: parent.bottom diff --git a/Widgets/Notification/NotificationHistory.qml b/Widgets/Notification/NotificationHistory.qml index be98dd0..7e6277a 100644 --- a/Widgets/Notification/NotificationHistory.qml +++ b/Widgets/Notification/NotificationHistory.qml @@ -5,7 +5,7 @@ import qs.Settings import QtQuick.Layouts import qs.Components -// The popup window + PanelWithOverlay { id: notificationHistoryWin property string historyFilePath: Settings.settingsDir + "notification_history.json" diff --git a/Widgets/Notification/NotificationIcon.qml b/Widgets/Notification/NotificationIcon.qml index 988483a..cfb7b6f 100644 --- a/Widgets/Notification/NotificationIcon.qml +++ b/Widgets/Notification/NotificationIcon.qml @@ -9,13 +9,13 @@ Item { width: 22; height: 22 property bool isSilence: false - // Process for executing CLI commands + Process { id: rightClickProcess command: ["qs","ipc", "call", "globalIPC", "toggleNotificationPopup"] } - // Bell icon/button + Item { id: bell width: 22; height: 22 @@ -34,7 +34,7 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: function(mouse): void { + onClicked: function(mouse) { if (mouse.button === Qt.RightButton) { root.isSilence = !root.isSilence; rightClickProcess.running = true; @@ -55,8 +55,9 @@ Item { StyledTooltip { id: notificationTooltip text: "Notification History" + positionAbove: false tooltipVisible: false targetItem: bell delay: 200 } -} \ No newline at end of file +} diff --git a/Widgets/Notification/NotificationManager.qml b/Widgets/Notification/NotificationManager.qml index 8ac1d5d..94294b9 100644 --- a/Widgets/Notification/NotificationManager.qml +++ b/Widgets/Notification/NotificationManager.qml @@ -14,7 +14,7 @@ PanelWindow { anchors.top: true anchors.right: true - margins.top: -20 // keep as you want + margins.top: -20 margins.right: 6 property var notifications: [] @@ -52,7 +52,7 @@ PanelWindow { anchors.right: parent.right spacing: window.spacing width: parent.width - clip: false // prevent clipping during animation + clip: false // Prevent clipping during animation Repeater { model: notifications diff --git a/Widgets/Notification/NotificationPopup.qml b/Widgets/Notification/NotificationPopup.qml index 42ad0ba..5b4fd5d 100644 --- a/Widgets/Notification/NotificationPopup.qml +++ b/Widgets/Notification/NotificationPopup.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import Quickshell +import Quickshell.Widgets import qs.Settings PanelWindow { @@ -9,7 +10,7 @@ PanelWindow { implicitHeight: notificationColumn.implicitHeight color: "transparent" visible: notificationsVisible && notificationModel.count > 0 - screen: Quickshell.primaryScreen !== undefined ? Quickshell.primaryScreen : null + screen: (typeof modelData !== 'undefined' ? modelData : Quickshell.primaryScreen) focusable: false property bool barVisible: true @@ -114,38 +115,37 @@ PanelWindow { id: iconBackground width: 36 height: 36 - radius: width / 2 // Circular + radius: width / 2 color: Theme.accentPrimary anchors.verticalCenter: parent.verticalCenter border.color: Qt.darker(Theme.accentPrimary, 1.2) border.width: 1.5 - // Get all possible icon sources from notification + // Priority order for notification icons: image > appIcon > icon property var iconSources: [rawNotification?.image || "", rawNotification?.appIcon || "", rawNotification?.icon || ""] - // Try to load notification icon - Image { + // Load notification icon with fallback handling + IconImage { id: iconImage anchors.fill: parent anchors.margins: 4 - fillMode: Image.PreserveAspectFit - smooth: true - cache: false asynchronous: true - sourceSize.width: 36 - sourceSize.height: 36 + backer.fillMode: Image.PreserveAspectFit source: { + // Try each icon source in priority order for (var i = 0; i < iconBackground.iconSources.length; i++) { var icon = iconBackground.iconSources[i]; if (!icon) continue; + // Handle special path format from some notifications if (icon.includes("?path=")) { const [name, path] = icon.split("?path="); const fileName = name.substring(name.lastIndexOf("/") + 1); return `file://${path}/${fileName}`; } + // Handle absolute file paths if (icon.startsWith('/')) { return "file://" + icon; } @@ -157,7 +157,7 @@ PanelWindow { visible: status === Image.Ready && source.toString() !== "" } - // Fallback to first letter of app name + // Fallback: show first letter of app name when no icon available Text { anchors.centerIn: parent visible: !iconImage.visible diff --git a/Widgets/Overview.qml b/Widgets/Overview.qml index c23b79f..9702eba 100644 --- a/Widgets/Overview.qml +++ b/Widgets/Overview.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Effects import Quickshell import Quickshell.Wayland -import Qt5Compat.GraphicalEffects import qs.Services import qs.Settings @@ -34,15 +33,16 @@ ShellRoot { source: wallpaperSource cache: true smooth: true - visible: wallpaperSource !== "" // Show the original for FastBlur input + mipmap: false + visible: wallpaperSource !== "" } MultiEffect { id: overviewBgBlur anchors.fill: parent source: bgImage blurEnabled: true - blur: 0.48 // controls blur strength (0 to 1) - blurMax: 128 // max blur radius in pixels + blur: 0.48 + blurMax: 128 } Rectangle { anchors.fill: parent @@ -53,4 +53,4 @@ ShellRoot { } } } -} \ No newline at end of file +} diff --git a/Widgets/SettingsWindow/SettingsWindow.qml b/Widgets/SettingsWindow/SettingsWindow.qml new file mode 100644 index 0000000..29a695d --- /dev/null +++ b/Widgets/SettingsWindow/SettingsWindow.qml @@ -0,0 +1,369 @@ +import Quickshell +import Quickshell.Wayland +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import qs.Settings +import qs.Widgets.SettingsWindow.Tabs + +PanelWindow { + id: panelMain + implicitHeight: screen.height / 2 + implicitWidth: screen.width / 2 + color: "transparent" + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + + Component { + id: generalSettings + General {} + } + + Component { + id: barSettings + Bar {} + } + + Component { + id: timeWeatherSettings + TimeWeather {} + } + + Component { + id: recordingSettings + Recording {} + } + + Component { + id: networkSettings + Network {} + } + + Component { + id: miscSettings + Misc {} + } + + Component { + id: aboutSettings + About {} + } + + Component { + id: displaySettings + Display {} + } + + + Rectangle { + id: background + color: Theme.backgroundPrimary + anchors.fill: parent + radius: 20 + border.color: Theme.outline + border.width: 1 + + MultiEffect { + source: background + anchors.fill: background + shadowEnabled: true + shadowColor: Theme.shadow + shadowOpacity: 0.3 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 2 + shadowBlur: 12 + } + } + + + Rectangle { + id: settings + color: Theme.backgroundTertiary + anchors { + left: tabs.right + top: parent.top + bottom: parent.bottom + right: parent.right + margins: 12 + } + topRightRadius: 20 + bottomRightRadius: 20 + + + Rectangle { + id: headerArea + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: 16 + } + height: 48 + color: "transparent" + + RowLayout { + anchors.fill: parent + spacing: 12 + + + Text { + id: tabName + text: "General" + font.pixelSize: 18 + font.bold: true + color: Theme.textPrimary + Layout.fillWidth: true + } + + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent" + border.color: Theme.accentPrimary + border.width: 1 + + Text { + anchors.centerIn: parent + text: "close" + font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" + font.pixelSize: 18 + color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary + } + + MouseArea { + id: closeButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: panelMain.visible = false + } + } + } + } + + + Rectangle { + anchors { + top: headerArea.bottom + left: parent.left + right: parent.right + margins: 16 + } + height: 1 + color: Theme.outline + opacity: 0.3 + } + + Item { + id: settingsContainer + anchors { + top: headerArea.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + margins: 24 + topMargin: 32 + } + + + Loader { + id: settingsLoader + anchors.fill: parent + sourceComponent: generalSettings + opacity: 1 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { + duration: 150 + easing.type: Easing.InOutQuad + } + } + } + + + Loader { + id: settingsLoader2 + anchors.fill: parent + opacity: 0 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { + duration: 150 + easing.type: Easing.InOutQuad + } + } + } + } + } + + + Rectangle { + id: tabs + color: Theme.surface + width: screen.width / 9 + height: panelMain.height + topLeftRadius: 20 + bottomLeftRadius: 20 + border.color: Theme.outline + border.width: 1 + + Column { + width: parent.width + spacing: 0 + topPadding: 8 + + Repeater { + id: repeater + model: [ + { icon: "tune", text: "General" }, + { icon: "space_dashboard", text: "Bar" }, + { icon: "schedule", text: "Time & Weather" }, + { icon: "photo_camera", text: "Recording" }, + { icon: "wifi", text: "Network" }, + { icon: "monitor", text: "Display" }, + { icon: "settings_suggest", text: "Misc" }, + { icon: "info", text: "About" } + ] + + delegate: Column { + width: tabs.width + height: 40 + + Item { + width: parent.width + height: 39 + + RowLayout { + anchors.fill: parent + spacing: 8 + + + Rectangle { + id: activeIndicator + Layout.leftMargin: 8 + Layout.preferredWidth: 3 + Layout.preferredHeight: 24 + Layout.alignment: Qt.AlignVCenter + radius: 2 + color: Theme.accentPrimary + opacity: index === 0 ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 200 } } + } + + + Label { + id: icon + text: modelData.icon + font.family: "Material Symbols Outlined" + font.pixelSize: 24 + color: index === 0 ? Theme.accentPrimary : Theme.textPrimary + opacity: index === 0 ? 1 : 0.8 + Layout.leftMargin: 20 + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + Layout.alignment: Qt.AlignVCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + + Label { + id: label + text: modelData.text + font.pixelSize: 12 + color: index === 0 ? Theme.accentPrimary : Theme.textSecondary + font.weight: index === 0 ? Font.DemiBold : Font.Normal + Layout.fillWidth: true + Layout.preferredHeight: 24 + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.leftMargin: 4 + Layout.rightMargin: 16 + verticalAlignment: Text.AlignVCenter + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onClicked: { + + const newComponent = { + 0: generalSettings, + 1: barSettings, + 2: timeWeatherSettings, + 3: recordingSettings, + 4: networkSettings, + 5: displaySettings, + 6: miscSettings, + 7: aboutSettings + }[index]; + + + const tabNames = [ + "General", + "Bar", + "Time & Weather", + "Recording", + "Network", + "Display", + "Misc", + "About" + ]; + tabName.text = tabNames[index]; + + + if (settingsLoader.opacity === 1) { + + settingsLoader2.sourceComponent = newComponent; + settingsLoader.opacity = 0; + settingsLoader2.opacity = 1; + } else { + + settingsLoader.sourceComponent = newComponent; + settingsLoader2.opacity = 0; + settingsLoader.opacity = 1; + } + + + for (let i = 0; i < repeater.count; i++) { + let item = repeater.itemAt(i); + if (item) { + + let containerItem = item.children[0]; + + let rowLayout = containerItem.children[0]; + + let indicator = rowLayout.children[0]; + let icon = rowLayout.children[1]; + let label = rowLayout.children[2]; + + indicator.opacity = i === index ? 1 : 0; + icon.color = i === index ? Theme.accentPrimary : Theme.textPrimary; + icon.opacity = i === index ? 1 : 0.8; + label.color = i === index ? Theme.accentPrimary : Theme.textSecondary; + label.font.weight = i === index ? Font.Bold : Font.Normal; + } + } + } + } + } + + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.6 + visible: index < (repeater.count - 1) + } + } + } + } + } +} \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/About.qml b/Widgets/SettingsWindow/Tabs/About.qml new file mode 100644 index 0000000..3ee1942 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/About.qml @@ -0,0 +1,405 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import Quickshell.Io +import qs.Settings +import qs.Components + +Item { + id: root + + property string latestVersion: "Unknown" + property string currentVersion: "Unknown" + property var contributors: [] + property string githubDataPath: Settings.settingsDir + "github_data.json" + + Process { + id: currentVersionProcess + command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"] + stdout: StdioCollector { + onStreamFinished: { + const version = text.trim() + if (version && version !== "Unknown") { + root.currentVersion = version + } else { + + currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir + " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"] + currentVersionProcess.running = true + } + } + } + Component.onCompleted: { + running = true + } + } + + FileView { + id: githubDataFile + path: root.githubDataPath + blockLoading: true + printErrors: true + watchChanges: true + + JsonAdapter { + id: githubData + property string version: "Unknown" + property var contributors: [] + property double timestamp: 0 + } + + onFileChanged: githubDataFile.reload() + onLoaded: loadFromFile() + onLoadFailed: function(error) { + console.log("GitHub data file doesn't exist yet, creating it...") + githubData.version = "Unknown" + githubData.contributors = [] + githubData.timestamp = 0 + githubDataFile.writeAdapter() + fetchFromGitHub() + } + Component.onCompleted: if (path) reload() + } + + function loadFromFile() { + const now = Date.now() + const data = githubData + + if (!data.timestamp || (now - data.timestamp > 3600000)) { + console.log("[About] Cache expired or missing, fetching new data from GitHub...") + fetchFromGitHub() + return + } + console.log("[About] Loading cached GitHub data (age: " + Math.round((now - data.timestamp) / 60000) + " minutes)") + if (data.version) { + root.latestVersion = data.version + } + if (data.contributors) { + root.contributors = data.contributors + } + } + + Process { + id: versionProcess + command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/releases/latest"] + stdout: StdioCollector { + onStreamFinished: { + try { + const data = JSON.parse(text) + if (data.tag_name) { + const version = data.tag_name + githubData.version = version + root.latestVersion = version + console.log("[About] Latest version fetched from GitHub:", version) + } else { + console.log("No tag_name in GitHub response") + } + saveData() + } catch (e) { + console.error("Failed to parse version:", e) + } + } + } + } + + Process { + id: contributorsProcess + command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/contributors?per_page=100"] + stdout: StdioCollector { + onStreamFinished: { + try { + const data = JSON.parse(text) + githubData.contributors = data || [] + root.contributors = githubData.contributors + console.log("[About] Contributors data fetched from GitHub:", githubData.contributors.length, "contributors") + saveData() + } catch (e) { + console.error("Failed to parse contributors:", e) + root.contributors = [] + } + } + } + } + + function fetchFromGitHub() { + versionProcess.running = true + contributorsProcess.running = true + } + + function saveData() { + githubData.timestamp = Date.now() + Qt.callLater(() => { + githubDataFile.writeAdapter() + }) + } + + Item { + anchors.fill: parent + + + ColumnLayout { + id: mainLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: 8 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 32 + } + + Text { + text: "Noctalia" + font.pixelSize: 24 + font.bold: true + color: Theme.textPrimary + Layout.alignment: Qt.AlignCenter + } + + GridLayout { + Layout.alignment: Qt.AlignCenter + columns: 2 + rowSpacing: 4 + columnSpacing: 8 + + Text { + text: "Latest Version:" + font.pixelSize: 16 + color: Theme.textSecondary + Layout.alignment: Qt.AlignRight + } + + Text { + text: root.latestVersion + font.pixelSize: 16 + color: Theme.textPrimary + font.bold: true + } + + Text { + text: "Installed Version:" + font.pixelSize: 16 + color: Theme.textSecondary + Layout.alignment: Qt.AlignRight + } + + Text { + text: root.currentVersion + font.pixelSize: 16 + color: Theme.textPrimary + font.bold: true + } + } + + + Rectangle { + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 8 + Layout.preferredWidth: updateText.implicitWidth + 46 + Layout.preferredHeight: 32 + radius: 20 + color: updateArea.containsMouse ? Theme.accentPrimary : "transparent" + border.color: Theme.accentPrimary + border.width: 1 + visible: { + if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown") { + return false + } + const latest = root.latestVersion.replace("v", "").split(".") + const current = root.currentVersion.replace("v", "").split(".") + + + for (let i = 0; i < Math.max(latest.length, current.length); i++) { + const l = parseInt(latest[i] || "0") + const c = parseInt(current[i] || "0") + if (l > c) return true + if (l < c) return false + } + return false + } + + RowLayout { + anchors.centerIn: parent + spacing: 8 + + Text { + text: "system_update" + font.family: "Material Symbols Outlined" + font.pixelSize: 18 + color: updateArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary + } + + Text { + id: updateText + text: "Download latest release" + font.pixelSize: 14 + color: updateArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary + } + } + + MouseArea { + id: updateArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]) + } + } + } + + Text { + text: "Description something something <.< I hate writing text..." + font.pixelSize: 14 + color: Theme.textSecondary + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 16 + } + + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 32 + Layout.leftMargin: 32 + Layout.rightMargin: 32 + spacing: 16 + + RowLayout { + Layout.alignment: Qt.AlignCenter + spacing: 8 + + Text { + text: "Contributors" + font.pixelSize: 18 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "(" + root.contributors.length + ")" + font.pixelSize: 14 + color: Theme.textSecondary + } + } + + ScrollView { + Layout.fillWidth: true + Layout.preferredHeight: 300 + clip: true + + Item { + anchors.fill: parent + + GridView { + id: contributorsGrid + anchors.centerIn: parent + width: Math.min(parent.width, Math.ceil(root.contributors.length / 3) * 200) + height: parent.height + cellWidth: 200 + cellHeight: 110 + model: root.contributors + + delegate: Rectangle { + width: contributorsGrid.cellWidth - 4 + height: contributorsGrid.cellHeight - 10 + radius: 20 + color: contributorArea.containsMouse ? Theme.highlight : "transparent" + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 12 + + + Item { + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + + Image { + id: avatarImage + anchors.fill: parent + source: modelData.avatar_url || "" + sourceSize: Qt.size(80, 80) + visible: false + mipmap: true + smooth: true + asynchronous: true + fillMode: Image.PreserveAspectCrop + cache: true + } + + MultiEffect { + anchors.fill: parent + source: avatarImage + maskEnabled: true + maskSource: mask + } + + Item { + id: mask + anchors.fill: parent + layer.enabled: true + visible: false + Rectangle { + anchors.fill: parent + radius: avatarImage.width / 2 + } + } + + + Text { + anchors.centerIn: parent + text: "person" + font.family: "Material Symbols Outlined" + font.pixelSize: 24 + color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textPrimary + visible: !avatarImage.source || avatarImage.status !== Image.Ready + } + } + + + ColumnLayout { + spacing: 4 + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + + Text { + text: modelData.login || "Unknown" + font.pixelSize: 13 + color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textPrimary + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: (modelData.contributions || 0) + " commits" + font.pixelSize: 11 + color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textSecondary + } + } + } + + MouseArea { + id: contributorArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData.html_url) { + Quickshell.execDetached(["xdg-open", modelData.html_url]) + } + } + } + } + } + } + } + } + + + } + } + } \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/Bar.qml b/Widgets/SettingsWindow/Tabs/Bar.qml new file mode 100644 index 0000000..53e1cc7 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Bar.qml @@ -0,0 +1,380 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Settings +import qs.Components + +ColumnLayout { + id: root + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Bar Elements" + font.pixelSize: 16 + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 8 + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Show Active Window Icon" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Display the icon of the currently focused window in the bar" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: activeWindowIconSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.showActiveWindowIcon ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.showActiveWindowIcon ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: activeWindowIconThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.showActiveWindowIcon ? activeWindowIconSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.showActiveWindowIcon = !Settings.settings.showActiveWindowIcon; + } + } + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Show Active Window" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Display the title of the currently focused window below the bar" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: activeWindowSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.showActiveWindow ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.showActiveWindow ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: activeWindowThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.showActiveWindow ? activeWindowSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.showActiveWindow = !Settings.settings.showActiveWindow; + } + } + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Show System Info" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Display system information (CPU, RAM, etc.) in the bar" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: systemInfoSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.showSystemInfoInBar ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.showSystemInfoInBar ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: systemInfoThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.showSystemInfoInBar ? systemInfoSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.showSystemInfoInBar = !Settings.settings.showSystemInfoInBar; + } + } + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Show Taskbar" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Display a taskbar showing currently open windows" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: taskbarSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.showTaskbar ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.showTaskbar ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: taskbarThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.showTaskbar ? taskbarSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.showTaskbar = !Settings.settings.showTaskbar; + } + } + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Show Media" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Display media controls and information in the bar" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: mediaSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.showMediaInBar ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.showMediaInBar ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: mediaThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.showMediaInBar ? mediaSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.showMediaInBar = !Settings.settings.showMediaInBar; + } + } + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } +} \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml b/Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml new file mode 100644 index 0000000..e8b9c8a --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml @@ -0,0 +1,97 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Settings +import qs.Components + +Rectangle { + id: root + width: 64 + height: 32 + radius: 16 + color: Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 + + property bool useFahrenheit: Settings.settings.useFahrenheit + + + Rectangle { + id: slider + width: parent.width / 2 - 4 + height: parent.height - 4 + radius: 14 + color: Theme.accentPrimary + x: 2 + (useFahrenheit ? parent.width / 2 : 0) + y: 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + + Row { + anchors.fill: parent + spacing: 0 + + + Item { + width: parent.width / 2 + height: parent.height + + Text { + anchors.centerIn: parent + text: "°C" + font.pixelSize: 13 + font.bold: !useFahrenheit + color: !useFahrenheit ? Theme.onAccent : Theme.textPrimary + + Behavior on color { + ColorAnimation { duration: 200 } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (useFahrenheit) { + Settings.settings.useFahrenheit = false; + } + } + } + } + + + Item { + width: parent.width / 2 + height: parent.height + + Text { + anchors.centerIn: parent + text: "°F" + font.pixelSize: 13 + font.bold: useFahrenheit + color: useFahrenheit ? Theme.onAccent : Theme.textPrimary + + Behavior on color { + ColorAnimation { duration: 200 } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!useFahrenheit) { + Settings.settings.useFahrenheit = true; + } + } + } + } + } +} \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/Display.qml b/Widgets/SettingsWindow/Tabs/Display.qml new file mode 100644 index 0000000..ee4a159 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Display.qml @@ -0,0 +1,354 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Settings +import qs.Components + +ColumnLayout { + id: root + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + // Get list of available monitors/screens + property var monitors: Quickshell.screens || [] + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Monitor Selection" + font.pixelSize: 16 + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 8 + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Bar Monitors" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Select which monitors to display the top panel/bar on" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + } + + + Flow { + Layout.fillWidth: true + spacing: 8 + + Repeater { + model: root.monitors + delegate: Rectangle { + id: barCheckbox + property bool isChecked: false + + Component.onCompleted: { + // Initialize checkbox state from settings + let monitors = Settings.settings.barMonitors || []; + isChecked = monitors.includes(modelData.name); + } + + width: checkboxContent.implicitWidth + 16 + height: 32 + radius: 16 + color: isChecked ? Theme.accentPrimary : Theme.surfaceVariant + border.color: isChecked ? Theme.accentPrimary : Theme.outline + border.width: 1 + + RowLayout { + id: checkboxContent + anchors.centerIn: parent + spacing: 6 + + Text { + text: barCheckbox.isChecked ? "check" : "" + font.family: "Material Symbols Outlined" + font.pixelSize: 14 + color: barCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary + visible: barCheckbox.isChecked + } + + Text { + text: modelData.name || "Unknown" + font.pixelSize: 12 + color: barCheckbox.isChecked ? Theme.onAccent : Theme.textPrimary + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + isChecked = !isChecked; + + // Update settings array when checkbox is toggled + let monitors = Settings.settings.barMonitors || []; + monitors = [...monitors]; // Create copy to trigger reactivity + + if (isChecked) { + if (!monitors.includes(modelData.name)) { + monitors.push(modelData.name); + } + } else { + monitors = monitors.filter(name => name !== modelData.name); + } + + Settings.settings.barMonitors = monitors; + console.log("Bar monitors updated:", JSON.stringify(monitors)); + } + } + } + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Dock Monitors" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Select which monitors to display the application dock on" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + } + + + Flow { + Layout.fillWidth: true + spacing: 8 + + Repeater { + model: root.monitors + delegate: Rectangle { + id: dockCheckbox + property bool isChecked: false + + Component.onCompleted: { + // Initialize with current settings + let monitors = Settings.settings.dockMonitors || []; + isChecked = monitors.includes(modelData.name); + } + + width: checkboxContent.implicitWidth + 16 + height: 32 + radius: 16 + color: isChecked ? Theme.accentPrimary : Theme.surfaceVariant + border.color: isChecked ? Theme.accentPrimary : Theme.outline + border.width: 1 + + RowLayout { + id: checkboxContent + anchors.centerIn: parent + spacing: 6 + + Text { + text: dockCheckbox.isChecked ? "check" : "" + font.family: "Material Symbols Outlined" + font.pixelSize: 14 + color: dockCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary + visible: dockCheckbox.isChecked + } + + Text { + text: modelData.name || "Unknown" + font.pixelSize: 12 + color: dockCheckbox.isChecked ? Theme.onAccent : Theme.textPrimary + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + // Toggle state immediately for UI responsiveness + isChecked = !isChecked; + + // Update settings + let monitors = Settings.settings.dockMonitors || []; + monitors = [...monitors]; // Copy array + + if (isChecked) { + // Add to array if not already there + if (!monitors.includes(modelData.name)) { + monitors.push(modelData.name); + } + } else { + // Remove from array + monitors = monitors.filter(name => name !== modelData.name); + } + + Settings.settings.dockMonitors = monitors; + console.log("Dock monitors updated:", JSON.stringify(monitors)); + } + } + } + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Notification Monitors" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Select which monitors to display system notifications on" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + } + + + Flow { + Layout.fillWidth: true + spacing: 8 + + Repeater { + model: root.monitors + delegate: Rectangle { + id: notificationCheckbox + property bool isChecked: false + + Component.onCompleted: { + // Initialize with current settings + let monitors = Settings.settings.notificationMonitors || []; + isChecked = monitors.includes(modelData.name); + } + + width: checkboxContent.implicitWidth + 16 + height: 32 + radius: 16 + color: isChecked ? Theme.accentPrimary : Theme.surfaceVariant + border.color: isChecked ? Theme.accentPrimary : Theme.outline + border.width: 1 + + RowLayout { + id: checkboxContent + anchors.centerIn: parent + spacing: 6 + + Text { + text: notificationCheckbox.isChecked ? "check" : "" + font.family: "Material Symbols Outlined" + font.pixelSize: 14 + color: notificationCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary + visible: notificationCheckbox.isChecked + } + + Text { + text: modelData.name || "Unknown" + font.pixelSize: 12 + color: notificationCheckbox.isChecked ? Theme.onAccent : Theme.textPrimary + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + // Toggle state immediately for UI responsiveness + isChecked = !isChecked; + + // Update settings + let monitors = Settings.settings.notificationMonitors || []; + monitors = [...monitors]; // Copy array + + if (isChecked) { + // Add to array if not already there + if (!monitors.includes(modelData.name)) { + monitors.push(modelData.name); + } + } else { + // Remove from array + monitors = monitors.filter(name => name !== modelData.name); + } + + Settings.settings.notificationMonitors = monitors; + console.log("Notification monitors updated:", JSON.stringify(monitors)); + } + } + } + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } +} \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/General.qml b/Widgets/SettingsWindow/Tabs/General.qml new file mode 100644 index 0000000..4d0ffda --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/General.qml @@ -0,0 +1,339 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Settings +import qs.Components + +ColumnLayout { + id: root + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Profile" + font.pixelSize: 16 + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 8 + } + + + ColumnLayout { + spacing: 2 + Layout.fillWidth: true + + Text { + text: "Profile Image" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Your profile picture displayed in various places throughout the shell" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + Layout.bottomMargin: 4 + } + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + Rectangle { + width: 48 + height: 48 + radius: 24 + + Rectangle { + anchors.fill: parent + color: "transparent" + radius: 24 + border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 2 + z: 2 + } + + Avatar {} + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + radius: 16 + color: Theme.surfaceVariant + border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + + TextInput { + id: profileImageInput + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + anchors.topMargin: 6 + anchors.bottomMargin: 6 + text: Settings.settings.profileImage + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: TextInput.AlignVCenter + clip: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhUrlCharactersOnly + onTextChanged: { + Settings.settings.profileImage = text; + } + MouseArea { + anchors.fill: parent + cursorShape: Qt.IBeamCursor + onClicked: profileImageInput.forceActiveFocus() + } + } + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 16 + } + + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "User Interface" + font.pixelSize: 16 + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 8 + } + + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Show Corners" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Display rounded corners on screen edges" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: cornersSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.showCorners ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.showCorners ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: cornersThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.showCorners ? cornersSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.showCorners = !Settings.settings.showCorners; + } + } + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 4 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Show Dock" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Display a dock at the bottom of the screen for quick access to applications" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: dockSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.showDock ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.showDock ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: dockThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.showDock ? dockSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.showDock = !Settings.settings.showDock; + } + } + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 4 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Dim Desktop" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Dim the desktop when panels or menus are open" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: dimSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.dimPanels ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.dimPanels ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: dimThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.dimPanels ? dimSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.dimPanels = !Settings.settings.dimPanels; + } + } + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } +} \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/Misc.qml b/Widgets/SettingsWindow/Tabs/Misc.qml new file mode 100644 index 0000000..19d0e74 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Misc.qml @@ -0,0 +1,137 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Settings +import qs.Components + +ColumnLayout { + id: root + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Media" + font.pixelSize: 16 + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 8 + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + Text { + text: "Visualizer Type" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Choose the style of the audio visualizer" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + Layout.bottomMargin: 4 + } + + ComboBox { + id: visualizerTypeComboBox + Layout.fillWidth: true + Layout.preferredHeight: 40 + model: ["radial", "fire", "diamond"] + currentIndex: model.indexOf(Settings.settings.visualizerType) + + background: Rectangle { + implicitWidth: 120 + implicitHeight: 40 + color: Theme.surfaceVariant + border.color: visualizerTypeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + radius: 16 + } + + contentItem: Text { + leftPadding: 12 + rightPadding: visualizerTypeComboBox.indicator.width + visualizerTypeComboBox.spacing + text: visualizerTypeComboBox.displayText.charAt(0).toUpperCase() + visualizerTypeComboBox.displayText.slice(1) + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + indicator: Text { + x: visualizerTypeComboBox.width - width - 12 + y: visualizerTypeComboBox.topPadding + (visualizerTypeComboBox.availableHeight - height) / 2 + text: "arrow_drop_down" + font.family: "Material Symbols Outlined" + font.pixelSize: 24 + color: Theme.textPrimary + } + + popup: Popup { + y: visualizerTypeComboBox.height + width: visualizerTypeComboBox.width + implicitHeight: contentItem.implicitHeight + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: visualizerTypeComboBox.popup.visible ? visualizerTypeComboBox.delegateModel : null + currentIndex: visualizerTypeComboBox.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + color: Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 + radius: 16 + } + } + + delegate: ItemDelegate { + width: visualizerTypeComboBox.width + contentItem: Text { + text: modelData.charAt(0).toUpperCase() + modelData.slice(1) + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + highlighted: visualizerTypeComboBox.highlightedIndex === index + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + } + + onActivated: { + Settings.settings.visualizerType = model[index]; + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } +} \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/Network.qml b/Widgets/SettingsWindow/Tabs/Network.qml new file mode 100644 index 0000000..3f79e32 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Network.qml @@ -0,0 +1,193 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth +import qs.Settings +import qs.Components + +ColumnLayout { + id: root + spacing: 24 + + Component.onCompleted: { + + Quickshell.execDetached(["nmcli", "-t", "-f", "WIFI", "radio"]) + } + + + ColumnLayout { + spacing: 16 + Layout.fillWidth: true + + Text { + text: "Wi-Fi" + font.pixelSize: 16 + font.bold: true + color: Theme.textPrimary + } + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Enable Wi-Fi" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Turn Wi-Fi radio on or off" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: wifiSwitch + width: 52 + height: 32 + radius: 16 + property bool checked: Settings.settings.wifiEnabled + color: checked ? Theme.accentPrimary : Theme.surfaceVariant + border.color: checked ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: wifiThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: wifiSwitch.checked ? wifiSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.wifiEnabled = !Settings.settings.wifiEnabled + Quickshell.execDetached(["nmcli", "radio", "wifi", Settings.settings.wifiEnabled ? "on" : "off"]) + } + } + } + } + } + } + + + ColumnLayout { + spacing: 16 + Layout.fillWidth: true + Layout.topMargin: 16 + + Text { + text: "Bluetooth" + font.pixelSize: 16 + font.bold: true + color: Theme.textPrimary + } + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Enable Bluetooth" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Turn Bluetooth radio on or off" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: bluetoothSwitch + width: 52 + height: 32 + radius: 16 + property bool checked: Settings.settings.bluetoothEnabled + color: checked ? Theme.accentPrimary : Theme.surfaceVariant + border.color: checked ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: bluetoothThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: bluetoothSwitch.checked ? bluetoothSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (Bluetooth.defaultAdapter) { + Settings.settings.bluetoothEnabled = !Settings.settings.bluetoothEnabled + Bluetooth.defaultAdapter.enabled = Settings.settings.bluetoothEnabled + if (Bluetooth.defaultAdapter.enabled) { + Bluetooth.defaultAdapter.discovering = true + } + } + } + } + } + } + } + } + + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } +} \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/Record.qml b/Widgets/SettingsWindow/Tabs/Record.qml new file mode 100644 index 0000000..b1faa8b --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Record.qml @@ -0,0 +1,19 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Settings +import qs.Components + +ColumnLayout { + id: root + spacing: 24 + + Text { + text: "Coming soon..." + font.pixelSize: 16 + font.bold: true + color: Theme.textPrimary + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 32 + } +} \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/Recording.qml b/Widgets/SettingsWindow/Tabs/Recording.qml new file mode 100644 index 0000000..1d5677f --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Recording.qml @@ -0,0 +1,812 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Settings +import qs.Components + +ColumnLayout { + id: root + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + ScrollView { + id: scrollView + Layout.fillWidth: true + Layout.fillHeight: true + padding: 0 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Screen Recording" + font.pixelSize: 16 + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 8 + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + Text { + text: "Output Directory" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Directory where screen recordings will be saved" + font.pixelSize: 12 + color: Theme.textSecondary + Layout.bottomMargin: 4 + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + radius: 16 + color: Theme.surfaceVariant + border.color: videoPathInput.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + + TextInput { + id: videoPathInput + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + anchors.topMargin: 6 + anchors.bottomMargin: 6 + text: Settings.settings.videoPath !== undefined ? Settings.settings.videoPath : "" + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: TextInput.AlignVCenter + clip: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhUrlCharactersOnly + onTextChanged: { + Settings.settings.videoPath = text; + } + MouseArea { + anchors.fill: parent + cursorShape: Qt.IBeamCursor + onClicked: videoPathInput.forceActiveFocus() + } + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Frame Rate" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Target frame rate for screen recordings (default: 60)" + font.pixelSize: 12 + color: Theme.textSecondary + Layout.bottomMargin: 4 + } + + SpinBox { + id: frameRateSpinBox + Layout.fillWidth: true + Layout.preferredHeight: 40 + from: 24 + to: 144 + value: Settings.settings.recordingFrameRate || 60 + stepSize: 1 + + onValueChanged: { + Settings.settings.recordingFrameRate = value; + } + + background: Rectangle { + implicitWidth: 120 + implicitHeight: 40 + color: Theme.surfaceVariant + border.color: frameRateSpinBox.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + radius: 16 + } + + contentItem: TextInput { + text: frameRateSpinBox.textFromValue(frameRateSpinBox.value, frameRateSpinBox.locale) + font.pixelSize: 13 + color: Theme.textPrimary + selectionColor: Theme.accentPrimary + selectedTextColor: Theme.onAccent + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + readOnly: false + selectByMouse: true + validator: IntValidator { + bottom: frameRateSpinBox.from + top: frameRateSpinBox.to + } + inputMethodHints: Qt.ImhDigitsOnly + + onTextChanged: { + var newValue = parseInt(text); + if (!isNaN(newValue) && newValue >= frameRateSpinBox.from && newValue <= frameRateSpinBox.to) { + frameRateSpinBox.value = newValue; + } + } + + onEditingFinished: { + var newValue = parseInt(text); + if (isNaN(newValue) || newValue < frameRateSpinBox.from || newValue > frameRateSpinBox.to) { + text = frameRateSpinBox.textFromValue(frameRateSpinBox.value, frameRateSpinBox.locale); + } + } + } + + up.indicator: Rectangle { + x: parent.width - width + height: parent.height + width: height + color: "transparent" + radius: 16 + + Text { + text: "add" + font.family: "Material Symbols Outlined" + font.pixelSize: 20 + color: Theme.textPrimary + anchors.centerIn: parent + } + } + + down.indicator: Rectangle { + x: 0 + height: parent.height + width: height + color: "transparent" + radius: 16 + + Text { + text: "remove" + font.family: "Material Symbols Outlined" + font.pixelSize: 20 + color: Theme.textPrimary + anchors.centerIn: parent + } + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Audio Source" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Audio source to capture during recording" + font.pixelSize: 12 + color: Theme.textSecondary + Layout.bottomMargin: 4 + } + + ComboBox { + id: audioSourceComboBox + Layout.fillWidth: true + Layout.preferredHeight: 40 + model: ["default_output", "default_input", "both"] + currentIndex: model.indexOf(Settings.settings.recordingAudioSource || "default_output") + + background: Rectangle { + implicitWidth: 120 + implicitHeight: 40 + color: Theme.surfaceVariant + border.color: audioSourceComboBox.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + radius: 16 + } + + contentItem: Text { + leftPadding: 12 + rightPadding: audioSourceComboBox.indicator.width + audioSourceComboBox.spacing + text: { + switch(audioSourceComboBox.currentText) { + case "default_output": return "System Audio"; + case "default_input": return "Microphone"; + case "both": return "System Audio + Microphone"; + default: return audioSourceComboBox.currentText; + } + } + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + indicator: Text { + x: audioSourceComboBox.width - width - 12 + y: audioSourceComboBox.topPadding + (audioSourceComboBox.availableHeight - height) / 2 + text: "arrow_drop_down" + font.family: "Material Symbols Outlined" + font.pixelSize: 24 + color: Theme.textPrimary + } + + popup: Popup { + y: audioSourceComboBox.height + width: audioSourceComboBox.width + implicitHeight: contentItem.implicitHeight + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: audioSourceComboBox.popup.visible ? audioSourceComboBox.delegateModel : null + currentIndex: audioSourceComboBox.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + color: Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 + radius: 16 + } + } + + delegate: ItemDelegate { + width: audioSourceComboBox.width + contentItem: Text { + text: { + switch(modelData) { + case "default_output": return "System Audio"; + case "default_input": return "Microphone"; + case "both": return "System Audio + Microphone"; + default: return modelData; + } + } + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + highlighted: audioSourceComboBox.highlightedIndex === index + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + } + + onActivated: { + Settings.settings.recordingAudioSource = model[index]; + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Video Quality" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Higher quality results in larger file sizes" + font.pixelSize: 12 + color: Theme.textSecondary + Layout.bottomMargin: 4 + } + + ComboBox { + id: qualityComboBox + Layout.fillWidth: true + Layout.preferredHeight: 40 + model: ["medium", "high", "very_high", "ultra"] + currentIndex: model.indexOf(Settings.settings.recordingQuality || "very_high") + + background: Rectangle { + implicitWidth: 120 + implicitHeight: 40 + color: Theme.surfaceVariant + border.color: qualityComboBox.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + radius: 16 + } + + contentItem: Text { + leftPadding: 12 + rightPadding: qualityComboBox.indicator.width + qualityComboBox.spacing + text: { + switch(qualityComboBox.currentText) { + case "medium": return "Medium"; + case "high": return "High"; + case "very_high": return "Very High"; + case "ultra": return "Ultra"; + default: return qualityComboBox.currentText; + } + } + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + indicator: Text { + x: qualityComboBox.width - width - 12 + y: qualityComboBox.topPadding + (qualityComboBox.availableHeight - height) / 2 + text: "arrow_drop_down" + font.family: "Material Symbols Outlined" + font.pixelSize: 24 + color: Theme.textPrimary + } + + popup: Popup { + y: qualityComboBox.height + width: qualityComboBox.width + implicitHeight: contentItem.implicitHeight + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: qualityComboBox.popup.visible ? qualityComboBox.delegateModel : null + currentIndex: qualityComboBox.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + color: Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 + radius: 16 + } + } + + delegate: ItemDelegate { + width: qualityComboBox.width + contentItem: Text { + text: { + switch(modelData) { + case "medium": return "Medium"; + case "high": return "High"; + case "very_high": return "Very High"; + case "ultra": return "Ultra"; + default: return modelData; + } + } + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + highlighted: qualityComboBox.highlightedIndex === index + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + } + + onActivated: { + Settings.settings.recordingQuality = model[index]; + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Video Codec" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Different codecs offer different compression and compatibility" + font.pixelSize: 12 + color: Theme.textSecondary + Layout.bottomMargin: 4 + } + + ComboBox { + id: codecComboBox + Layout.fillWidth: true + Layout.preferredHeight: 40 + model: ["h264", "hevc", "av1", "vp8", "vp9"] + currentIndex: model.indexOf(Settings.settings.recordingCodec || "h264") + + background: Rectangle { + implicitWidth: 120 + implicitHeight: 40 + color: Theme.surfaceVariant + border.color: codecComboBox.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + radius: 16 + } + + contentItem: Text { + leftPadding: 12 + rightPadding: codecComboBox.indicator.width + codecComboBox.spacing + text: codecComboBox.currentText.toUpperCase() + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + indicator: Text { + x: codecComboBox.width - width - 12 + y: codecComboBox.topPadding + (codecComboBox.availableHeight - height) / 2 + text: "arrow_drop_down" + font.family: "Material Symbols Outlined" + font.pixelSize: 24 + color: Theme.textPrimary + } + + popup: Popup { + y: codecComboBox.height + width: codecComboBox.width + implicitHeight: contentItem.implicitHeight + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: codecComboBox.popup.visible ? codecComboBox.delegateModel : null + currentIndex: codecComboBox.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + color: Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 + radius: 16 + } + } + + delegate: ItemDelegate { + width: codecComboBox.width + contentItem: Text { + text: modelData.toUpperCase() + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + highlighted: codecComboBox.highlightedIndex === index + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + } + + onActivated: { + Settings.settings.recordingCodec = model[index]; + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Audio Codec" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Opus is recommended for best performance and smallest audio size" + font.pixelSize: 12 + color: Theme.textSecondary + Layout.bottomMargin: 4 + } + + ComboBox { + id: audioCodecComboBox + Layout.fillWidth: true + Layout.preferredHeight: 40 + model: ["opus", "aac"] + currentIndex: model.indexOf(Settings.settings.audioCodec || "opus") + + background: Rectangle { + implicitWidth: 120 + implicitHeight: 40 + color: Theme.surfaceVariant + border.color: audioCodecComboBox.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + radius: 16 + } + + contentItem: Text { + leftPadding: 12 + rightPadding: audioCodecComboBox.indicator.width + audioCodecComboBox.spacing + text: audioCodecComboBox.currentText.toUpperCase() + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + indicator: Text { + x: audioCodecComboBox.width - width - 12 + y: audioCodecComboBox.topPadding + (audioCodecComboBox.availableHeight - height) / 2 + text: "arrow_drop_down" + font.family: "Material Symbols Outlined" + font.pixelSize: 24 + color: Theme.textPrimary + } + + popup: Popup { + y: audioCodecComboBox.height + width: audioCodecComboBox.width + implicitHeight: contentItem.implicitHeight + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: audioCodecComboBox.popup.visible ? audioCodecComboBox.delegateModel : null + currentIndex: audioCodecComboBox.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + color: Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 + radius: 16 + } + } + + delegate: ItemDelegate { + width: audioCodecComboBox.width + contentItem: Text { + text: modelData.toUpperCase() + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + highlighted: audioCodecComboBox.highlightedIndex === index + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + } + + onActivated: { + Settings.settings.audioCodec = model[index]; + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Color Range" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Limited is recommended for better compatibility" + font.pixelSize: 12 + color: Theme.textSecondary + Layout.bottomMargin: 4 + } + + ComboBox { + id: colorRangeComboBox + Layout.fillWidth: true + Layout.preferredHeight: 40 + model: ["limited", "full"] + currentIndex: model.indexOf(Settings.settings.colorRange || "limited") + + background: Rectangle { + implicitWidth: 120 + implicitHeight: 40 + color: Theme.surfaceVariant + border.color: colorRangeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + radius: 16 + } + + contentItem: Text { + leftPadding: 12 + rightPadding: colorRangeComboBox.indicator.width + colorRangeComboBox.spacing + text: colorRangeComboBox.currentText.charAt(0).toUpperCase() + colorRangeComboBox.currentText.slice(1) + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + indicator: Text { + x: colorRangeComboBox.width - width - 12 + y: colorRangeComboBox.topPadding + (colorRangeComboBox.availableHeight - height) / 2 + text: "arrow_drop_down" + font.family: "Material Symbols Outlined" + font.pixelSize: 24 + color: Theme.textPrimary + } + + popup: Popup { + y: colorRangeComboBox.height + width: colorRangeComboBox.width + implicitHeight: contentItem.implicitHeight + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: colorRangeComboBox.popup.visible ? colorRangeComboBox.delegateModel : null + currentIndex: colorRangeComboBox.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + color: Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 + radius: 16 + } + } + + delegate: ItemDelegate { + width: colorRangeComboBox.width + contentItem: Text { + text: modelData.charAt(0).toUpperCase() + modelData.slice(1) + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + highlighted: colorRangeComboBox.highlightedIndex === index + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + } + + onActivated: { + Settings.settings.colorRange = model[index]; + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Show Cursor" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Record mouse cursor in the video" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: cursorSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.showCursor ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.showCursor ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: cursorThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.showCursor ? cursorSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.showCursor = !Settings.settings.showCursor; + } + } + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 24 + } + } + } +} \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/TimeWeather.qml b/Widgets/SettingsWindow/Tabs/TimeWeather.qml new file mode 100644 index 0000000..7ee1c75 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/TimeWeather.qml @@ -0,0 +1,283 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Settings +import qs.Components +import qs.Widgets.SettingsWindow.Tabs.Components + +ColumnLayout { + id: root + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Time" + font.pixelSize: 16 + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 8 + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Use 12 Hour Clock" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Display time in 12-hour format (e.g., 2:30 PM) instead of 24-hour format" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: use12HourClockSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.use12HourClock ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.use12HourClock ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: use12HourClockThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.use12HourClock ? use12HourClockSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.use12HourClock = !Settings.settings.use12HourClock; + } + } + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "US Style Date" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Display dates in MM/DD/YYYY format instead of DD/MM/YYYY" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: reverseDayMonthSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.reverseDayMonth ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.reverseDayMonth ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: reverseDayMonthThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.reverseDayMonth ? reverseDayMonthSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.reverseDayMonth = !Settings.settings.reverseDayMonth; + } + } + } + } + } + } + + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + Layout.topMargin: 16 + + Text { + text: "Weather" + font.pixelSize: 16 + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 8 + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + Text { + text: "City" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Your city name for weather information" + font.pixelSize: 12 + color: Theme.textSecondary + Layout.fillWidth: true + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + radius: 16 + color: Theme.surfaceVariant + border.color: cityInput.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + + TextInput { + id: cityInput + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + anchors.topMargin: 6 + anchors.bottomMargin: 6 + text: Settings.settings.weatherCity + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: TextInput.AlignVCenter + clip: true + focus: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhNone + + onTextChanged: { + Settings.settings.weatherCity = text; + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.IBeamCursor + onClicked: { + cityInput.forceActiveFocus(); + } + } + } + } + } + + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Temperature Unit" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Choose between Celsius and Fahrenheit" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + UnitSelector {} + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } +} \ No newline at end of file diff --git a/Widgets/Sidebar/Config/ProfileSettings.qml b/Widgets/Sidebar/Config/ProfileSettings.qml index 0c74453..3451e25 100644 --- a/Widgets/Sidebar/Config/ProfileSettings.qml +++ b/Widgets/Sidebar/Config/ProfileSettings.qml @@ -1,13 +1,15 @@ import QtQuick import QtQuick.Layouts +import QtQuick.Effects import QtQuick.Controls -import Qt5Compat.GraphicalEffects +import Quickshell.Widgets +import qs.Components import qs.Settings Rectangle { id: profileSettingsCard Layout.fillWidth: true - Layout.preferredHeight: 650 + Layout.preferredHeight: 690 color: Theme.surface radius: 18 @@ -53,47 +55,23 @@ Rectangle { spacing: 8 Layout.fillWidth: true + // Profile image Rectangle { - width: 40 - height: 40 - radius: 20 - color: Theme.surfaceVariant - border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 + width: 48 + height: 48 + radius: 24 - Image { - id: avatarImage + // Border + Rectangle { anchors.fill: parent - anchors.margins: 2 - source: Settings.settings.profileImage - fillMode: Image.PreserveAspectCrop - visible: false - asynchronous: true - cache: false - sourceSize.width: 64 - sourceSize.height: 64 - } - - OpacityMask { - anchors.fill: avatarImage - source: avatarImage - maskSource: Rectangle { - width: avatarImage.width - height: avatarImage.height - radius: avatarImage.width / 2 - visible: false - } - visible: Settings.settings.profileImage !== "" + color: "transparent" + radius: 24 + border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 2 + z: 2 } - Text { - anchors.centerIn: parent - text: "person" - font.family: "Material Symbols Outlined" - font.pixelSize: 20 - color: Theme.accentPrimary - visible: Settings.settings.profileImage === "" - } + Avatar {} } Rectangle { @@ -121,7 +99,7 @@ Rectangle { activeFocusOnTab: true inputMethodHints: Qt.ImhUrlCharactersOnly onTextChanged: { - Settings.settings.profileImage = text + Settings.settings.profileImage = text; } MouseArea { anchors.fill: parent @@ -182,7 +160,7 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - Settings.settings.showActiveWindowIcon = !Settings.settings.showActiveWindowIcon + Settings.settings.showActiveWindowIcon = !Settings.settings.showActiveWindowIcon; } } } @@ -237,7 +215,7 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - Settings.settings.showSystemInfoInBar = !Settings.settings.showSystemInfoInBar + Settings.settings.showSystemInfoInBar = !Settings.settings.showSystemInfoInBar; } } } @@ -292,7 +270,7 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - Settings.settings.showCorners = !Settings.settings.showCorners + Settings.settings.showCorners = !Settings.settings.showCorners; } } } @@ -347,7 +325,62 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - Settings.settings.showTaskbar = !Settings.settings.showTaskbar + Settings.settings.showTaskbar = !Settings.settings.showTaskbar; + } + } + } + } + + // Show Dock Setting + RowLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Show Dock" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Item { + Layout.fillWidth: true + } + + Rectangle { + id: dockSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.showDock ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.showDock ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: dockThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.showDock ? taskbarSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.showDock = !Settings.settings.showDock; } } } @@ -402,7 +435,7 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - Settings.settings.showMediaInBar = !Settings.settings.showMediaInBar + Settings.settings.showMediaInBar = !Settings.settings.showMediaInBar; } } } @@ -457,7 +490,7 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - Settings.settings.dimPanels = !Settings.settings.dimPanels + Settings.settings.dimPanels = !Settings.settings.dimPanels; } } } @@ -596,7 +629,7 @@ Rectangle { activeFocusOnTab: true inputMethodHints: Qt.ImhUrlCharactersOnly onTextChanged: { - Settings.settings.videoPath = text + Settings.settings.videoPath = text; } MouseArea { anchors.fill: parent @@ -607,4 +640,4 @@ Rectangle { } } } -} \ No newline at end of file +} diff --git a/Widgets/Sidebar/Config/SettingsModal.qml b/Widgets/Sidebar/Config/SettingsModal.qml index d4d4ccf..056f6f5 100644 --- a/Widgets/Sidebar/Config/SettingsModal.qml +++ b/Widgets/Sidebar/Config/SettingsModal.qml @@ -22,7 +22,7 @@ PanelWindow { Rectangle { anchors.fill: parent color: Theme.backgroundPrimary - radius: 24 + radius: 20 z: 0 ColumnLayout { @@ -31,7 +31,6 @@ PanelWindow { anchors.leftMargin: 32 anchors.rightMargin: 32 anchors.topMargin: 32 - spacing: 24 // Header @@ -85,14 +84,14 @@ PanelWindow { } } - // Tabs bar (moved here) + // Tabs bar (reordered) Tabs { id: settingsTabs Layout.fillWidth: true tabsModel: [ - { icon: "cloud", label: "Weather" }, { icon: "settings", label: "System" }, - { icon: "wallpaper", label: "Wallpaper" } + { icon: "wallpaper", label: "Wallpaper" }, + { icon: "cloud", label: "Weather" } ] } @@ -115,7 +114,32 @@ PanelWindow { id: tabContentLoader anchors.top: parent.top width: parent.width - sourceComponent: settingsTabs.currentIndex === 0 ? weatherTab : settingsTabs.currentIndex === 1 ? systemTab : wallpaperTab + sourceComponent: settingsTabs.currentIndex === 0 ? systemTab : settingsTabs.currentIndex === 1 ? wallpaperTab : weatherTab + } + } + + Component { + id: systemTab + ColumnLayout { + anchors.fill: parent + ProfileSettings { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + anchors.margins: 16 + } + } + } + + Component { + id: wallpaperTab + ColumnLayout { + anchors.fill: parent + WallpaperSettings { + id: wallpaperSettings + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + anchors.margins: 16 + } } } @@ -130,29 +154,6 @@ PanelWindow { } } } - Component { - id: systemTab - ColumnLayout { - anchors.fill: parent - ProfileSettings { - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - anchors.margins: 16 - } - } - } - Component { - id: wallpaperTab - ColumnLayout { - anchors.fill: parent - WallpaperSettings { - id: wallpaperSettings - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - anchors.margins: 16 - } - } - } } } } @@ -160,7 +161,6 @@ PanelWindow { // Function to open the modal and initialize temp values function openSettings() { visible = true; - // Force focus on the text input after a short delay focusTimer.start(); } @@ -174,20 +174,16 @@ PanelWindow { interval: 100 repeat: false onTriggered: { - if (visible) - // Focus will be handled by the individual components - {} - } - } - - // Release focus when modal becomes invisible - onVisibleChanged: { - if (!visible) { - // Focus will be handled by the individual components - if (typeof weather !== 'undefined' && weather !== null && weather.fetchCityWeather) { - weather.fetchCityWeather(); + if (visible) { + // Focus logic can go here if needed } } } -} + // Refresh weather data when hidden + onVisibleChanged: { + if (!visible && typeof weather !== 'undefined' && weather !== null && weather.fetchCityWeather) { + weather.fetchCityWeather(); + } + } +} diff --git a/Widgets/Sidebar/Config/WallpaperSettings.qml b/Widgets/Sidebar/Config/WallpaperSettings.qml index c824721..730a935 100644 --- a/Widgets/Sidebar/Config/WallpaperSettings.qml +++ b/Widgets/Sidebar/Config/WallpaperSettings.qml @@ -1,12 +1,13 @@ import QtQuick -import QtQuick.Layouts import QtQuick.Controls +import QtQuick.Layouts import qs.Settings Rectangle { id: wallpaperSettingsCard + Layout.fillWidth: true - Layout.preferredHeight: 720 + Layout.preferredHeight: Settings.settings.useSWWW ? 720 : 360 color: Theme.surface radius: 18 @@ -15,25 +16,28 @@ Rectangle { anchors.margins: 18 spacing: 12 - // Header - RowLayout { - Layout.fillWidth: true - spacing: 12 - Text { - text: "image" - font.family: "Material Symbols Outlined" - font.pixelSize: 20 - color: Theme.accentPrimary - } - Text { - text: "Wallpaper Settings" - font.family: Theme.fontFamily - font.pixelSize: 16 - font.bold: true - color: Theme.textPrimary + + RowLayout { Layout.fillWidth: true + spacing: 12 + + Text { + text: "image" + font.family: "Material Symbols Outlined" + font.pixelSize: 20 + color: Theme.accentPrimary + } + + Text { + text: "Wallpaper Settings" + font.family: Theme.fontFamily + font.pixelSize: 16 + font.bold: true + color: Theme.textPrimary + Layout.fillWidth: true + } } - } + ColumnLayout { spacing: 8 @@ -47,7 +51,7 @@ Rectangle { color: Theme.textPrimary } - // Folder Path Input + Rectangle { Layout.fillWidth: true Layout.preferredHeight: 40 @@ -55,8 +59,10 @@ Rectangle { color: Theme.surfaceVariant border.color: folderInput.activeFocus ? Theme.accentPrimary : Theme.outline border.width: 1 + TextInput { id: folderInput + anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top @@ -77,72 +83,22 @@ Rectangle { onTextChanged: { Settings.settings.wallpaperFolder = text; } + MouseArea { anchors.fill: parent cursorShape: Qt.IBeamCursor onClicked: folderInput.forceActiveFocus() } + } + } + } - // Use SWWW Setting - RowLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - Text { - text: "Use SWWW" - font.pixelSize: 13 - font.bold: true - color: Theme.textPrimary - } - Item { - Layout.fillWidth: true - } - // Custom Material 3 Switch - Rectangle { - id: swwwSwitch - width: 52 - height: 32 - radius: 16 - color: Settings.settings.useSWWW ? Theme.accentPrimary : Theme.surfaceVariant - border.color: Settings.settings.useSWWW ? Theme.accentPrimary : Theme.outline - border.width: 2 - - Rectangle { - id: swwwThumb - width: 28 - height: 28 - radius: 14 - color: Theme.surface - border.color: Theme.outline - border.width: 1 - y: 2 - x: Settings.settings.useSWWW ? swwwSwitch.width - width - 2 : 2 - - Behavior on x { - NumberAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - Settings.settings.useSWWW = !Settings.settings.useSWWW; - } - } - } - } - - // Random Wallpaper Setting RowLayout { spacing: 8 Layout.fillWidth: true @@ -162,6 +118,7 @@ Rectangle { // Custom Material 3 Switch Rectangle { id: randomWallpaperSwitch + width: 52 height: 32 radius: 16 @@ -171,6 +128,7 @@ Rectangle { Rectangle { id: randomWallpaperThumb + width: 28 height: 28 radius: 14 @@ -185,7 +143,9 @@ Rectangle { duration: 200 easing.type: Easing.OutCubic } + } + } MouseArea { @@ -195,10 +155,12 @@ Rectangle { Settings.settings.randomWallpaper = !Settings.settings.randomWallpaper; } } + } + } - // Use Wallpaper Theme Setting + RowLayout { spacing: 8 Layout.fillWidth: true @@ -218,6 +180,7 @@ Rectangle { // Custom Material 3 Switch Rectangle { id: wallpaperThemeSwitch + width: 52 height: 32 radius: 16 @@ -227,6 +190,7 @@ Rectangle { Rectangle { id: wallpaperThemeThumb + width: 28 height: 28 radius: 14 @@ -241,7 +205,9 @@ Rectangle { duration: 200 easing.type: Easing.OutCubic } + } + } MouseArea { @@ -251,10 +217,12 @@ Rectangle { Settings.settings.useWallpaperTheme = !Settings.settings.useWallpaperTheme; } } + } + } - // Wallpaper Interval Setting + ColumnLayout { spacing: 12 Layout.fillWidth: true @@ -262,6 +230,7 @@ Rectangle { RowLayout { Layout.fillWidth: true + Text { text: "Wallpaper Interval (seconds)" font.pixelSize: 13 @@ -278,16 +247,21 @@ Rectangle { font.pixelSize: 13 color: Theme.textPrimary } + } Slider { id: intervalSlider + Layout.fillWidth: true from: 10 to: 900 stepSize: 10 value: Settings.settings.wallpaperInterval snapMode: Slider.SnapAlways + onMoved: { + Settings.settings.wallpaperInterval = Math.round(value); + } background: Rectangle { x: intervalSlider.leftPadding @@ -305,6 +279,7 @@ Rectangle { color: Theme.accentPrimary radius: 2 } + } handle: Rectangle { @@ -318,17 +293,78 @@ Rectangle { border.width: 2 } - onMoved: { - Settings.settings.wallpaperInterval = Math.round(value); - } } + } - // Resize Mode Setting + + RowLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Use SWWW" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Item { + Layout.fillWidth: true + } + + // Custom Material 3 Switch + Rectangle { + id: swwwSwitch + + width: 52 + height: 32 + radius: 16 + color: Settings.settings.useSWWW ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.useSWWW ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: swwwThumb + + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.useSWWW ? swwwSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + + } + + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.useSWWW = !Settings.settings.useSWWW; + } + } + + } + + } + + ColumnLayout { spacing: 12 Layout.fillWidth: true Layout.topMargin: 16 + visible: Settings.settings.useSWWW Text { text: "Resize Mode" @@ -339,10 +375,14 @@ Rectangle { ComboBox { id: resizeComboBox + Layout.fillWidth: true Layout.preferredHeight: 40 model: ["no", "crop", "fit", "stretch"] currentIndex: model.indexOf(Settings.settings.wallpaperResize) + onActivated: { + Settings.settings.wallpaperResize = model[index]; + } background: Rectangle { implicitWidth: 120 @@ -385,7 +425,9 @@ Rectangle { model: resizeComboBox.popup.visible ? resizeComboBox.delegateModel : null currentIndex: resizeComboBox.highlightedIndex - ScrollIndicator.vertical: ScrollIndicator {} + ScrollIndicator.vertical: ScrollIndicator { + } + } background: Rectangle { @@ -394,10 +436,13 @@ Rectangle { border.width: 1 radius: 16 } + } delegate: ItemDelegate { width: resizeComboBox.width + highlighted: resizeComboBox.highlightedIndex === index + contentItem: Text { text: modelData font.family: Theme.fontFamily @@ -406,24 +451,23 @@ Rectangle { verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } - highlighted: resizeComboBox.highlightedIndex === index background: Rectangle { color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" } + } - onActivated: { - Settings.settings.wallpaperResize = model[index]; - } } + } - // Transition Type Setting + ColumnLayout { spacing: 12 Layout.fillWidth: true Layout.topMargin: 16 + visible: Settings.settings.useSWWW Text { text: "Transition Type" @@ -434,10 +478,14 @@ Rectangle { ComboBox { id: transitionTypeComboBox + Layout.fillWidth: true Layout.preferredHeight: 40 model: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"] currentIndex: model.indexOf(Settings.settings.transitionType) + onActivated: { + Settings.settings.transitionType = model[index]; + } background: Rectangle { implicitWidth: 120 @@ -480,7 +528,9 @@ Rectangle { model: transitionTypeComboBox.popup.visible ? transitionTypeComboBox.delegateModel : null currentIndex: transitionTypeComboBox.highlightedIndex - ScrollIndicator.vertical: ScrollIndicator {} + ScrollIndicator.vertical: ScrollIndicator { + } + } background: Rectangle { @@ -489,10 +539,13 @@ Rectangle { border.width: 1 radius: 16 } + } delegate: ItemDelegate { width: transitionTypeComboBox.width + highlighted: transitionTypeComboBox.highlightedIndex === index + contentItem: Text { text: modelData font.family: Theme.fontFamily @@ -501,27 +554,27 @@ Rectangle { verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } - highlighted: transitionTypeComboBox.highlightedIndex === index background: Rectangle { color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" } + } - onActivated: { - Settings.settings.transitionType = model[index]; - } } + } - // Transition FPS Setting + ColumnLayout { spacing: 12 Layout.fillWidth: true Layout.topMargin: 16 + visible: Settings.settings.useSWWW RowLayout { Layout.fillWidth: true + Text { text: "Transition FPS" font.pixelSize: 13 @@ -538,16 +591,21 @@ Rectangle { font.pixelSize: 13 color: Theme.textPrimary } + } Slider { id: fpsSlider + Layout.fillWidth: true from: 30 to: 500 stepSize: 5 value: Settings.settings.transitionFps snapMode: Slider.SnapAlways + onMoved: { + Settings.settings.transitionFps = Math.round(value); + } background: Rectangle { x: fpsSlider.leftPadding @@ -565,6 +623,7 @@ Rectangle { color: Theme.accentPrimary radius: 2 } + } handle: Rectangle { @@ -578,20 +637,20 @@ Rectangle { border.width: 2 } - onMoved: { - Settings.settings.transitionFps = Math.round(value); - } } + } - // Transition Duration Setting + ColumnLayout { spacing: 12 Layout.fillWidth: true Layout.topMargin: 16 - + visible: Settings.settings.useSWWW + RowLayout { Layout.fillWidth: true + Text { text: "Transition Duration (seconds)" font.pixelSize: 13 @@ -608,16 +667,21 @@ Rectangle { font.pixelSize: 13 color: Theme.textPrimary } + } Slider { id: durationSlider + Layout.fillWidth: true - from: 0.250 - to: 10.0 - stepSize: 0.050 + from: 0.25 + to: 10 + stepSize: 0.05 value: Settings.settings.transitionDuration snapMode: Slider.SnapAlways + onMoved: { + Settings.settings.transitionDuration = value; + } background: Rectangle { x: durationSlider.leftPadding @@ -635,6 +699,7 @@ Rectangle { color: Theme.accentPrimary radius: 2 } + } handle: Rectangle { @@ -648,10 +713,10 @@ Rectangle { border.width: 2 } - onMoved: { - Settings.settings.transitionDuration = value; - } } + } + } + } diff --git a/Widgets/Sidebar/Config/WeatherSettings.qml b/Widgets/Sidebar/Config/WeatherSettings.qml index d7689dc..511e79a 100644 --- a/Widgets/Sidebar/Config/WeatherSettings.qml +++ b/Widgets/Sidebar/Config/WeatherSettings.qml @@ -14,7 +14,7 @@ Rectangle { anchors.margins: 18 spacing: 12 - // Weather Settings Header + RowLayout { Layout.fillWidth: true spacing: 12 @@ -36,7 +36,7 @@ Rectangle { } } - // Weather City Setting + ColumnLayout { spacing: 8 Layout.fillWidth: true @@ -93,7 +93,7 @@ Rectangle { } } - // Temperature Unit Setting + RowLayout { spacing: 12 Layout.fillWidth: true @@ -160,7 +160,7 @@ Rectangle { } - // Random Wallpaper Setting + RowLayout { spacing: 8 Layout.fillWidth: true @@ -216,7 +216,7 @@ Rectangle { } } - // Reverse Day Month Setting + RowLayout { spacing: 8 Layout.fillWidth: true diff --git a/Widgets/Sidebar/Panel/BluetoothPanel.qml b/Widgets/Sidebar/Panel/BluetoothPanel.qml index f87aa8f..3837d5c 100644 --- a/Widgets/Sidebar/Panel/BluetoothPanel.qml +++ b/Widgets/Sidebar/Panel/BluetoothPanel.qml @@ -12,7 +12,7 @@ Item { id: root property alias panel: bluetoothPanelModal - // For showing error/status messages + property string statusMessage: "" property bool statusPopupVisible: false @@ -90,7 +90,7 @@ Item { Rectangle { anchors.fill: parent color: Theme.backgroundPrimary - radius: 24 + radius: 20 ColumnLayout { anchors.fill: parent @@ -145,7 +145,7 @@ Item { opacity: 0.12 } - // Content area (centered, in a card) + Rectangle { Layout.fillWidth: true Layout.preferredHeight: 640 diff --git a/Widgets/Sidebar/Panel/Music.qml b/Widgets/Sidebar/Panel/Music.qml index 553f3b3..6d24310 100644 --- a/Widgets/Sidebar/Panel/Music.qml +++ b/Widgets/Sidebar/Panel/Music.qml @@ -1,7 +1,7 @@ -import QtQuick +import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Qt5Compat.GraphicalEffects +import QtQuick.Effects import qs.Settings import qs.Components import qs.Services @@ -53,24 +53,108 @@ Rectangle { spacing: 12 visible: !!MusicManager.currentPlayer - // Album art and spectrum + // Player selector + ComboBox { + id: playerSelector + Layout.fillWidth: true + Layout.preferredHeight: 40 + visible: MusicManager.getAvailablePlayers().length > 1 + model: MusicManager.getAvailablePlayers() + textRole: "identity" + currentIndex: MusicManager.selectedPlayerIndex + + background: Rectangle { + implicitWidth: 120 + implicitHeight: 40 + color: Theme.surfaceVariant + border.color: playerSelector.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + radius: 16 + } + + contentItem: Text { + leftPadding: 12 + rightPadding: playerSelector.indicator.width + playerSelector.spacing + text: playerSelector.displayText + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + indicator: Text { + x: playerSelector.width - width - 12 + y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2 + text: "arrow_drop_down" + font.family: "Material Symbols Outlined" + font.pixelSize: 24 + color: Theme.textPrimary + } + + popup: Popup { + y: playerSelector.height + width: playerSelector.width + implicitHeight: contentItem.implicitHeight + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: playerSelector.popup.visible ? playerSelector.delegateModel : null + currentIndex: playerSelector.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + color: Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 + radius: 16 + } + } + + delegate: ItemDelegate { + width: playerSelector.width + contentItem: Text { + text: modelData.identity + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + highlighted: playerSelector.highlightedIndex === index + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + } + + onActivated: { + MusicManager.selectedPlayerIndex = index; + MusicManager.updateCurrentPlayer(); + } + } + + // Album art with spectrum visualizer RowLayout { spacing: 12 Layout.fillWidth: true - // Album art with spectrum + // Album art container with circular spectrum overlay Item { id: albumArtContainer - width: 96; height: 96 // enough for spectrum and art (will adjust if needed) + width: 96 + height: 96 // enough for spectrum and art (will adjust if needed) Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - // Spectrum visualizer + // Circular spectrum visualizer around album art CircularSpectrum { id: spectrum values: MusicManager.cavaValues anchors.centerIn: parent - innerRadius: 30 // just outside 60x60 album art - outerRadius: 48 // how far bars extend + innerRadius: 30 // Position just outside 60x60 album art + outerRadius: 48 // Extend bars outward from album art fillColor: Theme.accentPrimary strokeColor: Theme.accentPrimary strokeWidth: 0 @@ -80,7 +164,8 @@ Rectangle { // Album art image Rectangle { id: albumArtwork - width: 60; height: 60 + width: 60 + height: 60 anchors.centerIn: parent radius: 30 // circle color: Qt.darker(Theme.surface, 1.1) @@ -93,6 +178,7 @@ Rectangle { anchors.margins: 2 fillMode: Image.PreserveAspectCrop smooth: true + mipmap: true cache: false asynchronous: true sourceSize.width: 60 @@ -100,20 +186,29 @@ Rectangle { source: MusicManager.trackArtUrl visible: source.toString() !== "" - // Rounded corners using layer + // Apply circular mask for rounded corners layer.enabled: true - layer.effect: OpacityMask { - cached: true - maskSource: Rectangle { - width: albumArt.width - height: albumArt.height - radius: albumArt.width / 2 // circle - visible: false - } + layer.effect: MultiEffect { + maskEnabled: true + maskSource: mask } } - // Fallback icon + Item { + id: mask + + anchors.fill: albumArt + layer.enabled: true + visible: false + + Rectangle { + width: albumArt.width + height: albumArt.height + radius: albumArt.width / 2 // circle + } + } + + // Fallback icon when no album art available Text { anchors.centerIn: parent text: "album" @@ -171,8 +266,12 @@ Rectangle { color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15) Layout.fillWidth: true - property real progressRatio: Math.min(1, MusicManager.trackLength > 0 ? - (MusicManager.currentPosition / MusicManager.trackLength) : 0) + property real progressRatio: { + if (!MusicManager.currentPlayer || !MusicManager.isPlaying || MusicManager.trackLength <= 0) { + return 0; + } + return Math.min(1, MusicManager.currentPosition / MusicManager.trackLength); + } Rectangle { id: progressFill @@ -182,7 +281,9 @@ Rectangle { color: Theme.accentPrimary Behavior on width { - NumberAnimation { duration: 200 } + NumberAnimation { + duration: 200 + } } } @@ -196,14 +297,16 @@ Rectangle { border.color: Qt.lighter(Theme.accentPrimary, 1.3) border.width: 1 - x: Math.max(0, Math.min(parent.width - width, progressFill.width - width/2)) + x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) anchors.verticalCenter: parent.verticalCenter visible: MusicManager.trackLength > 0 scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0 Behavior on scale { - NumberAnimation { duration: 150 } + NumberAnimation { + duration: 150 + } } } @@ -215,15 +318,15 @@ Rectangle { cursorShape: Qt.PointingHandCursor enabled: MusicManager.trackLength > 0 && MusicManager.canSeek - onClicked: function(mouse) { - let ratio = mouse.x / width - MusicManager.seekByRatio(ratio) + onClicked: function (mouse) { + let ratio = mouse.x / width; + MusicManager.seekByRatio(ratio); } - onPositionChanged: function(mouse) { + onPositionChanged: function (mouse) { if (pressed) { - let ratio = Math.max(0, Math.min(1, mouse.x / width)) - MusicManager.seekByRatio(ratio) + let ratio = Math.max(0, Math.min(1, mouse.x / width)); + MusicManager.seekByRatio(ratio); } } } @@ -318,4 +421,4 @@ Rectangle { } } } -} \ No newline at end of file +} diff --git a/Widgets/Sidebar/Panel/PanelPopup.qml b/Widgets/Sidebar/Panel/PanelPopup.qml index a656072..0ddc9c0 100644 --- a/Widgets/Sidebar/Panel/PanelPopup.qml +++ b/Widgets/Sidebar/Panel/PanelPopup.qml @@ -35,7 +35,7 @@ PanelWithOverlay { anchors.top: parent.top anchors.right: parent.right - // Animation properties + property real slideOffset: width property bool isAnimating: false @@ -59,15 +59,15 @@ PanelWithOverlay { if (sidebarPopupRect.settingsModal && sidebarPopupRect.settingsModal.visible) { sidebarPopupRect.settingsModal.visible = false; } - if (sidebarPopupRect.wallpaperPanelModal && sidebarPopupRect.wallpaperPanelModal.visible) { - sidebarPopupRect.wallpaperPanelModal.visible = false; + if (wallpaperPanel && wallpaperPanel.visible) { + wallpaperPanel.visible = false; + } + if (sidebarPopupRect.wifiPanelModal && sidebarPopupRect.wifiPanelModal.visible) { + sidebarPopupRect.wifiPanelModal.visible = false; + } + if (sidebarPopupRect.bluetoothPanelModal && sidebarPopupRect.bluetoothPanelModal.visible) { + sidebarPopupRect.bluetoothPanelModal.visible = false; } - if (sidebarPopupRect.wifiPanelModal && sidebarPopupRect.wifiPanelModal.visible) { - sidebarPopupRect.wifiPanelModal.visible = false; - } - if (sidebarPopupRect.bluetoothPanelModal && sidebarPopupRect.bluetoothPanelModal.visible) { - sidebarPopupRect.bluetoothPanelModal.visible = false; - } if (sidebarPopup.visible) { slideAnim.from = 0; slideAnim.to = width; @@ -85,7 +85,7 @@ PanelWithOverlay { onStopped: { if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) { sidebarPopup.visible = false; - // Stop monitoring and background tasks when hidden + if (weather) weather.stopWeatherFetch(); if (systemWidget) @@ -125,7 +125,6 @@ PanelWithOverlay { } property alias settingsModal: settingsModal - property alias wallpaperPanelModal: wallpaperPanelModal property alias wifiPanelModal: wifiPanel.panel property alias bluetoothPanelModal: bluetoothPanel.panel SettingsModal { @@ -314,7 +313,7 @@ PanelWithOverlay { settingsModal.visible = true; } onWallpaperRequested: { - wallpaperPanelModal.visible = true; + wallpaperPanel.visible = true; } } } @@ -339,7 +338,15 @@ PanelWithOverlay { videoPath += "/"; } var outputPath = videoPath + filename; - var command = "gpu-screen-recorder -w portal -f 60 -a default_output -o " + outputPath; + var command = "gpu-screen-recorder -w portal" + + " -f " + Settings.settings.recordingFrameRate + + " -a default_output" + + " -k " + Settings.settings.recordingCodec + + " -ac " + Settings.settings.audioCodec + + " -q " + Settings.settings.recordingQuality + + " -cursor " + (Settings.settings.showCursor ? "yes" : "no") + + " -cr " + Settings.settings.colorRange + + " -o " + outputPath; Quickshell.execDetached(["sh", "-c", command]); isRecording = true; quickAccessWidget.isRecording = true; @@ -403,15 +410,13 @@ PanelWithOverlay { } WallpaperPanel { - id: wallpaperPanelModal - visible: false + id: wallpaperPanel Component.onCompleted: { if (parent) { - wallpaperPanelModal.anchors.top = parent.top; - wallpaperPanelModal.anchors.right = parent.right; + anchors.top = parent.top; + anchors.right = parent.right; } } - // Add a close button inside WallpaperPanel.qml for user to close the modal } } } diff --git a/Widgets/Sidebar/Panel/PowerProfile.qml b/Widgets/Sidebar/Panel/PowerProfile.qml index c68fa4f..bdd9c49 100644 --- a/Widgets/Sidebar/Panel/PowerProfile.qml +++ b/Widgets/Sidebar/Panel/PowerProfile.qml @@ -17,7 +17,7 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter spacing: 20 - // Performance + Rectangle { width: 36; height: 36 radius: 18 @@ -63,7 +63,7 @@ Rectangle { } } - // Balanced + Rectangle { width: 36; height: 36 radius: 18 @@ -109,7 +109,7 @@ Rectangle { } } - // Power Saver + Rectangle { width: 36; height: 36 radius: 18 diff --git a/Widgets/Sidebar/Panel/QuickAccess.qml b/Widgets/Sidebar/Panel/QuickAccess.qml index 93975f1..ac33aec 100644 --- a/Widgets/Sidebar/Panel/QuickAccess.qml +++ b/Widgets/Sidebar/Panel/QuickAccess.qml @@ -1,7 +1,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -import Qt5Compat.GraphicalEffects +import QtQuick.Effects import Quickshell import Quickshell.Io import qs.Settings @@ -32,7 +32,7 @@ Rectangle { anchors.margins: 18 spacing: 12 - // Settings Button + Rectangle { id: settingsButton Layout.fillWidth: true @@ -75,7 +75,7 @@ Rectangle { } } - // Screen Recorder Button + Rectangle { id: recorderButton Layout.fillWidth: true @@ -123,7 +123,7 @@ Rectangle { } } - // Wallpaper Button + Rectangle { id: wallpaperButton Layout.fillWidth: true @@ -168,10 +168,10 @@ Rectangle { } } - // Properties + property bool panelVisible: false - // Timer to check if recording is active + Timer { interval: 2000 repeat: true @@ -185,7 +185,7 @@ Rectangle { } } - // Process to check if gpu-screen-recorder is running + Process { id: checkRecordingProcess command: ["pgrep", "-f", "gpu-screen-recorder.*portal"] diff --git a/Widgets/Sidebar/Panel/System.qml b/Widgets/Sidebar/Panel/System.qml index f2a6a21..1ffe456 100644 --- a/Widgets/Sidebar/Panel/System.qml +++ b/Widgets/Sidebar/Panel/System.qml @@ -1,9 +1,10 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -import Qt5Compat.GraphicalEffects +import QtQuick.Effects import Quickshell import Quickshell.Io +import Quickshell.Widgets import qs.Settings import qs.Widgets import qs.Widgets.LockScreen @@ -29,19 +30,19 @@ Rectangle { anchors.margins: 18 spacing: 12 - // User info row + RowLayout { Layout.fillWidth: true spacing: 12 - // Profile image + Rectangle { width: 48 height: 48 radius: 24 color: Theme.accentPrimary - // Border + Rectangle { anchors.fill: parent color: "transparent" @@ -51,41 +52,10 @@ Rectangle { z: 2 } - OpacityMask { - anchors.fill: parent - source: Image { - id: avatarImage - anchors.fill: parent - source: Settings.settings.profileImage !== undefined ? Settings.settings.profileImage : "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - sourceSize.width: 44 - sourceSize.height: 44 - } - maskSource: Rectangle { - width: 44 - height: 44 - radius: 22 - visible: false - } - visible: Settings.settings.profileImage !== undefined && Settings.settings.profileImage !== "" - z: 1 - } - - // Fallback icon - Text { - anchors.centerIn: parent - text: "person" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 - color: Theme.onAccent - visible: Settings.settings.profileImage === undefined || Settings.settings.profileImage === "" - z: 0 - } + Avatar {} } - // User info text + ColumnLayout { spacing: 4 Layout.fillWidth: true @@ -106,12 +76,12 @@ Rectangle { } } - // Spacer + Item { Layout.fillWidth: true } - // System menu button + Rectangle { id: systemButton width: 32 @@ -153,7 +123,7 @@ Rectangle { id: systemMenu anchors.top: systemButton.bottom anchors.right: systemButton.right - // System menu popup + Rectangle { width: 160 @@ -167,7 +137,7 @@ Rectangle { anchors.top: parent.top anchors.right: parent.right - // Position below system button + anchors.rightMargin: 32 anchors.topMargin: systemButton.y + systemButton.height + 48 @@ -176,7 +146,7 @@ Rectangle { anchors.margins: 8 spacing: 4 - // Lock button + Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 @@ -216,7 +186,7 @@ Rectangle { } } - // Suspend button + Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 @@ -255,7 +225,7 @@ Rectangle { } } - // Reboot button + Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 @@ -295,7 +265,7 @@ Rectangle { } } - // Logout button + Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 @@ -334,7 +304,7 @@ Rectangle { } } - // Shutdown button + Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 @@ -376,10 +346,10 @@ Rectangle { } } - // Properties + property string uptimeText: "--:--" - // Process to get uptime + Process { id: uptimeProcess command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"] @@ -410,7 +380,7 @@ Rectangle { running: false } - Process { + Process { id: logoutProcessNiri command: ["niri", "msg", "action", "quit", "--skip-confirmation"] running: false @@ -422,13 +392,19 @@ Rectangle { running: false } + Process { + id: logoutProcess + command: ["loginctl", "terminate-user", Quickshell.env("USER")] + running: false + } + function logout() { if (WorkspaceManager.isNiri) { logoutProcessNiri.running = true; } else if (WorkspaceManager.isHyprland) { logoutProcessHyprland.running = true; } else { - // fallback or error + console.warn("No supported compositor detected for logout"); } } @@ -445,19 +421,18 @@ Rectangle { rebootProcess.running = true; } - property bool panelVisible: false - // Trigger initial update when panel becomes visible + onPanelVisibleChanged: { if (panelVisible) { updateSystemInfo(); } } - // Timer to update uptime - only runs when panel is visible + Timer { - interval: 60000 // Update every minute + interval: 60000 repeat: true running: panelVisible onTriggered: updateSystemInfo() @@ -471,8 +446,8 @@ Rectangle { uptimeProcess.running = true; } - // Add lockscreen instance (hidden by default) + LockScreen { id: lockScreen } -} \ No newline at end of file +} diff --git a/Widgets/Sidebar/Panel/SystemMonitor.qml b/Widgets/Sidebar/Panel/SystemMonitor.qml index b9380e4..8b0e3ee 100644 --- a/Widgets/Sidebar/Panel/SystemMonitor.qml +++ b/Widgets/Sidebar/Panel/SystemMonitor.qml @@ -12,6 +12,7 @@ Rectangle { height: 250 color: "transparent" + // Track visibility state for panel integration property bool isVisible: false Rectangle { @@ -26,7 +27,8 @@ Rectangle { spacing: 12 Layout.alignment: Qt.AlignVCenter - // CPU Usage + + // CPU usage indicator with circular progress bar Item { width: 50; height: 50 CircularProgressBar { @@ -55,7 +57,8 @@ Rectangle { } } - // Cpu Temp + + // CPU temperature indicator with circular progress bar Item { width: 50; height: 50 CircularProgressBar { @@ -85,7 +88,8 @@ Rectangle { } } - // Memory Usage + + // Memory usage indicator with circular progress bar Item { width: 50; height: 50 CircularProgressBar { @@ -114,7 +118,8 @@ Rectangle { } } - // Disk Usage + + // Disk usage indicator with circular progress bar Item { width: 50; height: 50 CircularProgressBar { diff --git a/Widgets/Sidebar/Panel/WallpaperPanel.qml b/Widgets/Sidebar/Panel/WallpaperPanel.qml index aa444a5..be4c95f 100644 --- a/Widgets/Sidebar/Panel/WallpaperPanel.qml +++ b/Widgets/Sidebar/Panel/WallpaperPanel.qml @@ -30,7 +30,7 @@ PanelWindow { } onVisibleChanged: { - if (wallpaperPanelModal.visible) { + if (wallpaperPanel.visible) { wallpapers = WallpaperManager.wallpaperList } else { wallpapers = [] @@ -40,7 +40,7 @@ PanelWindow { Rectangle { anchors.fill: parent color: Theme.backgroundPrimary - radius: 24 + radius: 20 ColumnLayout { anchors.fill: parent anchors.margins: 32 @@ -81,7 +81,9 @@ PanelWindow { id: closeButtonArea anchors.fill: parent hoverEnabled: true - onClicked: wallpaperPanelModal.visible = false + onClicked: { + wallpaperPanel.visible = false; + } cursorShape: Qt.PointingHandCursor } } @@ -92,7 +94,7 @@ PanelWindow { color: Theme.outline opacity: 0.12 } - // Wallpaper grid area + Item { Layout.fillWidth: true Layout.fillHeight: true @@ -114,7 +116,7 @@ PanelWindow { cellWidth: Math.max(120, (scrollView.width / 3) - 12) cellHeight: cellWidth * 0.6 model: wallpapers - cacheBuffer: 32 + cacheBuffer: 64 leftMargin: 8 rightMargin: 8 topMargin: 8 @@ -129,7 +131,7 @@ PanelWindow { color: Qt.darker(Theme.backgroundPrimary, 1.1) radius: 12 border.color: Settings.settings.currentWallpaper === modelData ? Theme.accentPrimary : Theme.outline - border.width: Settings.settings.currentWallpaper === modelData ? 3 : 1 + border.width: 2 Image { id: wallpaperImage anchors.fill: parent @@ -137,8 +139,19 @@ PanelWindow { fillMode: Image.PreserveAspectCrop asynchronous: true cache: true - sourceSize.width: Math.min(width, 150) - sourceSize.height: Math.min(height, 90) + smooth: true + mipmap: true + + sourceSize.width: Math.min(width, 480) + sourceSize.height: Math.min(height, 270) + + opacity: (wallpaperImage.status == Image.Ready) ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } + } } MouseArea { anchors.fill: parent diff --git a/Widgets/Sidebar/Panel/Weather.qml b/Widgets/Sidebar/Panel/Weather.qml index f9feb53..c6d0fd8 100644 --- a/Widgets/Sidebar/Panel/Weather.qml +++ b/Widgets/Sidebar/Panel/Weather.qml @@ -54,17 +54,17 @@ Rectangle { anchors.margins: 18 spacing: 12 - // Current weather row + RowLayout { spacing: 12 Layout.fillWidth: true - // Weather icon and basic info section + RowLayout { spacing: 12 Layout.preferredWidth: 140 - // Weather icon + Text { id: weatherIcon text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud" @@ -103,13 +103,13 @@ Rectangle { } } } - // Spacer to push content to the right + Item { Layout.fillWidth: true } } - // Separator line + Rectangle { width: parent.width height: 1 @@ -119,7 +119,7 @@ Rectangle { Layout.bottomMargin: 2 } - // 5-day forecast row + RowLayout { spacing: 12 Layout.fillWidth: true @@ -132,7 +132,7 @@ Rectangle { spacing: 2 Layout.alignment: Qt.AlignHCenter Text { - // Day of the week (e.g., Mon) + text: Qt.formatDateTime(new Date(weatherData.daily.time[index]), "ddd") font.family: Theme.fontFamily font.pixelSize: 12 @@ -141,7 +141,7 @@ Rectangle { Layout.alignment: Qt.AlignHCenter } Text { - // Material Symbol icon + text: materialSymbolForCode(weatherData.daily.weathercode[index]) font.family: "Material Symbols Outlined" font.pixelSize: 22 @@ -150,7 +150,7 @@ Rectangle { Layout.alignment: Qt.AlignHCenter } Text { - // High/low temp + text: weatherData && weatherData.daily ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.daily.temperature_2m_max[index] * 9/5 + 32)}° / ${Math.round(weatherData.daily.temperature_2m_min[index] * 9/5 + 32)}°` : `${Math.round(weatherData.daily.temperature_2m_max[index])}° / ${Math.round(weatherData.daily.temperature_2m_min[index])}°`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--° / --°" : "--° / --°") font.family: Theme.fontFamily font.pixelSize: 12 @@ -162,7 +162,7 @@ Rectangle { } } - // Error message + Text { text: errorString color: Theme.error @@ -175,16 +175,16 @@ Rectangle { } } - // Weather code to Material Symbol ligature mapping + function materialSymbolForCode(code) { - if (code === 0) return "sunny"; // Clear - if (code === 1 || code === 2) return "partly_cloudy_day"; // Mainly clear/partly cloudy - if (code === 3) return "cloud"; // Overcast - if (code >= 45 && code <= 48) return "foggy"; // Fog - if (code >= 51 && code <= 67) return "rainy"; // Drizzle - if (code >= 71 && code <= 77) return "weather_snowy"; // Snow - if (code >= 80 && code <= 82) return "rainy"; // Rain showers - if (code >= 95 && code <= 99) return "thunderstorm"; // Thunderstorm + if (code === 0) return "sunny"; + if (code === 1 || code === 2) return "partly_cloudy_day"; + if (code === 3) return "cloud"; + if (code >= 45 && code <= 48) return "foggy"; + if (code >= 51 && code <= 67) return "rainy"; + if (code >= 71 && code <= 77) return "weather_snowy"; + if (code >= 80 && code <= 82) return "rainy"; + if (code >= 95 && code <= 99) return "thunderstorm"; return "cloud"; } function weatherDescriptionForCode(code) { diff --git a/Widgets/Sidebar/Panel/WifiPanel.qml b/Widgets/Sidebar/Panel/WifiPanel.qml index 008a3c1..e70ff7f 100644 --- a/Widgets/Sidebar/Panel/WifiPanel.qml +++ b/Widgets/Sidebar/Panel/WifiPanel.qml @@ -11,72 +11,128 @@ import qs.Helpers Item { property alias panel: wifiPanelModal - + function showAt() { wifiPanelModal.visible = true; wifiLogic.refreshNetworks(); } + Component.onCompleted: { + existingNetwork.running = true; + } + function signalIcon(signal) { - if (signal >= 80) return "network_wifi"; - if (signal >= 60) return "network_wifi_3_bar"; - if (signal >= 40) return "network_wifi_2_bar"; - if (signal >= 20) return "network_wifi_1_bar"; + if (signal >= 80) + return "network_wifi"; + if (signal >= 60) + return "network_wifi_3_bar"; + if (signal >= 40) + return "network_wifi_2_bar"; + if (signal >= 20) + return "network_wifi_1_bar"; return "wifi_0_bar"; } + Process { + id: existingNetwork + running: false + command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] + stdout: StdioCollector { + onStreamFinished: { + const lines = text.split("\n"); + const networksMap = {}; + + refreshIndicator.running = true; + refreshIndicator.visible = true; + + for (let i = 0; i < lines.length; ++i) { + const line = lines[i].trim(); + if (!line) + continue; + + const parts = line.split(":"); + if (parts.length < 2) { + console.warn("Malformed nmcli output line:", line); + continue; + } + + const ssid = wifiLogic.replaceQuickshell(parts[0]); + const type = parts[1]; + + if (ssid) { + networksMap[ssid] = { + ssid: ssid, + type: type + }; + } + } + scanProcess.existingNetwork = networksMap; + scanProcess.running = true; + } + } + } + Process { id: scanProcess running: false command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] - onRunningChanged: { - // Removed debug log - } + + property var existingNetwork + stdout: StdioCollector { onStreamFinished: { - var lines = text.split("\n"); - var nets = []; - var seen = {}; - for (var i = 0; i < lines.length; ++i) { - var line = lines[i].trim(); - if (!line) continue; - var parts = line.split(":"); - var ssid = parts[0]; - var security = parts[1]; - var signal = parseInt(parts[2]); - var inUse = parts[3] === "*"; + const lines = text.split("\n"); + const networksMap = {}; + + for (let i = 0; i < lines.length; ++i) { + const line = lines[i].trim(); + if (!line) + continue; + + const parts = line.split(":"); + if (parts.length < 4) { + console.warn("Malformed nmcli output line:", line); + continue; + } + const ssid = parts[0]; + const security = parts[1]; + const signal = parseInt(parts[2]); + const inUse = parts[3] === "*"; + if (ssid) { - if (!seen[ssid]) { - // First time seeing this SSID - nets.push({ ssid: ssid, security: security, signal: signal, connected: inUse }); - seen[ssid] = true; + if (!networksMap[ssid]) { + networksMap[ssid] = { + ssid: ssid, + security: security, + signal: signal, + connected: inUse, + existing: ssid in scanProcess.existingNetwork + }; } else { - // SSID already exists, update if this entry has better signal or is connected - for (var j = 0; j < nets.length; ++j) { - if (nets[j].ssid === ssid) { - // Update connection status if this entry is connected - if (inUse) { - nets[j].connected = true; - } - // Update signal if this entry has better signal - if (signal > nets[j].signal) { - nets[j].signal = signal; - nets[j].security = security; - } - break; - } + const existingNet = networksMap[ssid]; + if (inUse) { + existingNet.connected = true; + } + if (signal > existingNet.signal) { + existingNet.signal = signal; + existingNet.security = security; } } } } - wifiLogic.networks = nets; + + + wifiLogic.networks = networksMap; + scanProcess.existingNetwork = {}; + refreshIndicator.running = false; + refreshIndicator.visible = false; } } } QtObject { id: wifiLogic - property var networks: [] + property var networks: {} property var anchorItem: null property real anchorX property real anchorY @@ -90,54 +146,98 @@ Item { property string connectSecurity: "" property var pendingConnect: null property string detectedInterface: "" + property string actionPanelSsid: "" - function profileNameForSsid(ssid) { - return "quickshell-" + ssid.replace(/[^a-zA-Z0-9]/g, "_"); + function replaceQuickshell(ssid: string): string { + const newName = ssid.replace("quickshell-", ""); + + if (!ssid.startsWith("quickshell-")) { + return newName; + } + + if (wifiLogic.networks && newName in wifiLogic.networks) { + console.log(`Quickshell ${newName} already exists, deleting old profile`) + deleteProfileProcess.connName = ssid; + deleteProfileProcess.running = true; + } + + console.log(`Changing from ${ssid} to ${newName}`) + renameConnectionProcess.oldName = ssid; + renameConnectionProcess.newName = newName; + renameConnectionProcess.running = true; + + return newName; } + function disconnectNetwork(ssid) { - var profileName = wifiLogic.profileNameForSsid(ssid); + const profileName = ssid; disconnectProfileProcess.connectionName = profileName; disconnectProfileProcess.running = true; } function refreshNetworks() { - scanProcess.running = true; + existingNetwork.running = true; } function showAt() { wifiPanelModal.visible = true; wifiLogic.refreshNetworks(); } function connectNetwork(ssid, security) { - wifiLogic.pendingConnect = {ssid: ssid, security: security, password: ""}; - listConnectionsProcess.running = true; + wifiLogic.pendingConnect = { + ssid: ssid, + security: security, + password: "" + }; + wifiLogic.doConnect(); } function submitPassword() { - wifiLogic.pendingConnect = {ssid: wifiLogic.passwordPromptSsid, security: wifiLogic.connectSecurity, password: wifiLogic.passwordInput}; - listConnectionsProcess.running = true; + wifiLogic.pendingConnect = { + ssid: wifiLogic.passwordPromptSsid, + security: wifiLogic.connectSecurity, + password: wifiLogic.passwordInput + }; + wifiLogic.doConnect(); } function doConnect() { - var params = wifiLogic.pendingConnect; + const params = wifiLogic.pendingConnect; + if (!params) + return; + wifiLogic.connectingSsid = params.ssid; + + + const targetNetwork = wifiLogic.networks[params.ssid]; + + + if (targetNetwork && targetNetwork.existing) { + + upConnectionProcess.profileName = params.ssid; + upConnectionProcess.running = true; + wifiLogic.pendingConnect = null; + return; + } + + if (params.security && params.security !== "--") { getInterfaceProcess.running = true; - } else { - connectProcess.security = params.security; - connectProcess.ssid = params.ssid; - connectProcess.password = params.password; - connectProcess.running = true; - wifiLogic.pendingConnect = null; + return; } + connectProcess.security = params.security; + connectProcess.ssid = params.ssid; + connectProcess.password = params.password; + connectProcess.running = true; + wifiLogic.pendingConnect = null; } function isSecured(security) { return security && security.trim() !== "" && security.trim() !== "--"; } } - // Disconnect, delete profile, refresh + Process { id: disconnectProfileProcess property string connectionName: "" running: false - command: ["nmcli", "connection", "down", "id", connectionName] + command: ["nmcli", "connection", "down", connectionName] onRunningChanged: { if (!running) { wifiLogic.refreshNetworks(); @@ -145,63 +245,70 @@ Item { } } + // Process to rename a connection Process { - id: listConnectionsProcess + id: renameConnectionProcess running: false - command: ["nmcli", "-t", "-f", "NAME", "connection", "show"] + property string oldName: "" + property string newName: "" + command: ["nmcli", "connection", "modify", oldName, "connection.id", newName] + stdout: StdioCollector { onStreamFinished: { - var params = wifiLogic.pendingConnect; - var lines = text.split("\n"); - var expectedProfile = wifiLogic.profileNameForSsid(params.ssid); - var foundProfile = null; - for (var i = 0; i < lines.length; ++i) { - if (lines[i] === expectedProfile) { - foundProfile = lines[i]; - break; - } - } - if (foundProfile) { - // Profile exists, just bring it up (no password prompt) - upConnectionProcess.profileName = foundProfile; - upConnectionProcess.running = true; - } else { - // No profile: check if secured - if (wifiLogic.isSecured(params.security)) { - if (params.password && params.password.length > 0) { - // Password provided, proceed to connect - wifiLogic.doConnect(); - } else { - // No password yet, prompt for it - wifiLogic.passwordPromptSsid = params.ssid; - wifiLogic.passwordInput = ""; - wifiLogic.showPasswordPrompt = true; - wifiLogic.connectStatus = ""; - wifiLogic.connectStatusSsid = ""; - wifiLogic.connectError = ""; - wifiLogic.connectSecurity = params.security; - } - } else { - // Open, connect directly - wifiLogic.doConnect(); - } + console.log("Successfully renamed connection '" + + renameConnectionProcess.oldName + "' to '" + + renameConnectionProcess.newName + "'"); + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim() !== "" && !text.toLowerCase().includes("warning")) { + console.error("Error renaming connection:", text); } } } } - // Handles connecting to a Wi-Fi network, with or without password + + + // Process to rename a connection + Process { + id: deleteProfileProcess + running: false + property string connName: "" + command: ["nmcli", "connection", "delete", `'${connName}'`] + + stdout: StdioCollector { + onStreamFinished: { + console.log("Deleted connection '" + deleteProfileProcess.connName + "'"); + } + } + stderr: StdioCollector { + onStreamFinished: { + console.error("Error deleting connection '" + deleteProfileProcess.connName + "':", text); + } + } + } + + + Process { id: connectProcess property string ssid: "" property string password: "" property string security: "" running: false + onStarted: { + refreshIndicator.running = true; + } + onExited: (exitCode, exitStatus) => { + refreshIndicator.running = false; + } command: { if (password) { - return ["nmcli", "device", "wifi", "connect", ssid, "password", password] + return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password]; } else { - return ["nmcli", "device", "wifi", "connect", ssid] + return ["nmcli", "device", "wifi", "connect", `'${ssid}'`]; } } stdout: StdioCollector { @@ -229,7 +336,7 @@ Item { } } - // Finds the correct Wi-Fi interface for connection + Process { id: getInterfaceProcess running: false @@ -249,7 +356,7 @@ Item { addConnectionProcess.ifname = wifiLogic.detectedInterface; addConnectionProcess.ssid = params.ssid; addConnectionProcess.password = params.password; - addConnectionProcess.profileName = wifiLogic.profileNameForSsid(params.ssid); + addConnectionProcess.profileName = params.ssid; addConnectionProcess.security = params.security; addConnectionProcess.running = true; } else { @@ -263,7 +370,7 @@ Item { } } - // Adds a new Wi-Fi connection profile + Process { id: addConnectionProcess property string ifname: "" @@ -296,7 +403,7 @@ Item { } } - // Brings up the new connection profile and finalizes connection state + Process { id: upConnectionProcess property string profileName: "" @@ -329,10 +436,11 @@ Item { } } - // Wifi button (no background card) + Rectangle { id: wifiButton - width: 36; height: 36 + width: 36 + height: 36 radius: 18 border.color: Theme.accentPrimary border.width: 1 @@ -343,9 +451,7 @@ Item { text: "wifi" font.family: "Material Symbols Outlined" font.pixelSize: 22 - color: wifiButtonArea.containsMouse - ? Theme.backgroundPrimary - : Theme.accentPrimary + color: wifiButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } @@ -371,12 +477,12 @@ Item { margins.top: 0 WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand Component.onCompleted: { - wifiLogic.refreshNetworks() + wifiLogic.refreshNetworks(); } Rectangle { anchors.fill: parent color: Theme.backgroundPrimary - radius: 24 + radius: 20 ColumnLayout { anchors.fill: parent anchors.margins: 32 @@ -400,8 +506,29 @@ Item { color: Theme.textPrimary Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } + Spinner { + id: refreshIndicator + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + Layout.alignment: Qt.AlignVCenter + visible: false + running: false + color: Theme.accentPrimary + size: 22 + } + IconButton { + id: refreshButton + icon: "refresh" + onClicked: wifiLogic.refreshNetworks() + } + Rectangle { - width: 36; height: 36; radius: 18 + implicitWidth: 36 + implicitHeight: 36 + radius: 18 color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent" border.color: Theme.accentPrimary border.width: 1 @@ -463,11 +590,15 @@ Item { anchors.fill: parent spacing: 4 boundsBehavior: Flickable.StopAtBounds - model: wifiLogic.networks + model: wifiLogic.networks ? Object.values(wifiLogic.networks) : null delegate: Item { id: networkEntry + + required property var modelData + property var signalIcon: wifiPanel.signalIcon + width: parent.width - height: modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt ? 102 : 42 + height: (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt ? 102 : 42) + (modelData.ssid === wifiLogic.actionPanelSsid ? 60 : 0) ColumnLayout { anchors.fill: parent spacing: 0 @@ -504,7 +635,8 @@ Item { Layout.alignment: Qt.AlignVCenter } Item { - width: 22; height: 22 + width: 22 + height: 22 visible: wifiLogic.connectStatusSsid === modelData.ssid && wifiLogic.connectStatus !== "" RowLayout { anchors.fill: parent @@ -554,28 +686,29 @@ Item { verticalAlignment: Text.AlignVCenter Layout.alignment: Qt.AlignVCenter } - Item { - Layout.alignment: Qt.AlignVCenter - Layout.preferredHeight: 22 - Layout.preferredWidth: 22 - Spinner { - visible: wifiLogic.connectingSsid === modelData.ssid - running: wifiLogic.connectingSsid === modelData.ssid - color: Theme.accentPrimary - anchors.centerIn: parent - size: 22 - } - } + Item { + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: 22 + Layout.preferredWidth: 22 + Spinner { + visible: wifiLogic.connectingSsid === modelData.ssid + running: wifiLogic.connectingSsid === modelData.ssid + color: Theme.accentPrimary + anchors.centerIn: parent + size: 22 + } + } } MouseArea { id: networkMouseArea anchors.fill: parent hoverEnabled: true onClicked: { - if (modelData.connected) { - wifiLogic.disconnectNetwork(modelData.ssid); + + if (wifiLogic.actionPanelSsid === modelData.ssid) { + wifiLogic.actionPanelSsid = ""; // Close if already open } else { - wifiLogic.connectNetwork(modelData.ssid, modelData.security); + wifiLogic.actionPanelSsid = modelData.ssid; // Open for this network } } } @@ -586,8 +719,9 @@ Item { Layout.preferredHeight: 60 radius: 8 color: "transparent" - anchors.leftMargin: 32 - anchors.rightMargin: 32 + Layout.alignment: Qt.AlignLeft + Layout.leftMargin: 32 + Layout.rightMargin: 32 z: 2 RowLayout { anchors.fill: parent @@ -627,14 +761,18 @@ Item { } } Rectangle { - width: 80 - height: 36 + Layout.preferredWidth: 80 + Layout.preferredHeight: 36 radius: 18 color: Theme.accentPrimary border.color: Theme.accentPrimary border.width: 0 opacity: 1.0 - Behavior on color { ColorAnimation { duration: 100 } } + Behavior on color { + ColorAnimation { + duration: 100 + } + } MouseArea { anchors.fill: parent onClicked: wifiLogic.submitPassword() @@ -653,6 +791,113 @@ Item { } } } + + Rectangle { + visible: modelData.ssid === wifiLogic.actionPanelSsid + Layout.fillWidth: true + Layout.preferredHeight: 60 + radius: 8 + color: "transparent" + Layout.alignment: Qt.AlignLeft + Layout.leftMargin: 32 + Layout.rightMargin: 32 + z: 2 + RowLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 10 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 36 + visible: wifiLogic.isSecured(modelData.security) && !modelData.connected && !modelData.existing + Rectangle { + anchors.fill: parent + radius: 8 + color: "transparent" + border.color: actionPanelPasswordField.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + TextInput { + id: actionPanelPasswordField + anchors.fill: parent + anchors.margins: 12 + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: TextInput.AlignVCenter + clip: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhNone + echoMode: TextInput.Password + onAccepted: { + + wifiLogic.pendingConnect = { + ssid: modelData.ssid, + security: modelData.security, + password: text + }; + wifiLogic.doConnect(); + + wifiLogic.actionPanelSsid = ""; // Close the panel + } + } + } + } + + Rectangle { + Layout.preferredWidth: 80 + Layout.preferredHeight: 36 + radius: 18 + color: modelData.connected ? Theme.error : Theme.accentPrimary + border.color: modelData.connected ? Theme.error : Theme.accentPrimary + border.width: 0 + opacity: 1.0 + Behavior on color { + ColorAnimation { + duration: 100 + } + } + MouseArea { + anchors.fill: parent + onClicked: { + if (modelData.connected) { + + wifiLogic.disconnectNetwork(modelData.ssid); + } else { + + if (wifiLogic.isSecured(modelData.security) && !modelData.existing) { + + if (actionPanelPasswordField.text.length > 0) { + wifiLogic.pendingConnect = { + ssid: modelData.ssid, + security: modelData.security, + password: actionPanelPasswordField.text + }; + wifiLogic.doConnect(); + } + + } else { + + wifiLogic.connectNetwork(modelData.ssid, modelData.security); + } + } + wifiLogic.actionPanelSsid = ""; // Close the panel + } + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = modelData.connected ? Qt.darker(Theme.error, 1.1) : Qt.darker(Theme.accentPrimary, 1.1) + onExited: parent.color = modelData.connected ? Theme.error : Theme.accentPrimary + } + Text { + anchors.centerIn: parent + text: modelData.connected ? "wifi_off" : "check" + font.family: "Material Symbols Outlined" + font.pixelSize: 20 + color: Theme.backgroundPrimary + } + } + } + } } } } diff --git a/qmlls.ini b/qmlls.ini new file mode 100644 index 0000000..e69de29 diff --git a/shell.qml b/shell.qml index 737badc..5bd4e33 100644 --- a/shell.qml +++ b/shell.qml @@ -22,18 +22,17 @@ Scope { property var notificationHistoryWin: notificationHistoryWin property bool pendingReload: false - // Helper function to round value to nearest step + // Round volume to nearest 5% increment for consistent control function roundToStep(value, step) { return Math.round(value / step) * step; } - // Volume property reflecting current audio volume in 0-100 - // Will be kept in sync dynamically below + // Current audio volume (0-100), synced with system property int volume: (defaultAudioSink && defaultAudioSink.audio && !defaultAudioSink.audio.muted) ? Math.round(defaultAudioSink.audio.volume * 100) : 0 - // Function to update volume with clamping, stepping, and applying to audio sink + // Update volume with 5-step increments and apply to audio sink function updateVolume(vol) { var clamped = Math.max(0, Math.min(100, vol)); var stepped = roundToStep(clamped, 5); @@ -53,6 +52,15 @@ Scope { property var notificationHistoryWin: notificationHistoryWin } + // Create dock for each monitor (respects dockMonitors setting) + Variants { + model: Quickshell.screens + + Dock { + property var modelData + } + } + Applauncher { id: appLauncherPanel visible: false @@ -77,9 +85,15 @@ Scope { onNotification: function (notification) { console.log("Notification received:", notification.appName); notification.tracked = true; - if (notificationPopup.notificationsVisible) { - notificationPopup.addNotification(notification); + + // Distribute notification to all visible notification popups + for (let i = 0; i < notificationPopupVariants.count; i++) { + let popup = notificationPopupVariants.objectAt(i); + if (popup && popup.notificationsVisible) { + popup.addNotification(notification); + } } + if (notificationHistoryWin) { notificationHistoryWin.addToHistory({ id: notification.id, @@ -93,9 +107,19 @@ Scope { } } - NotificationPopup { - id: notificationPopup - barVisible: bar.visible + // Create notification popups for each selected monitor + Variants { + id: notificationPopupVariants + model: Quickshell.screens + + NotificationPopup { + property var modelData + barVisible: bar.visible + screen: modelData + visible: notificationsVisible && notificationModel.count > 0 && + (Settings.settings.notificationMonitors.includes(modelData.name) || + (Settings.settings.notificationMonitors.length === 0)) // Show on all if none selected + } } NotificationHistory { @@ -113,7 +137,7 @@ Scope { appLauncherPanel: appLauncherPanel lockScreen: lockScreen idleInhibitor: idleInhibitor - notificationPopup: notificationPopup + notificationPopupVariants: notificationPopupVariants } Connections { @@ -130,11 +154,12 @@ Scope { Timer { id: reloadTimer - interval: 500 // ms + interval: 500 repeat: false onTriggered: Quickshell.reload(true) } + // Handle screen configuration changes (delay reload if locked) Connections { target: Quickshell function onScreensChanged() { @@ -146,17 +171,15 @@ Scope { } } - // --- NEW: Keep volume property in sync with actual Pipewire audio sink volume --- - Connections { - target: defaultAudioSink.audio - onVolumeChanged: { + target: defaultAudioSink ? defaultAudioSink.audio : null + function onVolumeChanged() { if (defaultAudioSink.audio && !defaultAudioSink.audio.muted) { volume = Math.round(defaultAudioSink.audio.volume * 100); console.log("Volume changed externally to:", volume); } } - onMutedChanged: { + function onMutedChanged() { if (defaultAudioSink.audio) { if (defaultAudioSink.audio.muted) { volume = 0;