Merge branch 'dev'

This commit is contained in:
quadbyte 2025-08-07 15:46:49 -04:00
commit d2d993d621
70 changed files with 8102 additions and 2264 deletions

View file

@ -8,7 +8,9 @@ import qs.Components
PanelWindow {
id: taskbarWindow
visible: Settings.settings.showDock
visible: Settings.settings.showDock &&
(Settings.settings.dockMonitors.includes(modelData.name) ||
(Settings.settings.dockMonitors.length === 0))
screen: (typeof modelData !== 'undefined' ? modelData : null)
exclusionMode: ExclusionMode.Ignore
anchors.bottom: true
@ -245,7 +247,7 @@ PanelWindow {
contextMenuVisible = false;
contextMenuTarget = null;
contextMenuToplevel = null;
hidden = true; // Hide dock when context menu closes by clicking outside
hidden = true; // Hide dock when context menu closes
}
}
@ -281,7 +283,7 @@ PanelWindow {
spacing: 4
width: parent.width
// Close
Rectangle {
width: parent.width
height: 32
@ -300,7 +302,7 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
text: "close"
font.family: "Material Symbols Outlined"
font.pixelSize: 14
font.pixelSize: 14 * Theme.scale(Screen)
color: Theme.textPrimary
}
@ -308,7 +310,7 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
text: "Close"
font.family: Theme.fontFamily
font.pixelSize: 14
font.pixelSize: 14 * Theme.scale(Screen)
color: Theme.textPrimary
}
}
@ -322,7 +324,7 @@ PanelWindow {
onClicked: {
if (contextMenuToplevel?.close) contextMenuToplevel.close();
contextMenuVisible = false;
hidden = true; // Hide the dock here as well
hidden = true;
}
}
}

View file

