Settings rework...

This commit is contained in:
Ly-sec 2025-08-05 17:41:08 +02:00
parent 74b233798d
commit fb68300746
63 changed files with 7139 additions and 1026 deletions

View file

@ -4,7 +4,7 @@ import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import Qt5Compat.GraphicalEffects import QtQuick.Effects
import qs.Bar.Modules import qs.Bar.Modules
import qs.Settings import qs.Settings
import qs.Services import qs.Services
@ -15,6 +15,7 @@ import qs.Widgets.Sidebar
import qs.Widgets.Sidebar.Panel import qs.Widgets.Sidebar.Panel
import qs.Widgets.Notification import qs.Widgets.Notification
// Main bar component - creates panels on selected monitors with widgets and corners
Scope { Scope {
id: rootScope id: rootScope
property var shell property var shell
@ -38,7 +39,8 @@ Scope {
anchors.left: true anchors.left: true
anchors.right: true anchors.right: true
visible: true visible: Settings.settings.barMonitors.includes(modelData.name) ||
(Settings.settings.barMonitors.length === 0)
Rectangle { Rectangle {
id: barBackground id: barBackground
@ -103,6 +105,14 @@ Scope {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Wifi {
anchors.verticalCenter: parent.verticalCenter
}
Bluetooth {
anchors.verticalCenter: parent.verticalCenter
}
Battery { Battery {
id: widgetsBattery id: widgetsBattery
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -110,6 +120,7 @@ Scope {
Brightness { Brightness {
id: widgetsBrightness id: widgetsBrightness
screen: modelData
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@ -124,6 +135,10 @@ Scope {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
}
PanelPopup { PanelPopup {
id: sidebarPopup id: sidebarPopup
} }
@ -149,7 +164,8 @@ Scope {
screen: modelData screen: modelData
margins.top: 36 margins.top: 36
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true visible: Settings.settings.barMonitors.includes(modelData.name) ||
(Settings.settings.barMonitors.length === 0)
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Background
aboveWindows: false aboveWindows: false
WlrLayershell.namespace: "swww-daemon" WlrLayershell.namespace: "swww-daemon"
@ -175,7 +191,8 @@ Scope {
screen: modelData screen: modelData
margins.top: 36 margins.top: 36
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true visible: Settings.settings.barMonitors.includes(modelData.name) ||
(Settings.settings.barMonitors.length === 0)
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Background
aboveWindows: false aboveWindows: false
WlrLayershell.namespace: "swww-daemon" WlrLayershell.namespace: "swww-daemon"
@ -201,7 +218,8 @@ Scope {
color: "transparent" color: "transparent"
screen: modelData screen: modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true visible: Settings.settings.barMonitors.includes(modelData.name) ||
(Settings.settings.barMonitors.length === 0)
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Background
aboveWindows: false aboveWindows: false
WlrLayershell.namespace: "swww-daemon" WlrLayershell.namespace: "swww-daemon"
@ -227,7 +245,8 @@ Scope {
color: "transparent" color: "transparent"
screen: modelData screen: modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true visible: Settings.settings.barMonitors.includes(modelData.name) ||
(Settings.settings.barMonitors.length === 0)
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Background
aboveWindows: false aboveWindows: false
WlrLayershell.namespace: "swww-daemon" WlrLayershell.namespace: "swww-daemon"
@ -249,6 +268,6 @@ Scope {
} }
} }
// This alias exposes the visual bar's visibility to the outside world
property alias visible: barRootItem.visible property alias visible: barRootItem.visible
} }

View file

@ -1,9 +1,9 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Components
import qs.Settings
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets import Quickshell.Widgets
import qs.Components
import qs.Settings
PanelWindow { PanelWindow {
id: activeWindowPanel id: activeWindowPanel
@ -14,7 +14,7 @@ PanelWindow {
anchors.right: true anchors.right: true
focusable: false focusable: false
margins.top: barHeight margins.top: barHeight
visible: !activeWindowWrapper.finallyHidden visible: Settings.settings.showActiveWindow && !activeWindowWrapper.finallyHidden
implicitHeight: activeWindowTitleContainer.height implicitHeight: activeWindowTitleContainer.height
implicitWidth: 0 implicitWidth: 0
property int barHeight: 36 property int barHeight: 36
@ -121,6 +121,7 @@ PanelWindow {
source: ToplevelManager?.activeToplevel ? getIcon() : "" source: ToplevelManager?.activeToplevel ? getIcon() : ""
visible: Settings.settings.showActiveWindowIcon visible: Settings.settings.showActiveWindowIcon
anchors.verticalCenterOffset: -3 anchors.verticalCenterOffset: -3
} }
Text { Text {

View file

@ -3,18 +3,159 @@ import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Components import qs.Components
import qs.Settings import qs.Settings
import Quickshell.Wayland
import "../../Helpers/Fuzzysort.js" as Fuzzysort import "../../Helpers/Fuzzysort.js" as Fuzzysort
PanelWithOverlay { PanelWithOverlay {
Timer {
id: clipboardTimer
interval: 1000
repeat: true
running: appLauncherPanel.visible && searchField.text.startsWith(">clip")
onTriggered: {
updateClipboardHistory();
}
}
property var clipboardHistory: []
property bool clipboardInitialized: false
Process {
id: clipboardTypeProcess
property bool isLoading: false
property var currentTypes: []
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
currentTypes = String(stdout.text).trim().split('\n').filter(t => t);
const imageType = currentTypes.find(t => t.startsWith('image/'));
if (imageType) {
clipboardImageProcess.mimeType = imageType;
clipboardImageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`];
clipboardImageProcess.running = true;
} else {
clipboardHistoryProcess.command = ["wl-paste", "-n", "--type", "text/plain"];
clipboardHistoryProcess.running = true;
}
} else {
clipboardTypeProcess.isLoading = false;
}
}
stdout: StdioCollector {}
}
Process {
id: clipboardImageProcess
property string mimeType: ""
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
const base64 = stdout.text.trim();
if (base64) {
const entry = {
type: 'image',
mimeType: mimeType,
data: `data:${mimeType};base64,${base64}`,
timestamp: new Date().getTime()
};
const exists = clipboardHistory.find(item =>
item.type === 'image' && item.data === entry.data
);
if (!exists) {
clipboardHistory = [entry, ...clipboardHistory].slice(0, 20);
root.updateFilter();
}
}
}
if (!clipboardHistoryProcess.isLoading) {
clipboardInitialized = true;
}
clipboardTypeProcess.isLoading = false;
}
stdout: StdioCollector {}
}
Process {
id: clipboardHistoryProcess
property bool isLoading: false
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
const content = String(stdout.text).trim();
if (content && !content.startsWith("vscode-file://")) {
const entry = {
type: 'text',
content: content,
timestamp: new Date().getTime()
};
const exists = clipboardHistory.find(item => {
if (item.type === 'text') {
return item.content === content;
}
return item === content;
});
if (!exists) {
const newHistory = clipboardHistory.map(item => {
if (typeof item === 'string') {
return {
type: 'text',
content: item,
timestamp: new Date().getTime()
};
}
return item;
});
clipboardHistory = [entry, ...newHistory].slice(0, 20);
}
}
} else {
clipboardHistoryProcess.isLoading = false;
}
clipboardInitialized = true;
clipboardTypeProcess.isLoading = false;
root.updateFilter();
}
stdout: StdioCollector {}
}
function updateClipboardHistory() {
if (!clipboardTypeProcess.isLoading && !clipboardHistoryProcess.isLoading) {
clipboardTypeProcess.isLoading = true;
clipboardTypeProcess.command = ["wl-paste", "-l"];
clipboardTypeProcess.running = true;
}
}
id: appLauncherPanel id: appLauncherPanel
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
function isPinned(app) { function isPinned(app) {
return app && app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1; return app && app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1;
} }
function togglePin(app) { function togglePin(app) {
if (!app || !app.execString) return; if (!app || !app.execString) return;
var arr = Settings.settings.pinnedExecs ? Settings.settings.pinnedExecs.slice() : []; var arr = Settings.settings.pinnedExecs ? Settings.settings.pinnedExecs.slice() : [];
@ -101,9 +242,11 @@ PanelWithOverlay {
appLauncherPanel.visible = false; appLauncherPanel.visible = false;
} }
} }
function isMathExpression(str) { function isMathExpression(str) {
return /^[-+*/().0-9\s]+$/.test(str); return /^[-+*/().0-9\s]+$/.test(str);
} }
function safeEval(expr) { function safeEval(expr) {
try { try {
return Function('return (' + expr + ')')(); return Function('return (' + expr + ')')();
@ -111,13 +254,114 @@ PanelWithOverlay {
return undefined; return undefined;
} }
} }
function updateFilter() { function updateFilter() {
var query = searchField.text ? searchField.text.toLowerCase() : ""; var query = searchField.text ? searchField.text.toLowerCase() : "";
var apps = root.appModel.slice(); var apps = root.appModel.slice();
var results = []; var results = [];
// Calculator mode: starts with '='
if (query.startsWith("=")) {
var expr = searchField.text.slice(1).trim(); if (query === ">") {
results.push({
isCommand: true,
name: ">calc",
content: "Calculator - evaluate mathematical expressions",
icon: "calculate",
execute: function() {
searchField.text = ">calc ";
searchField.cursorPosition = searchField.text.length;
}
});
results.push({
isCommand: true,
name: ">clip",
content: "Clipboard history - browse and restore clipboard items",
icon: "content_paste",
execute: function() {
searchField.text = ">clip ";
searchField.cursorPosition = searchField.text.length;
}
});
root.filteredApps = results;
return;
}
if (query.startsWith(">clip")) {
if (!clipboardInitialized) {
updateClipboardHistory();
}
const searchTerm = query.slice(5).trim();
clipboardHistory.forEach(function(clip, index) {
let searchContent = clip.type === 'image' ?
clip.mimeType :
clip.content || clip; // Support both new object format and old string format
if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) {
let entry;
if (clip.type === 'image') {
entry = {
isClipboard: true,
name: "Image from " + new Date(clip.timestamp).toLocaleTimeString(),
content: "Image: " + clip.mimeType,
icon: "image",
type: 'image',
data: clip.data,
execute: function() {
// Convert base64 image data back to binary and copy to clipboard
const base64Data = clip.data.split(',')[1];
clipboardTypeProcess.command = ["sh", "-c", `echo '${base64Data}' | base64 -d | wl-copy -t '${clip.mimeType}'`];
clipboardTypeProcess.running = true;
}
};
} else {
const textContent = clip.content || clip; // Support both new object format and old string format
let displayContent = textContent;
let previewContent = "";
// Clean up whitespace for display
displayContent = displayContent.replace(/\s+/g, ' ').trim();
// Truncate long content and show preview
if (displayContent.length > 50) {
previewContent = displayContent;
// Show first line or first 50 characters as title
displayContent = displayContent.split('\n')[0].substring(0, 50) + "...";
}
entry = {
isClipboard: true,
name: displayContent,
content: previewContent || textContent,
icon: "content_paste",
execute: function() {
Quickshell.execDetached(["sh", "-c", "echo -n '" + textContent.replace(/'/g, "'\\''") + "' | wl-copy"]);
}
};
}
results.push(entry);
}
});
if (results.length === 0) {
results.push({
isClipboard: true,
name: "No clipboard history",
content: "No matching clipboard entries found",
icon: "content_paste_off"
});
}
root.filteredApps = results;
return;
}
if (query.startsWith(">calc")) {
var expr = searchField.text.slice(5).trim();
if (expr && isMathExpression(expr)) { if (expr && isMathExpression(expr)) {
var value = safeEval(expr); var value = safeEval(expr);
if (value !== undefined && value !== null && value !== "") { if (value !== undefined && value !== null && value !== "") {
@ -130,8 +374,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);
} }
if (!query || query.startsWith("=")) { }
// 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) {
results = results.concat(apps.sort(function (a, b) { results = results.concat(apps.sort(function (a, b) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
})); }));
@ -143,7 +406,7 @@ PanelWithOverlay {
return r.obj; return r.obj;
})); }));
} }
// Pinning logic: split into pinned and unpinned
var pinned = []; var pinned = [];
var unpinned = []; var unpinned = [];
for (var i = 0; i < results.length; ++i) { for (var i = 0; i < results.length; ++i) {
@ -161,10 +424,12 @@ PanelWithOverlay {
root.filteredApps = pinned.concat(unpinned); root.filteredApps = pinned.concat(unpinned);
root.selectedIndex = 0; root.selectedIndex = 0;
} }
function selectNext() { function selectNext() {
if (filteredApps.length > 0) if (filteredApps.length > 0)
selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1); selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1);
} }
function selectPrev() { function selectPrev() {
if (filteredApps.length > 0) if (filteredApps.length > 0)
selectedIndex = Math.max(selectedIndex - 1, 0); selectedIndex = Math.max(selectedIndex - 1, 0);
@ -182,6 +447,10 @@ PanelWithOverlay {
Quickshell.clipboardText = String(modelData.result); Quickshell.clipboardText = String(modelData.result);
Quickshell.execDetached(["notify-send", "Calculator Result", `${modelData.expr} = ${modelData.result} (copied to clipboard)`]); Quickshell.execDetached(["notify-send", "Calculator Result", `${modelData.expr} = ${modelData.result} (copied to clipboard)`]);
}); });
} else if (modelData.isCommand) {
modelData.execute();
return;
} else if (modelData.runInTerminal && termEmu){ } else if (modelData.runInTerminal && termEmu){
Quickshell.execDetached([termEmu, "-e", modelData.execString.trim()]); Quickshell.execDetached([termEmu, "-e", modelData.execString.trim()]);
} else if (modelData.execute) { } else if (modelData.execute) {
@ -200,19 +469,48 @@ PanelWithOverlay {
Component.onCompleted: updateFilter() Component.onCompleted: updateFilter()
ColumnLayout { RowLayout {
anchors.left: parent.left anchors.fill: parent
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: 32 anchors.margins: 32
spacing: 18 spacing: 18
// Search Bar
Rectangle {
id: previewPanel
Layout.preferredWidth: 200
Layout.fillHeight: true
color: Theme.surface
radius: 20
visible: false
Rectangle {
anchors.fill: parent
anchors.margins: 16
color: "transparent"
clip: true
Image {
id: previewImage
anchors.fill: parent
fillMode: Image.PreserveAspectFit
asynchronous: true
cache: true
smooth: true
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 18
Rectangle { Rectangle {
id: searchBar id: searchBar
color: Theme.surfaceVariant color: Theme.surfaceVariant
radius: 22 radius: 20
height: 48 height: 48
Layout.fillWidth: true Layout.fillWidth: true
border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline
@ -225,6 +523,7 @@ PanelWithOverlay {
anchors.leftMargin: 14 anchors.leftMargin: 14
anchors.rightMargin: 14 anchors.rightMargin: 14
spacing: 10 spacing: 10
Text { Text {
text: "search" text: "search"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
@ -233,6 +532,7 @@ PanelWithOverlay {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
TextField { TextField {
id: searchField id: searchField
placeholderText: "Search apps..." placeholderText: "Search apps..."
@ -263,11 +563,13 @@ PanelWithOverlay {
Keys.onEscapePressed: appLauncherPanel.hidePanel() Keys.onEscapePressed: appLauncherPanel.hidePanel()
} }
} }
Behavior on border.color { Behavior on border.color {
ColorAnimation { ColorAnimation {
duration: 120 duration: 120
} }
} }
Behavior on border.width { Behavior on border.width {
NumberAnimation { NumberAnimation {
duration: 120 duration: 120
@ -275,7 +577,7 @@ PanelWithOverlay {
} }
} }
// App List Card
Rectangle { Rectangle {
color: Theme.surface color: Theme.surface
radius: 20 radius: 20
@ -284,14 +586,6 @@ PanelWithOverlay {
clip: true clip: true
property int innerPadding: 16 property int innerPadding: 16
Item {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: parent.innerPadding
visible: false
}
ListView { ListView {
id: appList id: appList
anchors.fill: parent anchors.fill: parent
@ -302,7 +596,7 @@ PanelWithOverlay {
delegate: Item { delegate: Item {
id: appDelegate id: appDelegate
width: appList.width width: appList.width
height: 48 height: (modelData.isClipboard || modelData.isCommand) ? 64 : 48
property bool hovered: mouseArea.containsMouse property bool hovered: mouseArea.containsMouse
property bool isSelected: index === root.selectedIndex property bool isSelected: index === root.selectedIndex
@ -316,16 +610,19 @@ PanelWithOverlay {
? "transparent" ? "transparent"
: (hovered || isSelected ? Theme.accentPrimary : "transparent") : (hovered || isSelected ? Theme.accentPrimary : "transparent")
border.width: appLauncherPanel.isPinned(modelData) ? 0 : (hovered || isSelected ? 2 : 0) border.width: appLauncherPanel.isPinned(modelData) ? 0 : (hovered || isSelected ? 2 : 0)
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: 120 duration: 120
} }
} }
Behavior on border.color { Behavior on border.color {
ColorAnimation { ColorAnimation {
duration: 120 duration: 120
} }
} }
Behavior on border.width { Behavior on border.width {
NumberAnimation { NumberAnimation {
duration: 120 duration: 120
@ -338,23 +635,53 @@ PanelWithOverlay {
anchors.leftMargin: 10 anchors.leftMargin: 10
anchors.rightMargin: 10 anchors.rightMargin: 10
spacing: 10 spacing: 10
Item { Item {
width: 28 width: 28
height: 28 height: 28
property bool iconLoaded: !modelData.isCalculator && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error property bool iconLoaded: !modelData.isCalculator && !modelData.isClipboard && !modelData.isCommand && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error
Image { 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 id: iconImg
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectFit
smooth: true
cache: false
asynchronous: true asynchronous: true
source: modelData.isCalculator ? "qrc:/icons/calculate.svg" : Quickshell.iconPath(modelData.icon, "application-x-executable") source: modelData.isCalculator ? "qrc:/icons/calculate.svg" :
visible: modelData.isCalculator || parent.iconLoaded 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 { Text {
anchors.centerIn: parent anchors.centerIn: parent
visible: !modelData.isCalculator && !parent.iconLoaded visible: !modelData.isCalculator && !modelData.isClipboard && !modelData.isCommand && !parent.iconLoaded && modelData.type !== 'image'
text: "broken_image" text: "broken_image"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeHeader font.pixelSize: Theme.fontSizeHeader
@ -365,6 +692,7 @@ PanelWithOverlay {
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 1 spacing: 1
Text { Text {
text: modelData.name text: modelData.name
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textPrimary) color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textPrimary)
@ -375,21 +703,29 @@ PanelWithOverlay {
elide: Text.ElideRight elide: Text.ElideRight
Layout.fillWidth: true Layout.fillWidth: true
} }
Text { Text {
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : (modelData.comment || modelData.genericName || "No description available") 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) color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textSecondary : Theme.textSecondary)
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption font.pixelSize: Theme.fontSizeCaption
font.italic: !(modelData.comment || modelData.genericName) font.italic: !(modelData.comment || modelData.genericName)
opacity: (modelData.comment || modelData.genericName) ? 1.0 : 0.6 opacity: modelData.isClipboard ? 0.8 : modelData.isCommand ? 0.9 : ((modelData.comment || modelData.genericName) ? 1.0 : 0.6)
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: (modelData.isClipboard || modelData.isCommand) ? 2 : 1
wrapMode: (modelData.isClipboard || modelData.isCommand) ? Text.WordWrap : Text.NoWrap
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: (modelData.isClipboard || modelData.isCommand) ? implicitHeight : contentHeight
} }
} }
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }
Text { Text {
text: modelData.isCalculator ? "content_copy" : "chevron_right" text: modelData.isCalculator ? "content_copy" : "chevron_right"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
@ -398,9 +734,10 @@ PanelWithOverlay {
? Theme.onAccent ? Theme.onAccent
: (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textSecondary) : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textSecondary)
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
Layout.rightMargin: 8 // Add margin to separate from star Layout.rightMargin: 8
} }
// Add a spacing item between chevron and star
Item { width: 8; height: 1 } Item { width: 8; height: 1 }
} }
@ -417,7 +754,7 @@ PanelWithOverlay {
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: { onClicked: {
// Prevent app launch if click is inside pinArea
if (pinArea.containsMouse) return; if (pinArea.containsMouse) return;
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
appLauncherPanel.togglePin(modelData); appLauncherPanel.togglePin(modelData);
@ -449,13 +786,15 @@ PanelWithOverlay {
color: Theme.outline color: Theme.outline
opacity: index === appList.count - 1 ? 0 : 0.10 opacity: index === appList.count - 1 ? 0 : 0.10
} }
// Pin/Unpin button (move to last child for stacking)
Item { Item {
id: pinArea id: pinArea
width: 28; height: 28 width: 28; height: 28
z: 100 // Ensure above everything else z: 100
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
preventStealing: true preventStealing: true
@ -469,6 +808,7 @@ PanelWithOverlay {
event.accepted = true; event.accepted = true;
} }
} }
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "star" text: "star"
@ -507,3 +847,4 @@ PanelWithOverlay {
} }
} }
} }
}

View file

@ -4,6 +4,7 @@ import Quickshell.Services.UPower
import QtQuick.Layouts import QtQuick.Layouts
import qs.Components import qs.Components
import qs.Settings import qs.Settings
import "../../Helpers/Time.js" as Time
Item { Item {
id: batteryWidget id: batteryWidget
@ -73,18 +74,39 @@ Item {
} }
StyledTooltip { StyledTooltip {
id: batteryTooltip id: batteryTooltip
positionAbove: false
text: { text: {
let lines = []; let lines = [];
if (batteryWidget.isReady) { if (!batteryWidget.isReady) {
return "";
}
if (batteryWidget.battery.timeToEmpty > 0) {
lines.push("Time left: " + Time.formatVagueHumanReadableTime(batteryWidget.battery.timeToEmpty));
}
if (batteryWidget.battery.timeToFull > 0) {
lines.push("Time until full: " + Time.formatVagueHumanReadableTime(batteryWidget.battery.timeToFull));
}
if (batteryWidget.battery.changeRate !== undefined) {
const rate = batteryWidget.battery.changeRate;
if (rate > 0) {
lines.push(batteryWidget.charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(2) + " W");
}
else if (rate < 0) {
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W");
}
else {
lines.push("Estimating...");
}
}
else {
lines.push(batteryWidget.charging ? "Charging" : "Discharging"); lines.push(batteryWidget.charging ? "Charging" : "Discharging");
lines.push(Math.round(batteryWidget.percent) + "%"); }
if (batteryWidget.battery.changeRate !== undefined)
lines.push("Rate: " + batteryWidget.battery.changeRate.toFixed(2) + " W");
if (batteryWidget.battery.timeToEmpty > 0) if (batteryWidget.battery.healthPercentage !== undefined && batteryWidget.battery.healthPercentage > 0) {
lines.push("Time left: " + Math.floor(batteryWidget.battery.timeToEmpty / 60) + " min");
if (batteryWidget.battery.timeToFull > 0)
lines.push("Time to full: " + Math.floor(batteryWidget.battery.timeToFull / 60) + " min");
if (batteryWidget.battery.healthPercentage !== undefined)
lines.push("Health: " + Math.round(batteryWidget.battery.healthPercentage) + "%"); lines.push("Health: " + Math.round(batteryWidget.battery.healthPercentage) + "%");
} }
return lines.join("\n"); return lines.join("\n");

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

@ -0,0 +1,272 @@
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
color: mouseAreaBluetooth.containsMouse ? Theme.accentPrimary : Theme.textPrimary
}
MouseArea {
id: mouseAreaBluetooth
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
bluetoothMenu.visible = !bluetoothMenu.visible;
// Enable adapter and start discovery when menu opens
if (bluetoothMenu.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
}
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
color: Theme.accentPrimary
}
Text {
text: "Bluetooth Devices"
font.pixelSize: 18
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
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
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
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
color: Theme.textSecondary
}
Spinner {
running: true
color: Theme.accentPrimary
size: 16
}
}
}
}
}
}

View file

@ -128,6 +128,7 @@ Item {
StyledTooltip { StyledTooltip {
id: brightnessTooltip id: brightnessTooltip
text: "Brightness: " + brightness + "%" text: "Brightness: " + brightness + "%"
positionAbove: false
tooltipVisible: false tooltipVisible: false
targetItem: pill targetItem: pill
delay: 1500 delay: 1500

View file

@ -41,6 +41,7 @@ Rectangle {
StyledTooltip { StyledTooltip {
id: dateTooltip id: dateTooltip
text: Time.dateString text: Time.dateString
positionAbove: false
tooltipVisible: showTooltip && !calendar.visible tooltipVisible: showTooltip && !calendar.visible
targetItem: clockWidget targetItem: clockWidget
delay: 200 delay: 200

View file

@ -1,7 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick 2.15
import QtQuick.Controls import QtQuick.Controls 2.15
import QtQuick.Layouts import QtQuick.Layouts 1.15
import Quickshell import Quickshell
import qs.Settings import qs.Settings
@ -21,116 +21,452 @@ PopupWindow {
anchor.rect.x: anchorX anchor.rect.x: anchorX
anchor.rect.y: anchorY - 4 anchor.rect.y: anchorY - 4
// Recursive function to destroy all open submenus in delegate tree, safely avoiding infinite recursion
function destroySubmenusRecursively(item) {
if (!item || !item.contentItem) return;
var children = item.contentItem.children;
for (var i = 0; i < children.length; ++i) {
var child = children[i];
if (child.subMenu) {
child.subMenu.hideMenu();
child.subMenu.destroy();
child.subMenu = null;
}
// Recursively destroy submenus only if the child has contentItem to prevent issues
if (child.contentItem) {
destroySubmenusRecursively(child);
}
}
}
function showAt(item, x, y) { function showAt(item, x, y) {
if (!item) { if (!item) {
console.warn("CustomTrayMenu: anchorItem is undefined, not showing menu."); console.warn("CustomTrayMenu: anchorItem is undefined, won't show menu.");
return; return;
} }
anchorItem = item anchorItem = item;
anchorX = x anchorX = x;
anchorY = y anchorY = y;
visible = true visible = true;
forceActiveFocus() forceActiveFocus();
Qt.callLater(() => trayMenu.anchor.updateAnchor()) Qt.callLater(() => trayMenu.anchor.updateAnchor());
} }
function hideMenu() { function hideMenu() {
visible = false visible = false;
destroySubmenusRecursively(listView);
} }
Item { Item {
anchors.fill: parent anchors.fill: parent;
Keys.onEscapePressed: trayMenu.hideMenu() Keys.onEscapePressed: trayMenu.hideMenu();
} }
QsMenuOpener { QsMenuOpener {
id: opener id: opener;
menu: trayMenu.menu menu: trayMenu.menu;
} }
Rectangle { Rectangle {
id: bg id: bg;
anchors.fill: parent anchors.fill: parent;
color: Theme.surfaceVariant || "#222" color: Theme.backgroundPrimary || "#222";
border.color: Theme.outline || "#444" border.color: Theme.outline || "#444";
border.width: 1 border.width: 1;
radius: 12 radius: 12;
z: 0 z: 0;
} }
ListView { ListView {
id: listView id: listView;
anchors.fill: parent anchors.fill: parent;
anchors.margins: 6 anchors.margins: 6;
spacing: 2 spacing: 2;
interactive: false interactive: false;
enabled: trayMenu.visible enabled: trayMenu.visible;
clip: true clip: true;
model: ScriptModel { model: ScriptModel {
values: opener.children ? [...opener.children.values] : [] values: opener.children ? [...opener.children.values] : []
} }
delegate: Rectangle { delegate: Rectangle {
id: entry id: entry;
required property var modelData required property var modelData;
width: listView.width width: listView.width;
height: (modelData?.isSeparator) ? 8 : 32 height: (modelData?.isSeparator) ? 8 : 32;
color: "transparent" color: "transparent";
radius: 12 radius: 12;
property var subMenu: null;
Rectangle { Rectangle {
anchors.centerIn: parent anchors.centerIn: parent;
width: parent.width - 20 width: parent.width - 20;
height: 1 height: 1;
color: Qt.darker(Theme.surfaceVariant || "#222", 1.4) color: Qt.darker(Theme.backgroundPrimary || "#222", 1.4);
visible: modelData?.isSeparator ?? false visible: modelData?.isSeparator ?? false;
} }
Rectangle { Rectangle {
id: bg id: bg;
anchors.fill: parent anchors.fill: parent;
color: mouseArea.containsMouse ? Theme.highlight : "transparent" color: mouseArea.containsMouse ? Theme.highlight : "transparent";
radius: 8 radius: 8;
visible: !(modelData?.isSeparator ?? false) visible: !(modelData?.isSeparator ?? false);
property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary;
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent;
anchors.leftMargin: 12 anchors.leftMargin: 12;
anchors.rightMargin: 12 anchors.rightMargin: 12;
spacing: 8 spacing: 8;
Text { Text {
Layout.fillWidth: true Layout.fillWidth: true;
color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled;
text: modelData?.text ?? "" text: modelData?.text ?? "";
font.family: Theme.fontFamily font.family: Theme.fontFamily;
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall;
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter;
elide: Text.ElideRight elide: Text.ElideRight;
} }
Image { Image {
Layout.preferredWidth: 16 Layout.preferredWidth: 16;
Layout.preferredHeight: 16 Layout.preferredHeight: 16;
source: modelData?.icon ?? "" source: modelData?.icon ?? "";
visible: (modelData?.icon ?? "") !== "" visible: (modelData?.icon ?? "") !== "";
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit;
}
Text {
// Material Symbols Outlined chevron right for submenu
text: modelData?.hasChildren ? "menu" : "";
font.family: "Material Symbols Outlined";
font.pixelSize: 18;
verticalAlignment: Text.AlignVCenter;
visible: modelData?.hasChildren ?? false;
color: Theme.textPrimary;
} }
} }
MouseArea { MouseArea {
id: mouseArea id: mouseArea;
anchors.fill: parent anchors.fill: parent;
hoverEnabled: true hoverEnabled: true;
enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible;
onClicked: { onClicked: {
if (modelData && !modelData.isSeparator) { if (modelData && !modelData.isSeparator) {
modelData.triggered() if (modelData.hasChildren) {
trayMenu.hideMenu() // Submenus open on hover; ignore click here
return;
}
modelData.triggered();
trayMenu.hideMenu();
}
}
onEntered: {
if (!trayMenu.visible) return;
if (modelData?.hasChildren) {
// Close sibling submenus immediately
for (let i = 0; i < listView.contentItem.children.length; i++) {
const sibling = listView.contentItem.children[i];
if (sibling !== entry && sibling.subMenu) {
sibling.subMenu.hideMenu();
sibling.subMenu.destroy();
sibling.subMenu = null;
}
}
if (entry.subMenu) {
entry.subMenu.hideMenu();
entry.subMenu.destroy();
entry.subMenu = null;
}
var globalPos = entry.mapToGlobal(0, 0);
var submenuWidth = 180;
var gap = 12;
var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width);
var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap;
entry.subMenu = subMenuComponent.createObject(trayMenu, {
menu: modelData,
anchorItem: entry,
anchorX: anchorX,
anchorY: 0
});
entry.subMenu.showAt(entry, anchorX, 0);
} else {
// Hovered item without submenu; close siblings
for (let i = 0; i < listView.contentItem.children.length; i++) {
const sibling = listView.contentItem.children[i];
if (sibling.subMenu) {
sibling.subMenu.hideMenu();
sibling.subMenu.destroy();
sibling.subMenu = null;
}
}
if (entry.subMenu) {
entry.subMenu.hideMenu();
entry.subMenu.destroy();
entry.subMenu = null;
}
}
}
onExited: {
if (entry.subMenu && !entry.subMenu.containsMouse()) {
entry.subMenu.hideMenu();
entry.subMenu.destroy();
entry.subMenu = null;
}
}
}
}
// Simplified containsMouse without recursive calls to avoid stack overflow
function containsMouse() {
return mouseArea.containsMouse;
}
Component.onDestruction: {
if (subMenu) {
subMenu.destroy();
subMenu = null;
}
}
}
}
Component {
id: subMenuComponent;
PopupWindow {
id: subMenu;
implicitWidth: 180;
implicitHeight: Math.max(40, listView.contentHeight + 12);
visible: false;
color: "transparent";
property QsMenuHandle menu;
property var anchorItem: null;
property real anchorX;
property real anchorY;
anchor.item: anchorItem ? anchorItem : null;
anchor.rect.x: anchorX;
anchor.rect.y: anchorY;
function showAt(item, x, y) {
if (!item) {
console.warn("subMenuComponent: anchorItem is undefined, not showing menu.");
return;
}
anchorItem = item;
anchorX = x;
anchorY = y;
visible = true;
Qt.callLater(() => subMenu.anchor.updateAnchor());
}
function hideMenu() {
visible = false;
// Close all submenus recursively in this submenu
for (let i = 0; i < listView.contentItem.children.length; i++) {
const child = listView.contentItem.children[i];
if (child.subMenu) {
child.subMenu.hideMenu();
child.subMenu.destroy();
child.subMenu = null;
}
}
}
// Simplified containsMouse avoiding recursive calls
function containsMouse() {
return subMenu.containsMouse;
}
Item {
anchors.fill: parent;
Keys.onEscapePressed: subMenu.hideMenu();
}
QsMenuOpener {
id: opener;
menu: subMenu.menu;
}
Rectangle {
id: bg;
anchors.fill: parent;
color: Theme.backgroundPrimary || "#222";
border.color: Theme.outline || "#444";
border.width: 1;
radius: 12;
z: 0;
}
ListView {
id: listView;
anchors.fill: parent;
anchors.margins: 6;
spacing: 2;
interactive: false;
enabled: subMenu.visible;
clip: true;
model: ScriptModel {
values: opener.children ? [...opener.children.values] : [];
}
delegate: Rectangle {
id: entry;
required property var modelData;
width: listView.width;
height: (modelData?.isSeparator) ? 8 : 32;
color: "transparent";
radius: 12;
property var subMenu: null;
Rectangle {
anchors.centerIn: parent;
width: parent.width - 20;
height: 1;
color: Qt.darker(Theme.surfaceVariant || "#222", 1.4);
visible: modelData?.isSeparator ?? false;
}
Rectangle {
id: bg;
anchors.fill: parent;
color: mouseArea.containsMouse ? Theme.highlight : "transparent";
radius: 8;
visible: !(modelData?.isSeparator ?? false);
property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary;
RowLayout {
anchors.fill: parent;
anchors.leftMargin: 12;
anchors.rightMargin: 12;
spacing: 8;
Text {
Layout.fillWidth: true;
color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled;
text: modelData?.text ?? "";
font.family: Theme.fontFamily;
font.pixelSize: Theme.fontSizeSmall;
verticalAlignment: Text.AlignVCenter;
elide: Text.ElideRight;
}
Image {
Layout.preferredWidth: 16;
Layout.preferredHeight: 16;
source: modelData?.icon ?? "";
visible: (modelData?.icon ?? "") !== "";
fillMode: Image.PreserveAspectFit;
}
Text {
text: modelData?.hasChildren ? "\uE5CC" : "";
font.family: "Material Symbols Outlined";
font.pixelSize: 18;
verticalAlignment: Text.AlignVCenter;
visible: modelData?.hasChildren ?? false;
color: Theme.textPrimary;
}
}
MouseArea {
id: mouseArea;
anchors.fill: parent;
hoverEnabled: true;
enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && subMenu.visible;
onClicked: {
if (modelData && !modelData.isSeparator) {
if (modelData.hasChildren) {
return;
}
modelData.triggered();
trayMenu.hideMenu();
}
}
onEntered: {
if (!subMenu.visible) return;
if (modelData?.hasChildren) {
for (let i = 0; i < listView.contentItem.children.length; i++) {
const sibling = listView.contentItem.children[i];
if (sibling !== entry && sibling.subMenu) {
sibling.subMenu.hideMenu();
sibling.subMenu.destroy();
sibling.subMenu = null;
}
}
if (entry.subMenu) {
entry.subMenu.hideMenu();
entry.subMenu.destroy();
entry.subMenu = null;
}
var globalPos = entry.mapToGlobal(0, 0);
var submenuWidth = 180;
var gap = 12;
var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width);
var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap;
entry.subMenu = subMenuComponent.createObject(subMenu, {
menu: modelData,
anchorItem: entry,
anchorX: anchorX,
anchorY: 0
});
entry.subMenu.showAt(entry, anchorX, 0);
} else {
for (let i = 0; i < listView.contentItem.children.length; i++) {
const sibling = listView.contentItem.children[i];
if (sibling.subMenu) {
sibling.subMenu.hideMenu();
sibling.subMenu.destroy();
sibling.subMenu = null;
}
}
if (entry.subMenu) {
entry.subMenu.hideMenu();
entry.subMenu.destroy();
entry.subMenu = null;
}
}
}
onExited: {
if (entry.subMenu && !entry.subMenu.containsMouse()) {
entry.subMenu.hideMenu();
entry.subMenu.destroy();
entry.subMenu = null;
}
}
}
}
// Simplified & safe containsMouse avoiding recursion
function containsMouse() {
return mouseArea.containsMouse;
}
Component.onDestruction: {
if (subMenu) {
subMenu.destroy();
subMenu = null;
} }
} }
} }

View file

@ -1,7 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Qt5Compat.GraphicalEffects import Quickshell.Widgets
import QtQuick.Effects
import qs.Settings import qs.Settings
import qs.Services import qs.Services
import qs.Components import qs.Components
@ -54,17 +55,16 @@ Item {
anchors.margins: 1 anchors.margins: 1
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
smooth: true smooth: true
mipmap: true
cache: false cache: false
asynchronous: true asynchronous: true
sourceSize.width: 24
sourceSize.height: 24
source: MusicManager.trackArtUrl source: MusicManager.trackArtUrl
visible: source.toString() !== "" visible: source.toString() !== ""
// Rounded corners using layer // Rounded corners using layer
layer.enabled: true layer.enabled: true
layer.effect: OpacityMask { layer.effect: MultiEffect {
cached: true maskEnabled: true
maskSource: Rectangle { maskSource: Rectangle {
width: albumArt.width width: albumArt.width
height: albumArt.height height: albumArt.height

View file

@ -0,0 +1,80 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Settings
import qs.Components
import qs.Widgets.SettingsWindow
Item {
id: root
width: 22
height: 22
property var settingsWindow: null
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
color: mouseArea.containsMouse ? Theme.accentPrimary : Theme.textPrimary
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
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();
}
}
}
StyledTooltip {
text: "Settings"
targetItem: mouseArea
tooltipVisible: mouseArea.containsMouse
}
}
Component {
id: settingsComponent
SettingsWindow {}
}
// Clean up on destruction
Component.onDestruction: {
if (settingsWindow) {
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.destroy();
}
}
}

View file

@ -8,6 +8,8 @@ Row {
spacing: 10 spacing: 10
visible: Settings.settings.showSystemInfoInBar visible: Settings.settings.showSystemInfoInBar
width: Math.floor(cpuUsageLayout.width + cpuTempLayout.width + memoryUsageLayout.width + (2 * 10))
Row { Row {
id: cpuUsageLayout id: cpuUsageLayout
spacing: 6 spacing: 6

View file

@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Qt5Compat.GraphicalEffects import QtQuick.Effects
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import Quickshell.Widgets import Quickshell.Widgets
import qs.Settings import qs.Settings
@ -63,7 +63,8 @@ Row {
backer.fillMode: Image.PreserveAspectFit backer.fillMode: Image.PreserveAspectFit
source: { source: {
let icon = modelData?.icon || ""; let icon = modelData?.icon || "";
if (!icon) return ""; if (!icon)
return "";
// Process icon path // Process icon path
if (icon.includes("?path=")) { if (icon.includes("?path=")) {
const [name, path] = icon.split("?path="); const [name, path] = icon.split("?path=");
@ -80,9 +81,7 @@ Row {
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
} }
} }
Component.onCompleted: { Component.onCompleted: {}
}
} }
} }
@ -92,32 +91,32 @@ Row {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: (mouse) => { onClicked: mouse => {
if (!modelData) return; if (!modelData)
return;
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
// Close any open menu first // Close any open menu first
if (trayMenu && trayMenu.visible) { if (trayMenu && trayMenu.visible) {
trayMenu.hideMenu() trayMenu.hideMenu();
} }
if (!modelData.onlyMenu) { if (!modelData.onlyMenu) {
modelData.activate() modelData.activate();
} }
} else if (mouse.button === Qt.MiddleButton) { } else if (mouse.button === Qt.MiddleButton) {
// Close any open menu first // Close any open menu first
if (trayMenu && trayMenu.visible) { if (trayMenu && trayMenu.visible) {
trayMenu.hideMenu() trayMenu.hideMenu();
} }
modelData.secondaryActivate && modelData.secondaryActivate() modelData.secondaryActivate && modelData.secondaryActivate();
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
trayTooltip.tooltipVisible = false trayTooltip.tooltipVisible = false;
console.log("Right click on", modelData.id, "hasMenu:", modelData.hasMenu, "menu:", modelData.menu)
// If menu is already visible, close it // If menu is already visible, close it
if (trayMenu && trayMenu.visible) { if (trayMenu && trayMenu.visible) {
trayMenu.hideMenu() trayMenu.hideMenu();
return return;
} }
if (modelData.hasMenu && modelData.menu && trayMenu) { if (modelData.hasMenu && modelData.menu && trayMenu) {
@ -126,9 +125,9 @@ Row {
const menuY = height + 20; const menuY = height + 20;
trayMenu.menu = modelData.menu; trayMenu.menu = modelData.menu;
trayMenu.showAt(parent, menuX, menuY); trayMenu.showAt(parent, menuX, menuY);
} else { } else
// console.log("No menu available for", modelData.id, "or trayMenu not set") // console.log("No menu available for", modelData.id, "or trayMenu not set")
} {}
} }
} }
onEntered: trayTooltip.tooltipVisible = true onEntered: trayTooltip.tooltipVisible = true
@ -138,14 +137,15 @@ Row {
StyledTooltip { StyledTooltip {
id: trayTooltip id: trayTooltip
text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item" text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item"
positionAbove: false
tooltipVisible: false tooltipVisible: false
targetItem: trayIcon targetItem: trayIcon
delay: 200 delay: 200
} }
Component.onDestruction: { Component.onDestruction:
// No cache cleanup needed // No cache cleanup needed
} {}
} }
} }
} }

View file

@ -15,6 +15,7 @@ Item {
// Attach custom tooltip // Attach custom tooltip
StyledTooltip { StyledTooltip {
id: styledTooltip id: styledTooltip
positionAbove: false
} }
function getAppIcon(toplevel: Toplevel): string { function getAppIcon(toplevel: Toplevel): string {
@ -74,7 +75,6 @@ Item {
height: Math.max(12, Settings.settings.taskbarIconSize * 0.625) height: Math.max(12, Settings.settings.taskbarIconSize * 0.625)
anchors.centerIn: parent anchors.centerIn: parent
source: getAppIcon(modelData) source: getAppIcon(modelData)
smooth: true
visible: source.toString() !== "" visible: source.toString() !== ""
} }
@ -95,7 +95,8 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onEntered: { onEntered: {
styledTooltip.text = appTitle || appId; var text = appTitle || appId;
styledTooltip.text = text.length > 60 ? text.substring(0, 60) + "..." : text;
styledTooltip.targetItem = appButton; styledTooltip.targetItem = appButton;
styledTooltip.tooltipVisible = true; styledTooltip.tooltipVisible = true;
} }

View file

@ -47,7 +47,8 @@ Item {
StyledTooltip { StyledTooltip {
id: volumeTooltip id: volumeTooltip
text: "Volume: " + volume + "%\nScroll up/down to change volume.\nLeft click to open the input/output selection." text: "Volume: " + volume + "%\nLeft click for advanced settings.\nScroll up/down to change volume."
positionAbove: false
tooltipVisible: !ioSelector.visible && volumeDisplay.containsMouse tooltipVisible: !ioSelector.visible && volumeDisplay.containsMouse
targetItem: pillIndicator targetItem: pillIndicator
delay: 1500 delay: 1500

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

@ -0,0 +1,370 @@
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
color: mouseAreaWifi.containsMouse ? Theme.accentPrimary : Theme.textPrimary
}
MouseArea {
id: mouseAreaWifi
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
wifiMenu.visible = !wifiMenu.visible;
if (wifiMenu.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
}
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
color: Theme.accentPrimary
}
Text {
text: "WiFi Networks"
font.pixelSize: 18
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
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
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
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
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
color: "#43a047"
anchors.centerIn: parent
}
Text {
visible: network.connectStatus === "error" && !network.connectingSsid
text: "error"
font.family: "Material Symbols Outlined"
font.pixelSize: 18
color: Theme.error
anchors.centerIn: parent
}
}
Text {
visible: modelData.connected
text: "connected"
color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
font.pixelSize: 11
}
}
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
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
font.bold: true
}
}
}
}
}
}
}
}
}
}
}

View file

@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Window import QtQuick.Window
import Qt5Compat.GraphicalEffects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Settings import qs.Settings
@ -124,13 +124,13 @@ Item {
border.color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.1) border.color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.1)
border.width: 1 border.width: 1
layer.enabled: true layer.enabled: true
layer.effect: DropShadow { layer.effect: MultiEffect {
color: "black" shadowColor: "black"
radius: 12 // radius: 12
samples: 24
verticalOffset: 0 shadowVerticalOffset: 0
horizontalOffset: 0 shadowHorizontalOffset: 0
opacity: 0.10 shadowOpacity: 0.10
} }
} }

51
Components/Avatar.qml Normal file
View file

@ -0,0 +1,51 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.Settings
import QtQuick.Effects
Item {
anchors.fill: parent
anchors.margins: 2
Image {
id: avatarImage
anchors.fill: parent
source: "file://" + Settings.settings.profileImage
visible: false
mipmap: true
smooth: true
asynchronous: true
fillMode: Image.PreserveAspectCrop
}
MultiEffect {
anchors.fill: parent
source: avatarImage
maskEnabled: true
maskSource: mask
visible: Settings.settings.profileImage !== ""
}
Item {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors.fill: parent
radius: avatarImage.width / 2
}
}
// Fallback icon
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: Theme.onAccent
visible: Settings.settings.profileImage === undefined || Settings.settings.profileImage === ""
z: 0
}
}

View file

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

View file

@ -8,18 +8,22 @@ Window {
property bool tooltipVisible: false property bool tooltipVisible: false
property Item targetItem: null property Item targetItem: null
property int delay: 300 property int delay: 300
property bool positionAbove: true
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
color: "transparent" color: "transparent"
visible: false visible: false
minimumWidth: tooltipText.implicitWidth + 24
minimumHeight: tooltipText.implicitHeight + 16
property var _timerObj: null property var _timerObj: null
onTooltipVisibleChanged: { onTooltipVisibleChanged: {
if (tooltipVisible) { if (tooltipVisible) {
if (delay > 0) { if (delay > 0) {
if (_timerObj) { _timerObj.destroy(); _timerObj = null; } if (_timerObj) { _timerObj.destroy(); _timerObj = null; }
_timerObj = Qt.createQmlObject('import QtQuick 2.0; Timer { interval: ' + delay + '; running: true; repeat: false; onTriggered: tooltipWindow._showNow() }', tooltipWindow); _timerObj = Qt.createQmlObject(
'import QtQuick 2.0; Timer { interval: ' + delay + '; running: true; repeat: false; onTriggered: tooltipWindow._showNow() }',
tooltipWindow);
} else { } else {
_showNow(); _showNow();
} }
@ -27,30 +31,45 @@ Window {
_hideNow(); _hideNow();
} }
} }
function _showNow() { function _showNow() {
width = Math.max(50, tooltipText.implicitWidth + 24)
height = Math.max(50, tooltipText.implicitHeight + 16)
if (!targetItem) return; if (!targetItem) return;
if (positionAbove) {
// Position tooltip above the target item
var pos = targetItem.mapToGlobal(0, 0);
x = pos.x - width / 2 + targetItem.width / 2;
y = pos.y - height - 12; // 12 px margin above
} else {
// Position tooltip below the target item
var pos = targetItem.mapToGlobal(0, targetItem.height); var pos = targetItem.mapToGlobal(0, targetItem.height);
x = pos.x - width / 2 + targetItem.width / 2; x = pos.x - width / 2 + targetItem.width / 2;
y = pos.y + 12; y = pos.y + 12; // 12 px margin below
}
visible = true; visible = true;
} }
function _hideNow() { function _hideNow() {
visible = false; visible = false;
if (_timerObj) { _timerObj.destroy(); _timerObj = null; } if (_timerObj) { _timerObj.destroy(); _timerObj = null; }
} }
Connections { Connections {
target: tooltipWindow.targetItem target: tooltipWindow.targetItem
function onXChanged() { function onXChanged() {
if (tooltipWindow.visible) tooltipWindow._showNow() if (tooltipWindow.visible) tooltipWindow._showNow();
} }
function onYChanged() { function onYChanged() {
if (tooltipWindow.visible) tooltipWindow._showNow() if (tooltipWindow.visible) tooltipWindow._showNow();
} }
function onWidthChanged() { function onWidthChanged() {
if (tooltipWindow.visible) tooltipWindow._showNow() if (tooltipWindow.visible) tooltipWindow._showNow();
} }
function onHeightChanged() { function onHeightChanged() {
if (tooltipWindow.visible) tooltipWindow._showNow() if (tooltipWindow.visible) tooltipWindow._showNow();
} }
} }
@ -63,6 +82,7 @@ Window {
opacity: 0.97 opacity: 0.97
z: 1 z: 1
} }
Text { Text {
id: tooltipText id: tooltipText
text: tooltipWindow.text text: tooltipWindow.text
@ -76,15 +96,16 @@ Window {
padding: 8 padding: 8
z: 2 z: 2
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onExited: tooltipWindow.tooltipVisible = false onExited: tooltipWindow.tooltipVisible = false
cursorShape: Qt.ArrowCursor cursorShape: Qt.ArrowCursor
} }
onTextChanged: { onTextChanged: {
width = Math.max(minimumWidth, tooltipText.implicitWidth + 24); width = Math.max(minimumWidth, tooltipText.implicitWidth + 24);
height = Math.max(minimumHeight, tooltipText.implicitHeight + 16); height = Math.max(minimumHeight, tooltipText.implicitHeight + 16);
} }
} }

View file

@ -6,7 +6,7 @@ IpcHandler {
property var appLauncherPanel property var appLauncherPanel
property var lockScreen property var lockScreen
property IdleInhibitor idleInhibitor property IdleInhibitor idleInhibitor
property var notificationPopup property var notificationPopupVariants
target: "globalIPC" target: "globalIPC"
@ -17,10 +17,18 @@ IpcHandler {
function toggleNotificationPopup(): void { function toggleNotificationPopup(): void {
console.log("[IPC] NotificationPopup toggle() called") console.log("[IPC] NotificationPopup toggle() called")
notificationPopup.togglePopup();
if (notificationPopupVariants) {
for (let i = 0; i < notificationPopupVariants.count; i++) {
let popup = notificationPopupVariants.objectAt(i);
if (popup) {
popup.togglePopup();
}
}
}
} }
// Toggle Applauncher visibility
function toggleLauncher(): void { function toggleLauncher(): void {
if (!appLauncherPanel) { if (!appLauncherPanel) {
console.warn("AppLauncherIpcHandler: appLauncherPanel not set!"); console.warn("AppLauncherIpcHandler: appLauncherPanel not set!");
@ -34,7 +42,7 @@ IpcHandler {
} }
} }
// Toggle LockScreen
function toggleLock(): void { function toggleLock(): void {
if (!lockScreen) { if (!lockScreen) {
console.warn("LockScreenIpcHandler: lockScreen not set!"); console.warn("LockScreenIpcHandler: lockScreen not set!");

View file

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

18
Helpers/Time.js Normal file
View file

@ -0,0 +1,18 @@
function formatVagueHumanReadableTime(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60);
const seconds = totalSeconds - (hours * 3600) - (minutes * 60);
var str = "";
if (hours) {
str += hours.toString() + "h";
}
if (minutes) {
str += minutes.toString() + "m";
}
if (!hours && !minutes) {
str += seconds.toString() + "s";
}
return str;
}

View file

@ -214,6 +214,14 @@ Contributions are welcome! Feel free to open issues or submit pull requests.
While I actually didn't want to accept donations, more and more people are asking to donate so... I don't know, if you really feel like donating then I obviously highly appreciate it but **PLEASE** never feel forced to donate or anything. It won't change how I work on Noctalia, it's a project that I work on for fun in the end. While I actually didn't want to accept donations, more and more people are asking to donate so... I don't know, if you really feel like donating then I obviously highly appreciate it but **PLEASE** never feel forced to donate or anything. It won't change how I work on Noctalia, it's a project that I work on for fun in the end.
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R01IX85B) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R01IX85B)
---
#### Special Thanks
Thank you to everyone who supports me and this project 💜!
* Gohma
--- ---
## License ## License

View file

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

348
Services/Network.qml Normal file
View file

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

View file

@ -15,7 +15,7 @@ Singleton {
toggleRandomWallpaper(); toggleRandomWallpaper();
} }
} }
property string wallpaperDirectory: Settings.settings.wallpaperFolder
property var wallpaperList: [] property var wallpaperList: []
property string currentWallpaper: Settings.settings.currentWallpaper property string currentWallpaper: Settings.settings.currentWallpaper
property bool scanning: false property bool scanning: false
@ -46,6 +46,11 @@ Singleton {
} }
changeWallpaperProcess.running = true; changeWallpaperProcess.running = true;
} }
if (randomWallpaperTimer.running) {
randomWallpaperTimer.restart();
}
generateTheme(); generateTheme();
} }
@ -91,15 +96,17 @@ Singleton {
FolderListModel { FolderListModel {
id: folderModel id: folderModel
// Swww supports many images format but Quickshell only support a subset of those.
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"]
showDirs: false showDirs: false
sortField: FolderListModel.Name sortField: FolderListModel.Name
onStatusChanged: { onStatusChanged: {
if (status === FolderListModel.Ready) { if (status === FolderListModel.Ready) {
var files = []; var files = [];
var filesSwww = [];
for (var i = 0; i < count; i++) { for (var i = 0; i < count; i++) {
var fileph = (Settings.settings.wallpaperFolder !== undefined ? Settings.settings.wallpaperFolder : "") + "/" + get(i, "fileName"); var filepath = (Settings.settings.wallpaperFolder !== undefined ? Settings.settings.wallpaperFolder : "") + "/" + get(i, "fileName");
files.push(fileph); files.push(filepath);
} }
wallpaperList = files; wallpaperList = files;
scanning = false; scanning = false;

View file

@ -45,6 +45,7 @@ Singleton {
property string wallpaperFolder: "/usr/share/wallpapers" property string wallpaperFolder: "/usr/share/wallpapers"
property string currentWallpaper: "" property string currentWallpaper: ""
property string videoPath: "~/Videos/" property string videoPath: "~/Videos/"
property bool showActiveWindow: true
property bool showActiveWindowIcon: false property bool showActiveWindowIcon: false
property bool showSystemInfoInBar: false property bool showSystemInfoInBar: false
property bool showCorners: true property bool showCorners: true
@ -65,6 +66,22 @@ Singleton {
property real fontSizeMultiplier: 1.0 // Font size multiplier (1.0 = normal, 1.2 = 20% larger, 0.8 = 20% smaller) property real fontSizeMultiplier: 1.0 // Font size multiplier (1.0 = normal, 1.2 = 20% larger, 0.8 = 20% smaller)
property int taskbarIconSize: 24 // Taskbar icon button size in pixels (default: 32, smaller: 24, larger: 40) property int taskbarIconSize: 24 // Taskbar icon button size in pixels (default: 32, smaller: 24, larger: 40)
property var pinnedExecs: [] // Added for AppLauncher pinned apps property var pinnedExecs: [] // Added for AppLauncher pinned apps
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
} }
} }

View file

@ -35,6 +35,7 @@ ShellRoot {
visible: wallpaperSource !== "" visible: wallpaperSource !== ""
cache: true cache: true
smooth: true smooth: true
mipmap: false
} }
} }
} }

350
Widgets/Dock.qml Normal file
View file

@ -0,0 +1,350 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Settings
import qs.Components
PanelWindow {
id: taskbarWindow
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
anchors.left: true
anchors.right: true
focusable: false
color: "transparent"
implicitHeight: 43
// Auto-hide properties
property bool autoHide: true
property bool hidden: true
property int hideDelay: 500
property int showDelay: 100
property int hideAnimationDuration: 200
property int showAnimationDuration: 150
property int peekHeight: 2
property int fullHeight: taskbarContainer.height
// Track hover state
property bool dockHovered: false
property bool anyAppHovered: false
// Context menu properties
property bool contextMenuVisible: false
property var contextMenuTarget: null
property var contextMenuToplevel: null
// Timer for auto-hide delay
Timer {
id: hideTimer
interval: hideDelay
onTriggered: if (autoHide && !dockHovered && !anyAppHovered && !contextMenuVisible) hidden = true
}
// Timer for show delay
Timer {
id: showTimer
interval: showDelay
onTriggered: hidden = false
}
// Behavior for smooth hide/show animations
Behavior on margins.bottom {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: Easing.InOutQuad
}
}
// Mouse area at screen bottom to detect entry and keep dock visible
MouseArea {
id: screenEdgeMouseArea
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 10
hoverEnabled: true
propagateComposedEvents: true
onEntered: if (autoHide && hidden) showTimer.start()
onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered && !contextMenuVisible) hideTimer.start()
}
margins.bottom: hidden ? -(fullHeight - peekHeight) : 0
Rectangle {
id: taskbarContainer
width: taskbar.width + 40
height: Settings.settings.taskbarIconSize + 20
topLeftRadius: 16
topRightRadius: 16
color: Theme.backgroundSecondary
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
MouseArea {
id: dockMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onEntered: {
dockHovered = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
hidden = false
}
}
onExited: {
dockHovered = false
if (autoHide && !anyAppHovered && !contextMenuVisible) hideTimer.start()
}
}
Item {
id: taskbar
width: runningAppsRow.width
height: parent.height - 10
anchors.centerIn: parent
StyledTooltip { id: styledTooltip }
function getAppIcon(toplevel: Toplevel): string {
if (!toplevel) return "";
let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true);
if (!icon) icon = Quickshell.iconPath(toplevel.appId, true);
if (!icon) icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true);
if (!icon) icon = Quickshell.iconPath(toplevel.title, true);
return icon || Quickshell.iconPath("application-x-executable", true);
}
Row {
id: runningAppsRow
spacing: 12
height: parent.height
anchors.centerIn: parent
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
delegate: Rectangle {
id: appButton
width: Settings.settings.taskbarIconSize + 8
height: Settings.settings.taskbarIconSize + 8
radius: Math.max(6, Settings.settings.taskbarIconSize * 0.3)
color: isActive ? Theme.accentPrimary : (hovered ? Theme.surfaceVariant : "transparent")
border.color: isActive ? Qt.darker(Theme.accentPrimary, 1.2) : "transparent"
border.width: 1
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
property bool hovered: appMouseArea.containsMouse
property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : ""
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
IconImage {
id: appIcon
width: Math.max(20, Settings.settings.taskbarIconSize * 0.75)
height: Math.max(20, Settings.settings.taskbarIconSize * 0.75)
anchors.centerIn: parent
source: taskbar.getAppIcon(modelData)
visible: source.toString() !== ""
}
Text {
anchors.centerIn: parent
visible: !appIcon.visible
text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?"
font.family: Theme.fontFamily
font.pixelSize: Math.max(14, Settings.settings.taskbarIconSize * 0.5)
font.bold: true
color: appButton.isActive ? Theme.onAccent : Theme.textPrimary
}
MouseArea {
id: appMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: {
anyAppHovered = true
if (!contextMenuVisible) {
styledTooltip.text = appTitle || appId;
styledTooltip.targetItem = appButton;
styledTooltip.positionAbove = true;
styledTooltip.tooltipVisible = true;
}
if (autoHide) {
showTimer.stop()
hideTimer.stop()
hidden = false
}
}
onExited: {
anyAppHovered = false
if (!contextMenuVisible) {
styledTooltip.tooltipVisible = false;
}
if (autoHide && !dockHovered && !contextMenuVisible) hideTimer.start()
}
onClicked: function(mouse) {
if (mouse.button === Qt.MiddleButton && modelData?.close) {
modelData.close();
}
if (mouse.button === Qt.LeftButton && modelData?.activate) {
modelData.activate();
}
if (mouse.button === Qt.RightButton) {
styledTooltip.tooltipVisible = false;
contextMenuTarget = appButton;
contextMenuToplevel = modelData;
contextMenuVisible = true;
}
}
}
Rectangle {
visible: isActive
width: 6
height: 6
radius: 3
color: Theme.onAccent
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: -8
}
}
}
}
}
}
// Context Menu
PanelWindow {
id: contextMenuWindow
visible: contextMenuVisible
screen: taskbarWindow.screen
exclusionMode: ExclusionMode.Ignore
anchors.bottom: true
anchors.left: true
anchors.right: true
color: "transparent"
focusable: false
MouseArea {
anchors.fill: parent
onClicked: {
contextMenuVisible = false;
contextMenuTarget = null;
contextMenuToplevel = null;
hidden = true; // Hide dock when context menu closes
}
}
Rectangle {
id: contextMenuContainer
width: 80
height: contextMenuColumn.height + 0
radius: 16
color: Theme.backgroundPrimary
border.color: Theme.outline
border.width: 1
x: {
if (!contextMenuTarget) return 0;
// Get position relative to screen
const pos = contextMenuTarget.mapToItem(null, 0, 0);
// Center horizontally above the icon
let xPos = pos.x + (contextMenuTarget.width - width) / 2;
// Constrain to screen edges
return Math.max(0, Math.min(xPos, taskbarWindow.width - width));
}
y: {
if (!contextMenuTarget) return 0;
// Position above the dock
const pos = contextMenuTarget.mapToItem(null, 0, 0);
return pos.y - height + 32;
}
Column {
id: contextMenuColumn
anchors.centerIn: parent
spacing: 4
width: parent.width
Rectangle {
width: parent.width
height: 32
radius: 16
color: closeMouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
border.color: Theme.outline
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
spacing: 4
Text {
anchors.verticalCenter: parent.verticalCenter
text: "close"
font.family: "Material Symbols Outlined"
font.pixelSize: 14
color: Theme.textPrimary
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Close"
font.family: Theme.fontFamily
font.pixelSize: 14
color: Theme.textPrimary
}
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenuToplevel?.close) contextMenuToplevel.close();
contextMenuVisible = false;
hidden = true;
}
}
}
}
// Animation
scale: contextMenuVisible ? 1 : 0.9
opacity: contextMenuVisible ? 1 : 0
transformOrigin: Item.Bottom
Behavior on scale {
NumberAnimation {
duration: 150
easing.type: Easing.OutBack
}
}
Behavior on opacity {
NumberAnimation { duration: 100 }
}
}
}
}

View file

@ -6,7 +6,7 @@ import qs.Components
import qs.Settings import qs.Settings
Item { Item {
// Test mode
property bool testMode: false property bool testMode: false
property int testPercent: 49 property int testPercent: 49
property bool testCharging: true property bool testCharging: true
@ -21,7 +21,7 @@ Item {
height: row.height height: row.height
visible: testMode || (isReady && battery.isLaptopBattery) visible: testMode || (isReady && battery.isLaptopBattery)
// Choose icon based on charge and charging state
function batteryIcon() { function batteryIcon() {
if (!show) if (!show)
return ""; return "";
@ -32,7 +32,7 @@ Item {
if (percent >= 95) if (percent >= 95)
return "battery_android_full"; return "battery_android_full";
// Hardcoded battery symbols
if (percent >= 85) if (percent >= 85)
return "battery_android_6"; return "battery_android_6";
if (percent >= 70) if (percent >= 70)

View file

@ -2,11 +2,11 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import Qt5Compat.GraphicalEffects
import Quickshell.Wayland
import Quickshell import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Pam import Quickshell.Services.Pam
import Quickshell.Io import Quickshell.Io
import Quickshell.Widgets
import qs.Components import qs.Components
import qs.Settings import qs.Settings
import qs.Services import qs.Services
@ -32,7 +32,7 @@ WlSessionLock {
Component.onCompleted: { Component.onCompleted: {
Qt.callLater(function () { Qt.callLater(function () {
fetchWeatherData(); fetchWeatherData();
}) });
} }
function fetchWeatherData() { function fetchWeatherData() {
@ -135,8 +135,8 @@ WlSessionLock {
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
source: WallpaperManager.currentWallpaper !== "" ? WallpaperManager.currentWallpaper : "" source: WallpaperManager.currentWallpaper !== "" ? WallpaperManager.currentWallpaper : ""
cache: true cache: true
smooth: false smooth: true
visible: true // source for MultiEffect mipmap: false
} }
MultiEffect { MultiEffect {
@ -146,6 +146,7 @@ WlSessionLock {
blurEnabled: true blurEnabled: true
blur: 0.48 // controls blur strength (0 to 1) blur: 0.48 // controls blur strength (0 to 1)
blurMax: 128 // max blur radius in pixels blurMax: 128 // max blur radius in pixels
// transparentBorder: true
} }
ColumnLayout { ColumnLayout {
@ -160,39 +161,21 @@ WlSessionLock {
radius: 40 radius: 40
color: Theme.accentPrimary color: Theme.accentPrimary
Image { Rectangle {
id: avatarImage
anchors.fill: parent anchors.fill: parent
anchors.margins: 4 color: "transparent"
source: Settings.settings.profileImage radius: 40
fillMode: Image.PreserveAspectCrop border.color: Theme.accentPrimary
visible: false border.width: 3
asynchronous: true z: 2
}
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: avatarImage.width / 2
visible: false
}
visible: Settings.settings.profileImage !== ""
}
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Theme.onAccent
visible: Settings.settings.profileImage === ""
} }
Avatar {}
layer.enabled: true layer.enabled: true
layer.effect: Glow { layer.effect: MultiEffect {
color: Theme.accentPrimary shadowEnabled: true
radius: 8 shadowColor: Theme.accentPrimary
samples: 16
} }
} }
@ -257,7 +240,7 @@ WlSessionLock {
width: parent.width * 0.8 width: parent.width * 0.8
height: 44 height: 44
color: Theme.overlay color: Theme.overlay
radius: 22 radius: 20
visible: lock.errorMessage !== "" visible: lock.errorMessage !== ""
Text { Text {
@ -275,7 +258,7 @@ WlSessionLock {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
width: 120 width: 120
height: 44 height: 44
radius: 22 radius: 20
opacity: unlockButtonArea.containsMouse ? 0.8 : 0.5 opacity: unlockButtonArea.containsMouse ? 0.8 : 0.5
color: unlockButtonArea.containsMouse ? Theme.accentPrimary : Theme.surface color: unlockButtonArea.containsMouse ? Theme.accentPrimary : Theme.surface
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
@ -336,7 +319,7 @@ WlSessionLock {
} }
Rectangle { Rectangle {
width: infoColumn.width + 16 width: infoColumn.width + 32
height: infoColumn.height + 8 height: infoColumn.height + 8
color: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" color: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@ -404,7 +387,6 @@ WlSessionLock {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
} }
} }
@ -433,10 +415,8 @@ WlSessionLock {
anchors.margins: 32 anchors.margins: 32
spacing: 12 spacing: 12
BatteryCharge { BatteryCharge {}
} }
}
ColumnLayout { ColumnLayout {
anchors.right: parent.right anchors.right: parent.right

View file

@ -5,7 +5,7 @@ import qs.Settings
import QtQuick.Layouts import QtQuick.Layouts
import qs.Components import qs.Components
// The popup window
PanelWithOverlay { PanelWithOverlay {
id: notificationHistoryWin id: notificationHistoryWin
property string historyFilePath: Settings.settingsDir + "notification_history.json" property string historyFilePath: Settings.settingsDir + "notification_history.json"

View file

@ -9,13 +9,13 @@ Item {
width: 22; height: 22 width: 22; height: 22
property bool isSilence: false property bool isSilence: false
// Process for executing CLI commands
Process { Process {
id: rightClickProcess id: rightClickProcess
command: ["qs","ipc", "call", "globalIPC", "toggleNotificationPopup"] command: ["qs","ipc", "call", "globalIPC", "toggleNotificationPopup"]
} }
// Bell icon/button
Item { Item {
id: bell id: bell
width: 22; height: 22 width: 22; height: 22
@ -34,7 +34,7 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse): void { onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
root.isSilence = !root.isSilence; root.isSilence = !root.isSilence;
rightClickProcess.running = true; rightClickProcess.running = true;
@ -55,6 +55,7 @@ Item {
StyledTooltip { StyledTooltip {
id: notificationTooltip id: notificationTooltip
text: "Notification History" text: "Notification History"
positionAbove: false
tooltipVisible: false tooltipVisible: false
targetItem: bell targetItem: bell
delay: 200 delay: 200

View file

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

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Widgets
import qs.Settings import qs.Settings
PanelWindow { PanelWindow {
@ -9,7 +10,7 @@ PanelWindow {
implicitHeight: notificationColumn.implicitHeight implicitHeight: notificationColumn.implicitHeight
color: "transparent" color: "transparent"
visible: notificationsVisible && notificationModel.count > 0 visible: notificationsVisible && notificationModel.count > 0
screen: Quickshell.primaryScreen !== undefined ? Quickshell.primaryScreen : null screen: (typeof modelData !== 'undefined' ? modelData : Quickshell.primaryScreen)
focusable: false focusable: false
property bool barVisible: true property bool barVisible: true
@ -114,38 +115,37 @@ PanelWindow {
id: iconBackground id: iconBackground
width: 36 width: 36
height: 36 height: 36
radius: width / 2 // Circular radius: width / 2
color: Theme.accentPrimary color: Theme.accentPrimary
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
border.color: Qt.darker(Theme.accentPrimary, 1.2) border.color: Qt.darker(Theme.accentPrimary, 1.2)
border.width: 1.5 border.width: 1.5
// Get all possible icon sources from notification // Priority order for notification icons: image > appIcon > icon
property var iconSources: [rawNotification?.image || "", rawNotification?.appIcon || "", rawNotification?.icon || ""] property var iconSources: [rawNotification?.image || "", rawNotification?.appIcon || "", rawNotification?.icon || ""]
// Try to load notification icon // Load notification icon with fallback handling
Image { IconImage {
id: iconImage id: iconImage
anchors.fill: parent anchors.fill: parent
anchors.margins: 4 anchors.margins: 4
fillMode: Image.PreserveAspectFit
smooth: true
cache: false
asynchronous: true asynchronous: true
sourceSize.width: 36 backer.fillMode: Image.PreserveAspectFit
sourceSize.height: 36
source: { source: {
// Try each icon source in priority order
for (var i = 0; i < iconBackground.iconSources.length; i++) { for (var i = 0; i < iconBackground.iconSources.length; i++) {
var icon = iconBackground.iconSources[i]; var icon = iconBackground.iconSources[i];
if (!icon) if (!icon)
continue; continue;
// Handle special path format from some notifications
if (icon.includes("?path=")) { if (icon.includes("?path=")) {
const [name, path] = icon.split("?path="); const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1); const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`; return `file://${path}/${fileName}`;
} }
// Handle absolute file paths
if (icon.startsWith('/')) { if (icon.startsWith('/')) {
return "file://" + icon; return "file://" + icon;
} }
@ -157,7 +157,7 @@ PanelWindow {
visible: status === Image.Ready && source.toString() !== "" visible: status === Image.Ready && source.toString() !== ""
} }
// Fallback to first letter of app name // Fallback: show first letter of app name when no icon available
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
visible: !iconImage.visible visible: !iconImage.visible

View file

@ -2,7 +2,6 @@ import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Qt5Compat.GraphicalEffects
import qs.Services import qs.Services
import qs.Settings import qs.Settings
@ -34,15 +33,16 @@ ShellRoot {
source: wallpaperSource source: wallpaperSource
cache: true cache: true
smooth: true smooth: true
visible: wallpaperSource !== "" // Show the original for FastBlur input mipmap: false
visible: wallpaperSource !== ""
} }
MultiEffect { MultiEffect {
id: overviewBgBlur id: overviewBgBlur
anchors.fill: parent anchors.fill: parent
source: bgImage source: bgImage
blurEnabled: true blurEnabled: true
blur: 0.48 // controls blur strength (0 to 1) blur: 0.48
blurMax: 128 // max blur radius in pixels blurMax: 128
} }
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent

View file

@ -0,0 +1,369 @@
import Quickshell
import Quickshell.Wayland
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import qs.Settings
import qs.Widgets.SettingsWindow.Tabs
PanelWindow {
id: panelMain
implicitHeight: screen.height / 2
implicitWidth: screen.width / 2
color: "transparent"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Component {
id: generalSettings
General {}
}
Component {
id: barSettings
Bar {}
}
Component {
id: timeWeatherSettings
TimeWeather {}
}
Component {
id: recordingSettings
Recording {}
}
Component {
id: networkSettings
Network {}
}
Component {
id: miscSettings
Misc {}
}
Component {
id: aboutSettings
About {}
}
Component {
id: displaySettings
Display {}
}
Rectangle {
id: background
color: Theme.backgroundPrimary
anchors.fill: parent
radius: 20
border.color: Theme.outline
border.width: 1
MultiEffect {
source: background
anchors.fill: background
shadowEnabled: true
shadowColor: Theme.shadow
shadowOpacity: 0.3
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 12
}
}
Rectangle {
id: settings
color: Theme.backgroundTertiary
anchors {
left: tabs.right
top: parent.top
bottom: parent.bottom
right: parent.right
margins: 12
}
topRightRadius: 20
bottomRightRadius: 20
Rectangle {
id: headerArea
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 16
}
height: 48
color: "transparent"
RowLayout {
anchors.fill: parent
spacing: 12
Text {
id: tabName
text: "General"
font.pixelSize: 18
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
Rectangle {
width: 32
height: 32
radius: 16
color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1
Text {
anchors.centerIn: parent
text: "close"
font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 18
color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
MouseArea {
id: closeButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: panelMain.visible = false
}
}
}
}
Rectangle {
anchors {
top: headerArea.bottom
left: parent.left
right: parent.right
margins: 16
}
height: 1
color: Theme.outline
opacity: 0.3
}
Item {
id: settingsContainer
anchors {
top: headerArea.bottom
left: parent.left
right: parent.right
bottom: parent.bottom
margins: 24
topMargin: 32
}
Loader {
id: settingsLoader
anchors.fill: parent
sourceComponent: generalSettings
opacity: 1
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: 150
easing.type: Easing.InOutQuad
}
}
}
Loader {
id: settingsLoader2
anchors.fill: parent
opacity: 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: 150
easing.type: Easing.InOutQuad
}
}
}
}
}
Rectangle {
id: tabs
color: Theme.surface
width: screen.width / 9
height: panelMain.height
topLeftRadius: 20
bottomLeftRadius: 20
border.color: Theme.outline
border.width: 1
Column {
width: parent.width
spacing: 0
topPadding: 8
Repeater {
id: repeater
model: [
{ icon: "tune", text: "General" },
{ icon: "space_dashboard", text: "Bar" },
{ icon: "schedule", text: "Time & Weather" },
{ icon: "photo_camera", text: "Recording" },
{ icon: "wifi", text: "Network" },
{ icon: "monitor", text: "Display" },
{ icon: "settings_suggest", text: "Misc" },
{ icon: "info", text: "About" }
]
delegate: Column {
width: tabs.width
height: 40
Item {
width: parent.width
height: 39
RowLayout {
anchors.fill: parent
spacing: 8
Rectangle {
id: activeIndicator
Layout.leftMargin: 8
Layout.preferredWidth: 3
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignVCenter
radius: 2
color: Theme.accentPrimary
opacity: index === 0 ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 200 } }
}
Label {
id: icon
text: modelData.icon
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: index === 0 ? Theme.accentPrimary : Theme.textPrimary
opacity: index === 0 ? 1 : 0.8
Layout.leftMargin: 20
Layout.preferredWidth: 24
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Label {
id: label
text: modelData.text
font.pixelSize: 12
color: index === 0 ? Theme.accentPrimary : Theme.textSecondary
font.weight: index === 0 ? Font.DemiBold : Font.Normal
Layout.fillWidth: true
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.leftMargin: 4
Layout.rightMargin: 16
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
const newComponent = {
0: generalSettings,
1: barSettings,
2: timeWeatherSettings,
3: recordingSettings,
4: networkSettings,
5: displaySettings,
6: miscSettings,
7: aboutSettings
}[index];
const tabNames = [
"General",
"Bar",
"Time & Weather",
"Recording",
"Network",
"Display",
"Misc",
"About"
];
tabName.text = tabNames[index];
if (settingsLoader.opacity === 1) {
settingsLoader2.sourceComponent = newComponent;
settingsLoader.opacity = 0;
settingsLoader2.opacity = 1;
} else {
settingsLoader.sourceComponent = newComponent;
settingsLoader2.opacity = 0;
settingsLoader.opacity = 1;
}
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
let containerItem = item.children[0];
let rowLayout = containerItem.children[0];
let indicator = rowLayout.children[0];
let icon = rowLayout.children[1];
let label = rowLayout.children[2];
indicator.opacity = i === index ? 1 : 0;
icon.color = i === index ? Theme.accentPrimary : Theme.textPrimary;
icon.opacity = i === index ? 1 : 0.8;
label.color = i === index ? Theme.accentPrimary : Theme.textSecondary;
label.font.weight = i === index ? Font.Bold : Font.Normal;
}
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.6
visible: index < (repeater.count - 1)
}
}
}
}
}
}

View file

@ -0,0 +1,405 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import qs.Settings
import qs.Components
Item {
id: root
property string latestVersion: "Unknown"
property string currentVersion: "Unknown"
property var contributors: []
property string githubDataPath: Settings.settingsDir + "github_data.json"
Process {
id: currentVersionProcess
command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"]
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
}
}
}
Component.onCompleted: {
running = true
}
}
FileView {
id: githubDataFile
path: root.githubDataPath
blockLoading: true
printErrors: true
watchChanges: true
JsonAdapter {
id: githubData
property string version: "Unknown"
property var contributors: []
property double timestamp: 0
}
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()
}
function loadFromFile() {
const now = Date.now()
const data = githubData
if (!data.timestamp || (now - data.timestamp > 3600000)) {
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
}
}
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 = []
}
}
}
}
function fetchFromGitHub() {
versionProcess.running = true
contributorsProcess.running = true
}
function saveData() {
githubData.timestamp = Date.now()
Qt.callLater(() => {
githubDataFile.writeAdapter()
})
}
Item {
anchors.fill: parent
ColumnLayout {
id: mainLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: 8
Item {
Layout.fillWidth: true
Layout.preferredHeight: 32
}
Text {
text: "Noctalia"
font.pixelSize: 24
font.bold: true
color: Theme.textPrimary
Layout.alignment: Qt.AlignCenter
}
GridLayout {
Layout.alignment: Qt.AlignCenter
columns: 2
rowSpacing: 4
columnSpacing: 8
Text {
text: "Latest Version:"
font.pixelSize: 16
color: Theme.textSecondary
Layout.alignment: Qt.AlignRight
}
Text {
text: root.latestVersion
font.pixelSize: 16
color: Theme.textPrimary
font.bold: true
}
Text {
text: "Installed Version:"
font.pixelSize: 16
color: Theme.textSecondary
Layout.alignment: Qt.AlignRight
}
Text {
text: root.currentVersion
font.pixelSize: 16
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
color: updateArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
}
Text {
id: updateText
text: "Download latest release"
font.pixelSize: 14
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"])
}
}
}
Text {
text: "Description something something <.< I hate writing text..."
font.pixelSize: 14
color: Theme.textSecondary
Layout.alignment: Qt.AlignCenter
Layout.topMargin: 16
}
ColumnLayout {
Layout.fillWidth: true
Layout.topMargin: 32
Layout.leftMargin: 32
Layout.rightMargin: 32
spacing: 16
RowLayout {
Layout.alignment: Qt.AlignCenter
spacing: 8
Text {
text: "Contributors"
font.pixelSize: 18
font.bold: true
color: Theme.textPrimary
}
Text {
text: "(" + root.contributors.length + ")"
font.pixelSize: 14
color: Theme.textSecondary
}
}
ScrollView {
Layout.fillWidth: true
Layout.preferredHeight: 300
clip: true
Item {
anchors.fill: parent
GridView {
id: contributorsGrid
anchors.centerIn: parent
width: Math.min(parent.width, Math.ceil(root.contributors.length / 3) * 200)
height: parent.height
cellWidth: 200
cellHeight: 110
model: root.contributors
delegate: Rectangle {
width: contributorsGrid.cellWidth - 4
height: contributorsGrid.cellHeight - 10
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
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
color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textPrimary
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: (modelData.contributions || 0) + " commits"
font.pixelSize: 11
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,380 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Settings
import qs.Components
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Bar Elements"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 8
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Show Active Window Icon"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Display the icon of the currently focused window in the bar"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: activeWindowIconSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.showActiveWindowIcon ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.showActiveWindowIcon ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: activeWindowIconThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.showActiveWindowIcon ? activeWindowIconSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.showActiveWindowIcon = !Settings.settings.showActiveWindowIcon;
}
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Show Active Window"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Display the title of the currently focused window below the bar"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: activeWindowSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.showActiveWindow ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.showActiveWindow ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: activeWindowThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.showActiveWindow ? activeWindowSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.showActiveWindow = !Settings.settings.showActiveWindow;
}
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Show System Info"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Display system information (CPU, RAM, etc.) in the bar"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: systemInfoSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.showSystemInfoInBar ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.showSystemInfoInBar ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: systemInfoThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.showSystemInfoInBar ? systemInfoSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.showSystemInfoInBar = !Settings.settings.showSystemInfoInBar;
}
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Show Taskbar"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Display a taskbar showing currently open windows"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: taskbarSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.showTaskbar ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.showTaskbar ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: taskbarThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.showTaskbar ? taskbarSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.showTaskbar = !Settings.settings.showTaskbar;
}
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Show Media"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Display media controls and information in the bar"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: mediaSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.showMediaInBar ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.showMediaInBar ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: mediaThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.showMediaInBar ? mediaSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.showMediaInBar = !Settings.settings.showMediaInBar;
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}

View file

@ -0,0 +1,97 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Settings
import qs.Components
Rectangle {
id: root
width: 64
height: 32
radius: 16
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
property bool useFahrenheit: Settings.settings.useFahrenheit
Rectangle {
id: slider
width: parent.width / 2 - 4
height: parent.height - 4
radius: 14
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
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
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,354 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Settings
import qs.Components
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
// Get list of available monitors/screens
property var monitors: Quickshell.screens || []
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Monitor Selection"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 8
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Bar Monitors"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Select which monitors to display the top panel/bar on"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}
Flow {
Layout.fillWidth: true
spacing: 8
Repeater {
model: root.monitors
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
color: barCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary
visible: barCheckbox.isChecked
}
Text {
text: modelData.name || "Unknown"
font.pixelSize: 12
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
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Dock Monitors"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Select which monitors to display the application dock on"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}
Flow {
Layout.fillWidth: true
spacing: 8
Repeater {
model: root.monitors
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
color: dockCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary
visible: dockCheckbox.isChecked
}
Text {
text: modelData.name || "Unknown"
font.pixelSize: 12
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
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Notification Monitors"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Select which monitors to display system notifications on"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}
Flow {
Layout.fillWidth: true
spacing: 8
Repeater {
model: root.monitors
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
color: notificationCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary
visible: notificationCheckbox.isChecked
}
Text {
text: modelData.name || "Unknown"
font.pixelSize: 12
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));
}
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}

View file

@ -0,0 +1,339 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Settings
import qs.Components
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Profile"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 8
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
Text {
text: "Profile Image"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Your profile picture displayed in various places throughout the shell"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.bottomMargin: 4
}
RowLayout {
spacing: 8
Layout.fillWidth: true
Rectangle {
width: 48
height: 48
radius: 24
Rectangle {
anchors.fill: parent
color: "transparent"
radius: 24
border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 2
z: 2
}
Avatar {}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
radius: 16
color: Theme.surfaceVariant
border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: profileImageInput
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
anchors.topMargin: 6
anchors.bottomMargin: 6
text: Settings.settings.profileImage
font.pixelSize: 13
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()
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 16
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "User Interface"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 8
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Show Corners"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Display rounded corners on screen edges"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: cornersSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.showCorners ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.showCorners ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: cornersThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.showCorners ? cornersSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.showCorners = !Settings.settings.showCorners;
}
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 4
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Show Dock"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Display a dock at the bottom of the screen for quick access to applications"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: dockSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.showDock ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.showDock ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: dockThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.showDock ? dockSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.showDock = !Settings.settings.showDock;
}
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 4
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Dim Desktop"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Dim the desktop when panels or menus are open"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: dimSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.dimPanels ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.dimPanels ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: dimThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.dimPanels ? dimSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.dimPanels = !Settings.settings.dimPanels;
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}

View file

@ -0,0 +1,137 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Settings
import qs.Components
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Media"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 8
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Text {
text: "Visualizer Type"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Choose the style of the audio visualizer"
font.pixelSize: 12
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)
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: 1
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
contentItem: Text {
text: modelData.charAt(0).toUpperCase() + modelData.slice(1)
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
highlighted: visualizerTypeComboBox.highlightedIndex === index
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
onActivated: {
Settings.settings.visualizerType = model[index];
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}

View file

@ -0,0 +1,193 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import qs.Settings
import qs.Components
ColumnLayout {
id: root
spacing: 24
Component.onCompleted: {
Quickshell.execDetached(["nmcli", "-t", "-f", "WIFI", "radio"])
}
ColumnLayout {
spacing: 16
Layout.fillWidth: true
Text {
text: "Wi-Fi"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Enable Wi-Fi"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Turn Wi-Fi radio on or off"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: wifiSwitch
width: 52
height: 32
radius: 16
property bool checked: Settings.settings.wifiEnabled
color: checked ? Theme.accentPrimary : Theme.surfaceVariant
border.color: checked ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: wifiThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: wifiSwitch.checked ? wifiSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.wifiEnabled = !Settings.settings.wifiEnabled
Quickshell.execDetached(["nmcli", "radio", "wifi", Settings.settings.wifiEnabled ? "on" : "off"])
}
}
}
}
}
}
ColumnLayout {
spacing: 16
Layout.fillWidth: true
Layout.topMargin: 16
Text {
text: "Bluetooth"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Enable Bluetooth"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Turn Bluetooth radio on or off"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: bluetoothSwitch
width: 52
height: 32
radius: 16
property bool checked: Settings.settings.bluetoothEnabled
color: checked ? Theme.accentPrimary : Theme.surfaceVariant
border.color: checked ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: bluetoothThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: bluetoothSwitch.checked ? bluetoothSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (Bluetooth.defaultAdapter) {
Settings.settings.bluetoothEnabled = !Settings.settings.bluetoothEnabled
Bluetooth.defaultAdapter.enabled = Settings.settings.bluetoothEnabled
if (Bluetooth.defaultAdapter.enabled) {
Bluetooth.defaultAdapter.discovering = true
}
}
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: 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.Settings
import qs.Components
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: 0
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: "Screen Recording"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 8
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Text {
text: "Output Directory"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Directory where screen recordings will be saved"
font.pixelSize: 12
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
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
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Target frame rate for screen recordings (default: 60)"
font.pixelSize: 12
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
color: Theme.textPrimary
selectionColor: Theme.accentPrimary
selectedTextColor: Theme.onAccent
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
readOnly: false
selectByMouse: true
validator: IntValidator {
bottom: frameRateSpinBox.from
top: frameRateSpinBox.to
}
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);
}
}
}
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
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
color: Theme.textPrimary
anchors.centerIn: parent
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Audio Source"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Audio source to capture during recording"
font.pixelSize: 12
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")
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
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
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
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
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
highlighted: audioSourceComboBox.highlightedIndex === index
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
onActivated: {
Settings.settings.recordingAudioSource = model[index];
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Video Quality"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Higher quality results in larger file sizes"
font.pixelSize: 12
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")
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
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
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
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
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
highlighted: qualityComboBox.highlightedIndex === index
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
onActivated: {
Settings.settings.recordingQuality = model[index];
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Video Codec"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Different codecs offer different compression and compatibility"
font.pixelSize: 12
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")
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
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
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
contentItem: Text {
text: modelData.toUpperCase()
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
highlighted: codecComboBox.highlightedIndex === index
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
onActivated: {
Settings.settings.recordingCodec = model[index];
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Audio Codec"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Opus is recommended for best performance and smallest audio size"
font.pixelSize: 12
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")
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
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
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
contentItem: Text {
text: modelData.toUpperCase()
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
highlighted: audioCodecComboBox.highlightedIndex === index
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
onActivated: {
Settings.settings.audioCodec = model[index];
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Color Range"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Limited is recommended for better compatibility"
font.pixelSize: 12
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")
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
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
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
contentItem: Text {
text: modelData.charAt(0).toUpperCase() + modelData.slice(1)
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
highlighted: colorRangeComboBox.highlightedIndex === index
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
onActivated: {
Settings.settings.colorRange = model[index];
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Show Cursor"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Record mouse cursor in the video"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: cursorSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.showCursor ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.showCursor ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: cursorThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.showCursor ? cursorSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.showCursor = !Settings.settings.showCursor;
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 24
}
}
}
}

View file

@ -0,0 +1,283 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Settings
import qs.Components
import qs.Widgets.SettingsWindow.Tabs.Components
ColumnLayout {
id: root
spacing: 0
anchors.fill: parent
anchors.margins: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Time"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 8
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "Use 12 Hour Clock"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Display time in 12-hour format (e.g., 2:30 PM) instead of 24-hour format"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
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: use12HourClockThumb
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;
}
}
}
}
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
RowLayout {
spacing: 8
Layout.fillWidth: true
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: "US Style Date"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Display dates in MM/DD/YYYY format instead of DD/MM/YYYY"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
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;
}
}
}
}
}
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Layout.topMargin: 16
Text {
text: "Weather"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.bottomMargin: 8
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Text {
text: "City"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Your city name for weather information"
font.pixelSize: 12
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
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
font.bold: true
color: Theme.textPrimary
}
Text {
text: "Choose between Celsius and Fahrenheit"
font.pixelSize: 12
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
UnitSelector {}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}

View file

@ -1,13 +1,15 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Controls import QtQuick.Controls
import Qt5Compat.GraphicalEffects import Quickshell.Widgets
import qs.Components
import qs.Settings import qs.Settings
Rectangle { Rectangle {
id: profileSettingsCard id: profileSettingsCard
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 650 Layout.preferredHeight: 690
color: Theme.surface color: Theme.surface
radius: 18 radius: 18
@ -53,47 +55,23 @@ Rectangle {
spacing: 8 spacing: 8
Layout.fillWidth: true Layout.fillWidth: true
// Profile image
Rectangle { Rectangle {
width: 40 width: 48
height: 40 height: 48
radius: 20 radius: 24
color: Theme.surfaceVariant
border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
Image { // Border
id: avatarImage Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: 2 color: "transparent"
source: Settings.settings.profileImage radius: 24
fillMode: Image.PreserveAspectCrop border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline
visible: false border.width: 2
asynchronous: true z: 2
cache: false
sourceSize.width: 64
sourceSize.height: 64
} }
OpacityMask { Avatar {}
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: avatarImage.width / 2
visible: false
}
visible: Settings.settings.profileImage !== ""
}
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Theme.accentPrimary
visible: Settings.settings.profileImage === ""
}
} }
Rectangle { Rectangle {
@ -121,7 +99,7 @@ Rectangle {
activeFocusOnTab: true activeFocusOnTab: true
inputMethodHints: Qt.ImhUrlCharactersOnly inputMethodHints: Qt.ImhUrlCharactersOnly
onTextChanged: { onTextChanged: {
Settings.settings.profileImage = text Settings.settings.profileImage = text;
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@ -182,7 +160,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
Settings.settings.showActiveWindowIcon = !Settings.settings.showActiveWindowIcon Settings.settings.showActiveWindowIcon = !Settings.settings.showActiveWindowIcon;
} }
} }
} }
@ -237,7 +215,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
Settings.settings.showSystemInfoInBar = !Settings.settings.showSystemInfoInBar Settings.settings.showSystemInfoInBar = !Settings.settings.showSystemInfoInBar;
} }
} }
} }
@ -292,7 +270,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
Settings.settings.showCorners = !Settings.settings.showCorners Settings.settings.showCorners = !Settings.settings.showCorners;
} }
} }
} }
@ -347,7 +325,62 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
Settings.settings.showTaskbar = !Settings.settings.showTaskbar Settings.settings.showTaskbar = !Settings.settings.showTaskbar;
}
}
}
}
// Show Dock Setting
RowLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Show Dock"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Item {
Layout.fillWidth: true
}
Rectangle {
id: dockSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.showDock ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.showDock ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: dockThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.showDock ? taskbarSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.showDock = !Settings.settings.showDock;
} }
} }
} }
@ -402,7 +435,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
Settings.settings.showMediaInBar = !Settings.settings.showMediaInBar Settings.settings.showMediaInBar = !Settings.settings.showMediaInBar;
} }
} }
} }
@ -457,7 +490,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
Settings.settings.dimPanels = !Settings.settings.dimPanels Settings.settings.dimPanels = !Settings.settings.dimPanels;
} }
} }
} }
@ -596,7 +629,7 @@ Rectangle {
activeFocusOnTab: true activeFocusOnTab: true
inputMethodHints: Qt.ImhUrlCharactersOnly inputMethodHints: Qt.ImhUrlCharactersOnly
onTextChanged: { onTextChanged: {
Settings.settings.videoPath = text Settings.settings.videoPath = text;
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent

View file

@ -22,7 +22,7 @@ PanelWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Theme.backgroundPrimary color: Theme.backgroundPrimary
radius: 24 radius: 20
z: 0 z: 0
ColumnLayout { ColumnLayout {
@ -31,7 +31,6 @@ PanelWindow {
anchors.leftMargin: 32 anchors.leftMargin: 32
anchors.rightMargin: 32 anchors.rightMargin: 32
anchors.topMargin: 32 anchors.topMargin: 32
spacing: 24 spacing: 24
// Header // Header
@ -85,14 +84,14 @@ PanelWindow {
} }
} }
// Tabs bar (moved here) // Tabs bar (reordered)
Tabs { Tabs {
id: settingsTabs id: settingsTabs
Layout.fillWidth: true Layout.fillWidth: true
tabsModel: [ tabsModel: [
{ icon: "cloud", label: "Weather" },
{ icon: "settings", label: "System" }, { icon: "settings", label: "System" },
{ icon: "wallpaper", label: "Wallpaper" } { icon: "wallpaper", label: "Wallpaper" },
{ icon: "cloud", label: "Weather" }
] ]
} }
@ -115,7 +114,32 @@ PanelWindow {
id: tabContentLoader id: tabContentLoader
anchors.top: parent.top anchors.top: parent.top
width: parent.width width: parent.width
sourceComponent: settingsTabs.currentIndex === 0 ? weatherTab : settingsTabs.currentIndex === 1 ? systemTab : wallpaperTab sourceComponent: settingsTabs.currentIndex === 0 ? systemTab : settingsTabs.currentIndex === 1 ? wallpaperTab : weatherTab
}
}
Component {
id: systemTab
ColumnLayout {
anchors.fill: parent
ProfileSettings {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
anchors.margins: 16
}
}
}
Component {
id: wallpaperTab
ColumnLayout {
anchors.fill: parent
WallpaperSettings {
id: wallpaperSettings
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
anchors.margins: 16
}
} }
} }
@ -130,29 +154,6 @@ PanelWindow {
} }
} }
} }
Component {
id: systemTab
ColumnLayout {
anchors.fill: parent
ProfileSettings {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
anchors.margins: 16
}
}
}
Component {
id: wallpaperTab
ColumnLayout {
anchors.fill: parent
WallpaperSettings {
id: wallpaperSettings
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
anchors.margins: 16
}
}
}
} }
} }
} }
@ -160,7 +161,6 @@ PanelWindow {
// Function to open the modal and initialize temp values // Function to open the modal and initialize temp values
function openSettings() { function openSettings() {
visible = true; visible = true;
// Force focus on the text input after a short delay
focusTimer.start(); focusTimer.start();
} }
@ -174,20 +174,16 @@ PanelWindow {
interval: 100 interval: 100
repeat: false repeat: false
onTriggered: { onTriggered: {
if (visible) if (visible) {
// Focus will be handled by the individual components // Focus logic can go here if needed
{} }
} }
} }
// Release focus when modal becomes invisible // Refresh weather data when hidden
onVisibleChanged: { onVisibleChanged: {
if (!visible) { if (!visible && typeof weather !== 'undefined' && weather !== null && weather.fetchCityWeather) {
// Focus will be handled by the individual components
if (typeof weather !== 'undefined' && weather !== null && weather.fetchCityWeather) {
weather.fetchCityWeather(); weather.fetchCityWeather();
} }
} }
} }
}

