Settings rework...
This commit is contained in:
parent
74b233798d
commit
fb68300746
63 changed files with 7139 additions and 1026 deletions
35
Bar/Bar.qml
35
Bar/Bar.qml
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sort pinned apps alphabetically for consistent display
|
||||||
|
pinned.sort(function(a, b) {
|
||||||
|
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||||
|
});
|
||||||
|
root.filteredApps = pinned.concat(unpinned);
|
||||||
|
root.selectedIndex = 0;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (!query || query.startsWith("=")) {
|
if (!query) {
|
||||||
results = results.concat(apps.sort(function (a, b) {
|
results = results.concat(apps.sort(function (a, b) {
|
||||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||||
}));
|
}));
|
||||||
|
|
@ -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,310 +469,382 @@ PanelWithOverlay {
|
||||||
|
|
||||||
Component.onCompleted: updateFilter()
|
Component.onCompleted: updateFilter()
|
||||||
|
|
||||||
ColumnLayout {
|
RowLayout {
|
||||||
anchors.left: parent.left
|
anchors.fill: parent
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.margins: 32
|
anchors.margins: 32
|
||||||
spacing: 18
|
spacing: 18
|
||||||
|
|
||||||
// Search Bar
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: searchBar
|
id: previewPanel
|
||||||
color: Theme.surfaceVariant
|
Layout.preferredWidth: 200
|
||||||
radius: 22
|
Layout.fillHeight: true
|
||||||
height: 48
|
color: Theme.surface
|
||||||
Layout.fillWidth: true
|
radius: 20
|
||||||
border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline
|
visible: false
|
||||||
border.width: searchField.activeFocus ? 2 : 1
|
|
||||||
|
|
||||||
RowLayout {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.fill: parent
|
||||||
anchors.right: parent.right
|
anchors.margins: 16
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
color: "transparent"
|
||||||
anchors.leftMargin: 14
|
clip: true
|
||||||
anchors.rightMargin: 14
|
|
||||||
spacing: 10
|
|
||||||
Text {
|
|
||||||
text: "search"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: Theme.fontSizeHeader
|
|
||||||
color: searchField.activeFocus ? Theme.accentPrimary : Theme.textSecondary
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
TextField {
|
|
||||||
id: searchField
|
|
||||||
placeholderText: "Search apps..."
|
|
||||||
color: Theme.textPrimary
|
|
||||||
placeholderTextColor: Theme.textSecondary
|
|
||||||
background: null
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeBody
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
onTextChanged: root.updateFilter()
|
|
||||||
selectedTextColor: Theme.onAccent
|
|
||||||
selectionColor: Theme.accentPrimary
|
|
||||||
padding: 0
|
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
|
||||||
leftPadding: 0
|
|
||||||
rightPadding: 0
|
|
||||||
topPadding: 0
|
|
||||||
bottomPadding: 0
|
|
||||||
font.bold: true
|
|
||||||
Component.onCompleted: contentItem.cursorColor = Theme.textPrimary
|
|
||||||
onActiveFocusChanged: contentItem.cursorColor = Theme.textPrimary
|
|
||||||
|
|
||||||
Keys.onDownPressed: root.selectNext()
|
Image {
|
||||||
Keys.onUpPressed: root.selectPrev()
|
id: previewImage
|
||||||
Keys.onEnterPressed: root.activateSelected()
|
anchors.fill: parent
|
||||||
Keys.onReturnPressed: root.activateSelected()
|
fillMode: Image.PreserveAspectFit
|
||||||
Keys.onEscapePressed: appLauncherPanel.hidePanel()
|
asynchronous: true
|
||||||
}
|
cache: true
|
||||||
}
|
smooth: true
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: 120
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 120
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// App List Card
|
|
||||||
Rectangle {
|
ColumnLayout {
|
||||||
color: Theme.surface
|
|
||||||
radius: 20
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
clip: true
|
spacing: 18
|
||||||
property int innerPadding: 16
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.top: parent.top
|
Rectangle {
|
||||||
anchors.left: parent.left
|
id: searchBar
|
||||||
anchors.right: parent.right
|
color: Theme.surfaceVariant
|
||||||
height: parent.innerPadding
|
radius: 20
|
||||||
visible: false
|
height: 48
|
||||||
|
Layout.fillWidth: true
|
||||||
|
border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: searchField.activeFocus ? 2 : 1
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: 14
|
||||||
|
anchors.rightMargin: 14
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "search"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: Theme.fontSizeHeader
|
||||||
|
color: searchField.activeFocus ? Theme.accentPrimary : Theme.textSecondary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: searchField
|
||||||
|
placeholderText: "Search apps..."
|
||||||
|
color: Theme.textPrimary
|
||||||
|
placeholderTextColor: Theme.textSecondary
|
||||||
|
background: null
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeBody
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
onTextChanged: root.updateFilter()
|
||||||
|
selectedTextColor: Theme.onAccent
|
||||||
|
selectionColor: Theme.accentPrimary
|
||||||
|
padding: 0
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
leftPadding: 0
|
||||||
|
rightPadding: 0
|
||||||
|
topPadding: 0
|
||||||
|
bottomPadding: 0
|
||||||
|
font.bold: true
|
||||||
|
Component.onCompleted: contentItem.cursorColor = Theme.textPrimary
|
||||||
|
onActiveFocusChanged: contentItem.cursorColor = Theme.textPrimary
|
||||||
|
|
||||||
|
Keys.onDownPressed: root.selectNext()
|
||||||
|
Keys.onUpPressed: root.selectPrev()
|
||||||
|
Keys.onEnterPressed: root.activateSelected()
|
||||||
|
Keys.onReturnPressed: root.activateSelected()
|
||||||
|
Keys.onEscapePressed: appLauncherPanel.hidePanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 120
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
|
||||||
id: appList
|
Rectangle {
|
||||||
anchors.fill: parent
|
color: Theme.surface
|
||||||
anchors.margins: parent.innerPadding
|
radius: 20
|
||||||
spacing: 2
|
Layout.fillWidth: true
|
||||||
model: root.filteredApps
|
Layout.fillHeight: true
|
||||||
currentIndex: root.selectedIndex
|
clip: true
|
||||||
delegate: Item {
|
property int innerPadding: 16
|
||||||
id: appDelegate
|
|
||||||
width: appList.width
|
|
||||||
height: 48
|
|
||||||
property bool hovered: mouseArea.containsMouse
|
|
||||||
property bool isSelected: index === root.selectedIndex
|
|
||||||
|
|
||||||
Rectangle {
|
ListView {
|
||||||
anchors.fill: parent
|
id: appList
|
||||||
color: (hovered || isSelected)
|
anchors.fill: parent
|
||||||
? Theme.accentPrimary
|
anchors.margins: parent.innerPadding
|
||||||
: (appLauncherPanel.isPinned(modelData) ? Theme.surfaceVariant : "transparent")
|
spacing: 2
|
||||||
radius: 12
|
model: root.filteredApps
|
||||||
border.color: appLauncherPanel.isPinned(modelData)
|
currentIndex: root.selectedIndex
|
||||||
? "transparent"
|
delegate: Item {
|
||||||
: (hovered || isSelected ? Theme.accentPrimary : "transparent")
|
id: appDelegate
|
||||||
border.width: appLauncherPanel.isPinned(modelData) ? 0 : (hovered || isSelected ? 2 : 0)
|
width: appList.width
|
||||||
Behavior on color {
|
height: (modelData.isClipboard || modelData.isCommand) ? 64 : 48
|
||||||
ColorAnimation {
|
property bool hovered: mouseArea.containsMouse
|
||||||
duration: 120
|
property bool isSelected: index === root.selectedIndex
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: 120
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 120
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: 10
|
color: (hovered || isSelected)
|
||||||
anchors.rightMargin: 10
|
? Theme.accentPrimary
|
||||||
spacing: 10
|
: (appLauncherPanel.isPinned(modelData) ? Theme.surfaceVariant : "transparent")
|
||||||
|
radius: 12
|
||||||
|
border.color: appLauncherPanel.isPinned(modelData)
|
||||||
|
? "transparent"
|
||||||
|
: (hovered || isSelected ? Theme.accentPrimary : "transparent")
|
||||||
|
border.width: appLauncherPanel.isPinned(modelData) ? 0 : (hovered || isSelected ? 2 : 0)
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
property bool iconLoaded: !modelData.isCalculator && !modelData.isClipboard && !modelData.isCommand && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: clipboardImage
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: modelData.type === 'image'
|
||||||
|
source: modelData.data || ""
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
asynchronous: true
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
propagateComposedEvents: true
|
||||||
|
onContainsMouseChanged: {
|
||||||
|
if (containsMouse && modelData.type === 'image') {
|
||||||
|
previewImage.source = modelData.data;
|
||||||
|
previewPanel.visible = true;
|
||||||
|
} else {
|
||||||
|
previewPanel.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMouseXChanged: mouse.accepted = false
|
||||||
|
onMouseYChanged: mouse.accepted = false
|
||||||
|
onClicked: mouse.accepted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: iconImg
|
||||||
|
anchors.fill: parent
|
||||||
|
asynchronous: true
|
||||||
|
source: modelData.isCalculator ? "qrc:/icons/calculate.svg" :
|
||||||
|
modelData.isClipboard ? "qrc:/icons/" + modelData.icon + ".svg" :
|
||||||
|
modelData.isCommand ? "qrc:/icons/" + modelData.icon + ".svg" :
|
||||||
|
Quickshell.iconPath(modelData.icon, "application-x-executable")
|
||||||
|
visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded) && modelData.type !== 'image'
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !modelData.isCalculator && !modelData.isClipboard && !modelData.isCommand && !parent.iconLoaded && modelData.type !== 'image'
|
||||||
|
text: "broken_image"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: Theme.fontSizeHeader
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.name
|
||||||
|
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textPrimary)
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.bold: hovered || isSelected
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) :
|
||||||
|
modelData.isClipboard ? modelData.content :
|
||||||
|
modelData.isCommand ? modelData.content :
|
||||||
|
(modelData.comment || modelData.genericName || "No description available")
|
||||||
|
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textSecondary : Theme.textSecondary)
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeCaption
|
||||||
|
font.italic: !(modelData.comment || modelData.genericName)
|
||||||
|
opacity: modelData.isClipboard ? 0.8 : modelData.isCommand ? 0.9 : ((modelData.comment || modelData.genericName) ? 1.0 : 0.6)
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: (modelData.isClipboard || modelData.isCommand) ? 2 : 1
|
||||||
|
wrapMode: (modelData.isClipboard || modelData.isCommand) ? Text.WordWrap : Text.NoWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: (modelData.isClipboard || modelData.isCommand) ? implicitHeight : contentHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.isCalculator ? "content_copy" : "chevron_right"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: Theme.fontSizeBody
|
||||||
|
color: (hovered || isSelected)
|
||||||
|
? Theme.onAccent
|
||||||
|
: (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textSecondary)
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
Layout.rightMargin: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Item { width: 8; height: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: ripple
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.onAccent
|
||||||
|
opacity: 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
onClicked: {
|
||||||
|
|
||||||
|
if (pinArea.containsMouse) return;
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
appLauncherPanel.togglePin(modelData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ripple.opacity = 0.18;
|
||||||
|
rippleNumberAnimation.start();
|
||||||
|
root.selectedIndex = index;
|
||||||
|
root.activateSelected();
|
||||||
|
}
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onPressed: ripple.opacity = 0.18
|
||||||
|
onReleased: ripple.opacity = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
id: rippleNumberAnimation
|
||||||
|
target: ripple
|
||||||
|
property: "opacity"
|
||||||
|
to: 0.0
|
||||||
|
duration: 320
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: index === appList.count - 1 ? 0 : 0.10
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: 28
|
id: pinArea
|
||||||
height: 28
|
width: 28; height: 28
|
||||||
property bool iconLoaded: !modelData.isCalculator && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error
|
z: 100
|
||||||
Image {
|
anchors.right: parent.right
|
||||||
id: iconImg
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
fillMode: Image.PreserveAspectFit
|
preventStealing: true
|
||||||
smooth: true
|
z: 100
|
||||||
cache: false
|
hoverEnabled: true
|
||||||
asynchronous: true
|
cursorShape: Qt.PointingHandCursor
|
||||||
source: modelData.isCalculator ? "qrc:/icons/calculate.svg" : Quickshell.iconPath(modelData.icon, "application-x-executable")
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
visible: modelData.isCalculator || parent.iconLoaded
|
propagateComposedEvents: false
|
||||||
|
onClicked: {
|
||||||
|
appLauncherPanel.togglePin(modelData);
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: !modelData.isCalculator && !parent.iconLoaded
|
text: "star"
|
||||||
text: "broken_image"
|
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: Theme.fontSizeHeader
|
|
||||||
color: Theme.accentPrimary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 1
|
|
||||||
Text {
|
|
||||||
text: modelData.name
|
|
||||||
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textPrimary)
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.bold: hovered || isSelected
|
color: (parent.MouseArea.containsMouse || hovered || isSelected)
|
||||||
|
? Theme.onAccent
|
||||||
|
: (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textDisabled)
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
elide: Text.ElideRight
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
Text {
|
|
||||||
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : (modelData.comment || modelData.genericName || "No description available")
|
|
||||||
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textSecondary : Theme.textSecondary)
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeCaption
|
|
||||||
font.italic: !(modelData.comment || modelData.genericName)
|
|
||||||
opacity: (modelData.comment || modelData.genericName) ? 1.0 : 0.6
|
|
||||||
elide: Text.ElideRight
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: modelData.isCalculator ? "content_copy" : "chevron_right"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: Theme.fontSizeBody
|
|
||||||
color: (hovered || isSelected)
|
|
||||||
? Theme.onAccent
|
|
||||||
: (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textSecondary)
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
Layout.rightMargin: 8 // Add margin to separate from star
|
|
||||||
}
|
|
||||||
// Add a spacing item between chevron and star
|
|
||||||
Item { width: 8; height: 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: ripple
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Theme.onAccent
|
|
||||||
opacity: 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
onClicked: {
|
|
||||||
// Prevent app launch if click is inside pinArea
|
|
||||||
if (pinArea.containsMouse) return;
|
|
||||||
if (mouse.button === Qt.RightButton) {
|
|
||||||
appLauncherPanel.togglePin(modelData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ripple.opacity = 0.18;
|
|
||||||
rippleNumberAnimation.start();
|
|
||||||
root.selectedIndex = index;
|
|
||||||
root.activateSelected();
|
|
||||||
}
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: ripple.opacity = 0.18
|
|
||||||
onReleased: ripple.opacity = 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
id: rippleNumberAnimation
|
|
||||||
target: ripple
|
|
||||||
property: "opacity"
|
|
||||||
to: 0.0
|
|
||||||
duration: 320
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: index === appList.count - 1 ? 0 : 0.10
|
|
||||||
}
|
|
||||||
// Pin/Unpin button (move to last child for stacking)
|
|
||||||
Item {
|
|
||||||
id: pinArea
|
|
||||||
width: 28; height: 28
|
|
||||||
z: 100 // Ensure above everything else
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
preventStealing: true
|
|
||||||
z: 100
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
propagateComposedEvents: false
|
|
||||||
onClicked: {
|
|
||||||
appLauncherPanel.togglePin(modelData);
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "star"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: (parent.MouseArea.containsMouse || hovered || isSelected)
|
|
||||||
? Theme.onAccent
|
|
||||||
: (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textDisabled)
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Corners {
|
Corners {
|
||||||
id: launcherCornerRight
|
id: launcherCornerRight
|
||||||
position: "bottomleft"
|
position: "bottomleft"
|
||||||
size: 1.1
|
size: 1.1
|
||||||
fillColor: Theme.backgroundPrimary
|
fillColor: Theme.backgroundPrimary
|
||||||
anchors.top: root.top
|
anchors.top: root.top
|
||||||
offsetX: 416
|
offsetX: 416
|
||||||
offsetY: 0
|
offsetY: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Corners {
|
Corners {
|
||||||
id: launcherCornerLeft
|
id: launcherCornerLeft
|
||||||
position: "bottomright"
|
position: "bottomright"
|
||||||
size: 1.1
|
size: 1.1
|
||||||
fillColor: Theme.backgroundPrimary
|
fillColor: Theme.backgroundPrimary
|
||||||
anchors.top: root.top
|
anchors.top: root.top
|
||||||
offsetX: -416
|
offsetX: -416
|
||||||
offsetY: 0
|
offsetY: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,19 +74,40 @@ 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");
|
lines.push("Health: " + Math.round(batteryWidget.battery.healthPercentage) + "%");
|
||||||
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) + "%");
|
|
||||||
}
|
}
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
272
Bar/Modules/Bluetooth.qml
Normal file
272
Bar/Modules/Bluetooth.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,120 +21,456 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
80
Bar/Modules/SettingsButton.qml
Normal file
80
Bar/Modules/SettingsButton.qml
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -14,10 +14,10 @@ Row {
|
||||||
property var trayMenu
|
property var trayMenu
|
||||||
spacing: 8
|
spacing: 8
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
property bool containsMouse: false
|
property bool containsMouse: false
|
||||||
property var systemTray: SystemTray
|
property var systemTray: SystemTray
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: systemTray.items
|
model: systemTray.items
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
|
|
@ -26,7 +26,7 @@ Row {
|
||||||
// Hide Spotify icon, or adjust to your liking
|
// Hide Spotify icon, or adjust to your liking
|
||||||
visible: modelData && modelData.id !== "spotify"
|
visible: modelData && modelData.id !== "spotify"
|
||||||
property bool isHovered: trayMouseArea.containsMouse
|
property bool isHovered: trayMouseArea.containsMouse
|
||||||
|
|
||||||
// Hover scale animation
|
// Hover scale animation
|
||||||
scale: isHovered ? 1.15 : 1.0
|
scale: isHovered ? 1.15 : 1.0
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
|
|
@ -35,7 +35,7 @@ Row {
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subtle rotation on hover
|
// Subtle rotation on hover
|
||||||
rotation: isHovered ? 5 : 0
|
rotation: isHovered ? 5 : 0
|
||||||
Behavior on rotation {
|
Behavior on rotation {
|
||||||
|
|
@ -44,7 +44,7 @@ Row {
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: 16
|
width: 16
|
||||||
|
|
@ -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,72 +81,71 @@ Row {
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: trayMouseArea
|
id: trayMouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
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) {
|
||||||
// Anchor the menu to the tray icon item (parent) and position it below the icon
|
// Anchor the menu to the tray icon item (parent) and position it below the icon
|
||||||
const menuX = (width / 2) - (trayMenu.width / 2);
|
const menuX = (width / 2) - (trayMenu.width / 2);
|
||||||
const menuY = height + 20;
|
const menuY = height + 20;
|
||||||
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
|
||||||
onExited: trayTooltip.tooltipVisible = false
|
onExited: trayTooltip.tooltipVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
370
Bar/Modules/Wifi.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
51
Components/Avatar.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
var pos = targetItem.mapToGlobal(0, targetItem.height);
|
|
||||||
x = pos.x - width / 2 + targetItem.width / 2;
|
if (positionAbove) {
|
||||||
y = pos.y + 12;
|
// 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);
|
||||||
|
x = pos.x - width / 2 + targetItem.width / 2;
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
@ -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!");
|
||||||
|
|
|
||||||
|
|
@ -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
18
Helpers/Time.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R01IX85B)
|
[](https://ko-fi.com/R6R01IX85B)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Special Thanks
|
||||||
|
|
||||||
|
Thank you to everyone who supports me and this project 💜!
|
||||||
|
* Gohma
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
|
||||||
|
|
@ -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
348
Services/Network.qml
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
350
Widgets/Dock.qml
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -328,7 +311,7 @@ WlSessionLock {
|
||||||
position: "bottomright"
|
position: "bottomright"
|
||||||
size: 1.3
|
size: 1.3
|
||||||
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
|
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
|
||||||
offsetX: - Screen.width / 2 - 38
|
offsetX: -Screen.width / 2 - 38
|
||||||
offsetY: 0
|
offsetY: 0
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
visible: Settings.settings.showCorners
|
visible: Settings.settings.showCorners
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,13 +413,11 @@ WlSessionLock {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.margins: 32
|
anchors.margins: 32
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
BatteryCharge {
|
BatteryCharge {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,8 +55,9 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -53,4 +53,4 @@ ShellRoot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
369
Widgets/SettingsWindow/SettingsWindow.qml
Normal file
369
Widgets/SettingsWindow/SettingsWindow.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
405
Widgets/SettingsWindow/Tabs/About.qml
Normal file
405
Widgets/SettingsWindow/Tabs/About.qml
Normal 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
380
Widgets/SettingsWindow/Tabs/Bar.qml
Normal file
380
Widgets/SettingsWindow/Tabs/Bar.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
97
Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml
Normal file
97
Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
354
Widgets/SettingsWindow/Tabs/Display.qml
Normal file
354
Widgets/SettingsWindow/Tabs/Display.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
339
Widgets/SettingsWindow/Tabs/General.qml
Normal file
339
Widgets/SettingsWindow/Tabs/General.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
137
Widgets/SettingsWindow/Tabs/Misc.qml
Normal file
137
Widgets/SettingsWindow/Tabs/Misc.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
193
Widgets/SettingsWindow/Tabs/Network.qml
Normal file
193
Widgets/SettingsWindow/Tabs/Network.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Widgets/SettingsWindow/Tabs/Record.qml
Normal file
19
Widgets/SettingsWindow/Tabs/Record.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
812
Widgets/SettingsWindow/Tabs/Recording.qml
Normal file
812
Widgets/SettingsWindow/Tabs/Recording.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
283
Widgets/SettingsWindow/Tabs/TimeWeather.qml
Normal file
283
Widgets/SettingsWindow/Tabs/TimeWeather.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
|
||||||
anchors.fill: avatarImage
|
|
||||||
source: avatarImage
|
|
||||||
maskSource: Rectangle {
|
|
||||||
width: avatarImage.width
|
|
||||||
height: avatarImage.height
|
|
||||||
radius: avatarImage.width / 2
|
|
||||||
visible: false
|
|
||||||
}
|
|
||||||
visible: Settings.settings.profileImage !== ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Avatar {}
|
||||||
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
|
||||||
|
|
@ -607,4 +640,4 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (!visible) {
|
|
||||||
// Focus will be handled by the individual components
|
|
||||||
if (typeof weather !== 'undefined' && weather !== null && weather.fetchCityWeather) {
|
|
||||||
weather.fetchCityWeather();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// Refresh weather data when hidden
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible && typeof weather !== 'undefined' && weather !== null && weather.fetchCityWeather) {
|
||||||
|
weather.fetchCityWeather();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,25 +16,28 @@ Rectangle {
|
||||||
anchors.margins: 18
|
anchors.margins: 18
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
// Header
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 12
|
|
||||||
Text {
|
|
||||||
text: "image"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: 20
|
|
||||||
color: Theme.accentPrimary
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: "Wallpaper Settings"
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 16
|
|
||||||
font.bold: true
|
|
||||||
color: Theme.textPrimary
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "image"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 20
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Wallpaper Settings"
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 16
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize Mode 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,14 +297,16 @@ Rectangle {
|
||||||
border.color: Qt.lighter(Theme.accentPrimary, 1.3)
|
border.color: Qt.lighter(Theme.accentPrimary, 1.3)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
||||||
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width/2))
|
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2))
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
visible: MusicManager.trackLength > 0
|
visible: MusicManager.trackLength > 0
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,15 +318,15 @@ Rectangle {
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -318,4 +421,4 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,15 +59,15 @@ 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) {
|
||||||
|
sidebarPopupRect.wifiPanelModal.visible = false;
|
||||||
|
}
|
||||||
|
if (sidebarPopupRect.bluetoothPanelModal && sidebarPopupRect.bluetoothPanelModal.visible) {
|
||||||
|
sidebarPopupRect.bluetoothPanelModal.visible = false;
|
||||||
}
|
}
|
||||||
if (sidebarPopupRect.wifiPanelModal && sidebarPopupRect.wifiPanelModal.visible) {
|
|
||||||
sidebarPopupRect.wifiPanelModal.visible = false;
|
|
||||||
}
|
|
||||||
if (sidebarPopupRect.bluetoothPanelModal && sidebarPopupRect.bluetoothPanelModal.visible) {
|
|
||||||
sidebarPopupRect.bluetoothPanelModal.visible = false;
|
|
||||||
}
|
|
||||||
if (sidebarPopup.visible) {
|
if (sidebarPopup.visible) {
|
||||||
slideAnim.from = 0;
|
slideAnim.from = 0;
|
||||||
slideAnim.to = width;
|
slideAnim.to = width;
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -410,7 +380,7 @@ Rectangle {
|
||||||
running: false
|
running: false
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: logoutProcessNiri
|
id: logoutProcessNiri
|
||||||
command: ["niri", "msg", "action", "quit", "--skip-confirmation"]
|
command: ["niri", "msg", "action", "quit", "--skip-confirmation"]
|
||||||
running: false
|
running: false
|
||||||
|
|
@ -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,8 +446,8 @@ Rectangle {
|
||||||
uptimeProcess.running = true;
|
uptimeProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add lockscreen instance (hidden by default)
|
|
||||||
LockScreen {
|
LockScreen {
|
||||||
id: lockScreen
|
id: lockScreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -11,72 +11,128 @@ import qs.Helpers
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
property alias panel: wifiPanelModal
|
property alias panel: wifiPanelModal
|
||||||
|
|
||||||
function showAt() {
|
function showAt() {
|
||||||
wifiPanelModal.visible = true;
|
wifiPanelModal.visible = true;
|
||||||
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 (inUse) {
|
||||||
if (nets[j].ssid === ssid) {
|
existingNet.connected = true;
|
||||||
// Update connection status if this entry is connected
|
}
|
||||||
if (inUse) {
|
if (signal > existingNet.signal) {
|
||||||
nets[j].connected = true;
|
existingNet.signal = signal;
|
||||||
}
|
existingNet.security = security;
|
||||||
// Update signal if this entry has better signal
|
|
||||||
if (signal > nets[j].signal) {
|
|
||||||
nets[j].signal = signal;
|
|
||||||
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.ssid = params.ssid;
|
|
||||||
connectProcess.password = params.password;
|
|
||||||
connectProcess.running = true;
|
|
||||||
wifiLogic.pendingConnect = null;
|
|
||||||
}
|
}
|
||||||
|
connectProcess.security = params.security;
|
||||||
|
connectProcess.ssid = params.ssid;
|
||||||
|
connectProcess.password = params.password;
|
||||||
|
connectProcess.running = true;
|
||||||
|
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) {
|
stderr: StdioCollector {
|
||||||
foundProfile = lines[i];
|
onStreamFinished: {
|
||||||
break;
|
if (text.trim() !== "" && !text.toLowerCase().includes("warning")) {
|
||||||
}
|
console.error("Error renaming connection:", text);
|
||||||
}
|
|
||||||
if (foundProfile) {
|
|
||||||
// Profile exists, just bring it up (no password prompt)
|
|
||||||
upConnectionProcess.profileName = foundProfile;
|
|
||||||
upConnectionProcess.running = true;
|
|
||||||
} 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
|
||||||
|
|
@ -554,28 +686,29 @@ Item {
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
Item {
|
Item {
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
Layout.preferredHeight: 22
|
Layout.preferredHeight: 22
|
||||||
Layout.preferredWidth: 22
|
Layout.preferredWidth: 22
|
||||||
Spinner {
|
Spinner {
|
||||||
visible: wifiLogic.connectingSsid === modelData.ssid
|
visible: wifiLogic.connectingSsid === modelData.ssid
|
||||||
running: wifiLogic.connectingSsid === modelData.ssid
|
running: wifiLogic.connectingSsid === modelData.ssid
|
||||||
color: Theme.accentPrimary
|
color: Theme.accentPrimary
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: 22
|
size: 22
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: networkMouseArea
|
id: networkMouseArea
|
||||||
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
0
qmlls.ini
Normal file
55
shell.qml
55
shell.qml
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationPopup {
|
// Create notification popups for each selected monitor
|
||||||
id: notificationPopup
|
Variants {
|
||||||
barVisible: bar.visible
|
id: notificationPopupVariants
|
||||||
|
model: Quickshell.screens
|
||||||
|
|
||||||
|
NotificationPopup {
|
||||||
|
property var modelData
|
||||||
|
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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue