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

@ -1,26 +1,30 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import QtQuick.Effects
import qs.Bar.Modules import qs.Bar.Modules
import qs.Settings
import qs.Services
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import qs.Services
import qs.Settings
import qs.Widgets import qs.Widgets
import qs.Widgets.Sidebar
import qs.Widgets.Sidebar.Panel
import qs.Widgets.Notification import qs.Widgets.Notification
import qs.Widgets.SidePanel
// Main bar component - creates panels on selected monitors with widgets and corners
Scope { Scope {
id: rootScope id: rootScope
property var shell property var shell
property alias visible: barRootItem.visible
Item { Item {
id: barRootItem id: barRootItem
anchors.fill: parent anchors.fill: parent
Variants { Variants {
@ -31,19 +35,20 @@ Scope {
PanelWindow { PanelWindow {
id: panel id: panel
screen: modelData screen: modelData
color: "transparent" color: "transparent"
implicitHeight: barBackground.height implicitHeight: barBackground.height
anchors.top: true anchors.top: true
anchors.left: true anchors.left: true
anchors.right: true anchors.right: true
visible: Settings.settings.barMonitors.includes(modelData.name) || (Settings.settings.barMonitors.length === 0)
visible: true
Rectangle { Rectangle {
id: barBackground id: barBackground
width: parent.width width: parent.width
height: 36 height: 36 * Theme.scale(Screen)
color: Theme.backgroundPrimary color: Theme.backgroundPrimary
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
@ -51,10 +56,11 @@ Scope {
Row { Row {
id: leftWidgetsRow id: leftWidgetsRow
anchors.verticalCenter: barBackground.verticalCenter anchors.verticalCenter: barBackground.verticalCenter
anchors.left: barBackground.left anchors.left: barBackground.left
anchors.leftMargin: 18 anchors.leftMargin: 18 * Theme.scale(Screen)
spacing: 12 spacing: 12 * Theme.scale(Screen)
SystemInfo { SystemInfo {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -67,6 +73,7 @@ Scope {
Taskbar { Taskbar {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
ActiveWindow { ActiveWindow {
@ -75,6 +82,7 @@ Scope {
Workspace { Workspace {
id: workspace id: workspace
screen: modelData screen: modelData
anchors.horizontalCenter: barBackground.horizontalCenter anchors.horizontalCenter: barBackground.horizontalCenter
anchors.verticalCenter: barBackground.verticalCenter anchors.verticalCenter: barBackground.verticalCenter
@ -82,13 +90,15 @@ Scope {
Row { Row {
id: rightWidgetsRow id: rightWidgetsRow
anchors.verticalCenter: barBackground.verticalCenter anchors.verticalCenter: barBackground.verticalCenter
anchors.right: barBackground.right anchors.right: barBackground.right
anchors.rightMargin: 18 anchors.rightMargin: 18 * Theme.scale(Screen)
spacing: 12 spacing: 12 * Theme.scale(Screen)
SystemTray { SystemTray {
id: systemTrayModule id: systemTrayModule
shell: rootScope.shell shell: rootScope.shell
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
bar: panel bar: panel
@ -100,22 +110,34 @@ Scope {
} }
NotificationIcon { NotificationIcon {
shell: rootScope.shell
anchors.verticalCenter: parent.verticalCenter
}
Wifi {
anchors.verticalCenter: parent.verticalCenter
}
Bluetooth {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Battery { Battery {
id: widgetsBattery id: widgetsBattery
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Brightness { Brightness {
id: widgetsBrightness id: widgetsBrightness
screen: modelData screen: modelData
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Volume { Volume {
id: widgetsVolume id: widgetsVolume
shell: rootScope.shell shell: rootScope.shell
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@ -127,6 +149,8 @@ Scope {
PanelPopup { PanelPopup {
id: sidebarPopup id: sidebarPopup
shell: rootScope.shell
} }
Button { Button {
@ -135,121 +159,129 @@ Scope {
screen: modelData screen: modelData
sidebarPopup: sidebarPopup sidebarPopup: sidebarPopup
} }
} }
Background {}
Overview {}
} }
PanelWindow { Loader {
id: topLeftPanel active: Settings.settings.showCorners && (Settings.settings.barMonitors.includes(modelData.name) || (Settings.settings.barMonitors.length === 0))
anchors.top: true
anchors.left: true
color: "transparent" sourceComponent: Item {
screen: modelData PanelWindow {
margins.top: 36 id: topLeftPanel
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true anchors.top: true
WlrLayershell.layer: WlrLayer.Background anchors.left: true
aboveWindows: false color: "transparent"
WlrLayershell.namespace: "swww-daemon" screen: modelData
implicitHeight: 24 margins.top: 36
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "swww-daemon"
aboveWindows: false
implicitHeight: 24
Corner {
id: topLeftCorner
position: "bottomleft"
size: 1.3
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
offsetX: -39
offsetY: 0
anchors.top: parent.top
}
}
PanelWindow {
id: topRightPanel
anchors.top: true
anchors.right: true
color: "transparent"
screen: modelData
margins.top: 36
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "swww-daemon"
aboveWindows: false
implicitHeight: 24
Corner {
id: topRightCorner
position: "bottomright"
size: 1.3
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
offsetX: 39
offsetY: 0
anchors.top: parent.top
}
}
PanelWindow {
id: bottomLeftPanel
anchors.bottom: true
anchors.left: true
color: "transparent"
screen: modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "swww-daemon"
aboveWindows: false
implicitHeight: 24
Corner {
id: bottomLeftCorner
position: "topleft"
size: 1.3
fillColor: Theme.backgroundPrimary
offsetX: -39
offsetY: 0
anchors.top: parent.top
}
}
PanelWindow {
id: bottomRightPanel
anchors.bottom: true
anchors.right: true
color: "transparent"
screen: modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "swww-daemon"
aboveWindows: false
implicitHeight: 24
Corner{
id: bottomRightCorner
position: "topright"
size: 1.3
fillColor: Theme.backgroundPrimary
offsetX: 39
offsetY: 0
anchors.top: parent.top
}
}
Corners {
id: topLeftCorner
position: "bottomleft"
size: 1.3
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
offsetX: -39
offsetY: 0
anchors.top: parent.top
visible: Settings.settings.showCorners
} }
} }
PanelWindow {
id: topRightPanel
anchors.top: true
anchors.right: true
color: "transparent"
screen: modelData
margins.top: 36
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
aboveWindows: false
WlrLayershell.namespace: "swww-daemon"
implicitHeight: 24
Corners {
id: topRightCorner
position: "bottomright"
size: 1.3
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
offsetX: 39
offsetY: 0
anchors.top: parent.top
visible: Settings.settings.showCorners
}
}
PanelWindow {
id: bottomLeftPanel
anchors.bottom: true
anchors.left: true
color: "transparent"
screen: modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
aboveWindows: false
WlrLayershell.namespace: "swww-daemon"
implicitHeight: 24
Corners {
id: bottomLeftCorner
position: "topleft"
size: 1.3
fillColor: Theme.backgroundPrimary
offsetX: -39
offsetY: 0
anchors.top: parent.top
visible: Settings.settings.showCorners
}
}
PanelWindow {
id: bottomRightPanel
anchors.bottom: true
anchors.right: true
color: "transparent"
screen: modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
aboveWindows: false
WlrLayershell.namespace: "swww-daemon"
implicitHeight: 24
Corners {
id: bottomRightCorner
position: "topright"
size: 1.3
fillColor: Theme.backgroundPrimary
offsetX: 39
offsetY: 0
anchors.top: parent.top
visible: Settings.settings.showCorners
}
}
} }
} }
} }
// This alias exposes the visual bar's visibility to the outside world
property alias visible: barRootItem.visible
} }

View file

@ -7,6 +7,10 @@ import qs.Settings
PanelWindow { PanelWindow {
id: activeWindowPanel id: activeWindowPanel
// Lower case "screen" from modelData
property int barHeight: 36 * Theme.scale(screen)
screen: (typeof modelData !== 'undefined' ? modelData : null) screen: (typeof modelData !== 'undefined' ? modelData : null)
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
anchors.top: true anchors.top: true
@ -14,26 +18,25 @@ PanelWindow {
anchors.right: true anchors.right: true
focusable: false focusable: false
margins.top: barHeight margins.top: barHeight
visible: !activeWindowWrapper.finallyHidden visible: Settings.settings.showActiveWindow && !activeWindowWrapper.finallyHidden
implicitHeight: activeWindowTitleContainer.height implicitHeight: activeWindowTitleContainer.height
implicitWidth: 0 implicitWidth: 0
property int barHeight: 36
color: "transparent" color: "transparent"
function getIcon() { function getIcon() {
var icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId.toLowerCase(), true); var icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId.toLowerCase(), true);
if (!icon) { if (!icon) {
icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId, true); icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId, true);
}
if (!icon) {
icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title, true);
}
if (!icon) {
icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title.toLowerCase(), "application-x-executable");
}
return icon;
} }
if (!icon) {
icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title, true);
}
if (!icon) {
icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title.toLowerCase(), "application-x-executable");
}
return icon;
}
Item { Item {
id: activeWindowWrapper id: activeWindowWrapper
@ -44,7 +47,7 @@ PanelWindow {
Timer { Timer {
id: visibilityTimer id: visibilityTimer
interval: 1200 interval: 1500
running: false running: false
onTriggered: { onTriggered: {
activeWindowWrapper.shouldShow = false; activeWindowWrapper.shouldShow = false;
@ -101,64 +104,62 @@ PanelWindow {
Rectangle { Rectangle {
id: activeWindowTitleContainer id: activeWindowTitleContainer
color: Theme.backgroundPrimary color: "transparent"
bottomLeftRadius: Math.max(0, width / 2)
bottomRightRadius: Math.max(0, width / 2)
width: Math.min(barBackground.width - 200, activeWindowTitle.implicitWidth + (Settings.settings.showActiveWindowIcon ? 28 : 22))
width: Math.min(barBackground.width - 200, activeWindowTitle.implicitWidth + (Settings.settings.showActiveWindowIcon ? 28 : 22)) + 16
height: activeWindowTitle.implicitHeight + 12 height: activeWindowTitle.implicitHeight + 12
anchors.top: parent.top anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
IconImage { Rectangle {
id: icon id: innerRect
width: 12
height: 12
anchors.left: parent.left
anchors.leftMargin: 6
anchors.verticalCenter: parent.verticalCenter
source: ToplevelManager?.activeToplevel ? getIcon() : ""
visible: Settings.settings.showActiveWindowIcon
anchors.verticalCenterOffset: -3
bottomLeftRadius: Math.max(0, width / 2)
bottomRightRadius: Math.max(0, width / 2)
color: Theme.backgroundPrimary
anchors {
fill: parent
leftMargin: 0
rightMargin: 0
topMargin: -1
bottomMargin: 0
}
border.color: Theme.outline || "#444";
border.width: 1;
IconImage {
id: icon
width: 12
height: 12
anchors.left: parent.left
anchors.leftMargin: 14
anchors.verticalCenter: parent.verticalCenter
source: ToplevelManager?.activeToplevel ? getIcon() : ""
visible: Settings.settings.showActiveWindowIcon
anchors.verticalCenterOffset: -3
}
Text {
id: activeWindowTitle
text: ToplevelManager?.activeToplevel?.title && ToplevelManager?.activeToplevel?.title.length > 60 ? ToplevelManager?.activeToplevel?.title.substring(0, 60) + "..." : ToplevelManager?.activeToplevel?.title || ""
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
anchors.left: icon.right
anchors.leftMargin: Settings.settings.showActiveWindowIcon ? 4 : 6
anchors.right: parent.right
anchors.rightMargin: 14
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -3
horizontalAlignment: Settings.settings.showActiveWindowIcon ? Text.AlignRight : Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
maximumLineCount: 1
}
} }
Text {
id: activeWindowTitle
text: ToplevelManager?.activeToplevel?.title && ToplevelManager?.activeToplevel?.title.length > 60 ? ToplevelManager?.activeToplevel?.title.substring(0, 60) + "..." : ToplevelManager?.activeToplevel?.title || ""
font.pixelSize: 12
color: Theme.textSecondary
anchors.left: icon.right
anchors.leftMargin: Settings.settings.showActiveWindowIcon ? 4 : 6
anchors.right: parent.right
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -3
horizontalAlignment: Settings.settings.showActiveWindowIcon ? Text.AlignRight : Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
maximumLineCount: 1
}
}
Corners {
id: activeCornerRight
position: "bottomleft"
size: 1.1
fillColor: Theme.backgroundPrimary
offsetX: activeWindowTitleContainer.x + activeWindowTitleContainer.width - 34
offsetY: -1
anchors.top: activeWindowTitleContainer.top
}
Corners {
id: activeCornerLeft
position: "bottomright"
size: 1.1
fillColor: Theme.backgroundPrimary
anchors.top: activeWindowTitleContainer.top
x: activeWindowTitleContainer.x + 34 - width
offsetY: -1
} }
} }
} }

View file

@ -11,12 +11,151 @@ import qs.Settings
import "../../Helpers/Fuzzysort.js" as Fuzzysort import "../../Helpers/Fuzzysort.js" as Fuzzysort
PanelWithOverlay { PanelWithOverlay {
Timer {
id: clipboardTimer
interval: 1000
repeat: true
running: appLauncherPanel.visible && searchField.text.startsWith(">clip")
onTriggered: {
updateClipboardHistory();
}
}
property var clipboardHistory: []
property bool clipboardInitialized: false
Process {
id: clipboardTypeProcess
property bool isLoading: false
property var currentTypes: []
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
currentTypes = String(stdout.text).trim().split('\n').filter(t => t);
const imageType = currentTypes.find(t => t.startsWith('image/'));
if (imageType) {
clipboardImageProcess.mimeType = imageType;
clipboardImageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`];
clipboardImageProcess.running = true;
} else {
clipboardHistoryProcess.command = ["wl-paste", "-n", "--type", "text/plain"];
clipboardHistoryProcess.running = true;
}
} else {
clipboardTypeProcess.isLoading = false;
}
}
stdout: StdioCollector {}
}
Process {
id: clipboardImageProcess
property string mimeType: ""
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
const base64 = stdout.text.trim();
if (base64) {
const entry = {
type: 'image',
mimeType: mimeType,
data: `data:${mimeType};base64,${base64}`,
timestamp: new Date().getTime()
};
const exists = clipboardHistory.find(item =>
item.type === 'image' && item.data === entry.data
);
if (!exists) {
clipboardHistory = [entry, ...clipboardHistory].slice(0, 20);
root.updateFilter();
}
}
}
if (!clipboardHistoryProcess.isLoading) {
clipboardInitialized = true;
}
clipboardTypeProcess.isLoading = false;
}
stdout: StdioCollector {}
}
Process {
id: clipboardHistoryProcess
property bool isLoading: false
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
const content = String(stdout.text).trim();
if (content && !content.startsWith("vscode-file://")) {
const entry = {
type: 'text',
content: content,
timestamp: new Date().getTime()
};
const exists = clipboardHistory.find(item => {
if (item.type === 'text') {
return item.content === content;
}
return item === content;
});
if (!exists) {
const newHistory = clipboardHistory.map(item => {
if (typeof item === 'string') {
return {
type: 'text',
content: item,
timestamp: new Date().getTime()
};
}
return item;
});
clipboardHistory = [entry, ...newHistory].slice(0, 20);
}
}
} else {
clipboardHistoryProcess.isLoading = false;
}
clipboardInitialized = true;
clipboardTypeProcess.isLoading = false;
root.updateFilter();
}
stdout: StdioCollector {}
}
function updateClipboardHistory() {
if (!clipboardTypeProcess.isLoading && !clipboardHistoryProcess.isLoading) {
clipboardTypeProcess.isLoading = true;
clipboardTypeProcess.command = ["wl-paste", "-l"];
clipboardTypeProcess.running = true;
}
}
id: appLauncherPanel id: appLauncherPanel
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
function isPinned(app) { function isPinned(app) {
return app && app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1; return app && app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1;
} }
function togglePin(app) { function togglePin(app) {
if (!app || !app.execString) return; if (!app || !app.execString) return;
var arr = Settings.settings.pinnedExecs ? Settings.settings.pinnedExecs.slice() : []; var arr = Settings.settings.pinnedExecs ? Settings.settings.pinnedExecs.slice() : [];
@ -71,6 +210,11 @@ PanelWithOverlay {
root.selectedIndex = 0; root.selectedIndex = 0;
} }
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
Rectangle { Rectangle {
id: root id: root
width: 460 width: 460
@ -103,9 +247,11 @@ PanelWithOverlay {
appLauncherPanel.visible = false; appLauncherPanel.visible = false;
} }
} }
function isMathExpression(str) { function isMathExpression(str) {
return /^[-+*/().0-9\s]+$/.test(str); return /^[-+*/().0-9\s]+$/.test(str);
} }
function safeEval(expr) { function safeEval(expr) {
try { try {
return Function('return (' + expr + ')')(); return Function('return (' + expr + ')')();
@ -113,13 +259,114 @@ PanelWithOverlay {
return undefined; return undefined;
} }
} }
function updateFilter() { function updateFilter() {
var query = searchField.text ? searchField.text.toLowerCase() : ""; var query = searchField.text ? searchField.text.toLowerCase() : "";
var apps = root.appModel.slice(); var apps = root.appModel.slice();
var results = []; var results = [];
// Calculator mode: starts with '='
if (query.startsWith("=")) {
var expr = searchField.text.slice(1).trim(); if (query === ">") {
results.push({
isCommand: true,
name: ">calc",
content: "Calculator - evaluate mathematical expressions",
icon: "calculate",
execute: function() {
searchField.text = ">calc ";
searchField.cursorPosition = searchField.text.length;
}
});
results.push({
isCommand: true,
name: ">clip",
content: "Clipboard history - browse and restore clipboard items",
icon: "content_paste",
execute: function() {
searchField.text = ">clip ";
searchField.cursorPosition = searchField.text.length;
}
});
root.filteredApps = results;
return;
}
if (query.startsWith(">clip")) {
if (!clipboardInitialized) {
updateClipboardHistory();
}
const searchTerm = query.slice(5).trim();
clipboardHistory.forEach(function(clip, index) {
let searchContent = clip.type === 'image' ?
clip.mimeType :
clip.content || clip; // Support both new object format and old string format
if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) {
let entry;
if (clip.type === 'image') {
entry = {
isClipboard: true,
name: "Image from " + new Date(clip.timestamp).toLocaleTimeString(),
content: "Image: " + clip.mimeType,
icon: "image",
type: 'image',
data: clip.data,
execute: function() {
// Convert base64 image data back to binary and copy to clipboard
const base64Data = clip.data.split(',')[1];
clipboardTypeProcess.command = ["sh", "-c", `echo '${base64Data}' | base64 -d | wl-copy -t '${clip.mimeType}'`];
clipboardTypeProcess.running = true;
}
};
} else {
const textContent = clip.content || clip; // Support both new object format and old string format
let displayContent = textContent;
let previewContent = "";
// Clean up whitespace for display
displayContent = displayContent.replace(/\s+/g, ' ').trim();
// Truncate long content and show preview
if (displayContent.length > 50) {
previewContent = displayContent;
// Show first line or first 50 characters as title
displayContent = displayContent.split('\n')[0].substring(0, 50) + "...";
}
entry = {
isClipboard: true,
name: displayContent,
content: previewContent || textContent,
icon: "content_paste",
execute: function() {
Quickshell.execDetached(["sh", "-c", "echo -n '" + textContent.replace(/'/g, "'\\''") + "' | wl-copy"]);
}
};
}
results.push(entry);
}
});
if (results.length === 0) {
results.push({
isClipboard: true,
name: "No clipboard history",
content: "No matching clipboard entries found",
icon: "content_paste_off"
});
}
root.filteredApps = results;
return;
}
if (query.startsWith(">calc")) {
var expr = searchField.text.slice(5).trim();
if (expr && isMathExpression(expr)) { if (expr && isMathExpression(expr)) {
var value = safeEval(expr); var value = safeEval(expr);
if (value !== undefined && value !== null && value !== "") { if (value !== undefined && value !== null && value !== "") {
@ -132,8 +379,27 @@ PanelWithOverlay {
}); });
} }
} }
var pinned = [];
var unpinned = [];
for (var i = 0; i < results.length; ++i) {
var app = results[i];
if (app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1) {
pinned.push(app);
} else {
unpinned.push(app);
}
}
// Sort pinned apps alphabetically for consistent display
pinned.sort(function(a, b) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
root.filteredApps = pinned.concat(unpinned);
root.selectedIndex = 0;
return;
} }
if (!query || query.startsWith("=")) { if (!query) {
results = results.concat(apps.sort(function (a, b) { results = results.concat(apps.sort(function (a, b) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
})); }));
@ -145,7 +411,7 @@ PanelWithOverlay {
return r.obj; return r.obj;
})); }));
} }
// Pinning logic: split into pinned and unpinned
var pinned = []; var pinned = [];
var unpinned = []; var unpinned = [];
for (var i = 0; i < results.length; ++i) { for (var i = 0; i < results.length; ++i) {
@ -163,10 +429,12 @@ PanelWithOverlay {
root.filteredApps = pinned.concat(unpinned); root.filteredApps = pinned.concat(unpinned);
root.selectedIndex = 0; root.selectedIndex = 0;
} }
function selectNext() { function selectNext() {
if (filteredApps.length > 0) if (filteredApps.length > 0)
selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1); selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1);
} }
function selectPrev() { function selectPrev() {
if (filteredApps.length > 0) if (filteredApps.length > 0)
selectedIndex = Math.max(selectedIndex - 1, 0); selectedIndex = Math.max(selectedIndex - 1, 0);
@ -184,6 +452,10 @@ PanelWithOverlay {
Quickshell.clipboardText = String(modelData.result); Quickshell.clipboardText = String(modelData.result);
Quickshell.execDetached(["notify-send", "Calculator Result", `${modelData.expr} = ${modelData.result} (copied to clipboard)`]); Quickshell.execDetached(["notify-send", "Calculator Result", `${modelData.expr} = ${modelData.result} (copied to clipboard)`]);
}); });
} else if (modelData.isCommand) {
modelData.execute();
return;
} else if (modelData.runInTerminal && termEmu){ } else if (modelData.runInTerminal && termEmu){
Quickshell.execDetached([termEmu, "-e", modelData.execString.trim()]); Quickshell.execDetached([termEmu, "-e", modelData.execString.trim()]);
} else if (modelData.execute) { } else if (modelData.execute) {
@ -202,281 +474,356 @@ PanelWithOverlay {
Component.onCompleted: updateFilter() Component.onCompleted: updateFilter()
ColumnLayout { RowLayout {
anchors.left: parent.left anchors.fill: parent
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: 32 anchors.margins: 32
spacing: 18 spacing: 18
// Search Bar
Rectangle { Rectangle {
id: searchBar id: previewPanel
color: Theme.surfaceVariant Layout.preferredWidth: 200
Layout.fillHeight: true
color: Theme.surface
radius: 20 radius: 20
height: 48 visible: false
Layout.fillWidth: true
border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: searchField.activeFocus ? 2 : 1
RowLayout { Rectangle {
anchors.left: parent.left anchors.fill: parent
anchors.right: parent.right anchors.margins: 16
anchors.verticalCenter: parent.verticalCenter color: "transparent"
anchors.leftMargin: 14 clip: true
anchors.rightMargin: 14
spacing: 10
Text {
text: "search"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeHeader
color: searchField.activeFocus ? Theme.accentPrimary : Theme.textSecondary
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter
}
TextField {
id: searchField
placeholderText: "Search apps..."
color: Theme.textPrimary
placeholderTextColor: Theme.textSecondary
background: null
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeBody
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
onTextChanged: root.updateFilter()
selectedTextColor: Theme.onAccent
selectionColor: Theme.accentPrimary
padding: 0
verticalAlignment: TextInput.AlignVCenter
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
font.bold: true
Component.onCompleted: contentItem.cursorColor = Theme.textPrimary
onActiveFocusChanged: contentItem.cursorColor = Theme.textPrimary
Keys.onDownPressed: root.selectNext() Image {
Keys.onUpPressed: root.selectPrev() id: previewImage
Keys.onEnterPressed: root.activateSelected() anchors.fill: parent
Keys.onReturnPressed: root.activateSelected() fillMode: Image.PreserveAspectFit
Keys.onEscapePressed: appLauncherPanel.hidePanel() asynchronous: true
} cache: true
} smooth: true
Behavior on border.color {
ColorAnimation {
duration: 120
}
}
Behavior on border.width {
NumberAnimation {
duration: 120
} }
} }
} }
// App List Card
Rectangle { ColumnLayout {
color: Theme.surface
radius: 20
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
clip: true spacing: 18
property int innerPadding: 16
Item {
anchors.top: parent.top Rectangle {
anchors.left: parent.left id: searchBar
anchors.right: parent.right color: Theme.surfaceVariant
height: parent.innerPadding radius: 20
visible: false height: 48
Layout.fillWidth: true
border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: searchField.activeFocus ? 2 : 1
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 14
anchors.rightMargin: 14
spacing: 10
Text {
text: "search"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeHeader * Theme.scale(Screen)
color: searchField.activeFocus ? Theme.accentPrimary : Theme.textSecondary
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter
}
TextField {
id: searchField
placeholderText: "Search apps..."
color: Theme.textPrimary
placeholderTextColor: Theme.textSecondary
background: null
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
onTextChanged: root.updateFilter()
selectedTextColor: Theme.onAccent
selectionColor: Theme.accentPrimary
padding: 0
verticalAlignment: TextInput.AlignVCenter
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
font.bold: true
Component.onCompleted: contentItem.cursorColor = Theme.textPrimary
onActiveFocusChanged: contentItem.cursorColor = Theme.textPrimary
Keys.onDownPressed: root.selectNext()
Keys.onUpPressed: root.selectPrev()
Keys.onEnterPressed: root.activateSelected()
Keys.onReturnPressed: root.activateSelected()
Keys.onEscapePressed: appLauncherPanel.hidePanel()
}
}
Behavior on border.color {
ColorAnimation {
duration: 120
}
}
Behavior on border.width {
NumberAnimation {
duration: 120
}
}
} }
ListView {
id: appList
anchors.fill: parent
anchors.margins: parent.innerPadding
spacing: 2
model: root.filteredApps
currentIndex: root.selectedIndex
delegate: Item {
id: appDelegate
width: appList.width
height: 48
property bool hovered: mouseArea.containsMouse
property bool isSelected: index === root.selectedIndex
Rectangle { Rectangle {
anchors.fill: parent color: Theme.surface
color: (hovered || isSelected) radius: 20
? Theme.accentPrimary Layout.fillWidth: true
: (appLauncherPanel.isPinned(modelData) ? Theme.surfaceVariant : "transparent") Layout.fillHeight: true
radius: 12 clip: true
border.color: appLauncherPanel.isPinned(modelData) property int innerPadding: 16
? "transparent"
: (hovered || isSelected ? Theme.accentPrimary : "transparent") ListView {
border.width: appLauncherPanel.isPinned(modelData) ? 0 : (hovered || isSelected ? 2 : 0) id: appList
Behavior on color { anchors.fill: parent
ColorAnimation { anchors.margins: parent.innerPadding
duration: 120 spacing: 2
} model: root.filteredApps
} currentIndex: root.selectedIndex
Behavior on border.color { delegate: Item {
ColorAnimation { id: appDelegate
duration: 120 width: appList.width
} height: (modelData.isClipboard || modelData.isCommand) ? 64 : 48
} property bool hovered: mouseArea.containsMouse
Behavior on border.width { property bool isSelected: index === root.selectedIndex
NumberAnimation {
duration: 120 Rectangle {
} anchors.fill: parent
} color: (hovered || isSelected)
} ? Theme.accentPrimary
: (appLauncherPanel.isPinned(modelData) ? Theme.surfaceVariant : "transparent")
radius: 12
border.color: appLauncherPanel.isPinned(modelData)
? "transparent"
: (hovered || isSelected ? Theme.accentPrimary : "transparent")
border.width: appLauncherPanel.isPinned(modelData) ? 0 : (hovered || isSelected ? 2 : 0)
Behavior on color {
ColorAnimation {
duration: 120
}
}
Behavior on border.color {
ColorAnimation {
duration: 120
}
}
Behavior on border.width {
NumberAnimation {
duration: 120
}
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 10
Item {
width: 28
height: 28
property bool iconLoaded: !modelData.isCalculator && !modelData.isClipboard && !modelData.isCommand && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error
Image {
id: clipboardImage
anchors.fill: parent
visible: modelData.type === 'image'
source: modelData.data || ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
MouseArea {
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onContainsMouseChanged: {
if (containsMouse && modelData.type === 'image') {
previewImage.source = modelData.data;
previewPanel.visible = true;
} else {
previewPanel.visible = false;
}
}
onMouseXChanged: mouse.accepted = false
onMouseYChanged: mouse.accepted = false
onClicked: mouse.accepted = false
}
}
IconImage {
id: iconImg
anchors.fill: parent
asynchronous: true
source: modelData.isCalculator ? "qrc:/icons/calculate.svg" :
modelData.isClipboard ? "qrc:/icons/" + modelData.icon + ".svg" :
modelData.isCommand ? "qrc:/icons/" + modelData.icon + ".svg" :
Quickshell.iconPath(modelData.icon, "application-x-executable")
visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded) && modelData.type !== 'image'
}
Text {
anchors.centerIn: parent
visible: !modelData.isCalculator && !modelData.isClipboard && !modelData.isCommand && !parent.iconLoaded && modelData.type !== 'image'
text: "broken_image"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeHeader * Theme.scale(Screen)
color: Theme.accentPrimary
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 1
Text {
text: modelData.name
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textPrimary)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
font.bold: hovered || isSelected
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) :
modelData.isClipboard ? modelData.content :
modelData.isCommand ? modelData.content :
(modelData.comment || modelData.genericName || "No description available")
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textSecondary : Theme.textSecondary)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen)
font.italic: !(modelData.comment || modelData.genericName)
opacity: modelData.isClipboard ? 0.8 : modelData.isCommand ? 0.9 : ((modelData.comment || modelData.genericName) ? 1.0 : 0.6)
elide: Text.ElideRight
maximumLineCount: (modelData.isClipboard || modelData.isCommand) ? 2 : 1
wrapMode: (modelData.isClipboard || modelData.isCommand) ? Text.WordWrap : Text.NoWrap
Layout.fillWidth: true
Layout.preferredHeight: (modelData.isClipboard || modelData.isCommand) ? implicitHeight : contentHeight
}
}
Item {
Layout.fillWidth: true
}
Text {
text: modelData.isCalculator ? "content_copy" : "chevron_right"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
color: (hovered || isSelected)
? Theme.onAccent
: (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textSecondary)
verticalAlignment: Text.AlignVCenter
Layout.rightMargin: 8
}
Item { width: 8; height: 1 }
}
Rectangle {
id: ripple
anchors.fill: parent
color: Theme.onAccent
opacity: 0.0
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (pinArea.containsMouse) return;
if (mouse.button === Qt.RightButton) {
appLauncherPanel.togglePin(modelData);
return;
}
ripple.opacity = 0.18;
rippleNumberAnimation.start();
root.selectedIndex = index;
root.activateSelected();
}
cursorShape: Qt.PointingHandCursor
onPressed: ripple.opacity = 0.18
onReleased: ripple.opacity = 0.0
}
NumberAnimation {
id: rippleNumberAnimation
target: ripple
property: "opacity"
to: 0.0
duration: 320
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 1
color: Theme.outline
opacity: index === appList.count - 1 ? 0 : 0.10
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 10
Item { Item {
width: 28 id: pinArea
height: 28 width: 28; height: 28
property bool iconLoaded: !modelData.isCalculator && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error z: 100
IconImage { anchors.right: parent.right
id: iconImg anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent anchors.fill: parent
asynchronous: true preventStealing: true
source: modelData.isCalculator ? "qrc:/icons/calculate.svg" : Quickshell.iconPath(modelData.icon, "application-x-executable") z: 100
visible: modelData.isCalculator || parent.iconLoaded hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
propagateComposedEvents: false
onClicked: {
appLauncherPanel.togglePin(modelData);
event.accepted = true;
}
} }
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
visible: !modelData.isCalculator && !parent.iconLoaded text: "star"
text: "broken_image"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeHeader font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
color: Theme.accentPrimary color: (parent.MouseArea.containsMouse || hovered || isSelected)
} ? Theme.onAccent
} : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textDisabled)
ColumnLayout {
Layout.fillWidth: true
spacing: 1
Text {
text: modelData.name
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textPrimary)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.bold: hovered || isSelected
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
Layout.fillWidth: true
} }
Text {
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : (modelData.comment || modelData.genericName || "No description available")
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textSecondary : Theme.textSecondary)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption
font.italic: !(modelData.comment || modelData.genericName)
opacity: (modelData.comment || modelData.genericName) ? 1.0 : 0.6
elide: Text.ElideRight
Layout.fillWidth: true
}
}
Item {
Layout.fillWidth: true
}
Text {
text: modelData.isCalculator ? "content_copy" : "chevron_right"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeBody
color: (hovered || isSelected)
? Theme.onAccent
: (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textSecondary)
verticalAlignment: Text.AlignVCenter
Layout.rightMargin: 8 // Add margin to separate from star
}
// Add a spacing item between chevron and star
Item { width: 8; height: 1 }
}
Rectangle {
id: ripple
anchors.fill: parent
color: Theme.onAccent
opacity: 0.0
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
// Prevent app launch if click is inside pinArea
if (pinArea.containsMouse) return;
if (mouse.button === Qt.RightButton) {
appLauncherPanel.togglePin(modelData);
return;
}
ripple.opacity = 0.18;
rippleNumberAnimation.start();
root.selectedIndex = index;
root.activateSelected();
}
cursorShape: Qt.PointingHandCursor
onPressed: ripple.opacity = 0.18
onReleased: ripple.opacity = 0.0
}
NumberAnimation {
id: rippleNumberAnimation
target: ripple
property: "opacity"
to: 0.0
duration: 320
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 1
color: Theme.outline
opacity: index === appList.count - 1 ? 0 : 0.10
}
// Pin/Unpin button (move to last child for stacking)
Item {
id: pinArea
width: 28; height: 28
z: 100 // Ensure above everything else
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
preventStealing: true
z: 100
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
propagateComposedEvents: false
onClicked: {
appLauncherPanel.togglePin(modelData);
event.accepted = true;
}
}
Text {
anchors.centerIn: parent
text: "star"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeSmall
color: (parent.MouseArea.containsMouse || hovered || isSelected)
? Theme.onAccent
: (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textDisabled)
verticalAlignment: Text.AlignVCenter
} }
} }
} }
@ -484,25 +831,5 @@ PanelWithOverlay {
} }
} }
} }
Corners {
id: launcherCornerRight
position: "bottomleft"
size: 1.1
fillColor: Theme.backgroundPrimary
anchors.top: root.top
offsetX: 416
offsetY: 0
}
Corners {
id: launcherCornerLeft
position: "bottomright"
size: 1.1
fillColor: Theme.backgroundPrimary
anchors.top: root.top
offsetX: -416
offsetY: 0
}
} }
} }

View file

@ -7,13 +7,67 @@ import qs.Settings
PanelWithOverlay { PanelWithOverlay {
id: ioSelector id: ioSelector
signal panelClosed()
property int tabIndex: 0 property int tabIndex: 0
property Item anchorItem: null property Item anchorItem: null
signal panelClosed()
function sinkNodes() {
let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) {
return n.isSink && n.audio && n.isStream === false;
}) : [];
if (Pipewire.defaultAudioSink)
nodes = nodes.slice().sort(function(a, b) {
if (a.id === Pipewire.defaultAudioSink.id)
return -1;
if (b.id === Pipewire.defaultAudioSink.id)
return 1;
return 0;
});
return nodes;
}
function sourceNodes() {
let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) {
return !n.isSink && n.audio && n.isStream === false;
}) : [];
if (Pipewire.defaultAudioSource)
nodes = nodes.slice().sort(function(a, b) {
if (a.id === Pipewire.defaultAudioSource.id)
return -1;
if (b.id === Pipewire.defaultAudioSource.id)
return 1;
return 0;
});
return nodes;
}
Component.onCompleted: {
if (Pipewire.nodes && Pipewire.nodes.values) {
for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
var n = Pipewire.nodes.values[i];
}
}
}
Component.onDestruction: {
}
onVisibleChanged: {
if (!visible)
panelClosed();
}
// Bind all Pipewire nodes so their properties are valid // Bind all Pipewire nodes so their properties are valid
PwObjectTracker { PwObjectTracker {
id: nodeTracker id: nodeTracker
objects: Pipewire.nodes objects: Pipewire.nodes
} }
@ -27,6 +81,11 @@ PanelWithOverlay {
anchors.topMargin: 4 anchors.topMargin: 4
anchors.rightMargin: 4 anchors.rightMargin: 4
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 16 anchors.margins: 16
@ -40,239 +99,257 @@ PanelWithOverlay {
Tabs { Tabs {
id: ioTabs id: ioTabs
tabsModel: [
{ label: "Output", icon: "volume_up" }, tabsModel: [{
{ label: "Input", icon: "mic" } "label": "Output",
] "icon": "volume_up"
}, {
"label": "Input",
"icon": "mic"
}]
currentIndex: tabIndex currentIndex: tabIndex
onTabChanged: { onTabChanged: {
tabIndex = currentIndex; tabIndex = currentIndex;
} }
} }
} }
// Add vertical space between tabs and entries // Add vertical space between tabs and entries
Item { height: 36; Layout.fillWidth: true } Item {
height: 36
Layout.fillWidth: true
}
// Output Devices // Output Devices
Flickable { Flickable {
id: sinkList id: sinkList
visible: tabIndex === 0 visible: tabIndex === 0
contentHeight: sinkColumn.height contentHeight: sinkColumn.height
clip: true clip: true
interactive: contentHeight > height interactive: contentHeight > height
width: parent.width width: parent.width
height: 220 height: 220
ScrollBar.vertical: ScrollBar {}
ColumnLayout { ColumnLayout {
id: sinkColumn id: sinkColumn
width: sinkList.width width: sinkList.width
spacing: 6 spacing: 6
Repeater { Repeater {
model: ioSelector.sinkNodes() model: ioSelector.sinkNodes()
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 36 height: 36
color: "transparent" color: "transparent"
radius: 6 radius: 6
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 6 anchors.margins: 6
spacing: 8 spacing: 8
Text { Text {
text: "volume_up" text: "volume_up"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 16 font.pixelSize: 16 * Theme.scale(Screen)
color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 1 spacing: 1
Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button
Text { Text {
text: modelData.nickname || modelData.description || modelData.name text: modelData.nickname || modelData.description || modelData.name
font.bold: true font.bold: true
font.pixelSize: 12 font.pixelSize: 12 * Theme.scale(Screen)
color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
Layout.fillWidth: true Layout.fillWidth: true
} }
Text { Text {
text: modelData.description !== modelData.nickname ? modelData.description : "" text: modelData.description !== modelData.nickname ? modelData.description : ""
font.pixelSize: 10 font.pixelSize: 10 * Theme.scale(Screen)
color: Theme.textSecondary color: Theme.textSecondary
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
Rectangle { Rectangle {
visible: Pipewire.preferredDefaultAudioSink !== modelData visible: Pipewire.preferredDefaultAudioSink !== modelData
width: 60; height: 20 width: 60
height: 20
radius: 4 radius: 4
color: Theme.accentPrimary color: Theme.accentPrimary
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "Set" text: "Set"
color: Theme.onAccent color: Theme.onAccent
font.pixelSize: 10 font.pixelSize: 10 * Theme.scale(Screen)
font.bold: true font.bold: true
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: Pipewire.preferredDefaultAudioSink = modelData onClicked: Pipewire.preferredDefaultAudioSink = modelData
} }
} }
Text { Text {
text: "(Current)" text: "(Current)"
visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id
color: Theme.accentPrimary color: Theme.accentPrimary
font.pixelSize: 10 font.pixelSize: 10 * Theme.scale(Screen)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
} }
} }
} }
} }
ScrollBar.vertical: ScrollBar {
}
} }
// Input Devices // Input Devices
Flickable { Flickable {
id: sourceList id: sourceList
visible: tabIndex === 1 visible: tabIndex === 1
contentHeight: sourceColumn.height contentHeight: sourceColumn.height
clip: true clip: true
interactive: contentHeight > height interactive: contentHeight > height
width: parent.width width: parent.width
height: 220 height: 220
ScrollBar.vertical: ScrollBar {}
ColumnLayout { ColumnLayout {
id: sourceColumn id: sourceColumn
width: sourceList.width width: sourceList.width
spacing: 6 spacing: 6
Repeater { Repeater {
model: ioSelector.sourceNodes() model: ioSelector.sourceNodes()
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 36 height: 36
color: "transparent" color: "transparent"
radius: 6 radius: 6
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 6 anchors.margins: 6
spacing: 8 spacing: 8
Text { Text {
text: "mic" text: "mic"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 16 font.pixelSize: 16 * Theme.scale(Screen)
color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 1 spacing: 1
Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button
Text { Text {
text: modelData.nickname || modelData.description || modelData.name text: modelData.nickname || modelData.description || modelData.name
font.bold: true font.bold: true
font.pixelSize: 12 font.pixelSize: 12 * Theme.scale(Screen)
color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
Layout.fillWidth: true Layout.fillWidth: true
} }
Text { Text {
text: modelData.description !== modelData.nickname ? modelData.description : "" text: modelData.description !== modelData.nickname ? modelData.description : ""
font.pixelSize: 10 font.pixelSize: 10 * Theme.scale(Screen)
color: Theme.textSecondary color: Theme.textSecondary
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
Rectangle { Rectangle {
visible: Pipewire.preferredDefaultAudioSource !== modelData visible: Pipewire.preferredDefaultAudioSource !== modelData
width: 60; height: 20 width: 60
height: 20
radius: 4 radius: 4
color: Theme.accentPrimary color: Theme.accentPrimary
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "Set" text: "Set"
color: Theme.onAccent color: Theme.onAccent
font.pixelSize: 10 font.pixelSize: 10 * Theme.scale(Screen)
font.bold: true font.bold: true
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: Pipewire.preferredDefaultAudioSource = modelData onClicked: Pipewire.preferredDefaultAudioSource = modelData
} }
} }
Text { Text {
text: "(Current)" text: "(Current)"
visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id
color: Theme.accentPrimary color: Theme.accentPrimary
font.pixelSize: 10 font.pixelSize: 10 * Theme.scale(Screen)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
} }
} }
} }
} }
}
}
}
function sinkNodes() { ScrollBar.vertical: ScrollBar {
let nodes = Pipewire.nodes && Pipewire.nodes.values }
? Pipewire.nodes.values.filter(function(n) {
return n.isSink && n.audio && n.isStream === false;
})
: [];
if (Pipewire.defaultAudioSink) {
nodes = nodes.slice().sort(function(a, b) {
if (a.id === Pipewire.defaultAudioSink.id) return -1;
if (b.id === Pipewire.defaultAudioSink.id) return 1;
return 0;
});
}
return nodes;
}
function sourceNodes() {
let nodes = Pipewire.nodes && Pipewire.nodes.values
? Pipewire.nodes.values.filter(function(n) {
return !n.isSink && n.audio && n.isStream === false;
})
: [];
if (Pipewire.defaultAudioSource) {
nodes = nodes.slice().sort(function(a, b) {
if (a.id === Pipewire.defaultAudioSource.id) return -1;
if (b.id === Pipewire.defaultAudioSource.id) return 1;
return 0;
});
}
return nodes;
}
Component.onCompleted: {
if (Pipewire.nodes && Pipewire.nodes.values) {
for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
var n = Pipewire.nodes.values[i];
} }
} }
} }
Connections { Connections {
target: Pipewire
function onReadyChanged() { function onReadyChanged() {
if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) { if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) {
for (var i = 0; i < Pipewire.nodes.values.length; ++i) { for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
@ -280,15 +357,14 @@ PanelWithOverlay {
} }
} }
} }
function onDefaultAudioSinkChanged() { function onDefaultAudioSinkChanged() {
} }
function onDefaultAudioSourceChanged() { function onDefaultAudioSourceChanged() {
} }
target: Pipewire
} }
Component.onDestruction: {
}
onVisibleChanged: {
if (!visible) panelClosed();
}
} }

282
Bar/Modules/Bluetooth.qml Normal file
View file

@ -0,0 +1,282 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Bluetooth
import qs.Settings
import qs.Components
Item {
id: root
width: Settings.settings.bluetoothEnabled ? 22 : 0
height: Settings.settings.bluetoothEnabled ? 22 : 0
property bool menuVisible: false
// Bluetooth icon/button
Item {
id: bluetoothIcon
width: 22; height: 22
visible: Settings.settings.bluetoothEnabled
// Check if any devices are currently connected
property bool hasConnectedDevices: {
if (!Bluetooth.defaultAdapter) return false;
for (let i = 0; i < Bluetooth.defaultAdapter.devices.count; i++) {
if (Bluetooth.defaultAdapter.devices.valueAt(i).connected) {
return true;
}
}
return false;
}
Text {
id: bluetoothText
anchors.centerIn: parent
text: {
if (!Bluetooth.defaultAdapter || !Bluetooth.defaultAdapter.enabled) {
return "bluetooth_disabled"
} else if (parent.hasConnectedDevices) {
return "bluetooth_connected"
} else {
return "bluetooth"
}
}
font.family: mouseAreaBluetooth.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 16 * Theme.scale(Screen)
color: mouseAreaBluetooth.containsMouse ? Theme.accentPrimary : Theme.textPrimary
}
MouseArea {
id: mouseAreaBluetooth
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!bluetoothMenuLoader.active) {
bluetoothMenuLoader.loading = true;
}
if (bluetoothMenuLoader.item) {
bluetoothMenuLoader.item.visible = !bluetoothMenuLoader.item.visible;
// Enable adapter and start discovery when menu opens
if (bluetoothMenuLoader.item.visible && Bluetooth.defaultAdapter) {
if (!Bluetooth.defaultAdapter.enabled) {
Bluetooth.defaultAdapter.enabled = true;
}
if (!Bluetooth.defaultAdapter.discovering) {
Bluetooth.defaultAdapter.discovering = true;
}
}
}
}
onEntered: bluetoothTooltip.tooltipVisible = true
onExited: bluetoothTooltip.tooltipVisible = false
}
}
StyledTooltip {
id: bluetoothTooltip
text: "Bluetooth Devices"
positionAbove: false
tooltipVisible: false
targetItem: bluetoothIcon
delay: 200
}
// LazyLoader for Bluetooth menu
LazyLoader {
id: bluetoothMenuLoader
loading: false
component: PanelWindow {
id: bluetoothMenu
implicitWidth: 320
implicitHeight: 480
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: 0
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
onVisibleChanged: {
// Stop discovery when menu closes to save battery
if (!visible && Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering) {
Bluetooth.defaultAdapter.discovering = false;
}
}
Rectangle {
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 12
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 16
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
text: "bluetooth"
font.family: "Material Symbols Outlined"
font.pixelSize: 24 * Theme.scale(Screen)
color: Theme.accentPrimary
}
Text {
text: "Bluetooth Devices"
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
IconButton {
icon: "close"
onClicked: {
bluetoothMenu.visible = false;
if (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering) {
Bluetooth.defaultAdapter.discovering = false;
}
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.outline
opacity: 0.12
}
ListView {
id: deviceList
Layout.fillWidth: true
Layout.fillHeight: true
model: Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.devices : []
spacing: 8
clip: true
delegate: Item {
width: parent.width
height: 48
Rectangle {
anchors.fill: parent
radius: 8
color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.44) : (deviceMouseArea.containsMouse ? Theme.highlight : "transparent")
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 8
Text {
text: modelData.connected ? "bluetooth" : "bluetooth_disabled"
font.family: "Material Symbols Outlined"
font.pixelSize: 18 * Theme.scale(Screen)
color: deviceMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Text {
text: {
let deviceName = modelData.name || modelData.deviceName || "Unknown Device";
// Hide MAC addresses and show "Unknown Device" instead
let macPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
if (macPattern.test(deviceName)) {
return "Unknown Device";
}
return deviceName;
}
color: deviceMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textPrimary)
font.pixelSize: 14 * Theme.scale(Screen)
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: {
let deviceName = modelData.name || modelData.deviceName || "";
let macPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
if (macPattern.test(deviceName)) {
// Show MAC address in subtitle for unnamed devices
return modelData.address + " • " + (modelData.paired ? "Paired" : "Available");
} else {
// Show only status for named devices
return modelData.paired ? "Paired" : "Available";
}
}
color: deviceMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
font.pixelSize: 11 * Theme.scale(Screen)
elide: Text.ElideRight
Layout.fillWidth: true
}
}
Item {
Layout.preferredWidth: 22
Layout.preferredHeight: 22
visible: modelData.pairing || modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting
Spinner {
visible: parent.visible
running: parent.visible
color: Theme.accentPrimary
anchors.centerIn: parent
size: 22
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
// Handle device actions: disconnect, pair, or connect
if (modelData.connected) {
modelData.disconnect();
} else if (!modelData.paired) {
modelData.pair();
} else {
modelData.connect();
}
}
}
}
}
}
// Discovering indicator
RowLayout {
Layout.fillWidth: true
spacing: 8
visible: Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering
Text {
text: "Scanning for devices..."
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
}
Spinner {
running: true
color: Theme.accentPrimary
size: 16
}
}
}
}
}
}
}

View file

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

View file

@ -15,7 +15,7 @@ Rectangle {
text: Time.time text: Time.time
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.weight: Font.Bold font.weight: Font.Bold
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
color: Theme.textPrimary color: Theme.textPrimary
anchors.centerIn: parent anchors.centerIn: parent
} }

View file

@ -128,7 +128,7 @@ PopupWindow {
color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled; color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled;
text: modelData?.text ?? ""; text: modelData?.text ?? "";
font.family: Theme.fontFamily; font.family: Theme.fontFamily;
font.pixelSize: Theme.fontSizeSmall; font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen);
verticalAlignment: Text.AlignVCenter; verticalAlignment: Text.AlignVCenter;
elide: Text.ElideRight; elide: Text.ElideRight;
} }
@ -145,7 +145,7 @@ PopupWindow {
// Material Symbols Outlined chevron right for submenu // Material Symbols Outlined chevron right for submenu
text: modelData?.hasChildren ? "menu" : ""; text: modelData?.hasChildren ? "menu" : "";
font.family: "Material Symbols Outlined"; font.family: "Material Symbols Outlined";
font.pixelSize: 18; font.pixelSize: 18 * Theme.scale(Screen);
verticalAlignment: Text.AlignVCenter; verticalAlignment: Text.AlignVCenter;
visible: modelData?.hasChildren ?? false; visible: modelData?.hasChildren ?? false;
color: Theme.textPrimary; color: Theme.textPrimary;
@ -362,7 +362,7 @@ PopupWindow {
color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled; color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled;
text: modelData?.text ?? ""; text: modelData?.text ?? "";
font.family: Theme.fontFamily; font.family: Theme.fontFamily;
font.pixelSize: Theme.fontSizeSmall; font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen);
verticalAlignment: Text.AlignVCenter; verticalAlignment: Text.AlignVCenter;
elide: Text.ElideRight; elide: Text.ElideRight;
} }
@ -378,7 +378,7 @@ PopupWindow {
Text { Text {
text: modelData?.hasChildren ? "\uE5CC" : ""; text: modelData?.hasChildren ? "\uE5CC" : "";
font.family: "Material Symbols Outlined"; font.family: "Material Symbols Outlined";
font.pixelSize: 18; font.pixelSize: 18 * Theme.scale(Screen);
verticalAlignment: Text.AlignVCenter; verticalAlignment: Text.AlignVCenter;
visible: modelData?.hasChildren ?? false; visible: modelData?.hasChildren ?? false;
color: Theme.textPrimary; color: Theme.textPrimary;

View file

@ -10,7 +10,7 @@ import qs.Components
Item { Item {
id: mediaControl id: mediaControl
width: visible ? mediaRow.width : 0 width: visible ? mediaRow.width : 0
height: 36 height: 36 * Theme.scale(Screen)
visible: Settings.settings.showMediaInBar && MusicManager.currentPlayer visible: Settings.settings.showMediaInBar && MusicManager.currentPlayer
RowLayout { RowLayout {
@ -20,8 +20,8 @@ Item {
Item { Item {
id: albumArtContainer id: albumArtContainer
width: 24 width: 24 * Theme.scale(Screen)
height: 24 height: 24 * Theme.scale(Screen)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
// Circular spectrum visualizer // Circular spectrum visualizer
@ -29,8 +29,8 @@ Item {
id: spectrum id: spectrum
values: MusicManager.cavaValues values: MusicManager.cavaValues
anchors.centerIn: parent anchors.centerIn: parent
innerRadius: 10 innerRadius: 10 * Theme.scale(Screen)
outerRadius: 18 outerRadius: 18 * Theme.scale(Screen)
fillColor: Theme.accentPrimary fillColor: Theme.accentPrimary
strokeColor: Theme.accentPrimary strokeColor: Theme.accentPrimary
strokeWidth: 0 strokeWidth: 0
@ -40,10 +40,10 @@ Item {
// Album art image // Album art image
Rectangle { Rectangle {
id: albumArtwork id: albumArtwork
width: 20 width: 20 * Theme.scale(Screen)
height: 20 height: 20 * Theme.scale(Screen)
anchors.centerIn: parent anchors.centerIn: parent
radius: 12 // circle radius: 12 * Theme.scale(Screen) // circle
color: Qt.darker(Theme.surface, 1.1) color: Qt.darker(Theme.surface, 1.1)
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
border.width: 1 border.width: 1
@ -79,7 +79,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
text: "music_note" text: "music_note"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 14 font.pixelSize: 14 * Theme.scale(Screen)
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4) color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4)
visible: !albumArt.visible visible: !albumArt.visible
} }
@ -96,7 +96,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
text: MusicManager.isPlaying ? "pause" : "play_arrow" text: MusicManager.isPlaying ? "pause" : "play_arrow"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 14 font.pixelSize: 14 * Theme.scale(Screen)
color: "white" color: "white"
} }
} }
@ -117,7 +117,7 @@ Item {
text: MusicManager.trackTitle + " - " + MusicManager.trackArtist text: MusicManager.trackTitle + " - " + MusicManager.trackArtist
color: Theme.textPrimary color: Theme.textPrimary
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: 12 font.pixelSize: 12 * Theme.scale(Screen)
elide: Text.ElideRight elide: Text.ElideRight
Layout.maximumWidth: 300 Layout.maximumWidth: 300
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter

View file

@ -0,0 +1,71 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Settings
import qs.Components
import qs.Widgets.SettingsWindow
Item {
id: root
width: 22
height: 22
Rectangle {
id: button
anchors.fill: parent
color: "transparent"
radius: width / 2
Text {
anchors.centerIn: parent
text: "settings"
font.family: "Material Symbols Outlined"
font.pixelSize: 16 * Theme.scale(Screen)
color: mouseArea.containsMouse ? Theme.accentPrimary : Theme.textPrimary
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!settingsWindowLoader.active) {
// Start loading the settings window
settingsWindowLoader.loading = true;
}
if (settingsWindowLoader.item) {
// Toggle visibility
if (settingsWindowLoader.item.visible) {
settingsWindowLoader.item.visible = false;
} else {
settingsWindowLoader.item.visible = true;
}
}
}
}
StyledTooltip {
text: "Settings"
targetItem: mouseArea
tooltipVisible: mouseArea.containsMouse
}
}
// LazyLoader for SettingsWindow
LazyLoader {
id: settingsWindowLoader
loading: false
component: SettingsWindow {
// Handle window closure - just hide it, don't destroy
onVisibleChanged: {
if (!visible) {
// Window is hidden, but keep it loaded for reuse
}
}
}
}
}

View file

@ -8,8 +8,6 @@ Row {
spacing: 10 spacing: 10
visible: Settings.settings.showSystemInfoInBar visible: Settings.settings.showSystemInfoInBar
// The width calculation below is required to ensure our row width is an integer.
// If omitted the next component to the right might get blurry (Taskbar icons).
width: Math.floor(cpuUsageLayout.width + cpuTempLayout.width + memoryUsageLayout.width + (2 * 10)) width: Math.floor(cpuUsageLayout.width + cpuTempLayout.width + memoryUsageLayout.width + (2 * 10))
Row { Row {
@ -19,7 +17,7 @@ Row {
Text { Text {
id: cpuUsageIcon id: cpuUsageIcon
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeBody font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
text: "speed" text: "speed"
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -29,7 +27,7 @@ Row {
Text { Text {
id: cpuUsageText id: cpuUsageText
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
color: Theme.textPrimary color: Theme.textPrimary
text: Sysinfo.cpuUsageStr text: Sysinfo.cpuUsageStr
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -43,7 +41,7 @@ Row {
spacing: 3 spacing: 3
Text { Text {
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeBody font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
text: "thermometer" text: "thermometer"
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -52,7 +50,7 @@ Row {
Text { Text {
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
color: Theme.textPrimary color: Theme.textPrimary
text: Sysinfo.cpuTempStr text: Sysinfo.cpuTempStr
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -66,7 +64,7 @@ Row {
spacing: 3 spacing: 3
Text { Text {
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeBody font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
text: "memory" text: "memory"
color: Theme.accentPrimary color: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
@ -75,7 +73,7 @@ Row {
Text { Text {
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
color: Theme.textPrimary color: Theme.textPrimary
text: Sysinfo.memoryUsageStr text: Sysinfo.memoryUsageStr
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter

View file

@ -21,43 +21,27 @@ Row {
Repeater { Repeater {
model: systemTray.items model: systemTray.items
delegate: Item { delegate: Item {
width: 24 width: 24 * Theme.scale(Screen)
height: 24 height: 24 * Theme.scale(Screen)
// Hide Spotify icon, or adjust to your liking
visible: modelData && modelData.id !== "spotify" visible: modelData
property bool isHovered: trayMouseArea.containsMouse property bool isHovered: trayMouseArea.containsMouse
// Hover scale animation // No animations - static display
scale: isHovered ? 1.15 : 1.0
Behavior on scale {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
// Subtle rotation on hover
rotation: isHovered ? 5 : 0
Behavior on rotation {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Rectangle { Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
width: 16 width: 16 * Theme.scale(Screen)
height: 16 height: 16 * Theme.scale(Screen)
radius: 6 radius: 6 * Theme.scale(Screen)
color: "transparent" color: "transparent"
clip: true clip: true
IconImage { IconImage {
id: trayIcon id: trayIcon
anchors.centerIn: parent anchors.centerIn: parent
width: 16 width: 16 * Theme.scale(Screen)
height: 16 height: 16 * Theme.scale(Screen)
smooth: false smooth: false
asynchronous: true asynchronous: true
backer.fillMode: Image.PreserveAspectFit backer.fillMode: Image.PreserveAspectFit
@ -74,13 +58,6 @@ Row {
return icon; return icon;
} }
opacity: status === Image.Ready ? 1 : 0 opacity: status === Image.Ready ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Component.onCompleted: {} Component.onCompleted: {}
} }
} }
@ -122,7 +99,7 @@ Row {
if (modelData.hasMenu && modelData.menu && trayMenu) { if (modelData.hasMenu && modelData.menu && trayMenu) {
// Anchor the menu to the tray icon item (parent) and position it below the icon // Anchor the menu to the tray icon item (parent) and position it below the icon
const menuX = (width / 2) - (trayMenu.width / 2); const menuX = (width / 2) - (trayMenu.width / 2);
const menuY = height + 20; const menuY = height + 20 * Theme.scale(Screen);
trayMenu.menu = modelData.menu; trayMenu.menu = modelData.menu;
trayMenu.showAt(parent, menuX, menuY); trayMenu.showAt(parent, menuX, menuY);
} else } else

View file

@ -83,7 +83,7 @@ Item {
visible: !appIcon.visible visible: !appIcon.visible
text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?"
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Math.max(10, Settings.settings.taskbarIconSize * 0.4375) font.pixelSize: Math.max(10, Settings.settings.taskbarIconSize * 0.4375 * Theme.scale(Screen))
font.bold: true font.bold: true
color: appButton.isActive ? Theme.onAccent : Theme.textPrimary color: appButton.isActive ? Theme.onAccent : Theme.textPrimary
} }

380
Bar/Modules/Wifi.qml Normal file
View file

@ -0,0 +1,380 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Settings
import qs.Components
import qs.Services
Item {
id: root
width: Settings.settings.wifiEnabled ? 22 : 0
height: Settings.settings.wifiEnabled ? 22 : 0
property bool menuVisible: false
property string passwordPromptSsid: ""
property string passwordInput: ""
property bool showPasswordPrompt: false
Network {
id: network
}
// WiFi icon/button
Item {
id: wifiIcon
width: 22; height: 22
visible: Settings.settings.wifiEnabled
property int currentSignal: {
let maxSignal = 0;
for (const net in network.networks) {
if (network.networks[net].connected && network.networks[net].signal > maxSignal) {
maxSignal = network.networks[net].signal;
}
}
return maxSignal;
}
Text {
id: wifiText
anchors.centerIn: parent
text: {
let connected = false;
for (const net in network.networks) {
if (network.networks[net].connected) {
connected = true;
break;
}
}
return connected ? network.signalIcon(parent.currentSignal) : "wifi_off"
}
font.family: mouseAreaWifi.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 16 * Theme.scale(Screen)
color: mouseAreaWifi.containsMouse ? Theme.accentPrimary : Theme.textPrimary
}
MouseArea {
id: mouseAreaWifi
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!wifiMenuLoader.active) {
wifiMenuLoader.loading = true;
}
if (wifiMenuLoader.item) {
wifiMenuLoader.item.visible = !wifiMenuLoader.item.visible;
if (wifiMenuLoader.item.visible) {
network.onMenuOpened();
} else {
network.onMenuClosed();
}
}
}
onEntered: wifiTooltip.tooltipVisible = true
onExited: wifiTooltip.tooltipVisible = false
}
}
StyledTooltip {
id: wifiTooltip
text: "WiFi Networks"
positionAbove: false
tooltipVisible: false
targetItem: wifiIcon
delay: 200
}
// LazyLoader for WiFi menu
LazyLoader {
id: wifiMenuLoader
loading: false
component: PanelWindow {
id: wifiMenu
implicitWidth: 320
implicitHeight: 480
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: 0
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Rectangle {
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 12
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 16
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
text: "wifi"
font.family: "Material Symbols Outlined"
font.pixelSize: 24 * Theme.scale(Screen)
color: Theme.accentPrimary
}
Text {
text: "WiFi Networks"
font.pixelSize: 18 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
IconButton {
icon: "refresh"
onClicked: network.refreshNetworks()
}
IconButton {
icon: "close"
onClicked: {
wifiMenu.visible = false;
network.onMenuClosed();
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.outline
opacity: 0.12
}
ListView {
id: networkList
Layout.fillWidth: true
Layout.fillHeight: true
model: Object.values(network.networks)
spacing: 8
clip: true
delegate: Item {
width: parent.width
height: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 108 : 48 // 48 for network + 60 for password prompt
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 48
radius: 8
color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.44) : (networkMouseArea.containsMouse ? Theme.highlight : "transparent")
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 8
Text {
text: network.signalIcon(modelData.signal)
font.family: "Material Symbols Outlined"
font.pixelSize: 18 * Theme.scale(Screen)
color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Text {
text: modelData.ssid || "Unknown Network"
color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textPrimary)
font.pixelSize: 14 * Theme.scale(Screen)
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: modelData.security && modelData.security !== "--" ? modelData.security : "Open"
color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
font.pixelSize: 11 * Theme.scale(Screen)
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" && network.connectError.length > 0
text: network.connectError
color: Theme.error
font.pixelSize: 11 * Theme.scale(Screen)
elide: Text.ElideRight
Layout.fillWidth: true
}
}
Item {
Layout.preferredWidth: 22
Layout.preferredHeight: 22
visible: network.connectStatusSsid === modelData.ssid && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid)
Spinner {
visible: network.connectingSsid === modelData.ssid
running: network.connectingSsid === modelData.ssid
color: Theme.accentPrimary
anchors.centerIn: parent
size: 22
}
Text {
visible: network.connectStatus === "success" && !network.connectingSsid
text: "check_circle"
font.family: "Material Symbols Outlined"
font.pixelSize: 18 * Theme.scale(Screen)
color: "#43a047"
anchors.centerIn: parent
}
Text {
visible: network.connectStatus === "error" && !network.connectingSsid
text: "error"
font.family: "Material Symbols Outlined"
font.pixelSize: 18 * Theme.scale(Screen)
color: Theme.error
anchors.centerIn: parent
}
}
Text {
visible: modelData.connected
text: "connected"
color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
font.pixelSize: 11 * Theme.scale(Screen)
}
}
MouseArea {
id: networkMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (modelData.connected) {
network.disconnectNetwork(modelData.ssid);
} else if (network.isSecured(modelData.security) && !modelData.existing) {
passwordPromptSsid = modelData.ssid;
showPasswordPrompt = true;
passwordInput = ""; // Clear previous input
Qt.callLater(function() {
passwordInputField.forceActiveFocus();
});
} else {
network.connectNetwork(modelData.ssid, modelData.security);
}
}
}
}
// Password prompt section
Rectangle {
id: passwordPromptSection
Layout.fillWidth: true
Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0
Layout.margins: 8
visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt
color: Theme.surfaceVariant
radius: 8
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 10
Item {
Layout.fillWidth: true
Layout.preferredHeight: 36
Rectangle {
anchors.fill: parent
radius: 8
color: "transparent"
border.color: passwordInputField.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: passwordInputField
anchors.fill: parent
anchors.margins: 12
text: passwordInput
font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
echoMode: TextInput.Password
onTextChanged: passwordInput = text
onAccepted: {
network.submitPassword(passwordPromptSsid, passwordInput);
showPasswordPrompt = false;
}
MouseArea {
id: passwordInputMouseArea
anchors.fill: parent
onClicked: passwordInputField.forceActiveFocus()
}
}
}
}
Rectangle {
Layout.preferredWidth: 80
Layout.preferredHeight: 36
radius: 18
color: Theme.accentPrimary
border.color: Theme.accentPrimary
border.width: 0
opacity: 1.0
Behavior on color {
ColorAnimation {
duration: 100
}
}
MouseArea {
anchors.fill: parent
onClicked: {
network.submitPassword(passwordPromptSsid, passwordInput);
showPasswordPrompt = false;
}
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.color = Qt.darker(Theme.accentPrimary, 1.1)
onExited: parent.color = Theme.accentPrimary
}
Text {
anchors.centerIn: parent
text: "Connect"
color: Theme.backgroundPrimary
font.pixelSize: 14 * Theme.scale(Screen)
font.bold: true
}
}
}
}
}
}
}
}
}
}
}
}

View file

@ -40,7 +40,7 @@ Item {
return total; return total;
} }
height: 36 height: 36 * Theme.scale(Screen)
Component.onCompleted: { Component.onCompleted: {
localWorkspaces.clear(); localWorkspaces.clear();
@ -115,14 +115,14 @@ Item {
Rectangle { Rectangle {
id: workspaceBackground id: workspaceBackground
width: parent.width - 15 width: parent.width - 15 * Theme.scale(Screen)
height: 26 height: 26 * Theme.scale(Screen)
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
radius: 12 radius: 12 * Theme.scale(Screen)
color: Theme.surfaceVariant color: Theme.surfaceVariant
border.color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.1) border.color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.1)
border.width: 1 border.width: 1 * Theme.scale(Screen)
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
shadowColor: "black" shadowColor: "black"
@ -145,7 +145,7 @@ Item {
model: localWorkspaces model: localWorkspaces
Item { Item {
id: workspacePillContainer id: workspacePillContainer
height: 12 height: 12 * Theme.scale(Screen)
width: { width: {
if (model.isFocused) if (model.isFocused)
return 44; return 44;
@ -245,12 +245,12 @@ Item {
Rectangle { Rectangle {
id: pillBurst id: pillBurst
anchors.centerIn: workspacePillContainer anchors.centerIn: workspacePillContainer
width: workspacePillContainer.width + 18 * root.masterProgress width: workspacePillContainer.width + 18 * root.masterProgress * Theme.scale(Screen)
height: workspacePillContainer.height + 18 * root.masterProgress height: workspacePillContainer.height + 18 * root.masterProgress * Theme.scale(Screen)
radius: width / 2 radius: width / 2
color: "transparent" color: "transparent"
border.color: root.effectColor border.color: root.effectColor
border.width: 2 + 6 * (1.0 - root.masterProgress) border.width: (2 + 6 * (1.0 - root.masterProgress)) * Theme.scale(Screen)
opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0
visible: root.effectsActive && model.isFocused visible: root.effectsActive && model.isFocused
z: 1 z: 1

View file

@ -43,7 +43,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
text: "person" text: "person"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 24 font.pixelSize: 24 * Theme.scale(Screen)
color: Theme.onAccent color: Theme.onAccent
visible: Settings.settings.profileImage === undefined || Settings.settings.profileImage === "" visible: Settings.settings.profileImage === undefined || Settings.settings.profileImage === ""
z: 0 z: 0

View file

@ -5,21 +5,20 @@ Rectangle {
id: circularProgressBar id: circularProgressBar
color: "transparent" color: "transparent"
// Properties property real progress: 0.0
property real progress: 0.0 // 0.0 to 1.0
property int size: 80 property int size: 80
property color backgroundColor: Theme.surfaceVariant property color backgroundColor: Theme.surfaceVariant
property color progressColor: Theme.accentPrimary property color progressColor: Theme.accentPrimary
property int strokeWidth: 6 property int strokeWidth: 6 * Theme.scale(Screen)
property bool showText: true property bool showText: true
property string units: "%" property string units: "%"
property string text: Math.round(progress * 100) + units property string text: Math.round(progress * 100) + units
property int textSize: 10 property int textSize: 10 * Theme.scale(Screen)
property color textColor: Theme.textPrimary property color textColor: Theme.textPrimary
// Notch properties // Notch properties
property bool hasNotch: false property bool hasNotch: false
property real notchSize: 0.25 // Size of the notch as a fraction of the circle property real notchSize: 0.25
property string notchIcon: "" property string notchIcon: ""
property int notchIconSize: 12 property int notchIconSize: 12
property color notchIconColor: Theme.accentPrimary property color notchIconColor: Theme.accentPrimary
@ -32,6 +31,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
onPaint: { onPaint: {
// Setup canvas context and calculate dimensions
var ctx = getContext("2d") var ctx = getContext("2d")
var centerX = width / 2 var centerX = width / 2
var centerY = height / 2 var centerY = height / 2
@ -41,25 +41,22 @@ Rectangle {
var notchStartAngle = -notchAngle / 2 var notchStartAngle = -notchAngle / 2
var notchEndAngle = notchAngle / 2 var notchEndAngle = notchAngle / 2
// Clear canvas
ctx.reset() ctx.reset()
// Background circle
ctx.strokeStyle = backgroundColor ctx.strokeStyle = backgroundColor
ctx.lineWidth = strokeWidth ctx.lineWidth = strokeWidth
ctx.lineCap = "round" ctx.lineCap = "round"
ctx.beginPath() ctx.beginPath()
if (hasNotch) { if (hasNotch) {
// Draw background circle with notch on the right side // Draw background arc with notch gap
// Draw the arc excluding the notch area (notch is at 0 radians, right side)
ctx.arc(centerX, centerY, radius, notchEndAngle, 2 * Math.PI + notchStartAngle) ctx.arc(centerX, centerY, radius, notchEndAngle, 2 * Math.PI + notchStartAngle)
} else { } else {
// Draw full background circle
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI) ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI)
} }
ctx.stroke() ctx.stroke()
// Progress arc // Draw progress arc
if (progress > 0) { if (progress > 0) {
ctx.strokeStyle = progressColor ctx.strokeStyle = progressColor
ctx.lineWidth = strokeWidth ctx.lineWidth = strokeWidth
@ -67,15 +64,11 @@ Rectangle {
ctx.beginPath() ctx.beginPath()
if (hasNotch) { if (hasNotch) {
// Calculate progress with notch consideration // Calculate progress arc with notch gap
var availableAngle = 2 * Math.PI - notchAngle var availableAngle = 2 * Math.PI - notchAngle
var progressAngle = availableAngle * progress var progressAngle = availableAngle * progress
// Start from where the notch cutout begins (top-right) and go clockwise
var adjustedStartAngle = notchEndAngle var adjustedStartAngle = notchEndAngle
var adjustedEndAngle = adjustedStartAngle + progressAngle var adjustedEndAngle = adjustedStartAngle + progressAngle
// Ensure we don't exceed the available space
if (adjustedEndAngle > 2 * Math.PI + notchStartAngle) { if (adjustedEndAngle > 2 * Math.PI + notchStartAngle) {
adjustedEndAngle = 2 * Math.PI + notchStartAngle adjustedEndAngle = 2 * Math.PI + notchStartAngle
} }
@ -84,6 +77,7 @@ Rectangle {
ctx.arc(centerX, centerY, radius, adjustedStartAngle, adjustedEndAngle) ctx.arc(centerX, centerY, radius, adjustedStartAngle, adjustedEndAngle)
} }
} else { } else {
// Draw full progress arc
ctx.arc(centerX, centerY, radius, startAngle, startAngle + (2 * Math.PI * progress)) ctx.arc(centerX, centerY, radius, startAngle, startAngle + (2 * Math.PI * progress))
} }
ctx.stroke() ctx.stroke()

View file

@ -4,11 +4,11 @@ import qs.Settings
Item { Item {
id: root id: root
property int innerRadius: 34 property int innerRadius: 34 * Theme.scale(Screen)
property int outerRadius: 48 property int outerRadius: 48 * Theme.scale(Screen)
property color fillColor: "#fff" property color fillColor: "#fff"
property color strokeColor: "#fff" property color strokeColor: "#fff"
property int strokeWidth: 0 property int strokeWidth: 0 * Theme.scale(Screen)
property var values: [] property var values: []
property int usableOuter: 48 property int usableOuter: 48
@ -24,7 +24,7 @@ Item {
Rectangle { Rectangle {
property real value: root.values[index] property real value: root.values[index]
property real angle: (index / root.values.length) * 360 property real angle: (index / root.values.length) * 360
width: Math.max(2, (root.innerRadius * 2 * Math.PI) / root.values.length - 4) width: Math.max(2 * Theme.scale(Screen), (root.innerRadius * 2 * Math.PI) / root.values.length - 4 * Theme.scale(Screen))
height: Settings.settings.visualizerType === "diamond" ? value * 2 * (usableOuter - root.innerRadius) : value * (usableOuter - root.innerRadius) height: Settings.settings.visualizerType === "diamond" ? value * 2 * (usableOuter - root.innerRadius) : value * (usableOuter - root.innerRadius)
radius: width / 2 radius: width / 2
color: root.fillColor color: root.fillColor

View file

@ -6,43 +6,43 @@ Shape {
id: root id: root
property string position: "topleft" // Corner position: topleft/topright/bottomleft/bottomright property string position: "topleft" // Corner position: topleft/topright/bottomleft/bottomright
property real size: 1.0 // Scale multiplier for entire corner property real size: 1.0 * Theme.scale(Screen) // Scale multiplier for entire corner
property int concaveWidth: 100 * size property int concaveWidth: 100 * size
property int concaveHeight: 60 * size property int concaveHeight: 60 * size
property int offsetX: -20 property int offsetX: -20 * Theme.scale(Screen)
property int offsetY: -20 property int offsetY: -20 * Theme.scale(Screen)
property color fillColor: Theme.accentPrimary property color fillColor: Theme.accentPrimary
property int arcRadius: 20 * size property int arcRadius: 20 * size
property var modelData: null property var modelData: null
// Position flags derived from position string // Position flags derived from position string - calculated once
property bool _isTop: position.includes("top") readonly property bool _isTop: position.includes("top")
property bool _isLeft: position.includes("left") readonly property bool _isLeft: position.includes("left")
property bool _isRight: position.includes("right") readonly property bool _isRight: position.includes("right")
property bool _isBottom: position.includes("bottom") readonly property bool _isBottom: position.includes("bottom")
// Shift the path vertically if offsetY is negative to pull shape up // Shift the path vertically if offsetY is negative to pull shape up
property real pathOffsetY: Math.min(offsetY, 0) readonly property real pathOffsetY: Math.min(offsetY, 0)
// Base coordinates for left corner shape, shifted by pathOffsetY vertically // Base coordinates for left corner shape, shifted by pathOffsetY vertically
property real _baseStartX: 30 * size readonly property real _baseStartX: 30 * size
property real _baseStartY: (_isTop ? 20 * size : 0) + pathOffsetY readonly property real _baseStartY: (_isTop ? 20 * size : 0) + pathOffsetY
property real _baseLineX: 30 * size readonly property real _baseLineX: 30 * size
property real _baseLineY: (_isTop ? 0 : 20 * size) + pathOffsetY readonly property real _baseLineY: (_isTop ? 0 : 20 * size) + pathOffsetY
property real _baseArcX: 50 * size readonly property real _baseArcX: 50 * size
property real _baseArcY: (_isTop ? 20 * size : 0) + pathOffsetY readonly property real _baseArcY: (_isTop ? 20 * size : 0) + pathOffsetY
// Mirror coordinates for right corners // Mirror coordinates for right corners
property real _startX: _isRight ? (concaveWidth - _baseStartX) : _baseStartX readonly property real _startX: _isRight ? (concaveWidth - _baseStartX) : _baseStartX
property real _startY: _baseStartY readonly property real _startY: _baseStartY
property real _lineX: _isRight ? (concaveWidth - _baseLineX) : _baseLineX readonly property real _lineX: _isRight ? (concaveWidth - _baseLineX) : _baseLineX
property real _lineY: _baseLineY readonly property real _lineY: _baseLineY
property real _arcX: _isRight ? (concaveWidth - _baseArcX) : _baseArcX readonly property real _arcX: _isRight ? (concaveWidth - _baseArcX) : _baseArcX
property real _arcY: _baseArcY readonly property real _arcY: _baseArcY
// Arc direction varies by corner to maintain proper concave shape // Arc direction varies by corner to maintain proper concave shape
property int _arcDirection: { readonly property int _arcDirection: {
if (_isTop && _isLeft) return PathArc.Counterclockwise if (_isTop && _isLeft) return PathArc.Counterclockwise
if (_isTop && _isRight) return PathArc.Clockwise if (_isTop && _isRight) return PathArc.Clockwise
if (_isBottom && _isLeft) return PathArc.Clockwise if (_isBottom && _isLeft) return PathArc.Clockwise
@ -57,9 +57,10 @@ Shape {
x: _isLeft ? offsetX : (parent ? parent.width - width + offsetX : 0) x: _isLeft ? offsetX : (parent ? parent.width - width + offsetX : 0)
y: _isTop ? offsetY : (parent ? parent.height - height + offsetY : 0) y: _isTop ? offsetY : (parent ? parent.height - height + offsetY : 0)
preferredRendererType: Shape.CurveRenderer // Optimized rendering settings - reduced quality for better performance
layer.enabled: true preferredRendererType: Shape.GeometryRenderer // Use simpler renderer
layer.samples: 4 layer.enabled: false // Disable layer rendering to save memory
antialiasing: true // Use standard antialiasing instead of MSAA
ShapePath { ShapePath {
strokeWidth: 0 strokeWidth: 0

View file

@ -19,7 +19,7 @@ MouseArea {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: 8 radius: 8 * Theme.scale(Screen)
color: root.hovering ? Theme.accentPrimary : "transparent" color: root.hovering ? Theme.accentPrimary : "transparent"
} }
Text { Text {
@ -27,7 +27,7 @@ MouseArea {
anchors.centerIn: parent anchors.centerIn: parent
text: root.icon text: root.icon
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 24 font.pixelSize: 24 * Theme.scale(Screen)
color: root.hovering ? Theme.onAccent : Theme.textPrimary color: root.hovering ? Theme.onAccent : Theme.textPrimary
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter

View file

@ -13,8 +13,8 @@ Item {
property color iconCircleColor: Theme.accentPrimary property color iconCircleColor: Theme.accentPrimary
property color iconTextColor: Theme.backgroundPrimary property color iconTextColor: Theme.backgroundPrimary
property color collapsedIconColor: Theme.textPrimary property color collapsedIconColor: Theme.textPrimary
property int pillHeight: 22 property int pillHeight: 22 * Theme.scale(Screen)
property int iconSize: 22 property int iconSize: 22 * Theme.scale(Screen)
property int pillPaddingHorizontal: 14 property int pillPaddingHorizontal: 14
property bool autoHide: false property bool autoHide: false
@ -47,7 +47,7 @@ Item {
id: textItem id: textItem
anchors.centerIn: parent anchors.centerIn: parent
text: revealPill.text text: revealPill.text
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.weight: Font.Bold font.weight: Font.Bold
color: textColor color: textColor
@ -89,7 +89,7 @@ Item {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
font.family: showPill ? "Material Symbols Rounded" : "Material Symbols Outlined" font.family: showPill ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
text: revealPill.icon text: revealPill.icon
color: showPill ? iconTextColor : collapsedIconColor color: showPill ? iconTextColor : collapsedIconColor
} }

View file

@ -1,4 +1,5 @@
import QtQuick import QtQuick
import qs.Settings
Item { Item {
id: root id: root
@ -6,7 +7,7 @@ Item {
property bool running: false property bool running: false
property color color: "white" property color color: "white"
property int size: 16 property int size: 16
property int strokeWidth: 2 property int strokeWidth: 2 * Theme.scale(Screen)
property int duration: 1000 property int duration: 1000
implicitWidth: size implicitWidth: size

View file

@ -9,7 +9,6 @@ Window {
property Item targetItem: null property Item targetItem: null
property int delay: 300 property int delay: 300
// New property to control positioning: true => above, false => below
property bool positionAbove: true property bool positionAbove: true
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
@ -34,8 +33,8 @@ Window {
} }
function _showNow() { function _showNow() {
width = Math.max(50, tooltipText.implicitWidth + 24) width = Math.max(50 * Theme.scale(Screen), tooltipText.implicitWidth + 24 * Theme.scale(Screen))
height = Math.max(50, tooltipText.implicitHeight + 16) height = Math.max(50 * Theme.scale(Screen), tooltipText.implicitHeight + 16 * Theme.scale(Screen))
if (!targetItem) return; if (!targetItem) return;
@ -76,10 +75,10 @@ Window {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: 20 radius: 20 * Theme.scale(Screen)
color: Theme.backgroundTertiary || "#222" color: Theme.backgroundTertiary || "#222"
border.color: Theme.outline || "#444" border.color: Theme.outline || "#444"
border.width: 1 border.width: 1 * Theme.scale(Screen)
opacity: 0.97 opacity: 0.97
z: 1 z: 1
} }
@ -89,7 +88,7 @@ Window {
text: tooltipWindow.text text: tooltipWindow.text
color: Theme.textPrimary color: Theme.textPrimary
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
anchors.centerIn: parent anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
@ -106,7 +105,7 @@ Window {
} }
onTextChanged: { onTextChanged: {
width = Math.max(minimumWidth, tooltipText.implicitWidth + 24); width = Math.max(minimumWidth * Theme.scale(Screen), tooltipText.implicitWidth + 24 * Theme.scale(Screen));
height = Math.max(minimumHeight, tooltipText.implicitHeight + 16); height = Math.max(minimumHeight * Theme.scale(Screen), tooltipText.implicitHeight + 16 * Theme.scale(Screen));
} }
} }

View file

@ -17,8 +17,8 @@ Item {
model: root.tabsModel model: root.tabsModel
delegate: Rectangle { delegate: Rectangle {
id: tabWrapper id: tabWrapper
implicitHeight: tab.height implicitHeight: tab.height * Theme.scale(Screen)
implicitWidth: 56 implicitWidth: 56 * Theme.scale(Screen)
color: "transparent" color: "transparent"
property bool hovered: false property bool hovered: false
@ -48,7 +48,7 @@ Item {
Text { Text {
text: modelData.icon text: modelData.icon
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 22 font.pixelSize: 22 * Theme.scale(Screen)
color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : tabWrapper.hovered ? (Theme ? Theme.accentPrimary : "#7C3AED") : (Theme ? Theme.textSecondary : "#444") color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : tabWrapper.hovered ? (Theme ? Theme.accentPrimary : "#7C3AED") : (Theme ? Theme.textSecondary : "#444")
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
} }
@ -56,7 +56,7 @@ Item {
// Label // Label
Text { Text {
text: modelData.label text: modelData.label
font.pixelSize: 12 font.pixelSize: 12 * Theme.scale(Screen)
font.bold: index === root.currentIndex font.bold: index === root.currentIndex
color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : tabWrapper.hovered ? (Theme ? Theme.accentPrimary : "#7C3AED") : (Theme ? Theme.textSecondary : "#444") color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : tabWrapper.hovered ? (Theme ? Theme.accentPrimary : "#7C3AED") : (Theme ? Theme.textSecondary : "#444")
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
@ -64,9 +64,9 @@ Item {
// Underline for active tab // Underline for active tab
Rectangle { Rectangle {
width: 24 width: 24 * Theme.scale(Screen)
height: 2 height: 2 * Theme.scale(Screen)
radius: 1 radius: 1 * Theme.scale(Screen)
color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : "transparent" color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : "transparent"
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
} }

View file

@ -0,0 +1,88 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Settings
ColumnLayout {
id: root
property string label: ""
property string description: ""
property bool value: false
property var onToggled: function() {
}
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
ColumnLayout {
spacing: 4 * Theme.scale(Screen)
Layout.fillWidth: true
Text {
text: label
font.pixelSize: 13 * Theme.scale(Screen)
font.bold: true
color: Theme.textPrimary
}
Text {
text: description
font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: switcher
width: 52 * Theme.scale(Screen)
height: 32 * Theme.scale(Screen)
radius: 16 * Theme.scale(Screen)
color: value ? Theme.accentPrimary : Theme.surfaceVariant
border.color: value ? Theme.accentPrimary : Theme.outline
border.width: 2 * Theme.scale(Screen)
Rectangle {
width: 28 * Theme.scale(Screen)
height: 28 * Theme.scale(Screen)
radius: 14 * Theme.scale(Screen)
color: Theme.surface
border.color: Theme.outline
border.width: 1 * Theme.scale(Screen)
y: 2
x: value ? switcher.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.onToggled();
}
}
}
}
Rectangle {
height: 8 * Theme.scale(Screen)
}
}

View file

@ -14,9 +14,9 @@ IpcHandler {
idleInhibitor.toggle() idleInhibitor.toggle()
} }
function toggleNotificationPopup(): void { function toggleNotificationPopup(): void {
console.log("[IPC] NotificationPopup toggle() called") console.log("[IPC] NotificationPopup toggle() called")
// Use the global toggle function from the notification manager
notificationPopup.togglePopup(); notificationPopup.togglePopup();
} }

View file

@ -3,10 +3,10 @@ import Quickshell.Io
Process { Process {
id: idleRoot id: idleRoot
// Example: systemd-inhibit to prevent idle/sleep // Uses systemd-inhibit to prevent idle/sleep
command: ["systemd-inhibit", "--what=idle:sleep", "--who=noctalia", "--why=User requested", "sleep", "infinity"] command: ["systemd-inhibit", "--what=idle:sleep", "--who=noctalia", "--why=User requested", "sleep", "infinity"]
// Keep process running in background // Track background process state
property bool isRunning: running property bool isRunning: running
onStarted: { onStarted: {
@ -17,7 +17,7 @@ Process {
console.log("[IdleInhibitor] Process finished:", exitCode) console.log("[IdleInhibitor] Process finished:", exitCode)
} }
// Control functions
function start() { function start() {
if (!running) { if (!running) {
console.log("[IdleInhibitor] Starting idle inhibitor...") console.log("[IdleInhibitor] Starting idle inhibitor...")

View file

@ -8,7 +8,7 @@ import qs.Components
Singleton { Singleton {
id: manager id: manager
// Properties
property var currentPlayer: null property var currentPlayer: null
property real currentPosition: 0 property real currentPosition: 0
property int selectedPlayerIndex: 0 property int selectedPlayerIndex: 0
@ -25,14 +25,14 @@ Singleton {
property bool canSeek: currentPlayer ? currentPlayer.canSeek : false property bool canSeek: currentPlayer ? currentPlayer.canSeek : false
property bool hasPlayer: getAvailablePlayers().length > 0 property bool hasPlayer: getAvailablePlayers().length > 0
// Initialize
Item { Item {
Component.onCompleted: { Component.onCompleted: {
updateCurrentPlayer() updateCurrentPlayer()
} }
} }
// Returns available MPRIS players
function getAvailablePlayers() { function getAvailablePlayers() {
if (!Mpris.players || !Mpris.players.values) { if (!Mpris.players || !Mpris.players.values) {
return [] return []
@ -51,14 +51,14 @@ Singleton {
return controllablePlayers return controllablePlayers
} }
// Returns active player or first available
function findActivePlayer() { function findActivePlayer() {
let availablePlayers = getAvailablePlayers() let availablePlayers = getAvailablePlayers()
if (availablePlayers.length === 0) { if (availablePlayers.length === 0) {
return null return null
} }
// Use selected player if valid, otherwise use first available
if (selectedPlayerIndex < availablePlayers.length) { if (selectedPlayerIndex < availablePlayers.length) {
return availablePlayers[selectedPlayerIndex] return availablePlayers[selectedPlayerIndex]
} else { } else {
@ -67,7 +67,8 @@ Singleton {
} }
} }
// Updates currentPlayer and currentPosition
// Switch to the most recently active player
function updateCurrentPlayer() { function updateCurrentPlayer() {
let newPlayer = findActivePlayer() let newPlayer = findActivePlayer()
if (newPlayer !== currentPlayer) { if (newPlayer !== currentPlayer) {
@ -76,7 +77,7 @@ Singleton {
} }
} }
// Player control functions
function playPause() { function playPause() {
if (currentPlayer) { if (currentPlayer) {
if (currentPlayer.isPlaying) { if (currentPlayer.isPlaying) {
@ -118,6 +119,7 @@ Singleton {
} }
} }
// Seek to position based on ratio (0.0 to 1.0)
function seekByRatio(ratio) { function seekByRatio(ratio) {
if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) { if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) {
let seekPosition = ratio * currentPlayer.length let seekPosition = ratio * currentPlayer.length
@ -126,7 +128,7 @@ Singleton {
} }
} }
// Updates progress bar every second // Update progress bar every second while playing
Timer { Timer {
id: positionTimer id: positionTimer
interval: 1000 interval: 1000
@ -141,14 +143,14 @@ Singleton {
} }
} }
// Reset position when player state changes // Reset position when switching to inactive player
onCurrentPlayerChanged: { onCurrentPlayerChanged: {
if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) {
currentPosition = 0 currentPosition = 0
} }
} }
// Reacts to player list changes // Update current player when available players change
Connections { Connections {
target: Mpris.players target: Mpris.players
function onValuesChanged() { function onValuesChanged() {

348
Services/Network.qml Normal file
View file

@ -0,0 +1,348 @@
import QtQuick
import Quickshell.Io
QtObject {
id: root
property var networks: ({})
property string connectingSsid: ""
property string connectStatus: ""
property string connectStatusSsid: ""
property string connectError: ""
property string detectedInterface: ""
function signalIcon(signal) {
if (signal >= 80)
return "network_wifi";
if (signal >= 60)
return "network_wifi_3_bar";
if (signal >= 40)
return "network_wifi_2_bar";
if (signal >= 20)
return "network_wifi_1_bar";
return "wifi_0_bar";
}
function isSecured(security) {
return security && security.trim() !== "" && security.trim() !== "--";
}
function refreshNetworks() {
existingNetwork.running = true;
}
function connectNetwork(ssid, security) {
pendingConnect = {
ssid: ssid,
security: security,
password: ""
};
doConnect();
}
function submitPassword(ssid, password) {
pendingConnect = {
ssid: ssid,
security: networks[ssid].security,
password: password
};
doConnect();
}
function disconnectNetwork(ssid) {
disconnectProfileProcess.connectionName = ssid;
disconnectProfileProcess.running = true;
}
property var pendingConnect: null
function doConnect() {
const params = pendingConnect;
if (!params)
return;
connectingSsid = params.ssid;
connectStatus = "";
connectStatusSsid = params.ssid;
const targetNetwork = networks[params.ssid];
if (targetNetwork && targetNetwork.existing) {
upConnectionProcess.profileName = params.ssid;
upConnectionProcess.running = true;
pendingConnect = null;
return;
}
if (params.security && params.security !== "--") {
getInterfaceProcess.running = true;
return;
}
connectProcess.security = params.security;
connectProcess.ssid = params.ssid;
connectProcess.password = params.password;
connectProcess.running = true;
pendingConnect = null;
}
property int refreshInterval: 25000
// Only refresh when we have an active connection
property bool hasActiveConnection: {
for (const net in networks) {
if (networks[net].connected) {
return true;
}
}
return false;
}
property Timer refreshTimer: Timer {
interval: root.refreshInterval
// Only run timer when we're connected to a network
running: root.hasActiveConnection
repeat: true
onTriggered: root.refreshNetworks()
}
// Force a refresh when menu is opened
function onMenuOpened() {
refreshNetworks();
}
function onMenuClosed() {
// No need to do anything special on close
}
property Process disconnectProfileProcess: Process {
id: disconnectProfileProcess
property string connectionName: ""
running: false
command: ["nmcli", "connection", "down", connectionName]
onRunningChanged: {
if (!running) {
root.refreshNetworks();
}
}
}
property Process existingNetwork: Process {
id: existingNetwork
running: false
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n");
const networksMap = {};
for (let i = 0; i < lines.length; ++i) {
const line = lines[i].trim();
if (!line)
continue;
const parts = line.split(":");
if (parts.length < 2) {
console.warn("Malformed nmcli output line:", line);
continue;
}
const ssid = parts[0];
const type = parts[1];
if (ssid) {
networksMap[ssid] = {
ssid: ssid,
type: type
};
}
}
scanProcess.existingNetwork = networksMap;
scanProcess.running = true;
}
}
}
property Process scanProcess: Process {
id: scanProcess
running: false
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
property var existingNetwork
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n");
const networksMap = {};
for (let i = 0; i < lines.length; ++i) {
const line = lines[i].trim();
if (!line)
continue;
const parts = line.split(":");
if (parts.length < 4) {
console.warn("Malformed nmcli output line:", line);
continue;
}
const ssid = parts[0];
const security = parts[1];
const signal = parseInt(parts[2]);
const inUse = parts[3] === "*";
if (ssid) {
if (!networksMap[ssid]) {
networksMap[ssid] = {
ssid: ssid,
security: security,
signal: signal,
connected: inUse,
existing: ssid in scanProcess.existingNetwork
};
} else {
const existingNet = networksMap[ssid];
if (inUse) {
existingNet.connected = true;
}
if (signal > existingNet.signal) {
existingNet.signal = signal;
existingNet.security = security;
}
}
}
}
root.networks = networksMap;
scanProcess.existingNetwork = {};
}
}
}
property Process connectProcess: Process {
id: connectProcess
property string ssid: ""
property string password: ""
property string security: ""
running: false
command: {
if (password) {
return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password];
} else {
return ["nmcli", "device", "wifi", "connect", `'${ssid}'`];
}
}
stdout: StdioCollector {
onStreamFinished: {
root.connectingSsid = "";
root.connectStatus = "success";
root.connectStatusSsid = connectProcess.ssid;
root.connectError = "";
root.refreshNetworks();
}
}
stderr: StdioCollector {
onStreamFinished: {
root.connectingSsid = "";
root.connectStatus = "error";
root.connectStatusSsid = connectProcess.ssid;
root.connectError = text;
}
}
}
property Process getInterfaceProcess: Process {
id: getInterfaceProcess
running: false
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
stdout: StdioCollector {
onStreamFinished: {
var lines = text.split("\n");
for (var i = 0; i < lines.length; ++i) {
var parts = lines[i].split(":");
if (parts[1] === "wifi" && parts[2] !== "unavailable") {
root.detectedInterface = parts[0];
break;
}
}
if (root.detectedInterface) {
var params = root.pendingConnect;
addConnectionProcess.ifname = root.detectedInterface;
addConnectionProcess.ssid = params.ssid;
addConnectionProcess.password = params.password;
addConnectionProcess.profileName = params.ssid;
addConnectionProcess.security = params.security;
addConnectionProcess.running = true;
} else {
root.connectStatus = "error";
root.connectStatusSsid = root.pendingConnect.ssid;
root.connectError = "No Wi-Fi interface found.";
root.connectingSsid = "";
root.pendingConnect = null;
}
}
}
}
property Process addConnectionProcess: Process {
id: addConnectionProcess
property string ifname: ""
property string ssid: ""
property string password: ""
property string profileName: ""
property string security: ""
running: false
command: {
var cmd = ["nmcli", "connection", "add", "type", "wifi", "ifname", ifname, "con-name", profileName, "ssid", ssid];
if (security && security !== "--") {
cmd.push("wifi-sec.key-mgmt");
cmd.push("wpa-psk");
cmd.push("wifi-sec.psk");
cmd.push(password);
}
return cmd;
}
stdout: StdioCollector {
onStreamFinished: {
upConnectionProcess.profileName = addConnectionProcess.profileName;
upConnectionProcess.running = true;
}
}
stderr: StdioCollector {
onStreamFinished: {
upConnectionProcess.profileName = addConnectionProcess.profileName;
upConnectionProcess.running = true;
}
}
}
property Process upConnectionProcess: Process {
id: upConnectionProcess
property string profileName: ""
running: false
command: ["nmcli", "connection", "up", "id", profileName]
stdout: StdioCollector {
onStreamFinished: {
root.connectingSsid = "";
root.connectStatus = "success";
root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : "";
root.connectError = "";
root.pendingConnect = null;
root.refreshNetworks();
}
}
stderr: StdioCollector {
onStreamFinished: {
root.connectingSsid = "";
root.connectStatus = "error";
root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : "";
root.connectError = text;
root.pendingConnect = null;
}
}
}
Component.onCompleted: {
refreshNetworks();
}
}

View file

@ -45,9 +45,10 @@ Singleton {
property string wallpaperFolder: "/usr/share/wallpapers" property string wallpaperFolder: "/usr/share/wallpapers"
property string currentWallpaper: "" property string currentWallpaper: ""
property string videoPath: "~/Videos/" property string videoPath: "~/Videos/"
property bool showActiveWindow: true
property bool showActiveWindowIcon: false property bool showActiveWindowIcon: false
property bool showSystemInfoInBar: false property bool showSystemInfoInBar: false
property bool showCorners: true property bool showCorners: false
property bool showTaskbar: true property bool showTaskbar: true
property bool showMediaInBar: false property bool showMediaInBar: false
property bool useSWWW: false property bool useSWWW: false
@ -68,6 +69,19 @@ Singleton {
property bool showDock: true property bool showDock: true
property bool dockExclusive: false property bool dockExclusive: false
property bool wifiEnabled: false
property bool bluetoothEnabled: false
property int recordingFrameRate: 60
property string recordingQuality: "very_high"
property string recordingCodec: "h264"
property string audioCodec: "opus"
property bool showCursor: true
property string colorRange: "limited"
// Monitor/Display Settings
property var barMonitors: [] // Array of monitor names to show the bar on
property var dockMonitors: [] // Array of monitor names to show the dock on
property var notificationMonitors: [] // Array of monitor names to show notifications on, "*" means all monitors
} }
} }
@ -76,5 +90,7 @@ Singleton {
function onRandomWallpaperChanged() { WallpaperManager.toggleRandomWallpaper() } function onRandomWallpaperChanged() { WallpaperManager.toggleRandomWallpaper() }
function onWallpaperIntervalChanged() { WallpaperManager.restartRandomWallpaperTimer() } function onWallpaperIntervalChanged() { WallpaperManager.restartRandomWallpaperTimer() }
function onWallpaperFolderChanged() { WallpaperManager.loadWallpapers() } function onWallpaperFolderChanged() { WallpaperManager.loadWallpapers() }
function onNotificationMonitorsChanged() {
}
} }
} }

View file

@ -8,6 +8,22 @@ import qs.Settings
Singleton { Singleton {
id: root id: root
// Design screen width
readonly property int designScreenWidth: 1920
// Scaling dampening factor - reduces the scaling effect for higher resolutions
readonly property real scalingDampening: 0.2
// Automatic scaling based on screen width
function scale(currentScreen) {
if (currentScreen !== undefined) {
var rawRatio = currentScreen.width / designScreenWidth
// Apply dampening to reduce scaling for higher resolutions
return Math.min(2.0, 1.0 + (rawRatio - 1.0) * scalingDampening)
}
return 1.0
}
function applyOpacity(color, opacity) { function applyOpacity(color, opacity) {
return color.replace("#", "#" + opacity); return color.replace("#", "#" + opacity);
} }

View file

@ -0,0 +1,29 @@
# The kitty terminal template for wallust
# Add to wallust config: kitty = { src='kitty.conf', dst='~/.config/kitty/colors.conf'}
# And add to kitty config: include colors.conf
cursor {{ cursor }}
background {{ background }}
foreground {{ foreground }}
color0 {{ color0 }}
color1 {{ color1 }}
color2 {{ color2 }}
color3 {{ color3 }}
color4 {{ color4 }}
color5 {{ color5 }}
color6 {{ color6 }}
color7 {{ color7 }}
color8 {{ color8 }}
color9 {{ color9 }}
color10 {{ color10 }}
color11 {{ color11 }}
color12 {{ color12 }}
color13 {{ color13 }}
color14 {{ color14 }}
color15 {{ color15 }}
mark1_foreground {{ color6 | saturate(0.2) }}
mark2_foreground {{ color7 | saturate(0.2) }}
mark3_foreground {{ color6 | saturate(0.2) }}

View file

@ -0,0 +1,292 @@
// Niri configuration for CachyOS
// For documentation and full reference, see: https://github.com/YaLTeR/niri/wiki
// ────────────── Input Configuration ──────────────
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
input {
keyboard {
xkb {
layout "de" // Use the German keyboard layout
}
numlock // Enable numlock on startup
}
touchpad {
tap // Enable tap-to-click
natural-scroll // Enable natural (macOS-style) scrolling
}
focus-follows-mouse // Automatically focus windows under the mouse pointer
workspace-auto-back-and-forth // Enable workspace back & forth switching
}
// ────────────── Output Configuration ──────────────
// You can run `niri msg outputs` to get the correct name for your displays.
// You will have to remove "/-" and edit it before it takes effect.
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
output "DP-1" {
mode "2560x1440@359.979" // Set resolution and refresh rate
scale 1 // No scaling (use 2 for HiDPI)
}
// ────────────── Keybindings ──────────────
// https://github.com/YaLTeR/niri/wiki/Configuration:-Key-Bindings
binds {
MOD+SHIFT+ESCAPE { show-hotkey-overlay; }
// ─── Applications ───
MOD+RETURN hotkey-overlay-title="Open Terminal: Kitty" { spawn "kitty"; }
MOD+CTRL+RETURN hotkey-overlay-title="Open App Launcher: QS" { spawn "qs" "ipc" "call" "globalIPC" "toggleLauncher"; }
MOD+B hotkey-overlay-title="Open Browser: firefox" { spawn "firefox"; }
MOD+ALT+L hotkey-overlay-title="Lock Screen: swaylock" { spawn "swaylock"; }
// Please choose your own file manager
MOD+E hotkey-overlay-title="File Manager: Nautilus" { spawn "nautilus"; }
// ─── Audio Controls ───
XF86AUDIORAISEVOLUME allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
XF86AUDIOLOWERVOLUME allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
XF86AUDIOMUTE allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
XF86AUDIOMICMUTE allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
// ─── Window Movement and Focus ───
MOD+Q { close-window; }
MOD+LEFT { focus-column-left; }
MOD+H { focus-column-left; }
MOD+RIGHT { focus-column-right; }
MOD+L { focus-column-right; }
MOD+UP { focus-window-up; }
MOD+K { focus-window-up; }
MOD+DOWN { focus-window-down; }
MOD+J { focus-window-down; }
MOD+CTRL+LEFT { move-column-left; }
MOD+CTRL+H { move-column-left; }
MOD+CTRL+RIGHT { move-column-right; }
MOD+CTRL+L { move-column-right; }
MOD+CTRL+UP { move-window-up; }
MOD+CTRL+K { move-window-up; }
MOD+CTRL+DOWN { move-window-down; }
MOD+CTRL+J { move-window-down; }
MOD+HOME { focus-column-first; }
MOD+END { focus-column-last; }
MOD+CTRL+HOME { move-column-to-first; }
MOD+CTRL+END { move-column-to-last; }
MOD+SHIFT+LEFT { focus-monitor-left; }
MOD+SHIFT+RIGHT { focus-monitor-right; }
MOD+SHIFT+UP { focus-monitor-up; }
MOD+SHIFT+DOWN { focus-monitor-down; }
MOD+SHIFT+CTRL+LEFT { move-column-to-monitor-left; }
MOD+SHIFT+CTRL+RIGHT { move-column-to-monitor-right; }
MOD+SHIFT+CTRL+UP { move-column-to-monitor-up; }
MOD+SHIFT+CTRL+DOWN { move-column-to-monitor-down; }
// ─── Workspace Switching ───
MOD+WHEELSCROLLDOWN cooldown-ms=150 { focus-workspace-down; }
MOD+WHEELSCROLLUP cooldown-ms=150 { focus-workspace-up; }
MOD+CTRL+WHEELSCROLLDOWN cooldown-ms=150 { move-column-to-workspace-down; }
MOD+CTRL+WHEELSCROLLUP cooldown-ms=150 { move-column-to-workspace-up; }
MOD+WHEELSCROLLRIGHT { focus-column-right; }
MOD+WHEELSCROLLLEFT { focus-column-left; }
MOD+CTRL+WHEELSCROLLRIGHT { move-column-right; }
MOD+CTRL+WHEELSCROLLLEFT { move-column-left; }
MOD+SHIFT+WHEELSCROLLDOWN { focus-column-right; }
MOD+SHIFT+WHEELSCROLLUP { focus-column-left; }
MOD+CTRL+SHIFT+WHEELSCROLLDOWN { move-column-right; }
MOD+CTRL+SHIFT+WHEELSCROLLUP { move-column-left; }
MOD+1 { focus-workspace 1; }
MOD+2 { focus-workspace 2; }
MOD+3 { focus-workspace 3; }
MOD+4 { focus-workspace 4; }
MOD+5 { focus-workspace 5; }
MOD+6 { focus-workspace 6; }
MOD+7 { focus-workspace 7; }
MOD+8 { focus-workspace 8; }
MOD+9 { focus-workspace 9; }
MOD+CTRL+1 { move-column-to-workspace 1; }
MOD+CTRL+2 { move-column-to-workspace 2; }
MOD+CTRL+3 { move-column-to-workspace 3; }
MOD+CTRL+4 { move-column-to-workspace 4; }
MOD+CTRL+5 { move-column-to-workspace 5; }
MOD+CTRL+6 { move-column-to-workspace 6; }
MOD+CTRL+7 { move-column-to-workspace 7; }
MOD+CTRL+8 { move-column-to-workspace 8; }
MOD+CTRL+9 { move-column-to-workspace 9; }
MOD+TAB { focus-workspace-previous; }
// ─── Layout Controls ───
MOD+CTRL+F { expand-column-to-available-width; }
MOD+C { center-column; }
MOD+CTRL+C { center-visible-columns; }
MOD+MINUS { set-column-width "-10%"; }
MOD+EQUAL { set-column-width "+10%"; }
MOD+SHIFT+MINUS { set-window-height "-10%"; }
MOD+SHIFT+EQUAL { set-window-height "+10%"; }
// ─── Modes ───
MOD+T { toggle-window-floating; }
MOD+F { fullscreen-window; }
MOD+W { toggle-column-tabbed-display; }
// ─── Screenshots ───
CTRL+SHIFT+1 { screenshot; }
CTRL+SHIFT+2 { screenshot-screen; }
CTRL+SHIFT+3 { screenshot-window; }
// ─── Emergency Escape Key ───
// Use this when a fullscreen app blocks your keybinds.
// It disables any active keyboard shortcut inhibitor, restoring control.
MOD+ESCAPE allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
// ─── Exit / Power ───
CTRL+ALT+DELETE { quit; } // Also quits Niri
MOD+SHIFT+P { power-off-monitors; } // Turn off screens (useful for OLED or privacy)
MOD+O repeat=false { toggle-overview; }
}
// ────────────── Startup Applications ──────────────
// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#spawn-at-startup
spawn-at-startup "/usr/lib/polkit-kde-authentication-agent-1" "&" // Polkit
spawn-at-startup "xwayland-satellite" // XWayland support
spawn-at-startup "swww-daemon" // Wallpaper daemon
spawn-at-startup "swww img" "/usr/share/wallpapers/cachyos-wallpapers/Skyscraper.png" // Set wallpaper
spawn-at-startup "qs" // Launch Quickshell
spawn-at-startup "vesktop" // Launch Vesktop
prefer-no-csd // Disable program decorations
screenshot-path null // Disable screenshot saving
// ────────────── Layout Settings ──────────────
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout {
gaps 16 // Gap between windows
center-focused-column "never" // Dont auto-center focused column
preset-column-widths {
proportion 0.33333
proportion 0.5
proportion 0.66667
}
focus-ring {
width 3
active-color "{{ color4 }}"
inactive-color "{{ color0 }}"
}
shadow {
softness 30
spread 5
offset x=0 y=5
color "#0007"
}
background-color "transparent"
struts {}
}
// ────────────── Animation Settings ──────────────
// https://github.com/YaLTeR/niri/wiki/Configuration:-Animations
animations {
workspace-switch {
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
}
window-open {
duration-ms 200
curve "ease-out-quad"
}
window-close {
duration-ms 200
curve "ease-out-cubic"
}
horizontal-view-movement {
spring damping-ratio=1.0 stiffness=900 epsilon=0.0001
}
window-movement {
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
window-resize {
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
}
config-notification-open-close {
spring damping-ratio=0.6 stiffness=1200 epsilon=0.001
}
screenshot-ui-open {
duration-ms 300
curve "ease-out-quad"
}
overview-open-close {
spring damping-ratio=1.0 stiffness=900 epsilon=0.0001
}
}
// ────────────── Named Workspaces ──────────────
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
workspace "browser"
workspace "chat"
// ────────────── Window Rules ──────────────
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
window-rule {
match at-startup=true app-id="vesktop"
open-on-workspace "chat"
open-maximized true
}
window-rule {
match app-id="firefox"
open-on-workspace "browser"
open-maximized true
}
window-rule {
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
open-floating true // Always float Firefox PiP windows
}
window-rule {
geometry-corner-radius 20 // Set every window radius to 20
clip-to-geometry true
}
// ────────────── Layer Rules ──────────────
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layer-Rules
layer-rule {
match namespace="^swww-daemon$"
place-within-backdrop true
}
// ────────────── Environment Variables ──────────────
// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#environment
environment {
DISPLAY ":1"
ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORM "wayland"
QT_WAYLAND_DISABLE_WINDOWDECORATION "1"
XDG_SESSION_TYPE "wayland"
XDG_CURRENT_DESKTOP "niri"
}
// ────────────── Misc ──────────────
hotkey-overlay {
skip-at-startup
}

View file

@ -8,7 +8,9 @@ import qs.Components
PanelWindow { PanelWindow {
id: taskbarWindow 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) screen: (typeof modelData !== 'undefined' ? modelData : null)
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
anchors.bottom: true anchors.bottom: true
@ -245,7 +247,7 @@ PanelWindow {
contextMenuVisible = false; contextMenuVisible = false;
contextMenuTarget = null; contextMenuTarget = null;
contextMenuToplevel = 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 spacing: 4
width: parent.width width: parent.width
// Close
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 32 height: 32
@ -300,7 +302,7 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "close" text: "close"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 14 font.pixelSize: 14 * Theme.scale(Screen)
color: Theme.textPrimary color: Theme.textPrimary
} }
@ -308,7 +310,7 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Close" text: "Close"
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: 14 font.pixelSize: 14 * Theme.scale(Screen)
color: Theme.textPrimary color: Theme.textPrimary
} }
} }
@ -322,7 +324,7 @@ PanelWindow {
onClicked: { onClicked: {
if (contextMenuToplevel?.close) contextMenuToplevel.close(); if (contextMenuToplevel?.close) contextMenuToplevel.close();
contextMenuVisible = false; contextMenuVisible = false;
hidden = true; // Hide the dock here as well hidden = true;
} }
} }
} }

View file

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

View file

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

View file

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

View file

@ -8,25 +8,49 @@ Item {
id: root id: root
width: 22; height: 22 width: 22; height: 22
property bool isSilence: false property bool isSilence: false
property var shell: null
// Process for executing CLI commands
Process { Process {
id: rightClickProcess id: rightClickProcess
command: ["qs","ipc", "call", "globalIPC", "toggleNotificationPopup"] 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 { Item {
id: bell id: bell
width: 22; height: 22 width: 22; height: 22
Text { Text {
id: bellText id: bellText
anchors.centerIn: parent 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.family: mouseAreaBell.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 16 font.pixelSize: 16 * Theme.scale(Screen)
font.weight: notificationHistoryWin.hasUnread ? Font.Bold : Font.Normal font.weight: {
color: mouseAreaBell.containsMouse ? Theme.accentPrimary : (notificationHistoryWin.hasUnread ? Theme.accentPrimary : Theme.textDisabled) 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 { MouseArea {
id: mouseAreaBell id: mouseAreaBell
@ -42,10 +66,18 @@ Item {
} }
if (mouse.button === Qt.LeftButton){ if (mouse.button === Qt.LeftButton){
notificationHistoryWin.visible = !notificationHistoryWin.visible if (shell) {
return; 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 onEntered: notificationTooltip.tooltipVisible = true
onExited: notificationTooltip.tooltipVisible = false onExited: notificationTooltip.tooltipVisible = false

View file

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

View file

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

View file

@ -34,15 +34,15 @@ ShellRoot {
cache: true cache: true
smooth: true smooth: true
mipmap: false mipmap: false
visible: wallpaperSource !== "" // Show the original for FastBlur input visible: wallpaperSource !== ""
} }
MultiEffect { MultiEffect {
id: overviewBgBlur id: overviewBgBlur
anchors.fill: parent anchors.fill: parent
source: bgImage source: bgImage
blurEnabled: true blurEnabled: true
blur: 0.48 // controls blur strength (0 to 1) blur: 0.48
blurMax: 128 // max blur radius in pixels blurMax: 128
} }
Rectangle { Rectangle {
anchors.fill: parent 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 id: root
property alias panel: bluetoothPanelModal property alias panel: bluetoothPanelModal
// For showing error/status messages
property string statusMessage: "" property string statusMessage: ""
property bool statusPopupVisible: false property bool statusPopupVisible: false
@ -145,7 +145,7 @@ Item {
opacity: 0.12 opacity: 0.12
} }
// Content area (centered, in a card)
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 640 Layout.preferredHeight: 640

View file

@ -1,7 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Settings import qs.Settings
import qs.Widgets.Sidebar.Panel
Item { Item {
id: buttonRoot id: buttonRoot
@ -45,7 +44,7 @@ Item {
id: iconText id: iconText
text: "dashboard" text: "dashboard"
font.family: isActive ? "Material Symbols Rounded" : "Material Symbols Outlined" 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 color: sidebarPopup.visible ? Theme.accentPrimary : Theme.textPrimary
anchors.centerIn: parent anchors.centerIn: parent
z: 1 z: 1

View file

@ -8,15 +8,15 @@ import qs.Services
Rectangle { Rectangle {
id: musicCard id: musicCard
width: 360 width: 360 * Theme.scale(Screen)
height: 250 height: 250 * Theme.scale(Screen)
color: "transparent" color: "transparent"
Rectangle { Rectangle {
id: card id: card
anchors.fill: parent anchors.fill: parent
color: Theme.surface color: Theme.surface
radius: 18 radius: 18 * Theme.scale(Screen)
// Show fallback UI if no player is available // Show fallback UI if no player is available
Item { Item {
@ -26,12 +26,12 @@ Rectangle {
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 16 spacing: 16 * Theme.scale(Screen)
Text { Text {
text: "music_note" text: "music_note"
font.family: "Material Symbols Outlined" 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) color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
@ -40,7 +40,7 @@ Rectangle {
text: MusicManager.hasPlayer ? "No controllable player selected" : "No music player detected" 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) color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6)
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
} }
@ -49,45 +49,45 @@ Rectangle {
// Main player UI // Main player UI
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 18 anchors.margins: 18 * Theme.scale(Screen)
spacing: 12 spacing: 12 * Theme.scale(Screen)
visible: !!MusicManager.currentPlayer visible: !!MusicManager.currentPlayer
// Player selector // Player selector
ComboBox { ComboBox {
id: playerSelector id: playerSelector
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: 40 * Theme.scale(Screen)
visible: MusicManager.getAvailablePlayers().length > 1 visible: MusicManager.getAvailablePlayers().length > 1
model: MusicManager.getAvailablePlayers() model: MusicManager.getAvailablePlayers()
textRole: "identity" textRole: "identity"
currentIndex: MusicManager.selectedPlayerIndex currentIndex: MusicManager.selectedPlayerIndex
background: Rectangle { background: Rectangle {
implicitWidth: 120 implicitWidth: 120 * Theme.scale(Screen)
implicitHeight: 40 implicitHeight: 40 * Theme.scale(Screen)
color: Theme.surfaceVariant color: Theme.surfaceVariant
border.color: playerSelector.activeFocus ? Theme.accentPrimary : Theme.outline border.color: playerSelector.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1 border.width: 1 * Theme.scale(Screen)
radius: 16 radius: 16 * Theme.scale(Screen)
} }
contentItem: Text { contentItem: Text {
leftPadding: 12 leftPadding: 12 * Theme.scale(Screen)
rightPadding: playerSelector.indicator.width + playerSelector.spacing rightPadding: playerSelector.indicator.width + playerSelector.spacing
text: playerSelector.displayText text: playerSelector.displayText
font.pixelSize: 13 font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight elide: Text.ElideRight
} }
indicator: Text { indicator: Text {
x: playerSelector.width - width - 12 x: playerSelector.width - width - 12 * Theme.scale(Screen)
y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2 y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2
text: "arrow_drop_down" text: "arrow_drop_down"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 24 font.pixelSize: 24 * Theme.scale(Screen)
color: Theme.textPrimary color: Theme.textPrimary
} }
@ -95,7 +95,7 @@ Rectangle {
y: playerSelector.height y: playerSelector.height
width: playerSelector.width width: playerSelector.width
implicitHeight: contentItem.implicitHeight implicitHeight: contentItem.implicitHeight
padding: 1 padding: 1 * Theme.scale(Screen)
contentItem: ListView { contentItem: ListView {
clip: true clip: true
@ -109,8 +109,8 @@ Rectangle {
background: Rectangle { background: Rectangle {
color: Theme.surfaceVariant color: Theme.surfaceVariant
border.color: Theme.outline border.color: Theme.outline
border.width: 1 border.width: 1 * Theme.scale(Screen)
radius: 16 radius: 16 * Theme.scale(Screen)
} }
} }
@ -118,7 +118,7 @@ Rectangle {
width: playerSelector.width width: playerSelector.width
contentItem: Text { contentItem: Text {
text: modelData.identity text: modelData.identity
font.pixelSize: 13 font.pixelSize: 13 * Theme.scale(Screen)
color: Theme.textPrimary color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight elide: Text.ElideRight
@ -136,57 +136,57 @@ Rectangle {
} }
} }
// Album art and spectrum // Album art with spectrum visualizer
RowLayout { RowLayout {
spacing: 12 spacing: 12 * Theme.scale(Screen)
Layout.fillWidth: true Layout.fillWidth: true
// Album art with spectrum // Album art container with circular spectrum overlay
Item { Item {
id: albumArtContainer id: albumArtContainer
width: 96 width: 96 * Theme.scale(Screen)
height: 96 // enough for spectrum and art (will adjust if needed) height: 96 * Theme.scale(Screen) // enough for spectrum and art (will adjust if needed)
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
// Spectrum visualizer // Circular spectrum visualizer around album art
CircularSpectrum { CircularSpectrum {
id: spectrum id: spectrum
values: MusicManager.cavaValues values: MusicManager.cavaValues
anchors.centerIn: parent anchors.centerIn: parent
innerRadius: 30 // just outside 60x60 album art innerRadius: 30 * Theme.scale(Screen) // Position just outside 60x60 album art
outerRadius: 48 // how far bars extend outerRadius: 48 * Theme.scale(Screen) // Extend bars outward from album art
fillColor: Theme.accentPrimary fillColor: Theme.accentPrimary
strokeColor: Theme.accentPrimary strokeColor: Theme.accentPrimary
strokeWidth: 0 strokeWidth: 0 * Theme.scale(Screen)
z: 0 z: 0
} }
// Album art image // Album art image
Rectangle { Rectangle {
id: albumArtwork id: albumArtwork
width: 60 width: 60 * Theme.scale(Screen)
height: 60 height: 60 * Theme.scale(Screen)
anchors.centerIn: parent anchors.centerIn: parent
radius: 30 // circle radius: 30 * Theme.scale(Screen) // circle
color: Qt.darker(Theme.surface, 1.1) color: Qt.darker(Theme.surface, 1.1)
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) 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 { Image {
id: albumArt id: albumArt
anchors.fill: parent anchors.fill: parent
anchors.margins: 2 anchors.margins: 2 * Theme.scale(Screen)
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
smooth: true smooth: true
mipmap: true mipmap: true
cache: false cache: false
asynchronous: true asynchronous: true
sourceSize.width: 60 sourceSize.width: 60 * Theme.scale(Screen)
sourceSize.height: 60 sourceSize.height: 60 * Theme.scale(Screen)
source: MusicManager.trackArtUrl source: MusicManager.trackArtUrl
visible: source.toString() !== "" visible: source.toString() !== ""
// Rounded corners using layer // Apply circular mask for rounded corners
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
maskEnabled: true maskEnabled: true
@ -208,12 +208,12 @@ Rectangle {
} }
} }
// Fallback icon // Fallback icon when no album art available
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "album" text: "album"
font.family: "Material Symbols Outlined" 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) color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4)
visible: !albumArt.visible visible: !albumArt.visible
} }
@ -223,13 +223,13 @@ Rectangle {
// Track metadata // Track metadata
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4 * Theme.scale(Screen)
Text { Text {
text: MusicManager.trackTitle text: MusicManager.trackTitle
color: Theme.textPrimary color: Theme.textPrimary
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
font.bold: true font.bold: true
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.Wrap wrapMode: Text.Wrap
@ -241,7 +241,7 @@ Rectangle {
text: MusicManager.trackArtist text: MusicManager.trackArtist
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.8) color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.8)
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen)
elide: Text.ElideRight elide: Text.ElideRight
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -250,7 +250,7 @@ Rectangle {
text: MusicManager.trackAlbum text: MusicManager.trackAlbum
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6) color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6)
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen)
elide: Text.ElideRight elide: Text.ElideRight
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -261,8 +261,8 @@ Rectangle {
Rectangle { Rectangle {
id: progressBarBackground id: progressBarBackground
width: parent.width width: parent.width
height: 6 height: 6 * Theme.scale(Screen)
radius: 3 radius: 3 * Theme.scale(Screen)
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15) color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15)
Layout.fillWidth: true Layout.fillWidth: true
@ -290,12 +290,12 @@ Rectangle {
// Interactive progress handle // Interactive progress handle
Rectangle { Rectangle {
id: progressHandle id: progressHandle
width: 12 width: 12 * Theme.scale(Screen)
height: 12 height: 12 * Theme.scale(Screen)
radius: 6 radius: 6 * Theme.scale(Screen)
color: Theme.accentPrimary color: Theme.accentPrimary
border.color: Qt.lighter(Theme.accentPrimary, 1.3) 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)) x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -334,18 +334,18 @@ Rectangle {
// Media controls // Media controls
RowLayout { RowLayout {
spacing: 4 spacing: 4 * Theme.scale(Screen)
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
// Previous button // Previous button
Rectangle { Rectangle {
width: 28 width: 28 * Theme.scale(Screen)
height: 28 height: 28 * Theme.scale(Screen)
radius: 14 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) 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.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
border.width: 1 border.width: 1 * Theme.scale(Screen)
MouseArea { MouseArea {
id: previousButton id: previousButton
@ -360,19 +360,19 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
text: "skip_previous" text: "skip_previous"
font.family: "Material Symbols Outlined" 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) color: previousButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
} }
} }
// Play/Pause button // Play/Pause button
Rectangle { Rectangle {
width: 36 width: 36 * Theme.scale(Screen)
height: 36 height: 36 * Theme.scale(Screen)
radius: 18 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) 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.color: Theme.accentPrimary
border.width: 2 border.width: 2 * Theme.scale(Screen)
MouseArea { MouseArea {
id: playButton id: playButton
@ -387,19 +387,19 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
text: MusicManager.isPlaying ? "pause" : "play_arrow" text: MusicManager.isPlaying ? "pause" : "play_arrow"
font.family: "Material Symbols Outlined" 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) color: playButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
} }
} }
// Next button // Next button
Rectangle { Rectangle {
width: 28 width: 28 * Theme.scale(Screen)
height: 28 height: 28 * Theme.scale(Screen)
radius: 14 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) 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.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
border.width: 1 border.width: 1 * Theme.scale(Screen)
MouseArea { MouseArea {
id: nextButton id: nextButton
@ -414,7 +414,7 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
text: "skip_next" text: "skip_next"
font.family: "Material Symbols Outlined" 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) 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
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import qs.Settings
import qs.Widgets.Sidebar.Config
import qs.Components import qs.Components
import qs.Settings
import qs.Widgets.SettingsWindow
PanelWithOverlay { PanelWithOverlay {
id: sidebarPopup id: sidebarPopup
property var shell: null
function showAt() { function showAt() {
sidebarPopupRect.showAt(); sidebarPopupRect.showAt();
} }
@ -26,18 +28,44 @@ PanelWithOverlay {
sidebarPopupRect.hidePopup(); sidebarPopupRect.hidePopup();
} }
Rectangle { // Trigger initial weather loading when component is completed
id: sidebarPopupRect Component.onCompleted: {
implicitWidth: 500 // Load initial weather data after a short delay to ensure all components are ready
implicitHeight: 800 Qt.callLater(function() {
visible: parent.visible if (weather && weather.fetchCityWeather)
color: "transparent" weather.fetchCityWeather();
anchors.top: parent.top
anchors.right: parent.right });
}
Rectangle {
// Access the shell's SettingsWindow instead of creating a new one
id: sidebarPopupRect
// Animation properties
property real slideOffset: width property real slideOffset: width
property bool isAnimating: false 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() { function showAt() {
if (!sidebarPopup.visible) { if (!sidebarPopup.visible) {
@ -48,26 +76,18 @@ PanelWithOverlay {
slideAnim.running = true; slideAnim.running = true;
if (weather) if (weather)
weather.startWeatherFetch(); weather.startWeatherFetch();
if (systemWidget) if (systemWidget)
systemWidget.panelVisible = true; systemWidget.panelVisible = true;
if (quickAccessWidget)
quickAccessWidget.panelVisible = true;
} }
} }
function hidePopup() { function hidePopup() {
if (sidebarPopupRect.settingsModal && sidebarPopupRect.settingsModal.visible) { if (shell && shell.settingsWindow && shell.settingsWindow.visible)
sidebarPopupRect.settingsModal.visible = false; shell.settingsWindow.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 (sidebarPopup.visible) { if (sidebarPopup.visible) {
slideAnim.from = 0; slideAnim.from = 0;
slideAnim.to = width; 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 { NumberAnimation {
id: slideAnim id: slideAnim
target: sidebarPopupRect target: sidebarPopupRect
property: "slideOffset" property: "slideOffset"
duration: 300 duration: 300
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
onStopped: { onStopped: {
if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) { if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) {
sidebarPopup.visible = false; sidebarPopup.visible = false;
// Stop monitoring and background tasks when hidden
if (weather) if (weather)
weather.stopWeatherFetch(); weather.stopWeatherFetch();
if (systemWidget) if (systemWidget)
systemWidget.panelVisible = false; systemWidget.panelVisible = false;
if (quickAccessWidget)
quickAccessWidget.panelVisible = false;
} }
sidebarPopupRect.isAnimating = false; sidebarPopupRect.isAnimating = false;
} }
onStarted: { onStarted: {
sidebarPopupRect.isAnimating = true; sidebarPopupRect.isAnimating = true;
} }
} }
property int leftPadding: 20
property int bottomPadding: 20
Rectangle { Rectangle {
id: mainRectangle id: mainRectangle
width: sidebarPopupRect.width - sidebarPopupRect.leftPadding width: sidebarPopupRect.width - sidebarPopupRect.leftPadding
height: sidebarPopupRect.height - sidebarPopupRect.bottomPadding height: sidebarPopupRect.height - sidebarPopupRect.bottomPadding
anchors.top: sidebarPopupRect.top anchors.top: sidebarPopupRect.top
x: sidebarPopupRect.leftPadding + sidebarPopupRect.slideOffset x: sidebarPopupRect.leftPadding + sidebarPopupRect.slideOffset
y: 0 y: 0
color: Theme.backgroundPrimary color: Theme.backgroundPrimary
bottomLeftRadius: 20 bottomLeftRadius: 20 * Theme.scale(Screen)
z: 0 z: 0
Behavior on x { Behavior on x {
enabled: !sidebarPopupRect.isAnimating enabled: !sidebarPopupRect.isAnimating
NumberAnimation { NumberAnimation {
duration: 300 duration: 300
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
} }
} }
} }
property alias settingsModal: settingsModal
property alias wifiPanelModal: wifiPanel.panel
property alias bluetoothPanelModal: bluetoothPanel.panel // SettingsIcon component
SettingsModal { SettingsIcon {
id: settingsModal id: settingsModal
onWeatherRefreshRequested: {
if (weather && weather.fetchCityWeather)
weather.fetchCityWeather();
}
} }
Item { Item {
anchors.fill: mainRectangle anchors.fill: mainRectangle
x: sidebarPopupRect.slideOffset x: sidebarPopupRect.slideOffset
Keys.onEscapePressed: sidebarPopupRect.hidePopup()
Behavior on x {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 20 anchors.margins: 20 * Theme.scale(Screen)
spacing: 16 spacing: 4 * Theme.scale(Screen)
System { PowerMenu {
id: systemWidget id: systemWidget
settingsModal: settingsModal
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
z: 3 z: 3
} }
@ -162,7 +230,7 @@ PanelWithOverlay {
// Music and System Monitor row // Music and System Monitor row
RowLayout { RowLayout {
spacing: 12 spacing: 12 * Theme.scale(Screen)
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@ -174,241 +242,147 @@ PanelWithOverlay {
id: systemMonitor id: systemMonitor
z: 2 z: 2
} }
} }
// Power profile, Wifi and Bluetooth row // Power profile, Record and Wallpaper row
RowLayout { RowLayout {
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: 80 spacing: 10 * Theme.scale(Screen)
spacing: 16 Layout.preferredHeight: 80 * Theme.scale(Screen)
z: 3 z: 3
PowerProfile { PowerProfile {
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: 80 Layout.preferredHeight: 80 * Theme.scale(Screen)
} }
// Network card containing Wifi and Bluetooth // Record and Wallpaper card
Rectangle { Rectangle {
Layout.preferredHeight: 80 Layout.preferredHeight: 80 * Theme.scale(Screen)
Layout.preferredWidth: 140 Layout.preferredWidth: 140 * Theme.scale(Screen)
Layout.fillWidth: false Layout.fillWidth: false
color: Theme.surface color: Theme.surface
radius: 18 radius: 18 * Theme.scale(Screen)
Row { Row {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 20 spacing: 20 * Theme.scale(Screen)
// Wifi button // Record button
Rectangle { Rectangle {
id: wifiButton id: recordButton
width: 36
height: 36 width: 36 * Theme.scale(Screen)
radius: 18 height: 36 * Theme.scale(Screen)
radius: 18 * Theme.scale(Screen)
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1 * Theme.scale(Screen)
color: wifiButtonArea.containsMouse ? Theme.accentPrimary : "transparent" color: sidebarPopupRect.isRecording ? Theme.accentPrimary :
(recordButtonArea.containsMouse ? Theme.accentPrimary : "transparent")
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "wifi" text: "photo_camera"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 22 font.pixelSize: 22 * Theme.scale(Screen)
color: wifiButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary color: sidebarPopupRect.isRecording || recordButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
MouseArea { MouseArea {
id: wifiButtonArea id: recordButtonArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: wifiPanel.showAt() onClicked: {
if (sidebarPopupRect.isRecording) {
sidebarPopupRect.stopRecording();
sidebarPopup.dismiss();
} else {
sidebarPopupRect.startRecording();
sidebarPopup.dismiss();
}
}
} }
StyledTooltip { StyledTooltip {
text: "Wifi" text: sidebarPopupRect.isRecording ? "Stop Recording" : "Start Recording"
targetItem: wifiButtonArea targetItem: recordButtonArea
tooltipVisible: wifiButtonArea.containsMouse tooltipVisible: recordButtonArea.containsMouse
} }
} }
// Bluetooth button // Wallpaper button
Rectangle { Rectangle {
id: bluetoothButton id: wallpaperButton
width: 36
height: 36 width: 36 * Theme.scale(Screen)
radius: 18 height: 36 * Theme.scale(Screen)
radius: 18 * Theme.scale(Screen)
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1 * Theme.scale(Screen)
color: bluetoothButtonArea.containsMouse ? Theme.accentPrimary : "transparent" color: wallpaperButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "bluetooth" text: "image"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 22 font.pixelSize: 22 * Theme.scale(Screen)
color: bluetoothButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary color: wallpaperButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
MouseArea { MouseArea {
id: bluetoothButtonArea id: wallpaperButtonArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: bluetoothPanel.showAt() onClicked: {
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) {
settingsModal.openSettings(6);
sidebarPopup.dismiss();
}
}
} }
StyledTooltip { StyledTooltip {
text: "Bluetooth" text: "Wallpaper"
targetItem: bluetoothButtonArea targetItem: wallpaperButtonArea
tooltipVisible: bluetoothButtonArea.containsMouse 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 Behavior on x {
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 {
enabled: !sidebarPopupRect.isAnimating enabled: !sidebarPopupRect.isAnimating
NumberAnimation { NumberAnimation {
duration: 300 duration: 300
easing.type: Easing.OutCubic 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
import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Widgets import Quickshell.Widgets
import qs.Components
import qs.Helpers
import qs.Services
import qs.Settings import qs.Settings
import qs.Widgets import qs.Widgets
import qs.Widgets.LockScreen import qs.Widgets.LockScreen
import qs.Helpers
import qs.Services
import qs.Components
Rectangle { Rectangle {
id: systemWidget id: systemWidget
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: "--:--" property string uptimeText: "--:--"
property bool panelVisible: false
// Process to get uptime property var settingsModal: null
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 { Process {
id: logoutProcess id: logoutProcess
@ -399,14 +26,12 @@ Rectangle {
} }
function logout() { function logout() {
if (WorkspaceManager.isNiri) { if (WorkspaceManager.isNiri)
logoutProcessNiri.running = true; logoutProcessNiri.running = true;
} else if (WorkspaceManager.isHyprland) { else if (WorkspaceManager.isHyprland)
logoutProcessHyprland.running = true; logoutProcessHyprland.running = true;
} else { else
// fallback or error
console.warn("No supported compositor detected for logout"); console.warn("No supported compositor detected for logout");
}
} }
function suspend() { function suspend() {
@ -421,33 +46,479 @@ Rectangle {
rebootProcess.running = true; rebootProcess.running = true;
} }
property bool panelVisible: false function updateSystemInfo() {
uptimeProcess.running = true;
// Trigger initial update when panel becomes visible }
onPanelVisibleChanged: {
if (panelVisible) { width: 440 * Theme.scale(Screen)
updateSystemInfo(); 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 { Timer {
interval: 60000 // Update every minute interval: 60000
repeat: true repeat: true
running: panelVisible running: panelVisible
onTriggered: updateSystemInfo() onTriggered: updateSystemInfo()
} }
Component.onCompleted: {
uptimeProcess.running = true;
}
function updateSystemInfo() {
uptimeProcess.running = true;
}
// Add lockscreen instance (hidden by default)
LockScreen { LockScreen {
id: lockScreen id: lockScreen
} }
} }

View file

@ -7,22 +7,22 @@ import qs.Components
Rectangle { Rectangle {
id: card id: card
width: 200 width: 200 * Theme.scale(Screen)
height: 70 height: 70 * Theme.scale(Screen)
color: Theme.surface color: Theme.surface
radius: 18 radius: 18 * Theme.scale(Screen)
Row { Row {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 20 spacing: 20 * Theme.scale(Screen)
// Performance
Rectangle { Rectangle {
width: 36; height: 36 width: 36 * Theme.scale(Screen); height: 36 * Theme.scale(Screen)
radius: 18 radius: 18 * Theme.scale(Screen)
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1 * Theme.scale(Screen)
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance) color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance)
? Theme.accentPrimary ? Theme.accentPrimary
: (perfMouseArea.containsMouse ? Theme.accentPrimary : "transparent") : (perfMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
@ -33,7 +33,7 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
text: "speed" text: "speed"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 22 font.pixelSize: 22 * Theme.scale(Screen)
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance) || perfMouseArea.containsMouse color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance) || perfMouseArea.containsMouse
? Theme.backgroundPrimary ? Theme.backgroundPrimary
: Theme.accentPrimary : Theme.accentPrimary
@ -63,12 +63,12 @@ Rectangle {
} }
} }
// Balanced
Rectangle { Rectangle {
width: 36; height: 36 width: 36 * Theme.scale(Screen); height: 36 * Theme.scale(Screen)
radius: 18 radius: 18 * Theme.scale(Screen)
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1 * Theme.scale(Screen)
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced) color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced)
? Theme.accentPrimary ? Theme.accentPrimary
: (balMouseArea.containsMouse ? Theme.accentPrimary : "transparent") : (balMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
@ -79,7 +79,7 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
text: "balance" text: "balance"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 22 font.pixelSize: 22 * Theme.scale(Screen)
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced) || balMouseArea.containsMouse color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced) || balMouseArea.containsMouse
? Theme.backgroundPrimary ? Theme.backgroundPrimary
: Theme.accentPrimary : Theme.accentPrimary
@ -109,12 +109,12 @@ Rectangle {
} }
} }
// Power Saver
Rectangle { Rectangle {
width: 36; height: 36 width: 36 * Theme.scale(Screen); height: 36 * Theme.scale(Screen)
radius: 18 radius: 18 * Theme.scale(Screen)
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1 * Theme.scale(Screen)
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver) color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver)
? Theme.accentPrimary ? Theme.accentPrimary
: (saveMouseArea.containsMouse ? Theme.accentPrimary : "transparent") : (saveMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
@ -125,7 +125,7 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
text: "eco" text: "eco"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 22 font.pixelSize: 22 * Theme.scale(Screen)
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver) || saveMouseArea.containsMouse color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver) || saveMouseArea.containsMouse
? Theme.backgroundPrimary ? Theme.backgroundPrimary
: Theme.accentPrimary : 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 { Rectangle {
id: systemMonitor id: systemMonitor
width: 70 width: 70 * Theme.scale(Screen)
height: 250 height: 250 * Theme.scale(Screen)
color: "transparent" color: "transparent"
// Track visibility state for panel integration
property bool isVisible: false property bool isVisible: false
Rectangle { Rectangle {
id: card id: card
anchors.fill: parent anchors.fill: parent
color: Theme.surface color: Theme.surface
radius: 18 radius: 18 * Theme.scale(Screen)
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 8 anchors.margins: 8 * Theme.scale(Screen)
spacing: 12 spacing: 12 * Theme.scale(Screen)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
// CPU Usage
// CPU usage indicator with circular progress bar
Item { Item {
width: 50; height: 50 width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen)
CircularProgressBar { CircularProgressBar {
id: cpuBar id: cpuBar
progress: Sysinfo.cpuUsage / 100 progress: Sysinfo.cpuUsage / 100
size: 50 size: 50 * Theme.scale(Screen)
strokeWidth: 4 strokeWidth: 4 * Theme.scale(Screen)
hasNotch: true hasNotch: true
notchIcon: "speed" notchIcon: "speed"
notchIconSize: 14 notchIconSize: 14 * Theme.scale(Screen)
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
MouseArea { MouseArea {
@ -55,18 +57,19 @@ Rectangle {
} }
} }
// Cpu Temp
// CPU temperature indicator with circular progress bar
Item { Item {
width: 50; height: 50 width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen)
CircularProgressBar { CircularProgressBar {
id: tempBar id: tempBar
progress: Sysinfo.cpuTemp / 100 progress: Sysinfo.cpuTemp / 100
size: 50 size: 50 * Theme.scale(Screen)
strokeWidth: 4 strokeWidth: 4 * Theme.scale(Screen)
hasNotch: true hasNotch: true
units: "°C" units: "°C"
notchIcon: "thermometer" notchIcon: "thermometer"
notchIconSize: 14 notchIconSize: 14 * Theme.scale(Screen)
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
MouseArea { MouseArea {
@ -85,17 +88,18 @@ Rectangle {
} }
} }
// Memory Usage
// Memory usage indicator with circular progress bar
Item { Item {
width: 50; height: 50 width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen)
CircularProgressBar { CircularProgressBar {
id: memBar id: memBar
progress: Sysinfo.memoryUsagePer / 100 progress: Sysinfo.memoryUsagePer / 100
size: 50 size: 50 * Theme.scale(Screen)
strokeWidth: 4 strokeWidth: 4 * Theme.scale(Screen)
hasNotch: true hasNotch: true
notchIcon: "memory" notchIcon: "memory"
notchIconSize: 14 notchIconSize: 14 * Theme.scale(Screen)
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
MouseArea { MouseArea {
@ -114,17 +118,18 @@ Rectangle {
} }
} }
// Disk Usage
// Disk usage indicator with circular progress bar
Item { Item {
width: 50; height: 50 width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen)
CircularProgressBar { CircularProgressBar {
id: diskBar id: diskBar
progress: Sysinfo.diskUsage / 100 progress: Sysinfo.diskUsage / 100
size: 50 size: 50 * Theme.scale(Screen)
strokeWidth: 4 strokeWidth: 4 * Theme.scale(Screen)
hasNotch: true hasNotch: true
notchIcon: "storage" notchIcon: "storage"
notchIconSize: 14 notchIconSize: 14 * Theme.scale(Screen)
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
MouseArea { MouseArea {

View file

@ -2,12 +2,12 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import qs.Settings import qs.Settings
import "../../../Helpers/Weather.js" as WeatherHelper import "../../Helpers/Weather.js" as WeatherHelper
Rectangle { Rectangle {
id: weatherRoot id: weatherRoot
width: 440 width: 440 * Theme.scale(Screen)
height: 180 height: 180 * Theme.scale(Screen)
color: "transparent" color: "transparent"
anchors.horizontalCenterOffset: -2 anchors.horizontalCenterOffset: -2
@ -15,6 +15,20 @@ Rectangle {
property var weatherData: null property var weatherData: null
property string errorString: "" property string errorString: ""
property bool isVisible: false 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: { Component.onCompleted: {
if (isVisible) { if (isVisible) {
@ -23,20 +37,42 @@ Rectangle {
} }
function fetchCityWeather() { 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, WeatherHelper.fetchCityWeather(city,
function(result) { function(result) {
weatherData = result.weather; weatherData = result.weather;
lastFetchTime = currentTime;
errorString = ""; errorString = "";
isLoading = false;
}, },
function(err) { function(err) {
errorString = err; errorString = err;
isLoading = false;
} }
); );
} }
function startWeatherFetch() { function startWeatherFetch() {
isVisible = true isVisible = true
fetchCityWeather() // Force refresh when panel opens, regardless of time check
lastFetchTime = 0;
fetchCityWeather();
} }
function stopWeatherFetch() { function stopWeatherFetch() {
@ -47,81 +83,90 @@ Rectangle {
id: card id: card
anchors.fill: parent anchors.fill: parent
color: Theme.surface color: Theme.surface
radius: 18 radius: 18 * Theme.scale(Screen)
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 18 anchors.margins: 18 * Theme.scale(Screen)
spacing: 12 spacing: 12 * Theme.scale(Screen)
// Current weather row
RowLayout { RowLayout {
spacing: 12 spacing: 12 * Theme.scale(Screen)
Layout.fillWidth: true 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 { Text {
id: weatherIcon 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.family: "Material Symbols Outlined"
font.pixelSize: 28 font.pixelSize: 28 * Theme.scale(Screen)
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: Theme.accentPrimary color: isLoading ? Theme.accentPrimary : Theme.accentPrimary
Layout.alignment: Qt.AlignVCenter 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 { ColumnLayout {
spacing: 2 spacing: 2 * Theme.scale(Screen)
RowLayout { RowLayout {
spacing: 4 spacing: 4 * Theme.scale(Screen)
Text { Text {
text: city text: city
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: 14 font.pixelSize: 14 * Theme.scale(Screen)
font.bold: true font.bold: true
color: Theme.textPrimary color: Theme.textPrimary
} }
Text { Text {
text: weatherData && weatherData.timezone_abbreviation ? `(${weatherData.timezone_abbreviation})` : "" text: weatherData && weatherData.timezone_abbreviation ? `(${weatherData.timezone_abbreviation})` : ""
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: 10 font.pixelSize: 10 * Theme.scale(Screen)
color: Theme.textSecondary color: Theme.textSecondary
leftPadding: 2 leftPadding: 2 * Theme.scale(Screen)
} }
} }
Text { 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") 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.family: Theme.fontFamily
font.pixelSize: 24 font.pixelSize: 24 * Theme.scale(Screen)
font.bold: true font.bold: true
color: Theme.textPrimary color: Theme.textPrimary
} }
} }
} }
// Spacer to push content to the right
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
// Separator line
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 1 height: 1 * Theme.scale(Screen)
color: Qt.rgba(Theme.textSecondary.g, Theme.textSecondary.g, Theme.textSecondary.b, 0.12) color: Qt.rgba(Theme.textSecondary.g, Theme.textSecondary.g, Theme.textSecondary.b, 0.12)
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 2 Layout.topMargin: 2 * Theme.scale(Screen)
Layout.bottomMargin: 2 Layout.bottomMargin: 2 * Theme.scale(Screen)
} }
// 5-day forecast row
RowLayout { RowLayout {
spacing: 12 spacing: 12 * Theme.scale(Screen)
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
visible: weatherData && weatherData.daily && weatherData.daily.time visible: weatherData && weatherData.daily && weatherData.daily.time
@ -129,31 +174,31 @@ Rectangle {
Repeater { Repeater {
model: weatherData && weatherData.daily && weatherData.daily.time ? 5 : 0 model: weatherData && weatherData.daily && weatherData.daily.time ? 5 : 0
delegate: ColumnLayout { delegate: ColumnLayout {
spacing: 2 spacing: 2 * Theme.scale(Screen)
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Text { Text {
// Day of the week (e.g., Mon)
text: Qt.formatDateTime(new Date(weatherData.daily.time[index]), "ddd") text: Qt.formatDateTime(new Date(weatherData.daily.time[index]), "ddd")
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: 12 font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textSecondary color: Theme.textSecondary
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
Text { Text {
// Material Symbol icon
text: materialSymbolForCode(weatherData.daily.weathercode[index]) text: materialSymbolForCode(weatherData.daily.weathercode[index])
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 22 font.pixelSize: 22 * Theme.scale(Screen)
color: Theme.accentPrimary color: Theme.accentPrimary
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
Text { 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) ? "--° / --°" : "--° / --°") 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.family: Theme.fontFamily
font.pixelSize: 12 font.pixelSize: 12 * Theme.scale(Screen)
color: Theme.textPrimary color: Theme.textPrimary
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@ -162,29 +207,29 @@ Rectangle {
} }
} }
// Error message
Text { Text {
text: errorString text: errorString
color: Theme.error color: Theme.error
visible: errorString !== "" visible: errorString !== ""
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: 10 font.pixelSize: 10 * Theme.scale(Screen)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
} }
} }
// Weather code to Material Symbol ligature mapping
function materialSymbolForCode(code) { function materialSymbolForCode(code) {
if (code === 0) return "sunny"; // Clear if (code === 0) return "sunny";
if (code === 1 || code === 2) return "partly_cloudy_day"; // Mainly clear/partly cloudy if (code === 1 || code === 2) return "partly_cloudy_day";
if (code === 3) return "cloud"; // Overcast if (code === 3) return "cloud";
if (code >= 45 && code <= 48) return "foggy"; // Fog if (code >= 45 && code <= 48) return "foggy";
if (code >= 51 && code <= 67) return "rainy"; // Drizzle if (code >= 51 && code <= 67) return "rainy";
if (code >= 71 && code <= 77) return "weather_snowy"; // Snow if (code >= 71 && code <= 77) return "weather_snowy";
if (code >= 80 && code <= 82) return "rainy"; // Rain showers if (code >= 80 && code <= 82) return "rainy";
if (code >= 95 && code <= 99) return "thunderstorm"; // Thunderstorm if (code >= 95 && code <= 99) return "thunderstorm";
return "cloud"; return "cloud";
} }
function weatherDescriptionForCode(code) { function weatherDescriptionForCode(code) {

View file

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

0
qmlls.ini Normal file
View file

View file

@ -9,19 +9,26 @@ import qs.Bar.Modules
import qs.Widgets import qs.Widgets
import qs.Widgets.LockScreen import qs.Widgets.LockScreen
import qs.Widgets.Notification import qs.Widgets.Notification
import qs.Widgets.SettingsWindow
import qs.Settings import qs.Settings
import qs.Helpers import qs.Helpers
import "./Helpers/IdleInhibitor.qml"
import "./Helpers/IPCHandlers.qml"
Scope { Scope {
id: root id: root
property alias appLauncherPanel: appLauncherPanel property alias appLauncherPanel: appLauncherPanel
property var notificationHistoryWin: notificationHistoryWin property var notificationHistoryWin: notificationHistoryLoader.active ? notificationHistoryLoader.item : null
property var settingsWindow: null
property bool pendingReload: false property bool pendingReload: false
// Function to load notification history
function loadNotificationHistory() {
if (!notificationHistoryLoader.active) {
notificationHistoryLoader.loading = true;
}
return notificationHistoryLoader;
}
// Helper function to round value to nearest step // Helper function to round value to nearest step
function roundToStep(value, step) { function roundToStep(value, step) {
return Math.round(value / step) * step; return Math.round(value / step) * step;
@ -47,10 +54,22 @@ Scope {
Quickshell.shell = root; Quickshell.shell = root;
} }
Background {}
Overview {}
Bar { Bar {
id: bar id: bar
shell: root shell: root
property var notificationHistoryWin: notificationHistoryWin property var notificationHistoryWin: notificationHistoryLoader.active ? notificationHistoryLoader.item : null
}
Variants {
model: Quickshell.screens
Dock {
id: dock
property var modelData
}
} }
Dock { Dock {
@ -79,13 +98,14 @@ Scope {
NotificationServer { NotificationServer {
id: notificationServer id: notificationServer
onNotification: function (notification) { onNotification: function (notification) {
console.log("Notification received:", notification.appName); console.log("[Notification] Received notification:", notification.appName, "-", notification.summary);
notification.tracked = true; notification.tracked = true;
if (notificationPopup.notificationsVisible) { if (notificationPopup.notificationsVisible) {
// Add notification to the popup manager
notificationPopup.addNotification(notification); notificationPopup.addNotification(notification);
} }
if (notificationHistoryWin) { if (notificationHistoryLoader.active && notificationHistoryLoader.item) {
notificationHistoryWin.addToHistory({ notificationHistoryLoader.item.addToHistory({
id: notification.id, id: notification.id,
appName: notification.appName || "Notification", appName: notification.appName || "Notification",
summary: notification.summary || "", summary: notification.summary || "",
@ -99,11 +119,35 @@ Scope {
NotificationPopup { NotificationPopup {
id: notificationPopup id: notificationPopup
barVisible: bar.visible
} }
NotificationHistory { // LazyLoader for NotificationHistory - only load when needed
id: notificationHistoryWin LazyLoader {
id: notificationHistoryLoader
loading: false
component: NotificationHistory {}
}
// Centralized LazyLoader for SettingsWindow - prevents crashes on multiple opens
LazyLoader {
id: settingsWindowLoader
loading: false
component: SettingsWindow {
Component.onCompleted: {
root.settingsWindow = this;
}
}
}
// Function to safely show/hide settings window
function toggleSettingsWindow() {
if (!settingsWindowLoader.active) {
settingsWindowLoader.loading = true;
}
if (settingsWindowLoader.item) {
settingsWindowLoader.item.visible = !settingsWindowLoader.item.visible;
}
} }
// Reference to the default audio sink from Pipewire // Reference to the default audio sink from Pipewire
@ -144,14 +188,15 @@ Scope {
function onScreensChanged() { function onScreensChanged() {
if (lockScreen.locked) { if (lockScreen.locked) {
pendingReload = true; pendingReload = true;
} else { } /*else {
reloadTimer.restart(); reloadTimer.restart();
} } */
// ^commented out for now to fix QS crash on monitor wake.
// if it reintroduces the notification bug (https://github.com/Ly-sec/Noctalia/issues/32)...
// we need to find a different fix
} }
} }
// --- NEW: Keep volume property in sync with actual Pipewire audio sink volume ---
Connections { Connections {
target: defaultAudioSink ? defaultAudioSink.audio : null target: defaultAudioSink ? defaultAudioSink.audio : null
function onVolumeChanged() { function onVolumeChanged() {
@ -172,4 +217,5 @@ Scope {
} }
} }
} }
} }