Bugfix: PanelWithOverlay would close when clicking in the background of the panel.

This commit is contained in:
quadbyte 2025-08-06 20:41:39 -04:00
parent 0b5f1cd9e5
commit 8c7f6e491d
6 changed files with 543 additions and 314 deletions

View file

@ -7,13 +7,67 @@ import qs.Settings
PanelWithOverlay { PanelWithOverlay {
id: ioSelector id: ioSelector
signal panelClosed()
property int tabIndex: 0 property int tabIndex: 0
property Item anchorItem: null 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 // Bind all Pipewire nodes so their properties are valid
PwObjectTracker { PwObjectTracker {
id: nodeTracker id: nodeTracker
objects: Pipewire.nodes objects: Pipewire.nodes
} }
@ -27,6 +81,11 @@ PanelWithOverlay {
anchors.topMargin: 4 anchors.topMargin: 4
anchors.rightMargin: 4 anchors.rightMargin: 4
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 16 anchors.margins: 16
@ -40,45 +99,59 @@ PanelWithOverlay {
Tabs { Tabs {
id: ioTabs id: ioTabs
tabsModel: [
{ label: "Output", icon: "volume_up" }, tabsModel: [{
{ label: "Input", icon: "mic" } "label": "Output",
] "icon": "volume_up"
}, {
"label": "Input",
"icon": "mic"
}]
currentIndex: tabIndex currentIndex: tabIndex
onTabChanged: { onTabChanged: {
tabIndex = currentIndex; tabIndex = currentIndex;
} }
} }
} }
// Add vertical space between tabs and entries // Add vertical space between tabs and entries
Item { height: 36; Layout.fillWidth: true } Item {
height: 36
Layout.fillWidth: true
}
// Output Devices // Output Devices
Flickable { Flickable {
id: sinkList id: sinkList
visible: tabIndex === 0 visible: tabIndex === 0
contentHeight: sinkColumn.height contentHeight: sinkColumn.height
clip: true clip: true
interactive: contentHeight > height interactive: contentHeight > height
width: parent.width width: parent.width
height: 220 height: 220
ScrollBar.vertical: ScrollBar {}
ColumnLayout { ColumnLayout {
id: sinkColumn id: sinkColumn
width: sinkList.width width: sinkList.width
spacing: 6 spacing: 6
Repeater { Repeater {
model: ioSelector.sinkNodes() model: ioSelector.sinkNodes()
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 36 height: 36
color: "transparent" color: "transparent"
radius: 6 radius: 6
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 6 anchors.margins: 6
spacing: 8 spacing: 8
Text { Text {
text: "volume_up" text: "volume_up"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
@ -86,10 +159,12 @@ PanelWithOverlay {
color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 1 spacing: 1
Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button
Text { Text {
text: modelData.nickname || modelData.description || modelData.name text: modelData.nickname || modelData.description || modelData.name
font.bold: true font.bold: true
@ -99,6 +174,7 @@ PanelWithOverlay {
maximumLineCount: 1 maximumLineCount: 1
Layout.fillWidth: true Layout.fillWidth: true
} }
Text { Text {
text: modelData.description !== modelData.nickname ? modelData.description : "" text: modelData.description !== modelData.nickname ? modelData.description : ""
font.pixelSize: 10 font.pixelSize: 10
@ -107,15 +183,19 @@ PanelWithOverlay {
maximumLineCount: 1 maximumLineCount: 1
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
Rectangle { Rectangle {
visible: Pipewire.preferredDefaultAudioSink !== modelData visible: Pipewire.preferredDefaultAudioSink !== modelData
width: 60; height: 20 width: 60
height: 20
radius: 4 radius: 4
color: Theme.accentPrimary color: Theme.accentPrimary
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "Set" text: "Set"
@ -123,12 +203,15 @@ PanelWithOverlay {
font.pixelSize: 10 font.pixelSize: 10
font.bold: true font.bold: true
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: Pipewire.preferredDefaultAudioSink = modelData onClicked: Pipewire.preferredDefaultAudioSink = modelData
} }
} }
Text { Text {
text: "(Current)" text: "(Current)"
visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id
@ -136,37 +219,51 @@ PanelWithOverlay {
font.pixelSize: 10 font.pixelSize: 10
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
} }
} }
} }
} }
ScrollBar.vertical: ScrollBar {
}
} }
// Input Devices // Input Devices
Flickable { Flickable {
id: sourceList id: sourceList
visible: tabIndex === 1 visible: tabIndex === 1
contentHeight: sourceColumn.height contentHeight: sourceColumn.height
clip: true clip: true
interactive: contentHeight > height interactive: contentHeight > height
width: parent.width width: parent.width
height: 220 height: 220
ScrollBar.vertical: ScrollBar {}
ColumnLayout { ColumnLayout {
id: sourceColumn id: sourceColumn
width: sourceList.width width: sourceList.width
spacing: 6 spacing: 6
Repeater { Repeater {
model: ioSelector.sourceNodes() model: ioSelector.sourceNodes()
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 36 height: 36
color: "transparent" color: "transparent"
radius: 6 radius: 6
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 6 anchors.margins: 6
spacing: 8 spacing: 8
Text { Text {
text: "mic" text: "mic"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
@ -174,10 +271,12 @@ PanelWithOverlay {
color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 1 spacing: 1
Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button
Text { Text {
text: modelData.nickname || modelData.description || modelData.name text: modelData.nickname || modelData.description || modelData.name
font.bold: true font.bold: true
@ -187,6 +286,7 @@ PanelWithOverlay {
maximumLineCount: 1 maximumLineCount: 1
Layout.fillWidth: true Layout.fillWidth: true
} }
Text { Text {
text: modelData.description !== modelData.nickname ? modelData.description : "" text: modelData.description !== modelData.nickname ? modelData.description : ""
font.pixelSize: 10 font.pixelSize: 10
@ -195,15 +295,19 @@ PanelWithOverlay {
maximumLineCount: 1 maximumLineCount: 1
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
Rectangle { Rectangle {
visible: Pipewire.preferredDefaultAudioSource !== modelData visible: Pipewire.preferredDefaultAudioSource !== modelData
width: 60; height: 20 width: 60
height: 20
radius: 4 radius: 4
color: Theme.accentPrimary color: Theme.accentPrimary
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "Set" text: "Set"
@ -211,12 +315,15 @@ PanelWithOverlay {
font.pixelSize: 10 font.pixelSize: 10
font.bold: true font.bold: true
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: Pipewire.preferredDefaultAudioSource = modelData onClicked: Pipewire.preferredDefaultAudioSource = modelData
} }
} }
Text { Text {
text: "(Current)" text: "(Current)"
visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id
@ -224,55 +331,25 @@ PanelWithOverlay {
font.pixelSize: 10 font.pixelSize: 10
Layout.alignment: Qt.AlignVCenter 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;
} }
Component.onCompleted: {
if (Pipewire.nodes && Pipewire.nodes.values) {
for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
var n = Pipewire.nodes.values[i];
} }
} }
ScrollBar.vertical: ScrollBar {
}
}
}
} }
Connections { Connections {
target: Pipewire
function onReadyChanged() { function onReadyChanged() {
if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) { if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) {
for (var i = 0; i < Pipewire.nodes.values.length; ++i) { for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
@ -280,15 +357,14 @@ PanelWithOverlay {
} }
} }
} }
function onDefaultAudioSinkChanged() { function onDefaultAudioSinkChanged() {
} }
function onDefaultAudioSourceChanged() { function onDefaultAudioSourceChanged() {
} }
target: Pipewire
} }
Component.onDestruction: {
}
onVisibleChanged: {
if (!visible) panelClosed();
}
} }

View file

@ -1,11 +1,11 @@
import "../../Helpers/Holidays.js" as Holidays
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Wayland
import qs.Components import qs.Components
import qs.Settings import qs.Settings
import Quickshell.Wayland
import "../../Helpers/Holidays.js" as Holidays
PanelWithOverlay { PanelWithOverlay {
id: calendarOverlay id: calendarOverlay
@ -22,6 +22,11 @@ PanelWithOverlay {
anchors.topMargin: 4 anchors.topMargin: 4
anchors.rightMargin: 4 anchors.rightMargin: 4
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 16 anchors.margins: 16
@ -60,6 +65,7 @@ PanelWithOverlay {
calendar.month = newDate.getMonth(); calendar.month = newDate.getMonth();
} }
} }
} }
DayOfWeekRow { DayOfWeekRow {
@ -67,6 +73,7 @@ PanelWithOverlay {
spacing: 0 spacing: 0
Layout.leftMargin: 8 // Align with grid Layout.leftMargin: 8 // Align with grid
Layout.rightMargin: 8 Layout.rightMargin: 8
delegate: Text { delegate: Text {
text: shortName text: shortName
color: Theme.textPrimary color: Theme.textPrimary
@ -77,16 +84,11 @@ PanelWithOverlay {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
width: 32 width: 32
} }
} }
MonthGrid { MonthGrid {
id: calendar id: calendar
Layout.fillWidth: true
Layout.leftMargin: 8
Layout.rightMargin: 8
spacing: 0
month: Time.date.getMonth()
year: Time.date.getFullYear()
property var holidays: [] property var holidays: []
@ -96,12 +98,19 @@ PanelWithOverlay {
calendar.holidays = holidays; calendar.holidays = holidays;
}); });
} }
Layout.fillWidth: true
Layout.leftMargin: 8
Layout.rightMargin: 8
spacing: 0
month: Time.date.getMonth()
year: Time.date.getFullYear()
onMonthChanged: updateHolidays() onMonthChanged: updateHolidays()
onYearChanged: updateHolidays() onYearChanged: updateHolidays()
Component.onCompleted: updateHolidays() Component.onCompleted: updateHolidays()
// Optionally, update when the panel becomes visible // Optionally, update when the panel becomes visible
Connections { Connections {
target: calendarOverlay
function onVisibleChanged() { function onVisibleChanged() {
if (calendarOverlay.visible) { if (calendarOverlay.visible) {
calendar.month = Time.date.getMonth(); calendar.month = Time.date.getMonth();
@ -109,29 +118,35 @@ PanelWithOverlay {
calendar.updateHolidays(); calendar.updateHolidays();
} }
} }
target: calendarOverlay
} }
delegate: Rectangle { delegate: Rectangle {
width: 32
height: 32
radius: 8
property var holidayInfo: calendar.holidays.filter(function(h) { property var holidayInfo: calendar.holidays.filter(function(h) {
var d = new Date(h.date); var d = new Date(h.date);
return d.getDate() === model.day && d.getMonth() === model.month && d.getFullYear() === model.year; return d.getDate() === model.day && d.getMonth() === model.month && d.getFullYear() === model.year;
}) })
property bool isHoliday: holidayInfo.length > 0 property bool isHoliday: holidayInfo.length > 0
width: 32
height: 32
radius: 8
color: { color: {
if (model.today) if (model.today)
return Theme.accentPrimary; return Theme.accentPrimary;
if (mouseArea2.containsMouse) if (mouseArea2.containsMouse)
return Theme.backgroundTertiary; return Theme.backgroundTertiary;
return "transparent"; return "transparent";
} }
// Holiday dot indicator // Holiday dot indicator
Rectangle { Rectangle {
visible: isHoliday visible: isHoliday
width: 4; height: 4 width: 4
height: 4
radius: 4 radius: 4
color: Theme.accentTertiary color: Theme.accentTertiary
anchors.top: parent.top anchors.top: parent.top
@ -145,7 +160,7 @@ PanelWithOverlay {
anchors.centerIn: parent anchors.centerIn: parent
text: model.day text: model.day
color: model.today ? Theme.onAccent : Theme.textPrimary 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.pixelSize: 13
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.bold: model.today ? true : false font.bold: model.today ? true : false
@ -153,6 +168,7 @@ PanelWithOverlay {
MouseArea { MouseArea {
id: mouseArea2 id: mouseArea2
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onEntered: { onEntered: {
@ -167,21 +183,28 @@ PanelWithOverlay {
onExited: holidayTooltip.tooltipVisible = false onExited: holidayTooltip.tooltipVisible = false
} }
Behavior on color {
ColorAnimation {
duration: 150
}
}
StyledTooltip { StyledTooltip {
id: holidayTooltip id: holidayTooltip
text: "" text: ""
tooltipVisible: false tooltipVisible: false
targetItem: null targetItem: null
delay: 100 delay: 100
} }
Behavior on color {
ColorAnimation {
duration: 150
} }
} }
} }
} }
}
}
} }

View file

@ -1,61 +1,31 @@
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Settings
import QtQuick.Layouts
import qs.Components import qs.Components
import qs.Settings
PanelWithOverlay { PanelWithOverlay {
id: notificationHistoryWin id: notificationHistoryWin
property string historyFilePath: Settings.settingsDir + "notification_history.json" property string historyFilePath: Settings.settingsDir + "notification_history.json"
property bool hasUnread: notificationHistoryWinRect.hasUnread && !notificationHistoryWinRect.visible property bool hasUnread: notificationHistoryWinRect.hasUnread && !notificationHistoryWinRect.visible
function addToHistory(notification) { notificationHistoryWinRect.addToHistory(notification) }
function addToHistory(notification) {
notificationHistoryWinRect.addToHistory(notification);
}
Rectangle { Rectangle {
id: notificationHistoryWinRect id: notificationHistoryWinRect
implicitWidth: 400
property int maxPopupHeight: 800 property int maxPopupHeight: 800
property int minPopupHeight: 210 property int minPopupHeight: 210
property int contentHeight: headerRow.height + historyList.contentHeight + 56 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 int maxHistory: 100
property bool hasUnread: false property bool hasUnread: false
signal unreadChanged(bool hasUnread) 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() { function updateHasUnread() {
var unread = false; var unread = false;
for (let i = 0; i < historyModel.count; ++i) { for (let i = 0; i < historyModel.count; ++i) {
@ -80,9 +50,11 @@ PanelWithOverlay {
if (typeof n === 'object' && n !== null) { if (typeof n === 'object' && n !== null) {
if (n.read === undefined) if (n.read === undefined)
n.read = false; n.read = false;
// Mark as read if window is open // Mark as read if window is open
if (notificationHistoryWinRect.visible) if (notificationHistoryWinRect.visible)
n.read = true; n.read = true;
historyModel.append(n); historyModel.append(n);
} }
} }
@ -95,19 +67,19 @@ PanelWithOverlay {
const count = Math.min(historyModel.count, maxHistory); const count = Math.min(historyModel.count, maxHistory);
for (let i = 0; i < count; ++i) { for (let i = 0; i < count; ++i) {
let obj = historyModel.get(i); let obj = historyModel.get(i);
if (typeof obj === 'object' && obj !== null) { if (typeof obj === 'object' && obj !== null)
historyArray.push({ historyArray.push({
id: obj.id, "id": obj.id,
appName: obj.appName, "appName": obj.appName,
summary: obj.summary, "summary": obj.summary,
body: obj.body, "body": obj.body,
timestamp: obj.timestamp, "timestamp": obj.timestamp,
read: obj.read === undefined ? false : obj.read "read": obj.read === undefined ? false : obj.read
}); });
}
} }
historyAdapter.notifications = historyArray; historyAdapter.notifications = historyArray;
Qt.callLater(function () { Qt.callLater(function() {
historyFileView.writeAdapter(); historyFileView.writeAdapter();
}); });
updateHasUnread(); updateHasUnread();
@ -116,12 +88,12 @@ PanelWithOverlay {
function addToHistory(notification) { function addToHistory(notification) {
if (!notification.id) if (!notification.id)
notification.id = Date.now(); notification.id = Date.now();
if (!notification.timestamp) if (!notification.timestamp)
notification.timestamp = new Date().toISOString(); notification.timestamp = new Date().toISOString();
// Mark as read if window is open // Mark as read if window is open
notification.read = visible; notification.read = visible;
// Remove duplicate by id // Remove duplicate by id
for (let i = 0; i < historyModel.count; ++i) { for (let i = 0; i < historyModel.count; ++i) {
if (historyModel.get(i).id === notification.id) { if (historyModel.get(i).id === notification.id) {
@ -129,11 +101,10 @@ PanelWithOverlay {
break; break;
} }
} }
historyModel.insert(0, notification); historyModel.insert(0, notification);
if (historyModel.count > maxHistory) if (historyModel.count > maxHistory)
historyModel.remove(maxHistory); historyModel.remove(maxHistory);
saveHistory(); saveHistory();
} }
@ -146,6 +117,7 @@ PanelWithOverlay {
function formatTimestamp(ts) { function formatTimestamp(ts) {
if (!ts) if (!ts)
return ""; return "";
var date = typeof ts === "number" ? new Date(ts) : new Date(Date.parse(ts)); var date = typeof ts === "number" ? new Date(ts) : new Date(Date.parse(ts));
var y = date.getFullYear(); var y = date.getFullYear();
var m = (date.getMonth() + 1).toString().padStart(2, '0'); var m = (date.getMonth() + 1).toString().padStart(2, '0');
@ -155,6 +127,15 @@ PanelWithOverlay {
return `${y}-${m}-${d} ${h}:${min}`; 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: { onVisibleChanged: {
if (visible) { if (visible) {
// Mark all as read when popup is opened // Mark all as read when popup is opened
@ -167,9 +148,46 @@ PanelWithOverlay {
} }
if (changed) if (changed)
saveHistory(); 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 { Rectangle {
width: notificationHistoryWinRect.width width: notificationHistoryWinRect.width
height: notificationHistoryWinRect.height height: notificationHistoryWinRect.height
@ -184,6 +202,7 @@ PanelWithOverlay {
RowLayout { RowLayout {
id: headerRow id: headerRow
spacing: 4 spacing: 4
anchors.topMargin: 4 anchors.topMargin: 4
anchors.left: parent.left anchors.left: parent.left
@ -193,6 +212,7 @@ PanelWithOverlay {
Layout.preferredHeight: 52 Layout.preferredHeight: 52
anchors.leftMargin: 16 anchors.leftMargin: 16
anchors.rightMargin: 16 anchors.rightMargin: 16
Text { Text {
text: "Notification History" text: "Notification History"
font.pixelSize: 18 font.pixelSize: 18
@ -200,11 +220,14 @@ PanelWithOverlay {
color: Theme.textPrimary color: Theme.textPrimary
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }
Rectangle { Rectangle {
id: clearAllButton id: clearAllButton
width: 90 width: 90
height: 32 height: 32
radius: 16 radius: 16
@ -212,9 +235,11 @@ PanelWithOverlay {
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Row { Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 6 spacing: 6
Text { Text {
text: "delete_sweep" text: "delete_sweep"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
@ -222,6 +247,7 @@ PanelWithOverlay {
color: clearAllMouseArea.containsMouse ? Theme.onAccent : Theme.accentPrimary color: clearAllMouseArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
Text { Text {
text: "Clear" text: "Clear"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@ -229,15 +255,20 @@ PanelWithOverlay {
font.bold: true font.bold: true
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
} }
MouseArea { MouseArea {
id: clearAllMouseArea id: clearAllMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: notificationHistoryWinRect.clearHistory() onClicked: notificationHistoryWinRect.clearHistory()
} }
} }
} }
Rectangle { Rectangle {
@ -261,29 +292,36 @@ PanelWithOverlay {
radius: 20 radius: 20
z: 0 z: 0
} }
Rectangle { Rectangle {
id: listContainer id: listContainer
anchors.fill: parent anchors.fill: parent
anchors.topMargin: 12 anchors.topMargin: 12
anchors.bottomMargin: 12 anchors.bottomMargin: 12
color: "transparent" color: "transparent"
clip: true clip: true
Column { Column {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
ListView { ListView {
id: historyList id: historyList
width: parent.width width: parent.width
height: Math.min(contentHeight, parent.height) height: Math.min(contentHeight, parent.height)
spacing: 12 spacing: 12
model: historyModel.count > 0 ? historyModel : placeholderModel model: historyModel.count > 0 ? historyModel : placeholderModel
clip: true clip: true
delegate: Item { delegate: Item {
width: parent.width width: parent.width
height: notificationCard.implicitHeight + 12 height: notificationCard.implicitHeight + 12
Rectangle { Rectangle {
id: notificationCard id: notificationCard
width: parent.width - 24 width: parent.width - 24
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: Theme.backgroundPrimary color: Theme.backgroundPrimary
@ -292,16 +330,22 @@ PanelWithOverlay {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.margins: 0 anchors.margins: 0
implicitHeight: contentColumn.implicitHeight + 20 implicitHeight: contentColumn.implicitHeight + 20
Column { Column {
id: contentColumn id: contentColumn
anchors.fill: parent anchors.fill: parent
anchors.margins: 14 anchors.margins: 14
spacing: 6 spacing: 6
RowLayout { RowLayout {
id: headerRow2 id: headerRow2
spacing: 8 spacing: 8
Rectangle { Rectangle {
id: iconBackground id: iconBackground
width: 28 width: 28
height: 28 height: 28
radius: 20 radius: 20
@ -309,6 +353,7 @@ PanelWithOverlay {
border.color: Qt.darker(Theme.accentPrimary, 1.2) border.color: Qt.darker(Theme.accentPrimary, 1.2)
border.width: 1.2 border.width: 1.2
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: model.appName ? model.appName.charAt(0).toUpperCase() : "?" text: model.appName ? model.appName.charAt(0).toUpperCase() : "?"
@ -317,11 +362,15 @@ PanelWithOverlay {
font.bold: true font.bold: true
color: Theme.backgroundPrimary color: Theme.backgroundPrimary
} }
} }
Column { Column {
id: appInfoColumn id: appInfoColumn
spacing: 0 spacing: 0
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Text { Text {
text: model.appName || "No Notifications" text: model.appName || "No Notifications"
font.bold: true font.bold: true
@ -330,6 +379,7 @@ PanelWithOverlay {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
Text { Text {
visible: !model.isPlaceholder visible: !model.isPlaceholder
text: model.timestamp ? notificationHistoryWinRect.formatTimestamp(model.timestamp) : "" text: model.timestamp ? notificationHistoryWinRect.formatTimestamp(model.timestamp) : ""
@ -338,11 +388,15 @@ PanelWithOverlay {
font.pixelSize: Theme.fontSizeCaption font.pixelSize: Theme.fontSizeCaption
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
} }
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
Text { Text {
text: model.summary || (model.isPlaceholder ? "You're all caught up!" : "") text: model.summary || (model.isPlaceholder ? "You're all caught up!" : "")
color: Theme.textSecondary color: Theme.textSecondary
@ -351,6 +405,7 @@ PanelWithOverlay {
width: parent.width width: parent.width
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
Text { Text {
text: model.body || (model.isPlaceholder ? "No notifications to show." : "") text: model.body || (model.isPlaceholder ? "No notifications to show." : "")
color: Theme.textDisabled color: Theme.textDisabled
@ -359,12 +414,19 @@ PanelWithOverlay {
width: parent.width width: parent.width
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
} }
} }
} }
} }
} }
} }
} }
Rectangle { Rectangle {
@ -375,14 +437,20 @@ PanelWithOverlay {
ListModel { ListModel {
id: placeholderModel id: placeholderModel
ListElement { ListElement {
appName: "" appName: ""
summary: "" summary: ""
body: "" body: ""
isPlaceholder: true isPlaceholder: true
} }
} }
} }
} }
} }
} }

View file

@ -155,6 +155,11 @@ PanelWithOverlay {
// Center the settings window on screen // Center the settings window on screen
anchors.centerIn: parent anchors.centerIn: parent
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
Rectangle { Rectangle {
id: background id: background

View file

@ -3,23 +3,14 @@ import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import qs.Components
import qs.Settings import qs.Settings
import qs.Widgets.SettingsWindow import qs.Widgets.SettingsWindow
import qs.Components
PanelWithOverlay { PanelWithOverlay {
id: sidebarPopup id: sidebarPopup
property var shell: null
// Trigger initial weather loading when component is completed property var shell: null
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();
}
});
}
function showAt() { function showAt() {
sidebarPopupRect.showAt(); sidebarPopupRect.showAt();
@ -37,18 +28,27 @@ PanelWithOverlay {
sidebarPopupRect.hidePopup(); sidebarPopupRect.hidePopup();
} }
Rectangle { // Trigger initial weather loading when component is completed
id: sidebarPopupRect Component.onCompleted: {
implicitWidth: 500 // Load initial weather data after a short delay to ensure all components are ready
implicitHeight: 800 Qt.callLater(function() {
visible: parent.visible if (weather && weather.fetchCityWeather)
color: "transparent" weather.fetchCityWeather();
anchors.top: parent.top
anchors.right: parent.right
});
}
Rectangle {
// Access the shell's SettingsWindow instead of creating a new one
id: sidebarPopupRect
property real slideOffset: width property real slideOffset: width
property bool isAnimating: false property bool isAnimating: false
property int leftPadding: 20
property int bottomPadding: 20
// Recording properties
property bool isRecording: false
function showAt() { function showAt() {
if (!sidebarPopup.visible) { if (!sidebarPopup.visible) {
@ -59,24 +59,26 @@ PanelWithOverlay {
slideAnim.running = true; slideAnim.running = true;
if (weather) if (weather)
weather.startWeatherFetch(); weather.startWeatherFetch();
if (systemWidget) if (systemWidget)
systemWidget.panelVisible = true; systemWidget.panelVisible = true;
if (quickAccessWidget) if (quickAccessWidget)
quickAccessWidget.panelVisible = true; quickAccessWidget.panelVisible = true;
} }
} }
function hidePopup() { function hidePopup() {
if (shell && shell.settingsWindow && shell.settingsWindow.visible) { if (shell && shell.settingsWindow && shell.settingsWindow.visible)
shell.settingsWindow.visible = false; shell.settingsWindow.visible = false;
}
if (wifiPanelLoader.active && wifiPanelLoader.item && wifiPanelLoader.item.visible) { if (wifiPanelLoader.active && wifiPanelLoader.item && wifiPanelLoader.item.visible)
wifiPanelLoader.item.visible = false; wifiPanelLoader.item.visible = false;
}
if (bluetoothPanelLoader.active && bluetoothPanelLoader.item && bluetoothPanelLoader.item.visible) { if (bluetoothPanelLoader.active && bluetoothPanelLoader.item && bluetoothPanelLoader.item.visible)
bluetoothPanelLoader.item.visible = false; bluetoothPanelLoader.item.visible = false;
}
if (sidebarPopup.visible) { if (sidebarPopup.visible) {
slideAnim.from = 0; slideAnim.from = 0;
slideAnim.to = width; 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 { NumberAnimation {
id: slideAnim id: slideAnim
target: sidebarPopupRect target: sidebarPopupRect
property: "slideOffset" property: "slideOffset"
duration: 300 duration: 300
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
onStopped: { onStopped: {
if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) { if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) {
sidebarPopup.visible = false; sidebarPopup.visible = false;
if (weather) if (weather)
weather.stopWeatherFetch(); weather.stopWeatherFetch();
if (systemWidget) if (systemWidget)
systemWidget.panelVisible = false; systemWidget.panelVisible = false;
if (quickAccessWidget) if (quickAccessWidget)
quickAccessWidget.panelVisible = false; quickAccessWidget.panelVisible = false;
} }
sidebarPopupRect.isAnimating = false; sidebarPopupRect.isAnimating = false;
} }
onStarted: { onStarted: {
sidebarPopupRect.isAnimating = true; sidebarPopupRect.isAnimating = true;
} }
} }
property int leftPadding: 20
property int bottomPadding: 20
Rectangle { Rectangle {
id: mainRectangle id: mainRectangle
width: sidebarPopupRect.width - sidebarPopupRect.leftPadding width: sidebarPopupRect.width - sidebarPopupRect.leftPadding
height: sidebarPopupRect.height - sidebarPopupRect.bottomPadding height: sidebarPopupRect.height - sidebarPopupRect.bottomPadding
anchors.top: sidebarPopupRect.top anchors.top: sidebarPopupRect.top
@ -126,68 +178,69 @@ PanelWithOverlay {
Behavior on x { Behavior on x {
enabled: !sidebarPopupRect.isAnimating enabled: !sidebarPopupRect.isAnimating
NumberAnimation { NumberAnimation {
duration: 300 duration: 300
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
} }
}
} }
// Access the shell's SettingsWindow instead of creating a new one }
// LazyLoader for WifiPanel // LazyLoader for WifiPanel
LazyLoader { LazyLoader {
id: wifiPanelLoader id: wifiPanelLoader
loading: false loading: false
component: WifiPanel {}
component: WifiPanel {
}
} }
// LazyLoader for BluetoothPanel // LazyLoader for BluetoothPanel
LazyLoader { LazyLoader {
id: bluetoothPanelLoader id: bluetoothPanelLoader
loading: false loading: false
component: BluetoothPanel {}
component: BluetoothPanel {
} }
}
// SettingsIcon component // SettingsIcon component
SettingsIcon { SettingsIcon {
id: settingsModal id: settingsModal
onWeatherRefreshRequested: { onWeatherRefreshRequested: {
if (weather && weather.fetchCityWeather) { if (weather && weather.fetchCityWeather)
weather.fetchCityWeather(); weather.fetchCityWeather();
}
}
}
}
}
Item { Item {
anchors.fill: mainRectangle anchors.fill: mainRectangle
x: sidebarPopupRect.slideOffset x: sidebarPopupRect.slideOffset
Keys.onEscapePressed: sidebarPopupRect.hidePopup()
Behavior on x {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 20 anchors.margins: 20
spacing: 16 spacing: 16
System { PowerMenu {
id: systemWidget id: systemWidget
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
z: 3 z: 3
} }
Weather { Weather {
id: weather id: weather
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
z: 2 z: 2
} }
@ -204,8 +257,10 @@ PanelWithOverlay {
SystemMonitor { SystemMonitor {
id: systemMonitor id: systemMonitor
z: 2 z: 2
} }
} }
// Power profile, Wifi and Bluetooth row // Power profile, Wifi and Bluetooth row
@ -236,6 +291,7 @@ PanelWithOverlay {
// Wifi button // Wifi button
Rectangle { Rectangle {
id: wifiButton id: wifiButton
width: 36 width: 36
height: 36 height: 36
radius: 18 radius: 18
@ -255,16 +311,17 @@ PanelWithOverlay {
MouseArea { MouseArea {
id: wifiButtonArea id: wifiButtonArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (!wifiPanelLoader.active) { if (!wifiPanelLoader.active)
wifiPanelLoader.loading = true; wifiPanelLoader.loading = true;
}
if (wifiPanelLoader.item) { if (wifiPanelLoader.item)
wifiPanelLoader.item.showAt(); wifiPanelLoader.item.showAt();
}
} }
} }
@ -273,11 +330,13 @@ PanelWithOverlay {
targetItem: wifiButtonArea targetItem: wifiButtonArea
tooltipVisible: wifiButtonArea.containsMouse tooltipVisible: wifiButtonArea.containsMouse
} }
} }
// Bluetooth button // Bluetooth button
Rectangle { Rectangle {
id: bluetoothButton id: bluetoothButton
width: 36 width: 36
height: 36 height: 36
radius: 18 radius: 18
@ -297,16 +356,17 @@ PanelWithOverlay {
MouseArea { MouseArea {
id: bluetoothButtonArea id: bluetoothButtonArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (!bluetoothPanelLoader.active) { if (!bluetoothPanelLoader.active)
bluetoothPanelLoader.loading = true; bluetoothPanelLoader.loading = true;
}
if (bluetoothPanelLoader.item) { if (bluetoothPanelLoader.item)
bluetoothPanelLoader.item.showAt(); bluetoothPanelLoader.item.showAt();
}
} }
} }
@ -315,9 +375,13 @@ PanelWithOverlay {
targetItem: bluetoothButtonArea targetItem: bluetoothButtonArea
tooltipVisible: bluetoothButtonArea.containsMouse tooltipVisible: bluetoothButtonArea.containsMouse
} }
} }
} }
} }
} }
Item { Item {
@ -326,101 +390,60 @@ PanelWithOverlay {
// QuickAccess widget // QuickAccess widget
QuickAccess { QuickAccess {
// 6 is the wallpaper tab index
id: quickAccessWidget id: quickAccessWidget
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: -16 Layout.topMargin: -16
z: 2 z: 2
isRecording: sidebarPopupRect.isRecording isRecording: sidebarPopupRect.isRecording
onRecordingRequested: { onRecordingRequested: {
sidebarPopupRect.startRecording(); sidebarPopupRect.startRecording();
} }
onStopRecordingRequested: { onStopRecordingRequested: {
sidebarPopupRect.stopRecording(); sidebarPopupRect.stopRecording();
} }
onRecordingStateMismatch: function(actualState) {
onRecordingStateMismatch: function (actualState) {
isRecording = actualState; isRecording = actualState;
quickAccessWidget.isRecording = actualState; quickAccessWidget.isRecording = actualState;
} }
onSettingsRequested: { onSettingsRequested: {
// Use the SettingsModal's openSettings function // Use the SettingsModal's openSettings function
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) { if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings)
settingsModal.openSettings(); settingsModal.openSettings();
}
}
}
onWallpaperSelectorRequested: { onWallpaperSelectorRequested: {
// Use the SettingsModal's openSettings function with wallpaper tab (index 6) // Use the SettingsModal's openSettings function with wallpaper tab (index 6)
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) { if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings)
settingsModal.openSettings(6); // 6 is the wallpaper tab index settingsModal.openSettings(6);
} }
} }
}
}
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 " + 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 Behavior on x {
function stopRecording() { enabled: !sidebarPopupRect.isAnimating
Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]);
// Optionally, force kill after a delay NumberAnimation {
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', sidebarPopupRect); duration: 300
cleanupTimer.triggered.connect(function () { easing.type: Easing.OutCubic
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 { Loader {
active: Settings.settings.showCorners active: Settings.settings.showCorners
anchors.fill: parent anchors.fill: parent
sourceComponent: Item { sourceComponent: Item {
Corners { Corners {
id: sidebarCornerLeft id: sidebarCornerLeft
position: "bottomright" position: "bottomright"
size: 1.1 size: 1.1
fillColor: Theme.backgroundPrimary fillColor: Theme.backgroundPrimary
@ -430,15 +453,19 @@ PanelWithOverlay {
Behavior on offsetX { Behavior on offsetX {
enabled: !sidebarPopupRect.isAnimating enabled: !sidebarPopupRect.isAnimating
NumberAnimation { NumberAnimation {
duration: 300 duration: 300
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
} }
} }
} }
Corners { Corners {
id: sidebarCornerBottom id: sidebarCornerBottom
position: "bottomright" position: "bottomright"
size: 1.1 size: 1.1
fillColor: Theme.backgroundPrimary fillColor: Theme.backgroundPrimary
@ -448,13 +475,20 @@ PanelWithOverlay {
Behavior on offsetX { Behavior on offsetX {
enabled: !sidebarPopupRect.isAnimating enabled: !sidebarPopupRect.isAnimating
NumberAnimation { NumberAnimation {
duration: 300 duration: 300
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
} }
} }
} }
} }
} }
} }
} }

View file

@ -1,26 +1,64 @@
import QtQuick import QtQuick
import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Widgets import Quickshell.Widgets
import qs.Components
import qs.Helpers
import qs.Services
import qs.Settings import qs.Settings
import qs.Widgets import qs.Widgets
import qs.Widgets.LockScreen import qs.Widgets.LockScreen
import qs.Helpers
import qs.Services
import qs.Components
Rectangle { Rectangle {
id: systemWidget 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 width: 440
height: 80 height: 80
color: "transparent" color: "transparent"
anchors.horizontalCenterOffset: -2 anchors.horizontalCenterOffset: -2
onPanelVisibleChanged: {
if (panelVisible)
updateSystemInfo();
}
Component.onCompleted: {
uptimeProcess.running = true;
}
Rectangle { Rectangle {
id: card id: card
anchors.fill: parent anchors.fill: parent
color: Theme.surface color: Theme.surface
radius: 18 radius: 18
@ -30,19 +68,16 @@ Rectangle {
anchors.margins: 18 anchors.margins: 18
spacing: 12 spacing: 12
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 12 spacing: 12
Rectangle { Rectangle {
width: 48 width: 48
height: 48 height: 48
radius: 24 radius: 24
color: Theme.accentPrimary color: Theme.accentPrimary
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: "transparent" color: "transparent"
@ -52,9 +87,10 @@ Rectangle {
z: 2 z: 2
} }
Avatar {} Avatar {
} }
}
ColumnLayout { ColumnLayout {
spacing: 4 spacing: 4
@ -74,16 +110,16 @@ Rectangle {
font.pixelSize: 12 font.pixelSize: 12
color: Theme.textSecondary color: Theme.textSecondary
} }
}
}
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }
Rectangle { Rectangle {
id: systemButton id: systemButton
width: 32 width: 32
height: 32 height: 32
radius: 16 radius: 16
@ -101,6 +137,7 @@ Rectangle {
MouseArea { MouseArea {
id: systemButtonArea id: systemButtonArea
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
hoverEnabled: true hoverEnabled: true
@ -108,24 +145,30 @@ Rectangle {
systemMenu.visible = !systemMenu.visible; systemMenu.visible = !systemMenu.visible;
} }
} }
StyledTooltip { StyledTooltip {
id: systemTooltip id: systemTooltip
text: "System"
text: "Power Menu"
targetItem: systemButton targetItem: systemButton
tooltipVisible: systemButtonArea.containsMouse tooltipVisible: systemButtonArea.containsMouse
} }
} }
} }
} }
} }
PanelWithOverlay { PanelWithOverlay {
id: systemMenu id: systemMenu
anchors.top: systemButton.bottom anchors.top: systemButton.bottom
anchors.right: systemButton.right anchors.right: systemButton.right
Rectangle { Rectangle {
width: 160 width: 160
height: 220 height: 220
color: Theme.surface color: Theme.surface
@ -136,17 +179,19 @@ Rectangle {
z: 9999 z: 9999
anchors.top: parent.top anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 32 anchors.rightMargin: 32
anchors.topMargin: systemButton.y + systemButton.height + 48 anchors.topMargin: systemButton.y + systemButton.height + 48
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 8 anchors.margins: 8
spacing: 4 spacing: 4
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 36 Layout.preferredHeight: 36
@ -172,10 +217,12 @@ Rectangle {
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
MouseArea { MouseArea {
id: lockButtonArea id: lockButtonArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@ -184,8 +231,8 @@ Rectangle {
systemMenu.visible = false; systemMenu.visible = false;
} }
} }
}
}
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
@ -211,10 +258,12 @@ Rectangle {
color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
MouseArea { MouseArea {
id: suspendButtonArea id: suspendButtonArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@ -223,8 +272,8 @@ Rectangle {
systemMenu.visible = false; systemMenu.visible = false;
} }
} }
}
}
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
@ -251,10 +300,12 @@ Rectangle {
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
MouseArea { MouseArea {
id: rebootButtonArea id: rebootButtonArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@ -263,8 +314,8 @@ Rectangle {
systemMenu.visible = false; systemMenu.visible = false;
} }
} }
}
}
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
@ -290,10 +341,12 @@ Rectangle {
color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
MouseArea { MouseArea {
id: logoutButtonArea id: logoutButtonArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@ -302,8 +355,8 @@ Rectangle {
systemMenu.visible = false; systemMenu.visible = false;
} }
} }
}
}
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
@ -329,10 +382,12 @@ Rectangle {
color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
MouseArea { MouseArea {
id: shutdownButtonArea id: shutdownButtonArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@ -341,96 +396,72 @@ Rectangle {
systemMenu.visible = false; systemMenu.visible = false;
} }
} }
}
}
}
} }
}
property string uptimeText: "--:--" }
}
Process { Process {
id: uptimeProcess id: uptimeProcess
command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"] command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"]
running: false running: false
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
uptimeText = this.text.trim(); uptimeText = this.text.trim();
uptimeProcess.running = false; uptimeProcess.running = false;
} }
} }
} }
Process { Process {
id: shutdownProcess id: shutdownProcess
command: ["shutdown", "-h", "now"] command: ["shutdown", "-h", "now"]
running: false running: false
} }
Process { Process {
id: rebootProcess id: rebootProcess
command: ["reboot"] command: ["reboot"]
running: false running: false
} }
Process { Process {
id: suspendProcess id: suspendProcess
command: ["systemctl", "suspend"] command: ["systemctl", "suspend"]
running: false running: false
} }
Process { Process {
id: logoutProcessNiri id: logoutProcessNiri
command: ["niri", "msg", "action", "quit", "--skip-confirmation"] command: ["niri", "msg", "action", "quit", "--skip-confirmation"]
running: false running: false
} }
Process { Process {
id: logoutProcessHyprland id: logoutProcessHyprland
command: ["hyprctl", "dispatch", "exit"] command: ["hyprctl", "dispatch", "exit"]
running: false running: false
} }
Process { Process {
id: logoutProcess id: logoutProcess
command: ["loginctl", "terminate-user", Quickshell.env("USER")] command: ["loginctl", "terminate-user", Quickshell.env("USER")]
running: false 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 { Timer {
interval: 60000 interval: 60000
repeat: true repeat: true
@ -438,16 +469,8 @@ Rectangle {
onTriggered: updateSystemInfo() onTriggered: updateSystemInfo()
} }
Component.onCompleted: {
uptimeProcess.running = true;
}
function updateSystemInfo() {
uptimeProcess.running = true;
}
LockScreen { LockScreen {
id: lockScreen id: lockScreen
} }
} }