@ -6,7 +6,7 @@ import qs.Components
import qs.Settings
Item {
// Test mode
property bool testMode: false
property int testPercent: 49
property bool testCharging: true
@ -21,7 +21,7 @@ Item {
height: row.height
visible: testMode || (isReady && battery.isLaptopBattery)
// Choose icon based on charge and charging state
function batteryIcon() {
if (!show)
return "";
@ -32,7 +32,7 @@ Item {
if (percent >= 95)
return "battery_android_full";
// Hardcoded battery symbols
if (percent >= 85)
return "battery_android_6";
if (percent >= 70)
@ -58,7 +58,7 @@ Item {
Text {
text: batteryIcon()
font.family: "Material Symbols Outlined"
font.pixelSize: 28
font.pixelSize: 28 * Theme.scale(Screen)
color: charging ? Theme.accentPrimary : Theme.textSecondary
verticalAlignment: Text.AlignVBottom
}
@ -66,7 +66,7 @@ Item {
Text {
text: Math.round(percent) + "%"
font.family: Theme.fontFamily
font.pixelSize: 18
font.pixelSize: 18 * Theme.scale(Screen)
color: Theme.textSecondary
verticalAlignment: Text.AlignVBottom
}

View file

@ -152,21 +152,21 @@ WlSessionLock {
ColumnLayout {
anchors.centerIn: parent
spacing: 30
width: Math.min(parent.width * 0.8, 400)
width: Math.min(parent.width * 0.8, 400 * Theme.scale(Screen))
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 80
height: 80
radius: 40
width: 80 * Theme.scale(Screen)
height: 80 * Theme.scale(Screen)
radius: 40 * Theme.scale(Screen)
color: Theme.accentPrimary
Rectangle {
anchors.fill: parent
color: "transparent"
radius: 40
radius: 40 * Theme.scale(Screen)
border.color: Theme.accentPrimary
border.width: 3
border.width: 3 * Theme.scale(Screen)
z: 2
}
@ -183,28 +183,28 @@ WlSessionLock {
Layout.alignment: Qt.AlignHCenter
text: Quickshell.env("USER")
font.family: Theme.fontFamily
font.pixelSize: 24
font.pixelSize: 24 * Theme.scale(Screen)
font.weight: Font.Medium
color: Theme.textPrimary
}
Rectangle {
Layout.fillWidth: true
height: 50
radius: 25
height: 50 * Theme.scale(Screen)
radius: 25 * Theme.scale(Screen)
color: Theme.surface
opacity: passwordInput.activeFocus ? 0.8 : 0.3
border.color: passwordInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 2
border.width: 2 * Theme.scale(Screen)
TextInput {
id: passwordInput
anchors.fill: parent
anchors.margins: 15
anchors.margins: 15 * Theme.scale(Screen)
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
font.family: Theme.fontFamily
font.pixelSize: 16
font.pixelSize: 16 * Theme.scale(Screen)
color: Theme.textPrimary
echoMode: TextInput.Password
passwordCharacter: "●"
@ -218,7 +218,7 @@ WlSessionLock {
text: "Enter password..."
color: Theme.textSecondary
font.family: Theme.fontFamily
font.pixelSize: 16
font.pixelSize: 16 * Theme.scale(Screen)
visible: !passwordInput.text && !passwordInput.activeFocus
}
@ -238,9 +238,9 @@ WlSessionLock {
id: errorMessageRect
Layout.alignment: Qt.AlignHCenter
width: parent.width * 0.8
height: 44
height: 44 * Theme.scale(Screen)
color: Theme.overlay
radius: 20
radius: 20 * Theme.scale(Screen)
visible: lock.errorMessage !== ""
Text {
@ -248,7 +248,7 @@ WlSessionLock {
text: lock.errorMessage
color: Theme.error
font.family: Theme.fontFamily
font.pixelSize: 14
font.pixelSize: 14 * Theme.scale(Screen)
opacity: 1
visible: lock.errorMessage !== ""
}
@ -256,13 +256,13 @@ WlSessionLock {
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 120
height: 44
radius: 20
width: 120 * Theme.scale(Screen)
height: 44 * Theme.scale(Screen)
radius: 20 * Theme.scale(Screen)
opacity: unlockButtonArea.containsMouse ? 0.8 : 0.5
color: unlockButtonArea.containsMouse ? Theme.accentPrimary : Theme.surface
border.color: Theme.accentPrimary
border.width: 2
border.width: 2 * Theme.scale(Screen)
enabled: !lock.authenticating
Text {
@ -270,7 +270,7 @@ WlSessionLock {
anchors.centerIn: parent
text: lock.authenticating ? "..." : "Unlock"
font.family: Theme.fontFamily
font.pixelSize: 16
font.pixelSize: 16 * Theme.scale(Screen)
font.bold: true
color: unlockButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
@ -294,37 +294,13 @@ WlSessionLock {
}
}
Corners {
id: topRightCorner
position: "bottomleft"
size: 1.3
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
offsetX: screen.width / 2 + 38
offsetY: 0
anchors.top: parent.top
visible: Settings.settings.showCorners
z: 50
}
Corners {
id: topLeftCorner
position: "bottomright"
size: 1.3
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
offsetX: -Screen.width / 2 - 38
offsetY: 0
anchors.top: parent.top
visible: Settings.settings.showCorners
z: 51
}
Rectangle {
width: infoColumn.width + 32
height: infoColumn.height + 8
width: infoColumn.width + 32 * Theme.scale(Screen)
height: infoColumn.height + 8 * Theme.scale(Screen)
color: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
anchors.horizontalCenter: parent.horizontalCenter
bottomLeftRadius: 20
bottomRightRadius: 20
bottomLeftRadius: 20 * Theme.scale(Screen)
bottomRightRadius: 20 * Theme.scale(Screen)
ColumnLayout {
id: infoColumn
@ -338,7 +314,7 @@ WlSessionLock {
id: timeText
text: Qt.formatDateTime(new Date(), "HH:mm")
font.family: Theme.fontFamily
font.pixelSize: 48
font.pixelSize: 48 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
horizontalAlignment: Text.AlignHCenter
@ -348,7 +324,7 @@ WlSessionLock {
id: dateText
text: Qt.formatDateTime(new Date(), "dddd, MMMM d")
font.family: Theme.fontFamily
font.pixelSize: 16
font.pixelSize: 16 * Theme.scale(Screen)
color: Theme.textSecondary
opacity: 0.8
horizontalAlignment: Text.AlignHCenter
@ -364,7 +340,7 @@ WlSessionLock {
Text {
text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud"
font.family: "Material Symbols Outlined"
font.pixelSize: 28
font.pixelSize: 28 * Theme.scale(Screen)
color: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
}
@ -372,7 +348,7 @@ WlSessionLock {
Text {
text: weatherData && weatherData.current_weather ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9 / 5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--°F" : "--°C")
font.family: Theme.fontFamily
font.pixelSize: 18
font.pixelSize: 18 * Theme.scale(Screen)
color: Theme.textSecondary
verticalAlignment: Text.AlignVCenter
}
@ -383,7 +359,7 @@ WlSessionLock {
color: Theme.error
visible: weatherError !== ""
font.family: Theme.fontFamily
font.pixelSize: 10
font.pixelSize: 10 * Theme.scale(Screen)
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
@ -425,12 +401,12 @@ WlSessionLock {
spacing: 12
Rectangle {
width: 48
height: 48
radius: 24
width: 48 * Theme.scale(Screen)
height: 48 * Theme.scale(Screen)
radius: 24 * Theme.scale(Screen)
color: shutdownArea.containsMouse ? Theme.error : "transparent"
border.color: Theme.error
border.width: 1
border.width: 1 * Theme.scale(Screen)
MouseArea {
id: shutdownArea
@ -445,18 +421,18 @@ WlSessionLock {
anchors.centerIn: parent
text: "power_settings_new"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
font.pixelSize: 24 * Theme.scale(Screen)
color: shutdownArea.containsMouse ? Theme.onAccent : Theme.error
}
}
Rectangle {
width: 48
height: 48
radius: 24
width: 48 * Theme.scale(Screen)
height: 48 * Theme.scale(Screen)
radius: 24 * Theme.scale(Screen)
color: rebootArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1
border.width: 1 * Theme.scale(Screen)
MouseArea {
id: rebootArea
@ -471,18 +447,18 @@ WlSessionLock {
anchors.centerIn: parent
text: "refresh"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
font.pixelSize: 24 * Theme.scale(Screen)
color: rebootArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
}
Rectangle {
width: 48
height: 48
radius: 24
width: 48 * Theme.scale(Screen)
height: 48 * Theme.scale(Screen)
radius: 24 * Theme.scale(Screen)
color: logoutArea.containsMouse ? Theme.accentSecondary : "transparent"
border.color: Theme.accentSecondary
border.width: 1
border.width: 1 * Theme.scale(Screen)
MouseArea {
id: logoutArea
@ -497,7 +473,7 @@ WlSessionLock {
anchors.centerIn: parent
text: "exit_to_app"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
font.pixelSize: 24 * Theme.scale(Screen)
color: logoutArea.containsMouse ? Theme.onAccent : Theme.accentSecondary
}
}

View file

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

View file

@ -8,25 +8,49 @@ Item {
id: root
width: 22; height: 22
property bool isSilence: false
property var shell: null
// Process for executing CLI commands
Process {
id: rightClickProcess
command: ["qs","ipc", "call", "globalIPC", "toggleNotificationPopup"]
}
// Bell icon/button
// Timer to check when NotificationHistory is loaded
Timer {
id: checkHistoryTimer
interval: 50
repeat: true
onTriggered: {
if (shell && shell.notificationHistoryWin) {
shell.notificationHistoryWin.visible = true;
checkHistoryTimer.stop();
}
}
}
Item {
id: bell
width: 22; height: 22
Text {
id: bellText
anchors.centerIn: parent
text: notificationHistoryWin.hasUnread ? "notifications_unread" : "notifications"
text: {
if (shell && shell.notificationHistoryWin && shell.notificationHistoryWin.hasUnread) {
return "notifications_unread";
} else {
return "notifications";
}
}
font.family: mouseAreaBell.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 16
font.weight: notificationHistoryWin.hasUnread ? Font.Bold : Font.Normal
color: mouseAreaBell.containsMouse ? Theme.accentPrimary : (notificationHistoryWin.hasUnread ? Theme.accentPrimary : Theme.textDisabled)
font.pixelSize: 16 * Theme.scale(Screen)
font.weight: {
if (shell && shell.notificationHistoryWin && shell.notificationHistoryWin.hasUnread) {
return Font.Bold;
} else {
return Font.Normal;
}
}
color: mouseAreaBell.containsMouse ? Theme.accentPrimary : (shell && shell.notificationHistoryWin && shell.notificationHistoryWin.hasUnread ? Theme.accentPrimary : Theme.textDisabled)
}
MouseArea {
id: mouseAreaBell
@ -42,10 +66,18 @@ Item {
}
if (mouse.button === Qt.LeftButton){
notificationHistoryWin.visible = !notificationHistoryWin.visible
return;
if (shell) {
if (!shell.notificationHistoryWin) {
// Use the shell function to load notification history
shell.loadNotificationHistory();
checkHistoryTimer.start();
} else {
// Already loaded, just toggle visibility
shell.notificationHistoryWin.visible = !shell.notificationHistoryWin.visible;
}
}
return;
}
}
onEntered: notificationTooltip.tooltipVisible = true
onExited: notificationTooltip.tooltipVisible = false

View file

@ -14,7 +14,7 @@ PanelWindow {
anchors.top: true
anchors.right: true
margins.top: -20 // keep as you want
margins.top: -20
margins.right: 6
property var notifications: []
@ -52,7 +52,7 @@ PanelWindow {
anchors.right: parent.right
spacing: window.spacing
width: parent.width
clip: false // prevent clipping during animation
clip: false // Prevent clipping during animation
Repeater {
model: notifications

View file

@ -4,310 +4,389 @@ import Quickshell
import Quickshell.Widgets
import qs.Settings
PanelWindow {
id: window
implicitWidth: 350
implicitHeight: notificationColumn.implicitHeight
color: "transparent"
visible: notificationsVisible && notificationModel.count > 0
screen: Quickshell.primaryScreen !== undefined ? Quickshell.primaryScreen : null
focusable: false
// Main container that manages multiple notification popups for different monitors
Item {
id: notificationManager
anchors.fill: parent
property bool barVisible: true
// Get list of available monitors/screens
property var monitors: Quickshell.screens || []
Component.onCompleted: {
console.log("[NotificationPopup] Initialized with", monitors.length, "monitors");
for (let i = 0; i < monitors.length; i++) {
console.log("[NotificationPopup] Monitor", i, ":", monitors[i].name);
}
}
// Global visibility state for all notification popups
property bool notificationsVisible: true
anchors.top: true
anchors.right: true
margins.top: 6
margins.right: 6
ListModel {
id: notificationModel
}
property int maxVisible: 5
property int spacing: 5
function togglePopup(): void {
console.log("[NotificationPopup] Current state: " + notificationsVisible);
console.log("[NotificationManager] Current state: " + notificationsVisible);
notificationsVisible = !notificationsVisible;
console.log("[NotificationPopup] New state: " + notificationsVisible);
console.log("[NotificationManager] New state: " + notificationsVisible);
}
function addNotification(notification) {
notificationModel.insert(0, {
id: notification.id,
appName: notification.appName || "Notification",
summary: notification.summary || "",
body: notification.body || "",
urgency: notification.urgency || 0,
rawNotification: notification,
appeared: false,
dismissed: false
});
while (notificationModel.count > maxVisible) {
notificationModel.remove(notificationModel.count - 1);
}
}
function dismissNotificationById(id) {
for (var i = 0; i < notificationModel.count; i++) {
if (notificationModel.get(i).id === id) {
dismissNotificationByIndex(i);
break;
function addNotification(notification): void {
console.log("[NotificationPopup] Adding notification to popup manager");
// Add notification to all monitor popups
for (let i = 0; i < children.length; i++) {
let child = children[i];
if (child.addNotification) {
child.addNotification(notification);
}
}
}
function dismissNotificationByIndex(index) {
if (index >= 0 && index < notificationModel.count) {
var notif = notificationModel.get(index);
if (!notif.dismissed) {
notificationModel.set(index, {
id: notif.id,
appName: notif.appName,
summary: notif.summary,
body: notif.body,
rawNotification: notif.rawNotification,
appeared: notif.appeared,
dismissed: true
});
// Create a notification popup for each monitor
Repeater {
model: notificationManager.monitors
delegate: Item {
id: delegateItem
// Make addNotification accessible from the Item level
function addNotification(notification) {
if (panelWindow) {
panelWindow.addNotification(notification);
}
}
}
}
PanelWindow {
id: panelWindow
implicitWidth: 350
implicitHeight: Math.max(notificationColumn.height, 0)
color: "transparent"
visible: notificationManager.notificationsVisible && notificationModel.count > 0 && shouldShowOnThisMonitor
screen: modelData
focusable: false
Column {
id: notificationColumn
anchors.right: parent.right
spacing: window.spacing
width: parent.width
clip: false
property bool barVisible: true
property bool notificationsVisible: notificationManager.notificationsVisible
// Check if this monitor should show notifications - make it reactive to settings changes
property bool shouldShowOnThisMonitor: {
let notificationMonitors = Settings.settings.notificationMonitors || [];
let currentScreenName = modelData ? modelData.name : "";
// Show notifications on all monitors if notificationMonitors is empty or contains "*"
let shouldShow = notificationMonitors.length === 0 ||
notificationMonitors.includes("*") ||
notificationMonitors.includes(currentScreenName);
console.log("[NotificationPopup] Monitor", currentScreenName, "should show:", shouldShow, "monitors:", JSON.stringify(notificationMonitors));
return shouldShow;
}
Repeater {
id: notificationRepeater
model: notificationModel
// Watch for changes in notification monitors setting
Connections {
target: Settings.settings
function onNotificationMonitorsChanged() {
// Settings changed, visibility will update automatically
}
}
delegate: Rectangle {
id: notificationDelegate
width: parent.width
color: Theme.backgroundPrimary
radius: 20
border.color: model.urgency == 2 ? Theme.warning : Theme.outline
border.width: 1
anchors.top: true
anchors.right: true
margins.top: 6
margins.right: 6
property bool appeared: model.appeared
property bool dismissed: model.dismissed
property var rawNotification: model.rawNotification
ListModel {
id: notificationModel
}
x: appeared ? 0 : width
opacity: dismissed ? 0 : 1
height: dismissed ? 0 : contentRow.height + 20
property int maxVisible: 5
property int spacing: 5
Row {
id: contentRow
anchors.centerIn: parent
spacing: 10
width: parent.width - 20
function addNotification(notification) {
console.log("[NotificationPopup] Adding notification to monitor popup:", notification.appName);
notificationModel.insert(0, {
id: notification.id,
appName: notification.appName || "Notification",
summary: notification.summary || "",
body: notification.body || "",
urgency: notification.urgency || 0,
rawNotification: notification,
appeared: false,
dismissed: false
});
// Circular Icon container with border
Rectangle {
id: iconBackground
width: 36
height: 36
radius: width / 2 // Circular
color: Theme.accentPrimary
anchors.verticalCenter: parent.verticalCenter
border.color: Qt.darker(Theme.accentPrimary, 1.2)
border.width: 1.5
while (notificationModel.count > maxVisible) {
notificationModel.remove(notificationModel.count - 1);
}
}
// Get all possible icon sources from notification
property var iconSources: [rawNotification?.image || "", rawNotification?.appIcon || "", rawNotification?.icon || ""]
// Try to load notification icon
IconImage {
id: iconImage
anchors.fill: parent
anchors.margins: 4
asynchronous: true
backer.fillMode: Image.PreserveAspectFit
source: {
for (var i = 0; i < iconBackground.iconSources.length; i++) {
var icon = iconBackground.iconSources[i];
if (!icon)
continue;
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
if (icon.startsWith('/')) {
return "file://" + icon;
}
return icon;
}
return "";
}
visible: status === Image.Ready && source.toString() !== ""
function dismissNotificationById(id) {
for (var i = 0; i < notificationModel.count; i++) {
if (notificationModel.get(i).id === id) {
dismissNotificationByIndex(i);
break;
}
}
}
// Fallback to first letter of app name
Text {
anchors.centerIn: parent
visible: !iconImage.visible
text: model.appName ? model.appName.charAt(0).toUpperCase() : "?"
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeBody
font.bold: true
function dismissNotificationByIndex(index) {
if (index >= 0 && index < notificationModel.count) {
var notif = notificationModel.get(index);
if (!notif.dismissed) {
notificationModel.set(index, {
id: notif.id,
appName: notif.appName,
summary: notif.summary,
body: notif.body,
rawNotification: notif.rawNotification,
appeared: notif.appeared,
dismissed: true
});
}
}
}
Column {
id: notificationColumn
anchors.right: parent.right
spacing: panelWindow.spacing
width: parent.width
clip: false
Repeater {
id: notificationRepeater
model: notificationModel
delegate: Rectangle {
id: notificationDelegate
width: parent.width
color: Theme.backgroundPrimary
}
}
radius: 20
border.color: model.urgency == 2 ? Theme.warning : Theme.outline
border.width: 1
Column {
width: contentRow.width - iconBackground.width - 10
spacing: 5
property bool appeared: model.appeared
property bool dismissed: model.dismissed
property var rawNotification: model.rawNotification
Text {
text: model.appName
width: parent.width
color: Theme.textPrimary
font.family: Theme.fontFamily
font.bold: true
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
Text {
text: model.summary
width: parent.width
color: "#eeeeee"
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
visible: text !== ""
}
Text {
text: model.body
width: parent.width
color: "#cccccc"
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption
wrapMode: Text.Wrap
visible: text !== ""
}
}
}
x: appeared ? 0 : width
opacity: dismissed ? 0 : 1
height: dismissed ? 0 : Math.max(contentRow.height, 60) + 20
Timer {
interval: 4000
running: !dismissed
repeat: false
onTriggered: {
dismissAnimation.start();
if (rawNotification)
rawNotification.expire();
}
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: 10
width: parent.width - 20
MouseArea {
anchors.fill: parent
onClicked: {
dismissAnimation.start();
if (rawNotification)
rawNotification.dismiss();
}
}
// Circular Icon container with border
Rectangle {
id: iconBackground
width: 36
height: 36
radius: width / 2
color: Theme.accentPrimary
anchors.verticalCenter: parent.verticalCenter
border.color: Qt.darker(Theme.accentPrimary, 1.2)
border.width: 1.5
ParallelAnimation {
id: dismissAnimation
NumberAnimation {
target: notificationDelegate
property: "opacity"
to: 0
duration: 150
}
NumberAnimation {
target: notificationDelegate
property: "height"
to: 0
duration: 150
}
NumberAnimation {
target: notificationDelegate
property: "x"
to: width
duration: 150
easing.type: Easing.InQuad
}
onFinished: {
for (let i = 0; i < notificationModel.count; i++) {
if (notificationModel.get(i).id === notificationDelegate.id) {
notificationModel.remove(i);
break;
// Priority order for notification icons: image > appIcon > icon
property var iconSources: [rawNotification?.image || "", rawNotification?.appIcon || "", rawNotification?.icon || ""]
// Load notification icon with fallback handling
IconImage {
id: iconImage
anchors.fill: parent
anchors.margins: 4
asynchronous: true
backer.fillMode: Image.PreserveAspectFit
source: {
// Try each icon source in priority order
for (var i = 0; i < iconBackground.iconSources.length; i++) {
var icon = iconBackground.iconSources[i];
if (!icon)
continue;
// Handle special path format from some notifications
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
// Handle absolute file paths
if (icon.startsWith('/')) {
return "file://" + icon;
}
return icon;
}
return "";
}
visible: status === Image.Ready && source.toString() !== ""
}
// Fallback: show first letter of app name when no icon available
Text {
anchors.centerIn: parent
visible: !iconImage.visible
text: model.appName ? model.appName.charAt(0).toUpperCase() : "?"
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeBody
font.bold: true
color: Theme.backgroundPrimary
}
}
Column {
width: contentRow.width - iconBackground.width - 10
spacing: 5
Text {
text: model.appName
width: parent.width
color: Theme.textPrimary
font.family: Theme.fontFamily
font.bold: true
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
Text {
text: model.summary
width: parent.width
color: "#eeeeee"
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
visible: text !== ""
}
Text {
text: model.body
width: parent.width
color: "#cccccc"
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption
wrapMode: Text.Wrap
visible: text !== ""
}
}
}
Timer {
interval: 4000
running: !dismissed
repeat: false
onTriggered: {
dismissAnimation.start();
if (rawNotification)
rawNotification.expire();
}
}
MouseArea {
anchors.fill: parent
onClicked: {
dismissAnimation.start();
if (rawNotification)
rawNotification.dismiss();
}
}
ParallelAnimation {
id: dismissAnimation
NumberAnimation {
target: notificationDelegate
property: "opacity"
to: 0
duration: 150
}
NumberAnimation {
target: notificationDelegate
property: "height"
to: 0
duration: 150
}
NumberAnimation {
target: notificationDelegate
property: "x"
to: width
duration: 150
easing.type: Easing.InQuad
}
onFinished: {
for (let i = 0; i < notificationModel.count; i++) {
if (notificationModel.get(i).id === notificationDelegate.id) {
notificationModel.remove(i);
break;
}
}
}
}
ParallelAnimation {
id: appearAnimation
NumberAnimation {
target: notificationDelegate
property: "opacity"
to: 1
duration: 150
}
NumberAnimation {
target: notificationDelegate
property: "height"
to: Math.max(contentRow.height, 60) + 20
duration: 150
}
NumberAnimation {
target: notificationDelegate
property: "x"
to: 0
duration: 150
easing.type: Easing.OutQuad
}
}
Timer {
id: appearTimer
interval: 10
repeat: false
onTriggered: {
appearAnimation.start();
}
}
Component.onCompleted: {
if (!appeared) {
opacity = 0;
height = 0;
x = width;
// Small delay to ensure contentRow has proper height
appearTimer.start();
for (let i = 0; i < notificationModel.count; i++) {
if (notificationModel.get(i).id === notificationDelegate.id) {
var oldItem = notificationModel.get(i);
notificationModel.set(i, {
id: oldItem.id,
appName: oldItem.appName,
summary: oldItem.summary,
body: oldItem.body,
rawNotification: oldItem.rawNotification,
appeared: true,
read: oldItem.read,
dismissed: oldItem.dismissed
});
break;
}
}
}
}
}
}
}
ParallelAnimation {
id: appearAnimation
NumberAnimation {
target: notificationDelegate
property: "opacity"
to: 1
duration: 150
}
NumberAnimation {
target: notificationDelegate
property: "height"
to: contentRow.height + 20
duration: 150
}
NumberAnimation {
target: notificationDelegate
property: "x"
to: 0
duration: 150
easing.type: Easing.OutQuad
}
}
Component.onCompleted: {
if (!appeared) {
opacity = 0;
height = 0;
x = width;
appearAnimation.start();
for (let i = 0; i < notificationModel.count; i++) {
if (notificationModel.get(i).id === notificationDelegate.id) {
var oldItem = notificationModel.get(i);
notificationModel.set(i, {
id: oldItem.id,
appName: oldItem.appName,
summary: oldItem.summary,
body: oldItem.body,
rawNotification: oldItem.rawNotification,
appeared: true,
read: oldItem.read,
dismissed: oldItem.dismissed
});
break;
}
Connections {
target: Quickshell
function onScreensChanged() {
if (panelWindow.screen) {
x = panelWindow.screen.width - panelWindow.width - 20;
}
}
}
}
}
}
Connections {
target: Quickshell
function onScreensChanged() {
if (window.screen) {
x = window.screen.width - width - 20;
}
}
}
}

View file

@ -34,15 +34,15 @@ ShellRoot {
cache: true
smooth: true
mipmap: false
visible: wallpaperSource !== "" // Show the original for FastBlur input
visible: wallpaperSource !== ""
}
MultiEffect {
id: overviewBgBlur
anchors.fill: parent
source: bgImage
blurEnabled: true
blur: 0.48 // controls blur strength (0 to 1)
blurMax: 128 // max blur radius in pixels
blur: 0.48
blurMax: 128
}
Rectangle {
anchors.fill: parent

View file

@ -0,0 +1,500 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Components
import qs.Settings
import qs.Widgets.SettingsWindow.Tabs
import qs.Widgets.SettingsWindow.Tabs.Components
PanelWithOverlay {
id: panelMain
property int activeTabIndex: 0
// Function to show wallpaper selector
function showWallpaperSelector() {
if (wallpaperSelector)
wallpaperSelector.show();
}
// Function to show settings window
function showSettings() {
show();
}
// Function to load component for a specific tab
function loadComponentForTab(tabIndex) {
const componentMap = {
"0": generalSettings,
"1": barSettings,
"2": timeWeatherSettings,
"3": recordingSettings,
"4": networkSettings,
"5": displaySettings,
"6": wallpaperSettings,
"7": miscSettings,
"8": aboutSettings
};
const tabNames = ["General", "Bar", "Time & Weather", "Screen Recorder", "Network", "Display", "Wallpaper", "Misc", "About"];
if (componentMap[tabIndex]) {
settingsLoader.sourceComponent = componentMap[tabIndex];
if (tabName)
tabName.text = tabNames[tabIndex];
}
}
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
// Handle activeTabIndex changes
onActiveTabIndexChanged: {
if (activeTabIndex >= 0 && activeTabIndex <= 8)
loadComponentForTab(activeTabIndex);
}
// Add safety checks for component loading
Component.onCompleted: {
// Ensure we start with a valid tab
if (activeTabIndex < 0 || activeTabIndex > 8)
activeTabIndex = 0;
}
// Cleanup when window is hidden
onVisibleChanged: {
if (!visible) {
// Reset to default tab when hiding to prevent state issues
activeTabIndex = 0;
if (tabName)
tabName.text = "General";
}
}
Component {
id: generalSettings
General {
}
}
Component {
id: barSettings
Bar {
}
}
Component {
id: timeWeatherSettings
TimeWeather {
}
}
Component {
id: recordingSettings
ScreenRecorder {
}
}
Component {
id: networkSettings
Network {
}
}
Component {
id: miscSettings
Misc {
}
}
Component {
id: aboutSettings
About {
}
}
Component {
id: displaySettings
Display {
}
}
Component {
id: wallpaperSettings
Wallpaper {
}
}
Rectangle {
id: settingsWindowRect
implicitWidth: Quickshell.screens.length > 0 ? Math.min(Quickshell.screens[0].width * 2 / 3, 1200) * Theme.scale(Screen) : 600 * Theme.scale(Screen)
implicitHeight: Quickshell.screens.length > 0 ? Math.min(Quickshell.screens[0].height * 2 / 3, 800) * Theme.scale(Screen) : 400 * Theme.scale(Screen)
visible: parent.visible
color: "transparent"
// Center the settings window on screen
anchors.centerIn: parent
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
// Background rectangle
Rectangle {
id: background
color: Theme.backgroundPrimary
anchors.fill: parent
radius: 20 * Theme.scale(Screen)
border.color: Theme.outline
border.width: 1 * Theme.scale(Screen)
MultiEffect {
source: background
anchors.fill: background
shadowEnabled: true
shadowColor: Theme.shadow
shadowOpacity: 0.3
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 12
}
}
Rectangle {
id: settings
clip: true
color: Theme.backgroundPrimary
topRightRadius: 20 * Theme.scale(Screen)
bottomRightRadius: 20 * Theme.scale(Screen)
anchors {
left: tabs.right
top: parent.top
bottom: parent.bottom
right: parent.right
margins: 12
}
Rectangle {
id: headerArea
height: 48 * Theme.scale(Screen)
color: "transparent"
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 16
}
RowLayout {
anchors.fill: parent
spacing: 12 * Theme.scale(Screen)
Text {
id: tabName
text: wallpaperSelector.visible ? "Select Wallpaper" : (activeTabIndex === 0 ? "General" : activeTabIndex === 1 ? "Bar" : activeTabIndex === 2 ? "Time & Weather" : activeTabIndex === 3 ? "Screen Recorder" : activeTabIndex === 4 ? "Network" : activeTabIndex === 5 ? "Display" : activeTabIndex === 6 ? "Wallpaper" : activeTabIndex === 7 ? "Misc" : activeTabIndex === 8 ? "About" : "General")
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
// Wallpaper Selection Button (only visible on Wallpaper tab)
Rectangle {
width: 160 * Theme.scale(Screen)
height: 32 * Theme.scale(Screen)
radius: 16 * Theme.scale(Screen)
color: wallpaperButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1 * Theme.scale(Screen)
visible: activeTabIndex === 6 // Wallpaper tab index
Row {
anchors.centerIn: parent
spacing: 6 * Theme.scale(Screen)
Text {
text: "image"
font.family: wallpaperButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 16 * Theme.scale(Screen)
color: wallpaperButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Select Wallpaper"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: wallpaperButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: wallpaperButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Show the wallpaper selector
wallpaperSelector.show();
}
}
}
Rectangle {
width: 32 * Theme.scale(Screen)
height: 32 * Theme.scale(Screen)
radius: 16 * Theme.scale(Screen)
color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1 * Theme.scale(Screen)
Text {
anchors.centerIn: parent
text: "close"
font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 18 * Theme.scale(Screen)
color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
MouseArea {
id: closeButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// If wallpaper selector is open, close it instead of the settings window
if (wallpaperSelector.visible) {
wallpaperSelector.hide();
} else {
panelMain.dismiss();
}
}
}
}
}
}
Rectangle {
height: 1 * Theme.scale(Screen)
color: Theme.outline
opacity: 0.3
anchors {
top: headerArea.bottom
left: parent.left
right: parent.right
margins: 16
}
}
Item {
id: settingsContainer
anchors {
top: headerArea.bottom
left: parent.left
right: parent.right
bottom: parent.bottom
topMargin: 32
}
// Simplified single loader approach
Loader {
id: settingsLoader
anchors.fill: parent
sourceComponent: generalSettings
active: true
}
// Wallpaper Selector Component - positioned as overlay
WallpaperSelector {
id: wallpaperSelector
anchors.fill: parent
}
}
}
Rectangle {
id: tabs
color: Theme.surface
width: parent.width * 0.25
height: settingsWindowRect.height
topLeftRadius: 20 * Theme.scale(Screen)
bottomLeftRadius: 20 * Theme.scale(Screen)
border.color: Theme.outline
border.width: 1 * Theme.scale(Screen)
Column {
width: parent.width
spacing: 0 * Theme.scale(Screen)
topPadding: 8 * Theme.scale(Screen)
bottomPadding: 8 * Theme.scale(Screen)
Repeater {
id: repeater
model: [{
"icon": "tune",
"text": "General"
}, {
"icon": "space_dashboard",
"text": "Bar"
}, {
"icon": "schedule",
"text": "Time & Weather"
}, {
"icon": "photo_camera",
"text": "Screen Recorder"
}, {
"icon": "wifi",
"text": "Network"
}, {
"icon": "monitor",
"text": "Display"
}, {
"icon": "wallpaper",
"text": "Wallpaper"
}, {
"icon": "settings_suggest",
"text": "Misc"
}, {
"icon": "info",
"text": "About"
}]
delegate: Rectangle {
width: tabs.width
height: 48 * Theme.scale(Screen)
color: "transparent"
RowLayout {
anchors.fill: parent
spacing: 8 * Theme.scale(Screen)
Rectangle {
id: activeIndicator
Layout.leftMargin: 8 * Theme.scale(Screen)
Layout.preferredWidth: 3 * Theme.scale(Screen)
Layout.preferredHeight: 24 * Theme.scale(Screen)
Layout.alignment: Qt.AlignVCenter
radius: 2 * Theme.scale(Screen)
color: Theme.accentPrimary
opacity: index === activeTabIndex ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 200
}
}
}
Label {
id: icon
text: modelData.icon
font.family: "Material Symbols Outlined"
font.pixelSize: 24 * Theme.scale(Screen)
color: index === activeTabIndex ? Theme.accentPrimary : Theme.textPrimary
opacity: index === activeTabIndex ? 1 : 0.8
Layout.leftMargin: 20 * Theme.scale(Screen)
Layout.preferredWidth: 24 * Theme.scale(Screen)
Layout.preferredHeight: 24 * Theme.scale(Screen)
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 }
}
Label {
id: label
text: modelData.text
font.pixelSize: 16 * Theme.scale(Screen)
color: index === activeTabIndex ? Theme.accentPrimary : (tabMouseArea.containsMouse ? Theme.accentPrimary : Theme.textSecondary)
font.weight: index === activeTabIndex ? Font.DemiBold : (tabMouseArea.containsMouse ? Font.DemiBold : Font.Normal)
Layout.fillWidth: true
Layout.preferredHeight: 24 * Theme.scale(Screen)
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.leftMargin: 4 * Theme.scale(Screen)
Layout.rightMargin: 16 * Theme.scale(Screen)
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
activeTabIndex = index;
loadComponentForTab(index);
}
}
Rectangle {
width: parent.width
height: 1 * Theme.scale(Screen)
color: Theme.outline
opacity: 0.6
visible: index < (repeater.count - 1)
anchors.bottom: parent.bottom
}
}
}
}
}
}
}

View file

@ -0,0 +1,441 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Components
import qs.Settings
ColumnLayout {
id: root
property string latestVersion: "Unknown"
property string currentVersion: "Unknown"
property var contributors: []
property string githubDataPath: Settings.settingsDir + "github_data.json"
function loadFromFile() {
const now = Date.now();
const data = githubData;
if (!data.timestamp || (now - data.timestamp > 3.6e+06)) {
console.log("[About] Cache expired or missing, fetching new data from GitHub...");
fetchFromGitHub();
return ;
}
console.log("[About] Loading cached GitHub data (age: " + Math.round((now - data.timestamp) / 60000) + " minutes)");
if (data.version)
root.latestVersion = data.version;
if (data.contributors)
root.contributors = data.contributors;
}
function fetchFromGitHub() {
versionProcess.running = true;
contributorsProcess.running = true;
}
function saveData() {
githubData.timestamp = Date.now();
Qt.callLater(() => {
githubDataFile.writeAdapter();
});
}
spacing: 0
anchors.fill: parent
anchors.margins: 0
Process {
id: currentVersionProcess
command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"]
Component.onCompleted: {
running = true;
}
stdout: StdioCollector {
onStreamFinished: {
const version = text.trim();
if (version && version !== "Unknown") {
root.currentVersion = version;
} else {
currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir + " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"];
currentVersionProcess.running = true;
}
}
}
}
FileView {
id: githubDataFile
path: root.githubDataPath
blockLoading: true
printErrors: true
watchChanges: true
onFileChanged: githubDataFile.reload()
onLoaded: loadFromFile()
onLoadFailed: function(error) {
console.log("GitHub data file doesn't exist yet, creating it...");
githubData.version = "Unknown";
githubData.contributors = [];
githubData.timestamp = 0;
githubDataFile.writeAdapter();
fetchFromGitHub();
}
Component.onCompleted: {
if (path)
reload();
}
JsonAdapter {
id: githubData
property string version: "Unknown"
property var contributors: []
property double timestamp: 0
}
}
Process {
id: versionProcess
command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/releases/latest"]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text);
if (data.tag_name) {
const version = data.tag_name;
githubData.version = version;
root.latestVersion = version;
console.log("[About] Latest version fetched from GitHub:", version);
} else {
console.log("No tag_name in GitHub response");
}
saveData();
} catch (e) {
console.error("Failed to parse version:", e);
}
}
}
}
Process {
id: contributorsProcess
command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/contributors?per_page=100"]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text);
githubData.contributors = data || [];
root.contributors = githubData.contributors;
console.log("[About] Contributors data fetched from GitHub:", githubData.contributors.length, "contributors");
saveData();
} catch (e) {
console.error("Failed to parse contributors:", e);
root.contributors = [];
}
}
}
}
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: 16
rightPadding: 12
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Text {
text: "Noctalia: quiet by design"
font.pixelSize: 24 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: 8 * Theme.scale(Screen)
}
Text {
text: "It may just be another quickshell setup but it won't get in your way."
font.pixelSize: 14 * Theme.scale(Screen)
color: Theme.textSecondary
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: 16 * Theme.scale(Screen)
}
GridLayout {
Layout.alignment: Qt.AlignCenter
columns: 2
rowSpacing: 4
columnSpacing: 8
Text {
text: "Latest Version:"
font.pixelSize: 16 * Theme.scale(Screen)
color: Theme.textSecondary
Layout.alignment: Qt.AlignRight
}
Text {
text: root.latestVersion
font.pixelSize: 16 * Theme.scale(Screen)
color: Theme.textPrimary
font.bold: true
}
Text {
text: "Installed Version:"
font.pixelSize: 16 * Theme.scale(Screen)
color: Theme.textSecondary
Layout.alignment: Qt.AlignRight
}
Text {
text: root.currentVersion
font.pixelSize: 16 * Theme.scale(Screen)
color: Theme.textPrimary
font.bold: true
}
}
Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.topMargin: 8
Layout.preferredWidth: updateText.implicitWidth + 46
Layout.preferredHeight: 32
radius: 20
color: updateArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1
visible: {
if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown")
return false;
const latest = root.latestVersion.replace("v", "").split(".");
const current = root.currentVersion.replace("v", "").split(".");
for (let i = 0; i < Math.max(latest.length, current.length); i++) {
const l = parseInt(latest[i] || "0");
const c = parseInt(current[i] || "0");
if (l > c)
return true;
if (l < c)
return false;
}
return false;
}
RowLayout {
anchors.centerIn: parent
spacing: 8
Text {
text: "system_update"
font.family: "Material Symbols Outlined"
font.pixelSize: 18 * Theme.scale(Screen)
color: updateArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
}
Text {
id: updateText
text: "Download latest release"
font.pixelSize: 14 * Theme.scale(Screen)
color: updateArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
}
}
MouseArea {
id: updateArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]);
}
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 26
Layout.bottomMargin: 18
height: 1
color: Theme.outline
opacity: 0.3
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 32
Layout.rightMargin: 32
Layout.alignment: Qt.AlignCenter
spacing: 16
RowLayout {
Layout.alignment: Qt.AlignCenter
spacing: 8
Text {
text: "Contributors"
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "(" + root.contributors.length + ")"
font.pixelSize: 14 * Theme.scale(Screen)
color: Theme.textSecondary
}
}
GridView {
id: contributorsGrid
Layout.leftMargin: 32
Layout.rightMargin: 32
Layout.alignment: Qt.AlignCenter
width: 200 * 3
height: 300
cellWidth: 200
cellHeight: 100
model: root.contributors
delegate: Rectangle {
width: contributorsGrid.cellWidth - 8
height: contributorsGrid.cellHeight - 4
radius: 20
color: contributorArea.containsMouse ? Theme.highlight : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 12
Item {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Image {
id: avatarImage
anchors.fill: parent
source: modelData.avatar_url || ""
sourceSize: Qt.size(80, 80)
visible: false
mipmap: true
smooth: true
asynchronous: true
fillMode: Image.PreserveAspectCrop
cache: true
}
MultiEffect {
anchors.fill: parent
source: avatarImage
maskEnabled: true
maskSource: mask
}
Item {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors.fill: parent
radius: avatarImage.width / 2
}
}
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 24 * Theme.scale(Screen)
color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textPrimary
visible: !avatarImage.source || avatarImage.status !== Image.Ready
}
}
ColumnLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Text {
text: modelData.login || "Unknown"
font.pixelSize: 13 * Theme.scale(Screen)
color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textPrimary
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: (modelData.contributions || 0) + " commits"
font.pixelSize: 11 * Theme.scale(Screen)
color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textSecondary
}
}
}
MouseArea {
id: contributorArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.html_url)
Quickshell.execDetached(["xdg-open", modelData.html_url]);
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,86 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Settings
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: 16
rightPadding: 12
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Text {
text: "Elements"
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 16 * Theme.scale(Screen)
}
ToggleOption {
label: "Show Active Window"
description: "Display the title of the currently focused window below the bar"
value: Settings.settings.showActiveWindow
onToggled: function() {
Settings.settings.showActiveWindow = !Settings.settings.showActiveWindow;
}
}
ToggleOption {
label: "Show Active Window Icon"
description: "Display the icon of the currently focused window"
value: Settings.settings.showActiveWindowIcon
onToggled: function() {
Settings.settings.showActiveWindowIcon = !Settings.settings.showActiveWindowIcon;
}
}
ToggleOption {
label: "Show System Info"
description: "Display system information (CPU, RAM, Temperature)"
value: Settings.settings.showSystemInfoInBar
onToggled: function() {
Settings.settings.showSystemInfoInBar = !Settings.settings.showSystemInfoInBar;
}
}
ToggleOption {
label: "Show Taskbar"
description: "Display a taskbar showing currently open windows"
value: Settings.settings.showTaskbar
onToggled: function() {
Settings.settings.showTaskbar = !Settings.settings.showTaskbar;
}
}
ToggleOption {
label: "Show Media"
description: "Display media controls and information"
value: Settings.settings.showMediaInBar
onToggled: function() {
Settings.settings.showMediaInBar = !Settings.settings.showMediaInBar;
}
}
}
}
}

View file

@ -0,0 +1,97 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Settings
Rectangle {
id: root
width: 64 * Theme.scale(Screen)
height: 32 * Theme.scale(Screen)
radius: 16 * Theme.scale(Screen)
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1 * Theme.scale(Screen)
property bool useFahrenheit: Settings.settings.useFahrenheit
Rectangle {
id: slider
width: parent.width / 2 - 4 * Theme.scale(Screen)
height: parent.height - 4 * Theme.scale(Screen)
radius: 14 * Theme.scale(Screen)
color: Theme.accentPrimary
x: 2 + (useFahrenheit ? parent.width / 2 : 0)
y: 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
Row {
anchors.fill: parent
spacing: 0
Item {
width: parent.width / 2
height: parent.height
Text {
anchors.centerIn: parent
text: "°C"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: !useFahrenheit
color: !useFahrenheit ? Theme.onAccent : Theme.textPrimary
Behavior on color {
ColorAnimation { duration: 200 }
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (useFahrenheit) {
Settings.settings.useFahrenheit = false;
}
}
}
}
Item {
width: parent.width / 2
height: parent.height
Text {
anchors.centerIn: parent
text: "°F"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: useFahrenheit
color: useFahrenheit ? Theme.onAccent : Theme.textPrimary
Behavior on color {
ColorAnimation { duration: 200 }
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!useFahrenheit) {
Settings.settings.useFahrenheit = true;
}
}
}
}
}
}

View file

@ -0,0 +1,171 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import qs.Components
import qs.Services
import qs.Settings
Rectangle {
id: wallpaperOverlay
focus: true
// Function to show the overlay and load wallpapers
function show() {
// Ensure wallpapers are loaded
WallpaperManager.loadWallpapers();
wallpaperOverlay.visible = true;
wallpaperOverlay.forceActiveFocus();
}
// Function to hide the overlay
function hide() {
wallpaperOverlay.visible = false;
}
color: Theme.backgroundPrimary
visible: false
z: 1000
// Handle escape key to close
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
wallpaperOverlay.hide();
event.accepted = true;
}
}
// Click outside to close
MouseArea {
anchors.fill: parent
onClicked: {
wallpaperOverlay.hide();
}
}
// Content area that stops event propagation
MouseArea {
// Stop event propagation
anchors.fill: parent
anchors.margins: 24
onClicked: {
}
ColumnLayout {
anchors.fill: parent
spacing: 0
// Wallpaper Grid
Item {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
GridView {
id: wallpaperGrid
anchors.fill: parent
cellWidth: Math.max(120 * Theme.scale(Screen), (parent.width / 3) - 12 * Theme.scale(Screen))
cellHeight: cellWidth * 0.6
model: WallpaperManager.wallpaperList
cacheBuffer: 64
leftMargin: 8
rightMargin: 8
topMargin: 8
bottomMargin: 8
delegate: Item {
width: wallpaperGrid.cellWidth - 8 * Theme.scale(Screen)
height: wallpaperGrid.cellHeight - 8 * Theme.scale(Screen)
Rectangle {
id: wallpaperItem
anchors.fill: parent
anchors.margins: 3
color: Theme.surface
radius: 12 * Theme.scale(Screen)
border.color: Settings.settings.currentWallpaper === modelData ? Theme.accentPrimary : Theme.outline
border.width: 2 * Theme.scale(Screen)
Image {
id: wallpaperImage
anchors.fill: parent
anchors.margins: 2
source: modelData
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
smooth: true
mipmap: true
sourceSize.width: Math.min(width, 480 * Theme.scale(Screen))
sourceSize.height: Math.min(height, 270 * Theme.scale(Screen))
opacity: (wallpaperImage.status == Image.Ready) ? 1 : 0
// Apply circular mask for rounded corners
layer.enabled: true
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
layer.effect: MultiEffect {
maskEnabled: true
maskSource: mask
}
}
Item {
id: mask
anchors.fill: wallpaperImage
layer.enabled: true
visible: false
Rectangle {
width: wallpaperImage.width
height: wallpaperImage.height
radius: 12 * Theme.scale(Screen)
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
WallpaperManager.changeWallpaper(modelData);
wallpaperOverlay.hide();
}
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,373 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Settings
ColumnLayout {
id: root
// Get list of available monitors/screens
property var monitors: Quickshell.screens || []
// Sorted monitors by name
property var sortedMonitors: {
let sorted = [...monitors];
sorted.sort((a, b) => {
let nameA = a.name || "Unknown";
let nameB = b.name || "Unknown";
return nameA.localeCompare(nameB);
});
return sorted;
}
spacing: 0
anchors.fill: parent
anchors.margins: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: 16
rightPadding: 12
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Text {
text: "Monitor Selection"
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 16 * Theme.scale(Screen)
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Layout.bottomMargin: 8
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Bar Monitors"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Select which monitors to display the top panel/bar on"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}
Flow {
Layout.fillWidth: true
spacing: 8
Repeater {
model: root.sortedMonitors
delegate: Rectangle {
id: barCheckbox
property bool isChecked: false
Component.onCompleted: {
// Initialize checkbox state from settings
let monitors = Settings.settings.barMonitors || [];
isChecked = monitors.includes(modelData.name);
}
width: checkboxContent.implicitWidth + 16
height: 32
radius: 16
color: isChecked ? Theme.accentPrimary : Theme.surfaceVariant
border.color: isChecked ? Theme.accentPrimary : Theme.outline
border.width: 1
RowLayout {
id: checkboxContent
anchors.centerIn: parent
spacing: 6
Text {
text: barCheckbox.isChecked ? "check" : ""
font.family: "Material Symbols Outlined"
font.pixelSize: 14 * Theme.scale(Screen)
color: barCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary
visible: barCheckbox.isChecked
}
Text {
text: modelData.name || "Unknown"
font.pixelSize: 12 * Theme.scale(Screen)
color: barCheckbox.isChecked ? Theme.onAccent : Theme.textPrimary
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
isChecked = !isChecked;
// Update settings array when checkbox is toggled
let monitors = Settings.settings.barMonitors || [];
monitors = [...monitors]; // Create copy to trigger reactivity
if (isChecked) {
if (!monitors.includes(modelData.name)) {
monitors.push(modelData.name);
}
} else {
monitors = monitors.filter(name => name !== modelData.name);
}
Settings.settings.barMonitors = monitors;
console.log("Bar monitors updated:", JSON.stringify(monitors));
}
}
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Layout.bottomMargin: 8
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Dock Monitors"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Select which monitors to display the application dock on"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}
Flow {
Layout.fillWidth: true
spacing: 8
Repeater {
model: root.sortedMonitors
delegate: Rectangle {
id: dockCheckbox
property bool isChecked: false
Component.onCompleted: {
// Initialize with current settings
let monitors = Settings.settings.dockMonitors || [];
isChecked = monitors.includes(modelData.name);
}
width: checkboxContent.implicitWidth + 16
height: 32
radius: 16
color: isChecked ? Theme.accentPrimary : Theme.surfaceVariant
border.color: isChecked ? Theme.accentPrimary : Theme.outline
border.width: 1
RowLayout {
id: checkboxContent
anchors.centerIn: parent
spacing: 6
Text {
text: dockCheckbox.isChecked ? "check" : ""
font.family: "Material Symbols Outlined"
font.pixelSize: 14 * Theme.scale(Screen)
color: dockCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary
visible: dockCheckbox.isChecked
}
Text {
text: modelData.name || "Unknown"
font.pixelSize: 12 * Theme.scale(Screen)
color: dockCheckbox.isChecked ? Theme.onAccent : Theme.textPrimary
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Toggle state immediately for UI responsiveness
isChecked = !isChecked;
// Update settings
let monitors = Settings.settings.dockMonitors || [];
monitors = [...monitors]; // Copy array
if (isChecked) {
// Add to array if not already there
if (!monitors.includes(modelData.name)) {
monitors.push(modelData.name);
}
} else {
// Remove from array
monitors = monitors.filter(name => name !== modelData.name);
}
Settings.settings.dockMonitors = monitors;
console.log("Dock monitors updated:", JSON.stringify(monitors));
}
}
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Layout.bottomMargin: 8
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Notification Monitors"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Select which monitors to display system notifications on"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}
Flow {
Layout.fillWidth: true
spacing: 8
Repeater {
model: root.sortedMonitors
delegate: Rectangle {
id: notificationCheckbox
property bool isChecked: false
Component.onCompleted: {
// Initialize with current settings
let monitors = Settings.settings.notificationMonitors || [];
isChecked = monitors.includes(modelData.name);
}
width: checkboxContent.implicitWidth + 16
height: 32
radius: 16
color: isChecked ? Theme.accentPrimary : Theme.surfaceVariant
border.color: isChecked ? Theme.accentPrimary : Theme.outline
border.width: 1
RowLayout {
id: checkboxContent
anchors.centerIn: parent
spacing: 6
Text {
text: notificationCheckbox.isChecked ? "check" : ""
font.family: "Material Symbols Outlined"
font.pixelSize: 14 * Theme.scale(Screen)
color: notificationCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary
visible: notificationCheckbox.isChecked
}
Text {
text: modelData.name || "Unknown"
font.pixelSize: 12 * Theme.scale(Screen)
color: notificationCheckbox.isChecked ? Theme.onAccent : Theme.textPrimary
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Toggle state immediately for UI responsiveness
isChecked = !isChecked;
// Update settings
let monitors = Settings.settings.notificationMonitors || [];
monitors = [...monitors]; // Copy array
if (isChecked) {
// Add to array if not already there
if (!monitors.includes(modelData.name)) {
monitors.push(modelData.name);
}
} else {
// Remove from array
monitors = monitors.filter(name => name !== modelData.name);
}
Settings.settings.notificationMonitors = monitors;
console.log("Notification monitors updated:", JSON.stringify(monitors));
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,166 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Settings
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: 16
rightPadding: 12
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Text {
text: "Profile"
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 16 * Theme.scale(Screen)
}
Text {
text: "Profile Image"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 4 * Theme.scale(Screen)
}
Text {
text: "Your profile picture displayed in various places throughout the shell"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.bottomMargin: 4
}
RowLayout {
spacing: 8 * Theme.scale(Screen)
Layout.fillWidth: true
Rectangle {
width: 48 * Theme.scale(Screen)
height: 48 * Theme.scale(Screen)
radius: 24 * Theme.scale(Screen)
Rectangle {
anchors.fill: parent
color: "transparent"
radius: 24 * Theme.scale(Screen)
border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 2 * Theme.scale(Screen)
z: 2
}
Avatar {
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40 * Theme.scale(Screen)
radius: 16 * Theme.scale(Screen)
color: Theme.surfaceVariant
border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1 * Theme.scale(Screen)
TextInput {
id: profileImageInput
anchors.fill: parent
anchors.leftMargin: 12 * Theme.scale(Screen)
anchors.rightMargin: 12 * Theme.scale(Screen)
anchors.topMargin: 6 * Theme.scale(Screen)
anchors.bottomMargin: 6 * Theme.scale(Screen)
text: Settings.settings.profileImage
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhUrlCharactersOnly
onTextChanged: {
Settings.settings.profileImage = text;
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: profileImageInput.forceActiveFocus()
}
}
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 26 * Theme.scale(Screen)
Layout.bottomMargin: 18 * Theme.scale(Screen)
height: 1 * Theme.scale(Screen)
color: Theme.outline
opacity: 0.3
}
Text {
text: "User Interface"
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 16 * Theme.scale(Screen)
}
ToggleOption {
label: "Show Corners"
description: "Display rounded corners on the edge of the screen"
value: Settings.settings.showCorners
onToggled: function() {
Settings.settings.showCorners = !Settings.settings.showCorners;
}
}
ToggleOption {
label: "Show Dock"
description: "Display a dock at the bottom of the screen for quick access to applications"
value: Settings.settings.showDock
onToggled: function() {
Settings.settings.showDock = !Settings.settings.showDock;
}
}
ToggleOption {
label: "Dim Desktop"
description: "Dim the desktop when panels or menus are open"
value: Settings.settings.dimPanels
onToggled: function() {
Settings.settings.dimPanels = !Settings.settings.dimPanels;
}
}
}
}
}

View file

@ -0,0 +1,148 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Settings
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: 16
rightPadding: 12
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Text {
text: "Media"
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 16 * Theme.scale(Screen)
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Text {
text: "Visualizer Type"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Choose the style of the audio visualizer"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.bottomMargin: 4
}
ComboBox {
id: visualizerTypeComboBox
Layout.fillWidth: true
Layout.preferredHeight: 40
model: ["radial", "fire", "diamond"]
currentIndex: model.indexOf(Settings.settings.visualizerType)
onActivated: {
Settings.settings.visualizerType = model[index];
}
background: Rectangle {
implicitWidth: 120
implicitHeight: 40
color: Theme.surfaceVariant
border.color: visualizerTypeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
radius: 16
}
contentItem: Text {
leftPadding: 12
rightPadding: visualizerTypeComboBox.indicator.width + visualizerTypeComboBox.spacing
text: visualizerTypeComboBox.displayText.charAt(0).toUpperCase() + visualizerTypeComboBox.displayText.slice(1)
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
indicator: Text {
x: visualizerTypeComboBox.width - width - 12
y: visualizerTypeComboBox.topPadding + (visualizerTypeComboBox.availableHeight - height) / 2
text: "arrow_drop_down"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: Theme.textPrimary
}
popup: Popup {
y: visualizerTypeComboBox.height
width: visualizerTypeComboBox.width
implicitHeight: contentItem.implicitHeight
padding: 8
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: visualizerTypeComboBox.popup.visible ? visualizerTypeComboBox.delegateModel : null
currentIndex: visualizerTypeComboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {
}
}
background: Rectangle {
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
radius: 16
}
}
delegate: ItemDelegate {
width: visualizerTypeComboBox.width
highlighted: visualizerTypeComboBox.highlightedIndex === index
contentItem: Text {
text: modelData.charAt(0).toUpperCase() + modelData.slice(1)
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
}
}
}
}
}

View file

@ -0,0 +1,86 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import qs.Components
import qs.Settings
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: 16
rightPadding: 12
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Text {
text: "Wi-Fi"
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 16 * Theme.scale(Screen)
}
ToggleOption {
label: "Enable Wi-Fi"
description: "Turn Wi-Fi radio on or off"
value: Settings.settings.wifiEnabled
onToggled: function() {
Settings.settings.wifiEnabled = !Settings.settings.wifiEnabled;
Quickshell.execDetached(["nmcli", "radio", "wifi", Settings.settings.wifiEnabled ? "on" : "off"]);
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 26
Layout.bottomMargin: 18
height: 1
color: Theme.outline
opacity: 0.3
}
Text {
text: "Bluetooth"
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 16 * Theme.scale(Screen)
}
ToggleOption {
label: "Enable Bluetooth"
description: "Turn Bluetooth radio on or off"
value: Settings.settings.bluetoothEnabled
onToggled: function() {
if (Bluetooth.defaultAdapter) {
Settings.settings.bluetoothEnabled = !Settings.settings.bluetoothEnabled;
Bluetooth.defaultAdapter.enabled = Settings.settings.bluetoothEnabled;
if (Bluetooth.defaultAdapter.enabled)
Bluetooth.defaultAdapter.discovering = true;
}
}
}
}
}
}

View file

@ -0,0 +1,19 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Settings
import qs.Components
ColumnLayout {
id: root
spacing: 24
Text {
text: "Coming soon..."
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.alignment: Qt.AlignCenter
Layout.topMargin: 32
}
}

View file

@ -0,0 +1,812 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Settings
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: 16
rightPadding: 12
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
// Text {
// text: "Screen Recording"
// font.pixelSize: 18 * Theme.scale(Screen)
// font.bold: true
// color: Theme.textPrimary
// Layout.bottomMargin: 8
// }
spacing: 4
Layout.fillWidth: true
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Text {
text: "Output Directory"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Directory where screen recordings will be saved"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
Layout.bottomMargin: 4
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
radius: 16
color: Theme.surfaceVariant
border.color: videoPathInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: videoPathInput
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
anchors.topMargin: 6
anchors.bottomMargin: 6
text: Settings.settings.videoPath !== undefined ? Settings.settings.videoPath : ""
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhUrlCharactersOnly
onTextChanged: {
Settings.settings.videoPath = text;
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: videoPathInput.forceActiveFocus()
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Frame Rate"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Target frame rate for screen recordings (default: 60)"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
Layout.bottomMargin: 4
}
SpinBox {
id: frameRateSpinBox
Layout.fillWidth: true
Layout.preferredHeight: 40
from: 24
to: 144
value: Settings.settings.recordingFrameRate || 60
stepSize: 1
onValueChanged: {
Settings.settings.recordingFrameRate = value;
}
background: Rectangle {
implicitWidth: 120
implicitHeight: 40
color: Theme.surfaceVariant
border.color: frameRateSpinBox.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
radius: 16
}
contentItem: TextInput {
text: frameRateSpinBox.textFromValue(frameRateSpinBox.value, frameRateSpinBox.locale)
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
selectionColor: Theme.accentPrimary
selectedTextColor: Theme.onAccent
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
readOnly: false
selectByMouse: true
inputMethodHints: Qt.ImhDigitsOnly
onTextChanged: {
var newValue = parseInt(text);
if (!isNaN(newValue) && newValue >= frameRateSpinBox.from && newValue <= frameRateSpinBox.to)
frameRateSpinBox.value = newValue;
}
onEditingFinished: {
var newValue = parseInt(text);
if (isNaN(newValue) || newValue < frameRateSpinBox.from || newValue > frameRateSpinBox.to)
text = frameRateSpinBox.textFromValue(frameRateSpinBox.value, frameRateSpinBox.locale);
}
validator: IntValidator {
bottom: frameRateSpinBox.from
top: frameRateSpinBox.to
}
}
up.indicator: Rectangle {
x: parent.width - width
height: parent.height
width: height
color: "transparent"
radius: 16
Text {
text: "add"
font.family: "Material Symbols Outlined"
font.pixelSize: 20 * Theme.scale(Screen)
color: Theme.textPrimary
anchors.centerIn: parent
}
}
down.indicator: Rectangle {
x: 0
height: parent.height
width: height
color: "transparent"
radius: 16
Text {
text: "remove"
font.family: "Material Symbols Outlined"
font.pixelSize: 20 * Theme.scale(Screen)
color: Theme.textPrimary
anchors.centerIn: parent
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Audio Source"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Audio source to capture during recording"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
Layout.bottomMargin: 4
}
ComboBox {
id: audioSourceComboBox
Layout.fillWidth: true
Layout.preferredHeight: 40
model: ["default_output", "default_input", "both"]
currentIndex: model.indexOf(Settings.settings.recordingAudioSource || "default_output")
onActivated: {
Settings.settings.recordingAudioSource = model[index];
}
background: Rectangle {
implicitWidth: 120
implicitHeight: 40
color: Theme.surfaceVariant
border.color: audioSourceComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
radius: 16
}
contentItem: Text {
leftPadding: 12
rightPadding: audioSourceComboBox.indicator.width + audioSourceComboBox.spacing
text: {
switch (audioSourceComboBox.currentText) {
case "default_output":
return "System Audio";
case "default_input":
return "Microphone";
case "both":
return "System Audio + Microphone";
default:
return audioSourceComboBox.currentText;
}
}
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
indicator: Text {
x: audioSourceComboBox.width - width - 12
y: audioSourceComboBox.topPadding + (audioSourceComboBox.availableHeight - height) / 2
text: "arrow_drop_down"
font.family: "Material Symbols Outlined"
font.pixelSize: 24 * Theme.scale(Screen)
color: Theme.textPrimary
}
popup: Popup {
y: audioSourceComboBox.height
width: audioSourceComboBox.width
implicitHeight: contentItem.implicitHeight
padding: 1
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: audioSourceComboBox.popup.visible ? audioSourceComboBox.delegateModel : null
currentIndex: audioSourceComboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {
}
}
background: Rectangle {
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
radius: 16
}
}
delegate: ItemDelegate {
width: audioSourceComboBox.width
highlighted: audioSourceComboBox.highlightedIndex === index
contentItem: Text {
text: {
switch (modelData) {
case "default_output":
return "System Audio";
case "default_input":
return "Microphone";
case "both":
return "System Audio + Microphone";
default:
return modelData;
}
}
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Video Quality"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Higher quality results in larger file sizes"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
Layout.bottomMargin: 4
}
ComboBox {
id: qualityComboBox
Layout.fillWidth: true
Layout.preferredHeight: 40
model: ["medium", "high", "very_high", "ultra"]
currentIndex: model.indexOf(Settings.settings.recordingQuality || "very_high")
onActivated: {
Settings.settings.recordingQuality = model[index];
}
background: Rectangle {
implicitWidth: 120
implicitHeight: 40
color: Theme.surfaceVariant
border.color: qualityComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
radius: 16
}
contentItem: Text {
leftPadding: 12
rightPadding: qualityComboBox.indicator.width + qualityComboBox.spacing
text: {
switch (qualityComboBox.currentText) {
case "medium":
return "Medium";
case "high":
return "High";
case "very_high":
return "Very High";
case "ultra":
return "Ultra";
default:
return qualityComboBox.currentText;
}
}
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
indicator: Text {
x: qualityComboBox.width - width - 12
y: qualityComboBox.topPadding + (qualityComboBox.availableHeight - height) / 2
text: "arrow_drop_down"
font.family: "Material Symbols Outlined"
font.pixelSize: 24 * Theme.scale(Screen)
color: Theme.textPrimary
}
popup: Popup {
y: qualityComboBox.height
width: qualityComboBox.width
implicitHeight: contentItem.implicitHeight
padding: 1
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: qualityComboBox.popup.visible ? qualityComboBox.delegateModel : null
currentIndex: qualityComboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {
}
}
background: Rectangle {
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
radius: 16
}
}
delegate: ItemDelegate {
width: qualityComboBox.width
highlighted: qualityComboBox.highlightedIndex === index
contentItem: Text {
text: {
switch (modelData) {
case "medium":
return "Medium";
case "high":
return "High";
case "very_high":
return "Very High";
case "ultra":
return "Ultra";
default:
return modelData;
}
}
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Video Codec"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Different codecs offer different compression and compatibility"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
Layout.bottomMargin: 4
}
ComboBox {
id: codecComboBox
Layout.fillWidth: true
Layout.preferredHeight: 40
model: ["h264", "hevc", "av1", "vp8", "vp9"]
currentIndex: model.indexOf(Settings.settings.recordingCodec || "h264")
onActivated: {
Settings.settings.recordingCodec = model[index];
}
background: Rectangle {
implicitWidth: 120
implicitHeight: 40
color: Theme.surfaceVariant
border.color: codecComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
radius: 16
}
contentItem: Text {
leftPadding: 12
rightPadding: codecComboBox.indicator.width + codecComboBox.spacing
text: codecComboBox.currentText.toUpperCase()
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
indicator: Text {
x: codecComboBox.width - width - 12
y: codecComboBox.topPadding + (codecComboBox.availableHeight - height) / 2
text: "arrow_drop_down"
font.family: "Material Symbols Outlined"
font.pixelSize: 24 * Theme.scale(Screen)
color: Theme.textPrimary
}
popup: Popup {
y: codecComboBox.height
width: codecComboBox.width
implicitHeight: contentItem.implicitHeight
padding: 1
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: codecComboBox.popup.visible ? codecComboBox.delegateModel : null
currentIndex: codecComboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {
}
}
background: Rectangle {
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
radius: 16
}
}
delegate: ItemDelegate {
width: codecComboBox.width
highlighted: codecComboBox.highlightedIndex === index
contentItem: Text {
text: modelData.toUpperCase()
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Audio Codec"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Opus is recommended for best performance and smallest audio size"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
Layout.bottomMargin: 4
}
ComboBox {
id: audioCodecComboBox
Layout.fillWidth: true
Layout.preferredHeight: 40
model: ["opus", "aac"]
currentIndex: model.indexOf(Settings.settings.audioCodec || "opus")
onActivated: {
Settings.settings.audioCodec = model[index];
}
background: Rectangle {
implicitWidth: 120
implicitHeight: 40
color: Theme.surfaceVariant
border.color: audioCodecComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
radius: 16
}
contentItem: Text {
leftPadding: 12
rightPadding: audioCodecComboBox.indicator.width + audioCodecComboBox.spacing
text: audioCodecComboBox.currentText.toUpperCase()
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
indicator: Text {
x: audioCodecComboBox.width - width - 12
y: audioCodecComboBox.topPadding + (audioCodecComboBox.availableHeight - height) / 2
text: "arrow_drop_down"
font.family: "Material Symbols Outlined"
font.pixelSize: 24 * Theme.scale(Screen)
color: Theme.textPrimary
}
popup: Popup {
y: audioCodecComboBox.height
width: audioCodecComboBox.width
implicitHeight: contentItem.implicitHeight
padding: 1
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: audioCodecComboBox.popup.visible ? audioCodecComboBox.delegateModel : null
currentIndex: audioCodecComboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {
}
}
background: Rectangle {
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
radius: 16
}
}
delegate: ItemDelegate {
width: audioCodecComboBox.width
highlighted: audioCodecComboBox.highlightedIndex === index
contentItem: Text {
text: modelData.toUpperCase()
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Layout.bottomMargin: 16
Text {
text: "Color Range"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Limited is recommended for better compatibility"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
Layout.bottomMargin: 4
}
ComboBox {
id: colorRangeComboBox
Layout.fillWidth: true
Layout.preferredHeight: 40
model: ["limited", "full"]
currentIndex: model.indexOf(Settings.settings.colorRange || "limited")
onActivated: {
Settings.settings.colorRange = model[index];
}
background: Rectangle {
implicitWidth: 120
implicitHeight: 40
color: Theme.surfaceVariant
border.color: colorRangeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
radius: 16
}
contentItem: Text {
leftPadding: 12
rightPadding: colorRangeComboBox.indicator.width + colorRangeComboBox.spacing
text: colorRangeComboBox.currentText.charAt(0).toUpperCase() + colorRangeComboBox.currentText.slice(1)
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
indicator: Text {
x: colorRangeComboBox.width - width - 12
y: colorRangeComboBox.topPadding + (colorRangeComboBox.availableHeight - height) / 2
text: "arrow_drop_down"
font.family: "Material Symbols Outlined"
font.pixelSize: 24 * Theme.scale(Screen)
color: Theme.textPrimary
}
popup: Popup {
y: colorRangeComboBox.height
width: colorRangeComboBox.width
implicitHeight: contentItem.implicitHeight
padding: 1
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: colorRangeComboBox.popup.visible ? colorRangeComboBox.delegateModel : null
currentIndex: colorRangeComboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {
}
}
background: Rectangle {
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
radius: 16
}
}
delegate: ItemDelegate {
width: colorRangeComboBox.width
highlighted: colorRangeComboBox.highlightedIndex === index
contentItem: Text {
text: modelData.charAt(0).toUpperCase() + modelData.slice(1)
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
}
}
ToggleOption {
label: "Show Cursor"
description: "Record mouse cursor in the video"
value: Settings.settings.showCursor
onToggled: function() {
Settings.settings.showCursor = !Settings.settings.showCursor;
}
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 24
}
}
}
}

View file

@ -0,0 +1,176 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Settings
import qs.Widgets.SettingsWindow.Tabs.Components
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: 16
rightPadding: 12
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Text {
text: "Time"
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 16 * Theme.scale(Screen)
}
ToggleOption {
label: "Use 12 Hour Clock"
description: "Display time in 12-hour format (e.g., 2:30 PM) instead of 24-hour format"
value: Settings.settings.use12HourClock
onToggled: function() {
Settings.settings.use12HourClock = !Settings.settings.use12HourClock;
}
}
ToggleOption {
label: "US Style Date"
description: "Display dates in MM/DD/YYYY format instead of DD/MM/YYYY"
value: Settings.settings.reverseDayMonth
onToggled: function() {
Settings.settings.reverseDayMonth = !Settings.settings.reverseDayMonth;
}
}
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 26
Layout.bottomMargin: 18
height: 1
color: Theme.outline
opacity: 0.3
}
Text {
text: "Weather"
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 16 * Theme.scale(Screen)
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.bottomMargin: 8 * Theme.scale(Screen)
Text {
text: "City"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Your city name for weather information"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
Layout.fillWidth: true
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
radius: 16
color: Theme.surfaceVariant
border.color: cityInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: cityInput
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
anchors.topMargin: 6
anchors.bottomMargin: 6
text: Settings.settings.weatherCity
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
onTextChanged: {
Settings.settings.weatherCity = text;
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: {
cityInput.forceActiveFocus();
}
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Temperature Unit"
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Choose between Celsius and Fahrenheit"
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
UnitSelector {
}
}
}
}
}
}

View file

@ -0,0 +1,670 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Services
import qs.Settings
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: 16
rightPadding: 12
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Wallpaper Settings"
font.pixelSize: 18
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 8
}
// Wallpaper Settings Category
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
// Wallpaper Folder
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Text {
text: "Wallpaper Folder"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Path to your wallpaper folder"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
spacing: 8
Layout.fillWidth: true
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
radius: 16
color: Theme.surfaceVariant
border.color: folderInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: folderInput
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
anchors.topMargin: 6
anchors.bottomMargin: 6
text: Settings.settings.wallpaperFolder !== undefined ? Settings.settings.wallpaperFolder : ""
font.family: Theme.fontFamily
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhUrlCharactersOnly
onTextChanged: {
Settings.settings.wallpaperFolder = text;
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: folderInput.forceActiveFocus()
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 26
Layout.bottomMargin: 18
height: 1
color: Theme.outline
opacity: 0.3
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Automation"
font.pixelSize: 18
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 8
}
// Random Wallpaper
ToggleOption {
label: "Random Wallpaper"
description: "Automatically select random wallpapers from the folder"
value: Settings.settings.randomWallpaper
onToggled: function() {
Settings.settings.randomWallpaper = !Settings.settings.randomWallpaper;
}
}
// Use Wallpaper Theme
ToggleOption {
label: "Use Wallpaper Theme"
description: "Automatically adjust theme colors based on wallpaper"
value: Settings.settings.useWallpaperTheme
onToggled: function() {
Settings.settings.useWallpaperTheme = !Settings.settings.useWallpaperTheme;
}
}
// Wallpaper Interval
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Wallpaper Interval"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "How often to change wallpapers automatically (in seconds)"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
Text {
text: Settings.settings.wallpaperInterval + " seconds"
font.pixelSize: 13
color: Theme.textPrimary
}
Item {
Layout.fillWidth: true
}
}
Slider {
id: intervalSlider
Layout.fillWidth: true
from: 10
to: 900
stepSize: 10
value: Settings.settings.wallpaperInterval
snapMode: Slider.SnapAlways
onMoved: {
Settings.settings.wallpaperInterval = Math.round(value);
}
background: Rectangle {
x: intervalSlider.leftPadding
y: intervalSlider.topPadding + intervalSlider.availableHeight / 2 - height / 2
implicitWidth: 200
implicitHeight: 4
width: intervalSlider.availableWidth
height: implicitHeight
radius: 2
color: Theme.surfaceVariant
Rectangle {
width: intervalSlider.visualPosition * parent.width
height: parent.height
color: Theme.accentPrimary
radius: 2
}
}
handle: Rectangle {
x: intervalSlider.leftPadding + intervalSlider.visualPosition * (intervalSlider.availableWidth - width)
y: intervalSlider.topPadding + intervalSlider.availableHeight / 2 - height / 2
implicitWidth: 20
implicitHeight: 20
radius: 10
color: intervalSlider.pressed ? Theme.surfaceVariant : Theme.surface
border.color: Theme.accentPrimary
border.width: 2
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 26
Layout.bottomMargin: 18
height: 1
color: Theme.outline
opacity: 0.3
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "SWWW"
font.pixelSize: 18
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 8
}
// Use SWWW
ToggleOption {
label: "Use SWWW"
description: "Use SWWW daemon for advanced wallpaper management"
value: Settings.settings.useSWWW
onToggled: function() {
Settings.settings.useSWWW = !Settings.settings.useSWWW;
}
}
// SWWW Settings (only visible when useSWWW is enabled)
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
visible: Settings.settings.useSWWW
// Resize Mode
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Text {
text: "Resize Mode"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "How SWWW should resize wallpapers to fit the screen"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
radius: 16
color: Theme.surfaceVariant
border.color: resizeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
ComboBox {
id: resizeComboBox
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
anchors.topMargin: 6
anchors.bottomMargin: 6
model: ["no", "crop", "fit", "stretch"]
currentIndex: model.indexOf(Settings.settings.wallpaperResize)
onActivated: {
Settings.settings.wallpaperResize = model[index];
}
background: Rectangle {
color: "transparent"
}
contentItem: Text {
text: resizeComboBox.displayText
font: resizeComboBox.font
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
popup: Popup {
y: resizeComboBox.height
width: resizeComboBox.width
implicitHeight: contentItem.implicitHeight
padding: 1
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: resizeComboBox.popup.visible ? resizeComboBox.delegateModel : null
currentIndex: resizeComboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {
}
}
background: Rectangle {
color: Theme.surface
border.color: Theme.outline
border.width: 1
radius: 8
}
}
delegate: ItemDelegate {
width: resizeComboBox.width
highlighted: resizeComboBox.highlightedIndex === index
contentItem: Text {
text: modelData
color: Theme.textPrimary
font: resizeComboBox.font
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
background: Rectangle {
color: parent.highlighted ? Theme.accentPrimary : "transparent"
}
}
}
}
}
// Transition Type
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Transition Type"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Animation type when switching between wallpapers"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
radius: 16
color: Theme.surfaceVariant
border.color: transitionTypeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
ComboBox {
id: transitionTypeComboBox
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
anchors.topMargin: 6
anchors.bottomMargin: 6
model: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"]
currentIndex: model.indexOf(Settings.settings.transitionType)
onActivated: {
Settings.settings.transitionType = model[index];
}
background: Rectangle {
color: "transparent"
}
contentItem: Text {
text: transitionTypeComboBox.displayText
font: transitionTypeComboBox.font
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
popup: Popup {
y: transitionTypeComboBox.height
width: transitionTypeComboBox.width
implicitHeight: contentItem.implicitHeight
padding: 1
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: transitionTypeComboBox.popup.visible ? transitionTypeComboBox.delegateModel : null
currentIndex: transitionTypeComboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {
}
}
background: Rectangle {
color: Theme.surface
border.color: Theme.outline
border.width: 1
radius: 8
}
}
delegate: ItemDelegate {
width: transitionTypeComboBox.width
highlighted: transitionTypeComboBox.highlightedIndex === index
contentItem: Text {
text: modelData
color: Theme.textPrimary
font: transitionTypeComboBox.font
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
background: Rectangle {
color: parent.highlighted ? Theme.accentPrimary : "transparent"
}
}
}
}
}
// Transition FPS
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Transition FPS"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Frames per second for transition animations"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
Text {
text: Settings.settings.transitionFps + " FPS"
font.pixelSize: 13
color: Theme.textPrimary
}
Item {
Layout.fillWidth: true
}
}
Slider {
id: fpsSlider
Layout.fillWidth: true
from: 30
to: 500
stepSize: 5
value: Settings.settings.transitionFps
snapMode: Slider.SnapAlways
onMoved: {
Settings.settings.transitionFps = Math.round(value);
}
background: Rectangle {
x: fpsSlider.leftPadding
y: fpsSlider.topPadding + fpsSlider.availableHeight / 2 - height / 2
implicitWidth: 200
implicitHeight: 4
width: fpsSlider.availableWidth
height: implicitHeight
radius: 2
color: Theme.surfaceVariant
Rectangle {
width: fpsSlider.visualPosition * parent.width
height: parent.height
color: Theme.accentPrimary
radius: 2
}
}
handle: Rectangle {
x: fpsSlider.leftPadding + fpsSlider.visualPosition * (fpsSlider.availableWidth - width)
y: fpsSlider.topPadding + fpsSlider.availableHeight / 2 - height / 2
implicitWidth: 20
implicitHeight: 20
radius: 10
color: fpsSlider.pressed ? Theme.surfaceVariant : Theme.surface
border.color: Theme.accentPrimary
border.width: 2
}
}
}
// Transition Duration
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Transition Duration"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Duration of transition animations in seconds"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
Text {
text: Settings.settings.transitionDuration.toFixed(3) + " seconds"
font.pixelSize: 13
color: Theme.textPrimary
}
Item {
Layout.fillWidth: true
}
}
Slider {
id: durationSlider
Layout.fillWidth: true
from: 0.25
to: 10
stepSize: 0.05
value: Settings.settings.transitionDuration
snapMode: Slider.SnapAlways
onMoved: {
Settings.settings.transitionDuration = value;
}
background: Rectangle {
x: durationSlider.leftPadding
y: durationSlider.topPadding + durationSlider.availableHeight / 2 - height / 2
implicitWidth: 200
implicitHeight: 4
width: durationSlider.availableWidth
height: implicitHeight
radius: 2
color: Theme.surfaceVariant
Rectangle {
width: durationSlider.visualPosition * parent.width
height: parent.height
color: Theme.accentPrimary
radius: 2
}
}
handle: Rectangle {
x: durationSlider.leftPadding + durationSlider.visualPosition * (durationSlider.availableWidth - width)
y: durationSlider.topPadding + durationSlider.availableHeight / 2 - height / 2
implicitWidth: 20
implicitHeight: 20
radius: 10
color: durationSlider.pressed ? Theme.surfaceVariant : Theme.surface
border.color: Theme.accentPrimary
border.width: 2
}
}
}
}
}
}
}
}

View file

@ -12,7 +12,7 @@ Item {
id: root
property alias panel: bluetoothPanelModal
// For showing error/status messages
property string statusMessage: ""
property bool statusPopupVisible: false
@ -145,7 +145,7 @@ Item {
opacity: 0.12
}
// Content area (centered, in a card)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 640

View file

@ -1,7 +1,6 @@
import QtQuick
import Quickshell
import qs.Settings
import qs.Widgets.Sidebar.Panel
Item {
id: buttonRoot
@ -45,7 +44,7 @@ Item {
id: iconText
text: "dashboard"
font.family: isActive ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 16
font.pixelSize: 16 * Theme.scale(Screen)
color: sidebarPopup.visible ? Theme.accentPrimary : Theme.textPrimary
anchors.centerIn: parent
z: 1

View file

@ -8,15 +8,15 @@ import qs.Services
Rectangle {
id: musicCard
width: 360
height: 250
width: 360 * Theme.scale(Screen)
height: 250 * Theme.scale(Screen)
color: "transparent"
Rectangle {
id: card
anchors.fill: parent
color: Theme.surface
radius: 18
radius: 18 * Theme.scale(Screen)
// Show fallback UI if no player is available
Item {
@ -26,12 +26,12 @@ Rectangle {
ColumnLayout {
anchors.centerIn: parent
spacing: 16
spacing: 16 * Theme.scale(Screen)
Text {
text: "music_note"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeHeader
font.pixelSize: Theme.fontSizeHeader * Theme.scale(Screen)
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
Layout.alignment: Qt.AlignHCenter
}
@ -40,7 +40,7 @@ Rectangle {
text: MusicManager.hasPlayer ? "No controllable player selected" : "No music player detected"
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
Layout.alignment: Qt.AlignHCenter
}
}
@ -49,45 +49,45 @@ Rectangle {
// Main player UI
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
anchors.margins: 18 * Theme.scale(Screen)
spacing: 12 * Theme.scale(Screen)
visible: !!MusicManager.currentPlayer
// Player selector
ComboBox {
id: playerSelector
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: 40 * Theme.scale(Screen)
visible: MusicManager.getAvailablePlayers().length > 1
model: MusicManager.getAvailablePlayers()
textRole: "identity"
currentIndex: MusicManager.selectedPlayerIndex
background: Rectangle {
implicitWidth: 120
implicitHeight: 40
implicitWidth: 120 * Theme.scale(Screen)
implicitHeight: 40 * Theme.scale(Screen)
color: Theme.surfaceVariant
border.color: playerSelector.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
radius: 16
border.width: 1 * Theme.scale(Screen)
radius: 16 * Theme.scale(Screen)
}
contentItem: Text {
leftPadding: 12
leftPadding: 12 * Theme.scale(Screen)
rightPadding: playerSelector.indicator.width + playerSelector.spacing
text: playerSelector.displayText
font.pixelSize: 13
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
indicator: Text {
x: playerSelector.width - width - 12
x: playerSelector.width - width - 12 * Theme.scale(Screen)
y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2
text: "arrow_drop_down"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
font.pixelSize: 24 * Theme.scale(Screen)
color: Theme.textPrimary
}
@ -95,7 +95,7 @@ Rectangle {
y: playerSelector.height
width: playerSelector.width
implicitHeight: contentItem.implicitHeight
padding: 1
padding: 1 * Theme.scale(Screen)
contentItem: ListView {
clip: true
@ -109,8 +109,8 @@ Rectangle {
background: Rectangle {
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
radius: 16
border.width: 1 * Theme.scale(Screen)
radius: 16 * Theme.scale(Screen)
}
}
@ -118,7 +118,7 @@ Rectangle {
width: playerSelector.width
contentItem: Text {
text: modelData.identity
font.pixelSize: 13
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
@ -136,57 +136,57 @@ Rectangle {
}
}
// Album art and spectrum
// Album art with spectrum visualizer
RowLayout {
spacing: 12
spacing: 12 * Theme.scale(Screen)
Layout.fillWidth: true
// Album art with spectrum
// Album art container with circular spectrum overlay
Item {
id: albumArtContainer
width: 96
height: 96 // enough for spectrum and art (will adjust if needed)
width: 96 * Theme.scale(Screen)
height: 96 * Theme.scale(Screen) // enough for spectrum and art (will adjust if needed)
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
// Spectrum visualizer
// Circular spectrum visualizer around album art
CircularSpectrum {
id: spectrum
values: MusicManager.cavaValues
anchors.centerIn: parent
innerRadius: 30 // just outside 60x60 album art
outerRadius: 48 // how far bars extend
innerRadius: 30 * Theme.scale(Screen) // Position just outside 60x60 album art
outerRadius: 48 * Theme.scale(Screen) // Extend bars outward from album art
fillColor: Theme.accentPrimary
strokeColor: Theme.accentPrimary
strokeWidth: 0
strokeWidth: 0 * Theme.scale(Screen)
z: 0
}
// Album art image
Rectangle {
id: albumArtwork
width: 60
height: 60
width: 60 * Theme.scale(Screen)
height: 60 * Theme.scale(Screen)
anchors.centerIn: parent
radius: 30 // circle
radius: 30 * Theme.scale(Screen) // circle
color: Qt.darker(Theme.surface, 1.1)
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
border.width: 1
border.width: 1 * Theme.scale(Screen)
Image {
id: albumArt
anchors.fill: parent
anchors.margins: 2
anchors.margins: 2 * Theme.scale(Screen)
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: false
asynchronous: true
sourceSize.width: 60
sourceSize.height: 60
sourceSize.width: 60 * Theme.scale(Screen)
sourceSize.height: 60 * Theme.scale(Screen)
source: MusicManager.trackArtUrl
visible: source.toString() !== ""
// Rounded corners using layer
// Apply circular mask for rounded corners
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
@ -208,12 +208,12 @@ Rectangle {
}
}
// Fallback icon
// Fallback icon when no album art available
Text {
anchors.centerIn: parent
text: "album"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeBody
font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4)
visible: !albumArt.visible
}
@ -223,13 +223,13 @@ Rectangle {
// Track metadata
ColumnLayout {
Layout.fillWidth: true
spacing: 4
spacing: 4 * Theme.scale(Screen)
Text {
text: MusicManager.trackTitle
color: Theme.textPrimary
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
font.bold: true
elide: Text.ElideRight
wrapMode: Text.Wrap
@ -241,7 +241,7 @@ Rectangle {
text: MusicManager.trackArtist
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.8)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption
font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen)
elide: Text.ElideRight
Layout.fillWidth: true
}
@ -250,7 +250,7 @@ Rectangle {
text: MusicManager.trackAlbum
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption
font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen)
elide: Text.ElideRight
Layout.fillWidth: true
}
@ -261,8 +261,8 @@ Rectangle {
Rectangle {
id: progressBarBackground
width: parent.width
height: 6
radius: 3
height: 6 * Theme.scale(Screen)
radius: 3 * Theme.scale(Screen)
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15)
Layout.fillWidth: true
@ -290,12 +290,12 @@ Rectangle {
// Interactive progress handle
Rectangle {
id: progressHandle
width: 12
height: 12
radius: 6
width: 12 * Theme.scale(Screen)
height: 12 * Theme.scale(Screen)
radius: 6 * Theme.scale(Screen)
color: Theme.accentPrimary
border.color: Qt.lighter(Theme.accentPrimary, 1.3)
border.width: 1
border.width: 1 * Theme.scale(Screen)
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
@ -334,18 +334,18 @@ Rectangle {
// Media controls
RowLayout {
spacing: 4
spacing: 4 * Theme.scale(Screen)
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
// Previous button
Rectangle {
width: 28
height: 28
radius: 14
width: 28 * Theme.scale(Screen)
height: 28 * Theme.scale(Screen)
radius: 14 * Theme.scale(Screen)
color: previousButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1)
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
border.width: 1
border.width: 1 * Theme.scale(Screen)
MouseArea {
id: previousButton
@ -360,19 +360,19 @@ Rectangle {
anchors.centerIn: parent
text: "skip_previous"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeCaption
font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen)
color: previousButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
}
}
// Play/Pause button
Rectangle {
width: 36
height: 36
radius: 18
width: 36 * Theme.scale(Screen)
height: 36 * Theme.scale(Screen)
radius: 18 * Theme.scale(Screen)
color: playButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1)
border.color: Theme.accentPrimary
border.width: 2
border.width: 2 * Theme.scale(Screen)
MouseArea {
id: playButton
@ -387,19 +387,19 @@ Rectangle {
anchors.centerIn: parent
text: MusicManager.isPlaying ? "pause" : "play_arrow"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeBody
font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
color: playButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
}
}
// Next button
Rectangle {
width: 28
height: 28
radius: 14
width: 28 * Theme.scale(Screen)
height: 28 * Theme.scale(Screen)
radius: 14 * Theme.scale(Screen)
color: nextButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1)
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
border.width: 1
border.width: 1 * Theme.scale(Screen)
MouseArea {
id: nextButton
@ -414,7 +414,7 @@ Rectangle {
anchors.centerIn: parent
text: "skip_next"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeCaption
font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen)
color: nextButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
}
}

View file

@ -3,13 +3,15 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Settings
import qs.Widgets.Sidebar.Config
import qs.Components
import qs.Settings
import qs.Widgets.SettingsWindow
PanelWithOverlay {
id: sidebarPopup
property var shell: null
function showAt() {
sidebarPopupRect.showAt();
}
@ -26,18 +28,44 @@ PanelWithOverlay {
sidebarPopupRect.hidePopup();
}
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
// Animation properties
property real slideOffset: width
property bool isAnimating: false
property int leftPadding: 20 * Theme.scale(Screen)
property int bottomPadding: 20 * Theme.scale(Screen)
// Recording properties
property bool isRecording: false
Process {
id: checkRecordingProcess
command: ["pgrep", "-f", "gpu-screen-recorder.*portal"]
onExited: function(exitCode, exitStatus) {
var isActuallyRecording = exitCode === 0
if (isRecording && !isActuallyRecording) {
isRecording = isActuallyRecording
}
}
}
function checkRecordingStatus() {
if (isRecording) {
checkRecordingProcess.running = true
}
}
function showAt() {
if (!sidebarPopup.visible) {
@ -48,26 +76,18 @@ PanelWithOverlay {
slideAnim.running = true;
if (weather)
weather.startWeatherFetch();
if (systemWidget)
systemWidget.panelVisible = true;
if (quickAccessWidget)
quickAccessWidget.panelVisible = true;
}
}
function hidePopup() {
if (sidebarPopupRect.settingsModal && sidebarPopupRect.settingsModal.visible) {
sidebarPopupRect.settingsModal.visible = false;
}
if (wallpaperPanel && wallpaperPanel.visible) {
wallpaperPanel.visible = false;
}
if (sidebarPopupRect.wifiPanelModal && sidebarPopupRect.wifiPanelModal.visible) {
sidebarPopupRect.wifiPanelModal.visible = false;
}
if (sidebarPopupRect.bluetoothPanelModal && sidebarPopupRect.bluetoothPanelModal.visible) {
sidebarPopupRect.bluetoothPanelModal.visible = false;
}
if (shell && shell.settingsWindow && shell.settingsWindow.visible)
shell.settingsWindow.visible = false;
if (sidebarPopup.visible) {
slideAnim.from = 0;
slideAnim.to = width;
@ -75,81 +95,129 @@ PanelWithOverlay {
}
}
// Start screen recording using Quickshell.execDetached
function startRecording() {
var currentDate = new Date();
var hours = String(currentDate.getHours()).padStart(2, '0');
var minutes = String(currentDate.getMinutes()).padStart(2, '0');
var day = String(currentDate.getDate()).padStart(2, '0');
var month = String(currentDate.getMonth() + 1).padStart(2, '0');
var year = currentDate.getFullYear();
var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4";
var videoPath = Settings.settings.videoPath;
if (videoPath && !videoPath.endsWith("/"))
videoPath += "/";
var outputPath = videoPath + filename;
var command = "gpu-screen-recorder -w portal" + " -f " + Settings.settings.recordingFrameRate + " -a default_output" + " -k " + Settings.settings.recordingCodec + " -ac " + Settings.settings.audioCodec + " -q " + Settings.settings.recordingQuality + " -cursor " + (Settings.settings.showCursor ? "yes" : "no") + " -cr " + Settings.settings.colorRange + " -o " + outputPath;
Quickshell.execDetached(["sh", "-c", command]);
isRecording = true;
}
// Stop recording using Quickshell.execDetached
function stopRecording() {
Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]);
// Optionally, force kill after a delay
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', sidebarPopupRect);
cleanupTimer.triggered.connect(function() {
Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder.*portal' 2>/dev/null || true"]);
cleanupTimer.destroy();
});
isRecording = false;
}
implicitWidth: 500 * Theme.scale(Screen)
implicitHeight: 700 * Theme.scale(Screen)
visible: parent.visible
color: "transparent"
anchors.top: parent.top
anchors.right: parent.right
// Clean up processes on destruction
Component.onDestruction: {
if (isRecording)
stopRecording();
}
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
NumberAnimation {
id: slideAnim
target: sidebarPopupRect
property: "slideOffset"
duration: 300
easing.type: Easing.OutCubic
onStopped: {
if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) {
sidebarPopup.visible = false;
// Stop monitoring and background tasks when hidden
if (weather)
weather.stopWeatherFetch();
if (systemWidget)
systemWidget.panelVisible = false;
if (quickAccessWidget)
quickAccessWidget.panelVisible = false;
}
sidebarPopupRect.isAnimating = false;
}
onStarted: {
sidebarPopupRect.isAnimating = true;
}
}
property int leftPadding: 20
property int bottomPadding: 20
Rectangle {
id: mainRectangle
width: sidebarPopupRect.width - sidebarPopupRect.leftPadding
height: sidebarPopupRect.height - sidebarPopupRect.bottomPadding
anchors.top: sidebarPopupRect.top
x: sidebarPopupRect.leftPadding + sidebarPopupRect.slideOffset
y: 0
color: Theme.backgroundPrimary
bottomLeftRadius: 20
bottomLeftRadius: 20 * Theme.scale(Screen)
z: 0
Behavior on x {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
property alias settingsModal: settingsModal
property alias wifiPanelModal: wifiPanel.panel
property alias bluetoothPanelModal: bluetoothPanel.panel
SettingsModal {
// SettingsIcon component
SettingsIcon {
id: settingsModal
onWeatherRefreshRequested: {
if (weather && weather.fetchCityWeather)
weather.fetchCityWeather();
}
}
Item {
anchors.fill: mainRectangle
x: sidebarPopupRect.slideOffset
Behavior on x {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Keys.onEscapePressed: sidebarPopupRect.hidePopup()
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
anchors.margins: 20 * Theme.scale(Screen)
spacing: 4 * Theme.scale(Screen)
System {
PowerMenu {
id: systemWidget
settingsModal: settingsModal
Layout.alignment: Qt.AlignHCenter
z: 3
}
@ -162,7 +230,7 @@ PanelWithOverlay {
// Music and System Monitor row
RowLayout {
spacing: 12
spacing: 12 * Theme.scale(Screen)
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
@ -174,241 +242,147 @@ PanelWithOverlay {
id: systemMonitor
z: 2
}
}
// Power profile, Wifi and Bluetooth row
// Power profile, Record and Wallpaper row
RowLayout {
Layout.alignment: Qt.AlignLeft
Layout.preferredHeight: 80
spacing: 16
Layout.alignment: Qt.AlignVCenter
spacing: 10 * Theme.scale(Screen)
Layout.preferredHeight: 80 * Theme.scale(Screen)
z: 3
PowerProfile {
Layout.alignment: Qt.AlignLeft
Layout.preferredHeight: 80
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: 80 * Theme.scale(Screen)
}
// Network card containing Wifi and Bluetooth
// Record and Wallpaper card
Rectangle {
Layout.preferredHeight: 80
Layout.preferredWidth: 140
Layout.preferredHeight: 80 * Theme.scale(Screen)
Layout.preferredWidth: 140 * Theme.scale(Screen)
Layout.fillWidth: false
color: Theme.surface
radius: 18
radius: 18 * Theme.scale(Screen)
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: 20
spacing: 20 * Theme.scale(Screen)
// Wifi button
// Record button
Rectangle {
id: wifiButton
width: 36
height: 36
radius: 18
id: recordButton
width: 36 * Theme.scale(Screen)
height: 36 * Theme.scale(Screen)
radius: 18 * Theme.scale(Screen)
border.color: Theme.accentPrimary
border.width: 1
color: wifiButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.width: 1 * Theme.scale(Screen)
color: sidebarPopupRect.isRecording ? Theme.accentPrimary :
(recordButtonArea.containsMouse ? Theme.accentPrimary : "transparent")
Text {
anchors.centerIn: parent
text: "wifi"
text: "photo_camera"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: wifiButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
font.pixelSize: 22 * Theme.scale(Screen)
color: sidebarPopupRect.isRecording || recordButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
id: wifiButtonArea
id: recordButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: wifiPanel.showAt()
onClicked: {
if (sidebarPopupRect.isRecording) {
sidebarPopupRect.stopRecording();
sidebarPopup.dismiss();
} else {
sidebarPopupRect.startRecording();
sidebarPopup.dismiss();
}
}
}
StyledTooltip {
text: "Wifi"
targetItem: wifiButtonArea
tooltipVisible: wifiButtonArea.containsMouse
text: sidebarPopupRect.isRecording ? "Stop Recording" : "Start Recording"
targetItem: recordButtonArea
tooltipVisible: recordButtonArea.containsMouse
}
}
// Bluetooth button
// Wallpaper button
Rectangle {
id: bluetoothButton
width: 36
height: 36
radius: 18
id: wallpaperButton
width: 36 * Theme.scale(Screen)
height: 36 * Theme.scale(Screen)
radius: 18 * Theme.scale(Screen)
border.color: Theme.accentPrimary
border.width: 1
color: bluetoothButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.width: 1 * Theme.scale(Screen)
color: wallpaperButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
Text {
anchors.centerIn: parent
text: "bluetooth"
text: "image"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: bluetoothButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
font.pixelSize: 22 * Theme.scale(Screen)
color: wallpaperButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
id: bluetoothButtonArea
id: wallpaperButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: bluetoothPanel.showAt()
onClicked: {
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) {
settingsModal.openSettings(6);
sidebarPopup.dismiss();
}
}
}
StyledTooltip {
text: "Bluetooth"
targetItem: bluetoothButtonArea
tooltipVisible: bluetoothButtonArea.containsMouse
text: "Wallpaper"
targetItem: wallpaperButtonArea
tooltipVisible: wallpaperButtonArea.containsMouse
}
}
}
}
}
// Hidden panel components for modal functionality
WifiPanel {
id: wifiPanel
visible: false
}
BluetoothPanel {
id: bluetoothPanel
visible: false
}
Item {
Layout.fillHeight: true
}
// QuickAccess widget
QuickAccess {
id: quickAccessWidget
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: -16
z: 2
isRecording: sidebarPopupRect.isRecording
onRecordingRequested: {
sidebarPopupRect.startRecording();
}
onStopRecordingRequested: {
sidebarPopupRect.stopRecording();
}
onRecordingStateMismatch: function (actualState) {
isRecording = actualState;
quickAccessWidget.isRecording = actualState;
}
onSettingsRequested: {
settingsModal.visible = true;
}
onWallpaperRequested: {
wallpaperPanel.visible = true;
}
}
}
Keys.onEscapePressed: sidebarPopupRect.hidePopup()
}
// Recording properties
property bool isRecording: false
// Start screen recording using Quickshell.execDetached
function startRecording() {
var currentDate = new Date();
var hours = String(currentDate.getHours()).padStart(2, '0');
var minutes = String(currentDate.getMinutes()).padStart(2, '0');
var day = String(currentDate.getDate()).padStart(2, '0');
var month = String(currentDate.getMonth() + 1).padStart(2, '0');
var year = currentDate.getFullYear();
var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4";
var videoPath = Settings.settings.videoPath;
if (videoPath && !videoPath.endsWith("/")) {
videoPath += "/";
}
var outputPath = videoPath + filename;
var command = "gpu-screen-recorder -w portal -f 60 -a default_output -o " + outputPath;
Quickshell.execDetached(["sh", "-c", command]);
isRecording = true;
quickAccessWidget.isRecording = true;
}
// Stop recording using Quickshell.execDetached
function stopRecording() {
Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]);
// Optionally, force kill after a delay
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', sidebarPopupRect);
cleanupTimer.triggered.connect(function () {
Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder.*portal' 2>/dev/null || true"]);
cleanupTimer.destroy();
});
isRecording = false;
quickAccessWidget.isRecording = false;
}
// Clean up processes on destruction
Component.onDestruction: {
if (isRecording) {
stopRecording();
}
}
Corners {
id: sidebarCornerLeft
position: "bottomright"
size: 1.1
fillColor: Theme.backgroundPrimary
anchors.top: mainRectangle.top
offsetX: -447 + sidebarPopupRect.slideOffset
offsetY: 0
visible: Settings.settings.showCorners
Behavior on offsetX {
Behavior on x {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
Corners {
id: sidebarCornerBottom
position: "bottomright"
size: 1.1
fillColor: Theme.backgroundPrimary
offsetX: 33 + sidebarPopupRect.slideOffset
offsetY: 46
visible: Settings.settings.showCorners
Behavior on offsetX {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
WallpaperPanel {
id: wallpaperPanel
Component.onCompleted: {
if (parent) {
anchors.top = parent.top;
anchors.right = parent.right;
}
}
}
}
}

View file

@ -1,396 +1,23 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Components
import qs.Helpers
import qs.Services
import qs.Settings
import qs.Widgets
import qs.Widgets.LockScreen
import qs.Helpers
import qs.Services
import qs.Components
Rectangle {
id: systemWidget
width: 440
height: 80
color: "transparent"
anchors.horizontalCenterOffset: -2
Rectangle {
id: card
anchors.fill: parent
color: Theme.surface
radius: 18
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
// User info row
RowLayout {
Layout.fillWidth: true
spacing: 12
// Profile image
Rectangle {
width: 48
height: 48
radius: 24
color: Theme.accentPrimary
// Border
Rectangle {
anchors.fill: parent
color: "transparent"
radius: 24
border.color: Theme.accentPrimary
border.width: 2
z: 2
}
Avatar {}
}
// User info text
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: Quickshell.env("USER")
font.family: Theme.fontFamily
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
}
Text {
text: "System Uptime: " + uptimeText
font.family: Theme.fontFamily
font.pixelSize: 12
color: Theme.textSecondary
}
}
// Spacer
Item {
Layout.fillWidth: true
}
// System menu button
Rectangle {
id: systemButton
width: 32
height: 32
radius: 16
color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1
Text {
anchors.centerIn: parent
text: "power_settings_new"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.backgroundPrimary : Theme.accentPrimary
}
MouseArea {
id: systemButtonArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
systemMenu.visible = !systemMenu.visible;
}
}
StyledTooltip {
id: systemTooltip
text: "System"
targetItem: systemButton
tooltipVisible: systemButtonArea.containsMouse
}
}
}
}
}
PanelWithOverlay {
id: systemMenu
anchors.top: systemButton.bottom
anchors.right: systemButton.right
// System menu popup
Rectangle {
width: 160
height: 220
color: Theme.surface
radius: 8
border.color: Theme.outline
border.width: 1
visible: true
z: 9999
anchors.top: parent.top
anchors.right: parent.right
// Position below system button
anchors.rightMargin: 32
anchors.topMargin: systemButton.y + systemButton.height + 48
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 4
// Lock button
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: lockButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: "lock_outline"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Lock Screen"
font.family: Theme.fontFamily
font.pixelSize: 14
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: lockButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
lockScreen.locked = true;
systemMenu.visible = false;
}
}
}
// Suspend button
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: suspendButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: "bedtime"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Suspend"
font.pixelSize: 14
color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: suspendButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
suspend();
systemMenu.visible = false;
}
}
}
// Reboot button
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: rebootButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: "refresh"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Reboot"
font.family: Theme.fontFamily
font.pixelSize: 14
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: rebootButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
reboot();
systemMenu.visible = false;
}
}
}
// Logout button
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: logoutButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: "exit_to_app"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Logout"
font.pixelSize: 14
color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: logoutButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
logout();
systemMenu.visible = false;
}
}
}
// Shutdown button
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: shutdownButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: "power_settings_new"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Shutdown"
font.pixelSize: 14
color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: shutdownButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
shutdown();
systemMenu.visible = false;
}
}
}
}
}
}
// Properties
property string uptimeText: "--:--"
// Process to get uptime
Process {
id: uptimeProcess
command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"]
running: false
stdout: StdioCollector {
onStreamFinished: {
uptimeText = this.text.trim();
uptimeProcess.running = false;
}
}
}
Process {
id: shutdownProcess
command: ["shutdown", "-h", "now"]
running: false
}
Process {
id: rebootProcess
command: ["reboot"]
running: false
}
Process {
id: suspendProcess
command: ["systemctl", "suspend"]
running: false
}
Process {
id: logoutProcessNiri
command: ["niri", "msg", "action", "quit", "--skip-confirmation"]
running: false
}
Process {
id: logoutProcessHyprland
command: ["hyprctl", "dispatch", "exit"]
running: false
}
property bool panelVisible: false
property var settingsModal: null
Process {
id: logoutProcess
@ -399,14 +26,12 @@ Rectangle {
}
function logout() {
if (WorkspaceManager.isNiri) {
if (WorkspaceManager.isNiri)
logoutProcessNiri.running = true;
} else if (WorkspaceManager.isHyprland) {
else if (WorkspaceManager.isHyprland)
logoutProcessHyprland.running = true;
} else {
// fallback or error
else
console.warn("No supported compositor detected for logout");
}
}
function suspend() {
@ -421,33 +46,479 @@ Rectangle {
rebootProcess.running = true;
}
property bool panelVisible: false
// Trigger initial update when panel becomes visible
onPanelVisibleChanged: {
if (panelVisible) {
updateSystemInfo();
}
function updateSystemInfo() {
uptimeProcess.running = true;
}
width: 440 * Theme.scale(Screen)
height: 80 * Theme.scale(Screen)
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 * Theme.scale(Screen)
ColumnLayout {
anchors.fill: parent
anchors.margins: 18 * Theme.scale(Screen)
spacing: 12 * Theme.scale(Screen)
RowLayout {
Layout.fillWidth: true
spacing: 12 * Theme.scale(Screen)
Rectangle {
width: 48 * Theme.scale(Screen)
height: 48 * Theme.scale(Screen)
radius: 24 * Theme.scale(Screen)
color: Theme.accentPrimary
Rectangle {
anchors.fill: parent
color: "transparent"
radius: 24 * Theme.scale(Screen)
border.color: Theme.accentPrimary
border.width: 2 * Theme.scale(Screen)
z: 2
}
Avatar {
}
}
ColumnLayout {
spacing: 4 * Theme.scale(Screen)
Layout.fillWidth: true
Text {
text: Quickshell.env("USER")
font.family: Theme.fontFamily
font.pixelSize: 16 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: "System Uptime: " + uptimeText
font.family: Theme.fontFamily
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
}
}
Item {
Layout.fillWidth: true
}
Rectangle {
id: settingsButton
width: 32 * Theme.scale(Screen)
height: 32 * Theme.scale(Screen)
radius: 16 * Theme.scale(Screen)
color: settingsButtonArea.containsMouse || settingsButtonArea.pressed ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1 * Theme.scale(Screen)
Text {
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
text: "settings"
font.family: "Material Symbols Outlined"
font.pixelSize: 16 * Theme.scale(Screen)
color: settingsButtonArea.containsMouse || settingsButtonArea.pressed ? Theme.backgroundPrimary : Theme.accentPrimary
}
MouseArea {
id: settingsButtonArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings)
settingsModal.openSettings();
}
}
StyledTooltip {
id: settingsTooltip
text: "Settings"
targetItem: settingsButton
tooltipVisible: settingsButtonArea.containsMouse
}
}
Rectangle {
id: systemButton
width: 32 * Theme.scale(Screen)
height: 32 * Theme.scale(Screen)
radius: 16 * Theme.scale(Screen)
color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1 * Theme.scale(Screen)
Text {
anchors.centerIn: parent
text: "power_settings_new"
font.family: "Material Symbols Outlined"
font.pixelSize: 16 * Theme.scale(Screen)
color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.backgroundPrimary : Theme.accentPrimary
}
MouseArea {
id: systemButtonArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
systemMenu.visible = !systemMenu.visible;
}
}
StyledTooltip {
id: systemTooltip
text: "Power Menu"
targetItem: systemButton
tooltipVisible: systemButtonArea.containsMouse
}
}
}
}
}
PanelWithOverlay {
id: systemMenu
anchors.top: systemButton.bottom
anchors.right: systemButton.right
Rectangle {
width: 160 * Theme.scale(Screen)
height: 220 * Theme.scale(Screen)
color: Theme.surface
radius: 8 * Theme.scale(Screen)
border.color: Theme.outline
border.width: 1 * Theme.scale(Screen)
visible: true
z: 9999
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: 32 * Theme.scale(Screen)
anchors.topMargin: systemButton.y + systemButton.height + 48 * Theme.scale(Screen)
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 8 * Theme.scale(Screen)
spacing: 4 * Theme.scale(Screen)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36 * Theme.scale(Screen)
radius: 6 * Theme.scale(Screen)
color: lockButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12 * Theme.scale(Screen)
spacing: 8 * Theme.scale(Screen)
Text {
text: "lock_outline"
font.family: "Material Symbols Outlined"
font.pixelSize: 16 * Theme.scale(Screen)
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Lock Screen"
font.family: Theme.fontFamily
font.pixelSize: 14 * Theme.scale(Screen)
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: lockButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
lockScreen.locked = true;
systemMenu.visible = false;
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36 * Theme.scale(Screen)
radius: 6 * Theme.scale(Screen)
color: suspendButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12 * Theme.scale(Screen)
spacing: 8 * Theme.scale(Screen)
Text {
text: "bedtime"
font.family: "Material Symbols Outlined"
font.pixelSize: 16 * Theme.scale(Screen)
color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Suspend"
font.pixelSize: 14 * Theme.scale(Screen)
color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: suspendButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
suspend();
systemMenu.visible = false;
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36 * Theme.scale(Screen)
radius: 6 * Theme.scale(Screen)
color: rebootButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12 * Theme.scale(Screen)
spacing: 8 * Theme.scale(Screen)
Text {
text: "refresh"
font.family: "Material Symbols Outlined"
font.pixelSize: 16 * Theme.scale(Screen)
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Reboot"
font.family: Theme.fontFamily
font.pixelSize: 14 * Theme.scale(Screen)
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: rebootButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
reboot();
systemMenu.visible = false;
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36 * Theme.scale(Screen)
radius: 6 * Theme.scale(Screen)
color: logoutButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12 * Theme.scale(Screen)
spacing: 8 * Theme.scale(Screen)
Text {
text: "exit_to_app"
font.family: "Material Symbols Outlined"
font.pixelSize: 16 * Theme.scale(Screen)
color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Logout"
font.pixelSize: 14 * Theme.scale(Screen)
color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: logoutButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
logout();
systemMenu.visible = false;
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36 * Theme.scale(Screen)
radius: 6 * Theme.scale(Screen)
color: shutdownButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12 * Theme.scale(Screen)
spacing: 8 * Theme.scale(Screen)
Text {
text: "power_settings_new"
font.family: "Material Symbols Outlined"
font.pixelSize: 16 * Theme.scale(Screen)
color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Shutdown"
font.pixelSize: 14 * Theme.scale(Screen)
color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: shutdownButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
shutdown();
systemMenu.visible = false;
}
}
}
}
}
}
Process {
id: uptimeProcess
command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"]
running: false
stdout: StdioCollector {
onStreamFinished: {
uptimeText = this.text.trim();
uptimeProcess.running = false;
}
}
}
Process {
id: shutdownProcess
command: ["shutdown", "-h", "now"]
running: false
}
Process {
id: rebootProcess
command: ["reboot"]
running: false
}
Process {
id: suspendProcess
command: ["systemctl", "suspend"]
running: false
}
Process {
id: logoutProcessNiri
command: ["niri", "msg", "action", "quit", "--skip-confirmation"]
running: false
}
Process {
id: logoutProcessHyprland
command: ["hyprctl", "dispatch", "exit"]
running: false
}
Process {
id: logoutProcess
command: ["loginctl", "terminate-user", Quickshell.env("USER")]
running: false
}
// Timer to update uptime - only runs when panel is visible
Timer {
interval: 60000 // Update every minute
interval: 60000
repeat: true
running: panelVisible
onTriggered: updateSystemInfo()
}
Component.onCompleted: {
uptimeProcess.running = true;
}
function updateSystemInfo() {
uptimeProcess.running = true;
}
// Add lockscreen instance (hidden by default)
LockScreen {
id: lockScreen
}
}

View file

@ -7,22 +7,22 @@ import qs.Components
Rectangle {
id: card
width: 200
height: 70
width: 200 * Theme.scale(Screen)
height: 70 * Theme.scale(Screen)
color: Theme.surface
radius: 18
radius: 18 * Theme.scale(Screen)
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: 20
spacing: 20 * Theme.scale(Screen)
// Performance
Rectangle {
width: 36; height: 36
radius: 18
width: 36 * Theme.scale(Screen); height: 36 * Theme.scale(Screen)
radius: 18 * Theme.scale(Screen)
border.color: Theme.accentPrimary
border.width: 1
border.width: 1 * Theme.scale(Screen)
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance)
? Theme.accentPrimary
: (perfMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
@ -33,7 +33,7 @@ Rectangle {
anchors.centerIn: parent
text: "speed"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
font.pixelSize: 22 * Theme.scale(Screen)
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance) || perfMouseArea.containsMouse
? Theme.backgroundPrimary
: Theme.accentPrimary
@ -63,12 +63,12 @@ Rectangle {
}
}
// Balanced
Rectangle {
width: 36; height: 36
radius: 18
width: 36 * Theme.scale(Screen); height: 36 * Theme.scale(Screen)
radius: 18 * Theme.scale(Screen)
border.color: Theme.accentPrimary
border.width: 1
border.width: 1 * Theme.scale(Screen)
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced)
? Theme.accentPrimary
: (balMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
@ -79,7 +79,7 @@ Rectangle {
anchors.centerIn: parent
text: "balance"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
font.pixelSize: 22 * Theme.scale(Screen)
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced) || balMouseArea.containsMouse
? Theme.backgroundPrimary
: Theme.accentPrimary
@ -109,12 +109,12 @@ Rectangle {
}
}
// Power Saver
Rectangle {
width: 36; height: 36
radius: 18
width: 36 * Theme.scale(Screen); height: 36 * Theme.scale(Screen)
radius: 18 * Theme.scale(Screen)
border.color: Theme.accentPrimary
border.width: 1
border.width: 1 * Theme.scale(Screen)
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver)
? Theme.accentPrimary
: (saveMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
@ -125,7 +125,7 @@ Rectangle {
anchors.centerIn: parent
text: "eco"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
font.pixelSize: 22 * Theme.scale(Screen)
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver) || saveMouseArea.containsMouse
? Theme.backgroundPrimary
: Theme.accentPrimary

View file

@ -0,0 +1,94 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Settings
import qs.Services
import qs.Widgets.SettingsWindow
import qs.Components
PanelWindow {
id: settingsModal
implicitWidth: 480 * Theme.scale(Screen)
implicitHeight: 780 * Theme.scale(Screen)
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: 0
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
// Signal to request weather refresh
signal weatherRefreshRequested()
// Property to track the settings window instance
property var settingsWindow: null
// Function to open the modal and initialize temp values
function openSettings(initialTabIndex) {
if (!settingsWindow) {
// Create new window
settingsWindow = settingsComponent.createObject(null); // No parent to avoid dependency issues
if (settingsWindow) {
// Set the initial tab if provided
if (typeof initialTabIndex === 'number' && initialTabIndex >= 0 && initialTabIndex <= 8) {
settingsWindow.activeTabIndex = initialTabIndex;
}
settingsWindow.visible = true;
// Show wallpaper selector if opening wallpaper tab (after window is visible)
if (typeof initialTabIndex === 'number' && initialTabIndex === 6) {
Qt.callLater(function() {
if (settingsWindow && settingsWindow.showWallpaperSelector) {
settingsWindow.showWallpaperSelector();
}
}, 100); // Small delay to ensure window is fully loaded
}
// Handle window closure
settingsWindow.visibleChanged.connect(function() {
if (settingsWindow && !settingsWindow.visible) {
// Trigger weather refresh when settings close
weatherRefreshRequested();
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.destroy();
}
});
}
sidebarPopup.dismiss();
} else if (settingsWindow.visible) {
// Close and destroy window
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.visible = false;
windowToDestroy.destroy();
}
}
// Function to close the modal and release focus
function closeSettings() {
if (settingsWindow) {
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.visible = false;
windowToDestroy.destroy();
}
}
Component {
id: settingsComponent
SettingsWindow {}
}
// Clean up on destruction
Component.onDestruction: {
if (settingsWindow) {
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.destroy();
}
}
}

View file

@ -0,0 +1,81 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Settings
import qs.Services
import qs.Widgets.SettingsWindow
import qs.Components
PanelWindow {
id: settingsModal
implicitWidth: 480 * Theme.scale(Screen)
implicitHeight: 780 * Theme.scale(Screen)
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: 0
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
// Property to track the settings window instance
property var settingsWindow: null
// Function to open the modal and initialize temp values
function openSettings() {
if (!settingsWindow) {
// Create new window
settingsWindow = settingsComponent.createObject(null); // No parent to avoid dependency issues
if (settingsWindow) {
settingsWindow.visible = true;
// Handle window closure
settingsWindow.visibleChanged.connect(function() {
if (settingsWindow && !settingsWindow.visible) {
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.destroy();
}
});
}
} else if (settingsWindow.visible) {
// Close and destroy window
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.visible = false;
windowToDestroy.destroy();
}
}
// Function to close the modal and release focus
function closeSettings() {
if (settingsWindow) {
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.visible = false;
windowToDestroy.destroy();
}
}
Component {
id: settingsComponent
SettingsWindow {}
}
// Clean up on destruction
Component.onDestruction: {
if (settingsWindow) {
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.destroy();
}
}
// Refresh weather data when hidden
onVisibleChanged: {
if (!visible && typeof weather !== 'undefined' && weather !== null && weather.fetchCityWeather) {
weather.fetchCityWeather();
}
}
}

View file

@ -8,35 +8,37 @@ import qs.Settings
Rectangle {
id: systemMonitor
width: 70
height: 250
width: 70 * Theme.scale(Screen)
height: 250 * Theme.scale(Screen)
color: "transparent"
// Track visibility state for panel integration
property bool isVisible: false
Rectangle {
id: card
anchors.fill: parent
color: Theme.surface
radius: 18
radius: 18 * Theme.scale(Screen)
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 12
anchors.margins: 8 * Theme.scale(Screen)
spacing: 12 * Theme.scale(Screen)
Layout.alignment: Qt.AlignVCenter
// CPU Usage
// CPU usage indicator with circular progress bar
Item {
width: 50; height: 50
width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen)
CircularProgressBar {
id: cpuBar
progress: Sysinfo.cpuUsage / 100
size: 50
strokeWidth: 4
size: 50 * Theme.scale(Screen)
strokeWidth: 4 * Theme.scale(Screen)
hasNotch: true
notchIcon: "speed"
notchIconSize: 14
notchIconSize: 14 * Theme.scale(Screen)
Layout.alignment: Qt.AlignHCenter
}
MouseArea {
@ -55,18 +57,19 @@ Rectangle {
}
}
// Cpu Temp
// CPU temperature indicator with circular progress bar
Item {
width: 50; height: 50
width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen)
CircularProgressBar {
id: tempBar
progress: Sysinfo.cpuTemp / 100
size: 50
strokeWidth: 4
size: 50 * Theme.scale(Screen)
strokeWidth: 4 * Theme.scale(Screen)
hasNotch: true
units: "°C"
notchIcon: "thermometer"
notchIconSize: 14
notchIconSize: 14 * Theme.scale(Screen)
Layout.alignment: Qt.AlignHCenter
}
MouseArea {
@ -85,17 +88,18 @@ Rectangle {
}
}
// Memory Usage
// Memory usage indicator with circular progress bar
Item {
width: 50; height: 50
width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen)
CircularProgressBar {
id: memBar
progress: Sysinfo.memoryUsagePer / 100
size: 50
strokeWidth: 4
size: 50 * Theme.scale(Screen)
strokeWidth: 4 * Theme.scale(Screen)
hasNotch: true
notchIcon: "memory"
notchIconSize: 14
notchIconSize: 14 * Theme.scale(Screen)
Layout.alignment: Qt.AlignHCenter
}
MouseArea {
@ -114,17 +118,18 @@ Rectangle {
}
}
// Disk Usage
// Disk usage indicator with circular progress bar
Item {
width: 50; height: 50
width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen)
CircularProgressBar {
id: diskBar
progress: Sysinfo.diskUsage / 100
size: 50
strokeWidth: 4
size: 50 * Theme.scale(Screen)
strokeWidth: 4 * Theme.scale(Screen)
hasNotch: true
notchIcon: "storage"
notchIconSize: 14
notchIconSize: 14 * Theme.scale(Screen)
Layout.alignment: Qt.AlignHCenter
}
MouseArea {

View file

@ -2,12 +2,12 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import qs.Settings
import "../../../Helpers/Weather.js" as WeatherHelper
import "../../Helpers/Weather.js" as WeatherHelper
Rectangle {
id: weatherRoot
width: 440
height: 180
width: 440 * Theme.scale(Screen)
height: 180 * Theme.scale(Screen)
color: "transparent"
anchors.horizontalCenterOffset: -2
@ -15,6 +15,20 @@ Rectangle {
property var weatherData: null
property string errorString: ""
property bool isVisible: false
property int lastFetchTime: 0
property bool isLoading: false
// Auto-refetch weather when city changes
Connections {
target: Settings.settings
function onWeatherCityChanged() {
if (isVisible && city !== "") {
// Force refresh when city changes
lastFetchTime = 0;
fetchCityWeather();
}
}
}
Component.onCompleted: {
if (isVisible) {
@ -23,20 +37,42 @@ Rectangle {
}
function fetchCityWeather() {
if (!city || city.trim() === "") {
errorString = "No city configured";
return;
}
// Check if we should fetch new data (avoid fetching too frequently)
var currentTime = Date.now();
var timeSinceLastFetch = currentTime - lastFetchTime;
// Only skip if we have recent data AND lastFetchTime is not 0 (initial state)
if (lastFetchTime > 0 && timeSinceLastFetch < 60000) { // 1 minute
return; // Skip if last fetch was less than 1 minute ago
}
isLoading = true;
errorString = "";
WeatherHelper.fetchCityWeather(city,
function(result) {
weatherData = result.weather;
lastFetchTime = currentTime;
errorString = "";
isLoading = false;
},
function(err) {
errorString = err;
isLoading = false;
}
);
}
function startWeatherFetch() {
isVisible = true
fetchCityWeather()
// Force refresh when panel opens, regardless of time check
lastFetchTime = 0;
fetchCityWeather();
}
function stopWeatherFetch() {
@ -47,81 +83,90 @@ Rectangle {
id: card
anchors.fill: parent
color: Theme.surface
radius: 18
radius: 18 * Theme.scale(Screen)
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
anchors.margins: 18 * Theme.scale(Screen)
spacing: 12 * Theme.scale(Screen)
// Current weather row
RowLayout {
spacing: 12
spacing: 12 * Theme.scale(Screen)
Layout.fillWidth: true
// Weather icon and basic info section
RowLayout {
spacing: 12
Layout.preferredWidth: 140
// Weather icon
RowLayout {
spacing: 12 * Theme.scale(Screen)
Layout.preferredWidth: 140 * Theme.scale(Screen)
Text {
id: weatherIcon
text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud"
text: isLoading ? "sync" : (weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud")
font.family: "Material Symbols Outlined"
font.pixelSize: 28
font.pixelSize: 28 * Theme.scale(Screen)
verticalAlignment: Text.AlignVCenter
color: Theme.accentPrimary
color: isLoading ? Theme.accentPrimary : Theme.accentPrimary
Layout.alignment: Qt.AlignVCenter
// Add rotation animation for loading state
RotationAnimation on rotation {
running: isLoading
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
ColumnLayout {
spacing: 2
spacing: 2 * Theme.scale(Screen)
RowLayout {
spacing: 4
spacing: 4 * Theme.scale(Screen)
Text {
text: city
font.family: Theme.fontFamily
font.pixelSize: 14
font.pixelSize: 14 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: weatherData && weatherData.timezone_abbreviation ? `(${weatherData.timezone_abbreviation})` : ""
font.family: Theme.fontFamily
font.pixelSize: 10
font.pixelSize: 10 * Theme.scale(Screen)
color: Theme.textSecondary
leftPadding: 2
leftPadding: 2 * Theme.scale(Screen)
}
}
Text {
text: weatherData && weatherData.current_weather ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9/5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--°F" : "--°C")
font.family: Theme.fontFamily
font.pixelSize: 24
font.pixelSize: 24 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
}
}
// Spacer to push content to the right
Item {
Layout.fillWidth: true
}
}
// Separator line
Rectangle {
width: parent.width
height: 1
height: 1 * Theme.scale(Screen)
color: Qt.rgba(Theme.textSecondary.g, Theme.textSecondary.g, Theme.textSecondary.b, 0.12)
Layout.fillWidth: true
Layout.topMargin: 2
Layout.bottomMargin: 2
Layout.topMargin: 2 * Theme.scale(Screen)
Layout.bottomMargin: 2 * Theme.scale(Screen)
}
// 5-day forecast row
RowLayout {
spacing: 12
spacing: 12 * Theme.scale(Screen)
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
visible: weatherData && weatherData.daily && weatherData.daily.time
@ -129,31 +174,31 @@ Rectangle {
Repeater {
model: weatherData && weatherData.daily && weatherData.daily.time ? 5 : 0
delegate: ColumnLayout {
spacing: 2
spacing: 2 * Theme.scale(Screen)
Layout.alignment: Qt.AlignHCenter
Text {
// Day of the week (e.g., Mon)
text: Qt.formatDateTime(new Date(weatherData.daily.time[index]), "ddd")
font.family: Theme.fontFamily
font.pixelSize: 12
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
Text {
// Material Symbol icon
text: materialSymbolForCode(weatherData.daily.weathercode[index])
font.family: "Material Symbols Outlined"
font.pixelSize: 22
font.pixelSize: 22 * Theme.scale(Screen)
color: Theme.accentPrimary
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
Text {
// High/low temp
text: weatherData && weatherData.daily ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.daily.temperature_2m_max[index] * 9/5 + 32)}° / ${Math.round(weatherData.daily.temperature_2m_min[index] * 9/5 + 32)}°` : `${Math.round(weatherData.daily.temperature_2m_max[index])}° / ${Math.round(weatherData.daily.temperature_2m_min[index])}°`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--° / --°" : "--° / --°")
font.family: Theme.fontFamily
font.pixelSize: 12
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textPrimary
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
@ -162,29 +207,29 @@ Rectangle {
}
}
// Error message
Text {
text: errorString
color: Theme.error
visible: errorString !== ""
font.family: Theme.fontFamily
font.pixelSize: 10
font.pixelSize: 10 * Theme.scale(Screen)
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
}
}
// Weather code to Material Symbol ligature mapping
function materialSymbolForCode(code) {
if (code === 0) return "sunny"; // Clear
if (code === 1 || code === 2) return "partly_cloudy_day"; // Mainly clear/partly cloudy
if (code === 3) return "cloud"; // Overcast
if (code >= 45 && code <= 48) return "foggy"; // Fog
if (code >= 51 && code <= 67) return "rainy"; // Drizzle
if (code >= 71 && code <= 77) return "weather_snowy"; // Snow
if (code >= 80 && code <= 82) return "rainy"; // Rain showers
if (code >= 95 && code <= 99) return "thunderstorm"; // Thunderstorm
if (code === 0) return "sunny";
if (code === 1 || code === 2) return "partly_cloudy_day";
if (code === 3) return "cloud";
if (code >= 45 && code <= 48) return "foggy";
if (code >= 51 && code <= 67) return "rainy";
if (code >= 71 && code <= 77) return "weather_snowy";
if (code >= 80 && code <= 82) return "rainy";
if (code >= 95 && code <= 99) return "thunderstorm";
return "cloud";
}
function weatherDescriptionForCode(code) {

View file

@ -204,19 +204,19 @@ Item {
wifiLogic.connectingSsid = params.ssid;
// Find the target network in our networks data
const targetNetwork = wifiLogic.networks[params.ssid];
// Check if profile already exists using existing field
if (targetNetwork && targetNetwork.existing) {
// Profile exists, just bring it up (no password prompt)
upConnectionProcess.profileName = params.ssid;
upConnectionProcess.running = true;
wifiLogic.pendingConnect = null;
return;
}
// No existing profile, proceed with normal connection flow
if (params.security && params.security !== "--") {
getInterfaceProcess.running = true;
return;
@ -232,7 +232,7 @@ Item {
}
}
// Disconnect, delete profile, refresh
Process {
id: disconnectProfileProcess
property string connectionName: ""
@ -291,7 +291,7 @@ Item {
}
// Handles connecting to a Wi-Fi network, with or without password
Process {
id: connectProcess
property string ssid: ""
@ -336,7 +336,7 @@ Item {
}
}
// Finds the correct Wi-Fi interface for connection
Process {
id: getInterfaceProcess
running: false
@ -370,7 +370,7 @@ Item {
}
}
// Adds a new Wi-Fi connection profile
Process {
id: addConnectionProcess
property string ifname: ""
@ -403,7 +403,7 @@ Item {
}
}
// Brings up the new connection profile and finalizes connection state
Process {
id: upConnectionProcess
property string profileName: ""
@ -436,7 +436,7 @@ Item {
}
}
// Wifi button (no background card)
Rectangle {
id: wifiButton
width: 36
@ -516,8 +516,8 @@ Item {
Layout.alignment: Qt.AlignVCenter
visible: false
running: false
color: Theme.accentPrimary // Assuming Spinner supports color property
size: 22 // Based on the existing Spinner usage
color: Theme.accentPrimary
size: 22
}
IconButton {
id: refreshButton
@ -704,7 +704,7 @@ Item {
anchors.fill: parent
hoverEnabled: true
onClicked: {
// Toggle the action panel for this network
if (wifiLogic.actionPanelSsid === modelData.ssid) {
wifiLogic.actionPanelSsid = ""; // Close if already open
} else {
@ -791,7 +791,7 @@ Item {
}
}
}
// Action panel for network connection controls
Rectangle {
visible: modelData.ssid === wifiLogic.actionPanelSsid
Layout.fillWidth: true
@ -806,7 +806,7 @@ Item {
anchors.fill: parent
anchors.margins: 12
spacing: 10
// Password field for new secured networks
Item {
Layout.fillWidth: true
Layout.preferredHeight: 36
@ -830,7 +830,7 @@ Item {
inputMethodHints: Qt.ImhNone
echoMode: TextInput.Password
onAccepted: {
// Connect with the entered password
wifiLogic.pendingConnect = {
ssid: modelData.ssid,
security: modelData.security,
@ -843,7 +843,7 @@ Item {
}
}
}
// Connect/Disconnect button
Rectangle {
Layout.preferredWidth: 80
Layout.preferredHeight: 36
@ -861,12 +861,12 @@ Item {
anchors.fill: parent
onClicked: {
if (modelData.connected) {
// Disconnect from network
wifiLogic.disconnectNetwork(modelData.ssid);
} else {
// For secured networks, check if we need password
if (wifiLogic.isSecured(modelData.security) && !modelData.existing) {
// If password field is visible and has content, use it
if (actionPanelPasswordField.text.length > 0) {
wifiLogic.pendingConnect = {
ssid: modelData.ssid,
@ -875,10 +875,9 @@ Item {
};
wifiLogic.doConnect();
}
// For new networks without password entered, we might want to show an error or handle differently
// For now, we'll just close the panel
} else {
// Connect to open network
wifiLogic.connectNetwork(modelData.ssid, modelData.security);
}
}

View file

@ -1,56 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import qs.Settings
ColumnLayout {
property alias title: headerText.text
property bool expanded: false // Hidden by default
default property alias content: contentItem.children
Rectangle {
Layout.fillWidth: true
height: 44
radius: 12
color: Theme.surface
border.color: Theme.accentPrimary
border.width: 2
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 8
Item { width: 2 }
Text {
id: headerText
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeBody
font.bold: true
color: Theme.textPrimary
}
Item { Layout.fillWidth: true }
Rectangle {
width: 32; height: 32
color: "transparent"
Text {
anchors.centerIn: parent
text: expanded ? "expand_less" : "expand_more"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeBody
color: Theme.accentPrimary
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: expanded = !expanded
}
}
Item { height: 8 }
ColumnLayout {
id: contentItem
Layout.fillWidth: true
visible: expanded
spacing: 0
}
}

View file

@ -1,275 +0,0 @@
import QtQuick
import QtQuick.Layouts
import qs.Settings
Rectangle {
id: weatherSettingsCard
Layout.fillWidth: true
Layout.preferredHeight: 320
color: Theme.surface
radius: 18
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
// Weather Settings Header
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
text: "wb_sunny"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Theme.accentPrimary
}
Text {
text: "Weather Settings"
font.family: Theme.fontFamily
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
}
// Weather City Setting
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Text {
text: "City"
font.family: Theme.fontFamily
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
radius: 16
color: Theme.surfaceVariant
border.color: cityInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: cityInput
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.leftMargin: 12
anchors.rightMargin: 12
anchors.topMargin: 6
anchors.bottomMargin: 6
text: Settings.settings.weatherCity
font.family: Theme.fontFamily
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
onTextChanged: {
Settings.settings.weatherCity = text;
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: {
cityInput.forceActiveFocus();
}
}
}
}
}
// Temperature Unit Setting
RowLayout {
spacing: 12
Layout.fillWidth: true
Text {
text: "Temperature Unit"
font.family: Theme.fontFamily
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Item {
Layout.fillWidth: true
}
// Custom Material 3 Switch
Rectangle {
id: customSwitch
width: 52
height: 32
radius: 16
color: Theme.accentPrimary
border.color: Theme.accentPrimary
border.width: 2
Rectangle {
id: thumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.useFahrenheit ? customSwitch.width - width - 2 : 2
Text {
anchors.centerIn: parent
text: Settings.settings.useFahrenheit ? "\u00b0F" : "\u00b0C"
font.family: Theme.fontFamily
font.pixelSize: 12
font.bold: true
color: Theme.textPrimary
}
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.useFahrenheit = !Settings.settings.useFahrenheit;
}
}
}
}
// Random Wallpaper Setting
RowLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Use 12 Hour Clock"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Item {
Layout.fillWidth: true
}
// Custom Material 3 Switch
Rectangle {
id: use12HourClockSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.use12HourClock ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.use12HourClock ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: randomWallpaperThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.use12HourClock ? use12HourClockSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.use12HourClock = !Settings.settings.use12HourClock;
}
}
}
}
// Reverse Day Month Setting
RowLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "US Style Date"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Item {
Layout.fillWidth: true
}
// Custom Material 3 Switch
Rectangle {
id: reverseDayMonthSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.reverseDayMonth ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.reverseDayMonth ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: reverseDayMonthThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.reverseDayMonth ? reverseDayMonthSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.reverseDayMonth = !Settings.settings.reverseDayMonth;
}
}
}
}
}
}