diff --git a/Bar/Modules/AudioDeviceSelector.qml b/Bar/Modules/AudioDeviceSelector.qml index 965acc9..5af3ffa 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,48 +96,62 @@ 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" @@ -86,10 +159,12 @@ PanelWithOverlay { 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 @@ -99,6 +174,7 @@ PanelWithOverlay { maximumLineCount: 1 Layout.fillWidth: true } + Text { text: modelData.description !== modelData.nickname ? modelData.description : "" font.pixelSize: 10 @@ -107,15 +183,19 @@ PanelWithOverlay { 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" @@ -123,12 +203,15 @@ PanelWithOverlay { font.pixelSize: 10 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 @@ -136,37 +219,51 @@ PanelWithOverlay { font.pixelSize: 10 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" @@ -174,10 +271,12 @@ PanelWithOverlay { 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 @@ -187,6 +286,7 @@ PanelWithOverlay { maximumLineCount: 1 Layout.fillWidth: true } + Text { text: modelData.description !== modelData.nickname ? modelData.description : "" font.pixelSize: 10 @@ -195,15 +295,19 @@ PanelWithOverlay { 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" @@ -211,12 +315,15 @@ PanelWithOverlay { font.pixelSize: 10 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 @@ -224,55 +331,25 @@ PanelWithOverlay { font.pixelSize: 10 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/Calendar.qml b/Bar/Modules/Calendar.qml index 55f1b83..9e9d106 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 @@ -60,13 +65,15 @@ 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 @@ -77,16 +84,11 @@ PanelWithOverlay { 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,7 +160,7 @@ 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 + opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? 1 : 0.7) : 0.3 font.pixelSize: 13 font.family: Theme.fontFamily font.bold: model.today ? true : false @@ -153,6 +168,7 @@ PanelWithOverlay { 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/Widgets/Notification/NotificationHistory.qml b/Widgets/Notification/NotificationHistory.qml index 7e6277a..b0be4ba 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 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/SettingsWindow/SettingsWindow.qml b/Widgets/SettingsWindow/SettingsWindow.qml index b7d9cce..95b91d2 100644 --- a/Widgets/SettingsWindow/SettingsWindow.qml +++ b/Widgets/SettingsWindow/SettingsWindow.qml @@ -155,6 +155,11 @@ PanelWithOverlay { // Center the settings window on screen anchors.centerIn: parent + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + Rectangle { id: background diff --git a/Widgets/SidePanel/PanelPopup.qml b/Widgets/SidePanel/PanelPopup.qml index cc6dbe9..6119be8 100644 --- a/Widgets/SidePanel/PanelPopup.qml +++ b/Widgets/SidePanel/PanelPopup.qml @@ -3,23 +3,14 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Wayland +import qs.Components import qs.Settings import qs.Widgets.SettingsWindow -import qs.Components PanelWithOverlay { id: sidebarPopup - property var shell: null - // 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(); - } - }); - } + property var shell: null function showAt() { sidebarPopupRect.showAt(); @@ -37,18 +28,27 @@ PanelWithOverlay { sidebarPopupRect.hidePopup(); } - Rectangle { - id: sidebarPopupRect - implicitWidth: 500 - implicitHeight: 800 - visible: parent.visible - color: "transparent" - anchors.top: parent.top - anchors.right: parent.right + // 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 - property real slideOffset: width property bool isAnimating: false + property int leftPadding: 20 + property int bottomPadding: 20 + // Recording properties + property bool isRecording: false function showAt() { if (!sidebarPopup.visible) { @@ -59,24 +59,26 @@ PanelWithOverlay { slideAnim.running = true; if (weather) weather.startWeatherFetch(); + if (systemWidget) systemWidget.panelVisible = true; + if (quickAccessWidget) quickAccessWidget.panelVisible = true; + } } function hidePopup() { - if (shell && shell.settingsWindow && shell.settingsWindow.visible) { + if (shell && shell.settingsWindow && shell.settingsWindow.visible) shell.settingsWindow.visible = false; - } - if (wifiPanelLoader.active && wifiPanelLoader.item && wifiPanelLoader.item.visible) { + if (wifiPanelLoader.active && wifiPanelLoader.item && wifiPanelLoader.item.visible) wifiPanelLoader.item.visible = false; - } - if (bluetoothPanelLoader.active && bluetoothPanelLoader.item && bluetoothPanelLoader.item.visible) { + + if (bluetoothPanelLoader.active && bluetoothPanelLoader.item && bluetoothPanelLoader.item.visible) bluetoothPanelLoader.item.visible = false; - } + if (sidebarPopup.visible) { slideAnim.from = 0; slideAnim.to = width; @@ -84,37 +86,87 @@ 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; + 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; + } + + implicitWidth: 500 + implicitHeight: 800 + 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; - 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 @@ -126,68 +178,69 @@ PanelWithOverlay { Behavior on x { enabled: !sidebarPopupRect.isAnimating + NumberAnimation { duration: 300 easing.type: Easing.OutCubic } - } - } - // Access the shell's SettingsWindow instead of creating a new one + } + + } // LazyLoader for WifiPanel LazyLoader { id: wifiPanelLoader + loading: false - component: WifiPanel {} + + component: WifiPanel { + } + } // LazyLoader for BluetoothPanel LazyLoader { id: bluetoothPanelLoader + loading: false - component: BluetoothPanel {} + + component: BluetoothPanel { + } + } - - // SettingsIcon component SettingsIcon { id: settingsModal + onWeatherRefreshRequested: { - if (weather && weather.fetchCityWeather) { + 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 - System { + PowerMenu { id: systemWidget + Layout.alignment: Qt.AlignHCenter z: 3 } Weather { id: weather + Layout.alignment: Qt.AlignHCenter z: 2 } @@ -204,8 +257,10 @@ PanelWithOverlay { SystemMonitor { id: systemMonitor + z: 2 } + } // Power profile, Wifi and Bluetooth row @@ -236,6 +291,7 @@ PanelWithOverlay { // Wifi button Rectangle { id: wifiButton + width: 36 height: 36 radius: 18 @@ -255,16 +311,17 @@ PanelWithOverlay { MouseArea { id: wifiButtonArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - if (!wifiPanelLoader.active) { + if (!wifiPanelLoader.active) wifiPanelLoader.loading = true; - } - if (wifiPanelLoader.item) { + + if (wifiPanelLoader.item) wifiPanelLoader.item.showAt(); - } + } } @@ -273,11 +330,13 @@ PanelWithOverlay { targetItem: wifiButtonArea tooltipVisible: wifiButtonArea.containsMouse } + } // Bluetooth button Rectangle { id: bluetoothButton + width: 36 height: 36 radius: 18 @@ -297,16 +356,17 @@ PanelWithOverlay { MouseArea { id: bluetoothButtonArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - if (!bluetoothPanelLoader.active) { + if (!bluetoothPanelLoader.active) bluetoothPanelLoader.loading = true; - } - if (bluetoothPanelLoader.item) { + + if (bluetoothPanelLoader.item) bluetoothPanelLoader.item.showAt(); - } + } } @@ -315,9 +375,13 @@ PanelWithOverlay { targetItem: bluetoothButtonArea tooltipVisible: bluetoothButtonArea.containsMouse } + } + } + } + } Item { @@ -326,101 +390,60 @@ PanelWithOverlay { // QuickAccess widget QuickAccess { + // 6 is the wallpaper tab index + id: quickAccessWidget + Layout.alignment: Qt.AlignHCenter Layout.topMargin: -16 z: 2 isRecording: sidebarPopupRect.isRecording - onRecordingRequested: { sidebarPopupRect.startRecording(); } - onStopRecordingRequested: { sidebarPopupRect.stopRecording(); } - - onRecordingStateMismatch: function (actualState) { + onRecordingStateMismatch: function(actualState) { isRecording = actualState; quickAccessWidget.isRecording = actualState; } - onSettingsRequested: { // Use the SettingsModal's openSettings function - if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) { + if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) settingsModal.openSettings(); - } - } + } onWallpaperSelectorRequested: { // Use the SettingsModal's openSettings function with wallpaper tab (index 6) - if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) { - settingsModal.openSettings(6); // 6 is the wallpaper tab index - } + if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) + settingsModal.openSettings(6); + } } + } - Keys.onEscapePressed: sidebarPopupRect.hidePopup() - } - // Recording properties - property bool isRecording: false + Behavior on x { + enabled: !sidebarPopupRect.isAnimating - // 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(); + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } - 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; - 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(); - } } Loader { active: Settings.settings.showCorners anchors.fill: parent + sourceComponent: Item { Corners { id: sidebarCornerLeft + position: "bottomright" size: 1.1 fillColor: Theme.backgroundPrimary @@ -430,15 +453,19 @@ PanelWithOverlay { Behavior on offsetX { enabled: !sidebarPopupRect.isAnimating + NumberAnimation { duration: 300 easing.type: Easing.OutCubic } + } + } Corners { id: sidebarCornerBottom + position: "bottomright" size: 1.1 fillColor: Theme.backgroundPrimary @@ -448,13 +475,20 @@ PanelWithOverlay { Behavior on offsetX { enabled: !sidebarPopupRect.isAnimating + NumberAnimation { duration: 300 easing.type: Easing.OutCubic } + } + } + } + } + } + } diff --git a/Widgets/SidePanel/System.qml b/Widgets/SidePanel/PowerMenu.qml similarity index 97% rename from Widgets/SidePanel/System.qml rename to Widgets/SidePanel/PowerMenu.qml index 1ffe456..12d6da9 100644 --- a/Widgets/SidePanel/System.qml +++ b/Widgets/SidePanel/PowerMenu.qml @@ -1,26 +1,64 @@ 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 + + property string uptimeText: "--:--" + property bool panelVisible: false + + function logout() { + if (WorkspaceManager.isNiri) + logoutProcessNiri.running = true; + else if (WorkspaceManager.isHyprland) + logoutProcessHyprland.running = true; + else + console.warn("No supported compositor detected for logout"); + } + + function suspend() { + suspendProcess.running = true; + } + + function shutdown() { + shutdownProcess.running = true; + } + + function reboot() { + rebootProcess.running = true; + } + + function updateSystemInfo() { + uptimeProcess.running = true; + } + width: 440 height: 80 color: "transparent" anchors.horizontalCenterOffset: -2 + onPanelVisibleChanged: { + if (panelVisible) + updateSystemInfo(); + + } + Component.onCompleted: { + uptimeProcess.running = true; + } Rectangle { id: card + anchors.fill: parent color: Theme.surface radius: 18 @@ -30,19 +68,16 @@ Rectangle { anchors.margins: 18 spacing: 12 - RowLayout { Layout.fillWidth: true spacing: 12 - Rectangle { width: 48 height: 48 radius: 24 color: Theme.accentPrimary - Rectangle { anchors.fill: parent color: "transparent" @@ -52,10 +87,11 @@ Rectangle { z: 2 } - Avatar {} + Avatar { + } + } - ColumnLayout { spacing: 4 Layout.fillWidth: true @@ -74,16 +110,16 @@ Rectangle { font.pixelSize: 12 color: Theme.textSecondary } + } - Item { Layout.fillWidth: true } - Rectangle { id: systemButton + width: 32 height: 32 radius: 16 @@ -101,6 +137,7 @@ Rectangle { MouseArea { id: systemButtonArea + anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true @@ -108,24 +145,30 @@ Rectangle { systemMenu.visible = !systemMenu.visible; } } + StyledTooltip { id: systemTooltip - text: "System" + + text: "Power Menu" targetItem: systemButton tooltipVisible: systemButtonArea.containsMouse } + } + } + } + } PanelWithOverlay { id: systemMenu + anchors.top: systemButton.bottom anchors.right: systemButton.right Rectangle { - width: 160 height: 220 color: Theme.surface @@ -136,17 +179,19 @@ Rectangle { z: 9999 anchors.top: parent.top anchors.right: parent.right - - anchors.rightMargin: 32 anchors.topMargin: systemButton.y + systemButton.height + 48 + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + ColumnLayout { anchors.fill: parent anchors.margins: 8 spacing: 4 - Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 @@ -172,10 +217,12 @@ Rectangle { color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary Layout.fillWidth: true } + } MouseArea { id: lockButtonArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -184,9 +231,9 @@ Rectangle { systemMenu.visible = false; } } + } - Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 @@ -211,10 +258,12 @@ Rectangle { color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary Layout.fillWidth: true } + } MouseArea { id: suspendButtonArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -223,9 +272,9 @@ Rectangle { systemMenu.visible = false; } } + } - Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 @@ -251,10 +300,12 @@ Rectangle { color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary Layout.fillWidth: true } + } MouseArea { id: rebootButtonArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -263,9 +314,9 @@ Rectangle { systemMenu.visible = false; } } + } - Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 @@ -290,10 +341,12 @@ Rectangle { color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary Layout.fillWidth: true } + } MouseArea { id: logoutButtonArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -302,9 +355,9 @@ Rectangle { systemMenu.visible = false; } } + } - Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 @@ -329,10 +382,12 @@ Rectangle { color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary Layout.fillWidth: true } + } MouseArea { id: shutdownButtonArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -341,96 +396,72 @@ Rectangle { systemMenu.visible = false; } } + } + } + } + } - - property string uptimeText: "--:--" - - 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 } - function logout() { - if (WorkspaceManager.isNiri) { - logoutProcessNiri.running = true; - } else if (WorkspaceManager.isHyprland) { - logoutProcessHyprland.running = true; - } else { - - console.warn("No supported compositor detected for logout"); - } - } - - function suspend() { - suspendProcess.running = true; - } - - function shutdown() { - shutdownProcess.running = true; - } - - function reboot() { - rebootProcess.running = true; - } - - property bool panelVisible: false - - - onPanelVisibleChanged: { - if (panelVisible) { - updateSystemInfo(); - } - } - - Timer { interval: 60000 repeat: true @@ -438,16 +469,8 @@ Rectangle { onTriggered: updateSystemInfo() } - Component.onCompleted: { - uptimeProcess.running = true; - } - - function updateSystemInfo() { - uptimeProcess.running = true; - } - - LockScreen { id: lockScreen } + }