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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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()
}
function toggleNotificationPopup(): void {
console.log("[IPC] NotificationPopup toggle() called")
// Use the global toggle function from the notification manager
notificationPopup.togglePopup();
}
@ -43,4 +43,4 @@ IpcHandler {
console.log("[IPC] LockScreen show() called");
lockScreen.locked = true;
}
}
}

View file

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

View file

@ -231,4 +231,4 @@ Thank you to everyone who supports me and this project 💜!
## License
This project is licensed under the terms of the [MIT License](./LICENSE).
This project is licensed under the terms of the [MIT License](./LICENSE).

View file

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

0
qmlls.ini Normal file
View file

View file

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