diff --git a/Bar/Bar.qml b/Bar/Bar.qml index a65407cd..653f6e90 100644 --- a/Bar/Bar.qml +++ b/Bar/Bar.qml @@ -1,26 +1,30 @@ import QtQuick import QtQuick.Controls +import QtQuick.Effects import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Wayland -import QtQuick.Effects import qs.Bar.Modules -import qs.Settings -import qs.Services import qs.Components import qs.Helpers +import qs.Services +import qs.Settings import qs.Widgets -import qs.Widgets.Sidebar -import qs.Widgets.Sidebar.Panel import qs.Widgets.Notification +import qs.Widgets.SidePanel + +// Main bar component - creates panels on selected monitors with widgets and corners Scope { id: rootScope + property var shell + property alias visible: barRootItem.visible Item { id: barRootItem + anchors.fill: parent Variants { @@ -31,19 +35,20 @@ Scope { PanelWindow { id: panel + screen: modelData color: "transparent" implicitHeight: barBackground.height anchors.top: true anchors.left: true anchors.right: true - - visible: true + visible: Settings.settings.barMonitors.includes(modelData.name) || (Settings.settings.barMonitors.length === 0) Rectangle { id: barBackground + width: parent.width - height: 36 + height: 36 * Theme.scale(Screen) color: Theme.backgroundPrimary anchors.top: parent.top anchors.left: parent.left @@ -51,10 +56,11 @@ Scope { Row { id: leftWidgetsRow + anchors.verticalCenter: barBackground.verticalCenter anchors.left: barBackground.left - anchors.leftMargin: 18 - spacing: 12 + anchors.leftMargin: 18 * Theme.scale(Screen) + spacing: 12 * Theme.scale(Screen) SystemInfo { anchors.verticalCenter: parent.verticalCenter @@ -67,6 +73,7 @@ Scope { Taskbar { anchors.verticalCenter: parent.verticalCenter } + } ActiveWindow { @@ -75,6 +82,7 @@ Scope { Workspace { id: workspace + screen: modelData anchors.horizontalCenter: barBackground.horizontalCenter anchors.verticalCenter: barBackground.verticalCenter @@ -82,13 +90,15 @@ Scope { Row { id: rightWidgetsRow + anchors.verticalCenter: barBackground.verticalCenter anchors.right: barBackground.right - anchors.rightMargin: 18 - spacing: 12 + anchors.rightMargin: 18 * Theme.scale(Screen) + spacing: 12 * Theme.scale(Screen) SystemTray { id: systemTrayModule + shell: rootScope.shell anchors.verticalCenter: parent.verticalCenter bar: panel @@ -100,22 +110,34 @@ Scope { } NotificationIcon { + shell: rootScope.shell + anchors.verticalCenter: parent.verticalCenter + } + + Wifi { + anchors.verticalCenter: parent.verticalCenter + } + + Bluetooth { anchors.verticalCenter: parent.verticalCenter } Battery { id: widgetsBattery + anchors.verticalCenter: parent.verticalCenter } Brightness { id: widgetsBrightness + screen: modelData anchors.verticalCenter: parent.verticalCenter } Volume { id: widgetsVolume + shell: rootScope.shell anchors.verticalCenter: parent.verticalCenter } @@ -127,6 +149,8 @@ Scope { PanelPopup { id: sidebarPopup + + shell: rootScope.shell } Button { @@ -135,121 +159,129 @@ Scope { screen: modelData sidebarPopup: sidebarPopup } + } - Background {} - Overview {} } - PanelWindow { - id: topLeftPanel - anchors.top: true - anchors.left: true + Loader { + active: Settings.settings.showCorners && (Settings.settings.barMonitors.includes(modelData.name) || (Settings.settings.barMonitors.length === 0)) - color: "transparent" - screen: modelData - margins.top: 36 - WlrLayershell.exclusionMode: ExclusionMode.Ignore - visible: true - WlrLayershell.layer: WlrLayer.Background - aboveWindows: false - WlrLayershell.namespace: "swww-daemon" - implicitHeight: 24 + sourceComponent: Item { + PanelWindow { + id: topLeftPanel + + anchors.top: true + anchors.left: true + color: "transparent" + screen: modelData + margins.top: 36 + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.namespace: "swww-daemon" + aboveWindows: false + implicitHeight: 24 + + Corner { + id: topLeftCorner + + position: "bottomleft" + size: 1.3 + fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" + offsetX: -39 + offsetY: 0 + anchors.top: parent.top + } + + } + + PanelWindow { + id: topRightPanel + + anchors.top: true + anchors.right: true + color: "transparent" + screen: modelData + margins.top: 36 + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.namespace: "swww-daemon" + aboveWindows: false + implicitHeight: 24 + + Corner { + id: topRightCorner + + position: "bottomright" + size: 1.3 + fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" + offsetX: 39 + offsetY: 0 + anchors.top: parent.top + } + + } + + PanelWindow { + id: bottomLeftPanel + + anchors.bottom: true + anchors.left: true + color: "transparent" + screen: modelData + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.namespace: "swww-daemon" + aboveWindows: false + implicitHeight: 24 + + Corner { + id: bottomLeftCorner + + position: "topleft" + size: 1.3 + fillColor: Theme.backgroundPrimary + offsetX: -39 + offsetY: 0 + anchors.top: parent.top + } + + } + + PanelWindow { + id: bottomRightPanel + + anchors.bottom: true + anchors.right: true + color: "transparent" + screen: modelData + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.namespace: "swww-daemon" + aboveWindows: false + implicitHeight: 24 + + Corner{ + id: bottomRightCorner + + position: "topright" + size: 1.3 + fillColor: Theme.backgroundPrimary + offsetX: 39 + offsetY: 0 + anchors.top: parent.top + } + + } - Corners { - id: topLeftCorner - position: "bottomleft" - size: 1.3 - fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" - offsetX: -39 - offsetY: 0 - anchors.top: parent.top - visible: Settings.settings.showCorners } + } - PanelWindow { - id: topRightPanel - anchors.top: true - anchors.right: true - color: "transparent" - screen: modelData - margins.top: 36 - WlrLayershell.exclusionMode: ExclusionMode.Ignore - visible: true - WlrLayershell.layer: WlrLayer.Background - aboveWindows: false - WlrLayershell.namespace: "swww-daemon" - - implicitHeight: 24 - - Corners { - id: topRightCorner - position: "bottomright" - size: 1.3 - fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" - offsetX: 39 - offsetY: 0 - anchors.top: parent.top - visible: Settings.settings.showCorners - } - } - - PanelWindow { - id: bottomLeftPanel - anchors.bottom: true - anchors.left: true - color: "transparent" - screen: modelData - WlrLayershell.exclusionMode: ExclusionMode.Ignore - visible: true - WlrLayershell.layer: WlrLayer.Background - aboveWindows: false - WlrLayershell.namespace: "swww-daemon" - - implicitHeight: 24 - - Corners { - id: bottomLeftCorner - position: "topleft" - size: 1.3 - fillColor: Theme.backgroundPrimary - offsetX: -39 - offsetY: 0 - anchors.top: parent.top - visible: Settings.settings.showCorners - } - } - - PanelWindow { - id: bottomRightPanel - anchors.bottom: true - anchors.right: true - color: "transparent" - screen: modelData - WlrLayershell.exclusionMode: ExclusionMode.Ignore - visible: true - WlrLayershell.layer: WlrLayer.Background - aboveWindows: false - WlrLayershell.namespace: "swww-daemon" - - implicitHeight: 24 - - Corners { - id: bottomRightCorner - position: "topright" - size: 1.3 - fillColor: Theme.backgroundPrimary - offsetX: 39 - offsetY: 0 - anchors.top: parent.top - visible: Settings.settings.showCorners - } - } } + } + } - // This alias exposes the visual bar's visibility to the outside world - property alias visible: barRootItem.visible } diff --git a/Bar/Modules/ActiveWindow.qml b/Bar/Modules/ActiveWindow.qml index ab39551d..67c2f8a2 100644 --- a/Bar/Modules/ActiveWindow.qml +++ b/Bar/Modules/ActiveWindow.qml @@ -7,6 +7,10 @@ import qs.Settings PanelWindow { id: activeWindowPanel + + // Lower case "screen" from modelData + property int barHeight: 36 * Theme.scale(screen) + screen: (typeof modelData !== 'undefined' ? modelData : null) WlrLayershell.exclusionMode: ExclusionMode.Ignore anchors.top: true @@ -14,26 +18,25 @@ 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 color: "transparent" - function getIcon() { - var icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId.toLowerCase(), true); - if (!icon) { - icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId, true); - } - if (!icon) { - icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title, true); - } - if (!icon) { - icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title.toLowerCase(), "application-x-executable"); - } - - return icon; + function getIcon() { + var icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId.toLowerCase(), true); + if (!icon) { + icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId, true); } + if (!icon) { + icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title, true); + } + if (!icon) { + icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title.toLowerCase(), "application-x-executable"); + } + + return icon; + } Item { id: activeWindowWrapper @@ -44,7 +47,7 @@ PanelWindow { Timer { id: visibilityTimer - interval: 1200 + interval: 1500 running: false onTriggered: { activeWindowWrapper.shouldShow = false; @@ -101,64 +104,62 @@ PanelWindow { Rectangle { id: activeWindowTitleContainer - color: Theme.backgroundPrimary - bottomLeftRadius: Math.max(0, width / 2) - bottomRightRadius: Math.max(0, width / 2) + color: "transparent" - width: Math.min(barBackground.width - 200, activeWindowTitle.implicitWidth + (Settings.settings.showActiveWindowIcon ? 28 : 22)) + + width: Math.min(barBackground.width - 200, activeWindowTitle.implicitWidth + (Settings.settings.showActiveWindowIcon ? 28 : 22)) + 16 height: activeWindowTitle.implicitHeight + 12 anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter - IconImage { - id: icon - width: 12 - height: 12 - anchors.left: parent.left - anchors.leftMargin: 6 - anchors.verticalCenter: parent.verticalCenter - source: ToplevelManager?.activeToplevel ? getIcon() : "" - visible: Settings.settings.showActiveWindowIcon - anchors.verticalCenterOffset: -3 + Rectangle { + id: innerRect + bottomLeftRadius: Math.max(0, width / 2) + bottomRightRadius: Math.max(0, width / 2) + color: Theme.backgroundPrimary + anchors { + fill: parent + leftMargin: 0 + rightMargin: 0 + topMargin: -1 + bottomMargin: 0 + } + + + border.color: Theme.outline || "#444"; + border.width: 1; + + IconImage { + id: icon + width: 12 + height: 12 + anchors.left: parent.left + anchors.leftMargin: 14 + anchors.verticalCenter: parent.verticalCenter + source: ToplevelManager?.activeToplevel ? getIcon() : "" + visible: Settings.settings.showActiveWindowIcon + anchors.verticalCenterOffset: -3 + + } + + Text { + id: activeWindowTitle + text: ToplevelManager?.activeToplevel?.title && ToplevelManager?.activeToplevel?.title.length > 60 ? ToplevelManager?.activeToplevel?.title.substring(0, 60) + "..." : ToplevelManager?.activeToplevel?.title || "" + font.pixelSize: 12 * Theme.scale(Screen) + color: Theme.textSecondary + anchors.left: icon.right + anchors.leftMargin: Settings.settings.showActiveWindowIcon ? 4 : 6 + anchors.right: parent.right + anchors.rightMargin: 14 + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: -3 + horizontalAlignment: Settings.settings.showActiveWindowIcon ? Text.AlignRight : Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + maximumLineCount: 1 + } } - - Text { - id: activeWindowTitle - text: ToplevelManager?.activeToplevel?.title && ToplevelManager?.activeToplevel?.title.length > 60 ? ToplevelManager?.activeToplevel?.title.substring(0, 60) + "..." : ToplevelManager?.activeToplevel?.title || "" - font.pixelSize: 12 - color: Theme.textSecondary - anchors.left: icon.right - anchors.leftMargin: Settings.settings.showActiveWindowIcon ? 4 : 6 - anchors.right: parent.right - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: -3 - horizontalAlignment: Settings.settings.showActiveWindowIcon ? Text.AlignRight : Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - maximumLineCount: 1 - } - } - - Corners { - id: activeCornerRight - position: "bottomleft" - size: 1.1 - fillColor: Theme.backgroundPrimary - offsetX: activeWindowTitleContainer.x + activeWindowTitleContainer.width - 34 - offsetY: -1 - anchors.top: activeWindowTitleContainer.top - } - - Corners { - id: activeCornerLeft - position: "bottomright" - size: 1.1 - fillColor: Theme.backgroundPrimary - anchors.top: activeWindowTitleContainer.top - x: activeWindowTitleContainer.x + 34 - width - offsetY: -1 } } } diff --git a/Bar/Modules/Applauncher.qml b/Bar/Modules/Applauncher.qml index 5c7cc67e..8e58320b 100644 --- a/Bar/Modules/Applauncher.qml +++ b/Bar/Modules/Applauncher.qml @@ -11,12 +11,151 @@ import qs.Settings 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() : []; @@ -71,6 +210,11 @@ PanelWithOverlay { root.selectedIndex = 0; } + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + Rectangle { id: root width: 460 @@ -103,9 +247,11 @@ PanelWithOverlay { appLauncherPanel.visible = false; } } + function isMathExpression(str) { return /^[-+*/().0-9\s]+$/.test(str); } + function safeEval(expr) { try { return Function('return (' + expr + ')')(); @@ -113,13 +259,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 !== "") { @@ -132,8 +379,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()); })); @@ -145,7 +411,7 @@ PanelWithOverlay { return r.obj; })); } - // Pinning logic: split into pinned and unpinned + var pinned = []; var unpinned = []; for (var i = 0; i < results.length; ++i) { @@ -163,10 +429,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); @@ -184,6 +452,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) { @@ -202,281 +474,356 @@ 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 + id: previewPanel + Layout.preferredWidth: 200 + Layout.fillHeight: true + color: Theme.surface radius: 20 - height: 48 - Layout.fillWidth: true - border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: searchField.activeFocus ? 2 : 1 + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 - IconImage { - id: iconImg + id: pinArea + width: 28; height: 28 + z: 100 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + MouseArea { anchors.fill: parent - 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 + font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) + 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 } } } @@ -484,25 +831,5 @@ PanelWithOverlay { } } } - - 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 - } } } \ No newline at end of file diff --git a/Bar/Modules/AudioDeviceSelector.qml b/Bar/Modules/AudioDeviceSelector.qml index 965acc9a..3d45d92c 100644 --- a/Bar/Modules/AudioDeviceSelector.qml +++ b/Bar/Modules/AudioDeviceSelector.qml @@ -7,13 +7,67 @@ import qs.Settings PanelWithOverlay { id: ioSelector - signal panelClosed() + property int tabIndex: 0 property Item anchorItem: null + signal panelClosed() + + function sinkNodes() { + let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { + return n.isSink && n.audio && n.isStream === false; + }) : []; + if (Pipewire.defaultAudioSink) + nodes = nodes.slice().sort(function(a, b) { + if (a.id === Pipewire.defaultAudioSink.id) + return -1; + + if (b.id === Pipewire.defaultAudioSink.id) + return 1; + + return 0; + }); + + return nodes; + } + + function sourceNodes() { + let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { + return !n.isSink && n.audio && n.isStream === false; + }) : []; + if (Pipewire.defaultAudioSource) + nodes = nodes.slice().sort(function(a, b) { + if (a.id === Pipewire.defaultAudioSource.id) + return -1; + + if (b.id === Pipewire.defaultAudioSource.id) + return 1; + + return 0; + }); + + return nodes; + } + + Component.onCompleted: { + if (Pipewire.nodes && Pipewire.nodes.values) { + for (var i = 0; i < Pipewire.nodes.values.length; ++i) { + var n = Pipewire.nodes.values[i]; + } + } + } + Component.onDestruction: { + } + onVisibleChanged: { + if (!visible) + panelClosed(); + + } + // Bind all Pipewire nodes so their properties are valid PwObjectTracker { id: nodeTracker + objects: Pipewire.nodes } @@ -27,6 +81,11 @@ PanelWithOverlay { anchors.topMargin: 4 anchors.rightMargin: 4 + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + ColumnLayout { anchors.fill: parent anchors.margins: 16 @@ -37,242 +96,260 @@ PanelWithOverlay { Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter spacing: 0 - + Tabs { id: ioTabs - tabsModel: [ - { label: "Output", icon: "volume_up" }, - { label: "Input", icon: "mic" } - ] + + tabsModel: [{ + "label": "Output", + "icon": "volume_up" + }, { + "label": "Input", + "icon": "mic" + }] currentIndex: tabIndex onTabChanged: { tabIndex = currentIndex; } } + } // Add vertical space between tabs and entries - Item { height: 36; Layout.fillWidth: true } + Item { + height: 36 + Layout.fillWidth: true + } // Output Devices Flickable { id: sinkList + visible: tabIndex === 0 contentHeight: sinkColumn.height clip: true interactive: contentHeight > height width: parent.width height: 220 - ScrollBar.vertical: ScrollBar {} + ColumnLayout { id: sinkColumn + width: sinkList.width spacing: 6 + Repeater { model: ioSelector.sinkNodes() + Rectangle { width: parent.width height: 36 color: "transparent" radius: 6 + RowLayout { anchors.fill: parent anchors.margins: 6 spacing: 8 + Text { text: "volume_up" font.family: "Material Symbols Outlined" - font.pixelSize: 16 + font.pixelSize: 16 * Theme.scale(Screen) color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary Layout.alignment: Qt.AlignVCenter } + ColumnLayout { Layout.fillWidth: true spacing: 1 Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button + Text { text: modelData.nickname || modelData.description || modelData.name font.bold: true - font.pixelSize: 12 + font.pixelSize: 12 * Theme.scale(Screen) color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary elide: Text.ElideRight maximumLineCount: 1 Layout.fillWidth: true } + Text { text: modelData.description !== modelData.nickname ? modelData.description : "" - font.pixelSize: 10 + font.pixelSize: 10 * Theme.scale(Screen) color: Theme.textSecondary elide: Text.ElideRight maximumLineCount: 1 Layout.fillWidth: true } + } + Rectangle { visible: Pipewire.preferredDefaultAudioSink !== modelData - width: 60; height: 20 + width: 60 + height: 20 radius: 4 color: Theme.accentPrimary border.color: Theme.accentPrimary border.width: 1 Layout.alignment: Qt.AlignVCenter + Text { anchors.centerIn: parent text: "Set" color: Theme.onAccent - font.pixelSize: 10 + font.pixelSize: 10 * Theme.scale(Screen) font.bold: true } + MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: Pipewire.preferredDefaultAudioSink = modelData } + } + Text { text: "(Current)" visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id color: Theme.accentPrimary - font.pixelSize: 10 + font.pixelSize: 10 * Theme.scale(Screen) Layout.alignment: Qt.AlignVCenter } + } + } + } + } + + ScrollBar.vertical: ScrollBar { + } + } // Input Devices Flickable { id: sourceList + visible: tabIndex === 1 contentHeight: sourceColumn.height clip: true interactive: contentHeight > height width: parent.width height: 220 - ScrollBar.vertical: ScrollBar {} + ColumnLayout { id: sourceColumn + width: sourceList.width spacing: 6 + Repeater { model: ioSelector.sourceNodes() + Rectangle { width: parent.width height: 36 color: "transparent" radius: 6 + RowLayout { anchors.fill: parent anchors.margins: 6 spacing: 8 + Text { text: "mic" font.family: "Material Symbols Outlined" - font.pixelSize: 16 + font.pixelSize: 16 * Theme.scale(Screen) color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary Layout.alignment: Qt.AlignVCenter } + ColumnLayout { Layout.fillWidth: true spacing: 1 Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button + Text { text: modelData.nickname || modelData.description || modelData.name font.bold: true - font.pixelSize: 12 + font.pixelSize: 12 * Theme.scale(Screen) color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary elide: Text.ElideRight maximumLineCount: 1 Layout.fillWidth: true } + Text { text: modelData.description !== modelData.nickname ? modelData.description : "" - font.pixelSize: 10 + font.pixelSize: 10 * Theme.scale(Screen) color: Theme.textSecondary elide: Text.ElideRight maximumLineCount: 1 Layout.fillWidth: true } + } + Rectangle { visible: Pipewire.preferredDefaultAudioSource !== modelData - width: 60; height: 20 + width: 60 + height: 20 radius: 4 color: Theme.accentPrimary border.color: Theme.accentPrimary border.width: 1 Layout.alignment: Qt.AlignVCenter + Text { anchors.centerIn: parent text: "Set" color: Theme.onAccent - font.pixelSize: 10 + font.pixelSize: 10 * Theme.scale(Screen) font.bold: true } + MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: Pipewire.preferredDefaultAudioSource = modelData } + } + Text { text: "(Current)" visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id color: Theme.accentPrimary - font.pixelSize: 10 + font.pixelSize: 10 * Theme.scale(Screen) Layout.alignment: Qt.AlignVCenter } + } + } + } + } - } - } - } - function sinkNodes() { - let nodes = Pipewire.nodes && Pipewire.nodes.values - ? Pipewire.nodes.values.filter(function(n) { - return n.isSink && n.audio && n.isStream === false; - }) - : []; - if (Pipewire.defaultAudioSink) { - nodes = nodes.slice().sort(function(a, b) { - if (a.id === Pipewire.defaultAudioSink.id) return -1; - if (b.id === Pipewire.defaultAudioSink.id) return 1; - return 0; - }); - } - return nodes; - } - function sourceNodes() { - let nodes = Pipewire.nodes && Pipewire.nodes.values - ? Pipewire.nodes.values.filter(function(n) { - return !n.isSink && n.audio && n.isStream === false; - }) - : []; - if (Pipewire.defaultAudioSource) { - nodes = nodes.slice().sort(function(a, b) { - if (a.id === Pipewire.defaultAudioSource.id) return -1; - if (b.id === Pipewire.defaultAudioSource.id) return 1; - return 0; - }); - } - return nodes; - } + ScrollBar.vertical: ScrollBar { + } - Component.onCompleted: { - if (Pipewire.nodes && Pipewire.nodes.values) { - for (var i = 0; i < Pipewire.nodes.values.length; ++i) { - var n = Pipewire.nodes.values[i]; } + } + } Connections { - target: Pipewire function onReadyChanged() { if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) { for (var i = 0; i < Pipewire.nodes.values.length; ++i) { @@ -280,15 +357,14 @@ PanelWithOverlay { } } } + function onDefaultAudioSinkChanged() { } + function onDefaultAudioSourceChanged() { } + + target: Pipewire } - Component.onDestruction: { - } - onVisibleChanged: { - if (!visible) panelClosed(); - } -} \ No newline at end of file +} diff --git a/Bar/Modules/Bluetooth.qml b/Bar/Modules/Bluetooth.qml new file mode 100644 index 00000000..0c080dc7 --- /dev/null +++ b/Bar/Modules/Bluetooth.qml @@ -0,0 +1,282 @@ +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 * Theme.scale(Screen) + color: mouseAreaBluetooth.containsMouse ? Theme.accentPrimary : Theme.textPrimary + } + + MouseArea { + id: mouseAreaBluetooth + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!bluetoothMenuLoader.active) { + bluetoothMenuLoader.loading = true; + } + if (bluetoothMenuLoader.item) { + bluetoothMenuLoader.item.visible = !bluetoothMenuLoader.item.visible; + // Enable adapter and start discovery when menu opens + if (bluetoothMenuLoader.item.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 + } + + // LazyLoader for Bluetooth menu + LazyLoader { + id: bluetoothMenuLoader + loading: false + component: 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 * Theme.scale(Screen) + color: Theme.accentPrimary + } + + Text { + text: "Bluetooth Devices" + font.pixelSize: 18 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + color: Theme.textSecondary + } + + Spinner { + running: true + color: Theme.accentPrimary + size: 16 + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Bar/Modules/Calendar.qml b/Bar/Modules/Calendar.qml index 55f1b834..47757fa1 100644 --- a/Bar/Modules/Calendar.qml +++ b/Bar/Modules/Calendar.qml @@ -1,11 +1,11 @@ +import "../../Helpers/Holidays.js" as Holidays import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell +import Quickshell.Wayland import qs.Components import qs.Settings -import Quickshell.Wayland -import "../../Helpers/Holidays.js" as Holidays PanelWithOverlay { id: calendarOverlay @@ -22,6 +22,11 @@ PanelWithOverlay { anchors.topMargin: 4 anchors.rightMargin: 4 + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + ColumnLayout { anchors.fill: parent anchors.margins: 16 @@ -47,7 +52,7 @@ PanelWithOverlay { text: calendar.title color: Theme.textPrimary opacity: 0.7 - font.pixelSize: 13 + font.pixelSize: 13 * Theme.scale(Screen) font.family: Theme.fontFamily font.bold: true } @@ -60,33 +65,30 @@ PanelWithOverlay { calendar.month = newDate.getMonth(); } } + } DayOfWeekRow { Layout.fillWidth: true spacing: 0 - Layout.leftMargin: 8 // Align with grid + Layout.leftMargin: 8 // Align with grid Layout.rightMargin: 8 + delegate: Text { text: shortName color: Theme.textPrimary opacity: 0.8 - font.pixelSize: 13 + font.pixelSize: 13 * Theme.scale(Screen) font.family: Theme.fontFamily font.bold: true horizontalAlignment: Text.AlignHCenter width: 32 } + } MonthGrid { id: calendar - Layout.fillWidth: true - Layout.leftMargin: 8 - Layout.rightMargin: 8 - spacing: 0 - month: Time.date.getMonth() - year: Time.date.getFullYear() property var holidays: [] @@ -96,12 +98,19 @@ PanelWithOverlay { calendar.holidays = holidays; }); } + + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + spacing: 0 + month: Time.date.getMonth() + year: Time.date.getFullYear() onMonthChanged: updateHolidays() onYearChanged: updateHolidays() Component.onCompleted: updateHolidays() + // Optionally, update when the panel becomes visible Connections { - target: calendarOverlay function onVisibleChanged() { if (calendarOverlay.visible) { calendar.month = Time.date.getMonth(); @@ -109,29 +118,35 @@ PanelWithOverlay { calendar.updateHolidays(); } } + + target: calendarOverlay } delegate: Rectangle { - width: 32 - height: 32 - radius: 8 property var holidayInfo: calendar.holidays.filter(function(h) { var d = new Date(h.date); return d.getDate() === model.day && d.getMonth() === model.month && d.getFullYear() === model.year; }) property bool isHoliday: holidayInfo.length > 0 + + width: 32 + height: 32 + radius: 8 color: { if (model.today) return Theme.accentPrimary; + if (mouseArea2.containsMouse) return Theme.backgroundTertiary; + return "transparent"; } // Holiday dot indicator Rectangle { visible: isHoliday - width: 4; height: 4 + width: 4 + height: 4 radius: 4 color: Theme.accentTertiary anchors.top: parent.top @@ -145,14 +160,15 @@ PanelWithOverlay { anchors.centerIn: parent text: model.day color: model.today ? Theme.onAccent : Theme.textPrimary - opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? 1.0 : 0.7) : 0.3 - font.pixelSize: 13 + opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? 1 : 0.7) : 0.3 + font.pixelSize: 13 * Theme.scale(Screen) font.family: Theme.fontFamily font.bold: model.today ? true : false } MouseArea { id: mouseArea2 + anchors.fill: parent hoverEnabled: true onEntered: { @@ -167,21 +183,28 @@ PanelWithOverlay { onExited: holidayTooltip.tooltipVisible = false } - Behavior on color { - ColorAnimation { - duration: 150 - } - } - StyledTooltip { id: holidayTooltip + text: "" tooltipVisible: false targetItem: null delay: 100 } + + Behavior on color { + ColorAnimation { + duration: 150 + } + + } + } + } + } + } -} \ No newline at end of file + +} diff --git a/Bar/Modules/ClockWidget.qml b/Bar/Modules/ClockWidget.qml index eba2afba..bff0d357 100644 --- a/Bar/Modules/ClockWidget.qml +++ b/Bar/Modules/ClockWidget.qml @@ -15,7 +15,7 @@ Rectangle { text: Time.time font.family: Theme.fontFamily font.weight: Font.Bold - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) color: Theme.textPrimary anchors.centerIn: parent } diff --git a/Bar/Modules/CustomTrayMenu.qml b/Bar/Modules/CustomTrayMenu.qml index a64ac7b3..e5756487 100644 --- a/Bar/Modules/CustomTrayMenu.qml +++ b/Bar/Modules/CustomTrayMenu.qml @@ -128,7 +128,7 @@ PopupWindow { color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled; text: modelData?.text ?? ""; font.family: Theme.fontFamily; - font.pixelSize: Theme.fontSizeSmall; + font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen); verticalAlignment: Text.AlignVCenter; elide: Text.ElideRight; } @@ -145,7 +145,7 @@ PopupWindow { // Material Symbols Outlined chevron right for submenu text: modelData?.hasChildren ? "menu" : ""; font.family: "Material Symbols Outlined"; - font.pixelSize: 18; + font.pixelSize: 18 * Theme.scale(Screen); verticalAlignment: Text.AlignVCenter; visible: modelData?.hasChildren ?? false; color: Theme.textPrimary; @@ -362,7 +362,7 @@ PopupWindow { color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled; text: modelData?.text ?? ""; font.family: Theme.fontFamily; - font.pixelSize: Theme.fontSizeSmall; + font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen); verticalAlignment: Text.AlignVCenter; elide: Text.ElideRight; } @@ -378,7 +378,7 @@ PopupWindow { Text { text: modelData?.hasChildren ? "\uE5CC" : ""; font.family: "Material Symbols Outlined"; - font.pixelSize: 18; + font.pixelSize: 18 * Theme.scale(Screen); verticalAlignment: Text.AlignVCenter; visible: modelData?.hasChildren ?? false; color: Theme.textPrimary; diff --git a/Bar/Modules/Media.qml b/Bar/Modules/Media.qml index f2dbd1ff..d9da9f34 100644 --- a/Bar/Modules/Media.qml +++ b/Bar/Modules/Media.qml @@ -10,7 +10,7 @@ import qs.Components Item { id: mediaControl width: visible ? mediaRow.width : 0 - height: 36 + height: 36 * Theme.scale(Screen) visible: Settings.settings.showMediaInBar && MusicManager.currentPlayer RowLayout { @@ -20,8 +20,8 @@ Item { Item { id: albumArtContainer - width: 24 - height: 24 + width: 24 * Theme.scale(Screen) + height: 24 * Theme.scale(Screen) Layout.alignment: Qt.AlignVCenter // Circular spectrum visualizer @@ -29,8 +29,8 @@ Item { id: spectrum values: MusicManager.cavaValues anchors.centerIn: parent - innerRadius: 10 - outerRadius: 18 + innerRadius: 10 * Theme.scale(Screen) + outerRadius: 18 * Theme.scale(Screen) fillColor: Theme.accentPrimary strokeColor: Theme.accentPrimary strokeWidth: 0 @@ -40,10 +40,10 @@ Item { // Album art image Rectangle { id: albumArtwork - width: 20 - height: 20 + width: 20 * Theme.scale(Screen) + height: 20 * Theme.scale(Screen) anchors.centerIn: parent - radius: 12 // circle + radius: 12 * Theme.scale(Screen) // circle color: Qt.darker(Theme.surface, 1.1) border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) border.width: 1 @@ -79,7 +79,7 @@ Item { anchors.centerIn: parent text: "music_note" font.family: "Material Symbols Outlined" - font.pixelSize: 14 + font.pixelSize: 14 * Theme.scale(Screen) color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4) visible: !albumArt.visible } @@ -96,7 +96,7 @@ Item { anchors.centerIn: parent text: MusicManager.isPlaying ? "pause" : "play_arrow" font.family: "Material Symbols Outlined" - font.pixelSize: 14 + font.pixelSize: 14 * Theme.scale(Screen) color: "white" } } @@ -117,7 +117,7 @@ Item { text: MusicManager.trackTitle + " - " + MusicManager.trackArtist color: Theme.textPrimary font.family: Theme.fontFamily - font.pixelSize: 12 + font.pixelSize: 12 * Theme.scale(Screen) elide: Text.ElideRight Layout.maximumWidth: 300 Layout.alignment: Qt.AlignVCenter diff --git a/Bar/Modules/SettingsButton.qml b/Bar/Modules/SettingsButton.qml new file mode 100644 index 00000000..c832bd89 --- /dev/null +++ b/Bar/Modules/SettingsButton.qml @@ -0,0 +1,71 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Settings +import qs.Components +import qs.Widgets.SettingsWindow + +Item { + id: root + width: 22 + height: 22 + + 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 * Theme.scale(Screen) + color: mouseArea.containsMouse ? Theme.accentPrimary : Theme.textPrimary + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (!settingsWindowLoader.active) { + // Start loading the settings window + settingsWindowLoader.loading = true; + } + + if (settingsWindowLoader.item) { + // Toggle visibility + if (settingsWindowLoader.item.visible) { + settingsWindowLoader.item.visible = false; + } else { + settingsWindowLoader.item.visible = true; + } + } + } + } + + StyledTooltip { + text: "Settings" + targetItem: mouseArea + tooltipVisible: mouseArea.containsMouse + } + } + + // LazyLoader for SettingsWindow + LazyLoader { + id: settingsWindowLoader + loading: false + component: SettingsWindow { + // Handle window closure - just hide it, don't destroy + onVisibleChanged: { + if (!visible) { + // Window is hidden, but keep it loaded for reuse + } + } + } + } +} \ No newline at end of file diff --git a/Bar/Modules/SystemInfo.qml b/Bar/Modules/SystemInfo.qml index 7a0dc85c..bc87c292 100644 --- a/Bar/Modules/SystemInfo.qml +++ b/Bar/Modules/SystemInfo.qml @@ -8,8 +8,6 @@ Row { spacing: 10 visible: Settings.settings.showSystemInfoInBar - // The width calculation below is required to ensure our row width is an integer. - // If omitted the next component to the right might get blurry (Taskbar icons). width: Math.floor(cpuUsageLayout.width + cpuTempLayout.width + memoryUsageLayout.width + (2 * 10)) Row { @@ -19,7 +17,7 @@ Row { Text { id: cpuUsageIcon font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody + font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen) text: "speed" verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter @@ -29,7 +27,7 @@ Row { Text { id: cpuUsageText font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) color: Theme.textPrimary text: Sysinfo.cpuUsageStr anchors.verticalCenter: parent.verticalCenter @@ -43,7 +41,7 @@ Row { spacing: 3 Text { font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody + font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen) text: "thermometer" verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter @@ -52,7 +50,7 @@ Row { Text { font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) color: Theme.textPrimary text: Sysinfo.cpuTempStr anchors.verticalCenter: parent.verticalCenter @@ -66,7 +64,7 @@ Row { spacing: 3 Text { font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody + font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen) text: "memory" color: Theme.accentPrimary verticalAlignment: Text.AlignVCenter @@ -75,7 +73,7 @@ Row { Text { font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) color: Theme.textPrimary text: Sysinfo.memoryUsageStr anchors.verticalCenter: parent.verticalCenter diff --git a/Bar/Modules/SystemTray.qml b/Bar/Modules/SystemTray.qml index 7db68bf2..da6e04c4 100644 --- a/Bar/Modules/SystemTray.qml +++ b/Bar/Modules/SystemTray.qml @@ -21,43 +21,27 @@ Row { Repeater { model: systemTray.items delegate: Item { - width: 24 - height: 24 - // Hide Spotify icon, or adjust to your liking - visible: modelData && modelData.id !== "spotify" + width: 24 * Theme.scale(Screen) + height: 24 * Theme.scale(Screen) + + visible: modelData property bool isHovered: trayMouseArea.containsMouse - // Hover scale animation - scale: isHovered ? 1.15 : 1.0 - Behavior on scale { - NumberAnimation { - duration: 150 - easing.type: Easing.OutCubic - } - } - - // Subtle rotation on hover - rotation: isHovered ? 5 : 0 - Behavior on rotation { - NumberAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } + // No animations - static display Rectangle { anchors.centerIn: parent - width: 16 - height: 16 - radius: 6 + width: 16 * Theme.scale(Screen) + height: 16 * Theme.scale(Screen) + radius: 6 * Theme.scale(Screen) color: "transparent" clip: true IconImage { id: trayIcon anchors.centerIn: parent - width: 16 - height: 16 + width: 16 * Theme.scale(Screen) + height: 16 * Theme.scale(Screen) smooth: false asynchronous: true backer.fillMode: Image.PreserveAspectFit @@ -74,13 +58,6 @@ Row { return icon; } opacity: status === Image.Ready ? 1 : 0 - - Behavior on opacity { - NumberAnimation { - duration: 300 - easing.type: Easing.OutCubic - } - } Component.onCompleted: {} } } @@ -122,7 +99,7 @@ Row { 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; + const menuY = height + 20 * Theme.scale(Screen); trayMenu.menu = modelData.menu; trayMenu.showAt(parent, menuX, menuY); } else diff --git a/Bar/Modules/Taskbar.qml b/Bar/Modules/Taskbar.qml index 792babcc..249f4c01 100644 --- a/Bar/Modules/Taskbar.qml +++ b/Bar/Modules/Taskbar.qml @@ -83,7 +83,7 @@ Item { visible: !appIcon.visible text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" font.family: Theme.fontFamily - font.pixelSize: Math.max(10, Settings.settings.taskbarIconSize * 0.4375) + font.pixelSize: Math.max(10, Settings.settings.taskbarIconSize * 0.4375 * Theme.scale(Screen)) font.bold: true color: appButton.isActive ? Theme.onAccent : Theme.textPrimary } diff --git a/Bar/Modules/Wifi.qml b/Bar/Modules/Wifi.qml new file mode 100644 index 00000000..b2c783ec --- /dev/null +++ b/Bar/Modules/Wifi.qml @@ -0,0 +1,380 @@ +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 * Theme.scale(Screen) + color: mouseAreaWifi.containsMouse ? Theme.accentPrimary : Theme.textPrimary + } + + MouseArea { + id: mouseAreaWifi + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!wifiMenuLoader.active) { + wifiMenuLoader.loading = true; + } + if (wifiMenuLoader.item) { + wifiMenuLoader.item.visible = !wifiMenuLoader.item.visible; + if (wifiMenuLoader.item.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 + } + + // LazyLoader for WiFi menu + LazyLoader { + id: wifiMenuLoader + loading: false + component: 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 * Theme.scale(Screen) + color: Theme.accentPrimary + } + + Text { + text: "WiFi Networks" + font.pixelSize: 18 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + color: "#43a047" + anchors.centerIn: parent + } + + Text { + visible: network.connectStatus === "error" && !network.connectingSsid + text: "error" + font.family: "Material Symbols Outlined" + font.pixelSize: 18 * Theme.scale(Screen) + color: Theme.error + anchors.centerIn: parent + } + } + + Text { + visible: modelData.connected + text: "connected" + color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary + font.pixelSize: 11 * Theme.scale(Screen) + } + } + + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + font.bold: true + } + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Bar/Modules/Workspace.qml b/Bar/Modules/Workspace.qml index a658a6a4..61989854 100644 --- a/Bar/Modules/Workspace.qml +++ b/Bar/Modules/Workspace.qml @@ -40,7 +40,7 @@ Item { return total; } - height: 36 + height: 36 * Theme.scale(Screen) Component.onCompleted: { localWorkspaces.clear(); @@ -115,14 +115,14 @@ Item { Rectangle { id: workspaceBackground - width: parent.width - 15 - height: 26 + width: parent.width - 15 * Theme.scale(Screen) + height: 26 * Theme.scale(Screen) anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter - radius: 12 + radius: 12 * Theme.scale(Screen) color: Theme.surfaceVariant border.color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.1) - border.width: 1 + border.width: 1 * Theme.scale(Screen) layer.enabled: true layer.effect: MultiEffect { shadowColor: "black" @@ -145,7 +145,7 @@ Item { model: localWorkspaces Item { id: workspacePillContainer - height: 12 + height: 12 * Theme.scale(Screen) width: { if (model.isFocused) return 44; @@ -245,12 +245,12 @@ Item { Rectangle { id: pillBurst anchors.centerIn: workspacePillContainer - width: workspacePillContainer.width + 18 * root.masterProgress - height: workspacePillContainer.height + 18 * root.masterProgress + width: workspacePillContainer.width + 18 * root.masterProgress * Theme.scale(Screen) + height: workspacePillContainer.height + 18 * root.masterProgress * Theme.scale(Screen) radius: width / 2 color: "transparent" border.color: root.effectColor - border.width: 2 + 6 * (1.0 - root.masterProgress) + border.width: (2 + 6 * (1.0 - root.masterProgress)) * Theme.scale(Screen) opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 visible: root.effectsActive && model.isFocused z: 1 diff --git a/Components/Avatar.qml b/Components/Avatar.qml index 3a369bd3..b2c29a2b 100644 --- a/Components/Avatar.qml +++ b/Components/Avatar.qml @@ -43,7 +43,7 @@ Item { anchors.centerIn: parent text: "person" font.family: "Material Symbols Outlined" - font.pixelSize: 24 + font.pixelSize: 24 * Theme.scale(Screen) color: Theme.onAccent visible: Settings.settings.profileImage === undefined || Settings.settings.profileImage === "" z: 0 diff --git a/Components/CircularProgressBar.qml b/Components/CircularProgressBar.qml index 075f2017..a38bb1b5 100644 --- a/Components/CircularProgressBar.qml +++ b/Components/CircularProgressBar.qml @@ -5,21 +5,20 @@ 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 - property int strokeWidth: 6 + property int strokeWidth: 6 * Theme.scale(Screen) property bool showText: true property string units: "%" property string text: Math.round(progress * 100) + units - property int textSize: 10 + property int textSize: 10 * Theme.scale(Screen) property color textColor: Theme.textPrimary // 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/CircularSpectrum.qml b/Components/CircularSpectrum.qml index 634aefd1..7931fd40 100644 --- a/Components/CircularSpectrum.qml +++ b/Components/CircularSpectrum.qml @@ -4,11 +4,11 @@ import qs.Settings Item { id: root - property int innerRadius: 34 - property int outerRadius: 48 + property int innerRadius: 34 * Theme.scale(Screen) + property int outerRadius: 48 * Theme.scale(Screen) property color fillColor: "#fff" property color strokeColor: "#fff" - property int strokeWidth: 0 + property int strokeWidth: 0 * Theme.scale(Screen) property var values: [] property int usableOuter: 48 @@ -24,7 +24,7 @@ Item { Rectangle { property real value: root.values[index] property real angle: (index / root.values.length) * 360 - width: Math.max(2, (root.innerRadius * 2 * Math.PI) / root.values.length - 4) + width: Math.max(2 * Theme.scale(Screen), (root.innerRadius * 2 * Math.PI) / root.values.length - 4 * Theme.scale(Screen)) height: Settings.settings.visualizerType === "diamond" ? value * 2 * (usableOuter - root.innerRadius) : value * (usableOuter - root.innerRadius) radius: width / 2 color: root.fillColor diff --git a/Components/Corners.qml b/Components/Corner.qml similarity index 50% rename from Components/Corners.qml rename to Components/Corner.qml index 0ec37efe..716b30cf 100644 --- a/Components/Corners.qml +++ b/Components/Corner.qml @@ -6,43 +6,43 @@ Shape { id: root property string position: "topleft" // Corner position: topleft/topright/bottomleft/bottomright - property real size: 1.0 // Scale multiplier for entire corner + property real size: 1.0 * Theme.scale(Screen) // Scale multiplier for entire corner property int concaveWidth: 100 * size property int concaveHeight: 60 * size - property int offsetX: -20 - property int offsetY: -20 + property int offsetX: -20 * Theme.scale(Screen) + property int offsetY: -20 * Theme.scale(Screen) property color fillColor: Theme.accentPrimary property int arcRadius: 20 * size property var modelData: null - // Position flags derived from position string - property bool _isTop: position.includes("top") - property bool _isLeft: position.includes("left") - property bool _isRight: position.includes("right") - property bool _isBottom: position.includes("bottom") + // Position flags derived from position string - calculated once + readonly property bool _isTop: position.includes("top") + readonly property bool _isLeft: position.includes("left") + readonly property bool _isRight: position.includes("right") + readonly property bool _isBottom: position.includes("bottom") // Shift the path vertically if offsetY is negative to pull shape up - property real pathOffsetY: Math.min(offsetY, 0) + readonly property real pathOffsetY: Math.min(offsetY, 0) // Base coordinates for left corner shape, shifted by pathOffsetY vertically - property real _baseStartX: 30 * size - property real _baseStartY: (_isTop ? 20 * size : 0) + pathOffsetY - property real _baseLineX: 30 * size - property real _baseLineY: (_isTop ? 0 : 20 * size) + pathOffsetY - property real _baseArcX: 50 * size - property real _baseArcY: (_isTop ? 20 * size : 0) + pathOffsetY + readonly property real _baseStartX: 30 * size + readonly property real _baseStartY: (_isTop ? 20 * size : 0) + pathOffsetY + readonly property real _baseLineX: 30 * size + readonly property real _baseLineY: (_isTop ? 0 : 20 * size) + pathOffsetY + readonly property real _baseArcX: 50 * size + readonly property real _baseArcY: (_isTop ? 20 * size : 0) + pathOffsetY // Mirror coordinates for right corners - property real _startX: _isRight ? (concaveWidth - _baseStartX) : _baseStartX - property real _startY: _baseStartY - property real _lineX: _isRight ? (concaveWidth - _baseLineX) : _baseLineX - property real _lineY: _baseLineY - property real _arcX: _isRight ? (concaveWidth - _baseArcX) : _baseArcX - property real _arcY: _baseArcY + readonly property real _startX: _isRight ? (concaveWidth - _baseStartX) : _baseStartX + readonly property real _startY: _baseStartY + readonly property real _lineX: _isRight ? (concaveWidth - _baseLineX) : _baseLineX + readonly property real _lineY: _baseLineY + readonly property real _arcX: _isRight ? (concaveWidth - _baseArcX) : _baseArcX + readonly property real _arcY: _baseArcY // Arc direction varies by corner to maintain proper concave shape - property int _arcDirection: { + readonly property int _arcDirection: { if (_isTop && _isLeft) return PathArc.Counterclockwise if (_isTop && _isRight) return PathArc.Clockwise if (_isBottom && _isLeft) return PathArc.Clockwise @@ -57,9 +57,10 @@ Shape { x: _isLeft ? offsetX : (parent ? parent.width - width + offsetX : 0) y: _isTop ? offsetY : (parent ? parent.height - height + offsetY : 0) - preferredRendererType: Shape.CurveRenderer - layer.enabled: true - layer.samples: 4 + // Optimized rendering settings - reduced quality for better performance + preferredRendererType: Shape.GeometryRenderer // Use simpler renderer + layer.enabled: false // Disable layer rendering to save memory + antialiasing: true // Use standard antialiasing instead of MSAA ShapePath { strokeWidth: 0 diff --git a/Components/IconButton.qml b/Components/IconButton.qml index c6c0a853..93d623df 100644 --- a/Components/IconButton.qml +++ b/Components/IconButton.qml @@ -19,7 +19,7 @@ MouseArea { Rectangle { anchors.fill: parent - radius: 8 + radius: 8 * Theme.scale(Screen) color: root.hovering ? Theme.accentPrimary : "transparent" } Text { @@ -27,7 +27,7 @@ MouseArea { anchors.centerIn: parent text: root.icon font.family: "Material Symbols Outlined" - font.pixelSize: 24 + font.pixelSize: 24 * Theme.scale(Screen) color: root.hovering ? Theme.onAccent : Theme.textPrimary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/Components/PillIndicator.qml b/Components/PillIndicator.qml index afba1730..2e6d307e 100644 --- a/Components/PillIndicator.qml +++ b/Components/PillIndicator.qml @@ -13,8 +13,8 @@ Item { property color iconCircleColor: Theme.accentPrimary property color iconTextColor: Theme.backgroundPrimary property color collapsedIconColor: Theme.textPrimary - property int pillHeight: 22 - property int iconSize: 22 + property int pillHeight: 22 * Theme.scale(Screen) + property int iconSize: 22 * Theme.scale(Screen) property int pillPaddingHorizontal: 14 property bool autoHide: false @@ -47,7 +47,7 @@ Item { id: textItem anchors.centerIn: parent text: revealPill.text - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) font.family: Theme.fontFamily font.weight: Font.Bold color: textColor @@ -89,7 +89,7 @@ Item { Text { anchors.centerIn: parent font.family: showPill ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) text: revealPill.icon color: showPill ? iconTextColor : collapsedIconColor } diff --git a/Components/Spinner.qml b/Components/Spinner.qml index 9441c544..73c81043 100644 --- a/Components/Spinner.qml +++ b/Components/Spinner.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Settings Item { id: root @@ -6,7 +7,7 @@ Item { property bool running: false property color color: "white" property int size: 16 - property int strokeWidth: 2 + property int strokeWidth: 2 * Theme.scale(Screen) property int duration: 1000 implicitWidth: size diff --git a/Components/StyledTooltip.qml b/Components/StyledTooltip.qml index 58e56a44..ecf7ffde 100644 --- a/Components/StyledTooltip.qml +++ b/Components/StyledTooltip.qml @@ -9,7 +9,6 @@ Window { property Item targetItem: null property int delay: 300 - // New property to control positioning: true => above, false => below property bool positionAbove: true flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint @@ -34,8 +33,8 @@ Window { } function _showNow() { - width = Math.max(50, tooltipText.implicitWidth + 24) - height = Math.max(50, tooltipText.implicitHeight + 16) + width = Math.max(50 * Theme.scale(Screen), tooltipText.implicitWidth + 24 * Theme.scale(Screen)) + height = Math.max(50 * Theme.scale(Screen), tooltipText.implicitHeight + 16 * Theme.scale(Screen)) if (!targetItem) return; @@ -76,10 +75,10 @@ Window { Rectangle { anchors.fill: parent - radius: 20 + radius: 20 * Theme.scale(Screen) color: Theme.backgroundTertiary || "#222" border.color: Theme.outline || "#444" - border.width: 1 + border.width: 1 * Theme.scale(Screen) opacity: 0.97 z: 1 } @@ -89,7 +88,7 @@ Window { text: tooltipWindow.text color: Theme.textPrimary font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -106,7 +105,7 @@ Window { } onTextChanged: { - width = Math.max(minimumWidth, tooltipText.implicitWidth + 24); - height = Math.max(minimumHeight, tooltipText.implicitHeight + 16); + width = Math.max(minimumWidth * Theme.scale(Screen), tooltipText.implicitWidth + 24 * Theme.scale(Screen)); + height = Math.max(minimumHeight * Theme.scale(Screen), tooltipText.implicitHeight + 16 * Theme.scale(Screen)); } -} +} \ No newline at end of file diff --git a/Components/Tabs.qml b/Components/Tabs.qml index d3befeab..0f7f74ac 100644 --- a/Components/Tabs.qml +++ b/Components/Tabs.qml @@ -17,8 +17,8 @@ Item { model: root.tabsModel delegate: Rectangle { id: tabWrapper - implicitHeight: tab.height - implicitWidth: 56 + implicitHeight: tab.height * Theme.scale(Screen) + implicitWidth: 56 * Theme.scale(Screen) color: "transparent" property bool hovered: false @@ -48,7 +48,7 @@ Item { Text { text: modelData.icon font.family: "Material Symbols Outlined" - font.pixelSize: 22 + font.pixelSize: 22 * Theme.scale(Screen) color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : tabWrapper.hovered ? (Theme ? Theme.accentPrimary : "#7C3AED") : (Theme ? Theme.textSecondary : "#444") Layout.alignment: Qt.AlignCenter } @@ -56,7 +56,7 @@ Item { // Label Text { text: modelData.label - font.pixelSize: 12 + font.pixelSize: 12 * Theme.scale(Screen) font.bold: index === root.currentIndex color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : tabWrapper.hovered ? (Theme ? Theme.accentPrimary : "#7C3AED") : (Theme ? Theme.textSecondary : "#444") Layout.alignment: Qt.AlignCenter @@ -64,9 +64,9 @@ Item { // Underline for active tab Rectangle { - width: 24 - height: 2 - radius: 1 + width: 24 * Theme.scale(Screen) + height: 2 * Theme.scale(Screen) + radius: 1 * Theme.scale(Screen) color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : "transparent" Layout.alignment: Qt.AlignCenter } diff --git a/Components/ToggleOption.qml b/Components/ToggleOption.qml new file mode 100644 index 00000000..e99c7276 --- /dev/null +++ b/Components/ToggleOption.qml @@ -0,0 +1,88 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Components +import qs.Settings + +ColumnLayout { + id: root + + property string label: "" + property string description: "" + property bool value: false + property var onToggled: function() { + } + + Layout.fillWidth: true + + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 * Theme.scale(Screen) + Layout.fillWidth: true + + Text { + text: label + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: description + font.pixelSize: 12 * Theme.scale(Screen) + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + } + + Rectangle { + id: switcher + + width: 52 * Theme.scale(Screen) + height: 32 * Theme.scale(Screen) + radius: 16 * Theme.scale(Screen) + color: value ? Theme.accentPrimary : Theme.surfaceVariant + border.color: value ? Theme.accentPrimary : Theme.outline + border.width: 2 * Theme.scale(Screen) + + Rectangle { + width: 28 * Theme.scale(Screen) + height: 28 * Theme.scale(Screen) + radius: 14 * Theme.scale(Screen) + color: Theme.surface + border.color: Theme.outline + border.width: 1 * Theme.scale(Screen) + y: 2 + x: value ? switcher.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + + } + + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.onToggled(); + } + } + + } + + } + + Rectangle { + height: 8 * Theme.scale(Screen) + } + +} diff --git a/Helpers/IPCHandlers.qml b/Helpers/IPCHandlers.qml index 6cc0aea8..90ed5c7c 100644 --- a/Helpers/IPCHandlers.qml +++ b/Helpers/IPCHandlers.qml @@ -14,9 +14,9 @@ IpcHandler { idleInhibitor.toggle() } - function toggleNotificationPopup(): void { console.log("[IPC] NotificationPopup toggle() called") + // Use the global toggle function from the notification manager notificationPopup.togglePopup(); } @@ -43,4 +43,4 @@ IpcHandler { console.log("[IPC] LockScreen show() called"); lockScreen.locked = true; } -} +} \ No newline at end of file diff --git a/Helpers/IdleInhibitor.qml b/Helpers/IdleInhibitor.qml index de78a99d..d2d81662 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/README.md b/README.md index 68f86131..aa2554ce 100644 --- a/README.md +++ b/README.md @@ -231,4 +231,4 @@ Thank you to everyone who supports me and this project 💜! ## License -This project is licensed under the terms of the [MIT License](./LICENSE). +This project is licensed under the terms of the [MIT License](./LICENSE). \ No newline at end of file diff --git a/Services/MusicManager.qml b/Services/MusicManager.qml index 0f57222e..81236b10 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,7 +128,7 @@ Singleton { } } - // Updates progress bar every second + // Update progress bar every second while playing Timer { id: positionTimer interval: 1000 @@ -141,14 +143,14 @@ Singleton { } } - // Reset position when player state changes + // Reset position when switching to inactive player onCurrentPlayerChanged: { if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { currentPosition = 0 } } - // Reacts to player list changes + // 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 00000000..c1dd53a3 --- /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/Settings/Settings.qml b/Settings/Settings.qml index 32b7c3fa..0d066bfa 100644 --- a/Settings/Settings.qml +++ b/Settings/Settings.qml @@ -45,9 +45,10 @@ 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 + property bool showCorners: false property bool showTaskbar: true property bool showMediaInBar: false property bool useSWWW: false @@ -68,6 +69,19 @@ Singleton { 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, "*" means all monitors } } @@ -76,5 +90,7 @@ Singleton { function onRandomWallpaperChanged() { WallpaperManager.toggleRandomWallpaper() } function onWallpaperIntervalChanged() { WallpaperManager.restartRandomWallpaperTimer() } function onWallpaperFolderChanged() { WallpaperManager.loadWallpapers() } + function onNotificationMonitorsChanged() { + } } } \ No newline at end of file diff --git a/Settings/Theme.qml b/Settings/Theme.qml index 74f3afcd..2ffe87da 100644 --- a/Settings/Theme.qml +++ b/Settings/Theme.qml @@ -8,6 +8,22 @@ import qs.Settings Singleton { id: root + // Design screen width + readonly property int designScreenWidth: 1920 + + // Scaling dampening factor - reduces the scaling effect for higher resolutions + readonly property real scalingDampening: 0.2 + + // Automatic scaling based on screen width + function scale(currentScreen) { + if (currentScreen !== undefined) { + var rawRatio = currentScreen.width / designScreenWidth + // Apply dampening to reduce scaling for higher resolutions + return Math.min(2.0, 1.0 + (rawRatio - 1.0) * scalingDampening) + } + return 1.0 + } + function applyOpacity(color, opacity) { return color.replace("#", "#" + opacity); } diff --git a/Templates/templates/kitty.conf b/Templates/templates/kitty.conf new file mode 100644 index 00000000..040feda7 --- /dev/null +++ b/Templates/templates/kitty.conf @@ -0,0 +1,29 @@ +# The kitty terminal template for wallust +# Add to wallust config: kitty = { src='kitty.conf', dst='~/.config/kitty/colors.conf'} +# And add to kitty config: include colors.conf + +cursor {{ cursor }} + +background {{ background }} +foreground {{ foreground }} + +color0 {{ color0 }} +color1 {{ color1 }} +color2 {{ color2 }} +color3 {{ color3 }} +color4 {{ color4 }} +color5 {{ color5 }} +color6 {{ color6 }} +color7 {{ color7 }} +color8 {{ color8 }} +color9 {{ color9 }} +color10 {{ color10 }} +color11 {{ color11 }} +color12 {{ color12 }} +color13 {{ color13 }} +color14 {{ color14 }} +color15 {{ color15 }} + +mark1_foreground {{ color6 | saturate(0.2) }} +mark2_foreground {{ color7 | saturate(0.2) }} +mark3_foreground {{ color6 | saturate(0.2) }} diff --git a/Templates/templates/niri.kdl b/Templates/templates/niri.kdl new file mode 100644 index 00000000..cd99b52b --- /dev/null +++ b/Templates/templates/niri.kdl @@ -0,0 +1,292 @@ +// Niri configuration for CachyOS +// For documentation and full reference, see: https://github.com/YaLTeR/niri/wiki + +// ────────────── Input Configuration ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Input + +input { + keyboard { + xkb { + layout "de" // Use the German keyboard layout + } + numlock // Enable numlock on startup + } + + touchpad { + tap // Enable tap-to-click + natural-scroll // Enable natural (macOS-style) scrolling + } + + focus-follows-mouse // Automatically focus windows under the mouse pointer + workspace-auto-back-and-forth // Enable workspace back & forth switching +} + +// ────────────── Output Configuration ────────────── +// You can run `niri msg outputs` to get the correct name for your displays. +// You will have to remove "/-" and edit it before it takes effect. +// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs + + output "DP-1" { + mode "2560x1440@359.979" // Set resolution and refresh rate + scale 1 // No scaling (use 2 for HiDPI) +} + +// ────────────── Keybindings ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Key-Bindings + +binds { + MOD+SHIFT+ESCAPE { show-hotkey-overlay; } + + // ─── Applications ─── + MOD+RETURN hotkey-overlay-title="Open Terminal: Kitty" { spawn "kitty"; } + MOD+CTRL+RETURN hotkey-overlay-title="Open App Launcher: QS" { spawn "qs" "ipc" "call" "globalIPC" "toggleLauncher"; } + MOD+B hotkey-overlay-title="Open Browser: firefox" { spawn "firefox"; } + MOD+ALT+L hotkey-overlay-title="Lock Screen: swaylock" { spawn "swaylock"; } + + // Please choose your own file manager + MOD+E hotkey-overlay-title="File Manager: Nautilus" { spawn "nautilus"; } + + // ─── Audio Controls ─── + XF86AUDIORAISEVOLUME allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; } + XF86AUDIOLOWERVOLUME allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; } + XF86AUDIOMUTE allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; } + XF86AUDIOMICMUTE allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; } + + // ─── Window Movement and Focus ─── + MOD+Q { close-window; } + + MOD+LEFT { focus-column-left; } + MOD+H { focus-column-left; } + MOD+RIGHT { focus-column-right; } + MOD+L { focus-column-right; } + MOD+UP { focus-window-up; } + MOD+K { focus-window-up; } + MOD+DOWN { focus-window-down; } + MOD+J { focus-window-down; } + + MOD+CTRL+LEFT { move-column-left; } + MOD+CTRL+H { move-column-left; } + MOD+CTRL+RIGHT { move-column-right; } + MOD+CTRL+L { move-column-right; } + MOD+CTRL+UP { move-window-up; } + MOD+CTRL+K { move-window-up; } + MOD+CTRL+DOWN { move-window-down; } + MOD+CTRL+J { move-window-down; } + + MOD+HOME { focus-column-first; } + MOD+END { focus-column-last; } + MOD+CTRL+HOME { move-column-to-first; } + MOD+CTRL+END { move-column-to-last; } + + MOD+SHIFT+LEFT { focus-monitor-left; } + MOD+SHIFT+RIGHT { focus-monitor-right; } + MOD+SHIFT+UP { focus-monitor-up; } + MOD+SHIFT+DOWN { focus-monitor-down; } + + MOD+SHIFT+CTRL+LEFT { move-column-to-monitor-left; } + MOD+SHIFT+CTRL+RIGHT { move-column-to-monitor-right; } + MOD+SHIFT+CTRL+UP { move-column-to-monitor-up; } + MOD+SHIFT+CTRL+DOWN { move-column-to-monitor-down; } + + // ─── Workspace Switching ─── + MOD+WHEELSCROLLDOWN cooldown-ms=150 { focus-workspace-down; } + MOD+WHEELSCROLLUP cooldown-ms=150 { focus-workspace-up; } + MOD+CTRL+WHEELSCROLLDOWN cooldown-ms=150 { move-column-to-workspace-down; } + MOD+CTRL+WHEELSCROLLUP cooldown-ms=150 { move-column-to-workspace-up; } + + MOD+WHEELSCROLLRIGHT { focus-column-right; } + MOD+WHEELSCROLLLEFT { focus-column-left; } + MOD+CTRL+WHEELSCROLLRIGHT { move-column-right; } + MOD+CTRL+WHEELSCROLLLEFT { move-column-left; } + + MOD+SHIFT+WHEELSCROLLDOWN { focus-column-right; } + MOD+SHIFT+WHEELSCROLLUP { focus-column-left; } + MOD+CTRL+SHIFT+WHEELSCROLLDOWN { move-column-right; } + MOD+CTRL+SHIFT+WHEELSCROLLUP { move-column-left; } + + MOD+1 { focus-workspace 1; } + MOD+2 { focus-workspace 2; } + MOD+3 { focus-workspace 3; } + MOD+4 { focus-workspace 4; } + MOD+5 { focus-workspace 5; } + MOD+6 { focus-workspace 6; } + MOD+7 { focus-workspace 7; } + MOD+8 { focus-workspace 8; } + MOD+9 { focus-workspace 9; } + + MOD+CTRL+1 { move-column-to-workspace 1; } + MOD+CTRL+2 { move-column-to-workspace 2; } + MOD+CTRL+3 { move-column-to-workspace 3; } + MOD+CTRL+4 { move-column-to-workspace 4; } + MOD+CTRL+5 { move-column-to-workspace 5; } + MOD+CTRL+6 { move-column-to-workspace 6; } + MOD+CTRL+7 { move-column-to-workspace 7; } + MOD+CTRL+8 { move-column-to-workspace 8; } + MOD+CTRL+9 { move-column-to-workspace 9; } + + MOD+TAB { focus-workspace-previous; } + + // ─── Layout Controls ─── + MOD+CTRL+F { expand-column-to-available-width; } + MOD+C { center-column; } + MOD+CTRL+C { center-visible-columns; } + MOD+MINUS { set-column-width "-10%"; } + MOD+EQUAL { set-column-width "+10%"; } + MOD+SHIFT+MINUS { set-window-height "-10%"; } + MOD+SHIFT+EQUAL { set-window-height "+10%"; } + + // ─── Modes ─── + MOD+T { toggle-window-floating; } + MOD+F { fullscreen-window; } + MOD+W { toggle-column-tabbed-display; } + + // ─── Screenshots ─── + CTRL+SHIFT+1 { screenshot; } + CTRL+SHIFT+2 { screenshot-screen; } + CTRL+SHIFT+3 { screenshot-window; } + + // ─── Emergency Escape Key ─── + // Use this when a fullscreen app blocks your keybinds. + // It disables any active keyboard shortcut inhibitor, restoring control. + MOD+ESCAPE allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; } + + // ─── Exit / Power ─── + CTRL+ALT+DELETE { quit; } // Also quits Niri + MOD+SHIFT+P { power-off-monitors; } // Turn off screens (useful for OLED or privacy) + MOD+O repeat=false { toggle-overview; } +} + +// ────────────── Startup Applications ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#spawn-at-startup + + spawn-at-startup "/usr/lib/polkit-kde-authentication-agent-1" "&" // Polkit + spawn-at-startup "xwayland-satellite" // XWayland support + spawn-at-startup "swww-daemon" // Wallpaper daemon + spawn-at-startup "swww img" "/usr/share/wallpapers/cachyos-wallpapers/Skyscraper.png" // Set wallpaper + spawn-at-startup "qs" // Launch Quickshell + spawn-at-startup "vesktop" // Launch Vesktop + + prefer-no-csd // Disable program decorations + screenshot-path null // Disable screenshot saving + +// ────────────── Layout Settings ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout + + layout { + gaps 16 // Gap between windows + center-focused-column "never" // Don’t auto-center focused column + + preset-column-widths { + proportion 0.33333 + proportion 0.5 + proportion 0.66667 + } + + focus-ring { + width 3 + active-color "{{ color4 }}" + inactive-color "{{ color0 }}" + } + + shadow { + softness 30 + spread 5 + offset x=0 y=5 + color "#0007" + } + + background-color "transparent" + + struts {} + } + +// ────────────── Animation Settings ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Animations + animations { + workspace-switch { + spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001 + } + window-open { + duration-ms 200 + curve "ease-out-quad" + } + window-close { + duration-ms 200 + curve "ease-out-cubic" + } + horizontal-view-movement { + spring damping-ratio=1.0 stiffness=900 epsilon=0.0001 + } + window-movement { + spring damping-ratio=1.0 stiffness=800 epsilon=0.0001 + } + window-resize { + spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001 + } + config-notification-open-close { + spring damping-ratio=0.6 stiffness=1200 epsilon=0.001 + } + screenshot-ui-open { + duration-ms 300 + curve "ease-out-quad" + } + overview-open-close { + spring damping-ratio=1.0 stiffness=900 epsilon=0.0001 + } + } + +// ────────────── Named Workspaces ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules + + workspace "browser" + workspace "chat" + +// ────────────── Window Rules ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules + + window-rule { + match at-startup=true app-id="vesktop" + open-on-workspace "chat" + open-maximized true + } + + window-rule { + match app-id="firefox" + open-on-workspace "browser" + open-maximized true + } + + window-rule { + match app-id=r#"firefox$"# title="^Picture-in-Picture$" + open-floating true // Always float Firefox PiP windows + } + + window-rule { + geometry-corner-radius 20 // Set every window radius to 20 + clip-to-geometry true + } + +// ────────────── Layer Rules ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Layer-Rules + + layer-rule { + match namespace="^swww-daemon$" + place-within-backdrop true + } + +// ────────────── Environment Variables ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#environment + + environment { + DISPLAY ":1" + ELECTRON_OZONE_PLATFORM_HINT "auto" + QT_QPA_PLATFORM "wayland" + QT_WAYLAND_DISABLE_WINDOWDECORATION "1" + XDG_SESSION_TYPE "wayland" + XDG_CURRENT_DESKTOP "niri" + } + +// ────────────── Misc ────────────── +hotkey-overlay { + skip-at-startup +} \ No newline at end of file diff --git a/Widgets/Dock.qml b/Widgets/Dock.qml index 5dbcb5d9..578a97ab 100644 --- a/Widgets/Dock.qml +++ b/Widgets/Dock.qml @@ -8,7 +8,9 @@ import qs.Components PanelWindow { id: taskbarWindow - visible: Settings.settings.showDock + 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 @@ -245,7 +247,7 @@ PanelWindow { contextMenuVisible = false; contextMenuTarget = null; contextMenuToplevel = null; - hidden = true; // Hide dock when context menu closes by clicking outside + hidden = true; // Hide dock when context menu closes } } @@ -281,7 +283,7 @@ PanelWindow { spacing: 4 width: parent.width - // Close + Rectangle { width: parent.width height: 32 @@ -300,7 +302,7 @@ PanelWindow { anchors.verticalCenter: parent.verticalCenter text: "close" font.family: "Material Symbols Outlined" - font.pixelSize: 14 + font.pixelSize: 14 * Theme.scale(Screen) color: Theme.textPrimary } @@ -308,7 +310,7 @@ PanelWindow { anchors.verticalCenter: parent.verticalCenter text: "Close" font.family: Theme.fontFamily - font.pixelSize: 14 + font.pixelSize: 14 * Theme.scale(Screen) color: Theme.textPrimary } } @@ -322,7 +324,7 @@ PanelWindow { onClicked: { if (contextMenuToplevel?.close) contextMenuToplevel.close(); contextMenuVisible = false; - hidden = true; // Hide the dock here as well + hidden = true; } } } diff --git a/Widgets/LockScreen/BatteryCharge.qml b/Widgets/LockScreen/BatteryCharge.qml index 5e8b4c09..ad00d456 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) @@ -58,7 +58,7 @@ Item { Text { text: batteryIcon() font.family: "Material Symbols Outlined" - font.pixelSize: 28 + font.pixelSize: 28 * Theme.scale(Screen) color: charging ? Theme.accentPrimary : Theme.textSecondary verticalAlignment: Text.AlignVBottom } @@ -66,7 +66,7 @@ Item { Text { text: Math.round(percent) + "%" font.family: Theme.fontFamily - font.pixelSize: 18 + font.pixelSize: 18 * Theme.scale(Screen) color: Theme.textSecondary verticalAlignment: Text.AlignVBottom } diff --git a/Widgets/LockScreen/LockScreen.qml b/Widgets/LockScreen/LockScreen.qml index 2eaf4795..adf524c6 100644 --- a/Widgets/LockScreen/LockScreen.qml +++ b/Widgets/LockScreen/LockScreen.qml @@ -152,21 +152,21 @@ WlSessionLock { ColumnLayout { anchors.centerIn: parent spacing: 30 - width: Math.min(parent.width * 0.8, 400) + width: Math.min(parent.width * 0.8, 400 * Theme.scale(Screen)) Rectangle { Layout.alignment: Qt.AlignHCenter - width: 80 - height: 80 - radius: 40 + width: 80 * Theme.scale(Screen) + height: 80 * Theme.scale(Screen) + radius: 40 * Theme.scale(Screen) color: Theme.accentPrimary Rectangle { anchors.fill: parent color: "transparent" - radius: 40 + radius: 40 * Theme.scale(Screen) border.color: Theme.accentPrimary - border.width: 3 + border.width: 3 * Theme.scale(Screen) z: 2 } @@ -183,28 +183,28 @@ WlSessionLock { Layout.alignment: Qt.AlignHCenter text: Quickshell.env("USER") font.family: Theme.fontFamily - font.pixelSize: 24 + font.pixelSize: 24 * Theme.scale(Screen) font.weight: Font.Medium color: Theme.textPrimary } Rectangle { Layout.fillWidth: true - height: 50 - radius: 25 + height: 50 * Theme.scale(Screen) + radius: 25 * Theme.scale(Screen) color: Theme.surface opacity: passwordInput.activeFocus ? 0.8 : 0.3 border.color: passwordInput.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 2 + border.width: 2 * Theme.scale(Screen) TextInput { id: passwordInput anchors.fill: parent - anchors.margins: 15 + anchors.margins: 15 * Theme.scale(Screen) verticalAlignment: TextInput.AlignVCenter horizontalAlignment: TextInput.AlignHCenter font.family: Theme.fontFamily - font.pixelSize: 16 + font.pixelSize: 16 * Theme.scale(Screen) color: Theme.textPrimary echoMode: TextInput.Password passwordCharacter: "●" @@ -218,7 +218,7 @@ WlSessionLock { text: "Enter password..." color: Theme.textSecondary font.family: Theme.fontFamily - font.pixelSize: 16 + font.pixelSize: 16 * Theme.scale(Screen) visible: !passwordInput.text && !passwordInput.activeFocus } @@ -238,9 +238,9 @@ WlSessionLock { id: errorMessageRect Layout.alignment: Qt.AlignHCenter width: parent.width * 0.8 - height: 44 + height: 44 * Theme.scale(Screen) color: Theme.overlay - radius: 20 + radius: 20 * Theme.scale(Screen) visible: lock.errorMessage !== "" Text { @@ -248,7 +248,7 @@ WlSessionLock { text: lock.errorMessage color: Theme.error font.family: Theme.fontFamily - font.pixelSize: 14 + font.pixelSize: 14 * Theme.scale(Screen) opacity: 1 visible: lock.errorMessage !== "" } @@ -256,13 +256,13 @@ WlSessionLock { Rectangle { Layout.alignment: Qt.AlignHCenter - width: 120 - height: 44 - radius: 20 + width: 120 * Theme.scale(Screen) + height: 44 * Theme.scale(Screen) + radius: 20 * Theme.scale(Screen) opacity: unlockButtonArea.containsMouse ? 0.8 : 0.5 color: unlockButtonArea.containsMouse ? Theme.accentPrimary : Theme.surface border.color: Theme.accentPrimary - border.width: 2 + border.width: 2 * Theme.scale(Screen) enabled: !lock.authenticating Text { @@ -270,7 +270,7 @@ WlSessionLock { anchors.centerIn: parent text: lock.authenticating ? "..." : "Unlock" font.family: Theme.fontFamily - font.pixelSize: 16 + font.pixelSize: 16 * Theme.scale(Screen) font.bold: true color: unlockButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary } @@ -294,37 +294,13 @@ WlSessionLock { } } - Corners { - id: topRightCorner - position: "bottomleft" - size: 1.3 - fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" - offsetX: screen.width / 2 + 38 - offsetY: 0 - anchors.top: parent.top - visible: Settings.settings.showCorners - z: 50 - } - - Corners { - id: topLeftCorner - position: "bottomright" - size: 1.3 - fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" - offsetX: -Screen.width / 2 - 38 - offsetY: 0 - anchors.top: parent.top - visible: Settings.settings.showCorners - z: 51 - } - Rectangle { - width: infoColumn.width + 32 - height: infoColumn.height + 8 + width: infoColumn.width + 32 * Theme.scale(Screen) + height: infoColumn.height + 8 * Theme.scale(Screen) color: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" anchors.horizontalCenter: parent.horizontalCenter - bottomLeftRadius: 20 - bottomRightRadius: 20 + bottomLeftRadius: 20 * Theme.scale(Screen) + bottomRightRadius: 20 * Theme.scale(Screen) ColumnLayout { id: infoColumn @@ -338,7 +314,7 @@ WlSessionLock { id: timeText text: Qt.formatDateTime(new Date(), "HH:mm") font.family: Theme.fontFamily - font.pixelSize: 48 + font.pixelSize: 48 * Theme.scale(Screen) font.bold: true color: Theme.textPrimary horizontalAlignment: Text.AlignHCenter @@ -348,7 +324,7 @@ WlSessionLock { id: dateText text: Qt.formatDateTime(new Date(), "dddd, MMMM d") font.family: Theme.fontFamily - font.pixelSize: 16 + font.pixelSize: 16 * Theme.scale(Screen) color: Theme.textSecondary opacity: 0.8 horizontalAlignment: Text.AlignHCenter @@ -364,7 +340,7 @@ WlSessionLock { Text { text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud" font.family: "Material Symbols Outlined" - font.pixelSize: 28 + font.pixelSize: 28 * Theme.scale(Screen) color: Theme.accentPrimary verticalAlignment: Text.AlignVCenter } @@ -372,7 +348,7 @@ WlSessionLock { Text { text: weatherData && weatherData.current_weather ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9 / 5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--°F" : "--°C") font.family: Theme.fontFamily - font.pixelSize: 18 + font.pixelSize: 18 * Theme.scale(Screen) color: Theme.textSecondary verticalAlignment: Text.AlignVCenter } @@ -383,7 +359,7 @@ WlSessionLock { color: Theme.error visible: weatherError !== "" font.family: Theme.fontFamily - font.pixelSize: 10 + font.pixelSize: 10 * Theme.scale(Screen) horizontalAlignment: Text.AlignHCenter Layout.alignment: Qt.AlignHCenter } @@ -425,12 +401,12 @@ WlSessionLock { spacing: 12 Rectangle { - width: 48 - height: 48 - radius: 24 + width: 48 * Theme.scale(Screen) + height: 48 * Theme.scale(Screen) + radius: 24 * Theme.scale(Screen) color: shutdownArea.containsMouse ? Theme.error : "transparent" border.color: Theme.error - border.width: 1 + border.width: 1 * Theme.scale(Screen) MouseArea { id: shutdownArea @@ -445,18 +421,18 @@ WlSessionLock { anchors.centerIn: parent text: "power_settings_new" font.family: "Material Symbols Outlined" - font.pixelSize: 24 + font.pixelSize: 24 * Theme.scale(Screen) color: shutdownArea.containsMouse ? Theme.onAccent : Theme.error } } Rectangle { - width: 48 - height: 48 - radius: 24 + width: 48 * Theme.scale(Screen) + height: 48 * Theme.scale(Screen) + radius: 24 * Theme.scale(Screen) color: rebootArea.containsMouse ? Theme.accentPrimary : "transparent" border.color: Theme.accentPrimary - border.width: 1 + border.width: 1 * Theme.scale(Screen) MouseArea { id: rebootArea @@ -471,18 +447,18 @@ WlSessionLock { anchors.centerIn: parent text: "refresh" font.family: "Material Symbols Outlined" - font.pixelSize: 24 + font.pixelSize: 24 * Theme.scale(Screen) color: rebootArea.containsMouse ? Theme.onAccent : Theme.accentPrimary } } Rectangle { - width: 48 - height: 48 - radius: 24 + width: 48 * Theme.scale(Screen) + height: 48 * Theme.scale(Screen) + radius: 24 * Theme.scale(Screen) color: logoutArea.containsMouse ? Theme.accentSecondary : "transparent" border.color: Theme.accentSecondary - border.width: 1 + border.width: 1 * Theme.scale(Screen) MouseArea { id: logoutArea @@ -497,7 +473,7 @@ WlSessionLock { anchors.centerIn: parent text: "exit_to_app" font.family: "Material Symbols Outlined" - font.pixelSize: 24 + font.pixelSize: 24 * Theme.scale(Screen) color: logoutArea.containsMouse ? Theme.onAccent : Theme.accentSecondary } } diff --git a/Widgets/Notification/NotificationHistory.qml b/Widgets/Notification/NotificationHistory.qml index be98dd05..b0be4bac 100644 --- a/Widgets/Notification/NotificationHistory.qml +++ b/Widgets/Notification/NotificationHistory.qml @@ -1,61 +1,31 @@ import QtQuick +import QtQuick.Layouts import Quickshell import Quickshell.Io -import qs.Settings -import QtQuick.Layouts import qs.Components +import qs.Settings -// The popup window PanelWithOverlay { id: notificationHistoryWin + property string historyFilePath: Settings.settingsDir + "notification_history.json" property bool hasUnread: notificationHistoryWinRect.hasUnread && !notificationHistoryWinRect.visible - function addToHistory(notification) { notificationHistoryWinRect.addToHistory(notification) } + + function addToHistory(notification) { + notificationHistoryWinRect.addToHistory(notification); + } + Rectangle { id: notificationHistoryWinRect - implicitWidth: 400 + property int maxPopupHeight: 800 property int minPopupHeight: 210 property int contentHeight: headerRow.height + historyList.contentHeight + 56 - implicitHeight: Math.max(Math.min(contentHeight, maxPopupHeight), minPopupHeight) - visible: parent.visible - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: 4 - anchors.rightMargin: 4 - color: Theme.backgroundPrimary - radius: 20 - property int maxHistory: 100 property bool hasUnread: false + signal unreadChanged(bool hasUnread) - ListModel { - id: historyModel - } - - FileView { - id: historyFileView - path: notificationHistoryWin.historyFilePath - blockLoading: true - printErrors: true - watchChanges: true - - JsonAdapter { - id: historyAdapter - property var notifications: [] - } - - onFileChanged: historyFileView.reload() - onLoaded: notificationHistoryWinRect.loadHistory() - onLoadFailed: function (error) { - historyAdapter.notifications = []; - historyFileView.writeAdapter(); - } - Component.onCompleted: if (path) - reload() - } - function updateHasUnread() { var unread = false; for (let i = 0; i < historyModel.count; ++i) { @@ -80,9 +50,11 @@ PanelWithOverlay { if (typeof n === 'object' && n !== null) { if (n.read === undefined) n.read = false; + // Mark as read if window is open if (notificationHistoryWinRect.visible) n.read = true; + historyModel.append(n); } } @@ -95,19 +67,19 @@ PanelWithOverlay { const count = Math.min(historyModel.count, maxHistory); for (let i = 0; i < count; ++i) { let obj = historyModel.get(i); - if (typeof obj === 'object' && obj !== null) { + if (typeof obj === 'object' && obj !== null) historyArray.push({ - id: obj.id, - appName: obj.appName, - summary: obj.summary, - body: obj.body, - timestamp: obj.timestamp, - read: obj.read === undefined ? false : obj.read + "id": obj.id, + "appName": obj.appName, + "summary": obj.summary, + "body": obj.body, + "timestamp": obj.timestamp, + "read": obj.read === undefined ? false : obj.read }); - } + } historyAdapter.notifications = historyArray; - Qt.callLater(function () { + Qt.callLater(function() { historyFileView.writeAdapter(); }); updateHasUnread(); @@ -116,12 +88,12 @@ PanelWithOverlay { function addToHistory(notification) { if (!notification.id) notification.id = Date.now(); + if (!notification.timestamp) notification.timestamp = new Date().toISOString(); // Mark as read if window is open notification.read = visible; - // Remove duplicate by id for (let i = 0; i < historyModel.count; ++i) { if (historyModel.get(i).id === notification.id) { @@ -129,11 +101,10 @@ PanelWithOverlay { break; } } - historyModel.insert(0, notification); - if (historyModel.count > maxHistory) historyModel.remove(maxHistory); + saveHistory(); } @@ -146,6 +117,7 @@ PanelWithOverlay { function formatTimestamp(ts) { if (!ts) return ""; + var date = typeof ts === "number" ? new Date(ts) : new Date(Date.parse(ts)); var y = date.getFullYear(); var m = (date.getMonth() + 1).toString().padStart(2, '0'); @@ -155,6 +127,15 @@ PanelWithOverlay { return `${y}-${m}-${d} ${h}:${min}`; } + implicitWidth: 400 + implicitHeight: Math.max(Math.min(contentHeight, maxPopupHeight), minPopupHeight) + visible: parent.visible + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 4 + anchors.rightMargin: 4 + color: Theme.backgroundPrimary + radius: 20 onVisibleChanged: { if (visible) { // Mark all as read when popup is opened @@ -167,9 +148,46 @@ PanelWithOverlay { } if (changed) saveHistory(); + } } + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + ListModel { + id: historyModel + } + + FileView { + id: historyFileView + + path: notificationHistoryWin.historyFilePath + blockLoading: true + printErrors: true + watchChanges: true + onFileChanged: historyFileView.reload() + onLoaded: notificationHistoryWinRect.loadHistory() + onLoadFailed: function(error) { + historyAdapter.notifications = []; + historyFileView.writeAdapter(); + } + Component.onCompleted: { + if (path) { + reload(); + } + } + + JsonAdapter { + id: historyAdapter + + property var notifications: [] + } + + } + Rectangle { width: notificationHistoryWinRect.width height: notificationHistoryWinRect.height @@ -184,6 +202,7 @@ PanelWithOverlay { RowLayout { id: headerRow + spacing: 4 anchors.topMargin: 4 anchors.left: parent.left @@ -193,6 +212,7 @@ PanelWithOverlay { Layout.preferredHeight: 52 anchors.leftMargin: 16 anchors.rightMargin: 16 + Text { text: "Notification History" font.pixelSize: 18 @@ -200,11 +220,14 @@ PanelWithOverlay { color: Theme.textPrimary Layout.alignment: Qt.AlignVCenter } + Item { Layout.fillWidth: true } + Rectangle { id: clearAllButton + width: 90 height: 32 radius: 16 @@ -212,9 +235,11 @@ PanelWithOverlay { border.color: Theme.accentPrimary border.width: 1 Layout.alignment: Qt.AlignVCenter + Row { anchors.centerIn: parent spacing: 6 + Text { text: "delete_sweep" font.family: "Material Symbols Outlined" @@ -222,6 +247,7 @@ PanelWithOverlay { color: clearAllMouseArea.containsMouse ? Theme.onAccent : Theme.accentPrimary verticalAlignment: Text.AlignVCenter } + Text { text: "Clear" font.pixelSize: Theme.fontSizeSmall @@ -229,15 +255,20 @@ PanelWithOverlay { font.bold: true verticalAlignment: Text.AlignVCenter } + } + MouseArea { id: clearAllMouseArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: notificationHistoryWinRect.clearHistory() } + } + } Rectangle { @@ -261,29 +292,36 @@ PanelWithOverlay { radius: 20 z: 0 } + Rectangle { id: listContainer + anchors.fill: parent anchors.topMargin: 12 anchors.bottomMargin: 12 color: "transparent" clip: true + Column { anchors.fill: parent spacing: 0 ListView { id: historyList + width: parent.width height: Math.min(contentHeight, parent.height) spacing: 12 model: historyModel.count > 0 ? historyModel : placeholderModel clip: true + delegate: Item { width: parent.width height: notificationCard.implicitHeight + 12 + Rectangle { id: notificationCard + width: parent.width - 24 anchors.horizontalCenter: parent.horizontalCenter color: Theme.backgroundPrimary @@ -292,16 +330,22 @@ PanelWithOverlay { anchors.bottom: parent.bottom anchors.margins: 0 implicitHeight: contentColumn.implicitHeight + 20 + Column { id: contentColumn + anchors.fill: parent anchors.margins: 14 spacing: 6 + RowLayout { id: headerRow2 + spacing: 8 + Rectangle { id: iconBackground + width: 28 height: 28 radius: 20 @@ -309,6 +353,7 @@ PanelWithOverlay { border.color: Qt.darker(Theme.accentPrimary, 1.2) border.width: 1.2 Layout.alignment: Qt.AlignVCenter + Text { anchors.centerIn: parent text: model.appName ? model.appName.charAt(0).toUpperCase() : "?" @@ -317,11 +362,15 @@ PanelWithOverlay { font.bold: true color: Theme.backgroundPrimary } + } + Column { id: appInfoColumn + spacing: 0 Layout.alignment: Qt.AlignVCenter + Text { text: model.appName || "No Notifications" font.bold: true @@ -330,6 +379,7 @@ PanelWithOverlay { font.pixelSize: Theme.fontSizeSmall verticalAlignment: Text.AlignVCenter } + Text { visible: !model.isPlaceholder text: model.timestamp ? notificationHistoryWinRect.formatTimestamp(model.timestamp) : "" @@ -338,11 +388,15 @@ PanelWithOverlay { font.pixelSize: Theme.fontSizeCaption verticalAlignment: Text.AlignVCenter } + } + Item { Layout.fillWidth: true } + } + Text { text: model.summary || (model.isPlaceholder ? "You're all caught up!" : "") color: Theme.textSecondary @@ -351,6 +405,7 @@ PanelWithOverlay { width: parent.width wrapMode: Text.Wrap } + Text { text: model.body || (model.isPlaceholder ? "No notifications to show." : "") color: Theme.textDisabled @@ -359,12 +414,19 @@ PanelWithOverlay { width: parent.width wrapMode: Text.Wrap } + } + } + } + } + } + } + } Rectangle { @@ -375,14 +437,20 @@ PanelWithOverlay { ListModel { id: placeholderModel + ListElement { appName: "" summary: "" body: "" isPlaceholder: true } + } + } + } + } + } diff --git a/Widgets/Notification/NotificationIcon.qml b/Widgets/Notification/NotificationIcon.qml index 429fd1b2..dfc75a45 100644 --- a/Widgets/Notification/NotificationIcon.qml +++ b/Widgets/Notification/NotificationIcon.qml @@ -8,25 +8,49 @@ Item { id: root width: 22; height: 22 property bool isSilence: false + property var shell: null - // Process for executing CLI commands Process { id: rightClickProcess command: ["qs","ipc", "call", "globalIPC", "toggleNotificationPopup"] } - // Bell icon/button + // Timer to check when NotificationHistory is loaded + Timer { + id: checkHistoryTimer + interval: 50 + repeat: true + onTriggered: { + if (shell && shell.notificationHistoryWin) { + shell.notificationHistoryWin.visible = true; + checkHistoryTimer.stop(); + } + } + } + Item { id: bell width: 22; height: 22 Text { id: bellText anchors.centerIn: parent - text: notificationHistoryWin.hasUnread ? "notifications_unread" : "notifications" + text: { + if (shell && shell.notificationHistoryWin && shell.notificationHistoryWin.hasUnread) { + return "notifications_unread"; + } else { + return "notifications"; + } + } font.family: mouseAreaBell.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pixelSize: 16 - font.weight: notificationHistoryWin.hasUnread ? Font.Bold : Font.Normal - color: mouseAreaBell.containsMouse ? Theme.accentPrimary : (notificationHistoryWin.hasUnread ? Theme.accentPrimary : Theme.textDisabled) + font.pixelSize: 16 * Theme.scale(Screen) + font.weight: { + if (shell && shell.notificationHistoryWin && shell.notificationHistoryWin.hasUnread) { + return Font.Bold; + } else { + return Font.Normal; + } + } + color: mouseAreaBell.containsMouse ? Theme.accentPrimary : (shell && shell.notificationHistoryWin && shell.notificationHistoryWin.hasUnread ? Theme.accentPrimary : Theme.textDisabled) } MouseArea { id: mouseAreaBell @@ -42,10 +66,18 @@ Item { } if (mouse.button === Qt.LeftButton){ - notificationHistoryWin.visible = !notificationHistoryWin.visible - return; + if (shell) { + if (!shell.notificationHistoryWin) { + // Use the shell function to load notification history + shell.loadNotificationHistory(); + checkHistoryTimer.start(); + } else { + // Already loaded, just toggle visibility + shell.notificationHistoryWin.visible = !shell.notificationHistoryWin.visible; + } + } + return; } - } onEntered: notificationTooltip.tooltipVisible = true onExited: notificationTooltip.tooltipVisible = false diff --git a/Widgets/Notification/NotificationManager.qml b/Widgets/Notification/NotificationManager.qml index 8ac1d5d2..94294b92 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 dbce7719..761fd4ff 100644 --- a/Widgets/Notification/NotificationPopup.qml +++ b/Widgets/Notification/NotificationPopup.qml @@ -4,310 +4,389 @@ import Quickshell import Quickshell.Widgets import qs.Settings -PanelWindow { - id: window - implicitWidth: 350 - implicitHeight: notificationColumn.implicitHeight - color: "transparent" - visible: notificationsVisible && notificationModel.count > 0 - screen: Quickshell.primaryScreen !== undefined ? Quickshell.primaryScreen : null - focusable: false +// Main container that manages multiple notification popups for different monitors +Item { + id: notificationManager + anchors.fill: parent - property bool barVisible: true + // Get list of available monitors/screens + property var monitors: Quickshell.screens || [] + + Component.onCompleted: { + console.log("[NotificationPopup] Initialized with", monitors.length, "monitors"); + for (let i = 0; i < monitors.length; i++) { + console.log("[NotificationPopup] Monitor", i, ":", monitors[i].name); + } + } + + // Global visibility state for all notification popups property bool notificationsVisible: true - - anchors.top: true - anchors.right: true - margins.top: 6 - margins.right: 6 - - ListModel { - id: notificationModel - } - - property int maxVisible: 5 - property int spacing: 5 - + function togglePopup(): void { - console.log("[NotificationPopup] Current state: " + notificationsVisible); + console.log("[NotificationManager] Current state: " + notificationsVisible); notificationsVisible = !notificationsVisible; - console.log("[NotificationPopup] New state: " + notificationsVisible); + console.log("[NotificationManager] New state: " + notificationsVisible); } - - function addNotification(notification) { - notificationModel.insert(0, { - id: notification.id, - appName: notification.appName || "Notification", - summary: notification.summary || "", - body: notification.body || "", - urgency: notification.urgency || 0, - rawNotification: notification, - appeared: false, - dismissed: false - }); - - while (notificationModel.count > maxVisible) { - notificationModel.remove(notificationModel.count - 1); - } - } - - function dismissNotificationById(id) { - for (var i = 0; i < notificationModel.count; i++) { - if (notificationModel.get(i).id === id) { - dismissNotificationByIndex(i); - break; + + function addNotification(notification): void { + console.log("[NotificationPopup] Adding notification to popup manager"); + // Add notification to all monitor popups + for (let i = 0; i < children.length; i++) { + let child = children[i]; + if (child.addNotification) { + child.addNotification(notification); } } } - function dismissNotificationByIndex(index) { - if (index >= 0 && index < notificationModel.count) { - var notif = notificationModel.get(index); - if (!notif.dismissed) { - notificationModel.set(index, { - id: notif.id, - appName: notif.appName, - summary: notif.summary, - body: notif.body, - rawNotification: notif.rawNotification, - appeared: notif.appeared, - dismissed: true - }); + // Create a notification popup for each monitor + Repeater { + model: notificationManager.monitors + delegate: Item { + id: delegateItem + + // Make addNotification accessible from the Item level + function addNotification(notification) { + if (panelWindow) { + panelWindow.addNotification(notification); + } } - } - } + + PanelWindow { + id: panelWindow + implicitWidth: 350 + implicitHeight: Math.max(notificationColumn.height, 0) + color: "transparent" + visible: notificationManager.notificationsVisible && notificationModel.count > 0 && shouldShowOnThisMonitor + screen: modelData + focusable: false - Column { - id: notificationColumn - anchors.right: parent.right - spacing: window.spacing - width: parent.width - clip: false + property bool barVisible: true + property bool notificationsVisible: notificationManager.notificationsVisible + + // Check if this monitor should show notifications - make it reactive to settings changes + property bool shouldShowOnThisMonitor: { + let notificationMonitors = Settings.settings.notificationMonitors || []; + let currentScreenName = modelData ? modelData.name : ""; + // Show notifications on all monitors if notificationMonitors is empty or contains "*" + let shouldShow = notificationMonitors.length === 0 || + notificationMonitors.includes("*") || + notificationMonitors.includes(currentScreenName); + console.log("[NotificationPopup] Monitor", currentScreenName, "should show:", shouldShow, "monitors:", JSON.stringify(notificationMonitors)); + return shouldShow; + } - Repeater { - id: notificationRepeater - model: notificationModel + // Watch for changes in notification monitors setting + Connections { + target: Settings.settings + function onNotificationMonitorsChanged() { + // Settings changed, visibility will update automatically + } + } - delegate: Rectangle { - id: notificationDelegate - width: parent.width - color: Theme.backgroundPrimary - radius: 20 - border.color: model.urgency == 2 ? Theme.warning : Theme.outline - border.width: 1 + anchors.top: true + anchors.right: true + margins.top: 6 + margins.right: 6 - property bool appeared: model.appeared - property bool dismissed: model.dismissed - property var rawNotification: model.rawNotification + ListModel { + id: notificationModel + } - x: appeared ? 0 : width - opacity: dismissed ? 0 : 1 - height: dismissed ? 0 : contentRow.height + 20 + property int maxVisible: 5 + property int spacing: 5 - Row { - id: contentRow - anchors.centerIn: parent - spacing: 10 - width: parent.width - 20 + function addNotification(notification) { + console.log("[NotificationPopup] Adding notification to monitor popup:", notification.appName); + notificationModel.insert(0, { + id: notification.id, + appName: notification.appName || "Notification", + summary: notification.summary || "", + body: notification.body || "", + urgency: notification.urgency || 0, + rawNotification: notification, + appeared: false, + dismissed: false + }); - // Circular Icon container with border - Rectangle { - id: iconBackground - width: 36 - height: 36 - radius: width / 2 // Circular - color: Theme.accentPrimary - anchors.verticalCenter: parent.verticalCenter - border.color: Qt.darker(Theme.accentPrimary, 1.2) - border.width: 1.5 + while (notificationModel.count > maxVisible) { + notificationModel.remove(notificationModel.count - 1); + } + } - // Get all possible icon sources from notification - property var iconSources: [rawNotification?.image || "", rawNotification?.appIcon || "", rawNotification?.icon || ""] - - // Try to load notification icon - IconImage { - id: iconImage - anchors.fill: parent - anchors.margins: 4 - asynchronous: true - backer.fillMode: Image.PreserveAspectFit - source: { - for (var i = 0; i < iconBackground.iconSources.length; i++) { - var icon = iconBackground.iconSources[i]; - if (!icon) - continue; - - if (icon.includes("?path=")) { - const [name, path] = icon.split("?path="); - const fileName = name.substring(name.lastIndexOf("/") + 1); - return `file://${path}/${fileName}`; - } - - if (icon.startsWith('/')) { - return "file://" + icon; - } - - return icon; - } - return ""; - } - visible: status === Image.Ready && source.toString() !== "" + function dismissNotificationById(id) { + for (var i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).id === id) { + dismissNotificationByIndex(i); + break; } + } + } - // Fallback to first letter of app name - Text { - anchors.centerIn: parent - visible: !iconImage.visible - text: model.appName ? model.appName.charAt(0).toUpperCase() : "?" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeBody - font.bold: true + function dismissNotificationByIndex(index) { + if (index >= 0 && index < notificationModel.count) { + var notif = notificationModel.get(index); + if (!notif.dismissed) { + notificationModel.set(index, { + id: notif.id, + appName: notif.appName, + summary: notif.summary, + body: notif.body, + rawNotification: notif.rawNotification, + appeared: notif.appeared, + dismissed: true + }); + } + } + } + + Column { + id: notificationColumn + anchors.right: parent.right + spacing: panelWindow.spacing + width: parent.width + clip: false + + Repeater { + id: notificationRepeater + model: notificationModel + + delegate: Rectangle { + id: notificationDelegate + width: parent.width color: Theme.backgroundPrimary - } - } + radius: 20 + border.color: model.urgency == 2 ? Theme.warning : Theme.outline + border.width: 1 - Column { - width: contentRow.width - iconBackground.width - 10 - spacing: 5 + property bool appeared: model.appeared + property bool dismissed: model.dismissed + property var rawNotification: model.rawNotification - Text { - text: model.appName - width: parent.width - color: Theme.textPrimary - font.family: Theme.fontFamily - font.bold: true - font.pixelSize: Theme.fontSizeSmall - elide: Text.ElideRight - } - Text { - text: model.summary - width: parent.width - color: "#eeeeee" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall - wrapMode: Text.Wrap - visible: text !== "" - } - Text { - text: model.body - width: parent.width - color: "#cccccc" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeCaption - wrapMode: Text.Wrap - visible: text !== "" - } - } - } + x: appeared ? 0 : width + opacity: dismissed ? 0 : 1 + height: dismissed ? 0 : Math.max(contentRow.height, 60) + 20 - Timer { - interval: 4000 - running: !dismissed - repeat: false - onTriggered: { - dismissAnimation.start(); - if (rawNotification) - rawNotification.expire(); - } - } + Row { + id: contentRow + anchors.centerIn: parent + spacing: 10 + width: parent.width - 20 - MouseArea { - anchors.fill: parent - onClicked: { - dismissAnimation.start(); - if (rawNotification) - rawNotification.dismiss(); - } - } + // Circular Icon container with border + Rectangle { + id: iconBackground + width: 36 + height: 36 + radius: width / 2 + color: Theme.accentPrimary + anchors.verticalCenter: parent.verticalCenter + border.color: Qt.darker(Theme.accentPrimary, 1.2) + border.width: 1.5 - ParallelAnimation { - id: dismissAnimation - NumberAnimation { - target: notificationDelegate - property: "opacity" - to: 0 - duration: 150 - } - NumberAnimation { - target: notificationDelegate - property: "height" - to: 0 - duration: 150 - } - NumberAnimation { - target: notificationDelegate - property: "x" - to: width - duration: 150 - easing.type: Easing.InQuad - } - onFinished: { - for (let i = 0; i < notificationModel.count; i++) { - if (notificationModel.get(i).id === notificationDelegate.id) { - notificationModel.remove(i); - break; + // Priority order for notification icons: image > appIcon > icon + property var iconSources: [rawNotification?.image || "", rawNotification?.appIcon || "", rawNotification?.icon || ""] + + // Load notification icon with fallback handling + IconImage { + id: iconImage + anchors.fill: parent + anchors.margins: 4 + asynchronous: true + 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; + } + + return icon; + } + return ""; + } + visible: status === Image.Ready && source.toString() !== "" + } + + // Fallback: show first letter of app name when no icon available + Text { + anchors.centerIn: parent + visible: !iconImage.visible + text: model.appName ? model.appName.charAt(0).toUpperCase() : "?" + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeBody + font.bold: true + color: Theme.backgroundPrimary + } + } + + Column { + width: contentRow.width - iconBackground.width - 10 + spacing: 5 + + Text { + text: model.appName + width: parent.width + color: Theme.textPrimary + font.family: Theme.fontFamily + font.bold: true + font.pixelSize: Theme.fontSizeSmall + elide: Text.ElideRight + } + Text { + text: model.summary + width: parent.width + color: "#eeeeee" + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.Wrap + visible: text !== "" + } + Text { + text: model.body + width: parent.width + color: "#cccccc" + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeCaption + wrapMode: Text.Wrap + visible: text !== "" + } + } + } + + Timer { + interval: 4000 + running: !dismissed + repeat: false + onTriggered: { + dismissAnimation.start(); + if (rawNotification) + rawNotification.expire(); + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + dismissAnimation.start(); + if (rawNotification) + rawNotification.dismiss(); + } + } + + ParallelAnimation { + id: dismissAnimation + NumberAnimation { + target: notificationDelegate + property: "opacity" + to: 0 + duration: 150 + } + NumberAnimation { + target: notificationDelegate + property: "height" + to: 0 + duration: 150 + } + NumberAnimation { + target: notificationDelegate + property: "x" + to: width + duration: 150 + easing.type: Easing.InQuad + } + onFinished: { + for (let i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).id === notificationDelegate.id) { + notificationModel.remove(i); + break; + } + } + } + } + + ParallelAnimation { + id: appearAnimation + NumberAnimation { + target: notificationDelegate + property: "opacity" + to: 1 + duration: 150 + } + NumberAnimation { + target: notificationDelegate + property: "height" + to: Math.max(contentRow.height, 60) + 20 + duration: 150 + } + NumberAnimation { + target: notificationDelegate + property: "x" + to: 0 + duration: 150 + easing.type: Easing.OutQuad + } + } + + Timer { + id: appearTimer + interval: 10 + repeat: false + onTriggered: { + appearAnimation.start(); + } + } + + Component.onCompleted: { + if (!appeared) { + opacity = 0; + height = 0; + x = width; + // Small delay to ensure contentRow has proper height + appearTimer.start(); + for (let i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).id === notificationDelegate.id) { + var oldItem = notificationModel.get(i); + notificationModel.set(i, { + id: oldItem.id, + appName: oldItem.appName, + summary: oldItem.summary, + body: oldItem.body, + rawNotification: oldItem.rawNotification, + appeared: true, + read: oldItem.read, + dismissed: oldItem.dismissed + }); + break; + } + } + } } } } } - ParallelAnimation { - id: appearAnimation - NumberAnimation { - target: notificationDelegate - property: "opacity" - to: 1 - duration: 150 - } - NumberAnimation { - target: notificationDelegate - property: "height" - to: contentRow.height + 20 - duration: 150 - } - NumberAnimation { - target: notificationDelegate - property: "x" - to: 0 - duration: 150 - easing.type: Easing.OutQuad - } - } - - Component.onCompleted: { - if (!appeared) { - opacity = 0; - height = 0; - x = width; - appearAnimation.start(); - for (let i = 0; i < notificationModel.count; i++) { - if (notificationModel.get(i).id === notificationDelegate.id) { - var oldItem = notificationModel.get(i); - notificationModel.set(i, { - id: oldItem.id, - appName: oldItem.appName, - summary: oldItem.summary, - body: oldItem.body, - rawNotification: oldItem.rawNotification, - appeared: true, - read: oldItem.read, - dismissed: oldItem.dismissed - }); - break; - } + Connections { + target: Quickshell + function onScreensChanged() { + if (panelWindow.screen) { + x = panelWindow.screen.width - panelWindow.width - 20; } } } } } } - - Connections { - target: Quickshell - function onScreensChanged() { - if (window.screen) { - x = window.screen.width - width - 20; - } - } - } } diff --git a/Widgets/Overview.qml b/Widgets/Overview.qml index 8370ce06..9702ebaf 100644 --- a/Widgets/Overview.qml +++ b/Widgets/Overview.qml @@ -34,15 +34,15 @@ ShellRoot { cache: true smooth: true mipmap: false - visible: wallpaperSource !== "" // Show the original for FastBlur input + 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 diff --git a/Widgets/SettingsWindow/SettingsWindow.qml b/Widgets/SettingsWindow/SettingsWindow.qml new file mode 100644 index 00000000..d57e5e27 --- /dev/null +++ b/Widgets/SettingsWindow/SettingsWindow.qml @@ -0,0 +1,500 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Components +import qs.Settings +import qs.Widgets.SettingsWindow.Tabs +import qs.Widgets.SettingsWindow.Tabs.Components + +PanelWithOverlay { + id: panelMain + + property int activeTabIndex: 0 + + // Function to show wallpaper selector + function showWallpaperSelector() { + if (wallpaperSelector) + wallpaperSelector.show(); + + } + + // Function to show settings window + function showSettings() { + show(); + } + + // Function to load component for a specific tab + function loadComponentForTab(tabIndex) { + const componentMap = { + "0": generalSettings, + "1": barSettings, + "2": timeWeatherSettings, + "3": recordingSettings, + "4": networkSettings, + "5": displaySettings, + "6": wallpaperSettings, + "7": miscSettings, + "8": aboutSettings + }; + const tabNames = ["General", "Bar", "Time & Weather", "Screen Recorder", "Network", "Display", "Wallpaper", "Misc", "About"]; + if (componentMap[tabIndex]) { + settingsLoader.sourceComponent = componentMap[tabIndex]; + if (tabName) + tabName.text = tabNames[tabIndex]; + + } + } + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + // Handle activeTabIndex changes + onActiveTabIndexChanged: { + if (activeTabIndex >= 0 && activeTabIndex <= 8) + loadComponentForTab(activeTabIndex); + + } + // Add safety checks for component loading + Component.onCompleted: { + // Ensure we start with a valid tab + if (activeTabIndex < 0 || activeTabIndex > 8) + activeTabIndex = 0; + + } + // Cleanup when window is hidden + onVisibleChanged: { + if (!visible) { + // Reset to default tab when hiding to prevent state issues + activeTabIndex = 0; + if (tabName) + tabName.text = "General"; + + } + } + + Component { + id: generalSettings + + General { + } + + } + + Component { + id: barSettings + + Bar { + } + + } + + Component { + id: timeWeatherSettings + + TimeWeather { + } + + } + + Component { + id: recordingSettings + + ScreenRecorder { + } + + } + + Component { + id: networkSettings + + Network { + } + + } + + Component { + id: miscSettings + + Misc { + } + + } + + Component { + id: aboutSettings + + About { + } + + } + + Component { + id: displaySettings + + Display { + } + + } + + Component { + id: wallpaperSettings + + Wallpaper { + } + + } + + Rectangle { + id: settingsWindowRect + + implicitWidth: Quickshell.screens.length > 0 ? Math.min(Quickshell.screens[0].width * 2 / 3, 1200) * Theme.scale(Screen) : 600 * Theme.scale(Screen) + implicitHeight: Quickshell.screens.length > 0 ? Math.min(Quickshell.screens[0].height * 2 / 3, 800) * Theme.scale(Screen) : 400 * Theme.scale(Screen) + visible: parent.visible + color: "transparent" + // Center the settings window on screen + anchors.centerIn: parent + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + // Background rectangle + Rectangle { + id: background + + color: Theme.backgroundPrimary + anchors.fill: parent + radius: 20 * Theme.scale(Screen) + border.color: Theme.outline + border.width: 1 * Theme.scale(Screen) + + MultiEffect { + source: background + anchors.fill: background + shadowEnabled: true + shadowColor: Theme.shadow + shadowOpacity: 0.3 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 2 + shadowBlur: 12 + } + + } + + Rectangle { + id: settings + clip: true + + color: Theme.backgroundPrimary + topRightRadius: 20 * Theme.scale(Screen) + bottomRightRadius: 20 * Theme.scale(Screen) + + anchors { + left: tabs.right + top: parent.top + bottom: parent.bottom + right: parent.right + margins: 12 + } + + Rectangle { + id: headerArea + + height: 48 * Theme.scale(Screen) + color: "transparent" + + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: 16 + } + + RowLayout { + anchors.fill: parent + spacing: 12 * Theme.scale(Screen) + + Text { + id: tabName + + text: wallpaperSelector.visible ? "Select Wallpaper" : (activeTabIndex === 0 ? "General" : activeTabIndex === 1 ? "Bar" : activeTabIndex === 2 ? "Time & Weather" : activeTabIndex === 3 ? "Screen Recorder" : activeTabIndex === 4 ? "Network" : activeTabIndex === 5 ? "Display" : activeTabIndex === 6 ? "Wallpaper" : activeTabIndex === 7 ? "Misc" : activeTabIndex === 8 ? "About" : "General") + font.pixelSize: 18 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + Layout.fillWidth: true + } + + // Wallpaper Selection Button (only visible on Wallpaper tab) + Rectangle { + width: 160 * Theme.scale(Screen) + height: 32 * Theme.scale(Screen) + radius: 16 * Theme.scale(Screen) + color: wallpaperButtonArea.containsMouse ? Theme.accentPrimary : "transparent" + border.color: Theme.accentPrimary + border.width: 1 * Theme.scale(Screen) + visible: activeTabIndex === 6 // Wallpaper tab index + + Row { + anchors.centerIn: parent + spacing: 6 * Theme.scale(Screen) + + Text { + text: "image" + font.family: wallpaperButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" + font.pixelSize: 16 * Theme.scale(Screen) + color: wallpaperButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: "Select Wallpaper" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: wallpaperButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary + anchors.verticalCenter: parent.verticalCenter + } + + } + + MouseArea { + id: wallpaperButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + // Show the wallpaper selector + wallpaperSelector.show(); + } + } + + } + + Rectangle { + width: 32 * Theme.scale(Screen) + height: 32 * Theme.scale(Screen) + radius: 16 * Theme.scale(Screen) + color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent" + border.color: Theme.accentPrimary + border.width: 1 * Theme.scale(Screen) + + Text { + anchors.centerIn: parent + text: "close" + font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" + font.pixelSize: 18 * Theme.scale(Screen) + color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary + } + + MouseArea { + id: closeButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + // If wallpaper selector is open, close it instead of the settings window + if (wallpaperSelector.visible) { + wallpaperSelector.hide(); + } else { + panelMain.dismiss(); + } + } + } + + } + + } + + } + + Rectangle { + height: 1 * Theme.scale(Screen) + color: Theme.outline + opacity: 0.3 + + anchors { + top: headerArea.bottom + left: parent.left + right: parent.right + margins: 16 + } + + } + + Item { + id: settingsContainer + + anchors { + top: headerArea.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + topMargin: 32 + } + + // Simplified single loader approach + Loader { + id: settingsLoader + + anchors.fill: parent + sourceComponent: generalSettings + active: true + } + + // Wallpaper Selector Component - positioned as overlay + WallpaperSelector { + id: wallpaperSelector + anchors.fill: parent + } + + } + + } + + Rectangle { + id: tabs + + color: Theme.surface + width: parent.width * 0.25 + height: settingsWindowRect.height + topLeftRadius: 20 * Theme.scale(Screen) + bottomLeftRadius: 20 * Theme.scale(Screen) + border.color: Theme.outline + border.width: 1 * Theme.scale(Screen) + + Column { + width: parent.width + spacing: 0 * Theme.scale(Screen) + topPadding: 8 * Theme.scale(Screen) + bottomPadding: 8 * Theme.scale(Screen) + + Repeater { + id: repeater + + model: [{ + "icon": "tune", + "text": "General" + }, { + "icon": "space_dashboard", + "text": "Bar" + }, { + "icon": "schedule", + "text": "Time & Weather" + }, { + "icon": "photo_camera", + "text": "Screen Recorder" + }, { + "icon": "wifi", + "text": "Network" + }, { + "icon": "monitor", + "text": "Display" + }, { + "icon": "wallpaper", + "text": "Wallpaper" + }, { + "icon": "settings_suggest", + "text": "Misc" + }, { + "icon": "info", + "text": "About" + }] + + delegate: Rectangle { + width: tabs.width + height: 48 * Theme.scale(Screen) + color: "transparent" + + RowLayout { + anchors.fill: parent + spacing: 8 * Theme.scale(Screen) + + Rectangle { + id: activeIndicator + + Layout.leftMargin: 8 * Theme.scale(Screen) + Layout.preferredWidth: 3 * Theme.scale(Screen) + Layout.preferredHeight: 24 * Theme.scale(Screen) + Layout.alignment: Qt.AlignVCenter + radius: 2 * Theme.scale(Screen) + color: Theme.accentPrimary + opacity: index === activeTabIndex ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + + } + + } + + Label { + id: icon + + text: modelData.icon + font.family: "Material Symbols Outlined" + font.pixelSize: 24 * Theme.scale(Screen) + color: index === activeTabIndex ? Theme.accentPrimary : Theme.textPrimary + opacity: index === activeTabIndex ? 1 : 0.8 + Layout.leftMargin: 20 * Theme.scale(Screen) + Layout.preferredWidth: 24 * Theme.scale(Screen) + Layout.preferredHeight: 24 * Theme.scale(Screen) + Layout.alignment: Qt.AlignVCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } + } + + Label { + id: label + + text: modelData.text + font.pixelSize: 16 * Theme.scale(Screen) + color: index === activeTabIndex ? Theme.accentPrimary : (tabMouseArea.containsMouse ? Theme.accentPrimary : Theme.textSecondary) + font.weight: index === activeTabIndex ? Font.DemiBold : (tabMouseArea.containsMouse ? Font.DemiBold : Font.Normal) + Layout.fillWidth: true + Layout.preferredHeight: 24 * Theme.scale(Screen) + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.leftMargin: 4 * Theme.scale(Screen) + Layout.rightMargin: 16 * Theme.scale(Screen) + verticalAlignment: Text.AlignVCenter + } + + } + + MouseArea { + id: tabMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + activeTabIndex = index; + loadComponentForTab(index); + } + } + + Rectangle { + width: parent.width + height: 1 * Theme.scale(Screen) + color: Theme.outline + opacity: 0.6 + visible: index < (repeater.count - 1) + anchors.bottom: parent.bottom + } + + } + + } + + } + + } + + } + + } diff --git a/Widgets/SettingsWindow/Tabs/About.qml b/Widgets/SettingsWindow/Tabs/About.qml new file mode 100644 index 00000000..a1b32442 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/About.qml @@ -0,0 +1,441 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Components +import qs.Settings + +ColumnLayout { + id: root + + property string latestVersion: "Unknown" + property string currentVersion: "Unknown" + property var contributors: [] + property string githubDataPath: Settings.settingsDir + "github_data.json" + + function loadFromFile() { + const now = Date.now(); + const data = githubData; + if (!data.timestamp || (now - data.timestamp > 3.6e+06)) { + 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; + + } + + function fetchFromGitHub() { + versionProcess.running = true; + contributorsProcess.running = true; + } + + function saveData() { + githubData.timestamp = Date.now(); + Qt.callLater(() => { + githubDataFile.writeAdapter(); + }); + } + + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + Process { + id: currentVersionProcess + + command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"] + Component.onCompleted: { + running = true; + } + + 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; + } + } + } + + } + + FileView { + id: githubDataFile + + path: root.githubDataPath + blockLoading: true + printErrors: true + watchChanges: true + 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(); + + } + + JsonAdapter { + id: githubData + + property string version: "Unknown" + property var contributors: [] + property double timestamp: 0 + } + + } + + 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 = []; + } + } + } + + } + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Text { + text: "Noctalia: quiet by design" + font.pixelSize: 24 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + Layout.alignment: Qt.AlignCenter + Layout.bottomMargin: 8 * Theme.scale(Screen) + } + + Text { + text: "It may just be another quickshell setup but it won't get in your way." + font.pixelSize: 14 * Theme.scale(Screen) + color: Theme.textSecondary + Layout.alignment: Qt.AlignCenter + Layout.bottomMargin: 16 * Theme.scale(Screen) + } + + GridLayout { + Layout.alignment: Qt.AlignCenter + columns: 2 + rowSpacing: 4 + columnSpacing: 8 + + Text { + text: "Latest Version:" + font.pixelSize: 16 * Theme.scale(Screen) + color: Theme.textSecondary + Layout.alignment: Qt.AlignRight + } + + Text { + text: root.latestVersion + font.pixelSize: 16 * Theme.scale(Screen) + color: Theme.textPrimary + font.bold: true + } + + Text { + text: "Installed Version:" + font.pixelSize: 16 * Theme.scale(Screen) + color: Theme.textSecondary + Layout.alignment: Qt.AlignRight + } + + Text { + text: root.currentVersion + font.pixelSize: 16 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + color: updateArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary + } + + Text { + id: updateText + + text: "Download latest release" + font.pixelSize: 14 * Theme.scale(Screen) + 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"]); + } + } + + } + + // Separator + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + height: 1 + color: Theme.outline + opacity: 0.3 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 32 + Layout.rightMargin: 32 + Layout.alignment: Qt.AlignCenter + spacing: 16 + + RowLayout { + Layout.alignment: Qt.AlignCenter + spacing: 8 + + Text { + text: "Contributors" + font.pixelSize: 18 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "(" + root.contributors.length + ")" + font.pixelSize: 14 * Theme.scale(Screen) + color: Theme.textSecondary + } + + } + + GridView { + id: contributorsGrid + + Layout.leftMargin: 32 + Layout.rightMargin: 32 + Layout.alignment: Qt.AlignCenter + width: 200 * 3 + height: 300 + cellWidth: 200 + cellHeight: 100 + model: root.contributors + + delegate: Rectangle { + width: contributorsGrid.cellWidth - 8 + height: contributorsGrid.cellHeight - 4 + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textPrimary + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: (modelData.contributions || 0) + " commits" + font.pixelSize: 11 * Theme.scale(Screen) + 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]); + + } + } + + } + + } + + } + + } + + } + +} diff --git a/Widgets/SettingsWindow/Tabs/Bar.qml b/Widgets/SettingsWindow/Tabs/Bar.qml new file mode 100644 index 00000000..f37af0f6 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Bar.qml @@ -0,0 +1,86 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Components +import qs.Settings + +ColumnLayout { + id: root + + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Text { + text: "Elements" + font.pixelSize: 18 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 16 * Theme.scale(Screen) + } + + ToggleOption { + label: "Show Active Window" + description: "Display the title of the currently focused window below the bar" + value: Settings.settings.showActiveWindow + onToggled: function() { + Settings.settings.showActiveWindow = !Settings.settings.showActiveWindow; + } + } + + ToggleOption { + label: "Show Active Window Icon" + description: "Display the icon of the currently focused window" + value: Settings.settings.showActiveWindowIcon + onToggled: function() { + Settings.settings.showActiveWindowIcon = !Settings.settings.showActiveWindowIcon; + } + } + + ToggleOption { + label: "Show System Info" + description: "Display system information (CPU, RAM, Temperature)" + value: Settings.settings.showSystemInfoInBar + onToggled: function() { + Settings.settings.showSystemInfoInBar = !Settings.settings.showSystemInfoInBar; + } + } + + ToggleOption { + label: "Show Taskbar" + description: "Display a taskbar showing currently open windows" + value: Settings.settings.showTaskbar + onToggled: function() { + Settings.settings.showTaskbar = !Settings.settings.showTaskbar; + } + } + + ToggleOption { + label: "Show Media" + description: "Display media controls and information" + value: Settings.settings.showMediaInBar + onToggled: function() { + Settings.settings.showMediaInBar = !Settings.settings.showMediaInBar; + } + } + + } + + } + +} diff --git a/Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml b/Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml new file mode 100644 index 00000000..5ef776d3 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml @@ -0,0 +1,97 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Components +import qs.Settings + + +Rectangle { + id: root + width: 64 * Theme.scale(Screen) + height: 32 * Theme.scale(Screen) + radius: 16 * Theme.scale(Screen) + color: Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 * Theme.scale(Screen) + + property bool useFahrenheit: Settings.settings.useFahrenheit + + Rectangle { + id: slider + width: parent.width / 2 - 4 * Theme.scale(Screen) + height: parent.height - 4 * Theme.scale(Screen) + radius: 14 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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/Components/WallpaperSelector.qml b/Widgets/SettingsWindow/Tabs/Components/WallpaperSelector.qml new file mode 100644 index 00000000..5d2722d1 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Components/WallpaperSelector.qml @@ -0,0 +1,171 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import qs.Components +import qs.Services +import qs.Settings + +Rectangle { + id: wallpaperOverlay + focus: true + + // Function to show the overlay and load wallpapers + function show() { + // Ensure wallpapers are loaded + WallpaperManager.loadWallpapers(); + wallpaperOverlay.visible = true; + wallpaperOverlay.forceActiveFocus(); + } + + // Function to hide the overlay + function hide() { + wallpaperOverlay.visible = false; + } + + color: Theme.backgroundPrimary + visible: false + z: 1000 + + // Handle escape key to close + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Escape) { + wallpaperOverlay.hide(); + event.accepted = true; + } + } + + // Click outside to close + MouseArea { + anchors.fill: parent + onClicked: { + wallpaperOverlay.hide(); + } + } + + // Content area that stops event propagation + MouseArea { + // Stop event propagation + + anchors.fill: parent + anchors.margins: 24 + onClicked: { + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + + + // Wallpaper Grid + Item { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + ScrollView { + anchors.fill: parent + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + GridView { + id: wallpaperGrid + + anchors.fill: parent + cellWidth: Math.max(120 * Theme.scale(Screen), (parent.width / 3) - 12 * Theme.scale(Screen)) + cellHeight: cellWidth * 0.6 + model: WallpaperManager.wallpaperList + cacheBuffer: 64 + leftMargin: 8 + rightMargin: 8 + topMargin: 8 + bottomMargin: 8 + + delegate: Item { + width: wallpaperGrid.cellWidth - 8 * Theme.scale(Screen) + height: wallpaperGrid.cellHeight - 8 * Theme.scale(Screen) + + Rectangle { + id: wallpaperItem + + anchors.fill: parent + anchors.margins: 3 + color: Theme.surface + radius: 12 * Theme.scale(Screen) + border.color: Settings.settings.currentWallpaper === modelData ? Theme.accentPrimary : Theme.outline + border.width: 2 * Theme.scale(Screen) + + Image { + id: wallpaperImage + + anchors.fill: parent + anchors.margins: 2 + source: modelData + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + smooth: true + mipmap: true + sourceSize.width: Math.min(width, 480 * Theme.scale(Screen)) + sourceSize.height: Math.min(height, 270 * Theme.scale(Screen)) + opacity: (wallpaperImage.status == Image.Ready) ? 1 : 0 + // Apply circular mask for rounded corners + layer.enabled: true + + Behavior on opacity { + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } + + } + + layer.effect: MultiEffect { + maskEnabled: true + maskSource: mask + } + + } + + Item { + id: mask + + anchors.fill: wallpaperImage + layer.enabled: true + visible: false + + Rectangle { + width: wallpaperImage.width + height: wallpaperImage.height + radius: 12 * Theme.scale(Screen) + } + + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + WallpaperManager.changeWallpaper(modelData); + wallpaperOverlay.hide(); + } + } + + } + + } + + } + + } + + } + + } + + } + +} diff --git a/Widgets/SettingsWindow/Tabs/Display.qml b/Widgets/SettingsWindow/Tabs/Display.qml new file mode 100644 index 00000000..85e2623d --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Display.qml @@ -0,0 +1,373 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Components +import qs.Settings + +ColumnLayout { + id: root + + // Get list of available monitors/screens + property var monitors: Quickshell.screens || [] + + // Sorted monitors by name + property var sortedMonitors: { + let sorted = [...monitors]; + sorted.sort((a, b) => { + let nameA = a.name || "Unknown"; + let nameB = b.name || "Unknown"; + return nameA.localeCompare(nameB); + }); + return sorted; + } + + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Text { + text: "Monitor Selection" + font.pixelSize: 18 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 16 * Theme.scale(Screen) + } + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.bottomMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Bar Monitors" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Select which monitors to display the top panel/bar on" + font.pixelSize: 12 * Theme.scale(Screen) + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + } + + + Flow { + Layout.fillWidth: true + spacing: 8 + + Repeater { + model: root.sortedMonitors + 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 * Theme.scale(Screen) + color: barCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary + visible: barCheckbox.isChecked + } + + Text { + text: modelData.name || "Unknown" + font.pixelSize: 12 * Theme.scale(Screen) + 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 + Layout.bottomMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Dock Monitors" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Select which monitors to display the application dock on" + font.pixelSize: 12 * Theme.scale(Screen) + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + } + + + Flow { + Layout.fillWidth: true + spacing: 8 + + Repeater { + model: root.sortedMonitors + 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 * Theme.scale(Screen) + color: dockCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary + visible: dockCheckbox.isChecked + } + + Text { + text: modelData.name || "Unknown" + font.pixelSize: 12 * Theme.scale(Screen) + 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 + Layout.bottomMargin: 8 + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Notification Monitors" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Select which monitors to display system notifications on" + font.pixelSize: 12 * Theme.scale(Screen) + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + } + + + Flow { + Layout.fillWidth: true + spacing: 8 + + Repeater { + model: root.sortedMonitors + 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 * Theme.scale(Screen) + color: notificationCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary + visible: notificationCheckbox.isChecked + } + + Text { + text: modelData.name || "Unknown" + font.pixelSize: 12 * Theme.scale(Screen) + 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)); + } + } + } + } + } + } + + } + + } + +} \ 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 00000000..56005589 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/General.qml @@ -0,0 +1,166 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Components +import qs.Settings + +ColumnLayout { + id: root + + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Text { + text: "Profile" + font.pixelSize: 18 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 16 * Theme.scale(Screen) + } + + Text { + text: "Profile Image" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 4 * Theme.scale(Screen) + } + + Text { + text: "Your profile picture displayed in various places throughout the shell" + font.pixelSize: 12 * Theme.scale(Screen) + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + Layout.bottomMargin: 4 + } + + RowLayout { + spacing: 8 * Theme.scale(Screen) + Layout.fillWidth: true + + Rectangle { + width: 48 * Theme.scale(Screen) + height: 48 * Theme.scale(Screen) + radius: 24 * Theme.scale(Screen) + + Rectangle { + anchors.fill: parent + color: "transparent" + radius: 24 * Theme.scale(Screen) + border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 2 * Theme.scale(Screen) + z: 2 + } + + Avatar { + } + + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 * Theme.scale(Screen) + radius: 16 * Theme.scale(Screen) + color: Theme.surfaceVariant + border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 * Theme.scale(Screen) + + TextInput { + id: profileImageInput + + anchors.fill: parent + anchors.leftMargin: 12 * Theme.scale(Screen) + anchors.rightMargin: 12 * Theme.scale(Screen) + anchors.topMargin: 6 * Theme.scale(Screen) + anchors.bottomMargin: 6 * Theme.scale(Screen) + text: Settings.settings.profileImage + font.pixelSize: 13 * Theme.scale(Screen) + 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() + } + + } + + } + + } + + // Separator + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 26 * Theme.scale(Screen) + Layout.bottomMargin: 18 * Theme.scale(Screen) + height: 1 * Theme.scale(Screen) + color: Theme.outline + opacity: 0.3 + } + + Text { + text: "User Interface" + font.pixelSize: 18 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 16 * Theme.scale(Screen) + } + + ToggleOption { + label: "Show Corners" + description: "Display rounded corners on the edge of the screen" + value: Settings.settings.showCorners + onToggled: function() { + Settings.settings.showCorners = !Settings.settings.showCorners; + } + } + + ToggleOption { + label: "Show Dock" + description: "Display a dock at the bottom of the screen for quick access to applications" + value: Settings.settings.showDock + onToggled: function() { + Settings.settings.showDock = !Settings.settings.showDock; + } + } + + ToggleOption { + label: "Dim Desktop" + description: "Dim the desktop when panels or menus are open" + value: Settings.settings.dimPanels + onToggled: function() { + Settings.settings.dimPanels = !Settings.settings.dimPanels; + } + } + + } + + } + +} diff --git a/Widgets/SettingsWindow/Tabs/Misc.qml b/Widgets/SettingsWindow/Tabs/Misc.qml new file mode 100644 index 00000000..4281e255 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Misc.qml @@ -0,0 +1,148 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Components +import qs.Settings + +ColumnLayout { + id: root + + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Text { + text: "Media" + font.pixelSize: 18 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 16 * Theme.scale(Screen) + } + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + Text { + text: "Visualizer Type" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Choose the style of the audio visualizer" + font.pixelSize: 12 * Theme.scale(Screen) + 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) + onActivated: { + Settings.settings.visualizerType = model[index]; + } + + 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: 8 + + 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 + highlighted: visualizerTypeComboBox.highlightedIndex === index + + contentItem: Text { + text: modelData.charAt(0).toUpperCase() + modelData.slice(1) + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + + } + + } + + } + + } + + } + +} diff --git a/Widgets/SettingsWindow/Tabs/Network.qml b/Widgets/SettingsWindow/Tabs/Network.qml new file mode 100644 index 00000000..09ec6cef --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Network.qml @@ -0,0 +1,86 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth +import qs.Components +import qs.Settings + +ColumnLayout { + id: root + + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Text { + text: "Wi-Fi" + font.pixelSize: 18 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 16 * Theme.scale(Screen) + } + + ToggleOption { + label: "Enable Wi-Fi" + description: "Turn Wi-Fi radio on or off" + value: Settings.settings.wifiEnabled + onToggled: function() { + Settings.settings.wifiEnabled = !Settings.settings.wifiEnabled; + Quickshell.execDetached(["nmcli", "radio", "wifi", Settings.settings.wifiEnabled ? "on" : "off"]); + } + } + + // Separator + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + height: 1 + color: Theme.outline + opacity: 0.3 + } + + Text { + text: "Bluetooth" + font.pixelSize: 18 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 16 * Theme.scale(Screen) + } + + ToggleOption { + label: "Enable Bluetooth" + description: "Turn Bluetooth radio on or off" + value: Settings.settings.bluetoothEnabled + onToggled: function() { + if (Bluetooth.defaultAdapter) { + Settings.settings.bluetoothEnabled = !Settings.settings.bluetoothEnabled; + Bluetooth.defaultAdapter.enabled = Settings.settings.bluetoothEnabled; + if (Bluetooth.defaultAdapter.enabled) + Bluetooth.defaultAdapter.discovering = true; + + } + } + } + + } + + } + +} diff --git a/Widgets/SettingsWindow/Tabs/Record.qml b/Widgets/SettingsWindow/Tabs/Record.qml new file mode 100644 index 00000000..b1faa8be --- /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/ScreenRecorder.qml b/Widgets/SettingsWindow/Tabs/ScreenRecorder.qml new file mode 100644 index 00000000..166bf066 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/ScreenRecorder.qml @@ -0,0 +1,812 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Components +import qs.Settings + +ColumnLayout { + id: root + + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + 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 { + // Text { + // text: "Screen Recording" + // font.pixelSize: 18 * Theme.scale(Screen) + // font.bold: true + // color: Theme.textPrimary + // Layout.bottomMargin: 8 + // } + + spacing: 4 + Layout.fillWidth: true + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + Text { + text: "Output Directory" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Directory where screen recordings will be saved" + font.pixelSize: 12 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Target frame rate for screen recordings (default: 60)" + font.pixelSize: 12 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + color: Theme.textPrimary + selectionColor: Theme.accentPrimary + selectedTextColor: Theme.onAccent + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + readOnly: false + selectByMouse: true + 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); + + } + + validator: IntValidator { + bottom: frameRateSpinBox.from + top: frameRateSpinBox.to + } + + } + + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + color: Theme.textPrimary + anchors.centerIn: parent + } + + } + + } + + } + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Audio Source" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Audio source to capture during recording" + font.pixelSize: 12 * Theme.scale(Screen) + 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") + onActivated: { + Settings.settings.recordingAudioSource = model[index]; + } + + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 + highlighted: audioSourceComboBox.highlightedIndex === index + + 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 * Theme.scale(Screen) + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + + } + + } + + } + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Video Quality" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Higher quality results in larger file sizes" + font.pixelSize: 12 * Theme.scale(Screen) + 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") + onActivated: { + Settings.settings.recordingQuality = model[index]; + } + + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 + highlighted: qualityComboBox.highlightedIndex === index + + 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 * Theme.scale(Screen) + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + + } + + } + + } + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Video Codec" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Different codecs offer different compression and compatibility" + font.pixelSize: 12 * Theme.scale(Screen) + 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") + onActivated: { + Settings.settings.recordingCodec = model[index]; + } + + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 + highlighted: codecComboBox.highlightedIndex === index + + contentItem: Text { + text: modelData.toUpperCase() + font.pixelSize: 13 * Theme.scale(Screen) + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + + } + + } + + } + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Audio Codec" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Opus is recommended for best performance and smallest audio size" + font.pixelSize: 12 * Theme.scale(Screen) + 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") + onActivated: { + Settings.settings.audioCodec = model[index]; + } + + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 + highlighted: audioCodecComboBox.highlightedIndex === index + + contentItem: Text { + text: modelData.toUpperCase() + font.pixelSize: 13 * Theme.scale(Screen) + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + + } + + } + + } + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.bottomMargin: 16 + + Text { + text: "Color Range" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Limited is recommended for better compatibility" + font.pixelSize: 12 * Theme.scale(Screen) + 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") + onActivated: { + Settings.settings.colorRange = model[index]; + } + + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 + highlighted: colorRangeComboBox.highlightedIndex === index + + contentItem: Text { + text: modelData.charAt(0).toUpperCase() + modelData.slice(1) + font.pixelSize: 13 * Theme.scale(Screen) + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" + } + + } + + } + + } + + ToggleOption { + label: "Show Cursor" + description: "Record mouse cursor in the video" + value: Settings.settings.showCursor + onToggled: function() { + Settings.settings.showCursor = !Settings.settings.showCursor; + } + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 24 + } + + } + + } + +} diff --git a/Widgets/SettingsWindow/Tabs/TimeWeather.qml b/Widgets/SettingsWindow/Tabs/TimeWeather.qml new file mode 100644 index 00000000..e228df62 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/TimeWeather.qml @@ -0,0 +1,176 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Components +import qs.Settings +import qs.Widgets.SettingsWindow.Tabs.Components + +ColumnLayout { + id: root + + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Text { + text: "Time" + font.pixelSize: 18 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 16 * Theme.scale(Screen) + } + + ToggleOption { + label: "Use 12 Hour Clock" + description: "Display time in 12-hour format (e.g., 2:30 PM) instead of 24-hour format" + value: Settings.settings.use12HourClock + onToggled: function() { + Settings.settings.use12HourClock = !Settings.settings.use12HourClock; + } + } + + ToggleOption { + label: "US Style Date" + description: "Display dates in MM/DD/YYYY format instead of DD/MM/YYYY" + value: Settings.settings.reverseDayMonth + onToggled: function() { + Settings.settings.reverseDayMonth = !Settings.settings.reverseDayMonth; + } + } + + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + height: 1 + color: Theme.outline + opacity: 0.3 + } + + Text { + text: "Weather" + font.pixelSize: 18 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 16 * Theme.scale(Screen) + } + + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.bottomMargin: 8 * Theme.scale(Screen) + + Text { + text: "City" + font.pixelSize: 13 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Your city name for weather information" + font.pixelSize: 12 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + 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 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Choose between Celsius and Fahrenheit" + font.pixelSize: 12 * Theme.scale(Screen) + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + } + + UnitSelector { + } + + } + + } + + } + + } + +} diff --git a/Widgets/SettingsWindow/Tabs/Wallpaper.qml b/Widgets/SettingsWindow/Tabs/Wallpaper.qml new file mode 100644 index 00000000..583f1d16 --- /dev/null +++ b/Widgets/SettingsWindow/Tabs/Wallpaper.qml @@ -0,0 +1,670 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Components +import qs.Services +import qs.Settings + +ColumnLayout { + id: root + + spacing: 0 + anchors.fill: parent + anchors.margins: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + 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: "Wallpaper Settings" + font.pixelSize: 18 + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 8 + } + + // Wallpaper Settings Category + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + // Wallpaper Folder + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + Text { + text: "Wallpaper Folder" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Path to your wallpaper folder" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + radius: 16 + color: Theme.surfaceVariant + border.color: folderInput.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + + TextInput { + id: folderInput + + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + anchors.topMargin: 6 + anchors.bottomMargin: 6 + text: Settings.settings.wallpaperFolder !== undefined ? Settings.settings.wallpaperFolder : "" + font.family: Theme.fontFamily + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: TextInput.AlignVCenter + clip: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhUrlCharactersOnly + onTextChanged: { + Settings.settings.wallpaperFolder = text; + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.IBeamCursor + onClicked: folderInput.forceActiveFocus() + } + + } + + } + + } + + } + + } + + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + height: 1 + color: Theme.outline + opacity: 0.3 + } + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "Automation" + font.pixelSize: 18 + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 8 + } + + // Random Wallpaper + ToggleOption { + label: "Random Wallpaper" + description: "Automatically select random wallpapers from the folder" + value: Settings.settings.randomWallpaper + onToggled: function() { + Settings.settings.randomWallpaper = !Settings.settings.randomWallpaper; + } + } + + // Use Wallpaper Theme + ToggleOption { + label: "Use Wallpaper Theme" + description: "Automatically adjust theme colors based on wallpaper" + value: Settings.settings.useWallpaperTheme + onToggled: function() { + Settings.settings.useWallpaperTheme = !Settings.settings.useWallpaperTheme; + } + } + + // Wallpaper Interval + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Wallpaper Interval" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "How often to change wallpapers automatically (in seconds)" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + + Text { + text: Settings.settings.wallpaperInterval + " seconds" + font.pixelSize: 13 + color: Theme.textPrimary + } + + Item { + Layout.fillWidth: true + } + + } + + 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 + y: intervalSlider.topPadding + intervalSlider.availableHeight / 2 - height / 2 + implicitWidth: 200 + implicitHeight: 4 + width: intervalSlider.availableWidth + height: implicitHeight + radius: 2 + color: Theme.surfaceVariant + + Rectangle { + width: intervalSlider.visualPosition * parent.width + height: parent.height + color: Theme.accentPrimary + radius: 2 + } + + } + + handle: Rectangle { + x: intervalSlider.leftPadding + intervalSlider.visualPosition * (intervalSlider.availableWidth - width) + y: intervalSlider.topPadding + intervalSlider.availableHeight / 2 - height / 2 + implicitWidth: 20 + implicitHeight: 20 + radius: 10 + color: intervalSlider.pressed ? Theme.surfaceVariant : Theme.surface + border.color: Theme.accentPrimary + border.width: 2 + } + + } + + } + + } + + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + height: 1 + color: Theme.outline + opacity: 0.3 + } + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: "SWWW" + font.pixelSize: 18 + font.bold: true + color: Theme.textPrimary + Layout.bottomMargin: 8 + } + + // Use SWWW + ToggleOption { + label: "Use SWWW" + description: "Use SWWW daemon for advanced wallpaper management" + value: Settings.settings.useSWWW + onToggled: function() { + Settings.settings.useSWWW = !Settings.settings.useSWWW; + } + } + + // SWWW Settings (only visible when useSWWW is enabled) + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + visible: Settings.settings.useSWWW + + // Resize Mode + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + Text { + text: "Resize Mode" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "How SWWW should resize wallpapers to fit the screen" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + radius: 16 + color: Theme.surfaceVariant + border.color: resizeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + + ComboBox { + id: resizeComboBox + + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + anchors.topMargin: 6 + anchors.bottomMargin: 6 + model: ["no", "crop", "fit", "stretch"] + currentIndex: model.indexOf(Settings.settings.wallpaperResize) + onActivated: { + Settings.settings.wallpaperResize = model[index]; + } + + background: Rectangle { + color: "transparent" + } + + contentItem: Text { + text: resizeComboBox.displayText + font: resizeComboBox.font + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + } + + popup: Popup { + y: resizeComboBox.height + width: resizeComboBox.width + implicitHeight: contentItem.implicitHeight + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: resizeComboBox.popup.visible ? resizeComboBox.delegateModel : null + currentIndex: resizeComboBox.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator { + } + + } + + background: Rectangle { + color: Theme.surface + border.color: Theme.outline + border.width: 1 + radius: 8 + } + + } + + delegate: ItemDelegate { + width: resizeComboBox.width + highlighted: resizeComboBox.highlightedIndex === index + + contentItem: Text { + text: modelData + color: Theme.textPrimary + font: resizeComboBox.font + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + } + + background: Rectangle { + color: parent.highlighted ? Theme.accentPrimary : "transparent" + } + + } + + } + + } + + } + + // Transition Type + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Transition Type" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Animation type when switching between wallpapers" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + radius: 16 + color: Theme.surfaceVariant + border.color: transitionTypeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + + ComboBox { + id: transitionTypeComboBox + + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + anchors.topMargin: 6 + anchors.bottomMargin: 6 + 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 { + color: "transparent" + } + + contentItem: Text { + text: transitionTypeComboBox.displayText + font: transitionTypeComboBox.font + color: Theme.textPrimary + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + } + + popup: Popup { + y: transitionTypeComboBox.height + width: transitionTypeComboBox.width + implicitHeight: contentItem.implicitHeight + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: transitionTypeComboBox.popup.visible ? transitionTypeComboBox.delegateModel : null + currentIndex: transitionTypeComboBox.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator { + } + + } + + background: Rectangle { + color: Theme.surface + border.color: Theme.outline + border.width: 1 + radius: 8 + } + + } + + delegate: ItemDelegate { + width: transitionTypeComboBox.width + highlighted: transitionTypeComboBox.highlightedIndex === index + + contentItem: Text { + text: modelData + color: Theme.textPrimary + font: transitionTypeComboBox.font + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + } + + background: Rectangle { + color: parent.highlighted ? Theme.accentPrimary : "transparent" + } + + } + + } + + } + + } + + // Transition FPS + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Transition FPS" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Frames per second for transition animations" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + + Text { + text: Settings.settings.transitionFps + " FPS" + font.pixelSize: 13 + color: Theme.textPrimary + } + + Item { + Layout.fillWidth: true + } + + } + + 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 + y: fpsSlider.topPadding + fpsSlider.availableHeight / 2 - height / 2 + implicitWidth: 200 + implicitHeight: 4 + width: fpsSlider.availableWidth + height: implicitHeight + radius: 2 + color: Theme.surfaceVariant + + Rectangle { + width: fpsSlider.visualPosition * parent.width + height: parent.height + color: Theme.accentPrimary + radius: 2 + } + + } + + handle: Rectangle { + x: fpsSlider.leftPadding + fpsSlider.visualPosition * (fpsSlider.availableWidth - width) + y: fpsSlider.topPadding + fpsSlider.availableHeight / 2 - height / 2 + implicitWidth: 20 + implicitHeight: 20 + radius: 10 + color: fpsSlider.pressed ? Theme.surfaceVariant : Theme.surface + border.color: Theme.accentPrimary + border.width: 2 + } + + } + + } + + // Transition Duration + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Transition Duration" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "Duration of transition animations in seconds" + font.pixelSize: 12 + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + + Text { + text: Settings.settings.transitionDuration.toFixed(3) + " seconds" + font.pixelSize: 13 + color: Theme.textPrimary + } + + Item { + Layout.fillWidth: true + } + + } + + Slider { + id: durationSlider + + Layout.fillWidth: true + 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 + y: durationSlider.topPadding + durationSlider.availableHeight / 2 - height / 2 + implicitWidth: 200 + implicitHeight: 4 + width: durationSlider.availableWidth + height: implicitHeight + radius: 2 + color: Theme.surfaceVariant + + Rectangle { + width: durationSlider.visualPosition * parent.width + height: parent.height + color: Theme.accentPrimary + radius: 2 + } + + } + + handle: Rectangle { + x: durationSlider.leftPadding + durationSlider.visualPosition * (durationSlider.availableWidth - width) + y: durationSlider.topPadding + durationSlider.availableHeight / 2 - height / 2 + implicitWidth: 20 + implicitHeight: 20 + radius: 10 + color: durationSlider.pressed ? Theme.surfaceVariant : Theme.surface + border.color: Theme.accentPrimary + border.width: 2 + } + + } + + } + + } + + } + + } + + } + +} diff --git a/Widgets/Sidebar/Panel/BluetoothPanel.qml b/Widgets/SidePanel/BluetoothPanel.qml similarity index 99% rename from Widgets/Sidebar/Panel/BluetoothPanel.qml rename to Widgets/SidePanel/BluetoothPanel.qml index 155f29f4..3837d5cc 100644 --- a/Widgets/Sidebar/Panel/BluetoothPanel.qml +++ b/Widgets/SidePanel/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 @@ -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/Button.qml b/Widgets/SidePanel/Button.qml similarity index 96% rename from Widgets/Sidebar/Button.qml rename to Widgets/SidePanel/Button.qml index d1e642d9..d4b2b517 100644 --- a/Widgets/Sidebar/Button.qml +++ b/Widgets/SidePanel/Button.qml @@ -1,7 +1,6 @@ import QtQuick import Quickshell import qs.Settings -import qs.Widgets.Sidebar.Panel Item { id: buttonRoot @@ -45,7 +44,7 @@ Item { id: iconText text: "dashboard" font.family: isActive ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pixelSize: 16 + font.pixelSize: 16 * Theme.scale(Screen) color: sidebarPopup.visible ? Theme.accentPrimary : Theme.textPrimary anchors.centerIn: parent z: 1 diff --git a/Widgets/Sidebar/Panel/Music.qml b/Widgets/SidePanel/Music.qml similarity index 77% rename from Widgets/Sidebar/Panel/Music.qml rename to Widgets/SidePanel/Music.qml index b1d3948d..95ec4e41 100644 --- a/Widgets/Sidebar/Panel/Music.qml +++ b/Widgets/SidePanel/Music.qml @@ -8,15 +8,15 @@ import qs.Services Rectangle { id: musicCard - width: 360 - height: 250 + width: 360 * Theme.scale(Screen) + height: 250 * Theme.scale(Screen) color: "transparent" Rectangle { id: card anchors.fill: parent color: Theme.surface - radius: 18 + radius: 18 * Theme.scale(Screen) // Show fallback UI if no player is available Item { @@ -26,12 +26,12 @@ Rectangle { ColumnLayout { anchors.centerIn: parent - spacing: 16 + spacing: 16 * Theme.scale(Screen) Text { text: "music_note" font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeHeader + font.pixelSize: Theme.fontSizeHeader * Theme.scale(Screen) color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) Layout.alignment: Qt.AlignHCenter } @@ -40,7 +40,7 @@ Rectangle { text: MusicManager.hasPlayer ? "No controllable player selected" : "No music player detected" color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6) font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) Layout.alignment: Qt.AlignHCenter } } @@ -49,45 +49,45 @@ Rectangle { // Main player UI ColumnLayout { anchors.fill: parent - anchors.margins: 18 - spacing: 12 + anchors.margins: 18 * Theme.scale(Screen) + spacing: 12 * Theme.scale(Screen) visible: !!MusicManager.currentPlayer // Player selector ComboBox { id: playerSelector Layout.fillWidth: true - Layout.preferredHeight: 40 + Layout.preferredHeight: 40 * Theme.scale(Screen) visible: MusicManager.getAvailablePlayers().length > 1 model: MusicManager.getAvailablePlayers() textRole: "identity" currentIndex: MusicManager.selectedPlayerIndex background: Rectangle { - implicitWidth: 120 - implicitHeight: 40 + implicitWidth: 120 * Theme.scale(Screen) + implicitHeight: 40 * Theme.scale(Screen) color: Theme.surfaceVariant border.color: playerSelector.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - radius: 16 + border.width: 1 * Theme.scale(Screen) + radius: 16 * Theme.scale(Screen) } contentItem: Text { - leftPadding: 12 + leftPadding: 12 * Theme.scale(Screen) rightPadding: playerSelector.indicator.width + playerSelector.spacing text: playerSelector.displayText - font.pixelSize: 13 + font.pixelSize: 13 * Theme.scale(Screen) color: Theme.textPrimary verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } indicator: Text { - x: playerSelector.width - width - 12 + x: playerSelector.width - width - 12 * Theme.scale(Screen) y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2 text: "arrow_drop_down" font.family: "Material Symbols Outlined" - font.pixelSize: 24 + font.pixelSize: 24 * Theme.scale(Screen) color: Theme.textPrimary } @@ -95,7 +95,7 @@ Rectangle { y: playerSelector.height width: playerSelector.width implicitHeight: contentItem.implicitHeight - padding: 1 + padding: 1 * Theme.scale(Screen) contentItem: ListView { clip: true @@ -109,8 +109,8 @@ Rectangle { background: Rectangle { color: Theme.surfaceVariant border.color: Theme.outline - border.width: 1 - radius: 16 + border.width: 1 * Theme.scale(Screen) + radius: 16 * Theme.scale(Screen) } } @@ -118,7 +118,7 @@ Rectangle { width: playerSelector.width contentItem: Text { text: modelData.identity - font.pixelSize: 13 + font.pixelSize: 13 * Theme.scale(Screen) color: Theme.textPrimary verticalAlignment: Text.AlignVCenter elide: Text.ElideRight @@ -136,57 +136,57 @@ Rectangle { } } - // Album art and spectrum + // Album art with spectrum visualizer RowLayout { - spacing: 12 + spacing: 12 * Theme.scale(Screen) 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 * Theme.scale(Screen) + height: 96 * Theme.scale(Screen) // 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 * Theme.scale(Screen) // Position just outside 60x60 album art + outerRadius: 48 * Theme.scale(Screen) // Extend bars outward from album art fillColor: Theme.accentPrimary strokeColor: Theme.accentPrimary - strokeWidth: 0 + strokeWidth: 0 * Theme.scale(Screen) z: 0 } // Album art image Rectangle { id: albumArtwork - width: 60 - height: 60 + width: 60 * Theme.scale(Screen) + height: 60 * Theme.scale(Screen) anchors.centerIn: parent - radius: 30 // circle + radius: 30 * Theme.scale(Screen) // circle color: Qt.darker(Theme.surface, 1.1) border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) - border.width: 1 + border.width: 1 * Theme.scale(Screen) Image { id: albumArt anchors.fill: parent - anchors.margins: 2 + anchors.margins: 2 * Theme.scale(Screen) fillMode: Image.PreserveAspectCrop smooth: true mipmap: true cache: false asynchronous: true - sourceSize.width: 60 - sourceSize.height: 60 + sourceSize.width: 60 * Theme.scale(Screen) + sourceSize.height: 60 * Theme.scale(Screen) source: MusicManager.trackArtUrl visible: source.toString() !== "" - // Rounded corners using layer + // Apply circular mask for rounded corners layer.enabled: true layer.effect: MultiEffect { maskEnabled: true @@ -208,12 +208,12 @@ Rectangle { } } - // Fallback icon + // Fallback icon when no album art available Text { anchors.centerIn: parent text: "album" font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody + font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen) color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4) visible: !albumArt.visible } @@ -223,13 +223,13 @@ Rectangle { // Track metadata ColumnLayout { Layout.fillWidth: true - spacing: 4 + spacing: 4 * Theme.scale(Screen) Text { text: MusicManager.trackTitle color: Theme.textPrimary font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) font.bold: true elide: Text.ElideRight wrapMode: Text.Wrap @@ -241,7 +241,7 @@ Rectangle { text: MusicManager.trackArtist color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.8) font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeCaption + font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen) elide: Text.ElideRight Layout.fillWidth: true } @@ -250,7 +250,7 @@ Rectangle { text: MusicManager.trackAlbum color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6) font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeCaption + font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen) elide: Text.ElideRight Layout.fillWidth: true } @@ -261,8 +261,8 @@ Rectangle { Rectangle { id: progressBarBackground width: parent.width - height: 6 - radius: 3 + height: 6 * Theme.scale(Screen) + radius: 3 * Theme.scale(Screen) color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15) Layout.fillWidth: true @@ -290,12 +290,12 @@ Rectangle { // Interactive progress handle Rectangle { id: progressHandle - width: 12 - height: 12 - radius: 6 + width: 12 * Theme.scale(Screen) + height: 12 * Theme.scale(Screen) + radius: 6 * Theme.scale(Screen) color: Theme.accentPrimary border.color: Qt.lighter(Theme.accentPrimary, 1.3) - border.width: 1 + border.width: 1 * Theme.scale(Screen) x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) anchors.verticalCenter: parent.verticalCenter @@ -334,18 +334,18 @@ Rectangle { // Media controls RowLayout { - spacing: 4 + spacing: 4 * Theme.scale(Screen) Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter // Previous button Rectangle { - width: 28 - height: 28 - radius: 14 + width: 28 * Theme.scale(Screen) + height: 28 * Theme.scale(Screen) + radius: 14 * Theme.scale(Screen) color: previousButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1) border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) - border.width: 1 + border.width: 1 * Theme.scale(Screen) MouseArea { id: previousButton @@ -360,19 +360,19 @@ Rectangle { anchors.centerIn: parent text: "skip_previous" font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeCaption + font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen) color: previousButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) } } // Play/Pause button Rectangle { - width: 36 - height: 36 - radius: 18 + width: 36 * Theme.scale(Screen) + height: 36 * Theme.scale(Screen) + radius: 18 * Theme.scale(Screen) color: playButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1) border.color: Theme.accentPrimary - border.width: 2 + border.width: 2 * Theme.scale(Screen) MouseArea { id: playButton @@ -387,19 +387,19 @@ Rectangle { anchors.centerIn: parent text: MusicManager.isPlaying ? "pause" : "play_arrow" font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody + font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen) color: playButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) } } // Next button Rectangle { - width: 28 - height: 28 - radius: 14 + width: 28 * Theme.scale(Screen) + height: 28 * Theme.scale(Screen) + radius: 14 * Theme.scale(Screen) color: nextButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1) border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) - border.width: 1 + border.width: 1 * Theme.scale(Screen) MouseArea { id: nextButton @@ -414,7 +414,7 @@ Rectangle { anchors.centerIn: parent text: "skip_next" font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeCaption + font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen) color: nextButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) } } diff --git a/Widgets/Sidebar/Panel/PanelPopup.qml b/Widgets/SidePanel/PanelPopup.qml similarity index 54% rename from Widgets/Sidebar/Panel/PanelPopup.qml rename to Widgets/SidePanel/PanelPopup.qml index 9ccff1c6..af2cc3a1 100644 --- a/Widgets/Sidebar/Panel/PanelPopup.qml +++ b/Widgets/SidePanel/PanelPopup.qml @@ -3,13 +3,15 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Wayland -import qs.Settings -import qs.Widgets.Sidebar.Config import qs.Components +import qs.Settings +import qs.Widgets.SettingsWindow PanelWithOverlay { id: sidebarPopup + property var shell: null + function showAt() { sidebarPopupRect.showAt(); } @@ -26,18 +28,44 @@ PanelWithOverlay { sidebarPopupRect.hidePopup(); } + // Trigger initial weather loading when component is completed + Component.onCompleted: { + // Load initial weather data after a short delay to ensure all components are ready + Qt.callLater(function() { + if (weather && weather.fetchCityWeather) + weather.fetchCityWeather(); + + }); + } + Rectangle { + // Access the shell's SettingsWindow instead of creating a new one + id: sidebarPopupRect - implicitWidth: 500 - implicitHeight: 800 - visible: parent.visible - color: "transparent" - anchors.top: parent.top - anchors.right: parent.right - // Animation properties property real slideOffset: width property bool isAnimating: false + property int leftPadding: 20 * Theme.scale(Screen) + property int bottomPadding: 20 * Theme.scale(Screen) + // Recording properties + property bool isRecording: false + + Process { + id: checkRecordingProcess + command: ["pgrep", "-f", "gpu-screen-recorder.*portal"] + onExited: function(exitCode, exitStatus) { + var isActuallyRecording = exitCode === 0 + if (isRecording && !isActuallyRecording) { + isRecording = isActuallyRecording + } + } + } + + function checkRecordingStatus() { + if (isRecording) { + checkRecordingProcess.running = true + } + } function showAt() { if (!sidebarPopup.visible) { @@ -48,26 +76,18 @@ PanelWithOverlay { slideAnim.running = true; if (weather) weather.startWeatherFetch(); + if (systemWidget) systemWidget.panelVisible = true; - if (quickAccessWidget) - quickAccessWidget.panelVisible = true; } } function hidePopup() { - if (sidebarPopupRect.settingsModal && sidebarPopupRect.settingsModal.visible) { - sidebarPopupRect.settingsModal.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 (shell && shell.settingsWindow && shell.settingsWindow.visible) + shell.settingsWindow.visible = false; + + + if (sidebarPopup.visible) { slideAnim.from = 0; slideAnim.to = width; @@ -75,81 +95,129 @@ PanelWithOverlay { } } + // Start screen recording using Quickshell.execDetached + function startRecording() { + var currentDate = new Date(); + var hours = String(currentDate.getHours()).padStart(2, '0'); + var minutes = String(currentDate.getMinutes()).padStart(2, '0'); + var day = String(currentDate.getDate()).padStart(2, '0'); + var month = String(currentDate.getMonth() + 1).padStart(2, '0'); + var year = currentDate.getFullYear(); + var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4"; + var videoPath = Settings.settings.videoPath; + if (videoPath && !videoPath.endsWith("/")) + videoPath += "/"; + + var outputPath = videoPath + filename; + 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; + } + + // Stop recording using Quickshell.execDetached + function stopRecording() { + Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]); + // Optionally, force kill after a delay + var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', sidebarPopupRect); + cleanupTimer.triggered.connect(function() { + Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder.*portal' 2>/dev/null || true"]); + cleanupTimer.destroy(); + }); + isRecording = false; + } + + implicitWidth: 500 * Theme.scale(Screen) + implicitHeight: 700 * Theme.scale(Screen) + visible: parent.visible + color: "transparent" + anchors.top: parent.top + anchors.right: parent.right + // Clean up processes on destruction + Component.onDestruction: { + if (isRecording) + stopRecording(); + + } + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + NumberAnimation { id: slideAnim + target: sidebarPopupRect property: "slideOffset" duration: 300 easing.type: Easing.OutCubic - onStopped: { if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) { sidebarPopup.visible = false; - // Stop monitoring and background tasks when hidden if (weather) weather.stopWeatherFetch(); + if (systemWidget) systemWidget.panelVisible = false; - if (quickAccessWidget) - quickAccessWidget.panelVisible = false; } sidebarPopupRect.isAnimating = false; } - onStarted: { sidebarPopupRect.isAnimating = true; } } - property int leftPadding: 20 - property int bottomPadding: 20 - Rectangle { id: mainRectangle + width: sidebarPopupRect.width - sidebarPopupRect.leftPadding height: sidebarPopupRect.height - sidebarPopupRect.bottomPadding anchors.top: sidebarPopupRect.top x: sidebarPopupRect.leftPadding + sidebarPopupRect.slideOffset y: 0 color: Theme.backgroundPrimary - bottomLeftRadius: 20 + bottomLeftRadius: 20 * Theme.scale(Screen) z: 0 Behavior on x { enabled: !sidebarPopupRect.isAnimating + NumberAnimation { duration: 300 easing.type: Easing.OutCubic } + } + } - property alias settingsModal: settingsModal - property alias wifiPanelModal: wifiPanel.panel - property alias bluetoothPanelModal: bluetoothPanel.panel - SettingsModal { + + + // SettingsIcon component + SettingsIcon { id: settingsModal + + onWeatherRefreshRequested: { + if (weather && weather.fetchCityWeather) + weather.fetchCityWeather(); + + } } Item { anchors.fill: mainRectangle x: sidebarPopupRect.slideOffset - - Behavior on x { - enabled: !sidebarPopupRect.isAnimating - NumberAnimation { - duration: 300 - easing.type: Easing.OutCubic - } - } + Keys.onEscapePressed: sidebarPopupRect.hidePopup() ColumnLayout { anchors.fill: parent - anchors.margins: 20 - spacing: 16 + anchors.margins: 20 * Theme.scale(Screen) + spacing: 4 * Theme.scale(Screen) - System { + PowerMenu { id: systemWidget + settingsModal: settingsModal + Layout.alignment: Qt.AlignHCenter z: 3 } @@ -162,7 +230,7 @@ PanelWithOverlay { // Music and System Monitor row RowLayout { - spacing: 12 + spacing: 12 * Theme.scale(Screen) Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter @@ -174,241 +242,147 @@ PanelWithOverlay { id: systemMonitor z: 2 } + } - // Power profile, Wifi and Bluetooth row + // Power profile, Record and Wallpaper row RowLayout { - Layout.alignment: Qt.AlignLeft - Layout.preferredHeight: 80 - spacing: 16 + Layout.alignment: Qt.AlignVCenter + spacing: 10 * Theme.scale(Screen) + Layout.preferredHeight: 80 * Theme.scale(Screen) z: 3 PowerProfile { - Layout.alignment: Qt.AlignLeft - Layout.preferredHeight: 80 + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: 80 * Theme.scale(Screen) } - // Network card containing Wifi and Bluetooth + // Record and Wallpaper card Rectangle { - Layout.preferredHeight: 80 - Layout.preferredWidth: 140 + Layout.preferredHeight: 80 * Theme.scale(Screen) + Layout.preferredWidth: 140 * Theme.scale(Screen) Layout.fillWidth: false color: Theme.surface - radius: 18 + radius: 18 * Theme.scale(Screen) Row { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter - spacing: 20 + spacing: 20 * Theme.scale(Screen) - // Wifi button + // Record button Rectangle { - id: wifiButton - width: 36 - height: 36 - radius: 18 + id: recordButton + + width: 36 * Theme.scale(Screen) + height: 36 * Theme.scale(Screen) + radius: 18 * Theme.scale(Screen) border.color: Theme.accentPrimary - border.width: 1 - color: wifiButtonArea.containsMouse ? Theme.accentPrimary : "transparent" + border.width: 1 * Theme.scale(Screen) + color: sidebarPopupRect.isRecording ? Theme.accentPrimary : + (recordButtonArea.containsMouse ? Theme.accentPrimary : "transparent") Text { anchors.centerIn: parent - text: "wifi" + text: "photo_camera" font.family: "Material Symbols Outlined" - font.pixelSize: 22 - color: wifiButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary + font.pixelSize: 22 * Theme.scale(Screen) + color: sidebarPopupRect.isRecording || recordButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } MouseArea { - id: wifiButtonArea + id: recordButtonArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: wifiPanel.showAt() + onClicked: { + if (sidebarPopupRect.isRecording) { + sidebarPopupRect.stopRecording(); + sidebarPopup.dismiss(); + } else { + sidebarPopupRect.startRecording(); + sidebarPopup.dismiss(); + } + } } StyledTooltip { - text: "Wifi" - targetItem: wifiButtonArea - tooltipVisible: wifiButtonArea.containsMouse + text: sidebarPopupRect.isRecording ? "Stop Recording" : "Start Recording" + targetItem: recordButtonArea + tooltipVisible: recordButtonArea.containsMouse } + } - // Bluetooth button + // Wallpaper button Rectangle { - id: bluetoothButton - width: 36 - height: 36 - radius: 18 + id: wallpaperButton + + width: 36 * Theme.scale(Screen) + height: 36 * Theme.scale(Screen) + radius: 18 * Theme.scale(Screen) border.color: Theme.accentPrimary - border.width: 1 - color: bluetoothButtonArea.containsMouse ? Theme.accentPrimary : "transparent" + border.width: 1 * Theme.scale(Screen) + color: wallpaperButtonArea.containsMouse ? Theme.accentPrimary : "transparent" Text { anchors.centerIn: parent - text: "bluetooth" + text: "image" font.family: "Material Symbols Outlined" - font.pixelSize: 22 - color: bluetoothButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary + font.pixelSize: 22 * Theme.scale(Screen) + color: wallpaperButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } MouseArea { - id: bluetoothButtonArea + id: wallpaperButtonArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: bluetoothPanel.showAt() + onClicked: { + if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) { + settingsModal.openSettings(6); + sidebarPopup.dismiss(); + } + } } StyledTooltip { - text: "Bluetooth" - targetItem: bluetoothButtonArea - tooltipVisible: bluetoothButtonArea.containsMouse + text: "Wallpaper" + targetItem: wallpaperButtonArea + tooltipVisible: wallpaperButtonArea.containsMouse } + } + } + } + } - // Hidden panel components for modal functionality - WifiPanel { - id: wifiPanel - visible: false - } - BluetoothPanel { - id: bluetoothPanel - visible: false - } - Item { - Layout.fillHeight: true - } - // QuickAccess widget - QuickAccess { - id: quickAccessWidget - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: -16 - z: 2 - isRecording: sidebarPopupRect.isRecording - - onRecordingRequested: { - sidebarPopupRect.startRecording(); - } - - onStopRecordingRequested: { - sidebarPopupRect.stopRecording(); - } - - onRecordingStateMismatch: function (actualState) { - isRecording = actualState; - quickAccessWidget.isRecording = actualState; - } - - onSettingsRequested: { - settingsModal.visible = true; - } - onWallpaperRequested: { - wallpaperPanel.visible = true; - } - } } - Keys.onEscapePressed: sidebarPopupRect.hidePopup() - } - // Recording properties - property bool isRecording: false - - // Start screen recording using Quickshell.execDetached - function startRecording() { - var currentDate = new Date(); - var hours = String(currentDate.getHours()).padStart(2, '0'); - var minutes = String(currentDate.getMinutes()).padStart(2, '0'); - var day = String(currentDate.getDate()).padStart(2, '0'); - var month = String(currentDate.getMonth() + 1).padStart(2, '0'); - var year = currentDate.getFullYear(); - - var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4"; - var videoPath = Settings.settings.videoPath; - if (videoPath && !videoPath.endsWith("/")) { - videoPath += "/"; - } - var outputPath = videoPath + filename; - var command = "gpu-screen-recorder -w portal -f 60 -a default_output -o " + outputPath; - Quickshell.execDetached(["sh", "-c", command]); - isRecording = true; - quickAccessWidget.isRecording = true; - } - - // Stop recording using Quickshell.execDetached - function stopRecording() { - Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]); - // Optionally, force kill after a delay - var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', sidebarPopupRect); - cleanupTimer.triggered.connect(function () { - Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder.*portal' 2>/dev/null || true"]); - cleanupTimer.destroy(); - }); - isRecording = false; - quickAccessWidget.isRecording = false; - } - - // Clean up processes on destruction - Component.onDestruction: { - if (isRecording) { - stopRecording(); - } - } - - Corners { - id: sidebarCornerLeft - position: "bottomright" - size: 1.1 - fillColor: Theme.backgroundPrimary - anchors.top: mainRectangle.top - offsetX: -447 + sidebarPopupRect.slideOffset - offsetY: 0 - visible: Settings.settings.showCorners - - Behavior on offsetX { + Behavior on x { enabled: !sidebarPopupRect.isAnimating + NumberAnimation { duration: 300 easing.type: Easing.OutCubic } + } + } - Corners { - id: sidebarCornerBottom - position: "bottomright" - size: 1.1 - fillColor: Theme.backgroundPrimary - offsetX: 33 + sidebarPopupRect.slideOffset - offsetY: 46 - visible: Settings.settings.showCorners - - Behavior on offsetX { - enabled: !sidebarPopupRect.isAnimating - NumberAnimation { - duration: 300 - easing.type: Easing.OutCubic - } - } - } - - WallpaperPanel { - id: wallpaperPanel - Component.onCompleted: { - if (parent) { - anchors.top = parent.top; - anchors.right = parent.right; - } - } - } + } + } diff --git a/Widgets/Sidebar/Panel/System.qml b/Widgets/SidePanel/PowerMenu.qml similarity index 67% rename from Widgets/Sidebar/Panel/System.qml rename to Widgets/SidePanel/PowerMenu.qml index fa5397d9..81709357 100644 --- a/Widgets/Sidebar/Panel/System.qml +++ b/Widgets/SidePanel/PowerMenu.qml @@ -1,396 +1,23 @@ import QtQuick -import QtQuick.Layouts import QtQuick.Controls import QtQuick.Effects +import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Widgets +import qs.Components +import qs.Helpers +import qs.Services import qs.Settings import qs.Widgets import qs.Widgets.LockScreen -import qs.Helpers -import qs.Services -import qs.Components Rectangle { id: systemWidget - width: 440 - height: 80 - color: "transparent" - anchors.horizontalCenterOffset: -2 - Rectangle { - id: card - anchors.fill: parent - color: Theme.surface - radius: 18 - - ColumnLayout { - anchors.fill: parent - 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" - radius: 24 - border.color: Theme.accentPrimary - border.width: 2 - z: 2 - } - - Avatar {} - } - - // User info text - ColumnLayout { - spacing: 4 - Layout.fillWidth: true - - Text { - text: Quickshell.env("USER") - font.family: Theme.fontFamily - font.pixelSize: 16 - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "System Uptime: " + uptimeText - font.family: Theme.fontFamily - font.pixelSize: 12 - color: Theme.textSecondary - } - } - - // Spacer - Item { - Layout.fillWidth: true - } - - // System menu button - Rectangle { - id: systemButton - width: 32 - height: 32 - radius: 16 - color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.accentPrimary : "transparent" - border.color: Theme.accentPrimary - border.width: 1 - - Text { - anchors.centerIn: parent - text: "power_settings_new" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 - color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.backgroundPrimary : Theme.accentPrimary - } - - MouseArea { - id: systemButtonArea - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onClicked: { - systemMenu.visible = !systemMenu.visible; - } - } - StyledTooltip { - id: systemTooltip - text: "System" - targetItem: systemButton - tooltipVisible: systemButtonArea.containsMouse - } - } - } - } - } - - PanelWithOverlay { - id: systemMenu - anchors.top: systemButton.bottom - anchors.right: systemButton.right - // System menu popup - Rectangle { - - width: 160 - height: 220 - color: Theme.surface - radius: 8 - border.color: Theme.outline - border.width: 1 - visible: true - z: 9999 - anchors.top: parent.top - anchors.right: parent.right - - // Position below system button - anchors.rightMargin: 32 - anchors.topMargin: systemButton.y + systemButton.height + 48 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 4 - - // Lock button - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 - radius: 6 - color: lockButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - RowLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 8 - - Text { - text: "lock_outline" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 - color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - } - - Text { - text: "Lock Screen" - font.family: Theme.fontFamily - font.pixelSize: 14 - color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - Layout.fillWidth: true - } - } - - MouseArea { - id: lockButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - lockScreen.locked = true; - systemMenu.visible = false; - } - } - } - - // Suspend button - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 - radius: 6 - color: suspendButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - RowLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 8 - - Text { - text: "bedtime" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 - color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - } - - Text { - text: "Suspend" - font.pixelSize: 14 - color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - Layout.fillWidth: true - } - } - - MouseArea { - id: suspendButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - suspend(); - systemMenu.visible = false; - } - } - } - - // Reboot button - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 - radius: 6 - color: rebootButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - RowLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 8 - - Text { - text: "refresh" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 - color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - } - - Text { - text: "Reboot" - font.family: Theme.fontFamily - font.pixelSize: 14 - color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - Layout.fillWidth: true - } - } - - MouseArea { - id: rebootButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - reboot(); - systemMenu.visible = false; - } - } - } - - // Logout button - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 - radius: 6 - color: logoutButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - RowLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 8 - - Text { - text: "exit_to_app" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 - color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - } - - Text { - text: "Logout" - font.pixelSize: 14 - color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - Layout.fillWidth: true - } - } - - MouseArea { - id: logoutButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - logout(); - systemMenu.visible = false; - } - } - } - - // Shutdown button - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 - radius: 6 - color: shutdownButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - RowLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 8 - - Text { - text: "power_settings_new" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 - color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - } - - Text { - text: "Shutdown" - font.pixelSize: 14 - color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - Layout.fillWidth: true - } - } - - MouseArea { - id: shutdownButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - shutdown(); - systemMenu.visible = false; - } - } - } - } - } - } - - // 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"] - running: false - stdout: StdioCollector { - onStreamFinished: { - uptimeText = this.text.trim(); - uptimeProcess.running = false; - } - } - } - - Process { - id: shutdownProcess - command: ["shutdown", "-h", "now"] - running: false - } - - Process { - id: rebootProcess - command: ["reboot"] - running: false - } - - Process { - id: suspendProcess - command: ["systemctl", "suspend"] - running: false - } - - Process { - id: logoutProcessNiri - command: ["niri", "msg", "action", "quit", "--skip-confirmation"] - running: false - } - - Process { - id: logoutProcessHyprland - command: ["hyprctl", "dispatch", "exit"] - running: false - } + property bool panelVisible: false + property var settingsModal: null Process { id: logoutProcess @@ -399,14 +26,12 @@ Rectangle { } function logout() { - if (WorkspaceManager.isNiri) { + if (WorkspaceManager.isNiri) logoutProcessNiri.running = true; - } else if (WorkspaceManager.isHyprland) { + else if (WorkspaceManager.isHyprland) logoutProcessHyprland.running = true; - } else { - // fallback or error + else console.warn("No supported compositor detected for logout"); - } } function suspend() { @@ -421,33 +46,479 @@ Rectangle { rebootProcess.running = true; } - property bool panelVisible: false + function updateSystemInfo() { + uptimeProcess.running = true; + } - // Trigger initial update when panel becomes visible + width: 440 * Theme.scale(Screen) + height: 80 * Theme.scale(Screen) + color: "transparent" + anchors.horizontalCenterOffset: -2 onPanelVisibleChanged: { - if (panelVisible) { + if (panelVisible) updateSystemInfo(); + + } + Component.onCompleted: { + uptimeProcess.running = true; + } + + Rectangle { + id: card + + anchors.fill: parent + color: Theme.surface + radius: 18 * Theme.scale(Screen) + + ColumnLayout { + anchors.fill: parent + anchors.margins: 18 * Theme.scale(Screen) + spacing: 12 * Theme.scale(Screen) + + RowLayout { + Layout.fillWidth: true + spacing: 12 * Theme.scale(Screen) + + Rectangle { + width: 48 * Theme.scale(Screen) + height: 48 * Theme.scale(Screen) + radius: 24 * Theme.scale(Screen) + color: Theme.accentPrimary + + Rectangle { + anchors.fill: parent + color: "transparent" + radius: 24 * Theme.scale(Screen) + border.color: Theme.accentPrimary + border.width: 2 * Theme.scale(Screen) + z: 2 + } + + Avatar { + } + + } + + ColumnLayout { + spacing: 4 * Theme.scale(Screen) + Layout.fillWidth: true + + Text { + text: Quickshell.env("USER") + font.family: Theme.fontFamily + font.pixelSize: 16 * Theme.scale(Screen) + font.bold: true + color: Theme.textPrimary + } + + Text { + text: "System Uptime: " + uptimeText + font.family: Theme.fontFamily + font.pixelSize: 12 * Theme.scale(Screen) + color: Theme.textSecondary + } + + } + + Item { + Layout.fillWidth: true + } + + Rectangle { + id: settingsButton + + width: 32 * Theme.scale(Screen) + height: 32 * Theme.scale(Screen) + radius: 16 * Theme.scale(Screen) + color: settingsButtonArea.containsMouse || settingsButtonArea.pressed ? Theme.accentPrimary : "transparent" + border.color: Theme.accentPrimary + border.width: 1 * Theme.scale(Screen) + + Text { + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + text: "settings" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * Theme.scale(Screen) + color: settingsButtonArea.containsMouse || settingsButtonArea.pressed ? Theme.backgroundPrimary : Theme.accentPrimary + } + + MouseArea { + id: settingsButtonArea + + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) + settingsModal.openSettings(); + } + } + + StyledTooltip { + id: settingsTooltip + + text: "Settings" + targetItem: settingsButton + tooltipVisible: settingsButtonArea.containsMouse + } + + } + + Rectangle { + id: systemButton + + width: 32 * Theme.scale(Screen) + height: 32 * Theme.scale(Screen) + radius: 16 * Theme.scale(Screen) + color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.accentPrimary : "transparent" + border.color: Theme.accentPrimary + border.width: 1 * Theme.scale(Screen) + + Text { + anchors.centerIn: parent + text: "power_settings_new" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * Theme.scale(Screen) + color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.backgroundPrimary : Theme.accentPrimary + } + + MouseArea { + id: systemButtonArea + + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + systemMenu.visible = !systemMenu.visible; + } + } + + StyledTooltip { + id: systemTooltip + + text: "Power Menu" + targetItem: systemButton + tooltipVisible: systemButtonArea.containsMouse + } + + } + + } + } + + } + + PanelWithOverlay { + id: systemMenu + + anchors.top: systemButton.bottom + anchors.right: systemButton.right + + Rectangle { + width: 160 * Theme.scale(Screen) + height: 220 * Theme.scale(Screen) + color: Theme.surface + radius: 8 * Theme.scale(Screen) + border.color: Theme.outline + border.width: 1 * Theme.scale(Screen) + visible: true + z: 9999 + anchors.top: parent.top + anchors.right: parent.right + anchors.rightMargin: 32 * Theme.scale(Screen) + anchors.topMargin: systemButton.y + systemButton.height + 48 * Theme.scale(Screen) + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 * Theme.scale(Screen) + spacing: 4 * Theme.scale(Screen) + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * Theme.scale(Screen) + radius: 6 * Theme.scale(Screen) + color: lockButtonArea.containsMouse ? Theme.accentPrimary : "transparent" + + RowLayout { + anchors.fill: parent + anchors.margins: 12 * Theme.scale(Screen) + spacing: 8 * Theme.scale(Screen) + + Text { + text: "lock_outline" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * Theme.scale(Screen) + color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary + } + + Text { + text: "Lock Screen" + font.family: Theme.fontFamily + font.pixelSize: 14 * Theme.scale(Screen) + color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary + Layout.fillWidth: true + } + + } + + MouseArea { + id: lockButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + lockScreen.locked = true; + systemMenu.visible = false; + } + } + + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * Theme.scale(Screen) + radius: 6 * Theme.scale(Screen) + color: suspendButtonArea.containsMouse ? Theme.accentPrimary : "transparent" + + RowLayout { + anchors.fill: parent + anchors.margins: 12 * Theme.scale(Screen) + spacing: 8 * Theme.scale(Screen) + + Text { + text: "bedtime" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * Theme.scale(Screen) + color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary + } + + Text { + text: "Suspend" + font.pixelSize: 14 * Theme.scale(Screen) + color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary + Layout.fillWidth: true + } + + } + + MouseArea { + id: suspendButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + suspend(); + systemMenu.visible = false; + } + } + + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * Theme.scale(Screen) + radius: 6 * Theme.scale(Screen) + color: rebootButtonArea.containsMouse ? Theme.accentPrimary : "transparent" + + RowLayout { + anchors.fill: parent + anchors.margins: 12 * Theme.scale(Screen) + spacing: 8 * Theme.scale(Screen) + + Text { + text: "refresh" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * Theme.scale(Screen) + color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary + } + + Text { + text: "Reboot" + font.family: Theme.fontFamily + font.pixelSize: 14 * Theme.scale(Screen) + color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary + Layout.fillWidth: true + } + + } + + MouseArea { + id: rebootButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + reboot(); + systemMenu.visible = false; + } + } + + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * Theme.scale(Screen) + radius: 6 * Theme.scale(Screen) + color: logoutButtonArea.containsMouse ? Theme.accentPrimary : "transparent" + + RowLayout { + anchors.fill: parent + anchors.margins: 12 * Theme.scale(Screen) + spacing: 8 * Theme.scale(Screen) + + Text { + text: "exit_to_app" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * Theme.scale(Screen) + color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary + } + + Text { + text: "Logout" + font.pixelSize: 14 * Theme.scale(Screen) + color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary + Layout.fillWidth: true + } + + } + + MouseArea { + id: logoutButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + logout(); + systemMenu.visible = false; + } + } + + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * Theme.scale(Screen) + radius: 6 * Theme.scale(Screen) + color: shutdownButtonArea.containsMouse ? Theme.accentPrimary : "transparent" + + RowLayout { + anchors.fill: parent + anchors.margins: 12 * Theme.scale(Screen) + spacing: 8 * Theme.scale(Screen) + + Text { + text: "power_settings_new" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * Theme.scale(Screen) + color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary + } + + Text { + text: "Shutdown" + font.pixelSize: 14 * Theme.scale(Screen) + color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary + Layout.fillWidth: true + } + + } + + MouseArea { + id: shutdownButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + shutdown(); + systemMenu.visible = false; + } + } + + } + + } + + } + + } + + Process { + id: uptimeProcess + + command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + uptimeText = this.text.trim(); + uptimeProcess.running = false; + } + } + + } + + Process { + id: shutdownProcess + + command: ["shutdown", "-h", "now"] + running: false + } + + Process { + id: rebootProcess + + command: ["reboot"] + running: false + } + + Process { + id: suspendProcess + + command: ["systemctl", "suspend"] + running: false + } + + Process { + id: logoutProcessNiri + + command: ["niri", "msg", "action", "quit", "--skip-confirmation"] + running: false + } + + Process { + id: logoutProcessHyprland + + command: ["hyprctl", "dispatch", "exit"] + running: false + } + + Process { + id: logoutProcess + + command: ["loginctl", "terminate-user", Quickshell.env("USER")] + running: false } - // Timer to update uptime - only runs when panel is visible Timer { - interval: 60000 // Update every minute + interval: 60000 repeat: true running: panelVisible onTriggered: updateSystemInfo() } - Component.onCompleted: { - uptimeProcess.running = true; - } - - function updateSystemInfo() { - uptimeProcess.running = true; - } - - // Add lockscreen instance (hidden by default) LockScreen { id: lockScreen } + } diff --git a/Widgets/Sidebar/Panel/PowerProfile.qml b/Widgets/SidePanel/PowerProfile.qml similarity index 86% rename from Widgets/Sidebar/Panel/PowerProfile.qml rename to Widgets/SidePanel/PowerProfile.qml index c68fa4f3..409ebacd 100644 --- a/Widgets/Sidebar/Panel/PowerProfile.qml +++ b/Widgets/SidePanel/PowerProfile.qml @@ -7,22 +7,22 @@ import qs.Components Rectangle { id: card - width: 200 - height: 70 + width: 200 * Theme.scale(Screen) + height: 70 * Theme.scale(Screen) color: Theme.surface - radius: 18 + radius: 18 * Theme.scale(Screen) Row { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter - spacing: 20 + spacing: 20 * Theme.scale(Screen) + - // Performance Rectangle { - width: 36; height: 36 - radius: 18 + width: 36 * Theme.scale(Screen); height: 36 * Theme.scale(Screen) + radius: 18 * Theme.scale(Screen) border.color: Theme.accentPrimary - border.width: 1 + border.width: 1 * Theme.scale(Screen) color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance) ? Theme.accentPrimary : (perfMouseArea.containsMouse ? Theme.accentPrimary : "transparent") @@ -33,7 +33,7 @@ Rectangle { anchors.centerIn: parent text: "speed" font.family: "Material Symbols Outlined" - font.pixelSize: 22 + font.pixelSize: 22 * Theme.scale(Screen) color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance) || perfMouseArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary @@ -63,12 +63,12 @@ Rectangle { } } - // Balanced + Rectangle { - width: 36; height: 36 - radius: 18 + width: 36 * Theme.scale(Screen); height: 36 * Theme.scale(Screen) + radius: 18 * Theme.scale(Screen) border.color: Theme.accentPrimary - border.width: 1 + border.width: 1 * Theme.scale(Screen) color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced) ? Theme.accentPrimary : (balMouseArea.containsMouse ? Theme.accentPrimary : "transparent") @@ -79,7 +79,7 @@ Rectangle { anchors.centerIn: parent text: "balance" font.family: "Material Symbols Outlined" - font.pixelSize: 22 + font.pixelSize: 22 * Theme.scale(Screen) color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced) || balMouseArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary @@ -109,12 +109,12 @@ Rectangle { } } - // Power Saver + Rectangle { - width: 36; height: 36 - radius: 18 + width: 36 * Theme.scale(Screen); height: 36 * Theme.scale(Screen) + radius: 18 * Theme.scale(Screen) border.color: Theme.accentPrimary - border.width: 1 + border.width: 1 * Theme.scale(Screen) color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver) ? Theme.accentPrimary : (saveMouseArea.containsMouse ? Theme.accentPrimary : "transparent") @@ -125,7 +125,7 @@ Rectangle { anchors.centerIn: parent text: "eco" font.family: "Material Symbols Outlined" - font.pixelSize: 22 + font.pixelSize: 22 * Theme.scale(Screen) color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver) || saveMouseArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary diff --git a/Widgets/SidePanel/SettingsIcon.qml b/Widgets/SidePanel/SettingsIcon.qml new file mode 100644 index 00000000..bdae2483 --- /dev/null +++ b/Widgets/SidePanel/SettingsIcon.qml @@ -0,0 +1,94 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Settings +import qs.Services +import qs.Widgets.SettingsWindow +import qs.Components + +PanelWindow { + id: settingsModal + implicitWidth: 480 * Theme.scale(Screen) + implicitHeight: 780 * Theme.scale(Screen) + visible: false + color: "transparent" + anchors.top: true + anchors.right: true + margins.right: 0 + margins.top: 0 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + // Signal to request weather refresh + signal weatherRefreshRequested() + + // Property to track the settings window instance + property var settingsWindow: null + + // Function to open the modal and initialize temp values + function openSettings(initialTabIndex) { + if (!settingsWindow) { + // Create new window + settingsWindow = settingsComponent.createObject(null); // No parent to avoid dependency issues + if (settingsWindow) { + // Set the initial tab if provided + if (typeof initialTabIndex === 'number' && initialTabIndex >= 0 && initialTabIndex <= 8) { + settingsWindow.activeTabIndex = initialTabIndex; + } + settingsWindow.visible = true; + + // Show wallpaper selector if opening wallpaper tab (after window is visible) + if (typeof initialTabIndex === 'number' && initialTabIndex === 6) { + Qt.callLater(function() { + if (settingsWindow && settingsWindow.showWallpaperSelector) { + settingsWindow.showWallpaperSelector(); + } + }, 100); // Small delay to ensure window is fully loaded + } + // Handle window closure + settingsWindow.visibleChanged.connect(function() { + if (settingsWindow && !settingsWindow.visible) { + // Trigger weather refresh when settings close + weatherRefreshRequested(); + var windowToDestroy = settingsWindow; + settingsWindow = null; + windowToDestroy.destroy(); + } + }); + } + sidebarPopup.dismiss(); + } else if (settingsWindow.visible) { + // Close and destroy window + var windowToDestroy = settingsWindow; + settingsWindow = null; + windowToDestroy.visible = false; + windowToDestroy.destroy(); + } + } + + // Function to close the modal and release focus + function closeSettings() { + if (settingsWindow) { + var windowToDestroy = settingsWindow; + settingsWindow = null; + windowToDestroy.visible = false; + windowToDestroy.destroy(); + } + } + + Component { + id: settingsComponent + SettingsWindow {} + } + + // Clean up on destruction + Component.onDestruction: { + if (settingsWindow) { + var windowToDestroy = settingsWindow; + settingsWindow = null; + windowToDestroy.destroy(); + } + } + +} diff --git a/Widgets/SidePanel/SettingsModal.qml b/Widgets/SidePanel/SettingsModal.qml new file mode 100644 index 00000000..5a47ecfc --- /dev/null +++ b/Widgets/SidePanel/SettingsModal.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Settings +import qs.Services +import qs.Widgets.SettingsWindow +import qs.Components + +PanelWindow { + id: settingsModal + implicitWidth: 480 * Theme.scale(Screen) + implicitHeight: 780 * Theme.scale(Screen) + visible: false + color: "transparent" + anchors.top: true + anchors.right: true + margins.right: 0 + margins.top: 0 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + // Property to track the settings window instance + property var settingsWindow: null + + // Function to open the modal and initialize temp values + function openSettings() { + 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(); + } + } + + // Function to close the modal and release focus + function closeSettings() { + if (settingsWindow) { + var windowToDestroy = settingsWindow; + settingsWindow = null; + windowToDestroy.visible = false; + windowToDestroy.destroy(); + } + } + + Component { + id: settingsComponent + SettingsWindow {} + } + + // Clean up on destruction + Component.onDestruction: { + if (settingsWindow) { + var windowToDestroy = settingsWindow; + settingsWindow = null; + windowToDestroy.destroy(); + } + } + + // Refresh weather data when hidden + onVisibleChanged: { + if (!visible && typeof weather !== 'undefined' && weather !== null && weather.fetchCityWeather) { + weather.fetchCityWeather(); + } + } +} diff --git a/Widgets/Sidebar/Panel/SystemMonitor.qml b/Widgets/SidePanel/SystemMonitor.qml similarity index 72% rename from Widgets/Sidebar/Panel/SystemMonitor.qml rename to Widgets/SidePanel/SystemMonitor.qml index b9380e47..dd0464a5 100644 --- a/Widgets/Sidebar/Panel/SystemMonitor.qml +++ b/Widgets/SidePanel/SystemMonitor.qml @@ -8,35 +8,37 @@ import qs.Settings Rectangle { id: systemMonitor - width: 70 - height: 250 + width: 70 * Theme.scale(Screen) + height: 250 * Theme.scale(Screen) color: "transparent" + // Track visibility state for panel integration property bool isVisible: false Rectangle { id: card anchors.fill: parent color: Theme.surface - radius: 18 + radius: 18 * Theme.scale(Screen) ColumnLayout { anchors.fill: parent - anchors.margins: 8 - spacing: 12 + anchors.margins: 8 * Theme.scale(Screen) + spacing: 12 * Theme.scale(Screen) Layout.alignment: Qt.AlignVCenter - // CPU Usage + + // CPU usage indicator with circular progress bar Item { - width: 50; height: 50 + width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen) CircularProgressBar { id: cpuBar progress: Sysinfo.cpuUsage / 100 - size: 50 - strokeWidth: 4 + size: 50 * Theme.scale(Screen) + strokeWidth: 4 * Theme.scale(Screen) hasNotch: true notchIcon: "speed" - notchIconSize: 14 + notchIconSize: 14 * Theme.scale(Screen) Layout.alignment: Qt.AlignHCenter } MouseArea { @@ -55,18 +57,19 @@ Rectangle { } } - // Cpu Temp + + // CPU temperature indicator with circular progress bar Item { - width: 50; height: 50 + width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen) CircularProgressBar { id: tempBar progress: Sysinfo.cpuTemp / 100 - size: 50 - strokeWidth: 4 + size: 50 * Theme.scale(Screen) + strokeWidth: 4 * Theme.scale(Screen) hasNotch: true units: "°C" notchIcon: "thermometer" - notchIconSize: 14 + notchIconSize: 14 * Theme.scale(Screen) Layout.alignment: Qt.AlignHCenter } MouseArea { @@ -85,17 +88,18 @@ Rectangle { } } - // Memory Usage + + // Memory usage indicator with circular progress bar Item { - width: 50; height: 50 + width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen) CircularProgressBar { id: memBar progress: Sysinfo.memoryUsagePer / 100 - size: 50 - strokeWidth: 4 + size: 50 * Theme.scale(Screen) + strokeWidth: 4 * Theme.scale(Screen) hasNotch: true notchIcon: "memory" - notchIconSize: 14 + notchIconSize: 14 * Theme.scale(Screen) Layout.alignment: Qt.AlignHCenter } MouseArea { @@ -114,17 +118,18 @@ Rectangle { } } - // Disk Usage + + // Disk usage indicator with circular progress bar Item { - width: 50; height: 50 + width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen) CircularProgressBar { id: diskBar progress: Sysinfo.diskUsage / 100 - size: 50 - strokeWidth: 4 + size: 50 * Theme.scale(Screen) + strokeWidth: 4 * Theme.scale(Screen) hasNotch: true notchIcon: "storage" - notchIconSize: 14 + notchIconSize: 14 * Theme.scale(Screen) Layout.alignment: Qt.AlignHCenter } MouseArea { diff --git a/Widgets/Sidebar/Panel/Weather.qml b/Widgets/SidePanel/Weather.qml similarity index 62% rename from Widgets/Sidebar/Panel/Weather.qml rename to Widgets/SidePanel/Weather.qml index f9feb533..3927e931 100644 --- a/Widgets/Sidebar/Panel/Weather.qml +++ b/Widgets/SidePanel/Weather.qml @@ -2,12 +2,12 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import qs.Settings -import "../../../Helpers/Weather.js" as WeatherHelper +import "../../Helpers/Weather.js" as WeatherHelper Rectangle { id: weatherRoot - width: 440 - height: 180 + width: 440 * Theme.scale(Screen) + height: 180 * Theme.scale(Screen) color: "transparent" anchors.horizontalCenterOffset: -2 @@ -15,6 +15,20 @@ Rectangle { property var weatherData: null property string errorString: "" property bool isVisible: false + property int lastFetchTime: 0 + property bool isLoading: false + + // Auto-refetch weather when city changes + Connections { + target: Settings.settings + function onWeatherCityChanged() { + if (isVisible && city !== "") { + // Force refresh when city changes + lastFetchTime = 0; + fetchCityWeather(); + } + } + } Component.onCompleted: { if (isVisible) { @@ -23,20 +37,42 @@ Rectangle { } function fetchCityWeather() { + if (!city || city.trim() === "") { + errorString = "No city configured"; + return; + } + + // Check if we should fetch new data (avoid fetching too frequently) + var currentTime = Date.now(); + var timeSinceLastFetch = currentTime - lastFetchTime; + + // Only skip if we have recent data AND lastFetchTime is not 0 (initial state) + if (lastFetchTime > 0 && timeSinceLastFetch < 60000) { // 1 minute + return; // Skip if last fetch was less than 1 minute ago + } + + isLoading = true; + errorString = ""; + WeatherHelper.fetchCityWeather(city, function(result) { weatherData = result.weather; + lastFetchTime = currentTime; errorString = ""; + isLoading = false; }, function(err) { errorString = err; + isLoading = false; } ); } function startWeatherFetch() { isVisible = true - fetchCityWeather() + // Force refresh when panel opens, regardless of time check + lastFetchTime = 0; + fetchCityWeather(); } function stopWeatherFetch() { @@ -47,81 +83,90 @@ Rectangle { id: card anchors.fill: parent color: Theme.surface - radius: 18 + radius: 18 * Theme.scale(Screen) ColumnLayout { anchors.fill: parent - anchors.margins: 18 - spacing: 12 + anchors.margins: 18 * Theme.scale(Screen) + spacing: 12 * Theme.scale(Screen) - // Current weather row + RowLayout { - spacing: 12 + spacing: 12 * Theme.scale(Screen) Layout.fillWidth: true - // Weather icon and basic info section - RowLayout { - spacing: 12 - Layout.preferredWidth: 140 - // Weather icon + RowLayout { + spacing: 12 * Theme.scale(Screen) + Layout.preferredWidth: 140 * Theme.scale(Screen) + + Text { id: weatherIcon - text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud" + text: isLoading ? "sync" : (weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud") font.family: "Material Symbols Outlined" - font.pixelSize: 28 + font.pixelSize: 28 * Theme.scale(Screen) verticalAlignment: Text.AlignVCenter - color: Theme.accentPrimary + color: isLoading ? Theme.accentPrimary : Theme.accentPrimary Layout.alignment: Qt.AlignVCenter + + // Add rotation animation for loading state + RotationAnimation on rotation { + running: isLoading + from: 0 + to: 360 + duration: 1000 + loops: Animation.Infinite + } } ColumnLayout { - spacing: 2 + spacing: 2 * Theme.scale(Screen) RowLayout { - spacing: 4 + spacing: 4 * Theme.scale(Screen) Text { text: city font.family: Theme.fontFamily - font.pixelSize: 14 + font.pixelSize: 14 * Theme.scale(Screen) font.bold: true color: Theme.textPrimary } Text { text: weatherData && weatherData.timezone_abbreviation ? `(${weatherData.timezone_abbreviation})` : "" font.family: Theme.fontFamily - font.pixelSize: 10 + font.pixelSize: 10 * Theme.scale(Screen) color: Theme.textSecondary - leftPadding: 2 + leftPadding: 2 * Theme.scale(Screen) } } Text { text: weatherData && weatherData.current_weather ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9/5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--°F" : "--°C") font.family: Theme.fontFamily - font.pixelSize: 24 + font.pixelSize: 24 * Theme.scale(Screen) font.bold: true color: Theme.textPrimary } } } - // Spacer to push content to the right + Item { Layout.fillWidth: true } } - // Separator line + Rectangle { width: parent.width - height: 1 + height: 1 * Theme.scale(Screen) color: Qt.rgba(Theme.textSecondary.g, Theme.textSecondary.g, Theme.textSecondary.b, 0.12) Layout.fillWidth: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 + Layout.topMargin: 2 * Theme.scale(Screen) + Layout.bottomMargin: 2 * Theme.scale(Screen) } - // 5-day forecast row + RowLayout { - spacing: 12 + spacing: 12 * Theme.scale(Screen) Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter visible: weatherData && weatherData.daily && weatherData.daily.time @@ -129,31 +174,31 @@ Rectangle { Repeater { model: weatherData && weatherData.daily && weatherData.daily.time ? 5 : 0 delegate: ColumnLayout { - spacing: 2 + spacing: 2 * Theme.scale(Screen) 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 + font.pixelSize: 12 * Theme.scale(Screen) color: Theme.textSecondary horizontalAlignment: Text.AlignHCenter Layout.alignment: Qt.AlignHCenter } Text { - // Material Symbol icon + text: materialSymbolForCode(weatherData.daily.weathercode[index]) font.family: "Material Symbols Outlined" - font.pixelSize: 22 + font.pixelSize: 22 * Theme.scale(Screen) color: Theme.accentPrimary horizontalAlignment: Text.AlignHCenter 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 + font.pixelSize: 12 * Theme.scale(Screen) color: Theme.textPrimary horizontalAlignment: Text.AlignHCenter Layout.alignment: Qt.AlignHCenter @@ -162,29 +207,29 @@ Rectangle { } } - // Error message + Text { text: errorString color: Theme.error visible: errorString !== "" font.family: Theme.fontFamily - font.pixelSize: 10 + font.pixelSize: 10 * Theme.scale(Screen) horizontalAlignment: Text.AlignHCenter Layout.alignment: Qt.AlignHCenter } } } - // 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/SidePanel/WifiPanel.qml similarity index 95% rename from Widgets/Sidebar/Panel/WifiPanel.qml rename to Widgets/SidePanel/WifiPanel.qml index cdc6b300..e70ff7f8 100644 --- a/Widgets/Sidebar/Panel/WifiPanel.qml +++ b/Widgets/SidePanel/WifiPanel.qml @@ -204,19 +204,19 @@ Item { wifiLogic.connectingSsid = params.ssid; - // Find the target network in our networks data + const targetNetwork = wifiLogic.networks[params.ssid]; - // Check if profile already exists using existing field + if (targetNetwork && targetNetwork.existing) { - // Profile exists, just bring it up (no password prompt) + upConnectionProcess.profileName = params.ssid; upConnectionProcess.running = true; wifiLogic.pendingConnect = null; return; } - // No existing profile, proceed with normal connection flow + if (params.security && params.security !== "--") { getInterfaceProcess.running = true; return; @@ -232,7 +232,7 @@ Item { } } - // Disconnect, delete profile, refresh + Process { id: disconnectProfileProcess property string connectionName: "" @@ -291,7 +291,7 @@ Item { } - // Handles connecting to a Wi-Fi network, with or without password + Process { id: connectProcess property string ssid: "" @@ -336,7 +336,7 @@ Item { } } - // Finds the correct Wi-Fi interface for connection + Process { id: getInterfaceProcess running: false @@ -370,7 +370,7 @@ Item { } } - // Adds a new Wi-Fi connection profile + Process { id: addConnectionProcess property string ifname: "" @@ -403,7 +403,7 @@ Item { } } - // Brings up the new connection profile and finalizes connection state + Process { id: upConnectionProcess property string profileName: "" @@ -436,7 +436,7 @@ Item { } } - // Wifi button (no background card) + Rectangle { id: wifiButton width: 36 @@ -516,8 +516,8 @@ Item { Layout.alignment: Qt.AlignVCenter visible: false running: false - color: Theme.accentPrimary // Assuming Spinner supports color property - size: 22 // Based on the existing Spinner usage + color: Theme.accentPrimary + size: 22 } IconButton { id: refreshButton @@ -704,7 +704,7 @@ Item { anchors.fill: parent hoverEnabled: true onClicked: { - // Toggle the action panel for this network + if (wifiLogic.actionPanelSsid === modelData.ssid) { wifiLogic.actionPanelSsid = ""; // Close if already open } else { @@ -791,7 +791,7 @@ Item { } } } - // Action panel for network connection controls + Rectangle { visible: modelData.ssid === wifiLogic.actionPanelSsid Layout.fillWidth: true @@ -806,7 +806,7 @@ Item { anchors.fill: parent anchors.margins: 12 spacing: 10 - // Password field for new secured networks + Item { Layout.fillWidth: true Layout.preferredHeight: 36 @@ -830,7 +830,7 @@ Item { inputMethodHints: Qt.ImhNone echoMode: TextInput.Password onAccepted: { - // Connect with the entered password + wifiLogic.pendingConnect = { ssid: modelData.ssid, security: modelData.security, @@ -843,7 +843,7 @@ Item { } } } - // Connect/Disconnect button + Rectangle { Layout.preferredWidth: 80 Layout.preferredHeight: 36 @@ -861,12 +861,12 @@ Item { anchors.fill: parent onClicked: { if (modelData.connected) { - // Disconnect from network + wifiLogic.disconnectNetwork(modelData.ssid); } else { - // For secured networks, check if we need password + if (wifiLogic.isSecured(modelData.security) && !modelData.existing) { - // If password field is visible and has content, use it + if (actionPanelPasswordField.text.length > 0) { wifiLogic.pendingConnect = { ssid: modelData.ssid, @@ -875,10 +875,9 @@ Item { }; wifiLogic.doConnect(); } - // For new networks without password entered, we might want to show an error or handle differently - // For now, we'll just close the panel + } else { - // Connect to open network + wifiLogic.connectNetwork(modelData.ssid, modelData.security); } } diff --git a/Widgets/Sidebar/Config/CollapsibleCategory.qml b/Widgets/Sidebar/Config/CollapsibleCategory.qml deleted file mode 100644 index d0c2eb71..00000000 --- a/Widgets/Sidebar/Config/CollapsibleCategory.qml +++ /dev/null @@ -1,56 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import qs.Settings - -ColumnLayout { - property alias title: headerText.text - property bool expanded: false // Hidden by default - default property alias content: contentItem.children - - Rectangle { - Layout.fillWidth: true - height: 44 - radius: 12 - color: Theme.surface - border.color: Theme.accentPrimary - border.width: 2 - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 8 - Item { width: 2 } - Text { - id: headerText - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeBody - font.bold: true - color: Theme.textPrimary - } - Item { Layout.fillWidth: true } - Rectangle { - width: 32; height: 32 - color: "transparent" - Text { - anchors.centerIn: parent - text: expanded ? "expand_less" : "expand_more" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody - color: Theme.accentPrimary - } - } - } - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: expanded = !expanded - } - } - Item { height: 8 } - ColumnLayout { - id: contentItem - Layout.fillWidth: true - visible: expanded - spacing: 0 - } -} \ No newline at end of file diff --git a/Widgets/Sidebar/Config/WeatherSettings.qml b/Widgets/Sidebar/Config/WeatherSettings.qml deleted file mode 100644 index d7689dc0..00000000 --- a/Widgets/Sidebar/Config/WeatherSettings.qml +++ /dev/null @@ -1,275 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import qs.Settings - -Rectangle { - id: weatherSettingsCard - Layout.fillWidth: true - Layout.preferredHeight: 320 - color: Theme.surface - radius: 18 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 18 - spacing: 12 - - // Weather Settings Header - RowLayout { - Layout.fillWidth: true - spacing: 12 - - Text { - text: "wb_sunny" - font.family: "Material Symbols Outlined" - font.pixelSize: 20 - color: Theme.accentPrimary - } - - Text { - text: "Weather Settings" - font.family: Theme.fontFamily - font.pixelSize: 16 - font.bold: true - color: Theme.textPrimary - Layout.fillWidth: true - } - } - - // Weather City Setting - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - - Text { - text: "City" - font.family: Theme.fontFamily - font.pixelSize: 13 - font.bold: true - color: Theme.textPrimary - } - - 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.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.leftMargin: 12 - anchors.rightMargin: 12 - anchors.topMargin: 6 - anchors.bottomMargin: 6 - text: Settings.settings.weatherCity - font.family: Theme.fontFamily - 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(); - } - } - } - } - } - - // Temperature Unit Setting - RowLayout { - spacing: 12 - Layout.fillWidth: true - - Text { - text: "Temperature Unit" - font.family: Theme.fontFamily - font.pixelSize: 13 - font.bold: true - color: Theme.textPrimary - } - - Item { - Layout.fillWidth: true - } - - // Custom Material 3 Switch - Rectangle { - id: customSwitch - width: 52 - height: 32 - radius: 16 - color: Theme.accentPrimary - border.color: Theme.accentPrimary - border.width: 2 - - Rectangle { - id: thumb - width: 28 - height: 28 - radius: 14 - color: Theme.surface - border.color: Theme.outline - border.width: 1 - y: 2 - x: Settings.settings.useFahrenheit ? customSwitch.width - width - 2 : 2 - - Text { - anchors.centerIn: parent - text: Settings.settings.useFahrenheit ? "\u00b0F" : "\u00b0C" - font.family: Theme.fontFamily - font.pixelSize: 12 - font.bold: true - color: Theme.textPrimary - } - - Behavior on x { - NumberAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - Settings.settings.useFahrenheit = !Settings.settings.useFahrenheit; - } - } - } - - - } - - // Random Wallpaper Setting - RowLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - Text { - text: "Use 12 Hour Clock" - font.pixelSize: 13 - font.bold: true - color: Theme.textPrimary - } - - Item { - Layout.fillWidth: true - } - - // Custom Material 3 Switch - 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: randomWallpaperThumb - 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; - } - } - } - } - - // Reverse Day Month Setting - RowLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - Text { - text: "US Style Date" - font.pixelSize: 13 - font.bold: true - color: Theme.textPrimary - } - - Item { - Layout.fillWidth: true - } - - // Custom Material 3 Switch - 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; - } - } - } - } - } -} diff --git a/qmlls.ini b/qmlls.ini new file mode 100644 index 00000000..e69de29b diff --git a/shell.qml b/shell.qml index fb1daf3a..899dedd3 100644 --- a/shell.qml +++ b/shell.qml @@ -9,18 +9,25 @@ import qs.Bar.Modules import qs.Widgets import qs.Widgets.LockScreen import qs.Widgets.Notification +import qs.Widgets.SettingsWindow import qs.Settings import qs.Helpers -import "./Helpers/IdleInhibitor.qml" -import "./Helpers/IPCHandlers.qml" - Scope { id: root property alias appLauncherPanel: appLauncherPanel - property var notificationHistoryWin: notificationHistoryWin + property var notificationHistoryWin: notificationHistoryLoader.active ? notificationHistoryLoader.item : null + property var settingsWindow: null property bool pendingReload: false + + // Function to load notification history + function loadNotificationHistory() { + if (!notificationHistoryLoader.active) { + notificationHistoryLoader.loading = true; + } + return notificationHistoryLoader; + } // Helper function to round value to nearest step function roundToStep(value, step) { @@ -47,10 +54,22 @@ Scope { Quickshell.shell = root; } + Background {} + Overview {} + Bar { id: bar shell: root - property var notificationHistoryWin: notificationHistoryWin + property var notificationHistoryWin: notificationHistoryLoader.active ? notificationHistoryLoader.item : null + } + + Variants { + model: Quickshell.screens + + Dock { + id: dock + property var modelData + } } Dock { @@ -79,13 +98,14 @@ Scope { NotificationServer { id: notificationServer onNotification: function (notification) { - console.log("Notification received:", notification.appName); + console.log("[Notification] Received notification:", notification.appName, "-", notification.summary); notification.tracked = true; if (notificationPopup.notificationsVisible) { + // Add notification to the popup manager notificationPopup.addNotification(notification); } - if (notificationHistoryWin) { - notificationHistoryWin.addToHistory({ + if (notificationHistoryLoader.active && notificationHistoryLoader.item) { + notificationHistoryLoader.item.addToHistory({ id: notification.id, appName: notification.appName || "Notification", summary: notification.summary || "", @@ -99,11 +119,35 @@ Scope { NotificationPopup { id: notificationPopup - barVisible: bar.visible } - NotificationHistory { - id: notificationHistoryWin + // LazyLoader for NotificationHistory - only load when needed + LazyLoader { + id: notificationHistoryLoader + loading: false + component: NotificationHistory {} + } + + // Centralized LazyLoader for SettingsWindow - prevents crashes on multiple opens + LazyLoader { + id: settingsWindowLoader + loading: false + component: SettingsWindow { + Component.onCompleted: { + root.settingsWindow = this; + } + } + } + + // Function to safely show/hide settings window + function toggleSettingsWindow() { + if (!settingsWindowLoader.active) { + settingsWindowLoader.loading = true; + } + + if (settingsWindowLoader.item) { + settingsWindowLoader.item.visible = !settingsWindowLoader.item.visible; + } } // Reference to the default audio sink from Pipewire @@ -144,14 +188,15 @@ Scope { function onScreensChanged() { if (lockScreen.locked) { pendingReload = true; - } else { + } /*else { reloadTimer.restart(); - } + } */ + // ^commented out for now to fix QS crash on monitor wake. + // if it reintroduces the notification bug (https://github.com/Ly-sec/Noctalia/issues/32)... + // we need to find a different fix } } - // --- NEW: Keep volume property in sync with actual Pipewire audio sink volume --- - Connections { target: defaultAudioSink ? defaultAudioSink.audio : null function onVolumeChanged() { @@ -172,4 +217,5 @@ Scope { } } } -} + +} \ No newline at end of file