Settings: all tabed - screenrecorder will be a plugin soon

This commit is contained in:
Lemmy
2026-01-05 21:01:29 -05:00
parent ffa8cc58a1
commit a0ac019b16
57 changed files with 2740 additions and 2578 deletions
+40 -5
View File
@@ -995,6 +995,10 @@
}
},
"support": "Support us",
"tabs": {
"contributors": "Contributors",
"version": "Version"
},
"title": "About"
},
"audio": {
@@ -1063,9 +1067,20 @@
"tabs": {
"devices": "Devices",
"media": "Media",
"visualizer": "Visualizer",
"volumes": "Volumes"
},
"title": "Audio",
"visualizer": {
"frame-rate": {
"description": "Higher rates are smoother but use more resources.",
"label": "Frame rate"
},
"type": {
"description": "Choose a visualization type for media playback.",
"label": "Visualization type"
}
},
"volumes": {
"input-volume": {
"description": "Microphone input volume level.",
@@ -2191,9 +2206,10 @@
}
},
"tabs": {
"general": "General",
"general": "Appearance",
"duration": "Duration",
"history": "History",
"sounds": "Sounds",
"sound": "Sound",
"toast": "Toast"
},
"title": "Notifications",
@@ -2242,12 +2258,22 @@
"label": "Monitors display"
}
},
"section": {
"general": {
"tabs": {
"general": "General",
"events": "Events"
},
"general": {
"section": {
"description": "Configure visibility and behavior of OSD.",
"label": "General"
}
},
"events": {
"section": {
"description": "Select which events trigger the on-screen display.",
"label": "Events"
}
},
"title": "On-Screen Display",
"types": {
"brightness": {
@@ -2342,6 +2368,11 @@
"tooltip": "Remove plugin source"
}
},
"tabs": {
"available": "Available",
"installed": "Installed",
"sources": "Sources"
},
"title": "Plugins",
"uninstall": "Uninstall",
"uninstall-dialog": {
@@ -2499,6 +2530,10 @@
"description": "Display number labels (1-2-3-4...) on buttons and enable number keybinds for quick selection.",
"label": "Show number labels"
},
"tabs": {
"general": "General",
"actions": "Actions"
},
"title": "Session Menu"
},
"system-monitor": {
@@ -2765,7 +2800,7 @@
},
"tabs": {
"automation": "Automation",
"look-feel": "Look & feel",
"look": "Look",
"general": "General"
},
"title": "Wallpaper"
@@ -4,6 +4,7 @@ import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Panels.Settings.Tabs
import qs.Modules.Panels.Settings.Tabs.About
import qs.Modules.Panels.Settings.Tabs.Audio
import qs.Modules.Panels.Settings.Tabs.Bar
import qs.Modules.Panels.Settings.Tabs.ColorScheme
@@ -12,6 +13,8 @@ import qs.Modules.Panels.Settings.Tabs.Display
import qs.Modules.Panels.Settings.Tabs.Dock
import qs.Modules.Panels.Settings.Tabs.Launcher
import qs.Modules.Panels.Settings.Tabs.Notifications
import qs.Modules.Panels.Settings.Tabs.Osd
import qs.Modules.Panels.Settings.Tabs.Plugins
import qs.Modules.Panels.Settings.Tabs.Region
import qs.Modules.Panels.Settings.Tabs.SessionMenu
import qs.Modules.Panels.Settings.Tabs.SystemMonitor
+1 -1
View File
@@ -10,7 +10,7 @@ import qs.Widgets
SmartPanel {
id: root
preferredWidth: Math.round(820 * Style.uiScaleRatio)
preferredWidth: Math.round(840 * Style.uiScaleRatio)
preferredHeight: Math.round(910 * Style.uiScaleRatio)
// Settings panel mode: "centered", "attached", "window"
@@ -11,8 +11,8 @@ FloatingWindow {
id: root
title: "Noctalia"
minimumSize: Qt.size(820 * Style.uiScaleRatio, 910 * Style.uiScaleRatio)
implicitWidth: Math.round(820 * Style.uiScaleRatio)
minimumSize: Qt.size(840 * Style.uiScaleRatio, 910 * Style.uiScaleRatio)
implicitWidth: Math.round(840 * Style.uiScaleRatio)
implicitHeight: Math.round(910 * Style.uiScaleRatio)
color: Color.mSurface
@@ -0,0 +1,41 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.about.tabs.version")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.about.tabs.contributors")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
VersionSubTab {}
ContributorsSubTab {}
}
}
@@ -0,0 +1,257 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Noctalia
import qs.Widgets
ColumnLayout {
id: root
property var contributors: GitHubService.contributors
property int avatarCacheVersion: 0
readonly property int topContributorsCount: 20
Connections {
target: GitHubService
function onCachedAvatarsChanged() {
root.avatarCacheVersion++;
}
}
spacing: Style.marginL
NHeader {
description: root.contributors.length === 1 ? I18n.tr("settings.about.contributors.section.description", {
"count": root.contributors.length
}) : I18n.tr("settings.about.contributors.section.description_plural", {
"count": root.contributors.length
})
enableDescriptionRichText: true
}
// Top 20 contributors with full cards (avoids GridView shader crashes on Qt 6.8)
Flow {
id: topContributorsFlow
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
spacing: Style.marginM
Repeater {
model: Math.min(root.contributors.length, root.topContributorsCount)
delegate: Rectangle {
width: Math.max(Math.round(topContributorsFlow.width / 2 - Style.marginM - 1), Math.round(Style.baseWidgetSize * 4))
height: Math.round(Style.baseWidgetSize * 2.3)
radius: Style.radiusM
color: contributorArea.containsMouse ? Color.mHover : "transparent"
border.width: 1
border.color: contributorArea.containsMouse ? Color.mPrimary : Color.mOutline
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
// Avatar container with rectangular design (modern, no shader issues)
Item {
id: wrapper
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Style.baseWidgetSize * 1.8
Layout.preferredHeight: Style.baseWidgetSize * 1.8
property bool isRounded: false
// Background and image container
Item {
anchors.fill: parent
// Simple circular image (pre-rendered, no shaders)
Image {
anchors.fill: parent
source: {
// Depend on avatarCacheVersion to trigger re-evaluation
var _ = root.avatarCacheVersion;
// Try cached circular version first
var username = root.contributors[index].login;
var cached = GitHubService.getAvatarPath(username);
if (cached) {
wrapper.isRounded = true;
return cached;
}
// Fall back to original avatar URL
return root.contributors[index].avatar_url || "";
}
fillMode: Image.PreserveAspectFit // Fit since image is already circular with transparency
mipmap: true
smooth: true
asynchronous: true
visible: root.contributors[index].avatar_url !== undefined && root.contributors[index].avatar_url !== ""
opacity: status === Image.Ready ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
// Fallback icon
NIcon {
anchors.centerIn: parent
visible: !root.contributors[index].avatar_url || root.contributors[index].avatar_url === ""
icon: "person"
pointSize: Style.fontSizeL
color: Color.mPrimary
}
}
Rectangle {
visible: wrapper.isRounded
anchors.fill: parent
color: "transparent"
radius: width * 0.5
border.width: Style.borderM
border.color: Color.mPrimary
}
}
// Info column
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
NText {
text: root.contributors[index].login || "Unknown"
font.weight: Style.fontWeightBold
color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
pointSize: Style.fontSizeS
}
RowLayout {
spacing: Style.marginXS
Layout.fillWidth: true
NIcon {
icon: "git-commit"
pointSize: Style.fontSizeXS
color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant
}
NText {
text: `${(root.contributors[index].contributions || 0).toString()} commits`
pointSize: Style.fontSizeXS
color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant
}
}
}
// Hover indicator
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: "arrow-right"
pointSize: Style.fontSizeS
color: Color.mPrimary
opacity: contributorArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
MouseArea {
id: contributorArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.contributors[index].html_url)
Quickshell.execDetached(["xdg-open", root.contributors[index].html_url]);
}
}
}
}
}
// Remaining contributors (simple text links)
Flow {
id: remainingContributorsFlow
visible: root.contributors.length > root.topContributorsCount
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.topMargin: Style.marginL
spacing: Style.marginS
Repeater {
model: Math.max(0, root.contributors.length - root.topContributorsCount)
delegate: Rectangle {
width: nameText.implicitWidth + Style.marginM * 2
height: nameText.implicitHeight + Style.marginS * 2
radius: Style.radiusS
color: nameArea.containsMouse ? Color.mHover : "transparent"
border.width: Style.borderS
border.color: nameArea.containsMouse ? Color.mPrimary : Color.mOutline
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
NText {
id: nameText
anchors.centerIn: parent
text: root.contributors[index + root.topContributorsCount].login || "Unknown"
pointSize: Style.fontSizeXS
color: nameArea.containsMouse ? Color.mOnHover : Color.mOnSurface
font.weight: Style.fontWeightMedium
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
MouseArea {
id: nameArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.contributors[index + root.topContributorsCount].html_url)
Quickshell.execDetached(["xdg-open", root.contributors[index + root.topContributorsCount].html_url]);
}
}
}
}
}
}
@@ -0,0 +1,248 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Noctalia
import qs.Services.System
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
property string latestVersion: GitHubService.latestVersion
property string currentVersion: UpdateService.currentVersion
property string commitInfo: ""
readonly property bool isGitVersion: root.currentVersion.endsWith("-git")
spacing: Style.marginL
Component.onCompleted: {
Logger.d("VersionSubTab", "Current version:", root.currentVersion);
Logger.d("VersionSubTab", "Is git version:", root.isGitVersion);
// Only fetch commit info for -git versions
if (root.isGitVersion) {
// On NixOS, extract commit hash from the store path first
if (HostService.isNixOS) {
var shellDir = Quickshell.shellDir || "";
Logger.d("VersionSubTab", "Component.onCompleted - NixOS detected, shellDir:", shellDir);
if (shellDir) {
// Extract commit hash from path like: /nix/store/...-noctalia-shell-2025-11-30_225e6d3/share/noctalia-shell
// Pattern matches: noctalia-shell-YYYY-MM-DD_<commit_hash>
var match = shellDir.match(/noctalia-shell-\d{4}-\d{2}-\d{2}_([0-9a-f]{7,})/i);
if (match && match[1]) {
// Use first 7 characters of the commit hash
root.commitInfo = match[1].substring(0, 7);
Logger.d("VersionSubTab", "Component.onCompleted - Extracted commit from NixOS path:", root.commitInfo);
return;
} else {
Logger.d("VersionSubTab", "Component.onCompleted - Could not extract commit from NixOS path, trying fallback");
}
}
fetchGitCommit();
return;
} else {
// On non-NixOS systems, check for pacman first.
whichPacmanProcess.running = true;
return;
}
}
}
Timer {
id: gitFallbackTimer
interval: 500
running: false
onTriggered: {
if (!root.commitInfo) {
fetchGitCommit();
}
}
}
Process {
id: whichPacmanProcess
command: ["sh", "-c", "command -v pacman"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Logger.d("VersionSubTab", "whichPacmanProcess - pacman found, starting query");
pacmanProcess.running = true;
gitFallbackTimer.start();
} else {
Logger.d("VersionSubTab", "whichPacmanProcess - pacman not found, falling back to git");
fetchGitCommit();
}
}
}
Process {
id: pacmanProcess
command: ["pacman", "-Q", "noctalia-shell-git"]
running: false
onStarted: {
gitFallbackTimer.stop();
}
onExited: function (exitCode) {
gitFallbackTimer.stop();
Logger.d("VersionSubTab", "pacmanProcess - Process exited with code:", exitCode);
if (exitCode === 0) {
var output = stdout.text.trim();
Logger.d("VersionSubTab", "pacmanProcess - Output:", output);
var match = output.match(/noctalia-shell-git\s+(.+)/);
if (match && match[1]) {
// For Arch packages, the version format might be like: 3.4.0.r112.g3f00bec8-1
// Extract just the commit hash part if it exists
var version = match[1];
var commitMatch = version.match(/\.g([0-9a-f]{7,})/i);
if (commitMatch && commitMatch[1]) {
// Show short hash (first 7 characters)
root.commitInfo = commitMatch[1].substring(0, 7);
Logger.d("VersionSubTab", "pacmanProcess - Set commitInfo from Arch package:", root.commitInfo);
return; // Successfully got commit hash from Arch package
} else {
// If no commit hash in version format, still try git repo
Logger.d("VersionSubTab", "pacmanProcess - No commit hash in version, trying git");
fetchGitCommit();
}
} else {
// Unexpected output format, try git
Logger.d("VersionSubTab", "pacmanProcess - Unexpected output format, trying git");
fetchGitCommit();
}
} else {
// If not on Arch, try to get git commit from repository
Logger.d("VersionSubTab", "pacmanProcess - Package not found, trying git");
fetchGitCommit();
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
function fetchGitCommit() {
var shellDir = Quickshell.shellDir || "";
Logger.d("VersionSubTab", "fetchGitCommit - shellDir:", shellDir);
if (!shellDir) {
Logger.d("VersionSubTab", "fetchGitCommit - Cannot determine shell directory, skipping git commit fetch");
return;
}
gitProcess.workingDirectory = shellDir;
gitProcess.running = true;
}
Process {
id: gitProcess
command: ["git", "rev-parse", "--short", "HEAD"]
running: false
onExited: function (exitCode) {
Logger.d("VersionSubTab", "gitProcess - Process exited with code:", exitCode);
if (exitCode === 0) {
var gitOutput = stdout.text.trim();
Logger.d("VersionSubTab", "gitProcess - gitOutput:", gitOutput);
if (gitOutput) {
root.commitInfo = gitOutput;
Logger.d("VersionSubTab", "gitProcess - Set commitInfo to:", root.commitInfo);
}
} else {
Logger.d("VersionSubTab", "gitProcess - Git command failed. Exit code:", exitCode);
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
NHeader {
label: I18n.tr("settings.about.noctalia.section.label")
description: I18n.tr("settings.about.noctalia.section.description")
}
RowLayout {
spacing: Style.marginXL
// Versions
GridLayout {
columns: 2
rowSpacing: Style.marginXS
columnSpacing: Style.marginS
NText {
text: I18n.tr("settings.about.noctalia.latest-version")
color: Color.mOnSurface
}
NText {
text: root.latestVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
NText {
text: I18n.tr("settings.about.noctalia.installed-version")
color: Color.mOnSurface
}
NText {
text: root.currentVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
NText {
visible: root.isGitVersion
text: I18n.tr("settings.about.noctalia.git-commit")
color: Color.mOnSurface
}
NText {
visible: root.isGitVersion
text: root.commitInfo || I18n.tr("settings.about.noctalia.git-commit-loading")
color: Color.mOnSurface
font.weight: Style.fontWeightBold
font.family: root.commitInfo ? "monospace" : ""
pointSize: Style.fontSizeXS
}
}
}
// Action buttons row
RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
spacing: Style.marginM
NButton {
icon: "sparkles"
text: I18n.tr("settings.about.changelog")
fontSize: Style.fontSizeXS
iconSize: Style.fontSizeS
outlined: true
onClicked: {
var screen = PanelService.openedPanel?.screen || Quickshell.screens[0];
UpdateService.viewChangelog(screen);
}
}
NButton {
icon: "heart"
text: I18n.tr("settings.about.support")
fontSize: Style.fontSizeXS
iconSize: Style.fontSizeS
outlined: true
onClicked: {
Quickshell.execDetached(["xdg-open", "https://ko-fi.com/lysec"]);
ToastService.showNotice(I18n.tr("settings.about.support"), I18n.tr("toast.kofi.opened"));
}
}
}
}
-530
View File
@@ -1,530 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Noctalia
import qs.Services.System
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
property string latestVersion: GitHubService.latestVersion
property string currentVersion: UpdateService.currentVersion
property var contributors: GitHubService.contributors
property string commitInfo: ""
property int avatarCacheVersion: 0
readonly property int topContributorsCount: 20
Connections {
target: GitHubService
function onCachedAvatarsChanged() {
root.avatarCacheVersion++;
}
}
readonly property bool isGitVersion: root.currentVersion.endsWith("-git")
spacing: Style.marginL
Component.onCompleted: {
Logger.d("AboutTab", "Current version:", root.currentVersion);
Logger.d("AboutTab", "Is git version:", root.isGitVersion);
// Only fetch commit info for -git versions
if (root.isGitVersion) {
// On NixOS, extract commit hash from the store path first
if (HostService.isNixOS) {
var shellDir = Quickshell.shellDir || "";
Logger.d("AboutTab", "Component.onCompleted - NixOS detected, shellDir:", shellDir);
if (shellDir) {
// Extract commit hash from path like: /nix/store/...-noctalia-shell-2025-11-30_225e6d3/share/noctalia-shell
// Pattern matches: noctalia-shell-YYYY-MM-DD_<commit_hash>
var match = shellDir.match(/noctalia-shell-\d{4}-\d{2}-\d{2}_([0-9a-f]{7,})/i);
if (match && match[1]) {
// Use first 7 characters of the commit hash
root.commitInfo = match[1].substring(0, 7);
Logger.d("AboutTab", "Component.onCompleted - Extracted commit from NixOS path:", root.commitInfo);
return;
} else {
Logger.d("AboutTab", "Component.onCompleted - Could not extract commit from NixOS path, trying fallback");
}
}
fetchGitCommit();
return;
} else {
// On non-NixOS systems, check for pacman first.
whichPacmanProcess.running = true;
return;
}
}
}
Timer {
id: gitFallbackTimer
interval: 500
running: false
onTriggered: {
if (!root.commitInfo) {
fetchGitCommit();
}
}
}
Process {
id: whichPacmanProcess
command: ["sh", "-c", "command -v pacman"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Logger.d("AboutTab", "whichPacmanProcess - pacman found, starting query");
pacmanProcess.running = true;
gitFallbackTimer.start();
} else {
Logger.d("AboutTab", "whichPacmanProcess - pacman not found, falling back to git");
fetchGitCommit();
}
}
}
Process {
id: pacmanProcess
command: ["pacman", "-Q", "noctalia-shell-git"]
running: false
onStarted: {
gitFallbackTimer.stop();
}
onExited: function (exitCode) {
gitFallbackTimer.stop();
Logger.d("AboutTab", "pacmanProcess - Process exited with code:", exitCode);
if (exitCode === 0) {
var output = stdout.text.trim();
Logger.d("AboutTab", "pacmanProcess - Output:", output);
var match = output.match(/noctalia-shell-git\s+(.+)/);
if (match && match[1]) {
// For Arch packages, the version format might be like: 3.4.0.r112.g3f00bec8-1
// Extract just the commit hash part if it exists
var version = match[1];
var commitMatch = version.match(/\.g([0-9a-f]{7,})/i);
if (commitMatch && commitMatch[1]) {
// Show short hash (first 7 characters)
root.commitInfo = commitMatch[1].substring(0, 7);
Logger.d("AboutTab", "pacmanProcess - Set commitInfo from Arch package:", root.commitInfo);
return; // Successfully got commit hash from Arch package
} else {
// If no commit hash in version format, still try git repo
Logger.d("AboutTab", "pacmanProcess - No commit hash in version, trying git");
fetchGitCommit();
}
} else {
// Unexpected output format, try git
Logger.d("AboutTab", "pacmanProcess - Unexpected output format, trying git");
fetchGitCommit();
}
} else {
// If not on Arch, try to get git commit from repository
Logger.d("AboutTab", "pacmanProcess - Package not found, trying git");
fetchGitCommit();
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
function fetchGitCommit() {
var shellDir = Quickshell.shellDir || "";
Logger.d("AboutTab", "fetchGitCommit - shellDir:", shellDir);
if (!shellDir) {
Logger.d("AboutTab", "fetchGitCommit - Cannot determine shell directory, skipping git commit fetch");
return;
}
gitProcess.workingDirectory = shellDir;
gitProcess.running = true;
}
Process {
id: gitProcess
command: ["git", "rev-parse", "--short", "HEAD"]
running: false
onExited: function (exitCode) {
Logger.d("AboutTab", "gitProcess - Process exited with code:", exitCode);
if (exitCode === 0) {
var gitOutput = stdout.text.trim();
Logger.d("AboutTab", "gitProcess - gitOutput:", gitOutput);
if (gitOutput) {
root.commitInfo = gitOutput;
Logger.d("AboutTab", "gitProcess - Set commitInfo to:", root.commitInfo);
}
} else {
Logger.d("AboutTab", "gitProcess - Git command failed. Exit code:", exitCode);
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
NHeader {
label: I18n.tr("settings.about.noctalia.section.label")
description: I18n.tr("settings.about.noctalia.section.description")
}
RowLayout {
spacing: Style.marginXL
// Versions
GridLayout {
columns: 2
rowSpacing: Style.marginXS
columnSpacing: Style.marginS
NText {
text: I18n.tr("settings.about.noctalia.latest-version")
color: Color.mOnSurface
}
NText {
text: root.latestVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
NText {
text: I18n.tr("settings.about.noctalia.installed-version")
color: Color.mOnSurface
}
NText {
text: root.currentVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
NText {
visible: root.isGitVersion
text: I18n.tr("settings.about.noctalia.git-commit")
color: Color.mOnSurface
}
NText {
visible: root.isGitVersion
text: root.commitInfo || I18n.tr("settings.about.noctalia.git-commit-loading")
color: Color.mOnSurface
font.weight: Style.fontWeightBold
font.family: root.commitInfo ? "monospace" : ""
pointSize: Style.fontSizeXS
}
}
}
// Action buttons row
RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
spacing: Style.marginM
NButton {
icon: "sparkles"
text: I18n.tr("settings.about.changelog")
fontSize: Style.fontSizeXS
iconSize: Style.fontSizeS
outlined: true
onClicked: {
var screen = PanelService.openedPanel?.screen || Quickshell.screens[0];
UpdateService.viewChangelog(screen);
}
}
NButton {
visible: !HostService.isNixOS
icon: "wand"
text: I18n.tr("settings.general.launch-setup-wizard")
fontSize: Style.fontSizeXS
iconSize: Style.fontSizeS
outlined: true
onClicked: {
var targetScreen = PanelService.openedPanel ? PanelService.openedPanel.screen : (Quickshell.screens.length > 0 ? Quickshell.screens[0] : null);
if (!targetScreen) {
return;
}
var setupPanel = PanelService.getPanel("setupWizardPanel", targetScreen);
if (setupPanel) {
setupPanel.open();
} else {
Qt.callLater(() => {
var sp = PanelService.getPanel("setupWizardPanel", targetScreen);
if (sp)
sp.open();
});
}
}
}
NButton {
icon: "heart"
text: I18n.tr("settings.about.support")
fontSize: Style.fontSizeXS
iconSize: Style.fontSizeS
outlined: true
onClicked: {
Quickshell.execDetached(["xdg-open", "https://ko-fi.com/lysec"]);
ToastService.showNotice(I18n.tr("settings.about.support"), I18n.tr("toast.kofi.opened"));
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXXXL
Layout.bottomMargin: Style.marginL
}
// Contributors
NHeader {
label: I18n.tr("settings.about.contributors.section.label")
description: root.contributors.length === 1 ? I18n.tr("settings.about.contributors.section.description", {
"count": root.contributors.length
}) : I18n.tr("settings.about.contributors.section.description_plural", {
"count": root.contributors.length
})
enableDescriptionRichText: true
}
// Top 20 contributors with full cards (avoids GridView shader crashes on Qt 6.8)
Flow {
id: topContributorsFlow
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
spacing: Style.marginM
Repeater {
model: Math.min(root.contributors.length, root.topContributorsCount)
delegate: Rectangle {
width: Math.max(Math.round(topContributorsFlow.width / 2 - Style.marginM - 1), Math.round(Style.baseWidgetSize * 4))
height: Math.round(Style.baseWidgetSize * 2.3)
radius: Style.radiusM
color: contributorArea.containsMouse ? Color.mHover : "transparent"
border.width: 1
border.color: contributorArea.containsMouse ? Color.mPrimary : Color.mOutline
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
// Avatar container with rectangular design (modern, no shader issues)
Item {
id: wrapper
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Style.baseWidgetSize * 1.8
Layout.preferredHeight: Style.baseWidgetSize * 1.8
property bool isRounded: false
// Background and image container
Item {
anchors.fill: parent
// Simple circular image (pre-rendered, no shaders)
Image {
anchors.fill: parent
source: {
// Depend on avatarCacheVersion to trigger re-evaluation
var _ = root.avatarCacheVersion;
// Try cached circular version first
var username = root.contributors[index].login;
var cached = GitHubService.getAvatarPath(username);
if (cached) {
wrapper.isRounded = true;
return cached;
}
// Fall back to original avatar URL
return root.contributors[index].avatar_url || "";
}
fillMode: Image.PreserveAspectFit // Fit since image is already circular with transparency
mipmap: true
smooth: true
asynchronous: true
visible: root.contributors[index].avatar_url !== undefined && root.contributors[index].avatar_url !== ""
opacity: status === Image.Ready ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
// Fallback icon
NIcon {
anchors.centerIn: parent
visible: !root.contributors[index].avatar_url || root.contributors[index].avatar_url === ""
icon: "person"
pointSize: Style.fontSizeL
color: Color.mPrimary
}
}
Rectangle {
visible: wrapper.isRounded
anchors.fill: parent
color: "transparent"
radius: width * 0.5
border.width: Style.borderM
border.color: Color.mPrimary
}
}
// Info column
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
NText {
text: root.contributors[index].login || "Unknown"
font.weight: Style.fontWeightBold
color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
pointSize: Style.fontSizeS
}
RowLayout {
spacing: Style.marginXS
Layout.fillWidth: true
NIcon {
icon: "git-commit"
pointSize: Style.fontSizeXS
color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant
}
NText {
text: `${(root.contributors[index].contributions || 0).toString()} commits`
pointSize: Style.fontSizeXS
color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant
}
}
}
// Hover indicator
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: "arrow-right"
pointSize: Style.fontSizeS
color: Color.mPrimary
opacity: contributorArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
MouseArea {
id: contributorArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.contributors[index].html_url)
Quickshell.execDetached(["xdg-open", root.contributors[index].html_url]);
}
}
}
}
}
// Remaining contributors (simple text links)
Flow {
id: remainingContributorsFlow
visible: root.contributors.length > root.topContributorsCount
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.topMargin: Style.marginL
spacing: Style.marginS
Repeater {
model: Math.max(0, root.contributors.length - root.topContributorsCount)
delegate: Rectangle {
width: nameText.implicitWidth + Style.marginM * 2
height: nameText.implicitHeight + Style.marginS * 2
radius: Style.radiusS
color: nameArea.containsMouse ? Color.mHover : "transparent"
border.width: Style.borderS
border.color: nameArea.containsMouse ? Color.mPrimary : Color.mOutline
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
NText {
id: nameText
anchors.centerIn: parent
text: root.contributors[index + root.topContributorsCount].login || "Unknown"
pointSize: Style.fontSizeXS
color: nameArea.containsMouse ? Color.mOnHover : Color.mOnSurface
font.weight: Style.fontWeightMedium
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
MouseArea {
id: nameArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.contributors[index + root.topContributorsCount].html_url)
Quickshell.execDetached(["xdg-open", root.contributors[index + root.topContributorsCount].html_url]);
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
}
@@ -29,6 +29,11 @@ ColumnLayout {
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
NTabButton {
text: I18n.tr("settings.audio.tabs.visualizer")
tabIndex: 3
checked: subTabBar.currentIndex === 3
}
}
Item {
@@ -43,5 +48,6 @@ ColumnLayout {
VolumesSubTab {}
DevicesSubTab {}
MediaSubTab {}
VisualizerSubTab {}
}
}
@@ -11,11 +11,6 @@ ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.audio.devices.section.label")
description: I18n.tr("settings.audio.devices.section.description")
}
// Output Devices
ButtonGroup {
id: sinks
@@ -10,11 +10,6 @@ ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.audio.media.section.label")
description: I18n.tr("settings.audio.media.section.description")
}
// Preferred player
NTextInput {
label: I18n.tr("settings.audio.media.primary-player.label")
@@ -106,83 +101,4 @@ ColumnLayout {
}
}
}
// Audio Visualizer section
NComboBox {
label: I18n.tr("settings.audio.media.visualizer-type.label")
description: I18n.tr("settings.audio.media.visualizer-type.description")
model: [
{
"key": "none",
"name": I18n.tr("options.visualizer-types.none")
},
{
"key": "linear",
"name": I18n.tr("options.visualizer-types.linear")
},
{
"key": "mirrored",
"name": I18n.tr("options.visualizer-types.mirrored")
},
{
"key": "wave",
"name": I18n.tr("options.visualizer-types.wave")
}
]
currentKey: Settings.data.audio.visualizerType
defaultValue: Settings.getDefaultValue("audio.visualizerType")
onSelected: key => Settings.data.audio.visualizerType = key
}
NComboBox {
label: I18n.tr("settings.audio.media.frame-rate.label")
description: I18n.tr("settings.audio.media.frame-rate.description")
model: [
{
"key": "30",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "30"
})
},
{
"key": "60",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "60"
})
},
{
"key": "100",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "100"
})
},
{
"key": "120",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "120"
})
},
{
"key": "144",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "144"
})
},
{
"key": "165",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "165"
})
},
{
"key": "240",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "240"
})
}
]
currentKey: Settings.data.audio.cavaFrameRate
defaultValue: Settings.getDefaultValue("audio.cavaFrameRate")
onSelected: key => Settings.data.audio.cavaFrameRate = key
}
}
@@ -0,0 +1,89 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NComboBox {
label: I18n.tr("settings.audio.visualizer.type.label")
description: I18n.tr("settings.audio.visualizer.type.description")
model: [
{
"key": "none",
"name": I18n.tr("options.visualizer-types.none")
},
{
"key": "linear",
"name": I18n.tr("options.visualizer-types.linear")
},
{
"key": "mirrored",
"name": I18n.tr("options.visualizer-types.mirrored")
},
{
"key": "wave",
"name": I18n.tr("options.visualizer-types.wave")
}
]
currentKey: Settings.data.audio.visualizerType
defaultValue: Settings.getDefaultValue("audio.visualizerType")
onSelected: key => Settings.data.audio.visualizerType = key
}
NComboBox {
label: I18n.tr("settings.audio.visualizer.frame-rate.label")
description: I18n.tr("settings.audio.visualizer.frame-rate.description")
model: [
{
"key": "30",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "30"
})
},
{
"key": "60",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "60"
})
},
{
"key": "100",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "100"
})
},
{
"key": "120",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "120"
})
},
{
"key": "144",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "144"
})
},
{
"key": "165",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "165"
})
},
{
"key": "240",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "240"
})
}
]
currentKey: Settings.data.audio.cavaFrameRate
defaultValue: Settings.getDefaultValue("audio.cavaFrameRate")
onSelected: key => Settings.data.audio.cavaFrameRate = key
}
}
@@ -29,11 +29,6 @@ ColumnLayout {
}
}
NHeader {
label: I18n.tr("settings.audio.volumes.section.label")
description: I18n.tr("settings.audio.volumes.section.description")
}
// Master Volume
ColumnLayout {
spacing: Style.marginXXS
@@ -147,6 +142,10 @@ ColumnLayout {
}
}
NDivider {
Layout.fillWidth: true
}
// External mixer command
NTextInput {
label: I18n.tr("settings.audio.external-mixer.label")
@@ -262,8 +262,6 @@ ColumnLayout {
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
visible: !Settings.data.colorSchemes.useWallpaperColors
}
@@ -12,16 +12,15 @@ ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.color-scheme.templates.section.label")
description: I18n.tr("settings.color-scheme.templates.section.description")
NLabel {
label: I18n.tr("settings.color-scheme.templates.section.description")
}
NCollapsible {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.ui.label")
description: I18n.tr("settings.color-scheme.templates.ui.description")
defaultExpanded: false
expanded: true
NCheckbox {
label: "GTK"
@@ -64,7 +63,7 @@ ColumnLayout {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.compositors.label")
description: I18n.tr("settings.color-scheme.templates.compositors.description")
defaultExpanded: false
expanded: true
NCheckbox {
label: "Niri"
@@ -107,7 +106,7 @@ ColumnLayout {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.terminal.label")
description: I18n.tr("settings.color-scheme.templates.terminal.description")
defaultExpanded: false
expanded: true
NCheckbox {
label: "Alacritty"
@@ -174,7 +173,7 @@ ColumnLayout {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.programs.label")
description: I18n.tr("settings.color-scheme.templates.programs.description")
defaultExpanded: false
expanded: true
NCheckbox {
label: "Fuzzel"
@@ -370,7 +369,7 @@ ColumnLayout {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.misc.label")
description: I18n.tr("settings.color-scheme.templates.misc.description")
defaultExpanded: false
expanded: false
NCheckbox {
label: I18n.tr("settings.color-scheme.templates.misc.user-templates.label")
@@ -66,7 +66,6 @@ ColumnLayout {
NComboBox {
id: diskPathComboBox
Layout.fillWidth: true
Layout.topMargin: Style.marginM
label: I18n.tr("settings.control-center.system-monitor-disk-path.label")
description: I18n.tr("settings.control-center.system-monitor-disk-path.description")
model: {
@@ -2,7 +2,6 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "ControlCenter"
import qs.Commons
import qs.Services.System
import qs.Services.UI
@@ -12,23 +12,15 @@ ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.display.monitors.section.label")
description: I18n.tr("settings.display.monitors.section.description")
}
ColumnLayout {
spacing: Style.marginL
Repeater {
model: Quickshell.screens || []
delegate: Rectangle {
delegate: NBox {
Layout.fillWidth: true
implicitHeight: contentCol.implicitHeight + Style.marginL * 2
radius: Style.radiusM
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Style.borderS
color: Color.mSurface
property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData)
@@ -39,16 +31,33 @@ ColumnLayout {
y: Style.marginL
spacing: Style.marginXXS
NLabel {
label: modelData.name || "Unknown"
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignBottom
NText {
text: modelData.name || "Unknown"
pointSize: Style.fontSizeL
font.weight: Style.fontWeightSemiBold
Layout.alignment: Qt.AlignBottom
}
NText {
Layout.fillWidth: true
text: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignBottom
}
}
@@ -100,7 +109,8 @@ ColumnLayout {
Layout.fillHeight: true
NIcon {
icon: brightnessMonitor && brightnessMonitor.method == "internal" ? "device-laptop" : "device-desktop"
anchors.centerIn: parent
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0
}
}
@@ -15,11 +15,6 @@ ColumnLayout {
signal checkWlsunset
NHeader {
label: I18n.tr("settings.display.night-light.section.label")
description: I18n.tr("settings.display.night-light.section.description")
}
NToggle {
label: I18n.tr("settings.display.night-light.enable.label")
description: I18n.tr("settings.display.night-light.enable.description")
@@ -37,8 +32,8 @@ ColumnLayout {
}
ColumnLayout {
visible: Settings.data.nightLight.enabled
spacing: Style.marginM
enabled: Settings.data.nightLight.enabled
spacing: Style.marginL
Layout.fillWidth: true
NLabel {
@@ -146,81 +141,79 @@ ColumnLayout {
Layout.alignment: Qt.AlignVCenter
}
}
}
NToggle {
label: I18n.tr("settings.display.night-light.auto-schedule.label")
description: I18n.tr("settings.display.night-light.auto-schedule.description", {
"location": LocationService.stableName
})
checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
visible: Settings.data.nightLight.enabled
}
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
NLabel {
label: I18n.tr("settings.display.night-light.manual-schedule.label")
description: I18n.tr("settings.display.night-light.manual-schedule.description")
NToggle {
label: I18n.tr("settings.display.night-light.auto-schedule.label")
description: I18n.tr("settings.display.night-light.auto-schedule.description", {
"location": LocationService.stableName
})
checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
}
RowLayout {
Layout.fillWidth: true
ColumnLayout {
spacing: Style.marginS
NText {
text: I18n.tr("settings.display.night-light.manual-schedule.sunrise")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NComboBox {
model: root.timeOptions
currentKey: Settings.data.nightLight.manualSunrise
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-start")
onSelected: key => Settings.data.nightLight.manualSunrise = key
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
visible: !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
NText {
text: I18n.tr("settings.display.night-light.manual-schedule.sunset")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
NLabel {
label: I18n.tr("settings.display.night-light.manual-schedule.label")
description: I18n.tr("settings.display.night-light.manual-schedule.description")
}
NComboBox {
model: root.timeOptions
currentKey: Settings.data.nightLight.manualSunset
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-stop")
onSelected: key => Settings.data.nightLight.manualSunset = key
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("settings.display.night-light.manual-schedule.sunrise")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NComboBox {
model: root.timeOptions
currentKey: Settings.data.nightLight.manualSunrise
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-start")
onSelected: key => Settings.data.nightLight.manualSunrise = key
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("settings.display.night-light.manual-schedule.sunset")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NComboBox {
model: root.timeOptions
currentKey: Settings.data.nightLight.manualSunset
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-stop")
onSelected: key => Settings.data.nightLight.manualSunset = key
Layout.fillWidth: true
}
}
}
}
NToggle {
label: I18n.tr("settings.display.night-light.force-activation.label")
description: I18n.tr("settings.display.night-light.force-activation.description")
checked: Settings.data.nightLight.forced
onToggled: checked => {
Settings.data.nightLight.forced = checked;
if (checked && !Settings.data.nightLight.enabled) {
root.checkWlsunset();
} else {
NightLightService.apply();
NToggle {
label: I18n.tr("settings.display.night-light.force-activation.label")
description: I18n.tr("settings.display.night-light.force-activation.description")
checked: Settings.data.nightLight.forced
onToggled: checked => {
Settings.data.nightLight.forced = checked;
if (checked && !Settings.data.nightLight.enabled) {
root.checkWlsunset();
} else {
NightLightService.apply();
}
}
}
visible: Settings.data.nightLight.enabled
}
}
}
@@ -20,7 +20,7 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginL
visible: Settings.data.dock.enabled
enabled: Settings.data.dock.enabled
NComboBox {
Layout.fillWidth: true
+27 -14
View File
@@ -10,11 +10,6 @@ import qs.Widgets
ColumnLayout {
id: root
NHeader {
label: I18n.tr("settings.general.profile.section.label")
description: I18n.tr("settings.general.profile.section.description")
}
// Profile section
RowLayout {
Layout.fillWidth: true
@@ -63,8 +58,8 @@ ColumnLayout {
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
}
// Fonts
@@ -72,11 +67,6 @@ ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.general.fonts.section.label")
description: I18n.tr("settings.general.fonts.section.description")
}
// Font configuration section
ColumnLayout {
spacing: Style.marginL
@@ -178,7 +168,30 @@ ColumnLayout {
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
}
NButton {
visible: !HostService.isNixOS
icon: "wand"
text: I18n.tr("settings.general.launch-setup-wizard")
outlined: true
onClicked: {
var targetScreen = PanelService.openedPanel ? PanelService.openedPanel.screen : (Quickshell.screens.length > 0 ? Quickshell.screens[0] : null);
if (!targetScreen) {
return;
}
var setupPanel = PanelService.getPanel("setupWizardPanel", targetScreen);
if (setupPanel) {
setupPanel.open();
} else {
Qt.callLater(() => {
var sp = PanelService.getPanel("setupWizardPanel", targetScreen);
if (sp)
sp.open();
});
}
}
}
}
@@ -184,7 +184,5 @@ ColumnLayout {
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
}
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
@@ -44,7 +44,5 @@ ColumnLayout {
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
}
@@ -11,10 +11,6 @@ ColumnLayout {
id: root
spacing: Style.marginL
NHeader {
description: I18n.tr("settings.network.section.description")
}
NToggle {
label: I18n.tr("settings.network.wifi.label")
description: I18n.tr("settings.network.wifi.description")
@@ -43,10 +39,4 @@ ColumnLayout {
enabled: BluetoothService.enabled
onToggled: checked => Settings.data.network.bluetoothRssiPollingEnabled = checked
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
}
@@ -0,0 +1,115 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
label: I18n.tr("settings.notifications.duration.respect-expire.label")
description: I18n.tr("settings.notifications.duration.respect-expire.description")
checked: Settings.data.notifications.respectExpireTimeout
onToggled: checked => Settings.data.notifications.respectExpireTimeout = checked
defaultValue: Settings.getDefaultValue("notifications.respectExpireTimeout")
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.duration.low-urgency.label")
description: I18n.tr("settings.notifications.duration.low-urgency.description")
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.lowUrgencyDuration
onMoved: value => Settings.data.notifications.lowUrgencyDuration = value
text: Settings.data.notifications.lowUrgencyDuration + "s"
defaultValue: Settings.getDefaultValue("notifications.lowUrgencyDuration")
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.notifications.duration.reset")
onClicked: Settings.data.notifications.lowUrgencyDuration = 3
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.duration.normal-urgency.label")
description: I18n.tr("settings.notifications.duration.normal-urgency.description")
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.normalUrgencyDuration
onMoved: value => Settings.data.notifications.normalUrgencyDuration = value
text: Settings.data.notifications.normalUrgencyDuration + "s"
defaultValue: Settings.getDefaultValue("notifications.normalUrgencyDuration")
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.notifications.duration.reset")
onClicked: Settings.data.notifications.normalUrgencyDuration = 8
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.duration.critical-urgency.label")
description: I18n.tr("settings.notifications.duration.critical-urgency.description")
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.criticalUrgencyDuration
onMoved: value => Settings.data.notifications.criticalUrgencyDuration = value
text: Settings.data.notifications.criticalUrgencyDuration + "s"
defaultValue: Settings.getDefaultValue("notifications.criticalUrgencyDuration")
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.notifications.duration.reset")
onClicked: Settings.data.notifications.criticalUrgencyDuration = 15
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
@@ -15,11 +15,6 @@ ColumnLayout {
property var addMonitor
property var removeMonitor
NHeader {
label: I18n.tr("settings.notifications.settings.section.label")
description: I18n.tr("settings.notifications.settings.section.description")
}
NToggle {
label: I18n.tr("settings.notifications.settings.enabled.label")
description: I18n.tr("settings.notifications.settings.enabled.description")
@@ -92,8 +87,6 @@ ColumnLayout {
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
NHeader {
@@ -9,126 +9,6 @@ ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.notifications.duration.section.label")
description: I18n.tr("settings.notifications.duration.section.description")
}
NToggle {
label: I18n.tr("settings.notifications.duration.respect-expire.label")
description: I18n.tr("settings.notifications.duration.respect-expire.description")
checked: Settings.data.notifications.respectExpireTimeout
onToggled: checked => Settings.data.notifications.respectExpireTimeout = checked
defaultValue: Settings.getDefaultValue("notifications.respectExpireTimeout")
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.duration.low-urgency.label")
description: I18n.tr("settings.notifications.duration.low-urgency.description")
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.lowUrgencyDuration
onMoved: value => Settings.data.notifications.lowUrgencyDuration = value
text: Settings.data.notifications.lowUrgencyDuration + "s"
defaultValue: Settings.getDefaultValue("notifications.lowUrgencyDuration")
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.notifications.duration.reset")
onClicked: Settings.data.notifications.lowUrgencyDuration = 3
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.duration.normal-urgency.label")
description: I18n.tr("settings.notifications.duration.normal-urgency.description")
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.normalUrgencyDuration
onMoved: value => Settings.data.notifications.normalUrgencyDuration = value
text: Settings.data.notifications.normalUrgencyDuration + "s"
defaultValue: Settings.getDefaultValue("notifications.normalUrgencyDuration")
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.notifications.duration.reset")
onClicked: Settings.data.notifications.normalUrgencyDuration = 8
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.duration.critical-urgency.label")
description: I18n.tr("settings.notifications.duration.critical-urgency.description")
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.criticalUrgencyDuration
onMoved: value => Settings.data.notifications.criticalUrgencyDuration = value
text: Settings.data.notifications.criticalUrgencyDuration + "s"
defaultValue: Settings.getDefaultValue("notifications.criticalUrgencyDuration")
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.notifications.duration.reset")
onClicked: Settings.data.notifications.criticalUrgencyDuration = 15
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
NHeader {
label: I18n.tr("settings.notifications.history.section.label")
description: I18n.tr("settings.notifications.history.section.description")
}
NToggle {
label: I18n.tr("settings.notifications.history.low-urgency.label")
description: I18n.tr("settings.notifications.history.low-urgency.description")
@@ -48,7 +48,7 @@ ColumnLayout {
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.notifications.tabs.sounds")
text: I18n.tr("settings.notifications.tabs.duration")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
@@ -58,10 +58,15 @@ ColumnLayout {
checked: subTabBar.currentIndex === 2
}
NTabButton {
text: I18n.tr("settings.notifications.tabs.toast")
text: I18n.tr("settings.notifications.tabs.sound")
tabIndex: 3
checked: subTabBar.currentIndex === 3
}
NTabButton {
text: I18n.tr("settings.notifications.tabs.toast")
tabIndex: 4
checked: subTabBar.currentIndex === 4
}
}
Item {
@@ -77,13 +82,14 @@ ColumnLayout {
addMonitor: root.addMonitor
removeMonitor: root.removeMonitor
}
SoundsSubTab {
DurationSubTab {}
HistorySubTab {}
SoundSubTab {
onOpenUnifiedPicker: root.openUnifiedSoundPicker()
onOpenLowPicker: root.openLowSoundPicker()
onOpenNormalPicker: root.openNormalSoundPicker()
onOpenCriticalPicker: root.openCriticalSoundPicker()
}
HistorySubTab {}
ToastSubTab {}
}
@@ -16,11 +16,6 @@ ColumnLayout {
signal openNormalPicker
signal openCriticalPicker
NHeader {
label: I18n.tr("settings.notifications.sounds.section.label")
description: I18n.tr("settings.notifications.sounds.section.description")
}
// QtMultimedia unavailable message
NBox {
Layout.fillWidth: true
@@ -9,11 +9,6 @@ ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.notifications.toast.section.label")
description: I18n.tr("settings.notifications.toast.section.description")
}
NToggle {
label: I18n.tr("settings.notifications.toast.keyboard.label")
description: I18n.tr("settings.notifications.toast.keyboard.description")
@@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Modules.OSD
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var addType
property var removeType
Repeater {
model: [
{
type: OSD.Type.Volume,
key: "volume"
},
{
type: OSD.Type.InputVolume,
key: "input-volume"
},
{
type: OSD.Type.Brightness,
key: "brightness"
},
{
type: OSD.Type.LockKey,
key: "lockkey"
},
{
type: OSD.Type.CustomText,
key: "custom-text"
}
]
delegate: NCheckbox {
required property var modelData
Layout.fillWidth: true
label: I18n.tr("settings.osd.types." + modelData.key + ".label")
description: I18n.tr("settings.osd.types." + modelData.key + ".description")
checked: (Settings.data.osd.enabledTypes || []).includes(modelData.type)
onToggled: checked => {
if (checked) {
Settings.data.osd.enabledTypes = root.addType(Settings.data.osd.enabledTypes, modelData.type);
} else {
Settings.data.osd.enabledTypes = root.removeType(Settings.data.osd.enabledTypes, modelData.type);
}
}
}
}
}
@@ -0,0 +1,133 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var addMonitor
property var removeMonitor
NComboBox {
label: I18n.tr("settings.osd.location.label")
description: I18n.tr("settings.osd.location.description")
model: [
{
"key": "top",
"name": I18n.tr("options.osd.position.top_center")
},
{
"key": "top_left",
"name": I18n.tr("options.osd.position.top_left")
},
{
"key": "top_right",
"name": I18n.tr("options.osd.position.top_right")
},
{
"key": "bottom",
"name": I18n.tr("options.osd.position.bottom_center")
},
{
"key": "bottom_left",
"name": I18n.tr("options.osd.position.bottom_left")
},
{
"key": "bottom_right",
"name": I18n.tr("options.osd.position.bottom_right")
},
{
"key": "left",
"name": I18n.tr("options.osd.position.center_left")
},
{
"key": "right",
"name": I18n.tr("options.osd.position.center_right")
}
]
currentKey: Settings.data.osd.location || "top_right"
defaultValue: Settings.getDefaultValue("osd.location")
onSelected: key => Settings.data.osd.location = key
}
NToggle {
label: I18n.tr("settings.osd.enabled.label")
description: I18n.tr("settings.osd.enabled.description")
checked: Settings.data.osd.enabled
defaultValue: Settings.getDefaultValue("osd.enabled")
onToggled: checked => Settings.data.osd.enabled = checked
}
NToggle {
label: I18n.tr("settings.osd.always-on-top.label")
description: I18n.tr("settings.osd.always-on-top.description")
checked: Settings.data.osd.overlayLayer
defaultValue: Settings.getDefaultValue("osd.overlayLayer")
onToggled: checked => Settings.data.osd.overlayLayer = checked
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.osd.background-opacity.label")
description: I18n.tr("settings.osd.background-opacity.description")
from: 0
to: 100
stepSize: 1
value: Settings.data.osd.backgroundOpacity * 100
defaultValue: (Settings.getDefaultValue("osd.backgroundOpacity") || 1) * 100
onMoved: value => Settings.data.osd.backgroundOpacity = value / 100
text: Math.round(Settings.data.osd.backgroundOpacity * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.osd.duration.auto-hide.label")
description: I18n.tr("settings.osd.duration.auto-hide.description")
from: 500
to: 5000
stepSize: 100
value: Settings.data.osd.autoHideMs
defaultValue: Settings.getDefaultValue("osd.autoHideMs")
onMoved: value => Settings.data.osd.autoHideMs = value
text: Math.round(Settings.data.osd.autoHideMs / 1000 * 10) / 10 + "s"
}
NDivider {
Layout.fillWidth: true
}
NLabel {
label: I18n.tr("settings.osd.monitors.section.description")
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || I18n.tr("system.unknown")
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.osd.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.osd.monitors = root.addMonitor(Settings.data.osd.monitors, modelData.name);
} else {
Settings.data.osd.monitors = root.removeMonitor(Settings.data.osd.monitors, modelData.name);
}
}
}
}
}
@@ -0,0 +1,71 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice();
if (!arr.includes(name))
arr.push(name);
return arr;
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name;
});
}
function addType(list, type) {
const arr = (list || []).slice();
if (!arr.includes(type))
arr.push(type);
return arr;
}
function removeType(list, type) {
return (list || []).filter(function (t) {
return t !== type;
});
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.osd.tabs.general")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.osd.tabs.events")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
GeneralSubTab {
addMonitor: root.addMonitor
removeMonitor: root.removeMonitor
}
EventsSubTab {
addType: root.addType
removeType: root.removeType
}
}
}
-247
View File
@@ -1,247 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.OSD
import qs.Services.Compositor
import qs.Widgets
ColumnLayout {
id: root
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice();
if (!arr.includes(name))
arr.push(name);
return arr;
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name;
});
}
function addType(list, type) {
const arr = (list || []).slice();
if (!arr.includes(type))
arr.push(type);
return arr;
}
function removeType(list, type) {
return (list || []).filter(function (t) {
return t !== type;
});
}
// Display
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NComboBox {
label: I18n.tr("settings.osd.location.label")
description: I18n.tr("settings.osd.location.description")
model: [
{
"key": "top",
"name": I18n.tr("options.osd.position.top_center")
},
{
"key": "top_left",
"name": I18n.tr("options.osd.position.top_left")
},
{
"key": "top_right",
"name": I18n.tr("options.osd.position.top_right")
},
{
"key": "bottom",
"name": I18n.tr("options.osd.position.bottom_center")
},
{
"key": "bottom_left",
"name": I18n.tr("options.osd.position.bottom_left")
},
{
"key": "bottom_right",
"name": I18n.tr("options.osd.position.bottom_right")
},
{
"key": "left",
"name": I18n.tr("options.osd.position.center_left")
},
{
"key": "right",
"name": I18n.tr("options.osd.position.center_right")
}
]
currentKey: Settings.data.osd.location || "top_right"
defaultValue: Settings.getDefaultValue("osd.location")
onSelected: key => Settings.data.osd.location = key
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// General
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.osd.section.general.label")
description: I18n.tr("settings.osd.section.general.description")
}
NToggle {
label: I18n.tr("settings.osd.enabled.label")
description: I18n.tr("settings.osd.enabled.description")
checked: Settings.data.osd.enabled
defaultValue: Settings.getDefaultValue("osd.enabled")
onToggled: checked => Settings.data.osd.enabled = checked
}
NToggle {
label: I18n.tr("settings.osd.always-on-top.label")
description: I18n.tr("settings.osd.always-on-top.description")
checked: Settings.data.osd.overlayLayer
defaultValue: Settings.getDefaultValue("osd.overlayLayer")
onToggled: checked => Settings.data.osd.overlayLayer = checked
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.osd.background-opacity.label", "Background opacity")
description: I18n.tr("settings.osd.background-opacity.description", "Controls the transparency of the OSD background.")
from: 0
to: 100
stepSize: 1
value: Settings.data.osd.backgroundOpacity * 100
defaultValue: (Settings.getDefaultValue("osd.backgroundOpacity") || 1) * 100
onMoved: value => Settings.data.osd.backgroundOpacity = value / 100
text: Math.round(Settings.data.osd.backgroundOpacity * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.osd.duration.auto-hide.label")
description: I18n.tr("settings.osd.duration.auto-hide.description")
from: 500
to: 5000
stepSize: 100
value: Settings.data.osd.autoHideMs
defaultValue: Settings.getDefaultValue("osd.autoHideMs")
onMoved: value => Settings.data.osd.autoHideMs = value
text: Math.round(Settings.data.osd.autoHideMs / 1000 * 10) / 10 + "s"
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// OSD Types Configuration
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.osd.types.section.label")
description: I18n.tr("settings.osd.types.section.description")
}
Repeater {
model: [
{
type: OSD.Type.Volume,
key: "volume"
},
{
type: OSD.Type.InputVolume,
key: "input-volume"
},
{
type: OSD.Type.Brightness,
key: "brightness"
},
{
type: OSD.Type.LockKey,
key: "lockkey"
},
{
type: OSD.Type.CustomText,
key: "custom-text"
}
]
delegate: NCheckbox {
required property var modelData
Layout.fillWidth: true
label: I18n.tr("settings.osd.types." + modelData.key + ".label")
description: I18n.tr("settings.osd.types." + modelData.key + ".description")
checked: (Settings.data.osd.enabledTypes || []).includes(modelData.type)
onToggled: checked => {
if (checked) {
Settings.data.osd.enabledTypes = addType(Settings.data.osd.enabledTypes, modelData.type);
} else {
Settings.data.osd.enabledTypes = removeType(Settings.data.osd.enabledTypes, modelData.type);
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Monitor Configuration
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.osd.monitors.section.label")
description: I18n.tr("settings.osd.monitors.section.description")
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || I18n.tr("system.unknown")
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.osd.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.osd.monitors = addMonitor(Settings.data.osd.monitors, modelData.name);
} else {
Settings.data.osd.monitors = removeMonitor(Settings.data.osd.monitors, modelData.name);
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
}
@@ -0,0 +1,431 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../../../../../Helpers/FuzzySort.js" as Fuzzysort
import qs.Commons
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property string pluginFilter: "all"
property string pluginSearchText: ""
function stripAuthorEmail(author) {
if (!author)
return "";
var lastBracket = author.lastIndexOf("<");
if (lastBracket >= 0) {
return author.substring(0, lastBracket).trim();
}
return author;
}
// Filter controls
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
NTabBar {
id: filterTabBar
Layout.fillWidth: true
spacing: Style.marginM
currentIndex: 0
onCurrentIndexChanged: {
if (currentIndex === 0)
root.pluginFilter = "all";
else if (currentIndex === 1)
root.pluginFilter = "downloaded";
else if (currentIndex === 2)
root.pluginFilter = "notDownloaded";
}
NTabButton {
Layout.fillWidth: true
text: I18n.tr("settings.plugins.filter.all")
tabIndex: 0
checked: root.pluginFilter === "all"
}
NTabButton {
Layout.fillWidth: true
text: I18n.tr("settings.plugins.filter.downloaded")
tabIndex: 1
checked: root.pluginFilter === "downloaded"
}
NTabButton {
Layout.fillWidth: true
text: I18n.tr("settings.plugins.filter.not-downloaded")
tabIndex: 2
checked: root.pluginFilter === "notDownloaded"
}
}
NIconButton {
icon: "refresh"
tooltipText: I18n.tr("settings.plugins.refresh.tooltip")
baseSize: Style.baseWidgetSize * 0.9
onClicked: {
PluginService.refreshAvailablePlugins();
checkUpdatesTimer.restart();
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.refresh.refreshing"));
}
}
}
// Search input
NTextInput {
placeholderText: I18n.tr("placeholders.search")
inputIconName: "search"
text: root.pluginSearchText
onTextChanged: root.pluginSearchText = text
Layout.fillWidth: true
}
// Available plugins list
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
Repeater {
id: availablePluginsRepeater
model: {
var all = PluginService.availablePlugins || [];
var filtered = [];
// First apply download filter
for (var i = 0; i < all.length; i++) {
var plugin = all[i];
var downloaded = plugin.downloaded || false;
if (root.pluginFilter === "all") {
filtered.push(plugin);
} else if (root.pluginFilter === "downloaded" && downloaded) {
filtered.push(plugin);
} else if (root.pluginFilter === "notDownloaded" && !downloaded) {
filtered.push(plugin);
}
}
// Then apply fuzzy search if there's search text
var query = root.pluginSearchText.trim();
if (query !== "") {
var results = Fuzzysort.go(query, filtered, {
"keys": ["name", "description"],
"threshold": 0.35,
"limit": 50
});
filtered = [];
for (var j = 0; j < results.length; j++) {
filtered.push(results[j].obj);
}
} else {
// Sort by lastUpdated (most recent first) when not searching
filtered.sort(function (a, b) {
var dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
var dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
return dateB - dateA;
});
}
return filtered;
}
delegate: NBox {
id: pluginBox
property bool isHovered: hoverHandler.hovered
Layout.fillWidth: true
Layout.leftMargin: Style.borderS
Layout.rightMargin: Style.borderS
implicitHeight: Math.round(contentColumn.implicitHeight + Style.marginL * 2)
color: Color.mSurface
Behavior on implicitHeight {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
HoverHandler {
id: hoverHandler
}
ColumnLayout {
id: contentColumn
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginS
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
NIcon {
icon: "plugin"
pointSize: Style.fontSizeL
color: Color.mOnSurface
}
NText {
text: modelData.name
color: Color.mOnSurface
elide: Text.ElideRight
}
// Description excerpt - visible when not hovered
NText {
visible: !pluginBox.isHovered && modelData.description
text: modelData.description || ""
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
elide: Text.ElideRight
Layout.fillWidth: true
}
// Spacer when hovered or no description
Item {
visible: pluginBox.isHovered || !modelData.description
Layout.fillWidth: true
}
// Downloaded indicator
NIcon {
icon: "circle-check"
pointSize: Style.fontSizeL
color: Color.mPrimary
visible: modelData.downloaded === true
}
// Install/Uninstall button
NIconButton {
icon: modelData.downloaded ? "trash" : "download"
baseSize: Style.baseWidgetSize * 0.7
tooltipText: modelData.downloaded ? I18n.tr("settings.plugins.uninstall") : I18n.tr("settings.plugins.install")
onClicked: {
if (modelData.downloaded) {
// Construct composite key for available plugins
var pluginData = Object.assign({}, modelData);
pluginData.compositeKey = PluginRegistry.generateCompositeKey(modelData.id, modelData.source?.url || "");
uninstallDialog.pluginToUninstall = pluginData;
uninstallDialog.open();
} else {
installPlugin(modelData);
}
}
}
}
// Description - visible on hover
NText {
visible: pluginBox.isHovered && modelData.description
text: modelData.description || ""
font.pointSize: Style.fontSizeXS
color: Color.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Details row - visible on hover
RowLayout {
visible: pluginBox.isHovered
spacing: Style.marginS
Layout.fillWidth: true
NText {
text: "v" + modelData.version
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: "•"
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: stripAuthorEmail(modelData.author)
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: "•"
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: modelData.source ? modelData.source.name : ""
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
Item {
Layout.fillWidth: true
}
}
}
}
}
NLabel {
visible: availablePluginsRepeater.count === 0
label: I18n.tr("settings.plugins.available.no-plugins-label")
description: I18n.tr("settings.plugins.available.no-plugins-description")
Layout.fillWidth: true
}
}
// Uninstall confirmation dialog
Popup {
id: uninstallDialog
parent: Overlay.overlay
modal: true
dim: false
anchors.centerIn: parent
width: 400 * Style.uiScaleRatio
padding: Style.marginL
property var pluginToUninstall: null
background: Rectangle {
color: Color.mSurface
radius: Style.radiusS
border.color: Color.mPrimary
border.width: Style.borderM
}
contentItem: ColumnLayout {
width: parent.width
spacing: Style.marginL
NHeader {
label: I18n.tr("settings.plugins.uninstall-dialog.title")
description: I18n.tr("settings.plugins.uninstall-dialog.description", {
"plugin": uninstallDialog.pluginToUninstall?.name || ""
})
}
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
NButton {
text: I18n.tr("common.cancel")
onClicked: uninstallDialog.close()
}
NButton {
text: I18n.tr("settings.plugins.uninstall")
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
onClicked: {
if (uninstallDialog.pluginToUninstall) {
uninstallPlugin(uninstallDialog.pluginToUninstall.compositeKey);
uninstallDialog.close();
}
}
}
}
}
}
// Timer to check for updates after refresh starts
Timer {
id: checkUpdatesTimer
interval: 100
onTriggered: {
PluginService.checkForUpdates();
}
}
function installPlugin(pluginMetadata) {
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.installing", {
"plugin": pluginMetadata.name
}));
PluginService.installPlugin(pluginMetadata, false, function (success, error, registeredKey) {
if (success) {
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.install-success", {
"plugin": pluginMetadata.name
}));
// Auto-enable the plugin after installation (use registered key which may be composite)
PluginService.enablePlugin(registeredKey);
} else {
ToastService.showError(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.install-error", {
"error": error || "Unknown error"
}));
}
});
}
function uninstallPlugin(pluginId) {
var manifest = PluginRegistry.getPluginManifest(pluginId);
var pluginName = manifest?.name || pluginId;
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.uninstalling", {
"plugin": pluginName
}));
PluginService.uninstallPlugin(pluginId, function (success, error) {
if (success) {
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.uninstall-success", {
"plugin": pluginName
}));
} else {
ToastService.showError(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.uninstall-error", {
"error": error || "Unknown error"
}));
}
});
}
// Listen to plugin service signals
Connections {
target: PluginService
function onAvailablePluginsUpdated() {
// Force model refresh for available plugins
availablePluginsRepeater.model = undefined;
Qt.callLater(function () {
availablePluginsRepeater.model = Qt.binding(function () {
var all = PluginService.availablePlugins || [];
var filtered = [];
for (var i = 0; i < all.length; i++) {
var plugin = all[i];
var downloaded = plugin.downloaded || false;
if (root.pluginFilter === "all") {
filtered.push(plugin);
} else if (root.pluginFilter === "downloaded" && downloaded) {
filtered.push(plugin);
} else if (root.pluginFilter === "notDownloaded" && !downloaded) {
filtered.push(plugin);
}
}
return filtered;
});
});
// Manually trigger update check after a small delay to ensure all registries are loaded
Qt.callLater(function () {
PluginService.checkForUpdates();
});
}
}
}
@@ -0,0 +1,416 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
// Track which plugins are currently updating
property var updatingPlugins: ({})
property int installedPluginsRefreshCounter: 0
function stripAuthorEmail(author) {
if (!author)
return "";
var lastBracket = author.lastIndexOf("<");
if (lastBracket >= 0) {
return author.substring(0, lastBracket).trim();
}
return author;
}
// Check for updates when tab becomes visible
onVisibleChanged: {
if (visible && PluginService.pluginsFullyLoaded) {
PluginService.checkForUpdates();
}
}
// Update All button
NButton {
property int updateCount: Object.keys(PluginService.pluginUpdates).length
property bool isUpdating: false
text: I18n.tr("settings.plugins.update-all", {
"count": updateCount
})
icon: "download"
visible: updateCount >= 2
enabled: !isUpdating
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
Layout.fillWidth: true
onClicked: {
isUpdating = true;
var pluginIds = Object.keys(PluginService.pluginUpdates);
var currentIndex = 0;
function updateNext() {
if (currentIndex >= pluginIds.length) {
isUpdating = false;
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.update-all-success"));
return;
}
var pluginId = pluginIds[currentIndex];
currentIndex++;
PluginService.updatePlugin(pluginId, function (success, error) {
if (!success) {
Logger.w("InstalledSubTab", "Failed to update", pluginId + ":", error);
}
Qt.callLater(updateNext);
});
}
updateNext();
}
}
// Installed plugins list
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
Repeater {
id: installedPluginsRepeater
model: {
// Force refresh when counter changes
var _ = root.installedPluginsRefreshCounter;
var allIds = PluginRegistry.getAllInstalledPluginIds();
var plugins = [];
for (var i = 0; i < allIds.length; i++) {
var compositeKey = allIds[i];
var manifest = PluginRegistry.getPluginManifest(compositeKey);
if (manifest) {
// Create a copy of manifest and include update info, enabled state, and source info
var pluginData = JSON.parse(JSON.stringify(manifest));
pluginData.compositeKey = compositeKey;
pluginData.updateInfo = PluginService.pluginUpdates[compositeKey];
pluginData.pendingUpdateInfo = PluginService.pluginUpdatesPending[compositeKey];
pluginData.enabled = PluginRegistry.isPluginEnabled(compositeKey);
// Add source info
var parsed = PluginRegistry.parseCompositeKey(compositeKey);
pluginData.isOfficial = parsed.isOfficial;
if (!parsed.isOfficial) {
pluginData.sourceName = PluginRegistry.getSourceNameByHash(parsed.sourceHash);
}
plugins.push(pluginData);
}
}
return plugins;
}
delegate: NBox {
Layout.fillWidth: true
Layout.leftMargin: Style.borderS
Layout.rightMargin: Style.borderS
implicitHeight: Math.round(rowLayout.implicitHeight) + Style.marginL * 2
color: Color.mSurface
RowLayout {
id: rowLayout
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
NIcon {
icon: "plugin"
pointSize: Style.fontSizeXL
color: PluginService.hasPluginError(modelData.compositeKey) ? Color.mError : Color.mOnSurface
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
NText {
text: modelData.name
color: Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
text: modelData.description
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginS
NText {
text: {
if (modelData.updateInfo) {
return I18n.tr("settings.plugins.update-version", {
"current": modelData.version,
"new": modelData.updateInfo.availableVersion
});
} else if (modelData.pendingUpdateInfo) {
return I18n.tr("settings.plugins.update-pending", {
"current": modelData.version,
"new": modelData.pendingUpdateInfo.availableVersion,
"required": modelData.pendingUpdateInfo.minNoctaliaVersion
});
}
return "v" + modelData.version;
}
font.pointSize: Style.fontSizeXXS
color: modelData.updateInfo ? Color.mPrimary : (modelData.pendingUpdateInfo ? Color.mTertiary : Color.mOnSurfaceVariant)
font.weight: (modelData.updateInfo || modelData.pendingUpdateInfo) ? Style.fontWeightMedium : Style.fontWeightRegular
}
NText {
text: "•"
font.pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NText {
text: stripAuthorEmail(modelData.author)
font.pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
// Source indicator for non-official plugins
NText {
visible: !modelData.isOfficial
text: "•"
font.pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NText {
visible: !modelData.isOfficial
text: modelData.sourceName || I18n.tr("settings.plugins.source.custom")
font.pointSize: Style.fontSizeXXS
color: Color.mTertiary
}
}
// Error indicator
RowLayout {
spacing: Style.marginS
visible: PluginService.hasPluginError(modelData.compositeKey)
NIcon {
icon: "alert-triangle"
pointSize: Style.fontSizeS
color: Color.mError
}
NText {
property var errorInfo: PluginService.getPluginError(modelData.compositeKey)
text: errorInfo ? errorInfo.error : ""
font.pointSize: Style.fontSizeXXS
color: Color.mError
wrapMode: Text.WordWrap
Layout.fillWidth: true
elide: Text.ElideRight
maximumLineCount: 3
}
}
}
NIconButton {
icon: "settings"
tooltipText: I18n.tr("settings.plugins.settings.tooltip")
baseSize: Style.baseWidgetSize * 0.7
visible: modelData.entryPoints?.settings !== undefined
onClicked: {
pluginSettingsDialog.openPluginSettings(modelData);
}
}
NIconButton {
icon: "trash"
tooltipText: I18n.tr("settings.plugins.uninstall")
baseSize: Style.baseWidgetSize * 0.7
onClicked: {
uninstallDialog.pluginToUninstall = modelData;
uninstallDialog.open();
}
}
NButton {
id: updateButton
property string pluginId: modelData.compositeKey
property bool isUpdating: root.updatingPlugins[pluginId] === true
text: isUpdating ? I18n.tr("settings.plugins.updating") : I18n.tr("settings.plugins.update")
icon: isUpdating ? "" : "download"
visible: modelData.updateInfo !== undefined
enabled: !isUpdating
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
onClicked: {
var pid = pluginId;
var pname = modelData.name;
var pversion = modelData.updateInfo?.availableVersion || "";
var rootRef = root;
var updates = Object.assign({}, rootRef.updatingPlugins);
updates[pid] = true;
rootRef.updatingPlugins = updates;
PluginService.updatePlugin(pid, function (success, error) {
var updates2 = Object.assign({}, rootRef.updatingPlugins);
updates2[pid] = false;
rootRef.updatingPlugins = updates2;
if (success) {
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.update-success", {
"plugin": pname,
"version": pversion
}));
} else {
ToastService.showError(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.update-error", {
"plugin": pname,
"error": error || "Unknown error"
}));
}
});
}
}
NToggle {
checked: modelData.enabled
baseSize: Style.baseWidgetSize * 0.7
onToggled: checked => {
if (checked) {
PluginService.enablePlugin(modelData.compositeKey);
} else {
PluginService.disablePlugin(modelData.compositeKey);
}
}
}
}
}
}
NLabel {
visible: PluginRegistry.getAllInstalledPluginIds().length === 0
label: I18n.tr("settings.plugins.installed.no-plugins-label")
description: I18n.tr("settings.plugins.installed.no-plugins-description")
Layout.fillWidth: true
}
}
// Uninstall confirmation dialog
Popup {
id: uninstallDialog
parent: Overlay.overlay
modal: true
dim: false
anchors.centerIn: parent
width: 400 * Style.uiScaleRatio
padding: Style.marginL
property var pluginToUninstall: null
background: Rectangle {
color: Color.mSurface
radius: Style.radiusS
border.color: Color.mPrimary
border.width: Style.borderM
}
contentItem: ColumnLayout {
width: parent.width
spacing: Style.marginL
NHeader {
label: I18n.tr("settings.plugins.uninstall-dialog.title")
description: I18n.tr("settings.plugins.uninstall-dialog.description", {
"plugin": uninstallDialog.pluginToUninstall?.name || ""
})
}
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
NButton {
text: I18n.tr("common.cancel")
onClicked: uninstallDialog.close()
}
NButton {
text: I18n.tr("settings.plugins.uninstall")
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
onClicked: {
if (uninstallDialog.pluginToUninstall) {
root.uninstallPlugin(uninstallDialog.pluginToUninstall.compositeKey);
uninstallDialog.close();
}
}
}
}
}
}
// Plugin settings popup
NPluginSettingsPopup {
id: pluginSettingsDialog
parent: Overlay.overlay
showToastOnSave: true
}
function uninstallPlugin(pluginId) {
var manifest = PluginRegistry.getPluginManifest(pluginId);
var pluginName = manifest?.name || pluginId;
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.uninstalling", {
"plugin": pluginName
}));
PluginService.uninstallPlugin(pluginId, function (success, error) {
if (success) {
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.uninstall-success", {
"plugin": pluginName
}));
} else {
ToastService.showError(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.uninstall-error", {
"error": error || "Unknown error"
}));
}
});
}
// Listen to plugin registry changes
Connections {
target: PluginRegistry
function onPluginsChanged() {
root.installedPluginsRefreshCounter++;
}
}
// Listen to plugin service signals
Connections {
target: PluginService
function onPluginUpdatesChanged() {
root.installedPluginsRefreshCounter++;
}
}
}
@@ -0,0 +1,47 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.plugins.tabs.installed")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.plugins.tabs.available")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("settings.plugins.tabs.sources")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
InstalledSubTab {}
AvailableSubTab {}
SourcesSubTab {}
}
}
@@ -0,0 +1,181 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
// List of plugin sources
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
Repeater {
id: pluginSourcesRepeater
model: PluginRegistry.pluginSources || []
delegate: NBox {
Layout.fillWidth: true
implicitHeight: sourceRow.implicitHeight + Style.marginL * 2
color: Color.mSurface
RowLayout {
id: sourceRow
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
NIcon {
icon: "brand-github"
pointSize: Style.fontSizeL
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
NText {
text: modelData.name
color: Color.mOnSurface
Layout.fillWidth: true
}
NText {
text: modelData.url
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
elide: Text.ElideRight
}
}
NIconButton {
icon: "trash"
tooltipText: I18n.tr("settings.plugins.sources.remove.tooltip")
visible: index !== 0 // Cannot remove official source
baseSize: Style.baseWidgetSize * 0.7
onClicked: {
PluginRegistry.removePluginSource(modelData.url);
}
}
// Enable/Disable a source
NToggle {
checked: modelData.enabled !== false // Default to true if not set
baseSize: Style.baseWidgetSize * 0.7
onToggled: checked => {
PluginRegistry.setSourceEnabled(modelData.url, checked);
PluginService.refreshAvailablePlugins();
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.refresh.refreshing"));
}
}
}
}
}
}
// Add custom repository
NButton {
text: I18n.tr("settings.plugins.sources.add-custom")
icon: "plus"
onClicked: {
addSourceDialog.open();
}
Layout.fillWidth: true
}
// Add source dialog
Popup {
id: addSourceDialog
parent: Overlay.overlay
modal: true
dim: false
anchors.centerIn: parent
width: 500
padding: Style.marginL
background: Rectangle {
color: Color.mSurface
radius: Style.radiusS
border.color: Color.mPrimary
border.width: Style.borderM
}
contentItem: ColumnLayout {
width: parent.width
spacing: Style.marginL
NHeader {
label: I18n.tr("settings.plugins.sources.add-dialog.title")
description: I18n.tr("settings.plugins.sources.add-dialog.description")
}
NTextInput {
id: sourceNameInput
label: I18n.tr("settings.plugins.sources.add-dialog.name")
placeholderText: I18n.tr("settings.plugins.sources.add-dialog.name-placeholder")
Layout.fillWidth: true
}
NTextInput {
id: sourceUrlInput
label: I18n.tr("settings.plugins.sources.add-dialog.url")
placeholderText: "https://github.com/user/repo"
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
NButton {
text: I18n.tr("common.cancel")
onClicked: addSourceDialog.close()
}
NButton {
text: I18n.tr("common.add")
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
enabled: sourceNameInput.text.length > 0 && sourceUrlInput.text.length > 0
onClicked: {
if (PluginRegistry.addPluginSource(sourceNameInput.text, sourceUrlInput.text)) {
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.sources.add-dialog.success"));
PluginService.refreshAvailablePlugins();
addSourceDialog.close();
sourceNameInput.text = "";
sourceUrlInput.text = "";
} else {
ToastService.showError(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.sources.add-dialog.error"));
}
}
}
}
}
}
// Listen to plugin registry changes
Connections {
target: PluginRegistry
function onPluginsChanged() {
// Force model refresh for plugin sources
pluginSourcesRepeater.model = undefined;
Qt.callLater(function () {
pluginSourcesRepeater.model = Qt.binding(function () {
return PluginRegistry.pluginSources || [];
});
});
}
}
}
-957
View File
@@ -1,957 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../../../../Helpers/FuzzySort.js" as Fuzzysort
import qs.Commons
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
// Track which plugins are currently updating
property var updatingPlugins: ({})
property int installedPluginsRefreshCounter: 0
function stripAuthorEmail(author) {
if (!author)
return "";
var lastBracket = author.lastIndexOf("<");
if (lastBracket >= 0) {
return author.substring(0, lastBracket).trim();
}
return author;
}
// Check for updates when tab becomes visible
onVisibleChanged: {
if (visible && PluginService.pluginsFullyLoaded) {
PluginService.checkForUpdates();
}
}
// ------------------------------
// Installed Plugins
// ------------------------------
NHeader {
label: I18n.tr("settings.plugins.installed.label")
description: I18n.tr("settings.plugins.installed.description")
}
// Update All button
NButton {
property int updateCount: Object.keys(PluginService.pluginUpdates).length
property bool isUpdating: false
text: I18n.tr("settings.plugins.update-all", {
"count": updateCount
})
icon: "download"
visible: updateCount >= 2
enabled: !isUpdating
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
Layout.fillWidth: true
onClicked: {
isUpdating = true;
var pluginIds = Object.keys(PluginService.pluginUpdates);
var currentIndex = 0;
function updateNext() {
if (currentIndex >= pluginIds.length) {
isUpdating = false;
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.update-all-success"));
return;
}
var pluginId = pluginIds[currentIndex];
currentIndex++;
PluginService.updatePlugin(pluginId, function (success, error) {
if (!success) {
Logger.w("PluginsTab", "Failed to update", pluginId + ":", error);
}
Qt.callLater(updateNext);
});
}
updateNext();
}
}
// ------------------------------
// Installed plugins
// ------------------------------
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
Repeater {
id: installedPluginsRepeater
model: {
// Force refresh when counter changes
var _ = root.installedPluginsRefreshCounter;
var allIds = PluginRegistry.getAllInstalledPluginIds();
var plugins = [];
for (var i = 0; i < allIds.length; i++) {
var compositeKey = allIds[i];
var manifest = PluginRegistry.getPluginManifest(compositeKey);
if (manifest) {
// Create a copy of manifest and include update info, enabled state, and source info
var pluginData = JSON.parse(JSON.stringify(manifest));
pluginData.compositeKey = compositeKey;
pluginData.updateInfo = PluginService.pluginUpdates[compositeKey];
pluginData.pendingUpdateInfo = PluginService.pluginUpdatesPending[compositeKey];
pluginData.enabled = PluginRegistry.isPluginEnabled(compositeKey);
// Add source info
var parsed = PluginRegistry.parseCompositeKey(compositeKey);
pluginData.isOfficial = parsed.isOfficial;
if (!parsed.isOfficial) {
pluginData.sourceName = PluginRegistry.getSourceNameByHash(parsed.sourceHash);
}
plugins.push(pluginData);
}
}
return plugins;
}
delegate: NBox {
Layout.fillWidth: true
Layout.leftMargin: Style.borderS
Layout.rightMargin: Style.borderS
implicitHeight: Math.round(rowLayout.implicitHeight) + Style.marginL * 2
color: Color.mSurface
RowLayout {
id: rowLayout
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
NIcon {
icon: "plugin"
pointSize: Style.fontSizeXL
color: PluginService.hasPluginError(modelData.compositeKey) ? Color.mError : Color.mOnSurface
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
NText {
text: modelData.name
color: Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
text: modelData.description
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginS
NText {
text: {
if (modelData.updateInfo) {
return I18n.tr("settings.plugins.update-version", {
"current": modelData.version,
"new": modelData.updateInfo.availableVersion
});
} else if (modelData.pendingUpdateInfo) {
return I18n.tr("settings.plugins.update-pending", {
"current": modelData.version,
"new": modelData.pendingUpdateInfo.availableVersion,
"required": modelData.pendingUpdateInfo.minNoctaliaVersion
});
}
return "v" + modelData.version;
}
font.pointSize: Style.fontSizeXXS
color: modelData.updateInfo ? Color.mPrimary : (modelData.pendingUpdateInfo ? Color.mTertiary : Color.mOnSurfaceVariant)
font.weight: (modelData.updateInfo || modelData.pendingUpdateInfo) ? Style.fontWeightMedium : Style.fontWeightRegular
}
NText {
text: "•"
font.pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NText {
text: stripAuthorEmail(modelData.author)
font.pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
// Source indicator for non-official plugins
NText {
visible: !modelData.isOfficial
text: "•"
font.pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NText {
visible: !modelData.isOfficial
text: modelData.sourceName || I18n.tr("settings.plugins.source.custom")
font.pointSize: Style.fontSizeXXS
color: Color.mTertiary
}
}
// Error indicator
RowLayout {
spacing: Style.marginS
visible: PluginService.hasPluginError(modelData.compositeKey)
NIcon {
icon: "alert-triangle"
pointSize: Style.fontSizeS
color: Color.mError
}
NText {
property var errorInfo: PluginService.getPluginError(modelData.compositeKey)
text: errorInfo ? errorInfo.error : ""
font.pointSize: Style.fontSizeXXS
color: Color.mError
wrapMode: Text.WordWrap
Layout.fillWidth: true
elide: Text.ElideRight
maximumLineCount: 3
}
}
}
NIconButton {
icon: "settings"
tooltipText: I18n.tr("settings.plugins.settings.tooltip")
baseSize: Style.baseWidgetSize * 0.7
visible: modelData.entryPoints?.settings !== undefined
onClicked: {
pluginSettingsDialog.openPluginSettings(modelData);
}
}
NIconButton {
icon: "trash"
tooltipText: I18n.tr("settings.plugins.uninstall")
baseSize: Style.baseWidgetSize * 0.7
onClicked: {
uninstallDialog.pluginToUninstall = modelData;
uninstallDialog.open();
}
}
NButton {
id: updateButton
property string pluginId: modelData.compositeKey
property bool isUpdating: root.updatingPlugins[pluginId] === true
text: isUpdating ? I18n.tr("settings.plugins.updating") : I18n.tr("settings.plugins.update")
icon: isUpdating ? "" : "download"
visible: modelData.updateInfo !== undefined
enabled: !isUpdating
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
onClicked: {
var pid = pluginId;
var pname = modelData.name;
var pversion = modelData.updateInfo?.availableVersion || "";
var rootRef = root;
var updates = Object.assign({}, rootRef.updatingPlugins);
updates[pid] = true;
rootRef.updatingPlugins = updates;
PluginService.updatePlugin(pid, function (success, error) {
var updates2 = Object.assign({}, rootRef.updatingPlugins);
updates2[pid] = false;
rootRef.updatingPlugins = updates2;
if (success) {
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.update-success", {
"plugin": pname,
"version": pversion
}));
} else {
ToastService.showError(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.update-error", {
"plugin": pname,
"error": error || "Unknown error"
}));
}
});
}
}
NToggle {
checked: modelData.enabled
baseSize: Style.baseWidgetSize * 0.7
onToggled: checked => {
if (checked) {
PluginService.enablePlugin(modelData.compositeKey);
} else {
PluginService.disablePlugin(modelData.compositeKey);
}
}
}
}
}
}
NLabel {
visible: PluginRegistry.getAllInstalledPluginIds().length === 0
label: I18n.tr("settings.plugins.installed.no-plugins-label")
description: I18n.tr("settings.plugins.installed.no-plugins-description")
Layout.fillWidth: true
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// ------------------------------
// Available Plugins (Sources + Filter + List)
// ------------------------------
NHeader {
label: I18n.tr("settings.plugins.available.label")
description: I18n.tr("settings.plugins.available.description")
}
// Sources
NCollapsible {
Layout.fillWidth: true
label: I18n.tr("settings.plugins.sources.label")
description: I18n.tr("settings.plugins.sources.description")
expanded: false
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
// List of plugin sources
Repeater {
id: pluginSourcesRepeater
model: PluginRegistry.pluginSources || []
delegate: RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
NIcon {
icon: "brand-github"
pointSize: Style.fontSizeM
}
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NText {
text: modelData.name
color: Color.mOnSurface
Layout.fillWidth: true
}
NText {
text: modelData.url
font.pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
}
Item {
Layout.fillWidth: true
}
NIconButton {
icon: "trash"
tooltipText: I18n.tr("settings.plugins.sources.remove.tooltip")
visible: index !== 0 // Cannot remove official source
baseSize: Style.baseWidgetSize * 0.7
onClicked: {
PluginRegistry.removePluginSource(modelData.url);
}
}
// Enable/Disable a source
NToggle {
checked: modelData.enabled !== false // Default to true if not set
baseSize: Style.baseWidgetSize * 0.7
onToggled: checked => {
PluginRegistry.setSourceEnabled(modelData.url, checked);
PluginService.refreshAvailablePlugins();
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.refresh.refreshing"));
}
}
}
}
NDivider {
Layout.fillWidth: true
}
// Add custom repository
NButton {
text: I18n.tr("settings.plugins.sources.add-custom")
icon: "plus"
onClicked: {
addSourceDialog.open();
}
Layout.fillWidth: true
}
}
}
// Filter controls
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
NTabBar {
id: filterTabBar
Layout.fillWidth: true
spacing: Style.marginM
currentIndex: 0
onCurrentIndexChanged: {
if (currentIndex === 0)
pluginFilter = "all";
else if (currentIndex === 1)
pluginFilter = "downloaded";
else if (currentIndex === 2)
pluginFilter = "notDownloaded";
}
NTabButton {
Layout.fillWidth: true
text: I18n.tr("settings.plugins.filter.all")
tabIndex: 0
checked: pluginFilter === "all"
}
NTabButton {
Layout.fillWidth: true
text: I18n.tr("settings.plugins.filter.downloaded")
tabIndex: 1
checked: pluginFilter === "downloaded"
}
NTabButton {
Layout.fillWidth: true
text: I18n.tr("settings.plugins.filter.not-downloaded")
tabIndex: 2
checked: pluginFilter === "notDownloaded"
}
}
NIconButton {
icon: "refresh"
tooltipText: I18n.tr("settings.plugins.refresh.tooltip")
baseSize: Style.baseWidgetSize * 0.9
onClicked: {
PluginService.refreshAvailablePlugins();
checkUpdatesTimer.restart();
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.refresh.refreshing"));
}
}
}
property string pluginFilter: "all"
property string pluginSearchText: ""
// Search input
NTextInput {
placeholderText: I18n.tr("placeholders.search")
inputIconName: "search"
text: root.pluginSearchText
onTextChanged: root.pluginSearchText = text
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL
}
// Available plugins list
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
Repeater {
id: availablePluginsRepeater
model: {
var all = PluginService.availablePlugins || [];
var filtered = [];
// First apply download filter
for (var i = 0; i < all.length; i++) {
var plugin = all[i];
var downloaded = plugin.downloaded || false;
if (pluginFilter === "all") {
filtered.push(plugin);
} else if (pluginFilter === "downloaded" && downloaded) {
filtered.push(plugin);
} else if (pluginFilter === "notDownloaded" && !downloaded) {
filtered.push(plugin);
}
}
// Then apply fuzzy search if there's search text
var query = root.pluginSearchText.trim();
if (query !== "") {
var results = Fuzzysort.go(query, filtered, {
"keys": ["name", "description"],
"threshold": 0.35,
"limit": 50
});
filtered = [];
for (var j = 0; j < results.length; j++) {
filtered.push(results[j].obj);
}
} else {
// Sort by lastUpdated (most recent first) when not searching
filtered.sort(function (a, b) {
var dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
var dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
return dateB - dateA;
});
}
return filtered;
}
delegate: NBox {
id: pluginBox
property bool isHovered: hoverHandler.hovered
Layout.fillWidth: true
Layout.leftMargin: Style.borderS
Layout.rightMargin: Style.borderS
implicitHeight: Math.round(contentColumn.implicitHeight + Style.marginL * 2)
color: Color.mSurface
Behavior on implicitHeight {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
HoverHandler {
id: hoverHandler
}
ColumnLayout {
id: contentColumn
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginS
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
NIcon {
icon: "plugin"
pointSize: Style.fontSizeL
color: Color.mOnSurface
}
NText {
text: modelData.name
color: Color.mOnSurface
elide: Text.ElideRight
}
// Description excerpt - visible when not hovered
NText {
visible: !pluginBox.isHovered && modelData.description
text: modelData.description || ""
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
elide: Text.ElideRight
Layout.fillWidth: true
}
// Spacer when hovered or no description
Item {
visible: pluginBox.isHovered || !modelData.description
Layout.fillWidth: true
}
// Downloaded indicator
NIcon {
icon: "circle-check"
pointSize: Style.fontSizeL
color: Color.mPrimary
visible: modelData.downloaded === true
}
// Install/Uninstall button
NIconButton {
icon: modelData.downloaded ? "trash" : "download"
baseSize: Style.baseWidgetSize * 0.7
tooltipText: modelData.downloaded ? I18n.tr("settings.plugins.uninstall") : I18n.tr("settings.plugins.install")
onClicked: {
if (modelData.downloaded) {
// Construct composite key for available plugins
var pluginData = Object.assign({}, modelData);
pluginData.compositeKey = PluginRegistry.generateCompositeKey(modelData.id, modelData.source?.url || "");
uninstallDialog.pluginToUninstall = pluginData;
uninstallDialog.open();
} else {
installPlugin(modelData);
}
}
}
}
// Description - visible on hover
NText {
visible: pluginBox.isHovered && modelData.description
text: modelData.description || ""
font.pointSize: Style.fontSizeXS
color: Color.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Details row - visible on hover
RowLayout {
visible: pluginBox.isHovered
spacing: Style.marginS
Layout.fillWidth: true
NText {
text: "v" + modelData.version
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: "•"
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: stripAuthorEmail(modelData.author)
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: "•"
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: modelData.source ? modelData.source.name : ""
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
Item {
Layout.fillWidth: true
}
}
}
}
}
NLabel {
visible: availablePluginsRepeater.count === 0
label: I18n.tr("settings.plugins.available.no-plugins-label")
description: I18n.tr("settings.plugins.available.no-plugins-description")
Layout.fillWidth: true
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// ------------------------------
// Dialogs
// ------------------------------
// Add source dialog
Popup {
id: addSourceDialog
parent: Overlay.overlay
modal: true
dim: false
anchors.centerIn: parent
width: 500
padding: Style.marginL
background: Rectangle {
color: Color.mSurface
radius: Style.radiusS
border.color: Color.mPrimary
border.width: Style.borderM
}
contentItem: ColumnLayout {
width: parent.width
spacing: Style.marginL
NHeader {
label: I18n.tr("settings.plugins.sources.add-dialog.title")
description: I18n.tr("settings.plugins.sources.add-dialog.description")
}
NTextInput {
id: sourceNameInput
label: I18n.tr("settings.plugins.sources.add-dialog.name")
placeholderText: I18n.tr("settings.plugins.sources.add-dialog.name-placeholder")
Layout.fillWidth: true
}
NTextInput {
id: sourceUrlInput
label: I18n.tr("settings.plugins.sources.add-dialog.url")
placeholderText: "https://github.com/user/repo"
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
NButton {
text: I18n.tr("common.cancel")
onClicked: addSourceDialog.close()
}
NButton {
text: I18n.tr("common.add")
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
enabled: sourceNameInput.text.length > 0 && sourceUrlInput.text.length > 0
onClicked: {
if (PluginRegistry.addPluginSource(sourceNameInput.text, sourceUrlInput.text)) {
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.sources.add-dialog.success"));
PluginService.refreshAvailablePlugins();
addSourceDialog.close();
sourceNameInput.text = "";
sourceUrlInput.text = "";
} else {
ToastService.showError(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.sources.add-dialog.error"));
}
}
}
}
}
}
// Uninstall confirmation dialog
Popup {
id: uninstallDialog
parent: Overlay.overlay
modal: true
dim: false
anchors.centerIn: parent
width: 400 * Style.uiScaleRatio
padding: Style.marginL
property var pluginToUninstall: null
background: Rectangle {
color: Color.mSurface
radius: Style.radiusS
border.color: Color.mPrimary
border.width: Style.borderM
}
contentItem: ColumnLayout {
width: parent.width
spacing: Style.marginL
NHeader {
label: I18n.tr("settings.plugins.uninstall-dialog.title")
description: I18n.tr("settings.plugins.uninstall-dialog.description", {
"plugin": uninstallDialog.pluginToUninstall?.name || ""
})
}
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
NButton {
text: I18n.tr("common.cancel")
onClicked: uninstallDialog.close()
}
NButton {
text: I18n.tr("settings.plugins.uninstall")
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
onClicked: {
if (uninstallDialog.pluginToUninstall) {
root.uninstallPlugin(uninstallDialog.pluginToUninstall.compositeKey);
uninstallDialog.close();
}
}
}
}
}
}
// Plugin settings popup
NPluginSettingsPopup {
id: pluginSettingsDialog
parent: Overlay.overlay
showToastOnSave: true
}
// Timer to check for updates after refresh starts
Timer {
id: checkUpdatesTimer
interval: 100
onTriggered: {
PluginService.checkForUpdates();
}
}
// Timer to recheck updates after available plugins are updated
Timer {
id: recheckUpdatesTimer
interval: 50
onTriggered: {
PluginService.checkForUpdates();
}
}
// ------------------------------
// Functions
// ------------------------------
function installPlugin(pluginMetadata) {
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.installing", {
"plugin": pluginMetadata.name
}));
PluginService.installPlugin(pluginMetadata, false, function (success, error, registeredKey) {
if (success) {
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.install-success", {
"plugin": pluginMetadata.name
}));
// Auto-enable the plugin after installation (use registered key which may be composite)
PluginService.enablePlugin(registeredKey);
} else {
ToastService.showError(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.install-error", {
"error": error || "Unknown error"
}));
}
});
}
function uninstallPlugin(pluginId) {
var manifest = PluginRegistry.getPluginManifest(pluginId);
var pluginName = manifest?.name || pluginId;
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.uninstalling", {
"plugin": pluginName
}));
PluginService.uninstallPlugin(pluginId, function (success, error) {
if (success) {
ToastService.showNotice(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.uninstall-success", {
"plugin": pluginName
}));
} else {
ToastService.showError(I18n.tr("settings.plugins.title"), I18n.tr("settings.plugins.uninstall-error", {
"error": error || "Unknown error"
}));
}
});
}
// Listen to plugin registry changes
Connections {
target: PluginRegistry
function onPluginsChanged() {
// Force model refresh for installed plugins by incrementing counter
root.installedPluginsRefreshCounter++;
// Force model refresh for plugin sources
pluginSourcesRepeater.model = undefined;
Qt.callLater(function () {
pluginSourcesRepeater.model = Qt.binding(function () {
return PluginRegistry.pluginSources || [];
});
});
}
}
// Listen to plugin service signals
Connections {
target: PluginService
function onAvailablePluginsUpdated() {
// Force model refresh for available plugins
availablePluginsRepeater.model = undefined;
Qt.callLater(function () {
availablePluginsRepeater.model = Qt.binding(function () {
var all = PluginService.availablePlugins || [];
var filtered = [];
for (var i = 0; i < all.length; i++) {
var plugin = all[i];
var downloaded = plugin.downloaded || false;
if (root.pluginFilter === "all") {
filtered.push(plugin);
} else if (root.pluginFilter === "downloaded" && downloaded) {
filtered.push(plugin);
} else if (root.pluginFilter === "notDownloaded" && !downloaded) {
filtered.push(plugin);
}
}
return filtered;
});
});
// Manually trigger update check after a small delay to ensure all registries are loaded
Qt.callLater(function () {
PluginService.checkForUpdates();
});
}
function onPluginUpdatesChanged() {
// Increment counter to force installed plugins model refresh
root.installedPluginsRefreshCounter++;
}
}
}
@@ -97,11 +97,6 @@ ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.region.clock-panel.section.label")
description: I18n.tr("settings.region.clock-panel.section.description")
}
NToggle {
label: I18n.tr("settings.location.date-time.use-analog.label")
description: I18n.tr("settings.location.date-time.use-analog.description")
@@ -126,8 +121,6 @@ ColumnLayout {
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Calendar Cards Management Section
@@ -135,11 +128,6 @@ ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.location.calendar.cards.section.label")
description: I18n.tr("settings.location.calendar.cards.section.description")
}
Connections {
target: Settings.data.location
function onWeatherEnabledChanged() {
@@ -9,11 +9,6 @@ ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.location.date-time.section.label")
description: I18n.tr("settings.location.date-time.section.description")
}
NToggle {
label: I18n.tr("settings.location.date-time.12hour-format.label")
description: I18n.tr("settings.location.date-time.12hour-format.description")
@@ -9,11 +9,6 @@ ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.general.language.section.label")
description: I18n.tr("settings.general.language.section.description")
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("settings.general.language.select.label")
@@ -10,11 +10,6 @@ ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.location.location.section.label")
description: I18n.tr("settings.location.location.section.description")
}
// Location section
RowLayout {
Layout.fillWidth: true
@@ -55,22 +50,10 @@ ColumnLayout {
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Weather section
ColumnLayout {
spacing: Style.marginM
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.location.weather.section.label")
description: I18n.tr("settings.location.weather.section.description")
}
NToggle {
label: I18n.tr("settings.location.weather.enabled.label")
description: I18n.tr("settings.location.weather.enabled.description")
@@ -15,12 +15,12 @@ ColumnLayout {
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.region.tabs.location")
text: I18n.tr("settings.region.tabs.language")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.region.tabs.language")
text: I18n.tr("settings.region.tabs.location")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
@@ -45,8 +45,8 @@ ColumnLayout {
id: tabView
currentIndex: subTabBar.currentIndex
LocationSubTab {}
LanguageSubTab {}
LocationSubTab {}
DateSubTab {}
ClockPanelSubTab {}
}
@@ -52,8 +52,6 @@ ColumnLayout {
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Video Settings
@@ -250,8 +248,6 @@ ColumnLayout {
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Audio Settings
@@ -325,7 +321,5 @@ ColumnLayout {
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
}
@@ -0,0 +1,261 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var entriesModel: []
property var updateEntry
property var reorderEntries
property var openEntrySettingsDialog
// List of items
Item {
Layout.fillWidth: true
implicitHeight: listView.contentHeight
ListView {
id: listView
anchors.fill: parent
spacing: Style.marginS
interactive: false
clip: true
model: root.entriesModel
delegate: Item {
id: delegateItem
width: listView.width
height: contentRow.height
required property int index
required property var modelData
property bool dragging: false
property int dragStartY: 0
property int dragStartIndex: -1
property int dragTargetIndex: -1
Rectangle {
anchors.fill: parent
radius: Style.radiusM
color: delegateItem.dragging ? Color.mSurfaceVariant : "transparent"
border.color: delegateItem.dragging ? Color.mOutline : "transparent"
border.width: Style.borderS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
RowLayout {
id: contentRow
width: parent.width
spacing: Style.marginS
// Drag handle
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 0.7
Layout.preferredHeight: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignVCenter
radius: Style.radiusXS
color: dragHandleMouseArea.containsMouse ? Color.mSurfaceVariant : "transparent"
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
ColumnLayout {
anchors.centerIn: parent
spacing: 2
Repeater {
model: 3
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 0.28
Layout.preferredHeight: 2
radius: 1
color: Color.mOutline
}
}
}
MouseArea {
id: dragHandleMouseArea
anchors.fill: parent
cursorShape: Qt.SizeVerCursor
hoverEnabled: true
preventStealing: false
z: 1000
onPressed: mouse => {
delegateItem.dragStartIndex = delegateItem.index;
delegateItem.dragTargetIndex = delegateItem.index;
delegateItem.dragStartY = delegateItem.y;
delegateItem.dragging = true;
delegateItem.z = 999;
preventStealing = true;
}
onPositionChanged: mouse => {
if (delegateItem.dragging) {
var dy = mouse.y - height / 2;
var newY = delegateItem.y + dy;
newY = Math.max(0, Math.min(newY, listView.contentHeight - delegateItem.height));
delegateItem.y = newY;
var targetIndex = Math.floor((newY + delegateItem.height / 2) / (delegateItem.height + Style.marginS));
targetIndex = Math.max(0, Math.min(targetIndex, listView.count - 1));
delegateItem.dragTargetIndex = targetIndex;
}
}
onReleased: {
preventStealing = false;
if (delegateItem.dragStartIndex !== -1 && delegateItem.dragTargetIndex !== -1 && delegateItem.dragStartIndex !== delegateItem.dragTargetIndex) {
root.reorderEntries(delegateItem.dragStartIndex, delegateItem.dragTargetIndex);
}
delegateItem.dragging = false;
delegateItem.dragStartIndex = -1;
delegateItem.dragTargetIndex = -1;
delegateItem.z = 0;
}
onCanceled: {
preventStealing = false;
delegateItem.dragging = false;
delegateItem.dragStartIndex = -1;
delegateItem.dragTargetIndex = -1;
delegateItem.z = 0;
}
}
}
// Enable checkbox
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 0.7
Layout.preferredHeight: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignVCenter
radius: Style.radiusXS
color: modelData.enabled ? Color.mPrimary : Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
NIcon {
visible: modelData.enabled
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
icon: "check"
color: Color.mOnPrimary
pointSize: Math.max(Style.fontSizeXS, Style.baseWidgetSize * 0.35)
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.updateEntry(index, {
"enabled": !modelData.enabled
});
}
}
}
// Label
NText {
Layout.fillWidth: true
text: modelData.text
color: Color.mOnSurface
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
// Countdown toggle with icon (only shown when global countdown is enabled)
RowLayout {
visible: Settings.data.sessionMenu.enableCountdown
spacing: Style.marginXS
Layout.alignment: Qt.AlignVCenter
NIcon {
icon: "clock"
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeS
}
NToggle {
checked: modelData.countdownEnabled !== undefined ? modelData.countdownEnabled : true
onToggled: checked => root.updateEntry(delegateItem.index, {
"countdownEnabled": checked
})
}
}
// Settings button (cogwheel)
NIconButton {
icon: "settings"
tooltipText: I18n.tr("settings.session-menu.entry-settings.tooltip")
baseSize: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignVCenter
onClicked: {
root.openEntrySettingsDialog(delegateItem.index);
}
}
}
// Position binding for non-dragging state
y: {
if (delegateItem.dragging) {
return delegateItem.y;
}
var draggedIndex = -1;
var targetIndex = -1;
for (var i = 0; i < listView.count; i++) {
var item = listView.itemAtIndex(i);
if (item && item.dragging) {
draggedIndex = item.dragStartIndex;
targetIndex = item.dragTargetIndex;
break;
}
}
if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) {
var currentIndex = delegateItem.index;
if (draggedIndex < targetIndex) {
if (currentIndex > draggedIndex && currentIndex <= targetIndex) {
return (currentIndex - 1) * (delegateItem.height + Style.marginS);
}
} else {
if (currentIndex >= targetIndex && currentIndex < draggedIndex) {
return (currentIndex + 1) * (delegateItem.height + Style.marginS);
}
}
}
return delegateItem.index * (delegateItem.height + Style.marginS);
}
Behavior on y {
enabled: !delegateItem.dragging
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
}
}
}
}
@@ -0,0 +1,129 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.session-menu.large-buttons-style.label")
description: I18n.tr("settings.session-menu.large-buttons-style.description")
checked: Settings.data.sessionMenu.largeButtonsStyle
onToggled: checked => Settings.data.sessionMenu.largeButtonsStyle = checked
}
NComboBox {
visible: Settings.data.sessionMenu.largeButtonsStyle
Layout.fillWidth: true
label: I18n.tr("settings.session-menu.large-buttons-layout.label")
description: I18n.tr("settings.session-menu.large-buttons-layout.description")
model: [
{
"key": "grid",
"name": I18n.tr("options.session-menu-grid-layout.grid")
},
{
"key": "single-row",
"name": I18n.tr("options.session-menu-grid-layout.single-row")
}
]
currentKey: Settings.data.sessionMenu.largeButtonsLayout
defaultValue: Settings.getDefaultValue("sessionMenu.largeButtonsLayout")
onSelected: key => Settings.data.sessionMenu.largeButtonsLayout = key
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.session-menu.show-number-labels.label")
description: I18n.tr("settings.session-menu.show-number-labels.description")
checked: Settings.data.sessionMenu.showNumberLabels !== false
defaultValue: Settings.getDefaultValue("sessionMenu.showNumberLabels") ?? true
onToggled: checked => Settings.data.sessionMenu.showNumberLabels = checked
}
NComboBox {
label: I18n.tr("settings.session-menu.position.label")
description: I18n.tr("settings.session-menu.position.description")
Layout.fillWidth: true
model: [
{
"key": "center",
"name": I18n.tr("options.control-center.position.center")
},
{
"key": "top_center",
"name": I18n.tr("options.control-center.position.top_center")
},
{
"key": "top_left",
"name": I18n.tr("options.control-center.position.top_left")
},
{
"key": "top_right",
"name": I18n.tr("options.control-center.position.top_right")
},
{
"key": "bottom_center",
"name": I18n.tr("options.control-center.position.bottom_center")
},
{
"key": "bottom_left",
"name": I18n.tr("options.control-center.position.bottom_left")
},
{
"key": "bottom_right",
"name": I18n.tr("options.control-center.position.bottom_right")
}
]
currentKey: Settings.data.sessionMenu.position
onSelected: key => Settings.data.sessionMenu.position = key
visible: !Settings.data.sessionMenu.largeButtonsStyle
defaultValue: Settings.getDefaultValue("sessionMenu.position")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.session-menu.show-header.label")
description: I18n.tr("settings.session-menu.show-header.description")
checked: Settings.data.sessionMenu.showHeader
onToggled: checked => Settings.data.sessionMenu.showHeader = checked
visible: !Settings.data.sessionMenu.largeButtonsStyle
defaultValue: Settings.getDefaultValue("sessionMenu.showHeader")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.session-menu.enable-countdown.label")
description: I18n.tr("settings.session-menu.enable-countdown.description")
checked: Settings.data.sessionMenu.enableCountdown
onToggled: checked => Settings.data.sessionMenu.enableCountdown = checked
defaultValue: Settings.getDefaultValue("sessionMenu.enableCountdown")
}
ColumnLayout {
visible: Settings.data.sessionMenu.enableCountdown
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.session-menu.countdown-duration.label")
description: I18n.tr("settings.session-menu.countdown-duration.description")
}
NValueSlider {
Layout.fillWidth: true
from: 1000
to: 30000
stepSize: 1000
value: Settings.data.sessionMenu.countdownDuration
onMoved: value => Settings.data.sessionMenu.countdownDuration = value
text: Math.round(Settings.data.sessionMenu.countdownDuration / 1000) + "s"
defaultValue: Settings.getDefaultValue("sessionMenu.countdownDuration")
}
}
}
@@ -8,8 +8,7 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
spacing: 0
property list<var> entriesModel: []
property list<var> entriesDefault: [
@@ -165,394 +164,39 @@ ColumnLayout {
saveEntries();
}
NHeader {
label: I18n.tr("settings.session-menu.general.section.label")
description: I18n.tr("settings.session-menu.general.section.description")
}
NToggle {
NTabBar {
id: subTabBar
Layout.fillWidth: true
label: I18n.tr("settings.session-menu.large-buttons-style.label")
description: I18n.tr("settings.session-menu.large-buttons-style.description")
checked: Settings.data.sessionMenu.largeButtonsStyle
onToggled: checked => Settings.data.sessionMenu.largeButtonsStyle = checked
}
distributeEvenly: true
currentIndex: tabView.currentIndex
NComboBox {
visible: Settings.data.sessionMenu.largeButtonsStyle
Layout.fillWidth: true
label: I18n.tr("settings.session-menu.large-buttons-layout.label")
description: I18n.tr("settings.session-menu.large-buttons-layout.description")
model: [
{
"key": "grid",
"name": I18n.tr("options.session-menu-grid-layout.grid")
},
{
"key": "single-row",
"name": I18n.tr("options.session-menu-grid-layout.single-row")
}
]
currentKey: Settings.data.sessionMenu.largeButtonsLayout
defaultValue: Settings.getDefaultValue("sessionMenu.largeButtonsLayout")
onSelected: key => Settings.data.sessionMenu.largeButtonsLayout = key
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.session-menu.show-number-labels.label")
description: I18n.tr("settings.session-menu.show-number-labels.description")
checked: Settings.data.sessionMenu.showNumberLabels !== false
defaultValue: Settings.getDefaultValue("sessionMenu.showNumberLabels") ?? true
onToggled: checked => Settings.data.sessionMenu.showNumberLabels = checked
}
NComboBox {
label: I18n.tr("settings.session-menu.position.label")
description: I18n.tr("settings.session-menu.position.description")
Layout.fillWidth: true
model: [
{
"key": "center",
"name": I18n.tr("options.control-center.position.center")
},
{
"key": "top_center",
"name": I18n.tr("options.control-center.position.top_center")
},
{
"key": "top_left",
"name": I18n.tr("options.control-center.position.top_left")
},
{
"key": "top_right",
"name": I18n.tr("options.control-center.position.top_right")
},
{
"key": "bottom_center",
"name": I18n.tr("options.control-center.position.bottom_center")
},
{
"key": "bottom_left",
"name": I18n.tr("options.control-center.position.bottom_left")
},
{
"key": "bottom_right",
"name": I18n.tr("options.control-center.position.bottom_right")
}
]
currentKey: Settings.data.sessionMenu.position
onSelected: key => Settings.data.sessionMenu.position = key
visible: !Settings.data.sessionMenu.largeButtonsStyle
defaultValue: Settings.getDefaultValue("sessionMenu.position")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.session-menu.show-header.label")
description: I18n.tr("settings.session-menu.show-header.description")
checked: Settings.data.sessionMenu.showHeader
onToggled: checked => Settings.data.sessionMenu.showHeader = checked
visible: !Settings.data.sessionMenu.largeButtonsStyle
defaultValue: Settings.getDefaultValue("sessionMenu.showHeader")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.session-menu.enable-countdown.label")
description: I18n.tr("settings.session-menu.enable-countdown.description")
checked: Settings.data.sessionMenu.enableCountdown
onToggled: checked => Settings.data.sessionMenu.enableCountdown = checked
defaultValue: Settings.getDefaultValue("sessionMenu.enableCountdown")
}
ColumnLayout {
visible: Settings.data.sessionMenu.enableCountdown
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.session-menu.countdown-duration.label")
description: I18n.tr("settings.session-menu.countdown-duration.description")
NTabButton {
text: I18n.tr("settings.session-menu.tabs.general")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NValueSlider {
Layout.fillWidth: true
from: 1000
to: 30000
stepSize: 1000
value: Settings.data.sessionMenu.countdownDuration
onMoved: value => Settings.data.sessionMenu.countdownDuration = value
text: Math.round(Settings.data.sessionMenu.countdownDuration / 1000) + "s"
defaultValue: Settings.getDefaultValue("sessionMenu.countdownDuration")
NTabButton {
text: I18n.tr("settings.session-menu.tabs.actions")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
NDivider {
Item {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
Layout.preferredHeight: Style.marginL
}
// Entries Management Section
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
NHeader {
label: I18n.tr("settings.session-menu.entries.section.label")
description: I18n.tr("settings.session-menu.entries.section.description")
GeneralSubTab {}
ActionsSubTab {
entriesModel: root.entriesModel
updateEntry: root.updateEntry
reorderEntries: root.reorderEntries
openEntrySettingsDialog: root.openEntrySettingsDialog
}
// List of items
Item {
Layout.fillWidth: true
implicitHeight: listView.contentHeight
ListView {
id: listView
anchors.fill: parent
spacing: Style.marginS
interactive: false
clip: true
model: entriesModel
delegate: Item {
id: delegateItem
width: listView.width
height: contentRow.height
required property int index
required property var modelData
property bool dragging: false
property int dragStartY: 0
property int dragStartIndex: -1
property int dragTargetIndex: -1
Rectangle {
anchors.fill: parent
radius: Style.radiusM
color: delegateItem.dragging ? Color.mSurfaceVariant : "transparent"
border.color: delegateItem.dragging ? Color.mOutline : "transparent"
border.width: Style.borderS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
RowLayout {
id: contentRow
width: parent.width
spacing: Style.marginS
// Drag handle
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 0.7
Layout.preferredHeight: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignVCenter
radius: Style.radiusXS
color: dragHandleMouseArea.containsMouse ? Color.mSurfaceVariant : "transparent"
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
ColumnLayout {
anchors.centerIn: parent
spacing: 2
Repeater {
model: 3
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 0.28
Layout.preferredHeight: 2
radius: 1
color: Color.mOutline
}
}
}
MouseArea {
id: dragHandleMouseArea
anchors.fill: parent
cursorShape: Qt.SizeVerCursor
hoverEnabled: true
preventStealing: false
z: 1000
onPressed: mouse => {
delegateItem.dragStartIndex = delegateItem.index;
delegateItem.dragTargetIndex = delegateItem.index;
delegateItem.dragStartY = delegateItem.y;
delegateItem.dragging = true;
delegateItem.z = 999;
preventStealing = true;
}
onPositionChanged: mouse => {
if (delegateItem.dragging) {
var dy = mouse.y - height / 2;
var newY = delegateItem.y + dy;
newY = Math.max(0, Math.min(newY, listView.contentHeight - delegateItem.height));
delegateItem.y = newY;
var targetIndex = Math.floor((newY + delegateItem.height / 2) / (delegateItem.height + Style.marginS));
targetIndex = Math.max(0, Math.min(targetIndex, listView.count - 1));
delegateItem.dragTargetIndex = targetIndex;
}
}
onReleased: {
preventStealing = false;
if (delegateItem.dragStartIndex !== -1 && delegateItem.dragTargetIndex !== -1 && delegateItem.dragStartIndex !== delegateItem.dragTargetIndex) {
root.reorderEntries(delegateItem.dragStartIndex, delegateItem.dragTargetIndex);
}
delegateItem.dragging = false;
delegateItem.dragStartIndex = -1;
delegateItem.dragTargetIndex = -1;
delegateItem.z = 0;
}
onCanceled: {
preventStealing = false;
delegateItem.dragging = false;
delegateItem.dragStartIndex = -1;
delegateItem.dragTargetIndex = -1;
delegateItem.z = 0;
}
}
}
// Enable checkbox
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 0.7
Layout.preferredHeight: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignVCenter
radius: Style.radiusXS
color: modelData.enabled ? Color.mPrimary : Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
NIcon {
visible: modelData.enabled
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
icon: "check"
color: Color.mOnPrimary
pointSize: Math.max(Style.fontSizeXS, Style.baseWidgetSize * 0.35)
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.updateEntry(index, {
"enabled": !modelData.enabled
});
}
}
}
// Label
NText {
Layout.fillWidth: true
text: modelData.text
color: Color.mOnSurface
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
// Countdown toggle with icon (only shown when global countdown is enabled)
RowLayout {
visible: Settings.data.sessionMenu.enableCountdown
spacing: Style.marginXS
Layout.alignment: Qt.AlignVCenter
NIcon {
icon: "clock"
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeS
}
NToggle {
checked: modelData.countdownEnabled !== undefined ? modelData.countdownEnabled : true
onToggled: checked => root.updateEntry(delegateItem.index, {
"countdownEnabled": checked
})
}
}
// Settings button (cogwheel)
NIconButton {
icon: "settings"
tooltipText: I18n.tr("settings.session-menu.entry-settings.tooltip")
baseSize: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignVCenter
onClicked: {
openEntrySettingsDialog(delegateItem.index);
}
}
}
// Position binding for non-dragging state
y: {
if (delegateItem.dragging) {
return delegateItem.y;
}
var draggedIndex = -1;
var targetIndex = -1;
for (var i = 0; i < listView.count; i++) {
var item = listView.itemAtIndex(i);
if (item && item.dragging) {
draggedIndex = item.dragStartIndex;
targetIndex = item.dragTargetIndex;
break;
}
}
if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) {
var currentIndex = delegateItem.index;
if (draggedIndex < targetIndex) {
if (currentIndex > draggedIndex && currentIndex <= targetIndex) {
return (currentIndex - 1) * (delegateItem.height + Style.marginS);
}
} else {
if (currentIndex >= targetIndex && currentIndex < draggedIndex) {
return (currentIndex + 1) * (delegateItem.height + Style.marginS);
}
}
}
return delegateItem.index * (delegateItem.height + Style.marginS);
}
Behavior on y {
enabled: !delegateItem.dragging
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
}
@@ -11,12 +11,6 @@ ColumnLayout {
property var screen
NHeader {
Layout.fillWidth: true
label: I18n.tr("settings.system-monitor.general.section.label")
description: I18n.tr("settings.system-monitor.general.section.description")
}
NToggle {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
@@ -95,4 +89,17 @@ ColumnLayout {
}
}
}
NDivider {
Layout.fillWidth: true
}
NTextInput {
label: I18n.tr("settings.system-monitor.external-monitor.label")
description: I18n.tr("settings.system-monitor.external-monitor.description")
placeholderText: I18n.tr("settings.system-monitor.external-monitor.placeholder")
text: Settings.data.systemMonitor.externalMonitor
defaultValue: Settings.getDefaultValue("systemMonitor.externalMonitor")
onTextChanged: Settings.data.systemMonitor.externalMonitor = text
}
}
@@ -10,12 +10,6 @@ ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
Layout.fillWidth: true
label: I18n.tr("settings.system-monitor.polling-section.label")
description: I18n.tr("settings.system-monitor.polling-section.description")
}
// CPU Polling
RowLayout {
Layout.fillWidth: true
@@ -171,17 +165,8 @@ ColumnLayout {
}
}
NDivider {
NLabel {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
}
NTextInput {
label: I18n.tr("settings.system-monitor.external-monitor.label")
description: I18n.tr("settings.system-monitor.external-monitor.description")
placeholderText: I18n.tr("settings.system-monitor.external-monitor.placeholder")
text: Settings.data.systemMonitor.externalMonitor
defaultValue: Settings.getDefaultValue("systemMonitor.externalMonitor")
onTextChanged: Settings.data.systemMonitor.externalMonitor = text
description: I18n.tr("settings.system-monitor.polling-section.description")
}
}
@@ -10,12 +10,6 @@ ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
Layout.fillWidth: true
label: I18n.tr("settings.system-monitor.thresholds-section.label")
description: I18n.tr("settings.system-monitor.thresholds-section.description")
}
// CPU Usage
NText {
Layout.fillWidth: true
@@ -37,6 +31,7 @@ ColumnLayout {
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.threshold.warning")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
NSpinBox {
@@ -65,6 +60,7 @@ ColumnLayout {
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.threshold.critical")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
NSpinBox {
@@ -101,6 +97,7 @@ ColumnLayout {
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.threshold.warning")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
NSpinBox {
@@ -129,6 +126,7 @@ ColumnLayout {
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.threshold.critical")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
NSpinBox {
@@ -167,6 +165,7 @@ ColumnLayout {
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.threshold.warning")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
NSpinBox {
@@ -195,6 +194,7 @@ ColumnLayout {
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.threshold.critical")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
NSpinBox {
@@ -231,6 +231,7 @@ ColumnLayout {
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.threshold.warning")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
NSpinBox {
@@ -259,6 +260,7 @@ ColumnLayout {
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.threshold.critical")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
NSpinBox {
@@ -295,6 +297,7 @@ ColumnLayout {
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.threshold.warning")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
NSpinBox {
@@ -323,6 +326,7 @@ ColumnLayout {
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.threshold.critical")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
NSpinBox {
@@ -337,4 +341,8 @@ ColumnLayout {
}
}
}
NLabel {
Layout.fillWidth: true
description: I18n.tr("settings.system-monitor.thresholds-section.description")
}
}
@@ -22,7 +22,6 @@ ColumnLayout {
description: I18n.tr("settings.wallpaper.settings.enable-management.description")
checked: Settings.data.wallpaper.enabled
onToggled: checked => Settings.data.wallpaper.enabled = checked
Layout.bottomMargin: Style.marginL
defaultValue: Settings.getDefaultValue("wallpaper.enabled")
}
@@ -32,7 +31,6 @@ ColumnLayout {
description: I18n.tr("settings.wallpaper.settings.enable-overview.description")
checked: Settings.data.wallpaper.overviewEnabled
onToggled: checked => Settings.data.wallpaper.overviewEnabled = checked
Layout.bottomMargin: Style.marginL
defaultValue: Settings.getDefaultValue("wallpaper.overviewEnabled")
}
@@ -35,7 +35,7 @@ ColumnLayout {
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.wallpaper.tabs.look-feel")
text: I18n.tr("settings.wallpaper.tabs.look")
tabIndex: 1
checked: subTabBar.currentIndex === 1
visible: Settings.data.wallpaper.enabled
+11 -7
View File
@@ -8,8 +8,8 @@ ColumnLayout {
property string label: ""
property string description: ""
property bool expanded: false
property bool defaultExpanded: false
property real contentSpacing: Style.marginM
property bool _userInteracted: false
signal toggled(bool expanded)
@@ -31,6 +31,7 @@ ColumnLayout {
// Smooth color transitions
Behavior on color {
enabled: root._userInteracted
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
@@ -38,6 +39,7 @@ ColumnLayout {
}
Behavior on border.color {
enabled: root._userInteracted
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
@@ -51,6 +53,7 @@ ColumnLayout {
hoverEnabled: true
onClicked: {
root._userInteracted = true;
root.expanded = !root.expanded;
root.toggled(root.expanded);
}
@@ -86,6 +89,7 @@ ColumnLayout {
rotation: root.expanded ? 90 : 0
Behavior on rotation {
enabled: root._userInteracted
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
@@ -93,6 +97,7 @@ ColumnLayout {
}
Behavior on color {
enabled: root._userInteracted
ColorAnimation {
duration: Style.animationNormal
}
@@ -113,6 +118,7 @@ ColumnLayout {
wrapMode: Text.WordWrap
Behavior on color {
enabled: root._userInteracted
ColorAnimation {
duration: Style.animationNormal
}
@@ -130,6 +136,7 @@ ColumnLayout {
opacity: 0.87
Behavior on color {
enabled: root._userInteracted
ColorAnimation {
duration: Style.animationNormal
}
@@ -152,10 +159,11 @@ ColumnLayout {
border.width: Style.borderS
// Dynamic height based on content
Layout.preferredHeight: visible ? contentLayout.implicitHeight + (Style.marginL * 2) : 0
Layout.preferredHeight: expanded ? contentLayout.implicitHeight + (Style.marginL * 2) : 0
// Smooth height animation
Behavior on Layout.preferredHeight {
enabled: root._userInteracted
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
@@ -173,15 +181,11 @@ ColumnLayout {
// Fade in animation for content
opacity: root.expanded ? 1.0 : 0.0
Behavior on opacity {
enabled: root._userInteracted
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
}
// Initialize expanded state
Component.onCompleted: {
root.expanded = root.defaultExpanded;
}
}
+2
View File
@@ -13,6 +13,8 @@ ColumnLayout {
property bool showIndicator: false
property string indicatorTooltip: ""
opacity: enabled ? 1.0 : 0.6
spacing: Style.marginXXS
Layout.fillWidth: true
-1
View File
@@ -9,7 +9,6 @@ RowLayout {
property string label: ""
property string description: ""
property bool enabled: true
property bool checked: false
property bool hovering: false
property int baseSize: Math.round(Style.baseWidgetSize * 0.8 * Style.uiScaleRatio)