View file

@ -1,12 +1,13 @@
import QtQuick import QtQuick
import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts
import qs.Settings import qs.Settings
Rectangle { Rectangle {
id: wallpaperSettingsCard id: wallpaperSettingsCard
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 720 Layout.preferredHeight: Settings.settings.useSWWW ? 720 : 360
color: Theme.surface color: Theme.surface
radius: 18 radius: 18
@ -15,16 +16,18 @@ Rectangle {
anchors.margins: 18 anchors.margins: 18
spacing: 12 spacing: 12
// Header
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 12 spacing: 12
Text { Text {
text: "image" text: "image"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 20 font.pixelSize: 20
color: Theme.accentPrimary color: Theme.accentPrimary
} }
Text { Text {
text: "Wallpaper Settings" text: "Wallpaper Settings"
font.family: Theme.fontFamily font.family: Theme.fontFamily
@ -35,6 +38,7 @@ Rectangle {
} }
} }
ColumnLayout { ColumnLayout {
spacing: 8 spacing: 8
Layout.fillWidth: true Layout.fillWidth: true
@ -47,7 +51,7 @@ Rectangle {
color: Theme.textPrimary color: Theme.textPrimary
} }
// Folder Path Input
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: 40
@ -55,8 +59,10 @@ Rectangle {
color: Theme.surfaceVariant color: Theme.surfaceVariant
border.color: folderInput.activeFocus ? Theme.accentPrimary : Theme.outline border.color: folderInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1 border.width: 1
TextInput { TextInput {
id: folderInput id: folderInput
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
@ -77,72 +83,22 @@ Rectangle {
onTextChanged: { onTextChanged: {
Settings.settings.wallpaperFolder = text; Settings.settings.wallpaperFolder = text;
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.IBeamCursor cursorShape: Qt.IBeamCursor
onClicked: folderInput.forceActiveFocus() onClicked: folderInput.forceActiveFocus()
} }
}
}
} }
// Use SWWW Setting
RowLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Use SWWW"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
} }
Item {
Layout.fillWidth: true
} }
// Custom Material 3 Switch
Rectangle {
id: swwwSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.useSWWW ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.useSWWW ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: swwwThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.useSWWW ? swwwSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.useSWWW = !Settings.settings.useSWWW;
}
}
}
}
// Random Wallpaper Setting
RowLayout { RowLayout {
spacing: 8 spacing: 8
Layout.fillWidth: true Layout.fillWidth: true
@ -162,6 +118,7 @@ Rectangle {
// Custom Material 3 Switch // Custom Material 3 Switch
Rectangle { Rectangle {
id: randomWallpaperSwitch id: randomWallpaperSwitch
width: 52 width: 52
height: 32 height: 32
radius: 16 radius: 16
@ -171,6 +128,7 @@ Rectangle {
Rectangle { Rectangle {
id: randomWallpaperThumb id: randomWallpaperThumb
width: 28 width: 28
height: 28 height: 28
radius: 14 radius: 14
@ -185,7 +143,9 @@ Rectangle {
duration: 200 duration: 200
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
} }
} }
} }
MouseArea { MouseArea {
@ -195,10 +155,12 @@ Rectangle {
Settings.settings.randomWallpaper = !Settings.settings.randomWallpaper; Settings.settings.randomWallpaper = !Settings.settings.randomWallpaper;
} }
} }
}
} }
// Use Wallpaper Theme Setting }
RowLayout { RowLayout {
spacing: 8 spacing: 8
Layout.fillWidth: true Layout.fillWidth: true
@ -218,6 +180,7 @@ Rectangle {
// Custom Material 3 Switch // Custom Material 3 Switch
Rectangle { Rectangle {
id: wallpaperThemeSwitch id: wallpaperThemeSwitch
width: 52 width: 52
height: 32 height: 32
radius: 16 radius: 16
@ -227,6 +190,7 @@ Rectangle {
Rectangle { Rectangle {
id: wallpaperThemeThumb id: wallpaperThemeThumb
width: 28 width: 28
height: 28 height: 28
radius: 14 radius: 14
@ -241,7 +205,9 @@ Rectangle {
duration: 200 duration: 200
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
} }
} }
} }
MouseArea { MouseArea {
@ -251,10 +217,12 @@ Rectangle {
Settings.settings.useWallpaperTheme = !Settings.settings.useWallpaperTheme; Settings.settings.useWallpaperTheme = !Settings.settings.useWallpaperTheme;
} }
} }
}
} }
// Wallpaper Interval Setting }
ColumnLayout { ColumnLayout {
spacing: 12 spacing: 12
Layout.fillWidth: true Layout.fillWidth: true
@ -262,6 +230,7 @@ Rectangle {
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
Text { Text {
text: "Wallpaper Interval (seconds)" text: "Wallpaper Interval (seconds)"
font.pixelSize: 13 font.pixelSize: 13
@ -278,16 +247,21 @@ Rectangle {
font.pixelSize: 13 font.pixelSize: 13
color: Theme.textPrimary color: Theme.textPrimary
} }
} }
Slider { Slider {
id: intervalSlider id: intervalSlider
Layout.fillWidth: true Layout.fillWidth: true
from: 10 from: 10
to: 900 to: 900
stepSize: 10 stepSize: 10
value: Settings.settings.wallpaperInterval value: Settings.settings.wallpaperInterval
snapMode: Slider.SnapAlways snapMode: Slider.SnapAlways
onMoved: {
Settings.settings.wallpaperInterval = Math.round(value);
}
background: Rectangle { background: Rectangle {
x: intervalSlider.leftPadding x: intervalSlider.leftPadding
@ -305,6 +279,7 @@ Rectangle {
color: Theme.accentPrimary color: Theme.accentPrimary
radius: 2 radius: 2
} }
} }
handle: Rectangle { handle: Rectangle {
@ -318,17 +293,78 @@ Rectangle {
border.width: 2 border.width: 2
} }
onMoved: {
Settings.settings.wallpaperInterval = Math.round(value);
} }
}
RowLayout {
spacing: 8
Layout.fillWidth: true
Layout.topMargin: 8
Text {
text: "Use SWWW"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Item {
Layout.fillWidth: true
}
// Custom Material 3 Switch
Rectangle {
id: swwwSwitch
width: 52
height: 32
radius: 16
color: Settings.settings.useSWWW ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Settings.settings.useSWWW ? Theme.accentPrimary : Theme.outline
border.width: 2
Rectangle {
id: swwwThumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: Settings.settings.useSWWW ? swwwSwitch.width - width - 2 : 2
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.settings.useSWWW = !Settings.settings.useSWWW;
} }
} }
// Resize Mode Setting }
}
ColumnLayout { ColumnLayout {
spacing: 12 spacing: 12
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
visible: Settings.settings.useSWWW
Text { Text {
text: "Resize Mode" text: "Resize Mode"
@ -339,10 +375,14 @@ Rectangle {
ComboBox { ComboBox {
id: resizeComboBox id: resizeComboBox
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: 40
model: ["no", "crop", "fit", "stretch"] model: ["no", "crop", "fit", "stretch"]
currentIndex: model.indexOf(Settings.settings.wallpaperResize) currentIndex: model.indexOf(Settings.settings.wallpaperResize)
onActivated: {
Settings.settings.wallpaperResize = model[index];
}
background: Rectangle { background: Rectangle {
implicitWidth: 120 implicitWidth: 120
@ -385,7 +425,9 @@ Rectangle {
model: resizeComboBox.popup.visible ? resizeComboBox.delegateModel : null model: resizeComboBox.popup.visible ? resizeComboBox.delegateModel : null
currentIndex: resizeComboBox.highlightedIndex currentIndex: resizeComboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {} ScrollIndicator.vertical: ScrollIndicator {
}
} }
background: Rectangle { background: Rectangle {
@ -394,10 +436,13 @@ Rectangle {
border.width: 1 border.width: 1
radius: 16 radius: 16
} }
} }
delegate: ItemDelegate { delegate: ItemDelegate {
width: resizeComboBox.width width: resizeComboBox.width
highlighted: resizeComboBox.highlightedIndex === index
contentItem: Text { contentItem: Text {
text: modelData text: modelData
font.family: Theme.fontFamily font.family: Theme.fontFamily
@ -406,24 +451,23 @@ Rectangle {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight elide: Text.ElideRight
} }
highlighted: resizeComboBox.highlightedIndex === index
background: Rectangle { background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
} }
} }
onActivated: {
Settings.settings.wallpaperResize = model[index];
}
}
} }
// Transition Type Setting }
ColumnLayout { ColumnLayout {
spacing: 12 spacing: 12
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
visible: Settings.settings.useSWWW
Text { Text {
text: "Transition Type" text: "Transition Type"
@ -434,10 +478,14 @@ Rectangle {
ComboBox { ComboBox {
id: transitionTypeComboBox id: transitionTypeComboBox
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: 40
model: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"] model: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"]
currentIndex: model.indexOf(Settings.settings.transitionType) currentIndex: model.indexOf(Settings.settings.transitionType)
onActivated: {
Settings.settings.transitionType = model[index];
}
background: Rectangle { background: Rectangle {
implicitWidth: 120 implicitWidth: 120
@ -480,7 +528,9 @@ Rectangle {
model: transitionTypeComboBox.popup.visible ? transitionTypeComboBox.delegateModel : null model: transitionTypeComboBox.popup.visible ? transitionTypeComboBox.delegateModel : null
currentIndex: transitionTypeComboBox.highlightedIndex currentIndex: transitionTypeComboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {} ScrollIndicator.vertical: ScrollIndicator {
}
} }
background: Rectangle { background: Rectangle {
@ -489,10 +539,13 @@ Rectangle {
border.width: 1 border.width: 1
radius: 16 radius: 16
} }
} }
delegate: ItemDelegate { delegate: ItemDelegate {
width: transitionTypeComboBox.width width: transitionTypeComboBox.width
highlighted: transitionTypeComboBox.highlightedIndex === index
contentItem: Text { contentItem: Text {
text: modelData text: modelData
font.family: Theme.fontFamily font.family: Theme.fontFamily
@ -501,27 +554,27 @@ Rectangle {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight elide: Text.ElideRight
} }
highlighted: transitionTypeComboBox.highlightedIndex === index
background: Rectangle { background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
} }
} }
onActivated: {
Settings.settings.transitionType = model[index];
}
}
} }
// Transition FPS Setting }
ColumnLayout { ColumnLayout {
spacing: 12 spacing: 12
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
visible: Settings.settings.useSWWW
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
Text { Text {
text: "Transition FPS" text: "Transition FPS"
font.pixelSize: 13 font.pixelSize: 13
@ -538,16 +591,21 @@ Rectangle {
font.pixelSize: 13 font.pixelSize: 13
color: Theme.textPrimary color: Theme.textPrimary
} }
} }
Slider { Slider {
id: fpsSlider id: fpsSlider
Layout.fillWidth: true Layout.fillWidth: true
from: 30 from: 30
to: 500 to: 500
stepSize: 5 stepSize: 5
value: Settings.settings.transitionFps value: Settings.settings.transitionFps
snapMode: Slider.SnapAlways snapMode: Slider.SnapAlways
onMoved: {
Settings.settings.transitionFps = Math.round(value);
}
background: Rectangle { background: Rectangle {
x: fpsSlider.leftPadding x: fpsSlider.leftPadding
@ -565,6 +623,7 @@ Rectangle {
color: Theme.accentPrimary color: Theme.accentPrimary
radius: 2 radius: 2
} }
} }
handle: Rectangle { handle: Rectangle {
@ -578,20 +637,20 @@ Rectangle {
border.width: 2 border.width: 2
} }
onMoved: {
Settings.settings.transitionFps = Math.round(value);
}
}
} }
// Transition Duration Setting }
ColumnLayout { ColumnLayout {
spacing: 12 spacing: 12
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
visible: Settings.settings.useSWWW
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
Text { Text {
text: "Transition Duration (seconds)" text: "Transition Duration (seconds)"
font.pixelSize: 13 font.pixelSize: 13
@ -608,16 +667,21 @@ Rectangle {
font.pixelSize: 13 font.pixelSize: 13
color: Theme.textPrimary color: Theme.textPrimary
} }
} }
Slider { Slider {
id: durationSlider id: durationSlider
Layout.fillWidth: true Layout.fillWidth: true
from: 0.250 from: 0.25
to: 10.0 to: 10
stepSize: 0.050 stepSize: 0.05
value: Settings.settings.transitionDuration value: Settings.settings.transitionDuration
snapMode: Slider.SnapAlways snapMode: Slider.SnapAlways
onMoved: {
Settings.settings.transitionDuration = value;
}
background: Rectangle { background: Rectangle {
x: durationSlider.leftPadding x: durationSlider.leftPadding
@ -635,6 +699,7 @@ Rectangle {
color: Theme.accentPrimary color: Theme.accentPrimary
radius: 2 radius: 2
} }
} }
handle: Rectangle { handle: Rectangle {
@ -648,10 +713,10 @@ Rectangle {
border.width: 2 border.width: 2
} }
onMoved: {
Settings.settings.transitionDuration = value;
}
} }
} }
} }
} }

View file

@ -14,7 +14,7 @@ Rectangle {
anchors.margins: 18 anchors.margins: 18
spacing: 12 spacing: 12
// Weather Settings Header
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 12 spacing: 12
@ -36,7 +36,7 @@ Rectangle {
} }
} }
// Weather City Setting
ColumnLayout { ColumnLayout {
spacing: 8 spacing: 8
Layout.fillWidth: true Layout.fillWidth: true
@ -93,7 +93,7 @@ Rectangle {
} }
} }
// Temperature Unit Setting
RowLayout { RowLayout {
spacing: 12 spacing: 12
Layout.fillWidth: true Layout.fillWidth: true
@ -160,7 +160,7 @@ Rectangle {
} }
// Random Wallpaper Setting
RowLayout { RowLayout {
spacing: 8 spacing: 8
Layout.fillWidth: true Layout.fillWidth: true
@ -216,7 +216,7 @@ Rectangle {
} }
} }
// Reverse Day Month Setting
RowLayout { RowLayout {
spacing: 8 spacing: 8
Layout.fillWidth: true Layout.fillWidth: true

View file

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

View file

@ -1,7 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Qt5Compat.GraphicalEffects import QtQuick.Effects
import qs.Settings import qs.Settings
import qs.Components import qs.Components
import qs.Services import qs.Services
@ -53,24 +53,108 @@ Rectangle {
spacing: 12 spacing: 12
visible: !!MusicManager.currentPlayer visible: !!MusicManager.currentPlayer
// Album art and spectrum // Player selector
ComboBox {
id: playerSelector
Layout.fillWidth: true
Layout.preferredHeight: 40
visible: MusicManager.getAvailablePlayers().length > 1
model: MusicManager.getAvailablePlayers()
textRole: "identity"
currentIndex: MusicManager.selectedPlayerIndex
background: Rectangle {
implicitWidth: 120
implicitHeight: 40
color: Theme.surfaceVariant
border.color: playerSelector.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
radius: 16
}
contentItem: Text {
leftPadding: 12
rightPadding: playerSelector.indicator.width + playerSelector.spacing
text: playerSelector.displayText
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
indicator: Text {
x: playerSelector.width - width - 12
y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2
text: "arrow_drop_down"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: Theme.textPrimary
}
popup: Popup {
y: playerSelector.height
width: playerSelector.width
implicitHeight: contentItem.implicitHeight
padding: 1
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: playerSelector.popup.visible ? playerSelector.delegateModel : null
currentIndex: playerSelector.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {}
}
background: Rectangle {
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
radius: 16
}
}
delegate: ItemDelegate {
width: playerSelector.width
contentItem: Text {
text: modelData.identity
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
highlighted: playerSelector.highlightedIndex === index
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
onActivated: {
MusicManager.selectedPlayerIndex = index;
MusicManager.updateCurrentPlayer();
}
}
// Album art with spectrum visualizer
RowLayout { RowLayout {
spacing: 12 spacing: 12
Layout.fillWidth: true Layout.fillWidth: true
// Album art with spectrum // Album art container with circular spectrum overlay
Item { Item {
id: albumArtContainer id: albumArtContainer
width: 96; height: 96 // enough for spectrum and art (will adjust if needed) width: 96
height: 96 // enough for spectrum and art (will adjust if needed)
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
// Spectrum visualizer // Circular spectrum visualizer around album art
CircularSpectrum { CircularSpectrum {
id: spectrum id: spectrum
values: MusicManager.cavaValues values: MusicManager.cavaValues
anchors.centerIn: parent anchors.centerIn: parent
innerRadius: 30 // just outside 60x60 album art innerRadius: 30 // Position just outside 60x60 album art
outerRadius: 48 // how far bars extend outerRadius: 48 // Extend bars outward from album art
fillColor: Theme.accentPrimary fillColor: Theme.accentPrimary
strokeColor: Theme.accentPrimary strokeColor: Theme.accentPrimary
strokeWidth: 0 strokeWidth: 0
@ -80,7 +164,8 @@ Rectangle {
// Album art image // Album art image
Rectangle { Rectangle {
id: albumArtwork id: albumArtwork
width: 60; height: 60 width: 60
height: 60
anchors.centerIn: parent anchors.centerIn: parent
radius: 30 // circle radius: 30 // circle
color: Qt.darker(Theme.surface, 1.1) color: Qt.darker(Theme.surface, 1.1)
@ -93,6 +178,7 @@ Rectangle {
anchors.margins: 2 anchors.margins: 2
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
smooth: true smooth: true
mipmap: true
cache: false cache: false
asynchronous: true asynchronous: true
sourceSize.width: 60 sourceSize.width: 60
@ -100,20 +186,29 @@ Rectangle {
source: MusicManager.trackArtUrl source: MusicManager.trackArtUrl
visible: source.toString() !== "" visible: source.toString() !== ""
// Rounded corners using layer // Apply circular mask for rounded corners
layer.enabled: true layer.enabled: true
layer.effect: OpacityMask { layer.effect: MultiEffect {
cached: true maskEnabled: true
maskSource: Rectangle { maskSource: mask
width: albumArt.width
height: albumArt.height
radius: albumArt.width / 2 // circle
visible: false
}
} }
} }
// Fallback icon Item {
id: mask
anchors.fill: albumArt
layer.enabled: true
visible: false
Rectangle {
width: albumArt.width
height: albumArt.height
radius: albumArt.width / 2 // circle
}
}
// Fallback icon when no album art available
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "album" text: "album"
@ -171,8 +266,12 @@ Rectangle {
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15) color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15)
Layout.fillWidth: true Layout.fillWidth: true
property real progressRatio: Math.min(1, MusicManager.trackLength > 0 ? property real progressRatio: {
(MusicManager.currentPosition / MusicManager.trackLength) : 0) if (!MusicManager.currentPlayer || !MusicManager.isPlaying || MusicManager.trackLength <= 0) {
return 0;
}
return Math.min(1, MusicManager.currentPosition / MusicManager.trackLength);
}
Rectangle { Rectangle {
id: progressFill id: progressFill
@ -182,7 +281,9 @@ Rectangle {
color: Theme.accentPrimary color: Theme.accentPrimary
Behavior on width { Behavior on width {
NumberAnimation { duration: 200 } NumberAnimation {
duration: 200
}
} }
} }
@ -203,7 +304,9 @@ Rectangle {
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0 scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0
Behavior on scale { Behavior on scale {
NumberAnimation { duration: 150 } NumberAnimation {
duration: 150
}
} }
} }
@ -216,14 +319,14 @@ Rectangle {
enabled: MusicManager.trackLength > 0 && MusicManager.canSeek enabled: MusicManager.trackLength > 0 && MusicManager.canSeek
onClicked: function (mouse) { onClicked: function (mouse) {
let ratio = mouse.x / width let ratio = mouse.x / width;
MusicManager.seekByRatio(ratio) MusicManager.seekByRatio(ratio);
} }
onPositionChanged: function (mouse) { onPositionChanged: function (mouse) {
if (pressed) { if (pressed) {
let ratio = Math.max(0, Math.min(1, mouse.x / width)) let ratio = Math.max(0, Math.min(1, mouse.x / width));
MusicManager.seekByRatio(ratio) MusicManager.seekByRatio(ratio);
} }
} }
} }

View file

@ -35,7 +35,7 @@ PanelWithOverlay {
anchors.top: parent.top anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
// Animation properties
property real slideOffset: width property real slideOffset: width
property bool isAnimating: false property bool isAnimating: false
@ -59,8 +59,8 @@ PanelWithOverlay {
if (sidebarPopupRect.settingsModal && sidebarPopupRect.settingsModal.visible) { if (sidebarPopupRect.settingsModal && sidebarPopupRect.settingsModal.visible) {
sidebarPopupRect.settingsModal.visible = false; sidebarPopupRect.settingsModal.visible = false;
} }
if (sidebarPopupRect.wallpaperPanelModal && sidebarPopupRect.wallpaperPanelModal.visible) { if (wallpaperPanel && wallpaperPanel.visible) {
sidebarPopupRect.wallpaperPanelModal.visible = false; wallpaperPanel.visible = false;
} }
if (sidebarPopupRect.wifiPanelModal && sidebarPopupRect.wifiPanelModal.visible) { if (sidebarPopupRect.wifiPanelModal && sidebarPopupRect.wifiPanelModal.visible) {
sidebarPopupRect.wifiPanelModal.visible = false; sidebarPopupRect.wifiPanelModal.visible = false;
@ -85,7 +85,7 @@ PanelWithOverlay {
onStopped: { onStopped: {
if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) { if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) {
sidebarPopup.visible = false; sidebarPopup.visible = false;
// Stop monitoring and background tasks when hidden
if (weather) if (weather)
weather.stopWeatherFetch(); weather.stopWeatherFetch();
if (systemWidget) if (systemWidget)
@ -125,7 +125,6 @@ PanelWithOverlay {
} }
property alias settingsModal: settingsModal property alias settingsModal: settingsModal
property alias wallpaperPanelModal: wallpaperPanelModal
property alias wifiPanelModal: wifiPanel.panel property alias wifiPanelModal: wifiPanel.panel
property alias bluetoothPanelModal: bluetoothPanel.panel property alias bluetoothPanelModal: bluetoothPanel.panel
SettingsModal { SettingsModal {
@ -314,7 +313,7 @@ PanelWithOverlay {
settingsModal.visible = true; settingsModal.visible = true;
} }
onWallpaperRequested: { onWallpaperRequested: {
wallpaperPanelModal.visible = true; wallpaperPanel.visible = true;
} }
} }
} }
@ -339,7 +338,15 @@ PanelWithOverlay {
videoPath += "/"; videoPath += "/";
} }
var outputPath = videoPath + filename; var outputPath = videoPath + filename;
var command = "gpu-screen-recorder -w portal -f 60 -a default_output -o " + outputPath; 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]); Quickshell.execDetached(["sh", "-c", command]);
isRecording = true; isRecording = true;
quickAccessWidget.isRecording = true; quickAccessWidget.isRecording = true;
@ -403,15 +410,13 @@ PanelWithOverlay {
} }
WallpaperPanel { WallpaperPanel {
id: wallpaperPanelModal id: wallpaperPanel
visible: false
Component.onCompleted: { Component.onCompleted: {
if (parent) { if (parent) {
wallpaperPanelModal.anchors.top = parent.top; anchors.top = parent.top;
wallpaperPanelModal.anchors.right = parent.right; anchors.right = parent.right;
} }
} }
// Add a close button inside WallpaperPanel.qml for user to close the modal
} }
} }
} }

View file

@ -17,7 +17,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 20 spacing: 20
// Performance
Rectangle { Rectangle {
width: 36; height: 36 width: 36; height: 36
radius: 18 radius: 18
@ -63,7 +63,7 @@ Rectangle {
} }
} }
// Balanced
Rectangle { Rectangle {
width: 36; height: 36 width: 36; height: 36
radius: 18 radius: 18
@ -109,7 +109,7 @@ Rectangle {
} }
} }
// Power Saver
Rectangle { Rectangle {
width: 36; height: 36 width: 36; height: 36
radius: 18 radius: 18

View file

@ -1,7 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Qt5Compat.GraphicalEffects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Settings import qs.Settings
@ -32,7 +32,7 @@ Rectangle {
anchors.margins: 18 anchors.margins: 18
spacing: 12 spacing: 12
// Settings Button
Rectangle { Rectangle {
id: settingsButton id: settingsButton
Layout.fillWidth: true Layout.fillWidth: true
@ -75,7 +75,7 @@ Rectangle {
} }
} }
// Screen Recorder Button
Rectangle { Rectangle {
id: recorderButton id: recorderButton
Layout.fillWidth: true Layout.fillWidth: true
@ -123,7 +123,7 @@ Rectangle {
} }
} }
// Wallpaper Button
Rectangle { Rectangle {
id: wallpaperButton id: wallpaperButton
Layout.fillWidth: true Layout.fillWidth: true
@ -168,10 +168,10 @@ Rectangle {
} }
} }
// Properties
property bool panelVisible: false property bool panelVisible: false
// Timer to check if recording is active
Timer { Timer {
interval: 2000 interval: 2000
repeat: true repeat: true
@ -185,7 +185,7 @@ Rectangle {
} }
} }
// Process to check if gpu-screen-recorder is running
Process { Process {
id: checkRecordingProcess id: checkRecordingProcess
command: ["pgrep", "-f", "gpu-screen-recorder.*portal"] command: ["pgrep", "-f", "gpu-screen-recorder.*portal"]

View file

@ -1,9 +1,10 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Qt5Compat.GraphicalEffects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Widgets
import qs.Settings import qs.Settings
import qs.Widgets import qs.Widgets
import qs.Widgets.LockScreen import qs.Widgets.LockScreen
@ -29,19 +30,19 @@ Rectangle {
anchors.margins: 18 anchors.margins: 18
spacing: 12 spacing: 12
// User info row
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 12 spacing: 12
// Profile image
Rectangle { Rectangle {
width: 48 width: 48
height: 48 height: 48
radius: 24 radius: 24
color: Theme.accentPrimary color: Theme.accentPrimary
// Border
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: "transparent" color: "transparent"
@ -51,41 +52,10 @@ Rectangle {
z: 2 z: 2
} }
OpacityMask { Avatar {}
anchors.fill: parent
source: Image {
id: avatarImage
anchors.fill: parent
source: Settings.settings.profileImage !== undefined ? Settings.settings.profileImage : ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
sourceSize.width: 44
sourceSize.height: 44
}
maskSource: Rectangle {
width: 44
height: 44
radius: 22
visible: false
}
visible: Settings.settings.profileImage !== undefined && Settings.settings.profileImage !== ""
z: 1
} }
// Fallback icon
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: Theme.onAccent
visible: Settings.settings.profileImage === undefined || Settings.settings.profileImage === ""
z: 0
}
}
// User info text
ColumnLayout { ColumnLayout {
spacing: 4 spacing: 4
Layout.fillWidth: true Layout.fillWidth: true
@ -106,12 +76,12 @@ Rectangle {
} }
} }
// Spacer
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }
// System menu button
Rectangle { Rectangle {
id: systemButton id: systemButton
width: 32 width: 32
@ -153,7 +123,7 @@ Rectangle {
id: systemMenu id: systemMenu
anchors.top: systemButton.bottom anchors.top: systemButton.bottom
anchors.right: systemButton.right anchors.right: systemButton.right
// System menu popup
Rectangle { Rectangle {
width: 160 width: 160
@ -167,7 +137,7 @@ Rectangle {
anchors.top: parent.top anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
// Position below system button
anchors.rightMargin: 32 anchors.rightMargin: 32
anchors.topMargin: systemButton.y + systemButton.height + 48 anchors.topMargin: systemButton.y + systemButton.height + 48
@ -176,7 +146,7 @@ Rectangle {
anchors.margins: 8 anchors.margins: 8
spacing: 4 spacing: 4
// Lock button
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 36 Layout.preferredHeight: 36
@ -216,7 +186,7 @@ Rectangle {
} }
} }
// Suspend button
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 36 Layout.preferredHeight: 36
@ -255,7 +225,7 @@ Rectangle {
} }
} }
// Reboot button
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 36 Layout.preferredHeight: 36
@ -295,7 +265,7 @@ Rectangle {
} }
} }
// Logout button
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 36 Layout.preferredHeight: 36
@ -334,7 +304,7 @@ Rectangle {
} }
} }
// Shutdown button
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 36 Layout.preferredHeight: 36
@ -376,10 +346,10 @@ Rectangle {
} }
} }
// Properties
property string uptimeText: "--:--" property string uptimeText: "--:--"
// Process to get uptime
Process { Process {
id: uptimeProcess id: uptimeProcess
command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"] command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"]
@ -422,13 +392,19 @@ Rectangle {
running: false running: false
} }
Process {
id: logoutProcess
command: ["loginctl", "terminate-user", Quickshell.env("USER")]
running: false
}
function logout() { function logout() {
if (WorkspaceManager.isNiri) { if (WorkspaceManager.isNiri) {
logoutProcessNiri.running = true; logoutProcessNiri.running = true;
} else if (WorkspaceManager.isHyprland) { } else if (WorkspaceManager.isHyprland) {
logoutProcessHyprland.running = true; logoutProcessHyprland.running = true;
} else { } else {
// fallback or error
console.warn("No supported compositor detected for logout"); console.warn("No supported compositor detected for logout");
} }
} }
@ -445,19 +421,18 @@ Rectangle {
rebootProcess.running = true; rebootProcess.running = true;
} }
property bool panelVisible: false property bool panelVisible: false
// Trigger initial update when panel becomes visible
onPanelVisibleChanged: { onPanelVisibleChanged: {
if (panelVisible) { if (panelVisible) {
updateSystemInfo(); updateSystemInfo();
} }
} }
// Timer to update uptime - only runs when panel is visible
Timer { Timer {
interval: 60000 // Update every minute interval: 60000
repeat: true repeat: true
running: panelVisible running: panelVisible
onTriggered: updateSystemInfo() onTriggered: updateSystemInfo()
@ -471,7 +446,7 @@ Rectangle {
uptimeProcess.running = true; uptimeProcess.running = true;
} }
// Add lockscreen instance (hidden by default)
LockScreen { LockScreen {
id: lockScreen id: lockScreen
} }

View file

@ -12,6 +12,7 @@ Rectangle {
height: 250 height: 250
color: "transparent" color: "transparent"
// Track visibility state for panel integration
property bool isVisible: false property bool isVisible: false
Rectangle { Rectangle {
@ -26,7 +27,8 @@ Rectangle {
spacing: 12 spacing: 12
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
// CPU Usage
// CPU usage indicator with circular progress bar
Item { Item {
width: 50; height: 50 width: 50; height: 50
CircularProgressBar { CircularProgressBar {
@ -55,7 +57,8 @@ Rectangle {
} }
} }
// Cpu Temp
// CPU temperature indicator with circular progress bar
Item { Item {
width: 50; height: 50 width: 50; height: 50
CircularProgressBar { CircularProgressBar {
@ -85,7 +88,8 @@ Rectangle {
} }
} }
// Memory Usage
// Memory usage indicator with circular progress bar
Item { Item {
width: 50; height: 50 width: 50; height: 50
CircularProgressBar { CircularProgressBar {
@ -114,7 +118,8 @@ Rectangle {
} }
} }
// Disk Usage
// Disk usage indicator with circular progress bar
Item { Item {
width: 50; height: 50 width: 50; height: 50
CircularProgressBar { CircularProgressBar {

View file

@ -30,7 +30,7 @@ PanelWindow {
} }
onVisibleChanged: { onVisibleChanged: {
if (wallpaperPanelModal.visible) { if (wallpaperPanel.visible) {
wallpapers = WallpaperManager.wallpaperList wallpapers = WallpaperManager.wallpaperList
} else { } else {
wallpapers = [] wallpapers = []
@ -40,7 +40,7 @@ PanelWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Theme.backgroundPrimary color: Theme.backgroundPrimary
radius: 24 radius: 20
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 32 anchors.margins: 32
@ -81,7 +81,9 @@ PanelWindow {
id: closeButtonArea id: closeButtonArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onClicked: wallpaperPanelModal.visible = false onClicked: {
wallpaperPanel.visible = false;
}
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
} }
@ -92,7 +94,7 @@ PanelWindow {
color: Theme.outline color: Theme.outline
opacity: 0.12 opacity: 0.12
} }
// Wallpaper grid area
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
@ -114,7 +116,7 @@ PanelWindow {
cellWidth: Math.max(120, (scrollView.width / 3) - 12) cellWidth: Math.max(120, (scrollView.width / 3) - 12)
cellHeight: cellWidth * 0.6 cellHeight: cellWidth * 0.6
model: wallpapers model: wallpapers
cacheBuffer: 32 cacheBuffer: 64
leftMargin: 8 leftMargin: 8
rightMargin: 8 rightMargin: 8
topMargin: 8 topMargin: 8
@ -129,7 +131,7 @@ PanelWindow {
color: Qt.darker(Theme.backgroundPrimary, 1.1) color: Qt.darker(Theme.backgroundPrimary, 1.1)
radius: 12 radius: 12
border.color: Settings.settings.currentWallpaper === modelData ? Theme.accentPrimary : Theme.outline border.color: Settings.settings.currentWallpaper === modelData ? Theme.accentPrimary : Theme.outline
border.width: Settings.settings.currentWallpaper === modelData ? 3 : 1 border.width: 2
Image { Image {
id: wallpaperImage id: wallpaperImage
anchors.fill: parent anchors.fill: parent
@ -137,8 +139,19 @@ PanelWindow {
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
asynchronous: true asynchronous: true
cache: true cache: true
sourceSize.width: Math.min(width, 150) smooth: true
sourceSize.height: Math.min(height, 90) mipmap: true
sourceSize.width: Math.min(width, 480)
sourceSize.height: Math.min(height, 270)
opacity: (wallpaperImage.status == Image.Ready) ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent

View file

@ -54,17 +54,17 @@ Rectangle {
anchors.margins: 18 anchors.margins: 18
spacing: 12 spacing: 12
// Current weather row
RowLayout { RowLayout {
spacing: 12 spacing: 12
Layout.fillWidth: true Layout.fillWidth: true
// Weather icon and basic info section
RowLayout { RowLayout {
spacing: 12 spacing: 12
Layout.preferredWidth: 140 Layout.preferredWidth: 140
// Weather icon
Text { Text {
id: weatherIcon id: weatherIcon
text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud" text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud"
@ -103,13 +103,13 @@ Rectangle {
} }
} }
} }
// Spacer to push content to the right
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
// Separator line
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 1 height: 1
@ -119,7 +119,7 @@ Rectangle {
Layout.bottomMargin: 2 Layout.bottomMargin: 2
} }
// 5-day forecast row
RowLayout { RowLayout {
spacing: 12 spacing: 12
Layout.fillWidth: true Layout.fillWidth: true
@ -132,7 +132,7 @@ Rectangle {
spacing: 2 spacing: 2
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Text { Text {
// Day of the week (e.g., Mon)
text: Qt.formatDateTime(new Date(weatherData.daily.time[index]), "ddd") text: Qt.formatDateTime(new Date(weatherData.daily.time[index]), "ddd")
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: 12 font.pixelSize: 12
@ -141,7 +141,7 @@ Rectangle {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
Text { Text {
// Material Symbol icon
text: materialSymbolForCode(weatherData.daily.weathercode[index]) text: materialSymbolForCode(weatherData.daily.weathercode[index])
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 22 font.pixelSize: 22
@ -150,7 +150,7 @@ Rectangle {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
Text { Text {
// High/low temp
text: weatherData && weatherData.daily ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.daily.temperature_2m_max[index] * 9/5 + 32)}° / ${Math.round(weatherData.daily.temperature_2m_min[index] * 9/5 + 32)}°` : `${Math.round(weatherData.daily.temperature_2m_max[index])}° / ${Math.round(weatherData.daily.temperature_2m_min[index])}°`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--° / --°" : "--° / --°") text: weatherData && weatherData.daily ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.daily.temperature_2m_max[index] * 9/5 + 32)}° / ${Math.round(weatherData.daily.temperature_2m_min[index] * 9/5 + 32)}°` : `${Math.round(weatherData.daily.temperature_2m_max[index])}° / ${Math.round(weatherData.daily.temperature_2m_min[index])}°`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--° / --°" : "--° / --°")
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: 12 font.pixelSize: 12
@ -162,7 +162,7 @@ Rectangle {
} }
} }
// Error message
Text { Text {
text: errorString text: errorString
color: Theme.error color: Theme.error
@ -175,16 +175,16 @@ Rectangle {
} }
} }
// Weather code to Material Symbol ligature mapping
function materialSymbolForCode(code) { function materialSymbolForCode(code) {
if (code === 0) return "sunny"; // Clear if (code === 0) return "sunny";
if (code === 1 || code === 2) return "partly_cloudy_day"; // Mainly clear/partly cloudy if (code === 1 || code === 2) return "partly_cloudy_day";
if (code === 3) return "cloud"; // Overcast if (code === 3) return "cloud";
if (code >= 45 && code <= 48) return "foggy"; // Fog if (code >= 45 && code <= 48) return "foggy";
if (code >= 51 && code <= 67) return "rainy"; // Drizzle if (code >= 51 && code <= 67) return "rainy";
if (code >= 71 && code <= 77) return "weather_snowy"; // Snow if (code >= 71 && code <= 77) return "weather_snowy";
if (code >= 80 && code <= 82) return "rainy"; // Rain showers if (code >= 80 && code <= 82) return "rainy";
if (code >= 95 && code <= 99) return "thunderstorm"; // Thunderstorm if (code >= 95 && code <= 99) return "thunderstorm";
return "cloud"; return "cloud";
} }
function weatherDescriptionForCode(code) { function weatherDescriptionForCode(code) {

View file

@ -17,66 +17,122 @@ Item {
wifiLogic.refreshNetworks(); wifiLogic.refreshNetworks();
} }
Component.onCompleted: {
existingNetwork.running = true;
}
function signalIcon(signal) { function signalIcon(signal) {
if (signal >= 80) return "network_wifi"; if (signal >= 80)
if (signal >= 60) return "network_wifi_3_bar"; return "network_wifi";
if (signal >= 40) return "network_wifi_2_bar"; if (signal >= 60)
if (signal >= 20) return "network_wifi_1_bar"; 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"; return "wifi_0_bar";
} }
Process {
id: existingNetwork
running: false
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n");
const networksMap = {};
refreshIndicator.running = true;
refreshIndicator.visible = true;
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 = wifiLogic.replaceQuickshell(parts[0]);
const type = parts[1];
if (ssid) {
networksMap[ssid] = {
ssid: ssid,
type: type
};
}
}
scanProcess.existingNetwork = networksMap;
scanProcess.running = true;
}
}
}
Process { Process {
id: scanProcess id: scanProcess
running: false running: false
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
onRunningChanged: {
// Removed debug log property var existingNetwork
}
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
var lines = text.split("\n"); const lines = text.split("\n");
var nets = []; const networksMap = {};
var seen = {};
for (var i = 0; i < lines.length; ++i) { for (let i = 0; i < lines.length; ++i) {
var line = lines[i].trim(); const line = lines[i].trim();
if (!line) continue; if (!line)
var parts = line.split(":"); continue;
var ssid = parts[0];
var security = parts[1]; const parts = line.split(":");
var signal = parseInt(parts[2]); if (parts.length < 4) {
var inUse = parts[3] === "*"; 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 (ssid) {
if (!seen[ssid]) { if (!networksMap[ssid]) {
// First time seeing this SSID networksMap[ssid] = {
nets.push({ ssid: ssid, security: security, signal: signal, connected: inUse }); ssid: ssid,
seen[ssid] = true; security: security,
signal: signal,
connected: inUse,
existing: ssid in scanProcess.existingNetwork
};
} else { } else {
// SSID already exists, update if this entry has better signal or is connected const existingNet = networksMap[ssid];
for (var j = 0; j < nets.length; ++j) {
if (nets[j].ssid === ssid) {
// Update connection status if this entry is connected
if (inUse) { if (inUse) {
nets[j].connected = true; existingNet.connected = true;
} }
// Update signal if this entry has better signal if (signal > existingNet.signal) {
if (signal > nets[j].signal) { existingNet.signal = signal;
nets[j].signal = signal; existingNet.security = security;
nets[j].security = security;
}
break;
} }
} }
} }
} }
}
wifiLogic.networks = nets;
wifiLogic.networks = networksMap;
scanProcess.existingNetwork = {};
refreshIndicator.running = false;
refreshIndicator.visible = false;
} }
} }
} }
QtObject { QtObject {
id: wifiLogic id: wifiLogic
property var networks: [] property var networks: {}
property var anchorItem: null property var anchorItem: null
property real anchorX property real anchorX
property real anchorY property real anchorY
@ -90,54 +146,98 @@ Item {
property string connectSecurity: "" property string connectSecurity: ""
property var pendingConnect: null property var pendingConnect: null
property string detectedInterface: "" property string detectedInterface: ""
property string actionPanelSsid: ""
function profileNameForSsid(ssid) { function replaceQuickshell(ssid: string): string {
return "quickshell-" + ssid.replace(/[^a-zA-Z0-9]/g, "_"); const newName = ssid.replace("quickshell-", "");
if (!ssid.startsWith("quickshell-")) {
return newName;
} }
if (wifiLogic.networks && newName in wifiLogic.networks) {
console.log(`Quickshell ${newName} already exists, deleting old profile`)
deleteProfileProcess.connName = ssid;
deleteProfileProcess.running = true;
}
console.log(`Changing from ${ssid} to ${newName}`)
renameConnectionProcess.oldName = ssid;
renameConnectionProcess.newName = newName;
renameConnectionProcess.running = true;
return newName;
}
function disconnectNetwork(ssid) { function disconnectNetwork(ssid) {
var profileName = wifiLogic.profileNameForSsid(ssid); const profileName = ssid;
disconnectProfileProcess.connectionName = profileName; disconnectProfileProcess.connectionName = profileName;
disconnectProfileProcess.running = true; disconnectProfileProcess.running = true;
} }
function refreshNetworks() { function refreshNetworks() {
scanProcess.running = true; existingNetwork.running = true;
} }
function showAt() { function showAt() {
wifiPanelModal.visible = true; wifiPanelModal.visible = true;
wifiLogic.refreshNetworks(); wifiLogic.refreshNetworks();
} }
function connectNetwork(ssid, security) { function connectNetwork(ssid, security) {
wifiLogic.pendingConnect = {ssid: ssid, security: security, password: ""}; wifiLogic.pendingConnect = {
listConnectionsProcess.running = true; ssid: ssid,
security: security,
password: ""
};
wifiLogic.doConnect();
} }
function submitPassword() { function submitPassword() {
wifiLogic.pendingConnect = {ssid: wifiLogic.passwordPromptSsid, security: wifiLogic.connectSecurity, password: wifiLogic.passwordInput}; wifiLogic.pendingConnect = {
listConnectionsProcess.running = true; ssid: wifiLogic.passwordPromptSsid,
security: wifiLogic.connectSecurity,
password: wifiLogic.passwordInput
};
wifiLogic.doConnect();
} }
function doConnect() { function doConnect() {
var params = wifiLogic.pendingConnect; const params = wifiLogic.pendingConnect;
if (!params)
return;
wifiLogic.connectingSsid = params.ssid; wifiLogic.connectingSsid = params.ssid;
const targetNetwork = wifiLogic.networks[params.ssid];
if (targetNetwork && targetNetwork.existing) {
upConnectionProcess.profileName = params.ssid;
upConnectionProcess.running = true;
wifiLogic.pendingConnect = null;
return;
}
if (params.security && params.security !== "--") { if (params.security && params.security !== "--") {
getInterfaceProcess.running = true; getInterfaceProcess.running = true;
} else { return;
}
connectProcess.security = params.security; connectProcess.security = params.security;
connectProcess.ssid = params.ssid; connectProcess.ssid = params.ssid;
connectProcess.password = params.password; connectProcess.password = params.password;
connectProcess.running = true; connectProcess.running = true;
wifiLogic.pendingConnect = null; wifiLogic.pendingConnect = null;
} }
}
function isSecured(security) { function isSecured(security) {
return security && security.trim() !== "" && security.trim() !== "--"; return security && security.trim() !== "" && security.trim() !== "--";
} }
} }
// Disconnect, delete profile, refresh
Process { Process {
id: disconnectProfileProcess id: disconnectProfileProcess
property string connectionName: "" property string connectionName: ""
running: false running: false
command: ["nmcli", "connection", "down", "id", connectionName] command: ["nmcli", "connection", "down", connectionName]
onRunningChanged: { onRunningChanged: {
if (!running) { if (!running) {
wifiLogic.refreshNetworks(); wifiLogic.refreshNetworks();
@ -145,63 +245,70 @@ Item {
} }
} }
// Process to rename a connection
Process { Process {
id: listConnectionsProcess id: renameConnectionProcess
running: false running: false
command: ["nmcli", "-t", "-f", "NAME", "connection", "show"] property string oldName: ""
property string newName: ""
command: ["nmcli", "connection", "modify", oldName, "connection.id", newName]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
var params = wifiLogic.pendingConnect; console.log("Successfully renamed connection '" +
var lines = text.split("\n"); renameConnectionProcess.oldName + "' to '" +
var expectedProfile = wifiLogic.profileNameForSsid(params.ssid); renameConnectionProcess.newName + "'");
var foundProfile = null;
for (var i = 0; i < lines.length; ++i) {
if (lines[i] === expectedProfile) {
foundProfile = lines[i];
break;
} }
} }
if (foundProfile) { stderr: StdioCollector {
// Profile exists, just bring it up (no password prompt) onStreamFinished: {
upConnectionProcess.profileName = foundProfile; if (text.trim() !== "" && !text.toLowerCase().includes("warning")) {
upConnectionProcess.running = true; console.error("Error renaming connection:", text);
} else {
// No profile: check if secured
if (wifiLogic.isSecured(params.security)) {
if (params.password && params.password.length > 0) {
// Password provided, proceed to connect
wifiLogic.doConnect();
} else {
// No password yet, prompt for it
wifiLogic.passwordPromptSsid = params.ssid;
wifiLogic.passwordInput = "";
wifiLogic.showPasswordPrompt = true;
wifiLogic.connectStatus = "";
wifiLogic.connectStatusSsid = "";
wifiLogic.connectError = "";
wifiLogic.connectSecurity = params.security;
}
} else {
// Open, connect directly
wifiLogic.doConnect();
}
} }
} }
} }
} }
// Handles connecting to a Wi-Fi network, with or without password
// Process to rename a connection
Process {
id: deleteProfileProcess
running: false
property string connName: ""
command: ["nmcli", "connection", "delete", `'${connName}'`]
stdout: StdioCollector {
onStreamFinished: {
console.log("Deleted connection '" + deleteProfileProcess.connName + "'");
}
}
stderr: StdioCollector {
onStreamFinished: {
console.error("Error deleting connection '" + deleteProfileProcess.connName + "':", text);
}
}
}
Process { Process {
id: connectProcess id: connectProcess
property string ssid: "" property string ssid: ""
property string password: "" property string password: ""
property string security: "" property string security: ""
running: false running: false
onStarted: {
refreshIndicator.running = true;
}
onExited: (exitCode, exitStatus) => {
refreshIndicator.running = false;
}
command: { command: {
if (password) { if (password) {
return ["nmcli", "device", "wifi", "connect", ssid, "password", password] return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password];
} else { } else {
return ["nmcli", "device", "wifi", "connect", ssid] return ["nmcli", "device", "wifi", "connect", `'${ssid}'`];
} }
} }
stdout: StdioCollector { stdout: StdioCollector {
@ -229,7 +336,7 @@ Item {
} }
} }
// Finds the correct Wi-Fi interface for connection
Process { Process {
id: getInterfaceProcess id: getInterfaceProcess
running: false running: false
@ -249,7 +356,7 @@ Item {
addConnectionProcess.ifname = wifiLogic.detectedInterface; addConnectionProcess.ifname = wifiLogic.detectedInterface;
addConnectionProcess.ssid = params.ssid; addConnectionProcess.ssid = params.ssid;
addConnectionProcess.password = params.password; addConnectionProcess.password = params.password;
addConnectionProcess.profileName = wifiLogic.profileNameForSsid(params.ssid); addConnectionProcess.profileName = params.ssid;
addConnectionProcess.security = params.security; addConnectionProcess.security = params.security;
addConnectionProcess.running = true; addConnectionProcess.running = true;
} else { } else {
@ -263,7 +370,7 @@ Item {
} }
} }
// Adds a new Wi-Fi connection profile
Process { Process {
id: addConnectionProcess id: addConnectionProcess
property string ifname: "" property string ifname: ""
@ -296,7 +403,7 @@ Item {
} }
} }
// Brings up the new connection profile and finalizes connection state
Process { Process {
id: upConnectionProcess id: upConnectionProcess
property string profileName: "" property string profileName: ""
@ -329,10 +436,11 @@ Item {
} }
} }
// Wifi button (no background card)
Rectangle { Rectangle {
id: wifiButton id: wifiButton
width: 36; height: 36 width: 36
height: 36
radius: 18 radius: 18
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1
@ -343,9 +451,7 @@ Item {
text: "wifi" text: "wifi"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 22 font.pixelSize: 22
color: wifiButtonArea.containsMouse color: wifiButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
? Theme.backgroundPrimary
: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
@ -371,12 +477,12 @@ Item {
margins.top: 0 margins.top: 0
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Component.onCompleted: { Component.onCompleted: {
wifiLogic.refreshNetworks() wifiLogic.refreshNetworks();
} }
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Theme.backgroundPrimary color: Theme.backgroundPrimary
radius: 24 radius: 20
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 32 anchors.margins: 32
@ -400,8 +506,29 @@ Item {
color: Theme.textPrimary color: Theme.textPrimary
Layout.fillWidth: true Layout.fillWidth: true
} }
Item {
Layout.fillWidth: true
}
Spinner {
id: refreshIndicator
Layout.preferredWidth: 24
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignVCenter
visible: false
running: false
color: Theme.accentPrimary
size: 22
}
IconButton {
id: refreshButton
icon: "refresh"
onClicked: wifiLogic.refreshNetworks()
}
Rectangle { Rectangle {
width: 36; height: 36; radius: 18 implicitWidth: 36
implicitHeight: 36
radius: 18
color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent" color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1
@ -463,11 +590,15 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: 4 spacing: 4
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
model: wifiLogic.networks model: wifiLogic.networks ? Object.values(wifiLogic.networks) : null
delegate: Item { delegate: Item {
id: networkEntry id: networkEntry
required property var modelData
property var signalIcon: wifiPanel.signalIcon
width: parent.width width: parent.width
height: modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt ? 102 : 42 height: (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt ? 102 : 42) + (modelData.ssid === wifiLogic.actionPanelSsid ? 60 : 0)
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
@ -504,7 +635,8 @@ Item {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
Item { Item {
width: 22; height: 22 width: 22
height: 22
visible: wifiLogic.connectStatusSsid === modelData.ssid && wifiLogic.connectStatus !== "" visible: wifiLogic.connectStatusSsid === modelData.ssid && wifiLogic.connectStatus !== ""
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@ -572,10 +704,11 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onClicked: { onClicked: {
if (modelData.connected) {
wifiLogic.disconnectNetwork(modelData.ssid); if (wifiLogic.actionPanelSsid === modelData.ssid) {
wifiLogic.actionPanelSsid = ""; // Close if already open
} else { } else {
wifiLogic.connectNetwork(modelData.ssid, modelData.security); wifiLogic.actionPanelSsid = modelData.ssid; // Open for this network
} }
} }
} }
@ -586,8 +719,9 @@ Item {
Layout.preferredHeight: 60 Layout.preferredHeight: 60
radius: 8 radius: 8
color: "transparent" color: "transparent"
anchors.leftMargin: 32 Layout.alignment: Qt.AlignLeft
anchors.rightMargin: 32 Layout.leftMargin: 32
Layout.rightMargin: 32
z: 2 z: 2
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@ -627,14 +761,18 @@ Item {
} }
} }
Rectangle { Rectangle {
width: 80 Layout.preferredWidth: 80
height: 36 Layout.preferredHeight: 36
radius: 18 radius: 18
color: Theme.accentPrimary color: Theme.accentPrimary
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 0 border.width: 0
opacity: 1.0 opacity: 1.0
Behavior on color { ColorAnimation { duration: 100 } } Behavior on color {
ColorAnimation {
duration: 100
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: wifiLogic.submitPassword() onClicked: wifiLogic.submitPassword()
@ -653,6 +791,113 @@ Item {
} }
} }
} }
Rectangle {
visible: modelData.ssid === wifiLogic.actionPanelSsid
Layout.fillWidth: true
Layout.preferredHeight: 60
radius: 8
color: "transparent"
Layout.alignment: Qt.AlignLeft
Layout.leftMargin: 32
Layout.rightMargin: 32
z: 2
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 10
Item {
Layout.fillWidth: true
Layout.preferredHeight: 36
visible: wifiLogic.isSecured(modelData.security) && !modelData.connected && !modelData.existing
Rectangle {
anchors.fill: parent
radius: 8
color: "transparent"
border.color: actionPanelPasswordField.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: actionPanelPasswordField
anchors.fill: parent
anchors.margins: 12
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
echoMode: TextInput.Password
onAccepted: {
wifiLogic.pendingConnect = {
ssid: modelData.ssid,
security: modelData.security,
password: text
};
wifiLogic.doConnect();
wifiLogic.actionPanelSsid = ""; // Close the panel
}
}
}
}
Rectangle {
Layout.preferredWidth: 80
Layout.preferredHeight: 36
radius: 18
color: modelData.connected ? Theme.error : Theme.accentPrimary
border.color: modelData.connected ? Theme.error : Theme.accentPrimary
border.width: 0
opacity: 1.0
Behavior on color {
ColorAnimation {
duration: 100
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if (modelData.connected) {
wifiLogic.disconnectNetwork(modelData.ssid);
} else {
if (wifiLogic.isSecured(modelData.security) && !modelData.existing) {
if (actionPanelPasswordField.text.length > 0) {
wifiLogic.pendingConnect = {
ssid: modelData.ssid,
security: modelData.security,
password: actionPanelPasswordField.text
};
wifiLogic.doConnect();
}
} else {
wifiLogic.connectNetwork(modelData.ssid, modelData.security);
}
}
wifiLogic.actionPanelSsid = ""; // Close the panel
}
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.color = modelData.connected ? Qt.darker(Theme.error, 1.1) : Qt.darker(Theme.accentPrimary, 1.1)
onExited: parent.color = modelData.connected ? Theme.error : Theme.accentPrimary
}
Text {
anchors.centerIn: parent
text: modelData.connected ? "wifi_off" : "check"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Theme.backgroundPrimary
}
}
}
}
} }
} }
} }

0
qmlls.ini Normal file
View file

View file

@ -22,18 +22,17 @@ Scope {
property var notificationHistoryWin: notificationHistoryWin property var notificationHistoryWin: notificationHistoryWin
property bool pendingReload: false property bool pendingReload: false
// Helper function to round value to nearest step // Round volume to nearest 5% increment for consistent control
function roundToStep(value, step) { function roundToStep(value, step) {
return Math.round(value / step) * step; return Math.round(value / step) * step;
} }
// Volume property reflecting current audio volume in 0-100 // Current audio volume (0-100), synced with system
// Will be kept in sync dynamically below
property int volume: (defaultAudioSink && defaultAudioSink.audio && !defaultAudioSink.audio.muted) property int volume: (defaultAudioSink && defaultAudioSink.audio && !defaultAudioSink.audio.muted)
? Math.round(defaultAudioSink.audio.volume * 100) ? Math.round(defaultAudioSink.audio.volume * 100)
: 0 : 0
// Function to update volume with clamping, stepping, and applying to audio sink // Update volume with 5-step increments and apply to audio sink
function updateVolume(vol) { function updateVolume(vol) {
var clamped = Math.max(0, Math.min(100, vol)); var clamped = Math.max(0, Math.min(100, vol));
var stepped = roundToStep(clamped, 5); var stepped = roundToStep(clamped, 5);
@ -53,6 +52,15 @@ Scope {
property var notificationHistoryWin: notificationHistoryWin property var notificationHistoryWin: notificationHistoryWin
} }
// Create dock for each monitor (respects dockMonitors setting)
Variants {
model: Quickshell.screens
Dock {
property var modelData
}
}
Applauncher { Applauncher {
id: appLauncherPanel id: appLauncherPanel
visible: false visible: false
@ -77,9 +85,15 @@ Scope {
onNotification: function (notification) { onNotification: function (notification) {
console.log("Notification received:", notification.appName); console.log("Notification received:", notification.appName);
notification.tracked = true; notification.tracked = true;
if (notificationPopup.notificationsVisible) {
notificationPopup.addNotification(notification); // Distribute notification to all visible notification popups
for (let i = 0; i < notificationPopupVariants.count; i++) {
let popup = notificationPopupVariants.objectAt(i);
if (popup && popup.notificationsVisible) {
popup.addNotification(notification);
} }
}
if (notificationHistoryWin) { if (notificationHistoryWin) {
notificationHistoryWin.addToHistory({ notificationHistoryWin.addToHistory({
id: notification.id, id: notification.id,
@ -93,9 +107,19 @@ Scope {
} }
} }
// Create notification popups for each selected monitor
Variants {
id: notificationPopupVariants
model: Quickshell.screens
NotificationPopup { NotificationPopup {
id: notificationPopup property var modelData
barVisible: bar.visible barVisible: bar.visible
screen: modelData
visible: notificationsVisible && notificationModel.count > 0 &&
(Settings.settings.notificationMonitors.includes(modelData.name) ||
(Settings.settings.notificationMonitors.length === 0)) // Show on all if none selected
}
} }
NotificationHistory { NotificationHistory {
@ -113,7 +137,7 @@ Scope {
appLauncherPanel: appLauncherPanel appLauncherPanel: appLauncherPanel
lockScreen: lockScreen lockScreen: lockScreen
idleInhibitor: idleInhibitor idleInhibitor: idleInhibitor
notificationPopup: notificationPopup notificationPopupVariants: notificationPopupVariants
} }
Connections { Connections {
@ -130,11 +154,12 @@ Scope {
Timer { Timer {
id: reloadTimer id: reloadTimer
interval: 500 // ms interval: 500
repeat: false repeat: false
onTriggered: Quickshell.reload(true) onTriggered: Quickshell.reload(true)
} }
// Handle screen configuration changes (delay reload if locked)
Connections { Connections {
target: Quickshell target: Quickshell
function onScreensChanged() { function onScreensChanged() {
@ -146,17 +171,15 @@ Scope {
} }
} }
// --- NEW: Keep volume property in sync with actual Pipewire audio sink volume ---
Connections { Connections {
target: defaultAudioSink.audio target: defaultAudioSink ? defaultAudioSink.audio : null
onVolumeChanged: { function onVolumeChanged() {
if (defaultAudioSink.audio && !defaultAudioSink.audio.muted) { if (defaultAudioSink.audio && !defaultAudioSink.audio.muted) {
volume = Math.round(defaultAudioSink.audio.volume * 100); volume = Math.round(defaultAudioSink.audio.volume * 100);
console.log("Volume changed externally to:", volume); console.log("Volume changed externally to:", volume);
} }
} }
onMutedChanged: { function onMutedChanged() {
if (defaultAudioSink.audio) { if (defaultAudioSink.audio) {
if (defaultAudioSink.audio.muted) { if (defaultAudioSink.audio.muted) {
volume = 0; volume = 0;