fedora
This commit is contained in:
+48
@@ -0,0 +1,48 @@
|
||||
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
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.info")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.contributors")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.supporters")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
VersionSubTab {}
|
||||
ContributorsSubTab {}
|
||||
SupportersSubTab {}
|
||||
}
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
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: I18n.trp("panels.about.contributors-description", 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.margin2M
|
||||
height: nameText.implicitHeight + Style.margin2S
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var supporters: SupporterService.supporters
|
||||
property int avatarCacheVersion: 0
|
||||
|
||||
Connections {
|
||||
target: SupporterService
|
||||
function onCachedAvatarsChanged() {
|
||||
root.avatarCacheVersion++;
|
||||
}
|
||||
}
|
||||
|
||||
spacing: Style.marginL
|
||||
|
||||
NHeader {
|
||||
description: root.supporters.length === 0 ? I18n.tr("panels.about.supporters-loading") : I18n.trp("panels.about.supporters-desc", root.supporters.length)
|
||||
enableDescriptionRichText: true
|
||||
}
|
||||
|
||||
NButton {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
icon: "heart"
|
||||
text: I18n.tr("panels.about.become-supporter")
|
||||
outlined: true
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["xdg-open", "https://buymeacoffee.com/noctalia"]);
|
||||
ToastService.showNotice(I18n.tr("panels.about.support"), I18n.tr("toast.donation-opened"));
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
// Supporter cards
|
||||
Flow {
|
||||
id: supportersFlow
|
||||
visible: root.supporters.length > 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginL
|
||||
|
||||
Repeater {
|
||||
model: root.supporters.length
|
||||
|
||||
delegate: Rectangle {
|
||||
id: supporterCard
|
||||
|
||||
property bool hasGithub: !!root.supporters[index].github_username
|
||||
|
||||
width: Math.max(Math.round(supportersFlow.width / 2 - Style.marginL - 1), Math.round(Style.baseWidgetSize * 4.5))
|
||||
height: Math.round(Style.baseWidgetSize * 2.6)
|
||||
radius: Style.radiusM
|
||||
color: supporterArea.containsMouse && hasGithub ? Color.mHover : Qt.alpha(Color.mPrimary, 0.05)
|
||||
border.width: Style.borderM
|
||||
border.color: supporterArea.containsMouse && hasGithub ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.5)
|
||||
|
||||
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 with heart badge
|
||||
Item {
|
||||
id: avatarWrapper
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: Style.baseWidgetSize * 2.0
|
||||
Layout.preferredHeight: Style.baseWidgetSize * 2.0
|
||||
|
||||
property bool isRounded: false
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
visible: supporterCard.hasGithub
|
||||
source: {
|
||||
if (!supporterCard.hasGithub)
|
||||
return "";
|
||||
var _ = root.avatarCacheVersion;
|
||||
var username = root.supporters[index].github_username;
|
||||
var cached = SupporterService.getAvatarPath(username);
|
||||
if (cached) {
|
||||
avatarWrapper.isRounded = true;
|
||||
return cached;
|
||||
}
|
||||
return "https://github.com/" + username + ".png?size=256";
|
||||
}
|
||||
fillMode: Image.PreserveAspectFit
|
||||
mipmap: true
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
opacity: status === Image.Ready ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback logo for supporters without GitHub
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !supporterCard.hasGithub
|
||||
radius: width * 0.5
|
||||
color: Qt.alpha(Color.mPrimary, 0.15)
|
||||
|
||||
Image {
|
||||
anchors.centerIn: parent
|
||||
source: "../../../../../Assets/noctalia.svg"
|
||||
width: parent.width * 0.75
|
||||
height: width
|
||||
fillMode: Image.PreserveAspectFit
|
||||
mipmap: true
|
||||
smooth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: avatarWrapper.isRounded || !supporterCard.hasGithub
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
radius: width * 0.5
|
||||
border.width: Style.borderM
|
||||
border.color: Color.mPrimary
|
||||
}
|
||||
|
||||
// Heart badge
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -2
|
||||
width: Style.fontSizeM + Style.marginS
|
||||
height: width
|
||||
radius: width * 0.5
|
||||
color: Color.mPrimary
|
||||
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
icon: "heart-filled"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info column
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: root.supporters[index].name || root.supporters[index].github_username || "Unknown"
|
||||
font.weight: Style.fontWeightBold
|
||||
color: supporterArea.containsMouse && supporterCard.hasGithub ? Color.mOnHover : Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
pointSize: Style.fontSizeS
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.about.supporter-badge")
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mPrimary
|
||||
font.weight: Style.fontWeightMedium
|
||||
}
|
||||
}
|
||||
|
||||
// Hover indicator (only for supporters with GitHub)
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: "arrow-right"
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mPrimary
|
||||
visible: supporterCard.hasGithub
|
||||
opacity: supporterArea.containsMouse ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: supporterArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: supporterCard.hasGithub ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
var username = root.supporters[index].github_username;
|
||||
if (username) {
|
||||
Quickshell.execDetached(["xdg-open", "https://github.com/" + username]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+985
@@ -0,0 +1,985 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
opacity: 0
|
||||
|
||||
onSystemInfoLoadingChanged: {
|
||||
if (!systemInfoLoading)
|
||||
tabAppearAnim.start();
|
||||
}
|
||||
|
||||
NumberAnimation on opacity {
|
||||
id: tabAppearAnim
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
running: false
|
||||
}
|
||||
|
||||
property string latestVersion: GitHubService.latestVersion
|
||||
property string currentVersion: UpdateService.currentVersion
|
||||
property string commitInfo: ""
|
||||
property string qsVersion: ""
|
||||
property string qsRevision: ""
|
||||
|
||||
readonly property bool isGitVersion: root.currentVersion.endsWith("-git")
|
||||
readonly property int gigaB: (1024 * 1024 * 1024)
|
||||
readonly property int gigaD: (1000 * 1000 * 1000)
|
||||
|
||||
// Update status: compare versions
|
||||
readonly property bool updateAvailable: {
|
||||
if (!root.latestVersion || !root.currentVersion || root.latestVersion === I18n.tr("common.unknown"))
|
||||
return false;
|
||||
return UpdateService.compareVersions(root.latestVersion, root.currentVersion) > 0 && !root.isGitVersion;
|
||||
}
|
||||
readonly property bool isUpToDate: {
|
||||
if (!root.latestVersion || !root.currentVersion || root.latestVersion === I18n.tr("common.unknown"))
|
||||
return false;
|
||||
return UpdateService.compareVersions(root.latestVersion, root.currentVersion) <= 0;
|
||||
}
|
||||
|
||||
readonly property bool qsUpdateAvailable: {
|
||||
if (!GitHubService.latestQSVersion || !root.qsVersion || GitHubService.latestQSVersion === I18n.tr("common.unknown"))
|
||||
return false;
|
||||
return UpdateService.compareVersions(GitHubService.latestQSVersion, root.qsVersion) > 0;
|
||||
}
|
||||
|
||||
readonly property bool qsIsUpToDate: {
|
||||
if (!GitHubService.latestQSVersion || !root.qsVersion || GitHubService.latestQSVersion === I18n.tr("common.unknown"))
|
||||
return false;
|
||||
return UpdateService.compareVersions(GitHubService.latestQSVersion, root.qsVersion) <= 0;
|
||||
}
|
||||
|
||||
// System info properties
|
||||
property var systemInfo: null
|
||||
property bool systemInfoLoading: true
|
||||
property bool systemInfoAvailable: true
|
||||
|
||||
spacing: Style.marginL
|
||||
|
||||
function getModule(type) {
|
||||
if (!root.systemInfo)
|
||||
return null;
|
||||
return root.systemInfo.find(m => m.type === type);
|
||||
}
|
||||
|
||||
function getMonitorsText(separator) {
|
||||
const sep = separator || "\n";
|
||||
const screens = Quickshell.screens || [];
|
||||
const scales = CompositorService.displayScales || {};
|
||||
let lines = [];
|
||||
for (let i = 0; i < screens.length; i++) {
|
||||
const screen = screens[i];
|
||||
const name = screen.name || "Unknown";
|
||||
const scaleData = scales[name];
|
||||
const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0);
|
||||
lines.push(name + ": " + screen.width + "x" + screen.height + " @ " + scaleValue + "x");
|
||||
}
|
||||
return lines.join(sep);
|
||||
}
|
||||
|
||||
function getTelemetryPayload() {
|
||||
const screens = Quickshell.screens || [];
|
||||
const scales = CompositorService.displayScales || {};
|
||||
const monitors = [];
|
||||
for (let i = 0; i < screens.length; i++) {
|
||||
const screen = screens[i];
|
||||
const name = screen.name || "Unknown";
|
||||
const scaleData = scales[name];
|
||||
const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0);
|
||||
monitors.push({
|
||||
width: screen.width || 0,
|
||||
height: screen.height || 0,
|
||||
scale: scaleValue
|
||||
});
|
||||
}
|
||||
return {
|
||||
instanceId: TelemetryService.getInstanceId(),
|
||||
version: UpdateService.currentVersion,
|
||||
compositor: TelemetryService.getCompositorType(),
|
||||
os: HostService.osPretty || "Unknown",
|
||||
ramGb: Math.round((root.getModule("Memory")?.result?.total || 0) / root.gigaB),
|
||||
monitors: monitors,
|
||||
ui: {
|
||||
scaleRatio: Settings.data.general.scaleRatio,
|
||||
fontDefaultScale: Settings.data.ui.fontDefaultScale,
|
||||
fontFixedScale: Settings.data.ui.fontFixedScale
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function copyTelemetryData() {
|
||||
const payload = getTelemetryPayload();
|
||||
const json = JSON.stringify(payload, null, 2);
|
||||
Quickshell.execDetached(["wl-copy", json]);
|
||||
ToastService.showNotice(I18n.tr("panels.about.telemetry-title"), I18n.tr("panels.about.telemetry-data-copied"));
|
||||
}
|
||||
|
||||
function copyInfoToClipboard() {
|
||||
let info = "Noctalia Shell: " + root.currentVersion;
|
||||
if (root.isGitVersion && root.commitInfo) {
|
||||
info += " (" + root.commitInfo + ")";
|
||||
}
|
||||
info += "\n";
|
||||
|
||||
if (root.qsVersion) {
|
||||
let qsV = root.qsVersion.startsWith("v") ? root.qsVersion : "v" + root.qsVersion;
|
||||
info += "Noctalia QS: " + qsV;
|
||||
if (root.qsRevision) {
|
||||
info += " (" + root.qsRevision + ")";
|
||||
}
|
||||
info += "\n";
|
||||
}
|
||||
|
||||
info += "\nSystem Information\n";
|
||||
info += "==================\n";
|
||||
if (root.systemInfo) {
|
||||
const os = root.getModule("OS");
|
||||
const kernel = root.getModule("Kernel");
|
||||
const title = root.getModule("Title");
|
||||
const product = root.getModule("Host");
|
||||
const board = root.getModule("Board");
|
||||
const cpu = root.getModule("CPU");
|
||||
const gpu = root.getModule("GPU");
|
||||
const mem = root.getModule("Memory");
|
||||
const wm = root.getModule("WM");
|
||||
info += "OS: " + (os?.result?.prettyName || "N/A") + "\n";
|
||||
info += "Kernel: " + (kernel?.result?.release || "N/A") + "\n";
|
||||
info += "Host: " + (title?.result?.hostName || "N/A") + "\n";
|
||||
info += "Product: " + (product?.result?.name || "N/A") + "\n";
|
||||
info += "Board: " + (board?.result?.name || "N/A") + "\n";
|
||||
info += "CPU: " + (cpu?.result?.cpu || "N/A") + "\n";
|
||||
if (gpu?.result && Array.isArray(gpu.result) && gpu.result.length > 0) {
|
||||
info += "GPU: " + gpu.result.map(g => g.name || "Unknown").join(", ") + "\n";
|
||||
}
|
||||
if (mem?.result) {
|
||||
info += "Memory: " + (mem.result.total / root.gigaB).toFixed(1) + " GB \n";
|
||||
}
|
||||
if (wm?.result) {
|
||||
info += "WM: " + (wm.result.prettyName || wm.result.processName || "N/A") + "\n";
|
||||
}
|
||||
}
|
||||
const monitors = getMonitorsText("\n").split("\n");
|
||||
for (const mon of monitors) {
|
||||
info += "Monitor: " + mon + "\n";
|
||||
}
|
||||
info += "\nSettings\n";
|
||||
info += "========\n";
|
||||
info += "UI Scale: " + Settings.data.general.scaleRatio + "\n";
|
||||
info += "Default Font: " + (Settings.data.ui.fontDefault || "default") + " @ " + Settings.data.ui.fontDefaultScale + "x\n";
|
||||
info += "Fixed Font: " + (Settings.data.ui.fontFixed || "default") + " @ " + Settings.data.ui.fontFixedScale + "x\n";
|
||||
Quickshell.execDetached(["wl-copy", info]);
|
||||
ToastService.showNotice(I18n.tr("panels.about.title"), I18n.tr("panels.about.info-copied"));
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Check if fastfetch is available before trying to run it
|
||||
checkFastfetchProcess.running = true;
|
||||
qsVersionProcess.running = true;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: qsVersionProcess
|
||||
command: ["qs", "--version"]
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
var output = stdout.text.trim();
|
||||
// Format (old): "noctalia-qs 0.3.0, revision abc12345, distributed by: ..."
|
||||
// Format (new): "noctalia-qs 0.0.9 (revision b602b69c81d96a1d7c645328feb7b1e1d4b7b7a4, distributed by Unset)"
|
||||
// Only set if this is actually noctalia-qs; leave empty for upstream quickshell
|
||||
var match = output.match(/noctalia-qs\s+(\S+?)[\s,(]+revision\s*([0-9a-f]*)/i);
|
||||
if (match) {
|
||||
root.qsVersion = match[1];
|
||||
root.qsRevision = match[2] ? match[2].substring(0, 9) : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
// Check if fastfetch is available before attempting to run it
|
||||
Process {
|
||||
id: checkFastfetchProcess
|
||||
command: ["sh", "-c", "command -v fastfetch"]
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
// fastfetch is available, run it
|
||||
Logger.d("VersionSubTab", "fastfetch found, running it");
|
||||
fastfetchProcess.running = true;
|
||||
} else {
|
||||
// fastfetch not found, show error state immediately
|
||||
Logger.w("VersionSubTab", "fastfetch not found");
|
||||
root.systemInfoLoading = false;
|
||||
root.systemInfoAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fastfetchProcess
|
||||
command: ["fastfetch", "--format", "json", "--config", Quickshell.shellDir + "/Assets/Services/fastfetch/system-info.jsonc"]
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
root.systemInfoLoading = false;
|
||||
if (exitCode === 0) {
|
||||
try {
|
||||
root.systemInfo = JSON.parse(stdout.text);
|
||||
root.systemInfoAvailable = true;
|
||||
} catch (e) {
|
||||
Logger.w("VersionSubTab", "Failed to parse fastfetch JSON: " + e);
|
||||
root.systemInfoAvailable = false;
|
||||
}
|
||||
} else {
|
||||
root.systemInfoAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginXL
|
||||
|
||||
// Noctalia logo
|
||||
Image {
|
||||
source: "../../../../../Assets/noctalia.svg"
|
||||
width: 96 * Style.uiScaleRatio
|
||||
height: width
|
||||
fillMode: Image.PreserveAspectFit
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
mipmap: true
|
||||
smooth: true
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
rotation: Settings.isDebug ? 180 : 0
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
|
||||
property int debugTapCount: 0
|
||||
|
||||
Timer {
|
||||
id: debugTapTimer
|
||||
interval: 5000
|
||||
onTriggered: parent.debugTapCount = 0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if (parent.debugTapCount === 0) {
|
||||
debugTapTimer.restart();
|
||||
}
|
||||
parent.debugTapCount++;
|
||||
if (parent.debugTapCount >= 8) {
|
||||
parent.debugTapCount = 0;
|
||||
debugTapTimer.stop();
|
||||
Settings.isDebug = !Settings.isDebug;
|
||||
if (Settings.isDebug) {
|
||||
ToastService.showNotice("Debug", I18n.tr("panels.about.debug-enabled"));
|
||||
} else {
|
||||
ToastService.showNotice("Debug", I18n.tr("panels.about.debug-disabled"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
NHeader {
|
||||
label: "Noctalia Shell"
|
||||
}
|
||||
|
||||
// Versions
|
||||
GridLayout {
|
||||
columns: 2
|
||||
rowSpacing: Style.marginXS
|
||||
columnSpacing: Style.marginM
|
||||
|
||||
// Installed Version (Shell)
|
||||
NText {
|
||||
text: "Noctalia Shell:"
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: root.currentVersion
|
||||
color: Color.mOnSurface
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
// Git commit in parentheses
|
||||
NText {
|
||||
id: commitText
|
||||
visible: root.isGitVersion
|
||||
text: "(" + (root.commitInfo || I18n.tr("common.loading")) + ")"
|
||||
color: commitMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeXS
|
||||
font.underline: commitMouseArea.containsMouse && root.commitInfo
|
||||
|
||||
MouseArea {
|
||||
id: commitMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: root.commitInfo ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onEntered: {
|
||||
if (root.commitInfo) {
|
||||
TooltipService.show(commitText, I18n.tr("panels.about.view-commit"));
|
||||
}
|
||||
}
|
||||
onExited: TooltipService.hide()
|
||||
onClicked: {
|
||||
if (root.commitInfo) {
|
||||
Quickshell.execDetached(["xdg-open", "https://github.com/noctalia-dev/noctalia-shell/commit/" + root.commitInfo]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update status indicator
|
||||
NIcon {
|
||||
id: upToDateIcon
|
||||
visible: root.isUpToDate
|
||||
icon: "circle-check"
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mPrimary
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(upToDateIcon, I18n.tr("panels.about.up-to-date"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
id: updateAvailableIcon
|
||||
visible: root.updateAvailable
|
||||
icon: "arrow-up-circle"
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mPrimary
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(updateAvailableIcon, I18n.tr("panels.about.update-available"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Latest Version (Shell)
|
||||
NText {
|
||||
visible: root.updateAvailable
|
||||
text: I18n.tr("panels.about.noctalia-available")
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: root.updateAvailable
|
||||
text: root.latestVersion
|
||||
color: Color.mOnSurface
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
// Divider-like spacing
|
||||
Item {
|
||||
visible: root.qsUpdateAvailable || root.updateAvailable
|
||||
Layout.columnSpan: 2
|
||||
Layout.preferredHeight: Style.marginXS
|
||||
}
|
||||
|
||||
// Quickshell Version
|
||||
NText {
|
||||
visible: root.qsVersion !== ""
|
||||
text: "Noctalia QS:"
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: root.qsVersion !== ""
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: root.qsVersion.startsWith("v") ? root.qsVersion : "v" + root.qsVersion
|
||||
color: Color.mOnSurface
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
// Git revision in parentheses
|
||||
NText {
|
||||
id: qsRevisionText
|
||||
visible: root.qsRevision !== ""
|
||||
text: "(" + root.qsRevision + ")"
|
||||
color: qsRevisionMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeXS
|
||||
font.underline: qsRevisionMouseArea.containsMouse
|
||||
|
||||
MouseArea {
|
||||
id: qsRevisionMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: TooltipService.show(qsRevisionText, I18n.tr("panels.about.view-commit"))
|
||||
onExited: TooltipService.hide()
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["xdg-open", "https://github.com/noctalia-dev/noctalia-qs/commit/" + root.qsRevision]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update status indicator
|
||||
NIcon {
|
||||
id: qsUpToDateIcon
|
||||
visible: root.qsIsUpToDate
|
||||
icon: "circle-check"
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mPrimary
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(qsUpToDateIcon, I18n.tr("panels.about.up-to-date"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
id: qsUpdateAvailableIcon
|
||||
visible: root.qsUpdateAvailable
|
||||
icon: "arrow-up-circle"
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mPrimary
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(qsUpdateAvailableIcon, I18n.tr("panels.about.update-available"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Latest Quickshell Version
|
||||
NText {
|
||||
visible: root.qsUpdateAvailable
|
||||
text: I18n.tr("panels.about.noctalia-available")
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: root.qsUpdateAvailable
|
||||
text: GitHubService.latestQSVersion
|
||||
color: Color.mOnSurface
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: actionsGrid
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Style.marginM
|
||||
Layout.bottomMargin: Style.marginM
|
||||
rowSpacing: Style.marginM
|
||||
columnSpacing: Style.marginM
|
||||
|
||||
columns: (changelogBtn.implicitWidth + copyBtn.implicitWidth + supportBtn.implicitWidth + 2 * columnSpacing) < root.width ? 3 : 1
|
||||
|
||||
NButton {
|
||||
id: changelogBtn
|
||||
icon: "sparkles"
|
||||
text: I18n.tr("panels.about.changelog")
|
||||
outlined: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: {
|
||||
var screen = PanelService.openedPanel?.screen || SettingsPanelService.settingsWindow?.screen || PanelService.findScreenForPanels();
|
||||
SettingsPanelService.close(screen);
|
||||
UpdateService.viewChangelog(screen);
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
id: copyBtn
|
||||
icon: "copy"
|
||||
text: I18n.tr("panels.about.copy-info")
|
||||
outlined: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: root.copyInfoToClipboard()
|
||||
}
|
||||
|
||||
NButton {
|
||||
id: supportBtn
|
||||
icon: "heart"
|
||||
text: I18n.tr("panels.about.support")
|
||||
outlined: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["xdg-open", "https://buymeacoffee.com/noctalia"]);
|
||||
ToastService.showNotice(I18n.tr("panels.about.support"), I18n.tr("toast.donation-opened"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.about.changelog-on-startup")
|
||||
description: I18n.tr("panels.about.changelog-on-startup-desc")
|
||||
checked: Settings.data.general.showChangelogOnStartup
|
||||
onToggled: checked => Settings.data.general.showChangelogOnStartup = checked
|
||||
}
|
||||
|
||||
// System Information Section
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("panels.about.system-title")
|
||||
}
|
||||
|
||||
// Error state (fastfetch not installed)
|
||||
ColumnLayout {
|
||||
visible: !root.systemInfoAvailable
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-not-installed")
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-install-hint")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeXS
|
||||
}
|
||||
}
|
||||
|
||||
// System info grid
|
||||
GridLayout {
|
||||
id: sysInfo
|
||||
readonly property real textSize: Style.fontSizeS
|
||||
|
||||
visible: root.systemInfoAvailable && root.systemInfo
|
||||
Layout.fillWidth: true
|
||||
columns: 2
|
||||
rowSpacing: Style.marginXS
|
||||
columnSpacing: Style.marginM
|
||||
|
||||
// OS
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-os")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const os = root.getModule("OS");
|
||||
return os?.result?.prettyName || "N/A";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Kernel
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-kernel")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const kernel = root.getModule("Kernel");
|
||||
return kernel?.result?.release || "N/A";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Host
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-host")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const title = root.getModule("Title");
|
||||
return title?.result?.hostName || "N/A";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Product name
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-product")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const title = root.getModule("Host");
|
||||
return title?.result?.name || "N/A";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Board name
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-board")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const title = root.getModule("Board");
|
||||
return title?.result?.name || "N/A";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Uptime
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-uptime")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const value = root.getModule("Uptime")?.result?.uptime;
|
||||
return value ? Time.formatVagueHumanReadableDuration(value / 1000) : "-";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// CPU
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-cpu")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const cpu = root.getModule("CPU");
|
||||
if (!cpu?.result)
|
||||
return "N/A";
|
||||
let cpuText = cpu.result.cpu || "N/A";
|
||||
const cores = cpu.result.cores;
|
||||
if (cores?.logical) {
|
||||
cpuText += " (" + cores.logical + " threads)";
|
||||
}
|
||||
return cpuText;
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// GPU
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-gpu")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const gpu = root.getModule("GPU");
|
||||
if (!gpu?.result || !Array.isArray(gpu.result) || gpu.result.length === 0)
|
||||
return "N/A";
|
||||
return gpu.result.map(g => g.name || "Unknown").join(", ");
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Memory
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-memory")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const mem = root.getModule("Memory");
|
||||
if (!mem?.result)
|
||||
return "N/A";
|
||||
const used = (mem.result.used / root.gigaB).toFixed(1);
|
||||
const total = (mem.result.total / root.gigaB).toFixed(1);
|
||||
return used + " GiB / " + total + " GiB";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Disk
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-disk")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const disk = root.getModule("Disk");
|
||||
if (!disk?.result || !Array.isArray(disk.result) || disk.result.length === 0)
|
||||
return "N/A";
|
||||
const rootDisk = disk.result.find(d => d.mountpoint === "/");
|
||||
if (!rootDisk?.bytes)
|
||||
return "N/A";
|
||||
const used = (rootDisk.bytes.used / root.gigaD).toFixed(1);
|
||||
const total = (rootDisk.bytes.total / root.gigaD).toFixed(1);
|
||||
return used + " GB / " + total + " GB" + " (" + rootDisk.filesystem + ")";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// WM
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-wm")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const wm = root.getModule("WM");
|
||||
if (!wm?.result)
|
||||
return "N/A";
|
||||
let wmText = wm.result.prettyName || wm.result.processName || "N/A";
|
||||
if (wm.result.protocolName) {
|
||||
wmText += " (" + wm.result.protocolName + ")";
|
||||
}
|
||||
return wmText;
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Packages
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-packages")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const pkg = root.getModule("Packages");
|
||||
if (!pkg?.result)
|
||||
return "N/A";
|
||||
const result = pkg.result;
|
||||
if (result.all) {
|
||||
const managers = [];
|
||||
if (result.rpm > 0)
|
||||
managers.push("rpm: " + result.rpm);
|
||||
if (result.pacman > 0)
|
||||
managers.push("pacman: " + result.pacman);
|
||||
if (result.dpkg > 0)
|
||||
managers.push("dpkg: " + result.dpkg);
|
||||
if (result.flatpakSystem > 0 || result.flatpakUser > 0) {
|
||||
const flatpak = (result.flatpakSystem || 0) + (result.flatpakUser || 0);
|
||||
managers.push("flatpak: " + flatpak);
|
||||
}
|
||||
if (result.snap > 0)
|
||||
managers.push("snap: " + result.snap);
|
||||
if (result.nixSystem > 0 || result.nixUser > 0 || result.nixDefault > 0) {
|
||||
const nix = (result.nixSystem || 0) + (result.nixUser || 0) + (result.nixDefault || 0);
|
||||
managers.push("nix: " + nix);
|
||||
}
|
||||
if (result.brew > 0)
|
||||
managers.push("brew: " + result.brew);
|
||||
if (managers.length > 0) {
|
||||
return result.all + " (" + managers.join(", ") + ")";
|
||||
}
|
||||
return result.all.toString();
|
||||
}
|
||||
return "N/A";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Monitors (2 items per screen: label + value)
|
||||
Repeater {
|
||||
model: Quickshell.screens.length * 2
|
||||
|
||||
NText {
|
||||
readonly property int screenIndex: Math.floor(index / 2)
|
||||
readonly property bool isLabel: index % 2 === 0
|
||||
readonly property var screen: Quickshell.screens[screenIndex]
|
||||
|
||||
text: {
|
||||
if (isLabel)
|
||||
return I18n.tr("panels.about.system-monitor");
|
||||
const name = screen?.name || "Unknown";
|
||||
const scales = CompositorService.displayScales || {};
|
||||
const scaleData = scales[name];
|
||||
const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0);
|
||||
return name + ": " + (screen?.width || 0) + "x" + (screen?.height || 0) + " @ " + scaleValue + "x";
|
||||
}
|
||||
color: isLabel ? Color.mOnSurfaceVariant : Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: !isLabel
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Telemetry Section
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginL
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("panels.about.telemetry-title")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.about.telemetry-enabled")
|
||||
description: I18n.tr("panels.about.telemetry-desc")
|
||||
checked: Settings.data.general.telemetryEnabled
|
||||
onToggled: checked => Settings.data.general.telemetryEnabled = checked
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NButton {
|
||||
icon: "eye"
|
||||
text: I18n.tr("panels.about.telemetry-show-data")
|
||||
outlined: true
|
||||
onClicked: root.copyTelemetryData()
|
||||
}
|
||||
|
||||
NButton {
|
||||
icon: "shield-lock"
|
||||
text: I18n.tr("panels.about.privacy-policy")
|
||||
outlined: true
|
||||
onClicked: Quickshell.execDetached(["xdg-open", "https://noctalia.dev/privacy"])
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
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
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.volumes")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.devices")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.media")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.visualizer")
|
||||
tabIndex: 3
|
||||
checked: subTabBar.currentIndex === 3
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
VolumesSubTab {}
|
||||
DevicesSubTab {}
|
||||
MediaSubTab {}
|
||||
VisualizerSubTab {}
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Services.Media
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Output Devices
|
||||
ButtonGroup {
|
||||
id: sinks
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXS
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginL
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.audio.devices-output-device-label")
|
||||
description: I18n.tr("panels.audio.devices-output-device-description")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: AudioService.sinks
|
||||
NRadioButton {
|
||||
ButtonGroup.group: sinks
|
||||
required property PwNode modelData
|
||||
text: modelData.description
|
||||
checked: AudioService.sink?.id === modelData.id
|
||||
onClicked: {
|
||||
AudioService.setAudioSink(modelData);
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Input Devices
|
||||
ButtonGroup {
|
||||
id: sources
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.audio.devices-input-device-label")
|
||||
description: I18n.tr("panels.audio.devices-input-device-description")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: AudioService.sources
|
||||
NRadioButton {
|
||||
ButtonGroup.group: sources
|
||||
required property PwNode modelData
|
||||
text: modelData.description
|
||||
checked: AudioService.source?.id === modelData.id
|
||||
onClicked: AudioService.setAudioSource(modelData)
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Media
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Preferred player
|
||||
NTextInput {
|
||||
label: I18n.tr("panels.audio.media-primary-player-label")
|
||||
description: I18n.tr("panels.audio.media-primary-player-description")
|
||||
placeholderText: I18n.tr("panels.audio.media-primary-player-placeholder")
|
||||
text: Settings.data.audio.preferredPlayer
|
||||
defaultValue: Settings.getDefaultValue("audio.preferredPlayer")
|
||||
onTextChanged: {
|
||||
Settings.data.audio.preferredPlayer = text;
|
||||
MediaService.updateCurrentPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
// Blacklist editor
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NTextInputButton {
|
||||
id: blacklistInput
|
||||
label: I18n.tr("panels.audio.media-excluded-player-label")
|
||||
description: I18n.tr("panels.audio.media-excluded-player-description")
|
||||
placeholderText: I18n.tr("panels.audio.media-excluded-player-placeholder")
|
||||
buttonIcon: "add"
|
||||
Layout.fillWidth: true
|
||||
onButtonClicked: {
|
||||
const val = (blacklistInput.text || "").trim();
|
||||
if (val !== "") {
|
||||
const arr = (Settings.data.audio.mprisBlacklist || []);
|
||||
if (!arr.find(x => String(x).toLowerCase() === val.toLowerCase())) {
|
||||
Settings.data.audio.mprisBlacklist = [...arr, val];
|
||||
blacklistInput.text = "";
|
||||
MediaService.updateCurrentPlayer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Current blacklist entries
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.audio.mprisBlacklist
|
||||
delegate: Rectangle {
|
||||
required property string modelData
|
||||
property real pad: Style.marginS
|
||||
color: Qt.alpha(Color.mOnSurface, 0.125)
|
||||
border.color: Qt.alpha(Color.mOnSurface, Style.opacityLight)
|
||||
border.width: Style.borderS
|
||||
|
||||
RowLayout {
|
||||
id: chipRow
|
||||
spacing: Style.marginXS
|
||||
anchors.fill: parent
|
||||
anchors.margins: pad
|
||||
|
||||
NText {
|
||||
text: modelData
|
||||
color: Color.mOnSurface
|
||||
pointSize: Style.fontSizeS
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: Style.marginS
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: Style.marginXS
|
||||
onClicked: {
|
||||
const arr = (Settings.data.audio.mprisBlacklist || []);
|
||||
const idx = arr.findIndex(x => String(x) === modelData);
|
||||
if (idx >= 0) {
|
||||
arr.splice(idx, 1);
|
||||
Settings.data.audio.mprisBlacklist = arr;
|
||||
MediaService.updateCurrentPlayer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: chipRow.implicitWidth + pad * 2
|
||||
implicitHeight: Math.max(chipRow.implicitHeight + pad * 2, Style.baseWidgetSize * 0.8)
|
||||
radius: Style.radiusM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
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("panels.audio.visualizer-type-label")
|
||||
description: I18n.tr("panels.audio.visualizer-type-description")
|
||||
model: [
|
||||
{
|
||||
"key": "none",
|
||||
"name": I18n.tr("common.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
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.audio.spectrum-mirrored-label")
|
||||
description: I18n.tr("panels.audio.spectrum-mirrored-description")
|
||||
checked: Settings.data.audio.spectrumMirrored
|
||||
defaultValue: Settings.getDefaultValue("audio.spectrumMirrored")
|
||||
onToggled: Settings.data.audio.spectrumMirrored = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.audio.media-frame-rate-label")
|
||||
description: I18n.tr("panels.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.spectrumFrameRate
|
||||
defaultValue: Settings.getDefaultValue("audio.spectrumFrameRate")
|
||||
onSelected: key => Settings.data.audio.spectrumFrameRate = key
|
||||
}
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Media
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property real localVolume: AudioService.volume
|
||||
|
||||
Connections {
|
||||
target: AudioService
|
||||
function onSinkChanged() {
|
||||
localVolume = AudioService.volume;
|
||||
}
|
||||
function onVolumeChanged() {
|
||||
localVolume = AudioService.volume;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
|
||||
function onVolumeChanged() {
|
||||
localVolume = AudioService.volume;
|
||||
}
|
||||
}
|
||||
|
||||
// Output Volume
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.osd.types-volume-label")
|
||||
description: I18n.tr("panels.audio.volumes-output-volume-description")
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: localVolume
|
||||
stepSize: 0.01
|
||||
text: Math.round(AudioService.volume * 100) + "%"
|
||||
onMoved: value => localVolume = value
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 100
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (!AudioService.isSwitchingSink && Math.abs(localVolume - AudioService.volume) >= 0.01) {
|
||||
AudioService.setVolume(localVolume);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mute Toggle
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.audio.volumes-mute-output-label")
|
||||
description: I18n.tr("panels.audio.volumes-mute-output-description")
|
||||
checked: AudioService.muted
|
||||
onToggled: checked => AudioService.setOutputMuted(checked)
|
||||
}
|
||||
}
|
||||
|
||||
// Volume Feedback sound Toggle
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.audio.volumes-volume-feedback-label")
|
||||
description: I18n.tr("panels.audio.volumes-volume-feedback-description")
|
||||
checked: Settings.data.audio.volumeFeedback
|
||||
defaultValue: Settings.getDefaultValue("audio.volumeFeedback")
|
||||
onToggled: checked => Settings.data.audio.volumeFeedback = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
enabled: SoundService.multimediaAvailable && Settings.data.audio.volumeFeedback
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.audio.volumes-feedback-sound-file-label")
|
||||
description: I18n.tr("panels.audio.volumes-feedback-sound-file-description")
|
||||
}
|
||||
|
||||
NTextInputButton {
|
||||
enabled: parent.enabled
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder")
|
||||
text: Settings.data.audio.volumeFeedbackSoundFile ?? ""
|
||||
buttonIcon: "folder-open"
|
||||
buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file")
|
||||
onInputTextChanged: text => Settings.data.audio.volumeFeedbackSoundFile = text
|
||||
onButtonClicked: volumeFeedbackFilePicker.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Input Volume
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.osd.types-input-volume-label")
|
||||
description: I18n.tr("panels.audio.volumes-input-volume-description")
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: AudioService.inputVolume
|
||||
stepSize: 0.01
|
||||
text: Math.round(AudioService.inputVolume * 100) + "%"
|
||||
onMoved: value => AudioService.setInputVolume(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Input Mute Toggle
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.audio.volumes-mute-input-label")
|
||||
description: I18n.tr("panels.audio.volumes-mute-input-description")
|
||||
checked: AudioService.inputMuted
|
||||
onToggled: checked => AudioService.setInputMuted(checked)
|
||||
}
|
||||
}
|
||||
|
||||
// Volume Step Size
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NSpinBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.audio.volumes-step-size-label")
|
||||
description: I18n.tr("panels.audio.volumes-step-size-description")
|
||||
minimum: 1
|
||||
maximum: 25
|
||||
value: Settings.data.audio.volumeStep
|
||||
stepSize: 1
|
||||
suffix: "%"
|
||||
defaultValue: Settings.getDefaultValue("audio.volumeStep")
|
||||
onValueChanged: Settings.data.audio.volumeStep = value
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Raise maximum volume above 100%
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.audio.volumes-volume-overdrive-label")
|
||||
description: I18n.tr("panels.audio.volumes-volume-overdrive-description")
|
||||
checked: Settings.data.audio.volumeOverdrive
|
||||
defaultValue: Settings.getDefaultValue("audio.volumeOverdrive")
|
||||
onToggled: checked => Settings.data.audio.volumeOverdrive = checked
|
||||
}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: volumeFeedbackFilePicker
|
||||
title: I18n.tr("panels.audio.volumes-feedback-sound-file-select-title")
|
||||
selectionMode: "files"
|
||||
initialPath: Quickshell.env("HOME")
|
||||
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
Settings.data.audio.volumeFeedbackSoundFile = paths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-position-label")
|
||||
description: I18n.tr("panels.bar.appearance-position-description")
|
||||
model: [
|
||||
{
|
||||
"key": "top",
|
||||
"name": I18n.tr("positions.top")
|
||||
},
|
||||
{
|
||||
"key": "bottom",
|
||||
"name": I18n.tr("positions.bottom")
|
||||
},
|
||||
{
|
||||
"key": "left",
|
||||
"name": I18n.tr("positions.left")
|
||||
},
|
||||
{
|
||||
"key": "right",
|
||||
"name": I18n.tr("positions.right")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.bar.position
|
||||
defaultValue: Settings.getDefaultValue("bar.position")
|
||||
onSelected: key => Settings.data.bar.position = key
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-density-label")
|
||||
description: I18n.tr("panels.bar.appearance-density-description")
|
||||
model: [
|
||||
{
|
||||
"key": "mini",
|
||||
"name": I18n.tr("options.bar.density-mini")
|
||||
},
|
||||
{
|
||||
"key": "compact",
|
||||
"name": I18n.tr("options.bar.density-compact")
|
||||
},
|
||||
{
|
||||
"key": "default",
|
||||
"name": I18n.tr("options.bar.density-default")
|
||||
},
|
||||
{
|
||||
"key": "comfortable",
|
||||
"name": I18n.tr("options.bar.density-comfortable")
|
||||
},
|
||||
{
|
||||
"key": "spacious",
|
||||
"name": I18n.tr("options.bar.density-spacious")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.bar.density
|
||||
defaultValue: Settings.getDefaultValue("bar.density")
|
||||
onSelected: key => Settings.data.bar.density = key
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-type-label")
|
||||
description: I18n.tr("panels.bar.appearance-type-description")
|
||||
model: [
|
||||
{
|
||||
"key": "simple",
|
||||
"name": I18n.tr("options.bar.type-simple")
|
||||
},
|
||||
{
|
||||
"key": "floating",
|
||||
"name": I18n.tr("options.bar.type-floating")
|
||||
},
|
||||
{
|
||||
"key": "framed",
|
||||
"name": I18n.tr("options.bar.type-framed")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.bar.barType
|
||||
defaultValue: Settings.getDefaultValue("bar.barType")
|
||||
onSelected: key => {
|
||||
Settings.data.bar.barType = key;
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("panels.bar.appearance-display-mode-description")
|
||||
model: [
|
||||
{
|
||||
"key": "always_visible",
|
||||
"name": I18n.tr("hide-modes.visible")
|
||||
},
|
||||
{
|
||||
"key": "non_exclusive",
|
||||
"name": I18n.tr("hide-modes.non-exclusive")
|
||||
},
|
||||
{
|
||||
"key": "auto_hide",
|
||||
"name": I18n.tr("hide-modes.auto-hide")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.bar.displayMode
|
||||
defaultValue: Settings.getDefaultValue("bar.displayMode")
|
||||
onSelected: key => Settings.data.bar.displayMode = key
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.bar.appearance-use-separate-opacity-label")
|
||||
description: I18n.tr("panels.bar.appearance-use-separate-opacity-description")
|
||||
checked: Settings.data.bar.useSeparateOpacity
|
||||
defaultValue: Settings.getDefaultValue("bar.useSeparateOpacity")
|
||||
onToggled: checked => Settings.data.bar.useSeparateOpacity = checked
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.bar.useSeparateOpacity
|
||||
label: I18n.tr("panels.bar.appearance-background-opacity-label")
|
||||
description: I18n.tr("panels.bar.appearance-background-opacity-description")
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.bar.backgroundOpacity
|
||||
defaultValue: Settings.getDefaultValue("bar.backgroundOpacity")
|
||||
onMoved: value => Settings.data.bar.backgroundOpacity = value
|
||||
text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-font-scale-label")
|
||||
description: I18n.tr("panels.bar.appearance-font-scale-description")
|
||||
from: 0.5
|
||||
to: 2.0
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.bar.fontScale
|
||||
defaultValue: Settings.getDefaultValue("bar.fontScale")
|
||||
onMoved: value => Settings.data.bar.fontScale = value
|
||||
text: Math.floor(Settings.data.bar.fontScale * 100) + "%"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-widget-spacing-label")
|
||||
description: I18n.tr("panels.bar.appearance-widget-spacing-description")
|
||||
from: 0
|
||||
to: 30
|
||||
stepSize: 1
|
||||
showReset: true
|
||||
value: Settings.data.bar.widgetSpacing
|
||||
defaultValue: Settings.getDefaultValue("bar.widgetSpacing")
|
||||
onMoved: value => Settings.data.bar.widgetSpacing = value
|
||||
text: Settings.data.bar.widgetSpacing + "px"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-content-padding-label")
|
||||
description: I18n.tr("panels.bar.appearance-content-padding-description")
|
||||
from: 0
|
||||
to: 30
|
||||
stepSize: 1
|
||||
showReset: true
|
||||
value: Settings.data.bar.contentPadding
|
||||
defaultValue: Settings.getDefaultValue("bar.contentPadding")
|
||||
onMoved: value => Settings.data.bar.contentPadding = value
|
||||
text: Settings.data.bar.contentPadding + "px"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-show-outline-label")
|
||||
description: I18n.tr("panels.bar.appearance-show-outline-description")
|
||||
checked: Settings.data.bar.showOutline
|
||||
defaultValue: Settings.getDefaultValue("bar.showOutline")
|
||||
onToggled: checked => Settings.data.bar.showOutline = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-show-capsule-label")
|
||||
description: I18n.tr("panels.bar.appearance-show-capsule-description")
|
||||
checked: Settings.data.bar.showCapsule
|
||||
defaultValue: Settings.getDefaultValue("bar.showCapsule")
|
||||
onToggled: checked => Settings.data.bar.showCapsule = checked
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.bar.showCapsule
|
||||
label: I18n.tr("panels.bar.appearance-capsule-color-label")
|
||||
description: I18n.tr("panels.bar.appearance-capsule-color-description")
|
||||
noneColor: Color.mSurfaceVariant
|
||||
noneOnColor: Color.mOnSurfaceVariant
|
||||
currentKey: Settings.data.bar.capsuleColorKey
|
||||
onSelected: key => Settings.data.bar.capsuleColorKey = key
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.bar.showCapsule
|
||||
label: I18n.tr("panels.bar.appearance-capsule-opacity-label")
|
||||
description: I18n.tr("panels.bar.appearance-capsule-opacity-description")
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.bar.capsuleOpacity
|
||||
defaultValue: Settings.getDefaultValue("bar.capsuleOpacity")
|
||||
onMoved: value => Settings.data.bar.capsuleOpacity = value
|
||||
text: Math.floor(Settings.data.bar.capsuleOpacity * 100) + "%"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-enable-exclusion-zone-inset-label")
|
||||
description: I18n.tr("panels.bar.appearance-enable-exclusion-zone-inset-description")
|
||||
checked: Settings.data.bar.enableExclusionZoneInset
|
||||
defaultValue: Settings.getDefaultValue("bar.enableExclusionZoneInset")
|
||||
onToggled: checked => Settings.data.bar.enableExclusionZoneInset = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
visible: CompositorService.isNiri
|
||||
label: I18n.tr("panels.bar.appearance-hide-on-overview-label")
|
||||
description: I18n.tr("panels.bar.appearance-hide-on-overview-description")
|
||||
checked: Settings.data.bar.hideOnOverview
|
||||
defaultValue: Settings.getDefaultValue("bar.hideOnOverview")
|
||||
onToggled: checked => Settings.data.bar.hideOnOverview = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-outer-corners-label")
|
||||
description: I18n.tr("panels.bar.appearance-outer-corners-description")
|
||||
checked: Settings.data.bar.outerCorners
|
||||
visible: Settings.data.bar.barType === "simple"
|
||||
defaultValue: Settings.getDefaultValue("bar.outerCorners")
|
||||
onToggled: checked => Settings.data.bar.outerCorners = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: Settings.data.bar.barType === "framed"
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.bar.appearance-frame-settings-label")
|
||||
description: I18n.tr("panels.bar.appearance-frame-settings-description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginL
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-frame-thickness")
|
||||
from: 4
|
||||
to: 24
|
||||
stepSize: 1
|
||||
showReset: true
|
||||
value: Settings.data.bar.frameThickness
|
||||
defaultValue: Settings.getDefaultValue("bar.frameThickness")
|
||||
onMoved: value => Settings.data.bar.frameThickness = value
|
||||
text: Settings.data.bar.frameThickness + "px"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-frame-radius")
|
||||
from: 4
|
||||
to: 24
|
||||
stepSize: 1
|
||||
showReset: true
|
||||
value: Settings.data.bar.frameRadius
|
||||
defaultValue: Settings.getDefaultValue("bar.frameRadius")
|
||||
onMoved: value => Settings.data.bar.frameRadius = value
|
||||
text: Settings.data.bar.frameRadius + "px"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: Settings.data.bar.barType === "floating"
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
label: I18n.tr("panels.bar.appearance-margins-vertical")
|
||||
description: I18n.tr("panels.bar.appearance-margins-description")
|
||||
from: 0
|
||||
to: 500
|
||||
suffix: "px"
|
||||
value: Settings.data.bar.marginVertical
|
||||
defaultValue: Settings.getDefaultValue("bar.marginVertical")
|
||||
onValueChanged: Settings.data.bar.marginVertical = value
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
label: I18n.tr("panels.bar.appearance-margins-horizontal")
|
||||
description: I18n.tr("panels.bar.appearance-margins-description")
|
||||
from: 0
|
||||
to: 500
|
||||
suffix: "px"
|
||||
value: Settings.data.bar.marginHorizontal
|
||||
defaultValue: Settings.getDefaultValue("bar.marginHorizontal")
|
||||
onValueChanged: Settings.data.bar.marginHorizontal = value
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginS
|
||||
visible: Settings.data.bar.displayMode === "auto_hide"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: Settings.data.bar.displayMode === "auto_hide"
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-auto-hide-delay-label")
|
||||
description: I18n.tr("panels.bar.appearance-auto-hide-delay-description")
|
||||
from: 100
|
||||
to: 2000
|
||||
stepSize: 100
|
||||
showReset: true
|
||||
value: Settings.data.bar.autoHideDelay
|
||||
defaultValue: Settings.getDefaultValue("bar.autoHideDelay")
|
||||
onMoved: value => Settings.data.bar.autoHideDelay = value
|
||||
text: Settings.data.bar.autoHideDelay + "ms"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-auto-show-delay-label")
|
||||
description: I18n.tr("panels.bar.appearance-auto-show-delay-description")
|
||||
from: 0
|
||||
to: 500
|
||||
stepSize: 50
|
||||
showReset: true
|
||||
value: Settings.data.bar.autoShowDelay
|
||||
defaultValue: Settings.getDefaultValue("bar.autoShowDelay")
|
||||
onMoved: value => Settings.data.bar.autoShowDelay = value
|
||||
text: Settings.data.bar.autoShowDelay + "ms"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-show-on-workspace-switch-label")
|
||||
description: I18n.tr("panels.bar.appearance-show-on-workspace-switch-description")
|
||||
checked: Settings.data.bar.showOnWorkspaceSwitch
|
||||
defaultValue: Settings.getDefaultValue("bar.showOnWorkspaceSwitch")
|
||||
onToggled: checked => Settings.data.bar.showOnWorkspaceSwitch = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// Signal functions for widgets sub-tab (global widgets only).
|
||||
// These intentionally edit Settings.data.bar.widgets (global defaults),
|
||||
// not per-screen overrides. Per-screen editing is handled by MonitorWidgetsConfig.qml.
|
||||
function _addWidgetToSection(widgetId, section) {
|
||||
var newWidget = {
|
||||
"id": widgetId
|
||||
};
|
||||
if (BarWidgetRegistry.widgetHasUserSettings(widgetId)) {
|
||||
var metadata = BarWidgetRegistry.widgetMetadata[widgetId];
|
||||
if (metadata) {
|
||||
Object.keys(metadata).forEach(function (key) {
|
||||
newWidget[key] = metadata[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
Settings.data.bar.widgets[section].push(newWidget);
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
|
||||
function _removeWidgetFromSection(section, index) {
|
||||
var widgets = Settings.data.bar.widgets;
|
||||
if (index >= 0 && index < widgets[section].length) {
|
||||
var newArray = widgets[section].slice();
|
||||
var removedWidgets = newArray.splice(index, 1);
|
||||
widgets[section] = newArray;
|
||||
BarService.widgetsRevision++;
|
||||
|
||||
if (removedWidgets[0].id === "ControlCenter" && BarService.lookupWidget("ControlCenter") === undefined) {
|
||||
ToastService.showWarning(I18n.tr("toast.missing-control-center.label"), I18n.tr("toast.missing-control-center.description"), 6000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _reorderWidgetInSection(section, fromIndex, toIndex) {
|
||||
var widgets = Settings.data.bar.widgets;
|
||||
if (fromIndex >= 0 && fromIndex < widgets[section].length && toIndex >= 0 && toIndex < widgets[section].length) {
|
||||
var newArray = widgets[section].slice();
|
||||
var item = newArray[fromIndex];
|
||||
newArray.splice(fromIndex, 1);
|
||||
newArray.splice(toIndex, 0, item);
|
||||
widgets[section] = newArray;
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: _updateWidgetSettingsInSection does NOT increment revision
|
||||
// because it only changes settings, not widget structure
|
||||
function _updateWidgetSettingsInSection(section, index, settings) {
|
||||
Settings.data.bar.widgets[section][index] = settings;
|
||||
}
|
||||
|
||||
function _moveWidgetBetweenSections(fromSection, index, toSection) {
|
||||
var widgets = Settings.data.bar.widgets;
|
||||
if (index >= 0 && index < widgets[fromSection].length) {
|
||||
var widget = widgets[fromSection][index];
|
||||
var sourceArray = widgets[fromSection].slice();
|
||||
sourceArray.splice(index, 1);
|
||||
widgets[fromSection] = sourceArray;
|
||||
var targetArray = widgets[toSection].slice();
|
||||
targetArray.push(widget);
|
||||
widgets[toSection] = targetArray;
|
||||
BarService.widgetsRevision++;
|
||||
Logger.d("BarTab", "_moveWidgetBetweenSections: revision now", BarService.widgetsRevision);
|
||||
}
|
||||
}
|
||||
|
||||
function getWidgetLocations(widgetId) {
|
||||
if (!BarService)
|
||||
return [];
|
||||
const instances = BarService.getAllRegisteredWidgets();
|
||||
const locations = {};
|
||||
for (var i = 0; i < instances.length; i++) {
|
||||
if (instances[i].widgetId === widgetId) {
|
||||
const section = instances[i].section;
|
||||
if (section === "left")
|
||||
locations["arrow-bar-to-left"] = true;
|
||||
else if (section === "center")
|
||||
locations["layout-columns"] = true;
|
||||
else if (section === "right")
|
||||
locations["arrow-bar-to-right"] = true;
|
||||
}
|
||||
}
|
||||
return Object.keys(locations);
|
||||
}
|
||||
|
||||
function createBadges(isPlugin, locations) {
|
||||
const badges = [];
|
||||
if (isPlugin) {
|
||||
badges.push({
|
||||
"icon": "plugin",
|
||||
"color": Color.mSecondary
|
||||
});
|
||||
}
|
||||
locations.forEach(function (location) {
|
||||
badges.push({
|
||||
"icon": location,
|
||||
"color": Color.mOnSurfaceVariant
|
||||
});
|
||||
});
|
||||
return badges;
|
||||
}
|
||||
|
||||
function updateAvailableWidgetsModel() {
|
||||
availableWidgets.clear();
|
||||
const widgets = BarWidgetRegistry.getAvailableWidgets();
|
||||
widgets.forEach(entry => {
|
||||
const isPlugin = BarWidgetRegistry.isPluginWidget(entry);
|
||||
let displayName = entry;
|
||||
if (isPlugin) {
|
||||
const pluginId = entry.replace("plugin:", "");
|
||||
const manifest = PluginRegistry.getPluginManifest(pluginId);
|
||||
if (manifest && manifest.name) {
|
||||
displayName = manifest.name;
|
||||
} else {
|
||||
displayName = pluginId;
|
||||
}
|
||||
}
|
||||
availableWidgets.append({
|
||||
"key": entry,
|
||||
"name": displayName,
|
||||
"badges": createBadges(isPlugin, getWidgetLocations(entry))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: availableWidgets
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(updateAvailableWidgetsModel);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BarService
|
||||
function onActiveWidgetsChanged() {
|
||||
updateAvailableWidgetsModel();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BarWidgetRegistry
|
||||
function onPluginWidgetRegistryUpdated() {
|
||||
updateAvailableWidgetsModel();
|
||||
}
|
||||
}
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.appearance")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.widgets")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.behavior")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.monitors")
|
||||
tabIndex: 3
|
||||
checked: subTabBar.currentIndex === 3
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginS
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
AppearanceSubTab {}
|
||||
WidgetsSubTab {
|
||||
availableWidgets: availableWidgets
|
||||
addWidgetToSection: root._addWidgetToSection
|
||||
removeWidgetFromSection: root._removeWidgetFromSection
|
||||
reorderWidgetInSection: root._reorderWidgetInSection
|
||||
updateWidgetSettingsInSection: root._updateWidgetSettingsInSection
|
||||
moveWidgetBetweenSections: root._moveWidgetBetweenSections
|
||||
onOpenPluginSettings: manifest => pluginSettingsDialog.openPluginSettings(manifest)
|
||||
}
|
||||
BehaviorSubTab {}
|
||||
MonitorsSubTab {
|
||||
addMonitor: root.addMonitor
|
||||
removeMonitor: root.removeMonitor
|
||||
}
|
||||
}
|
||||
|
||||
NPluginSettingsPopup {
|
||||
id: pluginSettingsDialog
|
||||
parent: Overlay.overlay
|
||||
showToastOnSave: false
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
readonly property string effectiveWheelAction: Settings.data.bar.mouseWheelAction || "none"
|
||||
readonly property string effectiveMiddleClickAction: Settings.data.bar.middleClickAction || "none"
|
||||
readonly property string effectiveRightClickAction: Settings.data.bar.rightClickAction || "controlCenter"
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-workspace-scroll-label")
|
||||
description: I18n.tr("panels.bar.behavior-workspace-scroll-description")
|
||||
model: {
|
||||
var items = [
|
||||
{
|
||||
"key": "none",
|
||||
"name": I18n.tr("common.none")
|
||||
},
|
||||
{
|
||||
"key": "volume",
|
||||
"name": I18n.tr("common.volume")
|
||||
},
|
||||
{
|
||||
"key": "workspace",
|
||||
"name": I18n.tr("panels.bar.behavior-workspace-scroll-option-workspace")
|
||||
}
|
||||
];
|
||||
if (CompositorService.isNiri) {
|
||||
items.push({
|
||||
"key": "content",
|
||||
"name": I18n.tr("panels.bar.behavior-workspace-scroll-option-content")
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
currentKey: root.effectiveWheelAction
|
||||
defaultValue: Settings.getDefaultValue("bar.mouseWheelAction")
|
||||
onSelected: key => Settings.data.bar.mouseWheelAction = key
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.reverse-scrolling-label")
|
||||
description: I18n.tr("panels.general.reverse-scrolling-description")
|
||||
checked: Settings.data.bar.reverseScroll
|
||||
defaultValue: Settings.getDefaultValue("bar.reverseScroll")
|
||||
onToggled: checked => Settings.data.bar.reverseScroll = checked
|
||||
visible: Settings.data.bar.mouseWheelAction !== "none"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-wheel-wrap-label")
|
||||
description: I18n.tr("panels.bar.behavior-wheel-wrap-description")
|
||||
checked: Settings.data.bar.mouseWheelWrap
|
||||
defaultValue: Settings.getDefaultValue("bar.mouseWheelWrap")
|
||||
onToggled: checked => Settings.data.bar.mouseWheelWrap = checked
|
||||
visible: Settings.data.bar.mouseWheelAction === "workspace"
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-middle-click-label")
|
||||
description: I18n.tr("panels.bar.behavior-middle-click-description")
|
||||
model: [
|
||||
{
|
||||
"key": "none",
|
||||
"name": I18n.tr("common.none")
|
||||
},
|
||||
{
|
||||
"key": "controlCenter",
|
||||
"name": I18n.tr("tooltips.open-control-center")
|
||||
},
|
||||
{
|
||||
"key": "settings",
|
||||
"name": I18n.tr("tooltips.open-settings")
|
||||
},
|
||||
{
|
||||
"key": "launcherPanel",
|
||||
"name": I18n.tr("actions.open-launcher")
|
||||
},
|
||||
{
|
||||
"key": "command",
|
||||
"name": I18n.tr("actions.run-custom-command")
|
||||
}
|
||||
]
|
||||
currentKey: root.effectiveMiddleClickAction
|
||||
defaultValue: Settings.getDefaultValue("bar.middleClickAction")
|
||||
onSelected: key => Settings.data.bar.middleClickAction = key
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-middle-click-command-label")
|
||||
description: I18n.tr("panels.bar.behavior-middle-click-command-description")
|
||||
placeholderText: I18n.tr("panels.bar.behavior-middle-click-command-placeholder")
|
||||
text: Settings.data.bar.middleClickCommand
|
||||
fontFamily: Settings.data.ui.fontFixed
|
||||
onTextChanged: Settings.data.bar.middleClickCommand = text
|
||||
visible: Settings.data.bar.middleClickAction === "command"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-middle-click-follow-mouse-label")
|
||||
description: I18n.tr("panels.bar.behavior-middle-click-follow-mouse-description")
|
||||
checked: Settings.data.bar.middleClickFollowMouse
|
||||
defaultValue: Settings.getDefaultValue("bar.middleClickFollowMouse")
|
||||
onToggled: checked => Settings.data.bar.middleClickFollowMouse = checked
|
||||
visible: Settings.data.bar.middleClickAction !== "none" && Settings.data.bar.middleClickAction !== "command" && !(Settings.data.bar.middleClickAction === "settings" && Settings.data.ui.settingsPanelMode === "window")
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-right-click-label")
|
||||
description: I18n.tr("panels.bar.behavior-right-click-description")
|
||||
model: [
|
||||
{
|
||||
"key": "none",
|
||||
"name": I18n.tr("common.none")
|
||||
},
|
||||
{
|
||||
"key": "controlCenter",
|
||||
"name": I18n.tr("tooltips.open-control-center")
|
||||
},
|
||||
{
|
||||
"key": "settings",
|
||||
"name": I18n.tr("tooltips.open-settings")
|
||||
},
|
||||
{
|
||||
"key": "launcherPanel",
|
||||
"name": I18n.tr("actions.open-launcher")
|
||||
},
|
||||
{
|
||||
"key": "command",
|
||||
"name": I18n.tr("actions.run-custom-command")
|
||||
}
|
||||
]
|
||||
currentKey: root.effectiveRightClickAction
|
||||
defaultValue: Settings.getDefaultValue("bar.rightClickAction")
|
||||
onSelected: key => Settings.data.bar.rightClickAction = key
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-right-click-command-label")
|
||||
description: I18n.tr("panels.bar.behavior-right-click-command-description")
|
||||
placeholderText: I18n.tr("panels.bar.behavior-right-click-command-placeholder")
|
||||
text: Settings.data.bar.rightClickCommand
|
||||
fontFamily: Settings.data.ui.fontFixed
|
||||
onTextChanged: Settings.data.bar.rightClickCommand = text
|
||||
visible: Settings.data.bar.rightClickAction === "command"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-right-click-follow-mouse-label")
|
||||
description: I18n.tr("panels.bar.behavior-right-click-follow-mouse-description")
|
||||
checked: Settings.data.bar.rightClickFollowMouse
|
||||
defaultValue: Settings.getDefaultValue("bar.rightClickFollowMouse")
|
||||
onToggled: checked => Settings.data.bar.rightClickFollowMouse = checked
|
||||
visible: Settings.data.bar.rightClickAction !== "none" && Settings.data.bar.rightClickAction !== "command" && !(Settings.data.bar.rightClickAction === "settings" && Settings.data.ui.settingsPanelMode === "window")
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "../../Bar" as BarSettings
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property var addMonitor
|
||||
property var removeMonitor
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.bar.monitors-desc-new")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Monitor cards
|
||||
Repeater {
|
||||
model: Quickshell.screens || []
|
||||
delegate: NBox {
|
||||
id: monitorCard
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: cardContent.implicitHeight + Style.margin2L
|
||||
color: Color.mSurface
|
||||
|
||||
required property var modelData
|
||||
readonly property string screenName: modelData.name || "Unknown"
|
||||
readonly property real compositorScale: {
|
||||
const info = CompositorService.displayScales[screenName];
|
||||
return (info && info.scale) ? info.scale : 1.0;
|
||||
}
|
||||
readonly property bool barEnabled: (Settings.data.bar.monitors || []).indexOf(screenName) !== -1
|
||||
readonly property bool hasOverride: Settings.hasScreenOverride(screenName)
|
||||
|
||||
// Track if override is enabled (controls both visibility AND whether overrides are applied)
|
||||
readonly property bool overrideEnabled: Settings.isScreenOverrideEnabled(screenName)
|
||||
|
||||
// Get effective values for this screen
|
||||
readonly property string effectivePosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property string effectiveDensity: Settings.getBarDensityForScreen(screenName)
|
||||
|
||||
ColumnLayout {
|
||||
id: cardContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Header: Monitor name and specs
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXXS
|
||||
|
||||
NText {
|
||||
Layout.fillWidth: true
|
||||
text: monitorCard.screenName
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: {
|
||||
return I18n.tr("system.monitor-description", {
|
||||
"model": monitorCard.modelData.model || I18n.tr("common.unknown"),
|
||||
"width": Math.round(monitorCard.modelData.width * monitorCard.compositorScale),
|
||||
"height": Math.round(monitorCard.modelData.height * monitorCard.compositorScale),
|
||||
"scale": monitorCard.compositorScale
|
||||
});
|
||||
}
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
// Enable bar toggle
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
checked: monitorCard.barEnabled
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
Settings.data.bar.monitors = root.addMonitor(Settings.data.bar.monitors, monitorCard.screenName);
|
||||
} else {
|
||||
Settings.data.bar.monitors = root.removeMonitor(Settings.data.bar.monitors, monitorCard.screenName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.bar.monitors.includes(monitorCard.screenName)
|
||||
}
|
||||
|
||||
// Override section (only visible when bar is enabled)
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
visible: monitorCard.barEnabled
|
||||
|
||||
// Override toggle
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.monitor-override-settings")
|
||||
description: I18n.tr("panels.bar.monitor-override-settings-description")
|
||||
checked: monitorCard.overrideEnabled
|
||||
onToggled: checked => {
|
||||
Settings.setScreenOverride(monitorCard.screenName, "enabled", checked);
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
}
|
||||
|
||||
// Override controls (only visible when override toggle is on)
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
visible: monitorCard.overrideEnabled
|
||||
|
||||
// Position override
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-position-label")
|
||||
description: I18n.tr("panels.bar.appearance-position-description")
|
||||
model: [
|
||||
{
|
||||
"key": "top",
|
||||
"name": I18n.tr("positions.top")
|
||||
},
|
||||
{
|
||||
"key": "bottom",
|
||||
"name": I18n.tr("positions.bottom")
|
||||
},
|
||||
{
|
||||
"key": "left",
|
||||
"name": I18n.tr("positions.left")
|
||||
},
|
||||
{
|
||||
"key": "right",
|
||||
"name": I18n.tr("positions.right")
|
||||
}
|
||||
]
|
||||
currentKey: monitorCard.effectivePosition
|
||||
onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "position", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Density override
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-density-label")
|
||||
description: I18n.tr("panels.bar.appearance-density-description")
|
||||
model: [
|
||||
{
|
||||
"key": "mini",
|
||||
"name": I18n.tr("options.bar.density-mini")
|
||||
},
|
||||
{
|
||||
"key": "compact",
|
||||
"name": I18n.tr("options.bar.density-compact")
|
||||
},
|
||||
{
|
||||
"key": "default",
|
||||
"name": I18n.tr("options.bar.density-default")
|
||||
},
|
||||
{
|
||||
"key": "comfortable",
|
||||
"name": I18n.tr("options.bar.density-comfortable")
|
||||
},
|
||||
{
|
||||
"key": "spacious",
|
||||
"name": I18n.tr("options.bar.density-spacious")
|
||||
}
|
||||
]
|
||||
currentKey: monitorCard.effectiveDensity
|
||||
onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "density", key)
|
||||
}
|
||||
}
|
||||
|
||||
// DisplayMode override
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("panels.bar.appearance-display-mode-description")
|
||||
model: [
|
||||
{
|
||||
"key": "always_visible",
|
||||
"name": I18n.tr("hide-modes.visible")
|
||||
},
|
||||
{
|
||||
"key": "non_exclusive",
|
||||
"name": I18n.tr("hide-modes.non-exclusive")
|
||||
},
|
||||
{
|
||||
"key": "auto_hide",
|
||||
"name": I18n.tr("hide-modes.auto-hide")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.getBarDisplayModeForScreen(monitorCard.screenName)
|
||||
onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "displayMode", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Widgets configuration button and Reset all
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NButton {
|
||||
id: widgetConfigButton
|
||||
property bool expanded: false
|
||||
Layout.fillWidth: true
|
||||
fontSize: Style.fontSizeS
|
||||
text: I18n.tr("panels.bar.monitor-configure-widgets")
|
||||
icon: expanded ? "chevron-up" : "layout-grid"
|
||||
onClicked: expanded = !expanded
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: Settings.hasScreenOverride(monitorCard.screenName, "widgets")
|
||||
Layout.fillWidth: true
|
||||
fontSize: Style.fontSizeS
|
||||
text: I18n.tr("panels.bar.use-global-widgets")
|
||||
icon: "refresh"
|
||||
onClicked: {
|
||||
Settings.clearScreenOverride(monitorCard.screenName, "widgets");
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
Layout.fillWidth: true
|
||||
fontSize: Style.fontSizeS
|
||||
text: I18n.tr("panels.bar.monitor-reset-all")
|
||||
icon: "restore"
|
||||
onClicked: {
|
||||
Settings.clearScreenOverride(monitorCard.screenName);
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inline widget configuration
|
||||
BarSettings.MonitorWidgetsConfig {
|
||||
visible: widgetConfigButton.expanded
|
||||
screen: monitorCard.modelData
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property var availableWidgets
|
||||
property var addWidgetToSection
|
||||
property var removeWidgetFromSection
|
||||
property var reorderWidgetInSection
|
||||
property var updateWidgetSettingsInSection
|
||||
property var moveWidgetBetweenSections
|
||||
|
||||
signal openPluginSettings(var manifest)
|
||||
|
||||
// This sub-tab edits the global default widget configuration (Settings.data.bar.widgets).
|
||||
// Per-screen widget overrides are edited in MonitorWidgetsConfig.qml (Monitors sub-tab).
|
||||
|
||||
// determine bar orientation
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
|
||||
|
||||
function getSectionIcons() {
|
||||
return {
|
||||
"left": "arrow-bar-to-up",
|
||||
"center": "layout-distribute-horizontal",
|
||||
"right": "arrow-bar-to-down"
|
||||
};
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.bar.widgets-desc")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Left Section
|
||||
NSectionEditor {
|
||||
sectionName: root.barIsVertical ? I18n.tr("positions.top") : I18n.tr("positions.left")
|
||||
sectionId: "left"
|
||||
barIsVertical: root.barIsVertical
|
||||
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
|
||||
widgetRegistry: BarWidgetRegistry
|
||||
widgetModel: Settings.data.bar.widgets.left
|
||||
sectionIcons: root.getSectionIcons()
|
||||
availableWidgets: root.availableWidgets
|
||||
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
|
||||
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest)
|
||||
}
|
||||
|
||||
// Center Section
|
||||
NSectionEditor {
|
||||
sectionName: I18n.tr("positions.center")
|
||||
sectionId: "center"
|
||||
barIsVertical: root.barIsVertical
|
||||
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
|
||||
widgetRegistry: BarWidgetRegistry
|
||||
widgetModel: Settings.data.bar.widgets.center
|
||||
sectionIcons: root.getSectionIcons()
|
||||
availableWidgets: root.availableWidgets
|
||||
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
|
||||
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest)
|
||||
}
|
||||
|
||||
// Right Section
|
||||
NSectionEditor {
|
||||
sectionName: root.barIsVertical ? I18n.tr("positions.bottom") : I18n.tr("positions.right")
|
||||
sectionId: "right"
|
||||
barIsVertical: root.barIsVertical
|
||||
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
|
||||
widgetRegistry: BarWidgetRegistry
|
||||
widgetModel: Settings.data.bar.widgets.right
|
||||
sectionIcons: root.getSectionIcons()
|
||||
availableWidgets: root.availableWidgets
|
||||
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
|
||||
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest)
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "."
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.Theming
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
// Time dropdown options (00:00 .. 23:30)
|
||||
ListModel {
|
||||
id: timeOptions
|
||||
}
|
||||
|
||||
property var screen
|
||||
|
||||
function populateTimeOptions() {
|
||||
for (var h = 0; h < 24; h++) {
|
||||
for (var m = 0; m < 60; m += 30) {
|
||||
var hh = ("0" + h).slice(-2);
|
||||
var mm = ("0" + m).slice(-2);
|
||||
var key = hh + ":" + mm;
|
||||
timeOptions.append({
|
||||
"key": key,
|
||||
"name": key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(populateTimeOptions);
|
||||
}
|
||||
|
||||
// Download popup
|
||||
Loader {
|
||||
id: downloadPopupLoader
|
||||
active: false
|
||||
sourceComponent: SchemeDownloader {
|
||||
parent: Overlay.overlay
|
||||
}
|
||||
|
||||
property bool pendingOpen: false
|
||||
|
||||
function open() {
|
||||
pendingOpen = true;
|
||||
active = true;
|
||||
if (item) {
|
||||
item.open();
|
||||
pendingOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
onItemChanged: {
|
||||
if (item && pendingOpen) {
|
||||
item.open();
|
||||
pendingOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.colors")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.templates")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
ColorsSubTab {
|
||||
screen: root.screen
|
||||
timeOptions: timeOptions
|
||||
onOpenDownloadPopup: downloadPopupLoader.open()
|
||||
}
|
||||
TemplatesSubTab {}
|
||||
}
|
||||
}
|
||||
+458
@@ -0,0 +1,458 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.Theming
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property var timeOptions
|
||||
property var schemeColorsCache: ({})
|
||||
property int cacheVersion: 0
|
||||
property var screen
|
||||
|
||||
signal openDownloadPopup
|
||||
|
||||
function extractSchemeName(schemePath) {
|
||||
var pathParts = schemePath.split("/");
|
||||
var filename = pathParts[pathParts.length - 1];
|
||||
var schemeName = filename.replace(".json", "");
|
||||
|
||||
if (schemeName === "Noctalia-default") {
|
||||
schemeName = "Noctalia (default)";
|
||||
} else if (schemeName === "Noctalia-legacy") {
|
||||
schemeName = "Noctalia (legacy)";
|
||||
} else if (schemeName === "Tokyo-Night") {
|
||||
schemeName = "Tokyo Night";
|
||||
} else if (schemeName === "Rosepine") {
|
||||
schemeName = "Rose Pine";
|
||||
}
|
||||
|
||||
return schemeName;
|
||||
}
|
||||
|
||||
function getSchemeColor(schemeName, colorKey) {
|
||||
var _ = cacheVersion;
|
||||
|
||||
if (schemeColorsCache[schemeName]) {
|
||||
var entry = schemeColorsCache[schemeName];
|
||||
var variant = entry;
|
||||
|
||||
if (entry.dark || entry.light) {
|
||||
variant = Settings.data.colorSchemes.darkMode ? (entry.dark || entry.light) : (entry.light || entry.dark);
|
||||
}
|
||||
|
||||
if (variant && variant[colorKey]) {
|
||||
return variant[colorKey];
|
||||
}
|
||||
}
|
||||
|
||||
if (colorKey === "mSurface")
|
||||
return Color.mSurfaceVariant;
|
||||
if (colorKey === "mPrimary")
|
||||
return Color.mPrimary;
|
||||
if (colorKey === "mSecondary")
|
||||
return Color.mSecondary;
|
||||
if (colorKey === "mTertiary")
|
||||
return Color.mTertiary;
|
||||
if (colorKey === "mError")
|
||||
return Color.mError;
|
||||
return Color.mOnSurfaceVariant;
|
||||
}
|
||||
|
||||
function schemeLoaded(schemeName, jsonData) {
|
||||
var value = jsonData || {};
|
||||
schemeColorsCache[schemeName] = value;
|
||||
cacheVersion++;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ColorSchemeService
|
||||
function onSchemesChanged() {
|
||||
root.schemeColorsCache = {};
|
||||
root.cacheVersion++;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: fileLoaders
|
||||
visible: false
|
||||
|
||||
Repeater {
|
||||
model: ColorSchemeService.schemes
|
||||
delegate: Item {
|
||||
FileView {
|
||||
path: modelData
|
||||
blockLoading: false
|
||||
onLoaded: {
|
||||
var schemeName = root.extractSchemeName(path);
|
||||
|
||||
try {
|
||||
var jsonData = JSON.parse(text());
|
||||
root.schemeLoaded(schemeName, jsonData);
|
||||
} catch (e) {
|
||||
Logger.w("ColorSchemeTab", "Failed to parse JSON for scheme:", schemeName, e);
|
||||
root.schemeLoaded(schemeName, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("tooltips.switch-to-dark-mode")
|
||||
description: I18n.tr("panels.color-scheme.dark-mode-switch-description")
|
||||
checked: Settings.data.colorSchemes.darkMode
|
||||
onToggled: checked => {
|
||||
Settings.data.colorSchemes.darkMode = checked;
|
||||
root.cacheVersion++;
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.color-scheme.sync-gsettings-label")
|
||||
description: I18n.tr("panels.color-scheme.sync-gsettings-description")
|
||||
checked: Settings.data.colorSchemes.syncGsettings
|
||||
onToggled: checked => {
|
||||
Settings.data.colorSchemes.syncGsettings = checked;
|
||||
if (checked)
|
||||
ColorSchemeService.pushSystemColorScheme();
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.color-scheme.dark-mode-mode-label")
|
||||
description: I18n.tr("panels.color-scheme.dark-mode-mode-description")
|
||||
|
||||
model: [
|
||||
{
|
||||
"name": I18n.tr("panels.color-scheme.dark-mode-mode-off"),
|
||||
"key": "off"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("panels.color-scheme.dark-mode-mode-manual"),
|
||||
"key": "manual"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("common.location"),
|
||||
"key": "location"
|
||||
}
|
||||
]
|
||||
|
||||
currentKey: Settings.data.colorSchemes.schedulingMode
|
||||
|
||||
onSelected: key => {
|
||||
Settings.data.colorSchemes.schedulingMode = key;
|
||||
AppThemeService.generate();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
visible: Settings.data.colorSchemes.schedulingMode === "manual"
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.display.night-light-manual-schedule-label")
|
||||
description: I18n.tr("panels.display.night-light-manual-schedule-description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: false
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.display.night-light-manual-schedule-sunrise")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
model: root.timeOptions
|
||||
currentKey: Settings.data.colorSchemes.manualSunrise
|
||||
placeholder: I18n.tr("panels.display.night-light-manual-schedule-select-start")
|
||||
onSelected: key => Settings.data.colorSchemes.manualSunrise = key
|
||||
minimumWidth: 120
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 20
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.display.night-light-manual-schedule-sunset")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
model: root.timeOptions
|
||||
currentKey: Settings.data.colorSchemes.manualSunset
|
||||
placeholder: I18n.tr("panels.display.night-light-manual-schedule-select-stop")
|
||||
onSelected: key => Settings.data.colorSchemes.manualSunset = key
|
||||
minimumWidth: 120
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.color-scheme.color-source-use-wallpaper-colors-label")
|
||||
description: I18n.tr("panels.color-scheme.color-source-use-wallpaper-colors-description")
|
||||
checked: Settings.data.colorSchemes.useWallpaperColors
|
||||
onToggled: checked => {
|
||||
Settings.data.colorSchemes.useWallpaperColors = checked;
|
||||
if (checked) {
|
||||
AppThemeService.generate();
|
||||
} else {
|
||||
ToastService.showNotice(I18n.tr("toast.wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.disabled"), "settings-color-scheme");
|
||||
if (Settings.data.colorSchemes.predefinedScheme) {
|
||||
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.color-scheme.wallpaper-monitor-source-label")
|
||||
description: I18n.tr("panels.color-scheme.wallpaper-monitor-source-description")
|
||||
enabled: Settings.data.colorSchemes.useWallpaperColors
|
||||
model: {
|
||||
var m = [];
|
||||
if (Quickshell.screens) {
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
var screen = Quickshell.screens[i];
|
||||
var name = screen.name;
|
||||
var displayName = name + " (" + screen.width + "x" + screen.height + ")";
|
||||
m.push({
|
||||
"key": name,
|
||||
"name": displayName
|
||||
});
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
currentKey: Settings.data.colorSchemes.monitorForColors || (screen ? screen.name : "")
|
||||
onSelected: key => {
|
||||
Settings.data.colorSchemes.monitorForColors = key;
|
||||
AppThemeService.generate();
|
||||
}
|
||||
defaultValue: ""
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.color-scheme.wallpaper-method-label")
|
||||
description: I18n.tr("panels.color-scheme.wallpaper-method-description")
|
||||
enabled: Settings.data.colorSchemes.useWallpaperColors
|
||||
model: TemplateProcessor.schemeTypes
|
||||
currentKey: Settings.data.colorSchemes.generationMethod
|
||||
onSelected: key => {
|
||||
Settings.data.colorSchemes.generationMethod = key;
|
||||
AppThemeService.generate();
|
||||
}
|
||||
}
|
||||
|
||||
NBox {
|
||||
visible: Settings.data.colorSchemes.useWallpaperColors
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: descriptionColumn.implicitHeight + Style.margin2L
|
||||
color: Color.mSurface
|
||||
|
||||
Column {
|
||||
id: descriptionColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
NText {
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
text: I18n.tr("panels.color-scheme.method-description." + Settings.data.colorSchemes.generationMethod)
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
Row {
|
||||
id: colorPreviewRow
|
||||
spacing: Style.marginS
|
||||
|
||||
property int diameter: 16 * Style.uiScaleRatio
|
||||
|
||||
Repeater {
|
||||
model: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError]
|
||||
|
||||
Rectangle {
|
||||
width: colorPreviewRow.diameter
|
||||
height: colorPreviewRow.diameter
|
||||
radius: width * 0.5
|
||||
color: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
enabled: !Settings.data.colorSchemes.useWallpaperColors
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("panels.color-scheme.predefined-title")
|
||||
description: I18n.tr("panels.color-scheme.predefined-desc")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
rowSpacing: Style.marginM
|
||||
columnSpacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
Repeater {
|
||||
model: ColorSchemeService.schemes
|
||||
|
||||
Rectangle {
|
||||
id: schemeItem
|
||||
|
||||
property string schemePath: modelData
|
||||
property string schemeName: root.extractSchemeName(modelData)
|
||||
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
height: 50 * Style.uiScaleRatio
|
||||
radius: Style.radiusS
|
||||
color: root.getSchemeColor(schemeName, "mSurface")
|
||||
border.width: Style.borderL
|
||||
border.color: {
|
||||
if ((Settings.data.colorSchemes.predefinedScheme === schemeName) && schemeItem.enabled) {
|
||||
return Color.mSecondary;
|
||||
}
|
||||
if (itemMouseArea.containsMouse) {
|
||||
return Color.mHover;
|
||||
}
|
||||
return Color.mOutline;
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: scheme
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: schemeItem.schemeName
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
property int diameter: 16 * Style.uiScaleRatio
|
||||
|
||||
Rectangle {
|
||||
width: scheme.diameter
|
||||
height: scheme.diameter
|
||||
radius: scheme.diameter * 0.5
|
||||
color: root.getSchemeColor(schemeItem.schemeName, "mPrimary")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: scheme.diameter
|
||||
height: scheme.diameter
|
||||
radius: scheme.diameter * 0.5
|
||||
color: root.getSchemeColor(schemeItem.schemeName, "mSecondary")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: scheme.diameter
|
||||
height: scheme.diameter
|
||||
radius: scheme.diameter * 0.5
|
||||
color: root.getSchemeColor(schemeItem.schemeName, "mTertiary")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: scheme.diameter
|
||||
height: scheme.diameter
|
||||
radius: scheme.diameter * 0.5
|
||||
color: root.getSchemeColor(schemeItem.schemeName, "mError")
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemMouseArea
|
||||
anchors.fill: parent
|
||||
enabled: schemeItem.enabled
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
Settings.data.colorSchemes.useWallpaperColors = false;
|
||||
Logger.i("ColorSchemeTab", "Disabled wallpaper colors");
|
||||
|
||||
Settings.data.colorSchemes.predefinedScheme = schemeItem.schemeName;
|
||||
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: (Settings.data.colorSchemes.predefinedScheme === schemeItem.schemeName) && schemeItem.enabled
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.rightMargin: 0
|
||||
anchors.topMargin: -3
|
||||
width: 20
|
||||
height: 20
|
||||
radius: Math.min(Style.radiusL, width / 2)
|
||||
color: Color.mSecondary
|
||||
border.width: Style.borderS
|
||||
border.color: Color.mOnSecondary
|
||||
|
||||
NIcon {
|
||||
icon: "check"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSecondary
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("panels.color-scheme.download-button")
|
||||
icon: "download"
|
||||
onClicked: root.openDownloadPopup()
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.topMargin: Style.marginS
|
||||
}
|
||||
}
|
||||
}
|
||||
+972
@@ -0,0 +1,972 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Theming
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property var availableSchemes: []
|
||||
property bool fetching: false
|
||||
property bool downloading: false
|
||||
property bool hasInitialData: false // Track if we've loaded data at least once
|
||||
property string downloadError: ""
|
||||
property string downloadingScheme: ""
|
||||
property string pendingApplyScheme: "" // Scheme name to apply after reload
|
||||
property string lastStderrOutput: "" // Store stderr from download process
|
||||
property real lastApiFetchTime: 0 // Track when we last fetched from API to prevent rapid calls
|
||||
property int minApiFetchInterval: 60 // Minimum seconds between API fetches (1 minute)
|
||||
|
||||
// Cache for remote scheme colors
|
||||
property var schemeColorsCache: ({})
|
||||
property int cacheVersion: 0
|
||||
|
||||
// Cache for available schemes list (uses ShellState singleton)
|
||||
property int schemesCacheUpdateFrequency: 2 * 60 * 60 // 2 hours in seconds
|
||||
|
||||
// Cache for repo branch info (to reduce API calls during downloads)
|
||||
property string cachedBranch: "main"
|
||||
property string cachedBranchSha: ""
|
||||
|
||||
width: Math.max(500, contentColumn.implicitWidth + Style.margin2XL)
|
||||
height: Math.min(800, contentColumn.implicitHeight + Style.margin2XL)
|
||||
padding: Style.marginXL
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
dim: false
|
||||
anchors.centerIn: parent
|
||||
|
||||
// Helper function to get color from cached scheme data
|
||||
function getSchemeColor(schemeName, colorKey) {
|
||||
var _ = cacheVersion; // Create dependency
|
||||
|
||||
if (schemeColorsCache[schemeName]) {
|
||||
var entry = schemeColorsCache[schemeName];
|
||||
var variant = entry;
|
||||
|
||||
// Check if scheme has dark/light variants
|
||||
if (entry.dark || entry.light) {
|
||||
variant = Settings.data.colorSchemes.darkMode ? (entry.dark || entry.light) : (entry.light || entry.dark);
|
||||
}
|
||||
|
||||
if (variant && variant[colorKey]) {
|
||||
return variant[colorKey];
|
||||
}
|
||||
}
|
||||
|
||||
// Return visible defaults while loading
|
||||
var defaults = {
|
||||
"mSurface": Color.mSurfaceVariant,
|
||||
"mPrimary": Color.mPrimary,
|
||||
"mSecondary": Color.mSecondary,
|
||||
"mTertiary": Color.mTertiary,
|
||||
"mError": Color.mError,
|
||||
"mOnSurface": Color.mOnSurfaceVariant
|
||||
};
|
||||
return defaults[colorKey] || Color.mOnSurfaceVariant;
|
||||
}
|
||||
|
||||
// Colors are now provided directly in the registry, no need to fetch individual files
|
||||
function fetchSchemeColors(scheme) {
|
||||
return;
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderM
|
||||
}
|
||||
|
||||
function loadSchemesFromCache() {
|
||||
try {
|
||||
const now = Time.timestamp;
|
||||
const cacheData = ShellState.getColorSchemesList();
|
||||
const cachedSchemes = cacheData.schemes || [];
|
||||
const cachedTimestamp = cacheData.timestamp || 0;
|
||||
|
||||
// Check if cache is expired or missing
|
||||
if (!cachedTimestamp || (now >= cachedTimestamp + schemesCacheUpdateFrequency)) {
|
||||
// Migration is now handled in Settings.qml
|
||||
|
||||
// Only fetch from API if we haven't fetched recently (prevent rapid repeated calls)
|
||||
const timeSinceLastFetch = now - lastApiFetchTime;
|
||||
if (timeSinceLastFetch >= minApiFetchInterval) {
|
||||
Logger.d("ColorSchemeDownload", "Cache expired or missing, fetching new schemes");
|
||||
fetchAvailableSchemesFromAPI();
|
||||
return;
|
||||
} else {
|
||||
// Use cached data even if expired, to avoid rate limits
|
||||
Logger.d("ColorSchemeDownload", "Cache expired but recent API call detected, using cached data");
|
||||
if (cachedSchemes.length > 0) {
|
||||
availableSchemes = cachedSchemes;
|
||||
hasInitialData = true;
|
||||
fetching = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ageMinutes = Math.round((now - cachedTimestamp) / 60);
|
||||
Logger.d("ColorSchemeDownload", "Loading cached schemes from ShellState (age:", ageMinutes, "minutes)");
|
||||
|
||||
if (cachedSchemes.length > 0) {
|
||||
availableSchemes = cachedSchemes;
|
||||
// Restore color cache from cached schemes
|
||||
for (var i = 0; i < cachedSchemes.length; i++) {
|
||||
var cachedScheme = cachedSchemes[i];
|
||||
if (cachedScheme.dark || cachedScheme.light) {
|
||||
schemeColorsCache[cachedScheme.name] = {
|
||||
"dark": cachedScheme.dark || null,
|
||||
"light": cachedScheme.light || null
|
||||
};
|
||||
}
|
||||
}
|
||||
hasInitialData = true;
|
||||
cacheVersion++;
|
||||
fetching = false;
|
||||
} else {
|
||||
// Cache is empty, only fetch if we haven't fetched recently
|
||||
const timeSinceLastFetch = now - lastApiFetchTime;
|
||||
if (timeSinceLastFetch >= minApiFetchInterval) {
|
||||
fetchAvailableSchemesFromAPI();
|
||||
} else {
|
||||
Logger.d("ColorSchemeDownload", "Cache empty but recent API call detected, skipping fetch");
|
||||
fetching = false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.e("ColorSchemeDownload", "Failed to load schemes from cache:", error);
|
||||
fetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
function saveSchemesToCache() {
|
||||
try {
|
||||
ShellState.setColorSchemesList({
|
||||
schemes: availableSchemes,
|
||||
timestamp: Time.timestamp
|
||||
});
|
||||
Logger.d("ColorSchemeDownload", "Schemes list saved to ShellState");
|
||||
} catch (error) {
|
||||
Logger.e("ColorSchemeDownload", "Failed to save schemes to cache:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function fetchAvailableSchemes() {
|
||||
if (fetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to load from ShellState cache first
|
||||
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
|
||||
loadSchemesFromCache();
|
||||
} else {
|
||||
// ShellState not ready, fetch directly from API
|
||||
fetchAvailableSchemesFromAPI();
|
||||
}
|
||||
}
|
||||
|
||||
function fetchAvailableSchemesFromAPI() {
|
||||
if (fetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we've fetched recently to prevent rapid repeated calls
|
||||
const now = Time.timestamp;
|
||||
const timeSinceLastFetch = now - lastApiFetchTime;
|
||||
if (timeSinceLastFetch < minApiFetchInterval) {
|
||||
Logger.d("ColorSchemeDownload", "Skipping API fetch - too soon since last fetch (", Math.round(timeSinceLastFetch), "s ago)");
|
||||
return;
|
||||
}
|
||||
|
||||
fetching = true;
|
||||
lastApiFetchTime = now;
|
||||
// Don't clear availableSchemes immediately to prevent flicker - keep showing old list while fetching
|
||||
// availableSchemes = [];
|
||||
downloadError = "";
|
||||
|
||||
// Fetch registry.json
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
fetching = false;
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var registry = JSON.parse(xhr.responseText);
|
||||
if (registry && registry.themes && Array.isArray(registry.themes)) {
|
||||
var schemes = [];
|
||||
// Process themes
|
||||
for (var i = 0; i < registry.themes.length; i++) {
|
||||
var theme = registry.themes[i];
|
||||
schemes.push({
|
||||
"name": theme.name,
|
||||
"path": theme.path,
|
||||
"dark": theme.dark || null,
|
||||
"light": theme.light || null
|
||||
});
|
||||
schemeColorsCache[theme.name] = {
|
||||
"dark": theme.dark || null,
|
||||
"light": theme.light || null
|
||||
};
|
||||
}
|
||||
availableSchemes = schemes;
|
||||
hasInitialData = true;
|
||||
cacheVersion++;
|
||||
Logger.d("ColorSchemeDownload", "Fetched", schemes.length, "available schemes from registry.json");
|
||||
// Save to cache
|
||||
saveSchemesToCache();
|
||||
} else {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-invalid-response");
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
} catch (e) {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-parse-failed", {
|
||||
"error": e.toString()
|
||||
});
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
} else if (xhr.status === 403) {
|
||||
// Rate limit hit - try to use cache if available
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-rate-limit");
|
||||
Logger.w("ColorSchemeDownload", downloadError);
|
||||
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
|
||||
const cacheData = ShellState.getColorSchemesList();
|
||||
const cachedSchemes = cacheData.schemes || [];
|
||||
if (cachedSchemes.length > 0) {
|
||||
availableSchemes = cachedSchemes;
|
||||
// Restore color cache from cached schemes
|
||||
for (var j = 0; j < cachedSchemes.length; j++) {
|
||||
var cachedScheme = cachedSchemes[j];
|
||||
if (cachedScheme.dark || cachedScheme.light) {
|
||||
schemeColorsCache[cachedScheme.name] = {
|
||||
"dark": cachedScheme.dark || null,
|
||||
"light": cachedScheme.light || null
|
||||
};
|
||||
}
|
||||
}
|
||||
hasInitialData = true;
|
||||
cacheVersion++;
|
||||
Logger.i("ColorSchemeDownload", "Using cached schemes due to rate limit");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-api-error", {
|
||||
"status": xhr.status
|
||||
});
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open("GET", "https://raw.githubusercontent.com/noctalia-dev/noctalia-colorschemes/main/registry.json");
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function downloadScheme(scheme) {
|
||||
if (downloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
downloading = true;
|
||||
downloadingScheme = scheme.name;
|
||||
downloadError = "";
|
||||
|
||||
Logger.i("ColorSchemeDownload", "Downloading scheme:", scheme.name);
|
||||
|
||||
// Use cached branch/SHA if available, otherwise fetch
|
||||
if (cachedBranchSha) {
|
||||
// Use cached SHA directly
|
||||
getSchemeTreeWithSha(scheme, cachedBranch, cachedBranchSha);
|
||||
} else if (cachedBranch) {
|
||||
// We have branch name, just need SHA
|
||||
getSchemeTree(scheme, cachedBranch);
|
||||
} else {
|
||||
// Need to fetch branch info first
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var repoInfo = JSON.parse(xhr.responseText);
|
||||
var defaultBranch = repoInfo.default_branch || "main";
|
||||
cachedBranch = defaultBranch;
|
||||
// Now get the tree for the scheme directory
|
||||
getSchemeTree(scheme, defaultBranch);
|
||||
} catch (e) {
|
||||
// Fallback: try to get files directly
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
} else {
|
||||
// Fallback: try to get files directly
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes");
|
||||
xhr.send();
|
||||
}
|
||||
}
|
||||
|
||||
function getSchemeTree(scheme, branch) {
|
||||
// First get the SHA of the branch
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var refResponse = JSON.parse(xhr.responseText);
|
||||
var sha = refResponse.object ? refResponse.object.sha : null;
|
||||
if (sha) {
|
||||
// Cache the SHA for future downloads
|
||||
cachedBranchSha = sha;
|
||||
// Now get the tree
|
||||
getSchemeTreeWithSha(scheme, branch, sha);
|
||||
} else {
|
||||
// Fallback to direct method
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to direct method
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
} else {
|
||||
// Fallback to direct method
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes/git/refs/heads/" + branch);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function getSchemeTreeWithSha(scheme, branch, sha) {
|
||||
// Use git trees API to get all files recursively
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
if (response.tree && Array.isArray(response.tree)) {
|
||||
// Filter files that belong to this scheme
|
||||
var files = [];
|
||||
for (var i = 0; i < response.tree.length; i++) {
|
||||
var item = response.tree[i];
|
||||
if (item.type === "blob" && item.path.startsWith(scheme.path + "/")) {
|
||||
files.push({
|
||||
"path": item.path,
|
||||
"url": "https://raw.githubusercontent.com/noctalia-dev/noctalia-colorschemes/" + branch + "/" + item.path,
|
||||
"name": item.path.split("/").pop()
|
||||
});
|
||||
}
|
||||
}
|
||||
downloadSchemeFiles(scheme.name, files);
|
||||
} else {
|
||||
// Fallback to direct method
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
} catch (e) {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-parse-failed", {
|
||||
"error": e.toString()
|
||||
});
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
} else {
|
||||
// Fallback to direct method
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes/git/trees/" + sha + "?recursive=1");
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function getSchemeFilesDirect(scheme) {
|
||||
// Fallback: get files directly using contents API (non-recursive, but works)
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
if (Array.isArray(response)) {
|
||||
// Recursively get all files
|
||||
getAllFilesRecursive(scheme, response, []);
|
||||
} else {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-invalid-response");
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
} catch (e) {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-parse-failed", {
|
||||
"error": e.toString()
|
||||
});
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
} else {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-api-error", {
|
||||
"status": xhr.status
|
||||
});
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes/contents/" + scheme.path);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function getAllFilesRecursive(scheme, items, allFiles, callback) {
|
||||
if (!callback) {
|
||||
callback = function () {
|
||||
downloadSchemeFiles(scheme.name, allFiles);
|
||||
};
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var pending = 0;
|
||||
var completed = 0;
|
||||
|
||||
function checkComplete() {
|
||||
completed++;
|
||||
if (completed === items.length && pending === 0) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
if (item.type === "file") {
|
||||
allFiles.push({
|
||||
"path": item.path,
|
||||
"url": item.download_url,
|
||||
"name": item.name
|
||||
});
|
||||
checkComplete();
|
||||
} else if (item.type === "dir") {
|
||||
pending++;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function (dirItem) {
|
||||
return function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
pending--;
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var dirResponse = JSON.parse(xhr.responseText);
|
||||
if (Array.isArray(dirResponse)) {
|
||||
getAllFilesRecursive(scheme, dirResponse, allFiles, function () {
|
||||
checkComplete();
|
||||
});
|
||||
} else {
|
||||
checkComplete();
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.e("ColorSchemeDownload", "Failed to parse directory response:", e);
|
||||
checkComplete();
|
||||
}
|
||||
} else {
|
||||
checkComplete();
|
||||
}
|
||||
}
|
||||
};
|
||||
}(item);
|
||||
xhr.open("GET", item.url);
|
||||
xhr.send();
|
||||
} else {
|
||||
checkComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function downloadSchemeFiles(schemeName, files) {
|
||||
if (files.length === 0) {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-no-files");
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
return;
|
||||
}
|
||||
|
||||
var targetDir = ColorSchemeService.downloadedSchemesDirectory + "/" + schemeName;
|
||||
var downloadScript = "mkdir -p '" + targetDir + "'\n";
|
||||
|
||||
// Build download script for all files
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var file = files[i];
|
||||
var filePath = file.path;
|
||||
// Remove scheme name and leading / from path
|
||||
var relativePath = filePath;
|
||||
if (filePath.startsWith(schemeName + "/")) {
|
||||
relativePath = filePath.substring(schemeName.length + 1);
|
||||
} else if (filePath.startsWith("/" + schemeName + "/")) {
|
||||
relativePath = filePath.substring(schemeName.length + 2);
|
||||
}
|
||||
var localPath = targetDir + "/" + relativePath;
|
||||
var localDir = localPath.substring(0, localPath.lastIndexOf('/'));
|
||||
|
||||
downloadScript += "mkdir -p '" + localDir + "'\n";
|
||||
var downloadUrl = file.url || file.download_url;
|
||||
if (downloadUrl) {
|
||||
downloadScript += "curl -L -s -o '" + localPath + "' '" + downloadUrl + "' || wget -q -O '" + localPath + "' '" + downloadUrl + "'\n";
|
||||
}
|
||||
}
|
||||
|
||||
Logger.d("ColorSchemeDownload", "Downloading", files.length, "files for scheme", schemeName);
|
||||
|
||||
// Execute download script
|
||||
var stderrOutput = "";
|
||||
var downloadProcess = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
Process {
|
||||
id: downloadProcess
|
||||
command: ["sh", "-c", ` + JSON.stringify(downloadScript) + `]
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
Logger.e("ColorSchemeDownload", "Download stderr:", text);
|
||||
root.lastStderrOutput = text.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, root, "DownloadProcess_" + schemeName);
|
||||
|
||||
downloadProcess.exited.connect(function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
Logger.i("ColorSchemeDownload", "Scheme downloaded successfully:", schemeName);
|
||||
ToastService.showNotice(I18n.tr("panels.color-scheme.download-success-title"), I18n.tr("panels.color-scheme.download-success-description", {
|
||||
"scheme": schemeName
|
||||
}), "settings-color-scheme");
|
||||
// Set pending scheme to apply after reload
|
||||
pendingApplyScheme = schemeName;
|
||||
// Reload color schemes
|
||||
ColorSchemeService.loadColorSchemes();
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
} else {
|
||||
var errorDetails = "Exit code: " + exitCode;
|
||||
if (root.lastStderrOutput) {
|
||||
errorDetails += " - " + root.lastStderrOutput;
|
||||
}
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-download-failed", {
|
||||
"code": exitCode
|
||||
}) + "\n" + errorDetails;
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
ToastService.showError(I18n.tr("panels.color-scheme.download-error-title"), I18n.tr("panels.color-scheme.download-error-description", {
|
||||
"scheme": schemeName
|
||||
}) + "\n" + errorDetails);
|
||||
// Clean up the partially downloaded directory on failure
|
||||
var cleanupScript = "rm -rf '" + targetDir + "'";
|
||||
var cleanupProcess = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
id: cleanupProcess
|
||||
command: ["sh", "-c", ` + JSON.stringify(cleanupScript) + `]
|
||||
}
|
||||
`, root, "CleanupProcess_" + schemeName);
|
||||
|
||||
cleanupProcess.exited.connect(function (cleanupExitCode) {
|
||||
if (cleanupExitCode === 0) {
|
||||
Logger.d("ColorSchemeDownload", "Partially downloaded scheme directory cleaned up:", targetDir);
|
||||
} else {
|
||||
Logger.w("ColorSchemeDownload", "Failed to clean up partially downloaded scheme directory:", targetDir);
|
||||
}
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
cleanupProcess.destroy();
|
||||
});
|
||||
|
||||
cleanupProcess.running = true;
|
||||
}
|
||||
root.lastStderrOutput = "";
|
||||
downloadProcess.destroy();
|
||||
});
|
||||
|
||||
downloadProcess.running = true;
|
||||
}
|
||||
|
||||
function isSchemeInstalled(schemeName) {
|
||||
// Check if scheme already exists in ColorSchemeService
|
||||
for (var i = 0; i < ColorSchemeService.schemes.length; i++) {
|
||||
var path = ColorSchemeService.schemes[i];
|
||||
if (path.indexOf("/" + schemeName + "/") !== -1 || path.indexOf("/" + schemeName + ".json") !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isSchemeDownloaded(schemeName) {
|
||||
// Check if scheme is in the downloaded directory (not preinstalled)
|
||||
for (var i = 0; i < ColorSchemeService.schemes.length; i++) {
|
||||
var path = ColorSchemeService.schemes[i];
|
||||
if ((path.indexOf("/" + schemeName + "/") !== -1 || path.indexOf("/" + schemeName + ".json") !== -1) && path.indexOf(ColorSchemeService.downloadedSchemesDirectory) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function deleteScheme(schemeName) {
|
||||
if (downloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.i("ColorSchemeDownload", "Deleting scheme:", schemeName);
|
||||
|
||||
// Check if the deleted scheme is the currently active one
|
||||
var currentScheme = Settings.data.colorSchemes.predefinedScheme || "";
|
||||
var deletedSchemeDisplayName = ColorSchemeService.getBasename(schemeName);
|
||||
var needsReset = (currentScheme === deletedSchemeDisplayName);
|
||||
|
||||
// Only allow deleting downloaded schemes, not preinstalled ones
|
||||
var targetDir = ColorSchemeService.downloadedSchemesDirectory + "/" + schemeName;
|
||||
var deleteScript = "rm -rf '" + targetDir + "'";
|
||||
|
||||
var deleteProcess = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
id: deleteProcess
|
||||
command: ["sh", "-c", ` + JSON.stringify(deleteScript) + `]
|
||||
}
|
||||
`, root, "DeleteProcess_" + schemeName);
|
||||
|
||||
deleteProcess.exited.connect(function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
Logger.i("ColorSchemeDownload", "Scheme deleted successfully:", schemeName);
|
||||
ToastService.showNotice(I18n.tr("panels.color-scheme.delete-success-title"), I18n.tr("panels.color-scheme.delete-success-description", {
|
||||
"scheme": schemeName
|
||||
}), "settings-color-scheme");
|
||||
|
||||
// If the deleted scheme was the active one, reset to default BEFORE reloading
|
||||
if (needsReset) {
|
||||
Logger.i("ColorSchemeDownload", "Deleted scheme was active, resetting to Noctalia (default)");
|
||||
// Clear the setting immediately so ColorSchemeService won't try to apply the deleted scheme
|
||||
Settings.data.colorSchemes.predefinedScheme = "Noctalia (default)";
|
||||
// Apply the default scheme immediately
|
||||
ColorSchemeService.setPredefinedScheme("Noctalia (default)");
|
||||
}
|
||||
|
||||
// Reload color schemes
|
||||
ColorSchemeService.loadColorSchemes();
|
||||
} else {
|
||||
Logger.e("ColorSchemeDownload", "Delete failed with exit code:", exitCode);
|
||||
ToastService.showError(I18n.tr("panels.color-scheme.delete-error-title"), I18n.tr("panels.color-scheme.delete-error-description", {
|
||||
"scheme": schemeName
|
||||
}));
|
||||
}
|
||||
deleteProcess.destroy();
|
||||
});
|
||||
|
||||
deleteProcess.running = true;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ColorSchemeService
|
||||
function onScanningChanged() {
|
||||
// When scanning completes and we have a pending scheme, apply it
|
||||
if (!ColorSchemeService.scanning && pendingApplyScheme !== "") {
|
||||
var schemeToApply = pendingApplyScheme;
|
||||
pendingApplyScheme = ""; // Clear pending before applying
|
||||
|
||||
// Wait a tiny bit to ensure schemes array is updated
|
||||
applyTimer.schemeName = schemeToApply;
|
||||
applyTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: applyTimer
|
||||
property string schemeName: ""
|
||||
interval: 100 // Small delay to ensure schemes array is populated
|
||||
onTriggered: {
|
||||
if (schemeName !== "") {
|
||||
Logger.i("ColorSchemeDownload", "Auto-applying downloaded scheme:", schemeName);
|
||||
// Use setPredefinedScheme which will apply and set it as the current scheme
|
||||
ColorSchemeService.setPredefinedScheme(schemeName);
|
||||
schemeName = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
fetchAvailableSchemes();
|
||||
}
|
||||
|
||||
function preFetchSchemeColors() {
|
||||
if (availableSchemes.length > 0 && visible) {
|
||||
Qt.callLater(function () {
|
||||
// Just trigger a cache version update to ensure UI refreshes
|
||||
cacheVersion++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onAvailableSchemesChanged: preFetchSchemeColors()
|
||||
onVisibleChanged: {
|
||||
preFetchSchemeColors();
|
||||
|
||||
// Load schemes from ShellState when popup becomes visible
|
||||
if (visible) {
|
||||
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
|
||||
loadSchemesFromCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: typeof ShellState !== 'undefined' ? ShellState : null
|
||||
function onIsLoadedChanged() {
|
||||
if (root.visible && ShellState.isLoaded) {
|
||||
loadSchemesFromCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: contentColumn
|
||||
width: parent.width
|
||||
spacing: Style.marginL
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.color-scheme.download-title")
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
tooltipText: I18n.tr("common.refresh")
|
||||
enabled: !fetching && !downloading
|
||||
onClicked: {
|
||||
// Force refresh by clearing cache timestamp and fetching directly from API
|
||||
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
|
||||
ShellState.setColorSchemesList({
|
||||
schemes: [],
|
||||
timestamp: 0
|
||||
});
|
||||
}
|
||||
// Fetch directly from API to avoid cache check delay
|
||||
fetchAvailableSchemesFromAPI();
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Color.mOutline
|
||||
}
|
||||
|
||||
// Error message
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: errorText.implicitHeight + Style.marginM
|
||||
visible: downloadError !== ""
|
||||
color: Color.mError
|
||||
radius: Style.radiusS
|
||||
|
||||
NText {
|
||||
id: errorText
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
text: downloadError
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnError
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator - only show on initial load, not during refresh
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: fetching && !hasInitialData
|
||||
spacing: Style.marginM
|
||||
|
||||
NBusyIndicator {
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 20
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.color-scheme.download-fetching")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
// Schemes list - keep visible during refresh to prevent flicker
|
||||
NScrollView {
|
||||
id: schemesScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 400
|
||||
visible: hasInitialData && availableSchemes.length > 0
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
gradientColor: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
width: schemesScrollView.availableWidth
|
||||
spacing: Style.marginS
|
||||
|
||||
Repeater {
|
||||
model: availableSchemes
|
||||
|
||||
Rectangle {
|
||||
id: schemeItem
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 50 * Style.uiScaleRatio
|
||||
radius: Style.radiusS
|
||||
property string schemeName: modelData.name
|
||||
color: root.getSchemeColor(schemeName, "mSurface")
|
||||
border.width: Style.borderL
|
||||
border.color: hoverHandler.hovered ? root.getSchemeColor(schemeName, "mPrimary") : Color.mOutline
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.visible && !root.schemeColorsCache[schemeName]) {
|
||||
root.fetchSchemeColors(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: schemeRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Style.marginL
|
||||
anchors.rightMargin: Style.marginL
|
||||
spacing: Style.marginS
|
||||
|
||||
property string schemeName: modelData.name
|
||||
property int diameter: 16 * Style.uiScaleRatio
|
||||
property var colorKeys: ["mPrimary", "mSecondary", "mTertiary", "mError"]
|
||||
|
||||
NText {
|
||||
text: schemeRow.schemeName
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Color swatches
|
||||
Repeater {
|
||||
model: schemeRow.colorKeys
|
||||
Rectangle {
|
||||
width: schemeRow.diameter
|
||||
height: schemeRow.diameter
|
||||
radius: schemeRow.diameter * 0.5
|
||||
color: root.getSchemeColor(schemeRow.schemeName, modelData)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Download/Delete button
|
||||
NIconButton {
|
||||
property bool isDownloading: downloading && downloadingScheme === schemeRow.schemeName
|
||||
property bool isInstalled: root.isSchemeInstalled(schemeRow.schemeName)
|
||||
property bool isDownloaded: root.isSchemeDownloaded(schemeRow.schemeName)
|
||||
|
||||
icon: isDownloading ? "" : (isDownloaded ? "trash" : "download")
|
||||
tooltipText: isDownloading ? I18n.tr("panels.color-scheme.download-downloading") : (isDownloaded ? I18n.tr("common.delete") : I18n.tr("common.download"))
|
||||
enabled: !downloading
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: !isInstalled || isDownloaded // Show button only if not installed (can download) or if downloaded (can delete)
|
||||
onClicked: isDownloaded ? root.deleteScheme(schemeRow.schemeName) : root.downloadScheme(modelData)
|
||||
|
||||
NBusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
width: 16 * Style.uiScaleRatio
|
||||
height: 16 * Style.uiScaleRatio
|
||||
visible: parent.isDownloading
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginM
|
||||
visible: !fetching && availableSchemes.length === 0 && downloadError === ""
|
||||
|
||||
NIcon {
|
||||
icon: "package"
|
||||
pointSize: 48
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.color-scheme.download-empty")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.Theming
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Helper to format path description
|
||||
function getDesc(fallbackPath) {
|
||||
return I18n.tr("panels.color-scheme.templates-write-path", {
|
||||
"filepath": fallbackPath
|
||||
});
|
||||
}
|
||||
|
||||
// Build a combined list of all available templates from TemplateRegistry, sorted alphabetically
|
||||
readonly property var allTemplates: {
|
||||
var templates = [];
|
||||
|
||||
// Add terminals with category "terminal"
|
||||
for (var i = 0; i < TemplateRegistry.terminals.length; i++) {
|
||||
var t = TemplateRegistry.terminals[i];
|
||||
templates.push({
|
||||
"id": t.id,
|
||||
"name": t.name,
|
||||
"category": "terminal",
|
||||
"tooltip": getDesc(t.outputPath)
|
||||
});
|
||||
}
|
||||
|
||||
// Add applications
|
||||
for (var j = 0; j < TemplateRegistry.applications.length; j++) {
|
||||
var app = TemplateRegistry.applications[j];
|
||||
var path = "";
|
||||
|
||||
// Determine path to show
|
||||
if (app.outputs && app.outputs.length > 0) {
|
||||
var paths = [];
|
||||
for (var k = 0; k < app.outputs.length; k++) {
|
||||
paths.push(app.outputs[k].path);
|
||||
}
|
||||
path = paths.join("\n");
|
||||
} else if (app.id === "emacs") {
|
||||
// Emacs clients are detected dynamically by ProgramCheckerService
|
||||
var emacsClients = ProgramCheckerService.availableEmacsClients;
|
||||
if (emacsClients && emacsClients.length > 0) {
|
||||
var emacsPaths = [];
|
||||
for (var k = 0; k < emacsClients.length; k++) {
|
||||
emacsPaths.push(emacsClients[k].path);
|
||||
}
|
||||
path = emacsPaths.join("\n");
|
||||
} else {
|
||||
path = I18n.tr("panels.color-scheme.templates-none-detected");
|
||||
}
|
||||
} else if (app.clients && app.clients.length > 0) {
|
||||
var validClients = [];
|
||||
for (var k = 0; k < app.clients.length; k++) {
|
||||
var client = app.clients[k];
|
||||
var include = true;
|
||||
|
||||
if (app.id === "discord") {
|
||||
include = TemplateProcessor.isDiscordClientEnabled(client.name);
|
||||
} else if (app.id === "code") {
|
||||
// For code clients, resolve all theme paths dynamically (version-independent)
|
||||
if (TemplateProcessor.isCodeClientEnabled(client.name)) {
|
||||
var resolvedPaths = TemplateRegistry.resolvedCodeClientPaths(client.name);
|
||||
for (var p = 0; p < resolvedPaths.length; p++) {
|
||||
validClients.push(resolvedPaths[p]);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (include) {
|
||||
if (client.path)
|
||||
validClients.push(client.path);
|
||||
}
|
||||
}
|
||||
|
||||
if (validClients.length > 0) {
|
||||
path = validClients.join("\n");
|
||||
} else {
|
||||
path = I18n.tr("panels.color-scheme.templates-none-detected");
|
||||
}
|
||||
}
|
||||
|
||||
templates.push({
|
||||
"id": app.id,
|
||||
"name": app.name,
|
||||
"category": app.category || "misc",
|
||||
"tooltip": getDesc(path)
|
||||
});
|
||||
}
|
||||
|
||||
// Sort alphabetically by name
|
||||
templates.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return templates;
|
||||
}
|
||||
|
||||
// Category filter
|
||||
property string selectedCategory: ""
|
||||
|
||||
// Build available categories dynamically
|
||||
readonly property var availableCategories: {
|
||||
var cats = {};
|
||||
for (var i = 0; i < allTemplates.length; i++) {
|
||||
cats[allTemplates[i].category] = true;
|
||||
}
|
||||
return Object.keys(cats).sort();
|
||||
}
|
||||
|
||||
// Filter toggle
|
||||
property bool showOnlyActive: false
|
||||
|
||||
// Filtered templates based on category, search, and toggle
|
||||
property string searchText: ""
|
||||
readonly property var filteredTemplates: {
|
||||
var result = allTemplates;
|
||||
|
||||
// Filter by category first (unless searching)
|
||||
if (selectedCategory !== "" && searchText.trim() === "") {
|
||||
result = result.filter(t => t.category === selectedCategory);
|
||||
}
|
||||
|
||||
// Search overrides category filter
|
||||
if (searchText.trim() !== "") {
|
||||
var query = searchText.toLowerCase().trim();
|
||||
result = result.filter(t => t.name.toLowerCase().includes(query));
|
||||
}
|
||||
|
||||
// Filter by active if enabled (and not searching)
|
||||
if (showOnlyActive && searchText.trim() === "") {
|
||||
result = result.filter(t => isTemplateActive(t.id));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if a template is active
|
||||
function isTemplateActive(templateId) {
|
||||
for (var i = 0; i < Settings.data.templates.activeTemplates.length; i++) {
|
||||
if (Settings.data.templates.activeTemplates[i].id === templateId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Toggle a template on/off
|
||||
function toggleTemplate(templateId) {
|
||||
var current = Settings.data.templates.activeTemplates.slice();
|
||||
var existingIndex = -1;
|
||||
|
||||
for (var i = 0; i < current.length; i++) {
|
||||
if (current[i].id === templateId) {
|
||||
existingIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// Remove it
|
||||
current.splice(existingIndex, 1);
|
||||
} else {
|
||||
// Add it
|
||||
current.push({
|
||||
"id": templateId,
|
||||
"enabled": true
|
||||
});
|
||||
}
|
||||
|
||||
Settings.data.templates.activeTemplates = current;
|
||||
AppThemeService.generate();
|
||||
|
||||
// Clear search context on interaction to return to filtered view
|
||||
if (searchText !== "") {
|
||||
searchText = "";
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.color-scheme.templates-desc")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Category filter chips
|
||||
NTagFilter {
|
||||
tags: root.availableCategories
|
||||
selectedTag: root.selectedCategory
|
||||
onSelectedTagChanged: root.selectedCategory = selectedTag
|
||||
label: I18n.tr("panels.color-scheme.templates-filter-label")
|
||||
description: I18n.tr("panels.color-scheme.templates-filter-description")
|
||||
expanded: true
|
||||
}
|
||||
|
||||
// Search/filter input row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("placeholders.search")
|
||||
text: root.searchText
|
||||
onTextChanged: root.searchText = text
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "filter"
|
||||
tooltipText: root.showOnlyActive ? I18n.tr("actions.show-all") : I18n.tr("actions.show-active-only")
|
||||
|
||||
colorBg: root.showOnlyActive ? Color.mPrimary : Color.mSurface
|
||||
colorFg: root.showOnlyActive ? Color.mOnPrimary : Color.mOnSurface
|
||||
|
||||
onClicked: root.showOnlyActive = !root.showOnlyActive
|
||||
}
|
||||
}
|
||||
|
||||
// Chip grid - uniform columns
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
columns: 4
|
||||
columnSpacing: Style.marginS
|
||||
rowSpacing: Style.marginS
|
||||
|
||||
Repeater {
|
||||
model: filteredTemplates
|
||||
|
||||
Rectangle {
|
||||
id: chip
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.round(Style.baseWidgetSize * 0.9)
|
||||
radius: Style.iRadiusM
|
||||
color: chipMouse.containsMouse ? Color.mHover : (isActive ? Color.mPrimary : Color.mSurface)
|
||||
border.color: isActive ? Color.mPrimary : Color.mOutline
|
||||
border.width: Style.borderS
|
||||
|
||||
required property int index
|
||||
required property var modelData
|
||||
readonly property bool isActive: root.isTemplateActive(modelData.id)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: chipText
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Style.margin2L
|
||||
text: chip.modelData.name
|
||||
pointSize: Style.fontSizeS
|
||||
color: chipMouse.containsMouse ? Color.mOnHover : (isActive ? Color.mOnPrimary : Color.mOnSurface)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: chipMouse
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onClicked: root.toggleTemplate(chip.modelData.id)
|
||||
onEntered: {
|
||||
if (chip.modelData.tooltip) {
|
||||
TooltipService.show(chip, chip.modelData.tooltip, "bottom");
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
TooltipService.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No results message
|
||||
NText {
|
||||
visible: filteredTemplates.length === 0 && searchText.trim() !== ""
|
||||
text: I18n.tr("common.no-results")
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginM
|
||||
}
|
||||
|
||||
// User templates checkbox
|
||||
NCheckbox {
|
||||
label: I18n.tr("panels.color-scheme.templates-misc-user-templates-label")
|
||||
description: I18n.tr("panels.color-scheme.templates-misc-user-templates-description")
|
||||
checked: Settings.data.templates.enableUserTheming
|
||||
onToggled: checked => {
|
||||
Settings.data.templates.enableUserTheming = checked;
|
||||
if (checked) {
|
||||
TemplateRegistry.writeUserTemplatesToml();
|
||||
}
|
||||
AppThemeService.generate();
|
||||
}
|
||||
}
|
||||
}
|
||||
+786
@@ -0,0 +1,786 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
|
||||
import qs.Commons
|
||||
import qs.Services.Hardware
|
||||
import qs.Services.Networking
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: mainLayout.implicitHeight
|
||||
|
||||
// Configuration for shared use (e.g. by BluetoothPanel)
|
||||
property bool showOnlyLists: false
|
||||
|
||||
readonly property bool isScanningActive: BluetoothService.scanningActive
|
||||
readonly property bool isDiscoverable: BluetoothService.discoverable
|
||||
|
||||
// Device lists with local filtering logic
|
||||
readonly property var connectedDevices: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||
return [];
|
||||
var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && dev.connected);
|
||||
filtered = BluetoothService.dedupeDevices(filtered);
|
||||
return BluetoothService.sortDevices(filtered);
|
||||
}
|
||||
|
||||
readonly property var pairedDevices: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||
return [];
|
||||
var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted));
|
||||
filtered = BluetoothService.dedupeDevices(filtered);
|
||||
return BluetoothService.sortDevices(filtered);
|
||||
}
|
||||
|
||||
readonly property var unnamedAvailableDevices: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||
return [];
|
||||
return BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted);
|
||||
}
|
||||
|
||||
readonly property var availableDevices: {
|
||||
var list = root.unnamedAvailableDevices;
|
||||
|
||||
if (Settings.data.network.bluetoothHideUnnamedDevices) {
|
||||
list = list.filter(function (dev) {
|
||||
var dn = dev.name || dev.deviceName || "";
|
||||
var s = String(dn).trim();
|
||||
if (s.length === 0)
|
||||
return false;
|
||||
var lower = s.toLowerCase();
|
||||
if (lower === "unknown" || lower === "unnamed" || lower === "n/a" || lower === "na")
|
||||
return false;
|
||||
var addr = dev.address || dev.bdaddr || dev.mac || "";
|
||||
if (addr.length > 0) {
|
||||
var normName = s.toLowerCase().replace(/[^0-9a-z]/g, "");
|
||||
var normAddr = String(addr).toLowerCase().replace(/[^0-9a-z]/g, "");
|
||||
if (normName.length > 0 && normName === normAddr)
|
||||
return false;
|
||||
}
|
||||
var macRegexComb = /^(([0-9A-Fa-f]{2}[:\-]){5}[0-9A-Fa-f]{2}|([0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4}|[0-9A-Fa-f]{12})$/;
|
||||
if (macRegexComb.test(s)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
list = BluetoothService.dedupeDevices(list);
|
||||
return BluetoothService.sortDevices(list);
|
||||
}
|
||||
|
||||
// For managing expanded device details
|
||||
property string expandedDeviceKey: ""
|
||||
property bool detailsGrid: (Settings.data.network.bluetoothDetailsViewMode === "grid")
|
||||
|
||||
// Combined visibility check: tab must be visible AND the window must be visible
|
||||
readonly property bool effectivelyVisible: root.visible && Window.window && Window.window.visible
|
||||
|
||||
Connections {
|
||||
target: BluetoothService
|
||||
function onEnabledChanged() {
|
||||
stateChangeDebouncer.restart();
|
||||
}
|
||||
function onDiscoverableChanged() {
|
||||
stateChangeDebouncer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onEffectivelyVisibleChanged: stateChangeDebouncer.restart()
|
||||
|
||||
Timer {
|
||||
id: stateChangeDebouncer
|
||||
interval: 100 // 100ms debounce
|
||||
repeat: false
|
||||
onTriggered: root._updateScanningState()
|
||||
}
|
||||
|
||||
function _updateScanningState() {
|
||||
if (effectivelyVisible && BluetoothService.enabled && !showOnlyLists) {
|
||||
Logger.d("BluetoothPrefs", "Panel/tab active");
|
||||
if (!isScanningActive) {
|
||||
BluetoothService.setScanActive(true);
|
||||
}
|
||||
if (!Settings.data.network.disableDiscoverability && !isDiscoverable) {
|
||||
BluetoothService.setDiscoverable(true);
|
||||
}
|
||||
} else {
|
||||
Logger.d("BluetoothPrefs", "Panel/tab inactive");
|
||||
if (isScanningActive && !showOnlyLists) {
|
||||
BluetoothService.setScanActive(false);
|
||||
}
|
||||
if (isDiscoverable && !showOnlyLists) {
|
||||
BluetoothService.setDiscoverable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
// Ensure scanning is stopped when component is closed
|
||||
if (isScanningActive && !showOnlyLists) {
|
||||
BluetoothService.setScanActive(false);
|
||||
}
|
||||
// Ensure discoverable is disabled when component is closed
|
||||
if (isDiscoverable && !showOnlyLists) {
|
||||
BluetoothService.setDiscoverable(false);
|
||||
}
|
||||
Logger.d("BluetoothPrefs", "Panel closed");
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Style.marginL
|
||||
|
||||
// Master Control Section
|
||||
NBox {
|
||||
visible: !root.showOnlyLists
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: masterControlCol.implicitHeight + Style.margin2L
|
||||
color: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
id: masterControlCol
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("common.bluetooth")
|
||||
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
|
||||
checked: BluetoothService.enabled
|
||||
enabled: !NetworkService.airplaneModeEnabled && BluetoothService.bluetoothAvailable
|
||||
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: BluetoothService.enabled && isDiscoverable
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: BluetoothService.enabled && isDiscoverable
|
||||
Layout.fillWidth: true
|
||||
text: I18n.tr("panels.connections.bluetooth-discoverable", {
|
||||
hostName: HostService.hostName
|
||||
})
|
||||
color: Color.mOnSurfaceVariant
|
||||
richTextEnabled: true
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: !showOnlyLists
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Device List [1] (Connected)
|
||||
NBox {
|
||||
id: connectedDevicesBox
|
||||
visible: root.connectedDevices.length > 0 && BluetoothService.enabled
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: connectedDevicesCol.implicitHeight + Style.margin2M
|
||||
border.color: showOnlyLists ? Style.boxBorderColor : "transparent"
|
||||
color: showOnlyLists ? Color.mSurfaceVariant : "transparent"
|
||||
|
||||
ColumnLayout {
|
||||
id: connectedDevicesCol
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Style.marginM
|
||||
anchors.bottomMargin: Style.marginM
|
||||
anchors.leftMargin: showOnlyLists ? Style.marginL : 0
|
||||
anchors.rightMargin: showOnlyLists ? Style.marginL : 0
|
||||
spacing: Style.marginM
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bluetooth.panel.connected-devices")
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginS
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.connectedDevices
|
||||
delegate: nboxDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Devices List [2] (Paired)
|
||||
NBox {
|
||||
id: pairedDevicesBox
|
||||
visible: root.pairedDevices.length > 0 && BluetoothService.enabled
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: pairedDevicesCol.implicitHeight + Style.margin2M
|
||||
border.color: showOnlyLists ? Style.boxBorderColor : "transparent"
|
||||
color: showOnlyLists ? Color.mSurfaceVariant : "transparent"
|
||||
|
||||
ColumnLayout {
|
||||
id: pairedDevicesCol
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Style.marginM
|
||||
anchors.bottomMargin: Style.marginM
|
||||
anchors.leftMargin: showOnlyLists ? Style.marginL : 0
|
||||
anchors.rightMargin: showOnlyLists ? Style.marginL : 0
|
||||
spacing: Style.marginM
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bluetooth.panel.paired-devices")
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginS
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.pairedDevices
|
||||
delegate: nboxDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Device List [3] (Available)
|
||||
NBox {
|
||||
id: availableDevicesBox
|
||||
visible: !root.showOnlyLists && root.unnamedAvailableDevices.length > 0 && BluetoothService.enabled
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: availableDevicesCol.implicitHeight + Style.margin2M
|
||||
border.color: "transparent"
|
||||
color: showOnlyLists ? Color.mSurfaceVariant : "transparent"
|
||||
|
||||
ColumnLayout {
|
||||
id: availableDevicesCol
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Style.marginM
|
||||
anchors.bottomMargin: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bluetooth.panel.available-devices")
|
||||
description: BluetoothService.scanningActive ? I18n.tr("bluetooth.panel.scanning") : ""
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.availableDevices
|
||||
delegate: nboxDelegate
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: root.availableDevices.length === 0 && root.unnamedAvailableDevices.length > 0
|
||||
text: I18n.tr("panels.connections.bluetooth-devices-unnamed")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Style.marginL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: !showOnlyLists
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NBox {
|
||||
id: miscSettingsBox
|
||||
visible: !root.showOnlyLists && BluetoothService.enabled
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: miscSettingsCol.implicitHeight + Style.margin2XL
|
||||
color: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
id: miscSettingsCol
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginXL
|
||||
spacing: Style.marginM
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.connections.bluetooth-auto-connect-label")
|
||||
description: I18n.tr("panels.connections.bluetooth-auto-connect-description")
|
||||
checked: Settings.data.network.bluetoothAutoConnect
|
||||
onToggled: checked => Settings.data.network.bluetoothAutoConnect = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.connections.hide-unnamed-devices-label")
|
||||
description: I18n.tr("panels.connections.hide-unnamed-devices-description")
|
||||
checked: Settings.data.network.bluetoothHideUnnamedDevices
|
||||
onToggled: checked => Settings.data.network.bluetoothHideUnnamedDevices = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.connections.disable-discoverability-label")
|
||||
description: I18n.tr("panels.connections.disable-discoverability-description")
|
||||
checked: Settings.data.network.disableDiscoverability
|
||||
onToggled: checked => {
|
||||
Settings.data.network.disableDiscoverability = checked;
|
||||
BluetoothService.setDiscoverable(!checked);
|
||||
}
|
||||
}
|
||||
|
||||
// RSSI Polling
|
||||
NToggle {
|
||||
label: I18n.tr("panels.connections.bluetooth-rssi-polling-label")
|
||||
description: I18n.tr("panels.connections.bluetooth-rssi-polling-description")
|
||||
checked: Settings.data.network.bluetoothRssiPollingEnabled
|
||||
onToggled: checked => Settings.data.network.bluetoothRssiPollingEnabled = checked
|
||||
}
|
||||
NSpinBox {
|
||||
label: I18n.tr("panels.connections.bluetooth-rssi-polling-interval-label")
|
||||
description: I18n.tr("panels.connections.bluetooth-rssi-polling-interval-description")
|
||||
from: 10000
|
||||
to: 120000
|
||||
stepSize: 1000
|
||||
value: Settings.data.network.bluetoothRssiPollIntervalMs
|
||||
defaultValue: Settings.getDefaultValue("network.bluetoothRssiPollIntervalMs")
|
||||
onValueChanged: Settings.data.network.bluetoothRssiPollIntervalMs = value
|
||||
suffix: " ms"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: Settings.data.network.bluetoothRssiPollingEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shared Delegate
|
||||
Component {
|
||||
id: nboxDelegate
|
||||
NBox {
|
||||
id: device
|
||||
|
||||
readonly property bool canConnect: BluetoothService.canConnect(modelData)
|
||||
readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData)
|
||||
readonly property bool canPair: BluetoothService.canPair(modelData)
|
||||
readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData)
|
||||
readonly property bool isExpanded: root.expandedDeviceKey === BluetoothService.deviceKey(modelData)
|
||||
|
||||
function getContentColors(defaultColors = [Color.mSurface, Color.mOnSurface]) {
|
||||
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) {
|
||||
return [Color.mPrimary, Color.mOnPrimary];
|
||||
}
|
||||
if (modelData.connected && modelData.state !== BluetoothDeviceState.Disconnecting) {
|
||||
return [Color.mPrimary, Color.mOnPrimary];
|
||||
}
|
||||
if (modelData.blocked || modelData.state === BluetoothDeviceState.Disconnecting) {
|
||||
return [Color.mError, Color.mOnError];
|
||||
}
|
||||
return defaultColors;
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: deviceColumn.implicitHeight + (Style.marginXL)
|
||||
radius: Style.radiusM
|
||||
clip: true
|
||||
forceOpaque: true
|
||||
color: device.getContentColors()[0]
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
RowLayout {
|
||||
id: deviceLayout
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
icon: BluetoothService.getDeviceIcon(modelData)
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: device.getContentColors()[1]
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXXS
|
||||
|
||||
NText {
|
||||
text: modelData.name || modelData.deviceName
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
color: device.getContentColors()[1]
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: {
|
||||
const k = BluetoothService.getStatusKey(modelData);
|
||||
if (k === "pairing")
|
||||
return I18n.tr("common.pairing");
|
||||
if (k === "blocked")
|
||||
return I18n.tr("bluetooth.panel.blocked");
|
||||
if (k === "connecting")
|
||||
return I18n.tr("common.connecting");
|
||||
if (k === "disconnecting")
|
||||
return I18n.tr("common.disconnecting");
|
||||
return "";
|
||||
}
|
||||
visible: text !== ""
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Qt.alpha(device.getContentColors([Color.mSurfaceVariant, Color.mOnSurfaceVariant])[1], Style.opacityHeavy)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: modelData.batteryAvailable
|
||||
spacing: Style.marginS
|
||||
NIcon {
|
||||
icon: {
|
||||
var b = BluetoothService.getBatteryPercent(modelData);
|
||||
return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null);
|
||||
}
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Qt.alpha(device.getContentColors()[1], Style.opacityHeavy)
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
var b = BluetoothService.getBatteryPercent(modelData);
|
||||
return b === null ? "-" : (b + "%");
|
||||
}
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Qt.alpha(device.getContentColors([Color.mSurfaceVariant, Color.mOnSurfaceVariant])[1], Style.opacityHeavy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginS
|
||||
|
||||
NBusyIndicator {
|
||||
visible: isBusy
|
||||
running: visible && root.effectivelyVisible
|
||||
color: device.getContentColors()[1]
|
||||
size: Style.baseWidgetSize * 0.5
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
visible: modelData.connected && modelData.state !== BluetoothDeviceState.Disconnecting
|
||||
icon: "info"
|
||||
tooltipText: I18n.tr("common.info")
|
||||
baseSize: Style.baseWidgetSize * 0.75
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
onClicked: {
|
||||
const key = BluetoothService.deviceKey(modelData);
|
||||
root.expandedDeviceKey = (root.expandedDeviceKey === key) ? "" : key;
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
visible: !root.showOnlyLists && (modelData.paired || modelData.trusted) && !modelData.connected && !isBusy && !modelData.blocked
|
||||
icon: "trash"
|
||||
tooltipText: I18n.tr("common.unpair")
|
||||
baseSize: Style.baseWidgetSize * 0.75
|
||||
colorBg: Color.mPrimary
|
||||
colorFg: Color.mOnPrimary
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
onClicked: BluetoothService.unpairDevice(modelData)
|
||||
}
|
||||
|
||||
NButton {
|
||||
id: button
|
||||
visible: modelData.state !== BluetoothDeviceState.Connecting && modelData.state !== BluetoothDeviceState.Disconnecting
|
||||
enabled: (canConnect || canDisconnect || (root.showOnlyLists ? false : canPair)) && !isBusy
|
||||
fontSize: Style.fontSizeS
|
||||
backgroundColor: modelData.connected ? Color.mSurfaceVariant : Color.mPrimary
|
||||
textColor: modelData.connected ? Color.mOnSurface : Color.mOnPrimary
|
||||
text: {
|
||||
if (modelData.pairing)
|
||||
return I18n.tr("common.pairing");
|
||||
if (modelData.blocked)
|
||||
return I18n.tr("bluetooth.panel.blocked");
|
||||
if (modelData.connected)
|
||||
return I18n.tr("common.disconnect");
|
||||
if (!root.showOnlyLists && device.canPair)
|
||||
return I18n.tr("common.pair");
|
||||
return I18n.tr("common.connect");
|
||||
}
|
||||
onClicked: {
|
||||
if (modelData.connected) {
|
||||
BluetoothService.disconnectDevice(modelData);
|
||||
} else {
|
||||
if (!root.showOnlyLists && device.canPair) {
|
||||
BluetoothService.pairDevice(modelData);
|
||||
} else {
|
||||
BluetoothService.connectDeviceWithTrust(modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expanded info section
|
||||
Rectangle {
|
||||
visible: device.isExpanded
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: infoColumn.implicitHeight + Style.margin2S
|
||||
radius: Style.radiusXS
|
||||
color: Color.mSurfaceVariant
|
||||
border.width: Style.borderS
|
||||
border.color: Style.boxBorderColor
|
||||
clip: true
|
||||
|
||||
NIconButton {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Style.marginS
|
||||
icon: root.detailsGrid ? "layout-list" : "layout-grid"
|
||||
tooltipText: root.detailsGrid ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view")
|
||||
baseSize: Style.baseWidgetSize * 0.65
|
||||
onClicked: {
|
||||
root.detailsGrid = !root.detailsGrid;
|
||||
Settings.data.network.bluetoothDetailsViewMode = root.detailsGrid ? "grid" : "list";
|
||||
}
|
||||
z: 1
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: infoColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
flow: root.detailsGrid ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: root.detailsGrid ? 3 : 6
|
||||
columns: root.detailsGrid ? 2 : 1
|
||||
columnSpacing: Style.marginM
|
||||
rowSpacing: Style.marginXS
|
||||
|
||||
// --- Item 1: Signal Strength ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
spacing: Style.marginXS
|
||||
NIcon {
|
||||
icon: BluetoothService.getSignalIcon(modelData)
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
NText {
|
||||
text: BluetoothService.getSignalStrength(modelData)
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// --- Item 2: Battery ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
spacing: Style.marginXS
|
||||
NIcon {
|
||||
icon: {
|
||||
var b = BluetoothService.getBatteryPercent(modelData);
|
||||
return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null);
|
||||
}
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
var b = BluetoothService.getBatteryPercent(modelData);
|
||||
return b === null ? "-" : (b + "%");
|
||||
}
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
// --- Item 3: Pair state ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
spacing: Style.marginXS
|
||||
NIcon {
|
||||
icon: "link"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
NText {
|
||||
text: modelData.paired ? I18n.tr("common.yes") : I18n.tr("common.no")
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
// --- Item 4: Trust state ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
spacing: Style.marginXS
|
||||
NIcon {
|
||||
icon: "shield-check"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
NText {
|
||||
text: modelData.trusted ? I18n.tr("common.yes") : I18n.tr("common.no")
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
// --- Item 5: Address ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
spacing: Style.marginXS
|
||||
NIcon {
|
||||
icon: "hash"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
NText {
|
||||
text: modelData.address || "-"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
// --- Item 6: Auto-connect ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
Layout.topMargin: -Style.marginXXS
|
||||
spacing: Style.marginXS
|
||||
visible: Settings.data.network.bluetoothAutoConnect
|
||||
NIcon {
|
||||
icon: BluetoothService.getDeviceAutoConnect(modelData) ? "repeat" : "repeat-off"
|
||||
pointSize: Style.fontSizeXS
|
||||
}
|
||||
NCheckbox {
|
||||
label: I18n.tr("common.auto-connect")
|
||||
labelSize: Style.fontSizeXS
|
||||
baseSize: Style.baseWidgetSize * 0.5
|
||||
checked: BluetoothService.getDeviceAutoConnect(modelData)
|
||||
onToggled: checked => BluetoothService.setDeviceAutoConnect(modelData, checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PIN Authentication Overlay (This part needs some love :P)
|
||||
Rectangle {
|
||||
id: pinOverlay
|
||||
visible: !root.showOnlyLists && BluetoothService.pinRequired
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width * 0.9, 400)
|
||||
height: pinCol.implicitHeight + Style.margin2L
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusM
|
||||
border.color: Style.boxBorderColor
|
||||
border.width: Style.borderS
|
||||
z: 1000
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onClicked: mouse => mouse.accepted = true
|
||||
onWheel: wheel => wheel.accepted = true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: pinCol
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginL
|
||||
|
||||
NIcon {
|
||||
icon: "lock"
|
||||
pointSize: 48
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
NText {
|
||||
text: I18n.tr("panels.connections.authentication-required")
|
||||
pointSize: Style.fontSizeXL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NText {
|
||||
text: I18n.tr("panels.connections.pin-instructions")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NTextInput {
|
||||
id: pinInput
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "123456"
|
||||
inputIconName: "key"
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
text = "";
|
||||
inputItem.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
inputItem.onEditingFinished: {
|
||||
if (text.length > 0) {
|
||||
BluetoothService.submitPin(text);
|
||||
text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginM
|
||||
NButton {
|
||||
text: I18n.tr("common.cancel")
|
||||
icon: "x"
|
||||
onClicked: BluetoothService.cancelPairing()
|
||||
}
|
||||
NButton {
|
||||
text: I18n.tr("common.confirm")
|
||||
icon: "check"
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
enabled: pinInput.text.length > 0
|
||||
onClicked: {
|
||||
BluetoothService.submitPin(pinInput.text);
|
||||
pinInput.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Networking
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.wifi")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.bluetooth")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
WifiSubTab {}
|
||||
BluetoothSubTab {}
|
||||
}
|
||||
}
|
||||
+1462
File diff suppressed because it is too large
Load Diff
+87
@@ -0,0 +1,87 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NComboBox {
|
||||
id: controlCenterPosition
|
||||
label: I18n.tr("common.position")
|
||||
description: I18n.tr("panels.control-center.position-description")
|
||||
Layout.fillWidth: true
|
||||
model: [
|
||||
{
|
||||
"key": "close_to_bar_button",
|
||||
"name": I18n.tr("positions.close-to-bar")
|
||||
},
|
||||
{
|
||||
"key": "center",
|
||||
"name": I18n.tr("positions.center")
|
||||
},
|
||||
{
|
||||
"key": "top_center",
|
||||
"name": I18n.tr("positions.top-center")
|
||||
},
|
||||
{
|
||||
"key": "top_left",
|
||||
"name": I18n.tr("positions.top-left")
|
||||
},
|
||||
{
|
||||
"key": "top_right",
|
||||
"name": I18n.tr("positions.top-right")
|
||||
},
|
||||
{
|
||||
"key": "bottom_center",
|
||||
"name": I18n.tr("positions.bottom-center")
|
||||
},
|
||||
{
|
||||
"key": "bottom_left",
|
||||
"name": I18n.tr("positions.bottom-left")
|
||||
},
|
||||
{
|
||||
"key": "bottom_right",
|
||||
"name": I18n.tr("positions.bottom-right")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.controlCenter.position
|
||||
onSelected: function (key) {
|
||||
Settings.data.controlCenter.position = key;
|
||||
}
|
||||
defaultValue: Settings.getDefaultValue("controlCenter.position")
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
id: diskPathComboBox
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.control-center.system-monitor-disk-path-label")
|
||||
description: I18n.tr("panels.control-center.system-monitor-disk-path-description")
|
||||
model: {
|
||||
const paths = Object.keys(SystemStatService.diskPercents).sort();
|
||||
return paths.map(path => ({
|
||||
key: path,
|
||||
name: path
|
||||
}));
|
||||
}
|
||||
currentKey: Settings.data.controlCenter.diskPath || "/"
|
||||
onSelected: key => Settings.data.controlCenter.diskPath = key
|
||||
defaultValue: Settings.getDefaultValue("controlCenter.diskPath") || "/"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
required property var cardsModel
|
||||
required property list<var> cardsDefault
|
||||
|
||||
function saveCards() {
|
||||
var toSave = [];
|
||||
for (var i = 0; i < cardsModel.length; i++) {
|
||||
toSave.push({
|
||||
"id": cardsModel[i].id,
|
||||
"enabled": cardsModel[i].enabled
|
||||
});
|
||||
}
|
||||
Settings.data.controlCenter.cards = toSave;
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.control-center.cards-desc")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
Connections {
|
||||
target: Settings.data.location
|
||||
function onWeatherEnabledChanged() {
|
||||
// Auto-disable weather card when weather is disabled
|
||||
var newModel = root.cardsModel.slice();
|
||||
for (var i = 0; i < newModel.length; i++) {
|
||||
if (newModel[i].id === "weather-card") {
|
||||
newModel[i] = Object.assign({}, newModel[i], {
|
||||
"enabled": Settings.data.location.weatherEnabled
|
||||
});
|
||||
root.cardsModel = newModel;
|
||||
saveCards();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NReorderCheckboxes {
|
||||
Layout.fillWidth: true
|
||||
model: root.cardsModel
|
||||
disabledIds: Settings.data.location.weatherEnabled ? [] : ["weather-card"]
|
||||
onItemToggled: function (index, enabled) {
|
||||
var newModel = root.cardsModel.slice();
|
||||
newModel[index] = Object.assign({}, newModel[index], {
|
||||
"enabled": enabled
|
||||
});
|
||||
root.cardsModel = newModel;
|
||||
saveCards();
|
||||
}
|
||||
onItemsReordered: function (fromIndex, toIndex) {
|
||||
var newModel = root.cardsModel.slice();
|
||||
var item = newModel.splice(fromIndex, 1)[0];
|
||||
newModel.splice(toIndex, 0, item);
|
||||
root.cardsModel = newModel;
|
||||
saveCards();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
property list<var> cardsModel: []
|
||||
property list<var> cardsDefault: [
|
||||
{
|
||||
"id": "profile-card",
|
||||
"text": "Profile",
|
||||
"enabled": true,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "shortcuts-card",
|
||||
"text": "Shortcuts",
|
||||
"enabled": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "audio-card",
|
||||
"text": "Audio Sliders",
|
||||
"enabled": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "brightness-card",
|
||||
"text": "Brightness",
|
||||
"enabled": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "weather-card",
|
||||
"text": "Weather",
|
||||
"enabled": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "media-sysmon-card",
|
||||
"text": "Media and System Monitor",
|
||||
"enabled": true,
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
|
||||
Component.onCompleted: {
|
||||
// Fill out availableWidgets ListModel
|
||||
availableWidgets.clear();
|
||||
var sortedEntries = ControlCenterWidgetRegistry.getAvailableWidgets().slice().sort();
|
||||
sortedEntries.forEach(entry => {
|
||||
const isPlugin = ControlCenterWidgetRegistry.isPluginWidget(entry);
|
||||
let displayName = entry;
|
||||
let badges = [];
|
||||
if (isPlugin) {
|
||||
const pluginId = entry.replace("plugin:", "");
|
||||
const manifest = PluginRegistry.getPluginManifest(pluginId);
|
||||
if (manifest && manifest.name) {
|
||||
displayName = manifest.name;
|
||||
} else {
|
||||
displayName = pluginId;
|
||||
}
|
||||
badges.push({
|
||||
"icon": "plugin",
|
||||
"color": Color.mSecondary
|
||||
});
|
||||
}
|
||||
availableWidgets.append({
|
||||
"key": entry,
|
||||
"name": displayName,
|
||||
"badges": badges
|
||||
});
|
||||
});
|
||||
// Starts empty
|
||||
cardsModel = [];
|
||||
|
||||
// Add the cards available in settings
|
||||
for (var i = 0; i < Settings.data.controlCenter.cards.length; i++) {
|
||||
const settingCard = Settings.data.controlCenter.cards[i];
|
||||
|
||||
for (var j = 0; j < cardsDefault.length; j++) {
|
||||
if (settingCard.id === cardsDefault[j].id) {
|
||||
var card = cardsDefault[j];
|
||||
card.enabled = settingCard.enabled;
|
||||
// Auto-disable weather card if weather is disabled
|
||||
if (card.id === "weather-card" && !Settings.data.location.weatherEnabled) {
|
||||
card.enabled = false;
|
||||
}
|
||||
cardsModel.push(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any missing cards from default
|
||||
for (var i = 0; i < cardsDefault.length; i++) {
|
||||
var found = false;
|
||||
for (var j = 0; j < cardsModel.length; j++) {
|
||||
if (cardsModel[j].id === cardsDefault[i].id) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
var card = cardsDefault[i];
|
||||
// Auto-disable weather card if weather is disabled
|
||||
if (card.id === "weather-card" && !Settings.data.location.weatherEnabled) {
|
||||
card.enabled = false;
|
||||
}
|
||||
cardsModel.push(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.appearance")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.cards")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.shortcuts")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
AppearanceSubTab {}
|
||||
|
||||
CardsSubTab {
|
||||
cardsModel: root.cardsModel
|
||||
cardsDefault: root.cardsDefault
|
||||
}
|
||||
|
||||
ShortcutsSubTab {
|
||||
availableWidgets: availableWidgets
|
||||
onAddWidgetToSection: (widgetId, section) => _addWidgetToSection(widgetId, section)
|
||||
onRemoveWidgetFromSection: (section, index) => _removeWidgetFromSection(section, index)
|
||||
onReorderWidgetInSection: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettingsInSection: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidgetBetweenSections: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onOpenPluginSettingsRequested: manifest => pluginSettingsDialog.openPluginSettings(manifest)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// Signal functions
|
||||
// ---------------------------------
|
||||
function _addWidgetToSection(widgetId, section) {
|
||||
var newWidget = {
|
||||
"id": widgetId
|
||||
};
|
||||
if (ControlCenterWidgetRegistry.widgetHasUserSettings(widgetId)) {
|
||||
var metadata = ControlCenterWidgetRegistry.widgetMetadata[widgetId];
|
||||
if (metadata) {
|
||||
Object.keys(metadata).forEach(function (key) {
|
||||
newWidget[key] = metadata[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
Settings.data.controlCenter.shortcuts[section].push(newWidget);
|
||||
}
|
||||
|
||||
function _removeWidgetFromSection(section, index) {
|
||||
if (index >= 0 && index < Settings.data.controlCenter.shortcuts[section].length) {
|
||||
var newArray = Settings.data.controlCenter.shortcuts[section].slice();
|
||||
var removedWidgets = newArray.splice(index, 1);
|
||||
Settings.data.controlCenter.shortcuts[section] = newArray;
|
||||
}
|
||||
}
|
||||
|
||||
function _reorderWidgetInSection(section, fromIndex, toIndex) {
|
||||
if (fromIndex >= 0 && fromIndex < Settings.data.controlCenter.shortcuts[section].length && toIndex >= 0 && toIndex < Settings.data.controlCenter.shortcuts[section].length) {
|
||||
|
||||
// Create a new array to avoid modifying the original
|
||||
var newArray = Settings.data.controlCenter.shortcuts[section].slice();
|
||||
var item = newArray[fromIndex];
|
||||
newArray.splice(fromIndex, 1);
|
||||
newArray.splice(toIndex, 0, item);
|
||||
|
||||
Settings.data.controlCenter.shortcuts[section] = newArray;
|
||||
}
|
||||
}
|
||||
|
||||
function _moveWidgetBetweenSections(fromSection, index, toSection) {
|
||||
// Get the widget from the source section
|
||||
if (index >= 0 && index < Settings.data.controlCenter.shortcuts[fromSection].length) {
|
||||
var widget = Settings.data.controlCenter.shortcuts[fromSection][index];
|
||||
|
||||
// Remove from source section
|
||||
var sourceArray = Settings.data.controlCenter.shortcuts[fromSection].slice();
|
||||
sourceArray.splice(index, 1);
|
||||
Settings.data.controlCenter.shortcuts[fromSection] = sourceArray;
|
||||
|
||||
// Add to target section
|
||||
var targetArray = Settings.data.controlCenter.shortcuts[toSection].slice();
|
||||
targetArray.push(widget);
|
||||
Settings.data.controlCenter.shortcuts[toSection] = targetArray;
|
||||
}
|
||||
}
|
||||
|
||||
function _updateWidgetSettingsInSection(section, index, settings) {
|
||||
// Create a new array to trigger QML's change detection for persistence.
|
||||
// This is crucial for Settings.data to detect the change and persist it.
|
||||
var newSectionArray = Settings.data.controlCenter.shortcuts[section].slice();
|
||||
newSectionArray[index] = settings;
|
||||
Settings.data.controlCenter.shortcuts[section] = newSectionArray;
|
||||
Settings.saveImmediate();
|
||||
}
|
||||
|
||||
// Base list model for all combo boxes
|
||||
ListModel {
|
||||
id: availableWidgets
|
||||
}
|
||||
|
||||
// Shared Plugin Settings Popup
|
||||
NPluginSettingsPopup {
|
||||
id: pluginSettingsDialog
|
||||
parent: Overlay.overlay
|
||||
showToastOnSave: false
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
required property var availableWidgets
|
||||
|
||||
signal addWidgetToSection(string widgetId, string section)
|
||||
signal removeWidgetFromSection(string section, int index)
|
||||
signal reorderWidgetInSection(string section, int fromIndex, int toIndex)
|
||||
signal updateWidgetSettingsInSection(string section, int index, var settings)
|
||||
signal moveWidgetBetweenSections(string fromSection, int index, string toSection)
|
||||
signal openPluginSettingsRequested(var manifest)
|
||||
|
||||
function getSectionIcons() {
|
||||
return {
|
||||
"left": "arrow-bar-to-up",
|
||||
"right": "arrow-bar-to-down"
|
||||
};
|
||||
}
|
||||
|
||||
// Widgets Management Section
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Sections
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
// Left
|
||||
NSectionEditor {
|
||||
sectionName: I18n.tr("positions.left")
|
||||
sectionId: "left"
|
||||
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml")
|
||||
maxWidgets: Settings.data.controlCenter.shortcuts["right"].length > 5 ? 0 : (Settings.data.controlCenter.shortcuts["right"].length > 0 ? 5 : 10)
|
||||
widgetRegistry: ControlCenterWidgetRegistry
|
||||
widgetModel: Settings.data.controlCenter.shortcuts["left"]
|
||||
sectionIcons: root.getSectionIcons()
|
||||
availableWidgets: root.availableWidgets
|
||||
availableSections: ["left", "right"]
|
||||
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
|
||||
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onOpenPluginSettingsRequested: manifest => root.openPluginSettingsRequested(manifest)
|
||||
}
|
||||
|
||||
// Right
|
||||
NSectionEditor {
|
||||
sectionName: I18n.tr("positions.right")
|
||||
sectionId: "right"
|
||||
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml")
|
||||
maxWidgets: Settings.data.controlCenter.shortcuts["left"].length > 5 ? 0 : (Settings.data.controlCenter.shortcuts["left"].length > 0 ? 5 : 10)
|
||||
widgetRegistry: ControlCenterWidgetRegistry
|
||||
widgetModel: Settings.data.controlCenter.shortcuts["right"]
|
||||
sectionIcons: root.getSectionIcons()
|
||||
availableWidgets: root.availableWidgets
|
||||
availableSections: ["left", "right"]
|
||||
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
|
||||
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onOpenPluginSettingsRequested: manifest => root.openPluginSettingsRequested(manifest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
spacing: Style.marginL
|
||||
|
||||
// Available widgets model - declared early so Repeater delegates can access it
|
||||
property alias availableWidgetsModel: availableWidgets
|
||||
ListModel {
|
||||
id: availableWidgets
|
||||
}
|
||||
|
||||
// Listen for plugin widget registration/unregistration
|
||||
Connections {
|
||||
target: DesktopWidgetRegistry
|
||||
function onPluginWidgetRegistryUpdated() {
|
||||
updateAvailableWidgetsModel();
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.enabled-label")
|
||||
description: I18n.tr("panels.desktop-widgets.enabled-description")
|
||||
checked: Settings.data.desktopWidgets.enabled
|
||||
defaultValue: Settings.getDefaultValue("desktopWidgets.enabled")
|
||||
onToggled: checked => Settings.data.desktopWidgets.enabled = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
enabled: Settings.data.desktopWidgets.enabled
|
||||
label: I18n.tr("panels.desktop-widgets.overview-enabled-label")
|
||||
description: I18n.tr("panels.desktop-widgets.overview-enabled-description")
|
||||
checked: Settings.data.desktopWidgets.overviewEnabled
|
||||
defaultValue: Settings.getDefaultValue("desktopWidgets.overviewEnabled")
|
||||
onToggled: checked => Settings.data.desktopWidgets.overviewEnabled = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
enabled: Settings.data.desktopWidgets.enabled
|
||||
|
||||
NLabel {
|
||||
description: I18n.tr("panels.desktop-widgets.cpu-intensive-note")
|
||||
}
|
||||
|
||||
NButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginM
|
||||
Layout.bottomMargin: Style.marginM
|
||||
text: DesktopWidgetRegistry.editMode ? I18n.tr("panels.desktop-widgets.edit-mode-exit-button") : I18n.tr("panels.desktop-widgets.edit-mode-button-label")
|
||||
icon: "edit"
|
||||
onClicked: {
|
||||
DesktopWidgetRegistry.editMode = !DesktopWidgetRegistry.editMode;
|
||||
if (DesktopWidgetRegistry.editMode && Settings.data.ui.settingsPanelMode !== "window") {
|
||||
var item = root.parent;
|
||||
while (item) {
|
||||
if (item.closeRequested !== undefined) {
|
||||
item.closeRequested();
|
||||
break;
|
||||
}
|
||||
item = item.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One NSectionEditor per monitor
|
||||
Repeater {
|
||||
model: Quickshell.screens
|
||||
|
||||
NSectionEditor {
|
||||
required property var modelData
|
||||
readonly property real compositorScale: {
|
||||
const info = CompositorService.displayScales[modelData.name];
|
||||
return (info && info.scale) ? info.scale : 1.0;
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
sectionName: modelData.name
|
||||
sectionSubtitle: {
|
||||
// Format scale to 2 decimal places to prevent overly long text
|
||||
var formattedScale = compositorScale.toFixed(2);
|
||||
return "(" + modelData.width + "x" + modelData.height + " @ " + formattedScale + "x)";
|
||||
}
|
||||
|
||||
sectionId: modelData.name
|
||||
screen: modelData
|
||||
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml")
|
||||
widgetRegistry: DesktopWidgetRegistry
|
||||
widgetModel: getWidgetsForMonitor(modelData.name)
|
||||
availableWidgets: root.availableWidgetsModel
|
||||
availableSections: root.getScreenNames()
|
||||
sectionLabels: root.getScreenLabels()
|
||||
sectionIcons: root.getScreenIcons()
|
||||
draggable: false // Desktop widgets are positioned by X,Y, not list order
|
||||
crossSectionDraggable: true
|
||||
maxWidgets: -1
|
||||
onAddWidget: (widgetId, section) => _addWidgetToMonitor(modelData.name, widgetId)
|
||||
onRemoveWidget: (section, index) => _removeWidgetFromMonitor(modelData.name, index)
|
||||
onMoveWidget: (fromSection, index, toSection) => _moveWidgetToMonitor(fromSection, index, toSection)
|
||||
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsForMonitor(modelData.name, index, settings)
|
||||
pluginSettingsEntryPoints: ["desktopWidgetSettings", "settings"]
|
||||
onOpenPluginSettingsRequested: (manifest, entryPoint) => pluginSettingsDialog.openPluginSettings(manifest, entryPoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shared Plugin Settings Popup
|
||||
NPluginSettingsPopup {
|
||||
id: pluginSettingsDialog
|
||||
parent: Overlay.overlay
|
||||
showToastOnSave: false
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Use Qt.callLater to ensure DesktopWidgetRegistry is ready
|
||||
Qt.callLater(updateAvailableWidgetsModel);
|
||||
}
|
||||
|
||||
// Helper to get screen names array
|
||||
function getScreenNames() {
|
||||
var names = [];
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
names.push(Quickshell.screens[i].name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
// Helper to get screen labels map (just screen names, NSectionEditor adds "Move to" prefix)
|
||||
function getScreenLabels() {
|
||||
var labels = {};
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
var screen = Quickshell.screens[i];
|
||||
labels[screen.name] = screen.name;
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Helper to get screen icons map
|
||||
function getScreenIcons() {
|
||||
var icons = {};
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
icons[Quickshell.screens[i].name] = "device-desktop";
|
||||
}
|
||||
return icons;
|
||||
}
|
||||
|
||||
function updateAvailableWidgetsModel() {
|
||||
availableWidgets.clear();
|
||||
try {
|
||||
if (typeof DesktopWidgetRegistry === "undefined" || !DesktopWidgetRegistry) {
|
||||
Logger.e("DesktopWidgetsTab", "DesktopWidgetRegistry is not available");
|
||||
// Retry after a short delay
|
||||
Qt.callLater(function () {
|
||||
if (typeof DesktopWidgetRegistry !== "undefined" && DesktopWidgetRegistry) {
|
||||
updateAvailableWidgetsModel();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
var widgetIds = DesktopWidgetRegistry.getAvailableWidgets();
|
||||
Logger.d("DesktopWidgetsTab", "Found widgets:", widgetIds, "count:", widgetIds ? widgetIds.length : 0);
|
||||
if (!widgetIds || widgetIds.length === 0) {
|
||||
Logger.w("DesktopWidgetsTab", "No widgets found in registry");
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < widgetIds.length; i++) {
|
||||
var widgetId = widgetIds[i];
|
||||
var displayName = widgetId;
|
||||
|
||||
// Get plugin name for plugin widgets
|
||||
var isPlugin = false;
|
||||
if (DesktopWidgetRegistry.isPluginWidget(widgetId)) {
|
||||
isPlugin = true;
|
||||
var pluginId = widgetId.replace("plugin:", "");
|
||||
var manifest = PluginRegistry.getPluginManifest(pluginId);
|
||||
if (manifest && manifest.name) {
|
||||
displayName = manifest.name;
|
||||
}
|
||||
}
|
||||
|
||||
// Add plugin badge first (with custom color)
|
||||
const badges = [];
|
||||
if (isPlugin) {
|
||||
badges.push({
|
||||
"icon": "plugin",
|
||||
"color": Color.mSecondary
|
||||
});
|
||||
}
|
||||
if (DesktopWidgetRegistry.isCpuIntensive(widgetId)) {
|
||||
badges.push({
|
||||
"icon": "cpu-intensive",
|
||||
"color": Color.mSecondary
|
||||
});
|
||||
}
|
||||
|
||||
availableWidgets.append({
|
||||
"key": widgetId,
|
||||
"name": displayName,
|
||||
"badges": badges
|
||||
});
|
||||
}
|
||||
Logger.d("DesktopWidgetsTab", "Available widgets model count:", availableWidgets.count);
|
||||
} catch (e) {
|
||||
Logger.e("DesktopWidgetsTab", "Error updating available widgets:", e, e.stack);
|
||||
}
|
||||
}
|
||||
|
||||
// Get widgets for a specific monitor
|
||||
function getWidgetsForMonitor(monitorName) {
|
||||
var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || [];
|
||||
for (var i = 0; i < monitorWidgets.length; i++) {
|
||||
if (monitorWidgets[i].name === monitorName) {
|
||||
return monitorWidgets[i].widgets || [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Set widgets for a specific monitor
|
||||
function setWidgetsForMonitor(monitorName, widgets) {
|
||||
var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || [];
|
||||
var newMonitorWidgets = monitorWidgets.slice();
|
||||
var found = false;
|
||||
for (var i = 0; i < newMonitorWidgets.length; i++) {
|
||||
if (newMonitorWidgets[i].name === monitorName) {
|
||||
newMonitorWidgets[i] = {
|
||||
"name": monitorName,
|
||||
"widgets": widgets
|
||||
};
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
newMonitorWidgets.push({
|
||||
"name": monitorName,
|
||||
"widgets": widgets
|
||||
});
|
||||
}
|
||||
Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets;
|
||||
}
|
||||
|
||||
function _addWidgetToMonitor(monitorName, widgetId) {
|
||||
var newWidget = {
|
||||
"id": widgetId
|
||||
};
|
||||
if (DesktopWidgetRegistry.widgetHasUserSettings(widgetId)) {
|
||||
var metadata = DesktopWidgetRegistry.widgetMetadata[widgetId];
|
||||
if (metadata) {
|
||||
Object.keys(metadata).forEach(function (key) {
|
||||
newWidget[key] = metadata[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
// Set default positions
|
||||
if (widgetId === "Clock") {
|
||||
newWidget.x = 50;
|
||||
newWidget.y = 50;
|
||||
} else if (widgetId === "MediaPlayer") {
|
||||
newWidget.x = 100;
|
||||
newWidget.y = 200;
|
||||
} else if (widgetId === "AudioVisualizer") {
|
||||
newWidget.x = 120;
|
||||
newWidget.y = 280;
|
||||
} else if (widgetId === "Weather") {
|
||||
newWidget.x = 100;
|
||||
newWidget.y = 300;
|
||||
} else {
|
||||
// Default position for plugin widgets
|
||||
newWidget.x = 150;
|
||||
newWidget.y = 150;
|
||||
}
|
||||
var widgets = getWidgetsForMonitor(monitorName).slice();
|
||||
widgets.push(newWidget);
|
||||
setWidgetsForMonitor(monitorName, widgets);
|
||||
}
|
||||
|
||||
function _removeWidgetFromMonitor(monitorName, index) {
|
||||
var widgets = getWidgetsForMonitor(monitorName);
|
||||
if (index >= 0 && index < widgets.length) {
|
||||
var newArray = widgets.slice();
|
||||
newArray.splice(index, 1);
|
||||
setWidgetsForMonitor(monitorName, newArray);
|
||||
}
|
||||
}
|
||||
|
||||
function _updateWidgetSettingsForMonitor(monitorName, index, settings) {
|
||||
var widgets = getWidgetsForMonitor(monitorName);
|
||||
if (index >= 0 && index < widgets.length) {
|
||||
var newArray = widgets.slice();
|
||||
newArray[index] = Object.assign({}, newArray[index], settings);
|
||||
setWidgetsForMonitor(monitorName, newArray);
|
||||
}
|
||||
}
|
||||
|
||||
function _moveWidgetToMonitor(fromMonitor, index, toMonitor) {
|
||||
Logger.i("DesktopWidgetsTab", "Moving widget from", fromMonitor, "index", index, "to", toMonitor);
|
||||
var sourceWidgets = getWidgetsForMonitor(fromMonitor);
|
||||
if (index < 0 || index >= sourceWidgets.length) {
|
||||
Logger.e("DesktopWidgetsTab", "Invalid index", index, "for monitor", fromMonitor);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the widget to move
|
||||
var newSourceWidgets = sourceWidgets.slice();
|
||||
var widget = Object.assign({}, newSourceWidgets.splice(index, 1)[0]);
|
||||
|
||||
// Reset position and scale to ensure widget is accessible on new screen
|
||||
widget.x = 100;
|
||||
widget.y = 100;
|
||||
widget.scale = 1.0;
|
||||
|
||||
// Add to destination monitor
|
||||
var destWidgets = getWidgetsForMonitor(toMonitor).slice();
|
||||
destWidgets.push(widget);
|
||||
|
||||
// Update both monitors
|
||||
setWidgetsForMonitor(fromMonitor, newSourceWidgets);
|
||||
setWidgetsForMonitor(toMonitor, destWidgets);
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.Hardware
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL
|
||||
|
||||
Repeater {
|
||||
model: Quickshell.screens || []
|
||||
delegate: NBox {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Math.round(contentCol.implicitHeight + Style.margin2L)
|
||||
color: Color.mSurface
|
||||
|
||||
property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData)
|
||||
property real localBrightness: 0.5
|
||||
property bool localBrightnessChanging: false
|
||||
readonly property string automaticOptionLabel: {
|
||||
var baseLabel = I18n.tr("panels.display.monitors-backlight-device-auto-option");
|
||||
var autoDevicePath = (BrightnessService.availableBacklightDevices && BrightnessService.availableBacklightDevices.length > 0) ? BrightnessService.availableBacklightDevices[0] : "";
|
||||
if (autoDevicePath === "")
|
||||
return baseLabel;
|
||||
|
||||
var autoDeviceName = BrightnessService.getBacklightDeviceName(autoDevicePath) || autoDevicePath;
|
||||
return baseLabel + "(" + autoDeviceName + ")";
|
||||
}
|
||||
readonly property var backlightDeviceOptions: {
|
||||
var options = [
|
||||
{
|
||||
"key": "",
|
||||
"name": automaticOptionLabel
|
||||
}
|
||||
];
|
||||
|
||||
var devices = BrightnessService.availableBacklightDevices || [];
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
var devicePath = devices[i];
|
||||
var deviceName = BrightnessService.getBacklightDeviceName(devicePath) || devicePath;
|
||||
options.push({
|
||||
"key": devicePath,
|
||||
"name": deviceName
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
onBrightnessMonitorChanged: {
|
||||
if (brightnessMonitor && !localBrightnessChanging)
|
||||
localBrightness = brightnessMonitor.brightness || 0.5;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BrightnessService
|
||||
function onMonitorBrightnessChanged(monitor, newBrightness) {
|
||||
if (monitor === brightnessMonitor && !localBrightnessChanging) {
|
||||
localBrightness = newBrightness;
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: brightnessMonitor
|
||||
ignoreUnknownSignals: true
|
||||
function onBrightnessUpdated() {
|
||||
if (brightnessMonitor && !localBrightnessChanging) {
|
||||
localBrightness = brightnessMonitor.brightness || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: debounceTimer
|
||||
interval: 120
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable && Math.abs(localBrightness - brightnessMonitor.brightness) >= 0.005) {
|
||||
brightnessMonitor.setBrightness(localBrightness);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentCol
|
||||
width: parent.width - 2 * Style.marginL
|
||||
x: Style.marginL
|
||||
y: Style.marginL
|
||||
spacing: Style.marginXXS
|
||||
|
||||
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
|
||||
readonly property real compositorScale: {
|
||||
const info = CompositorService.displayScales[modelData.name];
|
||||
return (info && info.scale) ? info.scale : 1.0;
|
||||
}
|
||||
text: {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
visible: brightnessMonitor !== undefined && brightnessMonitor !== null
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginL
|
||||
|
||||
NText {
|
||||
text: I18n.tr("common.brightness")
|
||||
Layout.preferredWidth: 90
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
id: brightnessSlider
|
||||
from: 0
|
||||
to: 1
|
||||
value: localBrightness
|
||||
stepSize: 0.01
|
||||
enabled: brightnessMonitor ? brightnessMonitor.brightnessControlAvailable : false
|
||||
onMoved: value => {
|
||||
if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) {
|
||||
localBrightness = value;
|
||||
debounceTimer.restart();
|
||||
}
|
||||
}
|
||||
onPressedChanged: (pressed, value) => {
|
||||
localBrightnessChanging = pressed;
|
||||
if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) {
|
||||
if (pressed) {
|
||||
localBrightness = value;
|
||||
debounceTimer.restart();
|
||||
} else {
|
||||
localBrightness = value;
|
||||
debounceTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: brightnessMonitor ? Math.round(localBrightness * 100) + "%" : "N/A"
|
||||
Layout.preferredWidth: 55
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 30
|
||||
Layout.fillHeight: true
|
||||
NIcon {
|
||||
icon: brightnessMonitor && brightnessMonitor.method == "internal" ? "device-laptop" : "device-desktop"
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable && !(brightnessMonitor.method === "internal" && brightnessMonitor.initInProgress)
|
||||
text: !Settings.data.brightness.enableDdcSupport ? I18n.tr("panels.display.monitors-brightness-unavailable-ddc-disabled") : I18n.tr("panels.display.monitors-brightness-unavailable-generic")
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
visible: brightnessMonitor && brightnessMonitor.method === "internal"
|
||||
label: I18n.tr("panels.display.monitors-backlight-device-label")
|
||||
description: I18n.tr("panels.display.monitors-backlight-device-description")
|
||||
model: backlightDeviceOptions
|
||||
currentKey: BrightnessService.getMappedBacklightDevice(modelData.name) || ""
|
||||
onSelected: key => BrightnessService.setMappedBacklightDevice(modelData.name, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.display.monitors-brightness-step-label")
|
||||
description: I18n.tr("panels.display.monitors-brightness-step-description")
|
||||
minimum: 1
|
||||
maximum: 50
|
||||
value: Settings.data.brightness.brightnessStep
|
||||
stepSize: 1
|
||||
suffix: "%"
|
||||
onValueChanged: Settings.data.brightness.brightnessStep = value
|
||||
defaultValue: Settings.getDefaultValue("brightness.brightnessStep")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.display.monitors-enforce-minimum-label")
|
||||
description: I18n.tr("panels.display.monitors-enforce-minimum-description")
|
||||
checked: Settings.data.brightness.enforceMinimum
|
||||
onToggled: checked => Settings.data.brightness.enforceMinimum = checked
|
||||
defaultValue: Settings.getDefaultValue("brightness.enforceMinimum")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.display.monitors-external-brightness-label")
|
||||
description: I18n.tr("panels.display.monitors-external-brightness-description")
|
||||
checked: Settings.data.brightness.enableDdcSupport
|
||||
onToggled: checked => {
|
||||
Settings.data.brightness.enableDdcSupport = checked;
|
||||
}
|
||||
defaultValue: Settings.getDefaultValue("brightness.enableDdcSupport")
|
||||
}
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Location
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
// Time dropdown options (00:00 .. 23:30)
|
||||
ListModel {
|
||||
id: timeOptions
|
||||
}
|
||||
|
||||
function populateTimeOptions() {
|
||||
for (var h = 0; h < 24; h++) {
|
||||
for (var m = 0; m < 60; m += 30) {
|
||||
var hh = ("0" + h).slice(-2);
|
||||
var mm = ("0" + m).slice(-2);
|
||||
var key = hh + ":" + mm;
|
||||
timeOptions.append({
|
||||
"key": key,
|
||||
"name": key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(populateTimeOptions);
|
||||
}
|
||||
|
||||
// Check for wlsunset availability when enabling Night Light
|
||||
Process {
|
||||
id: wlsunsetCheck
|
||||
command: ["sh", "-c", "command -v wlsunset"]
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
Settings.data.nightLight.enabled = true;
|
||||
NightLightService.apply();
|
||||
ToastService.showNotice(I18n.tr("common.night-light"), I18n.tr("common.enabled"), "nightlight-on");
|
||||
} else {
|
||||
Settings.data.nightLight.enabled = false;
|
||||
ToastService.showWarning(I18n.tr("common.night-light"), I18n.tr("toast.night-light.not-installed"));
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.brightness")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.night-light")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
BrightnessSubTab {}
|
||||
NightLightSubTab {
|
||||
timeOptions: timeOptions
|
||||
onCheckWlsunset: wlsunsetCheck.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Location
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property var timeOptions
|
||||
|
||||
signal checkWlsunset
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.display.night-light-enable-label")
|
||||
description: I18n.tr("panels.display.night-light-enable-description")
|
||||
checked: Settings.data.nightLight.enabled
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
root.checkWlsunset();
|
||||
} else {
|
||||
Settings.data.nightLight.enabled = false;
|
||||
Settings.data.nightLight.forced = false;
|
||||
NightLightService.apply();
|
||||
ToastService.showNotice(I18n.tr("common.night-light"), I18n.tr("common.disabled"), "nightlight-off");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
enabled: Settings.data.nightLight.enabled
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.display.night-light-temperature-night")
|
||||
description: I18n.tr("panels.display.night-light-temperature-night-description")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NSlider {
|
||||
id: nightSlider
|
||||
Layout.fillWidth: true
|
||||
from: 1000
|
||||
to: 6500
|
||||
value: Settings.data.nightLight.nightTemp
|
||||
|
||||
onValueChanged: {
|
||||
var dayTemp = parseInt(Settings.data.nightLight.dayTemp);
|
||||
var v = Math.round(value);
|
||||
if (!isNaN(dayTemp)) {
|
||||
var maxNight = dayTemp - 500;
|
||||
v = Math.min(maxNight, Math.max(1000, v));
|
||||
} else {
|
||||
v = Math.max(1000, v);
|
||||
}
|
||||
if (v !== value)
|
||||
value = v;
|
||||
}
|
||||
|
||||
onPressedChanged: {
|
||||
if (!pressed) {
|
||||
var dayTemp = parseInt(Settings.data.nightLight.dayTemp);
|
||||
var v = Math.round(value);
|
||||
if (!isNaN(dayTemp)) {
|
||||
var maxNight = dayTemp - 500;
|
||||
v = Math.min(maxNight, Math.max(1000, v));
|
||||
} else {
|
||||
v = Math.max(1000, v);
|
||||
}
|
||||
Settings.data.nightLight.nightTemp = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: nightSlider.value + "K"
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.display.night-light-temperature-day")
|
||||
description: I18n.tr("panels.display.night-light-temperature-day-description")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NSlider {
|
||||
id: daySlider
|
||||
Layout.fillWidth: true
|
||||
from: 1000
|
||||
to: 6500
|
||||
value: Settings.data.nightLight.dayTemp
|
||||
|
||||
onValueChanged: {
|
||||
var nightTemp = parseInt(Settings.data.nightLight.nightTemp);
|
||||
var v = Math.round(value);
|
||||
if (!isNaN(nightTemp)) {
|
||||
var minDay = nightTemp + 500;
|
||||
v = Math.max(minDay, Math.min(6500, v));
|
||||
} else {
|
||||
v = Math.min(6500, v);
|
||||
}
|
||||
if (v !== value)
|
||||
value = v;
|
||||
}
|
||||
|
||||
onPressedChanged: {
|
||||
if (!pressed) {
|
||||
var nightTemp = parseInt(Settings.data.nightLight.nightTemp);
|
||||
var v = Math.round(value);
|
||||
if (!isNaN(nightTemp)) {
|
||||
var minDay = nightTemp + 500;
|
||||
v = Math.max(minDay, Math.min(6500, v));
|
||||
} else {
|
||||
v = Math.min(6500, v);
|
||||
}
|
||||
Settings.data.nightLight.dayTemp = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: daySlider.value + "K"
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.display.night-light-auto-schedule-label")
|
||||
description: I18n.tr("panels.display.night-light-auto-schedule-description", {
|
||||
"location": LocationService.stableName
|
||||
})
|
||||
checked: Settings.data.nightLight.autoSchedule
|
||||
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
visible: !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.display.night-light-manual-schedule-label")
|
||||
description: I18n.tr("panels.display.night-light-manual-schedule-description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.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("panels.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("panels.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("panels.display.night-light-manual-schedule-select-stop")
|
||||
onSelected: key => Settings.data.nightLight.manualSunset = key
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.display.night-light-force-activation-label")
|
||||
description: I18n.tr("panels.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+433
@@ -0,0 +1,433 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
readonly property color launcherPreviewColor: Color.resolveColorKey((Settings.data.dock.launcherIconColor !== undefined) ? Settings.data.dock.launcherIconColor : "none")
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.dock.enabled-label")
|
||||
description: I18n.tr("panels.dock.enabled-description")
|
||||
checked: Settings.data.dock.enabled
|
||||
defaultValue: Settings.getDefaultValue("dock.enabled")
|
||||
onToggled: checked => Settings.data.dock.enabled = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL
|
||||
enabled: Settings.data.dock.enabled
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.dock.appearance-position-label")
|
||||
description: I18n.tr("panels.dock.appearance-position-description")
|
||||
model: [
|
||||
{
|
||||
"key": "top",
|
||||
"name": I18n.tr("positions.top")
|
||||
},
|
||||
{
|
||||
"key": "bottom",
|
||||
"name": I18n.tr("positions.bottom")
|
||||
},
|
||||
{
|
||||
"key": "left",
|
||||
"name": I18n.tr("positions.left")
|
||||
},
|
||||
{
|
||||
"key": "right",
|
||||
"name": I18n.tr("positions.right")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.dock.position
|
||||
defaultValue: Settings.getDefaultValue("dock.position")
|
||||
onSelected: key => Settings.data.dock.position = key
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.dock.appearance-type-label")
|
||||
description: I18n.tr("panels.dock.appearance-type-description")
|
||||
model: [
|
||||
{
|
||||
"key": "floating",
|
||||
"name": I18n.tr("panels.dock.appearance-type-floating")
|
||||
},
|
||||
{
|
||||
"key": "attached",
|
||||
"name": I18n.tr("panels.dock.appearance-type-attached")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.dock.dockType
|
||||
defaultValue: Settings.getDefaultValue("dock.dockType")
|
||||
onSelected: key => Settings.data.dock.dockType = key
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
visible: Settings.data.dock.dockType === "floating"
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.display.title")
|
||||
description: I18n.tr("panels.dock.appearance-display-description")
|
||||
model: [
|
||||
{
|
||||
"key": "always_visible",
|
||||
"name": I18n.tr("hide-modes.visible")
|
||||
},
|
||||
{
|
||||
"key": "auto_hide",
|
||||
"name": I18n.tr("panels.dock.appearance-display-auto-hide")
|
||||
},
|
||||
{
|
||||
"key": "exclusive",
|
||||
"name": I18n.tr("panels.dock.appearance-display-exclusive")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.dock.displayMode
|
||||
defaultValue: Settings.getDefaultValue("dock.displayMode")
|
||||
onSelected: key => {
|
||||
Settings.data.dock.displayMode = key;
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.dock.dockType === "attached" && Settings.data.bar.barType === "framed"
|
||||
label: I18n.tr("panels.dock.appearance-sit-on-frame-label")
|
||||
description: I18n.tr("panels.dock.appearance-sit-on-frame-description")
|
||||
checked: Settings.data.dock.sitOnFrame
|
||||
defaultValue: Settings.getDefaultValue("dock.sitOnFrame")
|
||||
onToggled: checked => Settings.data.dock.sitOnFrame = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.dock.appearance-dock-indicator-label")
|
||||
description: I18n.tr("panels.dock.appearance-dock-indicator-description")
|
||||
checked: Settings.data.dock.showDockIndicator
|
||||
defaultValue: Settings.getDefaultValue("dock.showDockIndicator")
|
||||
onToggled: checked => Settings.data.dock.showDockIndicator = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.dock.showDockIndicator
|
||||
label: I18n.tr("panels.dock.appearance-indicator-thickness-label")
|
||||
description: I18n.tr("panels.dock.appearance-indicator-thickness-description")
|
||||
checked: (Settings.data.dock.indicatorThickness || 3) >= 6
|
||||
defaultValue: (Settings.getDefaultValue("dock.indicatorThickness") || 3) >= 6
|
||||
onToggled: checked => Settings.data.dock.indicatorThickness = checked ? 6 : 3
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.dock.showDockIndicator
|
||||
label: I18n.tr("panels.dock.appearance-indicator-color-label")
|
||||
description: I18n.tr("panels.dock.appearance-indicator-color-description")
|
||||
currentKey: Settings.data.dock.indicatorColor || "primary"
|
||||
defaultValue: Settings.getDefaultValue("dock.indicatorColor")
|
||||
onSelected: key => Settings.data.dock.indicatorColor = key
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.dock.showDockIndicator
|
||||
label: I18n.tr("panels.dock.appearance-indicator-opacity-label")
|
||||
description: I18n.tr("panels.dock.appearance-indicator-opacity-description")
|
||||
from: 0.1
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.dock.indicatorOpacity
|
||||
defaultValue: Settings.getDefaultValue("dock.indicatorOpacity")
|
||||
onMoved: value => Settings.data.dock.indicatorOpacity = value
|
||||
text: Math.floor(Settings.data.dock.indicatorOpacity * 100) + "%"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.osd.background-opacity-label")
|
||||
description: I18n.tr("panels.dock.appearance-background-opacity-description")
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.dock.backgroundOpacity
|
||||
defaultValue: Settings.getDefaultValue("dock.backgroundOpacity")
|
||||
onMoved: value => Settings.data.dock.backgroundOpacity = value
|
||||
text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.dock.appearance-dead-opacity-label")
|
||||
description: I18n.tr("panels.dock.appearance-dead-opacity-description")
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.dock.deadOpacity
|
||||
defaultValue: Settings.getDefaultValue("dock.deadOpacity")
|
||||
onMoved: value => Settings.data.dock.deadOpacity = value
|
||||
text: Math.floor(Settings.data.dock.deadOpacity * 100) + "%"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.dock.dockType === "floating"
|
||||
label: I18n.tr("panels.dock.appearance-floating-distance-label")
|
||||
description: I18n.tr("panels.dock.appearance-floating-distance-description")
|
||||
from: 0
|
||||
to: 4
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.dock.floatingRatio
|
||||
defaultValue: Settings.getDefaultValue("dock.floatingRatio")
|
||||
onMoved: value => Settings.data.dock.floatingRatio = value
|
||||
text: Math.floor(Settings.data.dock.floatingRatio * 100) + "%"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.dock.appearance-icon-size-label")
|
||||
description: I18n.tr("panels.dock.appearance-icon-size-description")
|
||||
from: 0
|
||||
to: 2
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.dock.size
|
||||
defaultValue: Settings.getDefaultValue("dock.size")
|
||||
onMoved: value => Settings.data.dock.size = value
|
||||
text: Math.floor(Settings.data.dock.size * 100) + "%"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
visible: Settings.data.dock.dockType === "floating" && Settings.data.dock.displayMode === "auto_hide"
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.dock.appearance-hide-show-speed-label")
|
||||
description: I18n.tr("panels.dock.appearance-hide-show-speed-description")
|
||||
from: 0.1
|
||||
to: 2.0
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.dock.animationSpeed
|
||||
defaultValue: Settings.getDefaultValue("dock.animationSpeed")
|
||||
onMoved: value => Settings.data.dock.animationSpeed = value
|
||||
text: (Settings.data.dock.animationSpeed * 100).toFixed(0) + "%"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.dock.appearance-inactive-indicators-label")
|
||||
description: I18n.tr("panels.dock.appearance-inactive-indicators-description")
|
||||
checked: Settings.data.dock.inactiveIndicators
|
||||
defaultValue: Settings.getDefaultValue("dock.inactiveIndicators")
|
||||
onToggled: checked => Settings.data.dock.inactiveIndicators = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.dock.appearance-pinned-static-label")
|
||||
description: I18n.tr("panels.dock.appearance-pinned-static-description")
|
||||
checked: Settings.data.dock.pinnedStatic
|
||||
defaultValue: Settings.getDefaultValue("dock.pinnedStatic")
|
||||
onToggled: checked => Settings.data.dock.pinnedStatic = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.dock.appearance-group-apps-label")
|
||||
description: I18n.tr("panels.dock.appearance-group-apps-description")
|
||||
checked: Settings.data.dock.groupApps
|
||||
defaultValue: Settings.getDefaultValue("dock.groupApps")
|
||||
onToggled: checked => Settings.data.dock.groupApps = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.dock.groupApps
|
||||
label: I18n.tr("panels.dock.appearance-group-click-action-label")
|
||||
description: I18n.tr("panels.dock.appearance-group-click-action-description")
|
||||
model: [
|
||||
{
|
||||
"key": "cycle",
|
||||
"name": I18n.tr("panels.dock.appearance-group-click-action-cycle")
|
||||
},
|
||||
{
|
||||
"key": "list",
|
||||
"name": I18n.tr("panels.dock.appearance-group-click-action-list")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.dock.groupClickAction
|
||||
defaultValue: Settings.getDefaultValue("dock.groupClickAction")
|
||||
onSelected: key => Settings.data.dock.groupClickAction = key
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.dock.groupApps
|
||||
label: I18n.tr("panels.dock.appearance-group-context-menu-mode-label")
|
||||
description: I18n.tr("panels.dock.appearance-group-context-menu-mode-description")
|
||||
model: [
|
||||
{
|
||||
"key": "list",
|
||||
"name": I18n.tr("panels.dock.appearance-group-context-menu-mode-list")
|
||||
},
|
||||
{
|
||||
"key": "extended",
|
||||
"name": I18n.tr("panels.dock.appearance-group-context-menu-mode-extended")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.dock.groupContextMenuMode
|
||||
defaultValue: Settings.getDefaultValue("dock.groupContextMenuMode")
|
||||
onSelected: key => Settings.data.dock.groupContextMenuMode = key
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.dock.groupApps
|
||||
label: I18n.tr("panels.dock.appearance-group-indicator-style-label")
|
||||
description: I18n.tr("panels.dock.appearance-group-indicator-style-description")
|
||||
model: [
|
||||
{
|
||||
"key": "number",
|
||||
"name": I18n.tr("panels.dock.appearance-group-indicator-style-number")
|
||||
},
|
||||
{
|
||||
"key": "dots",
|
||||
"name": I18n.tr("panels.dock.appearance-group-indicator-style-dots")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.dock.groupIndicatorStyle
|
||||
defaultValue: Settings.getDefaultValue("dock.groupIndicatorStyle")
|
||||
onSelected: key => Settings.data.dock.groupIndicatorStyle = key
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.dock.monitors-only-same-monitor-label")
|
||||
description: I18n.tr("panels.dock.monitors-only-same-monitor-description")
|
||||
checked: Settings.data.dock.onlySameOutput
|
||||
defaultValue: Settings.getDefaultValue("dock.onlySameOutput")
|
||||
onToggled: checked => Settings.data.dock.onlySameOutput = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.dock.appearance-colorize-icons-label")
|
||||
description: I18n.tr("panels.dock.appearance-colorize-icons-description")
|
||||
checked: Settings.data.dock.colorizeIcons
|
||||
defaultValue: Settings.getDefaultValue("dock.colorizeIcons")
|
||||
onToggled: checked => Settings.data.dock.colorizeIcons = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.dock.appearance-show-launcher-icon-label")
|
||||
description: I18n.tr("panels.dock.appearance-show-launcher-icon-description")
|
||||
checked: Settings.data.dock.showLauncherIcon
|
||||
defaultValue: Settings.getDefaultValue("dock.showLauncherIcon")
|
||||
onToggled: checked => Settings.data.dock.showLauncherIcon = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.dock.showLauncherIcon
|
||||
label: I18n.tr("panels.dock.appearance-launcher-position-label")
|
||||
description: I18n.tr("panels.dock.appearance-launcher-position-description")
|
||||
model: [
|
||||
{
|
||||
"key": "start",
|
||||
"name": I18n.tr("panels.dock.appearance-launcher-position-start")
|
||||
},
|
||||
{
|
||||
"key": "end",
|
||||
"name": I18n.tr("panels.dock.appearance-launcher-position-end")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.dock.launcherPosition
|
||||
defaultValue: Settings.getDefaultValue("dock.launcherPosition")
|
||||
onSelected: key => Settings.data.dock.launcherPosition = key
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.dock.showLauncherIcon
|
||||
label: I18n.tr("panels.dock.appearance-launcher-use-distro-logo-label")
|
||||
description: I18n.tr("panels.dock.appearance-launcher-use-distro-logo-description")
|
||||
checked: Settings.data.dock.launcherUseDistroLogo
|
||||
defaultValue: Settings.getDefaultValue("dock.launcherUseDistroLogo")
|
||||
onToggled: checked => Settings.data.dock.launcherUseDistroLogo = checked
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: Settings.data.dock.showLauncherIcon
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.dock.appearance-launcher-icon-label")
|
||||
description: I18n.tr("panels.dock.appearance-launcher-icon-description")
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
visible: !Settings.data.dock.launcherUseDistroLogo
|
||||
enabled: !Settings.data.dock.launcherUseDistroLogo
|
||||
icon: (Settings.data.dock.launcherIcon && Settings.data.dock.launcherIcon !== "") ? Settings.data.dock.launcherIcon : "search"
|
||||
colorFg: root.launcherPreviewColor
|
||||
colorFgHover: root.launcherPreviewColor
|
||||
tooltipText: I18n.tr("bar.control-center.browse-library")
|
||||
onClicked: launcherIconPicker.open()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: Settings.data.dock.launcherUseDistroLogo
|
||||
width: Style.toOdd(Style.baseWidgetSize * Style.uiScaleRatio)
|
||||
height: width
|
||||
radius: Math.min(Style.iRadiusL, width / 2)
|
||||
color: Color.smartAlpha(Color.mSurfaceVariant)
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
|
||||
Image {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width * 0.62
|
||||
height: width
|
||||
source: HostService.osLogo
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
layer.enabled: visible
|
||||
layer.effect: ShaderEffect {
|
||||
property color targetColor: root.launcherPreviewColor
|
||||
property real colorizeMode: 2.0
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: launcherIconPicker
|
||||
initialIcon: (Settings.data.dock.launcherIcon && Settings.data.dock.launcherIcon !== "") ? Settings.data.dock.launcherIcon : "search"
|
||||
onIconSelected: iconName => {
|
||||
Settings.data.dock.launcherIcon = iconName;
|
||||
Settings.saveImmediate();
|
||||
}
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.dock.showLauncherIcon
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: Settings.data.dock.launcherIconColor
|
||||
defaultValue: Settings.getDefaultValue("dock.launcherIconColor")
|
||||
onSelected: key => Settings.data.dock.launcherIconColor = key
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
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
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.appearance")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.monitors")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
AppearanceSubTab {}
|
||||
MonitorsSubTab {}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
enabled: Settings.data.dock.enabled
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.dock.monitors-desc")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Quickshell.screens || []
|
||||
delegate: NCheckbox {
|
||||
Layout.fillWidth: true
|
||||
readonly property real compositorScale: {
|
||||
const info = CompositorService.displayScales[modelData.name];
|
||||
return (info && info.scale) ? info.scale : 1.0;
|
||||
}
|
||||
label: modelData.name || "Unknown"
|
||||
description: {
|
||||
I18n.tr("system.monitor-description", {
|
||||
"model": modelData.model,
|
||||
"width": modelData.width * compositorScale,
|
||||
"height": modelData.height * compositorScale,
|
||||
"scale": compositorScale
|
||||
});
|
||||
}
|
||||
checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
Settings.data.dock.monitors = root.addMonitor(Settings.data.dock.monitors, modelData.name);
|
||||
} else {
|
||||
Settings.data.dock.monitors = root.removeMonitor(Settings.data.dock.monitors, modelData.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "../../../../../Helpers/QtObj2JS.js" as QtObj2JS
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
// Profile section
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginL
|
||||
|
||||
// Avatar preview
|
||||
NImageRounded {
|
||||
Layout.preferredWidth: 128 * Style.uiScaleRatio
|
||||
Layout.preferredHeight: width
|
||||
radius: width / 2
|
||||
imagePath: Settings.preprocessPath(Settings.data.general.avatarImage)
|
||||
fallbackIcon: "person"
|
||||
borderColor: Color.mPrimary
|
||||
borderWidth: Style.borderM
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
NText {
|
||||
text: HostService.displayName
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NTextInputButton {
|
||||
label: I18n.tr("panels.general.profile-picture-label")
|
||||
description: I18n.tr("panels.general.profile-picture-description")
|
||||
text: Settings.data.general.avatarImage
|
||||
placeholderText: '~/.face' // don't translate path
|
||||
buttonIcon: "photo"
|
||||
buttonTooltip: I18n.tr("panels.general.profile-tooltip")
|
||||
onInputTextChanged: text => Settings.data.general.avatarImage = text
|
||||
onButtonClicked: {
|
||||
avatarPicker.openFilePicker();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: avatarPicker
|
||||
title: I18n.tr("panels.general.profile-select-avatar")
|
||||
selectionMode: "files"
|
||||
initialPath: Settings.preprocessPath(Settings.data.general.avatarImage).substr(0, Settings.preprocessPath(Settings.data.general.avatarImage).lastIndexOf("/")) || Quickshell.env("HOME")
|
||||
nameFilters: ImageCacheService.basicImageFilters
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
Settings.data.general.avatarImage = paths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginM
|
||||
Layout.bottomMargin: Style.marginM
|
||||
}
|
||||
|
||||
// Fonts
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Font configuration section
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NSearchableComboBox {
|
||||
label: I18n.tr("panels.general.fonts-default-label")
|
||||
description: I18n.tr("panels.general.fonts-default-description")
|
||||
model: FontService.availableFonts
|
||||
currentKey: Settings.data.ui.fontDefault
|
||||
placeholder: I18n.tr("panels.general.fonts-default-placeholder")
|
||||
searchPlaceholder: I18n.tr("panels.general.fonts-default-search-placeholder")
|
||||
popupHeight: 420
|
||||
defaultValue: Settings.getDefaultValue("ui.fontDefault")
|
||||
settingsPath: "ui.fontDefault"
|
||||
onSelected: key => Settings.data.ui.fontDefault = key
|
||||
}
|
||||
|
||||
NSearchableComboBox {
|
||||
label: I18n.tr("panels.general.fonts-monospace-label")
|
||||
description: I18n.tr("panels.general.fonts-monospace-description")
|
||||
model: FontService.monospaceFonts
|
||||
currentKey: Settings.data.ui.fontFixed
|
||||
placeholder: I18n.tr("panels.general.fonts-monospace-placeholder")
|
||||
searchPlaceholder: I18n.tr("panels.general.fonts-monospace-search-placeholder")
|
||||
popupHeight: 320
|
||||
defaultValue: Settings.getDefaultValue("ui.fontFixed")
|
||||
settingsPath: "ui.fontFixed"
|
||||
onSelected: key => Settings.data.ui.fontFixed = key
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.fonts-default-scale-label")
|
||||
description: I18n.tr("panels.general.fonts-default-scale-description")
|
||||
from: 0.75
|
||||
to: 1.25
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.ui.fontDefaultScale
|
||||
defaultValue: Settings.getDefaultValue("ui.fontDefaultScale")
|
||||
onMoved: value => Settings.data.ui.fontDefaultScale = value
|
||||
text: Math.floor(Settings.data.ui.fontDefaultScale * 100) + "%"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.fonts-monospace-scale-label")
|
||||
description: I18n.tr("panels.general.fonts-monospace-scale-description")
|
||||
from: 0.75
|
||||
to: 1.25
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.ui.fontFixedScale
|
||||
defaultValue: Settings.getDefaultValue("ui.fontFixedScale")
|
||||
onMoved: value => Settings.data.ui.fontFixedScale = value
|
||||
text: Math.floor(Settings.data.ui.fontFixedScale * 100) + "%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginM
|
||||
Layout.bottomMargin: Style.marginM
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.reverse-scrolling-label")
|
||||
description: I18n.tr("panels.general.reverse-scrolling-description")
|
||||
checked: Settings.data.general.reverseScroll
|
||||
defaultValue: Settings.getDefaultValue("general.reverseScroll")
|
||||
onToggled: checked => Settings.data.general.reverseScroll = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.smooth-scrolling-label")
|
||||
description: I18n.tr("panels.general.smooth-scrolling-description")
|
||||
checked: Settings.data.general.smoothScrollEnabled
|
||||
defaultValue: Settings.getDefaultValue("general.smoothScrollEnabled")
|
||||
onToggled: checked => Settings.data.general.smoothScrollEnabled = checked
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginM
|
||||
Layout.bottomMargin: Style.marginM
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NButton {
|
||||
icon: "wand"
|
||||
text: I18n.tr("panels.general.launch-setup-wizard")
|
||||
outlined: true
|
||||
Layout.fillWidth: 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.telemetryOnlyMode = false;
|
||||
setupPanel.open();
|
||||
} else {
|
||||
Qt.callLater(() => {
|
||||
var sp = PanelService.getPanel("setupWizardPanel", targetScreen);
|
||||
if (sp) {
|
||||
sp.telemetryOnlyMode = false;
|
||||
sp.open();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
icon: "external-link"
|
||||
text: I18n.tr("common.documentation")
|
||||
outlined: true
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
Qt.openUrlExternally("https://docs.noctalia.dev");
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
icon: "json"
|
||||
text: I18n.tr("panels.general.copy-settings")
|
||||
outlined: true
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
var plainData = QtObj2JS.qtObjectToPlainObject(Settings.data);
|
||||
var json = JSON.stringify(plainData, null, 2);
|
||||
Quickshell.execDetached(["wl-copy", json]);
|
||||
ToastService.showNotice(I18n.tr("panels.general.settings-copied"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
width: parent.width
|
||||
|
||||
// Keybinds section
|
||||
NLabel {
|
||||
label: I18n.tr("panels.general.keybinds-title")
|
||||
description: I18n.tr("panels.general.keybinds-description")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NKeybindRecorder {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.keybinds-up")
|
||||
currentKeybinds: Settings.data.general.keybinds.keyUp
|
||||
defaultKeybind: "Up"
|
||||
settingsPath: "general.keybinds.keyUp"
|
||||
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyUp = newKeybinds
|
||||
}
|
||||
|
||||
NKeybindRecorder {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.keybinds-down")
|
||||
currentKeybinds: Settings.data.general.keybinds.keyDown
|
||||
defaultKeybind: "Down"
|
||||
settingsPath: "general.keybinds.keyDown"
|
||||
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyDown = newKeybinds
|
||||
}
|
||||
|
||||
NKeybindRecorder {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.keybinds-left")
|
||||
currentKeybinds: Settings.data.general.keybinds.keyLeft
|
||||
defaultKeybind: "Left"
|
||||
settingsPath: "general.keybinds.keyLeft"
|
||||
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyLeft = newKeybinds
|
||||
}
|
||||
|
||||
NKeybindRecorder {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.keybinds-right")
|
||||
currentKeybinds: Settings.data.general.keybinds.keyRight
|
||||
defaultKeybind: "Right"
|
||||
settingsPath: "general.keybinds.keyRight"
|
||||
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyRight = newKeybinds
|
||||
}
|
||||
|
||||
NKeybindRecorder {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.keybinds-enter")
|
||||
currentKeybinds: Settings.data.general.keybinds.keyEnter
|
||||
defaultKeybind: "Return"
|
||||
settingsPath: "general.keybinds.keyEnter"
|
||||
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyEnter = newKeybinds
|
||||
}
|
||||
|
||||
NKeybindRecorder {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.keybinds-escape")
|
||||
currentKeybinds: Settings.data.general.keybinds.keyEscape
|
||||
defaultKeybind: "Esc"
|
||||
settingsPath: "general.keybinds.keyEscape"
|
||||
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyEscape = newKeybinds
|
||||
}
|
||||
|
||||
NKeybindRecorder {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.keybinds-remove")
|
||||
currentKeybinds: Settings.data.general.keybinds.keyRemove
|
||||
defaultKeybind: "Del"
|
||||
settingsPath: "general.keybinds.keyRemove"
|
||||
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyRemove = newKeybinds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "../../../../Helpers/QtObj2JS.js" as QtObj2JS
|
||||
import "General"
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("panels.general.tab-basics")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("panels.general.tab-keybinds")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
BasicsSubTab {}
|
||||
KeybindsSubTab {}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
width: parent.width
|
||||
|
||||
// Enable/Disable Toggle
|
||||
NToggle {
|
||||
label: I18n.tr("panels.hooks.system-hooks-enable-label")
|
||||
description: I18n.tr("panels.hooks.system-hooks-enable-description")
|
||||
checked: Settings.data.hooks.enabled
|
||||
onToggled: checked => Settings.data.hooks.enabled = checked
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Info section
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.hooks.info-parameters-label")
|
||||
description: I18n.tr("panels.hooks.info-parameters-description")
|
||||
}
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
dim: true
|
||||
anchors.centerIn: parent
|
||||
|
||||
// Use a reasonable width/height relative to parent or fixed
|
||||
width: Math.min(600 * Style.uiScaleRatio, parent.width * 0.9)
|
||||
height: Math.min(contentLayout.implicitHeight + padding * 2, parent.height * 0.9)
|
||||
padding: Style.marginL
|
||||
|
||||
property string hookLabel: ""
|
||||
property string hookDescription: ""
|
||||
property string hookPlaceholder: ""
|
||||
property string initialValue: ""
|
||||
|
||||
signal saved(string newValue)
|
||||
signal test(string value)
|
||||
|
||||
property var _savedSlot: null
|
||||
property var _testSlot: null
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
commandInput.text = initialValue;
|
||||
commandInput.forceActiveFocus();
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: contentLayout
|
||||
spacing: Style.marginL
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
NText {
|
||||
text: root.hookLabel
|
||||
font.weight: Style.fontWeightBold
|
||||
pointSize: Style.fontSizeL
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Description/Help
|
||||
NText {
|
||||
text: root.hookDescription
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Input Area
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NTextInput {
|
||||
id: commandInput
|
||||
Layout.fillWidth: true
|
||||
placeholderText: root.hookPlaceholder
|
||||
// Allow multiline? Hooks are usually oneline commands but can be scripts.
|
||||
// NTextInput is likely single line. Let's assume single line for now as per previous implementation.
|
||||
}
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.test")
|
||||
icon: "caret-right"
|
||||
onClicked: root.test(commandInput.text)
|
||||
// Disable test if empty? Or maybe allow testing defined script.
|
||||
enabled: commandInput.text !== ""
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
} // Spacer
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.cancel")
|
||||
outlined: true
|
||||
onClicked: root.close()
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.save")
|
||||
icon: "check"
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
onClicked: {
|
||||
root.saved(commandInput.text);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string description: ""
|
||||
property string value: ""
|
||||
|
||||
signal editClicked
|
||||
|
||||
spacing: Style.marginM
|
||||
|
||||
NLabel {
|
||||
label: root.label
|
||||
description: root.description
|
||||
labelColor: root.value ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "settings"
|
||||
onClicked: root.editClicked()
|
||||
tooltipText: I18n.tr("common.edit")
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Control
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
enabled: Settings.data.hooks.enabled
|
||||
spacing: Style.marginL
|
||||
width: parent.width
|
||||
|
||||
// Shared Edit Popup
|
||||
HookEditPopup {
|
||||
id: editPopup
|
||||
parent: Overlay.overlay
|
||||
}
|
||||
|
||||
// Helper to open popup
|
||||
function openEdit(label, description, placeholder, value, onSave, onTest) {
|
||||
editPopup.hookLabel = label;
|
||||
editPopup.hookDescription = description;
|
||||
editPopup.hookPlaceholder = placeholder;
|
||||
editPopup.initialValue = value;
|
||||
|
||||
// Disconnect previous signals
|
||||
try {
|
||||
editPopup.saved.disconnect(editPopup._savedSlot);
|
||||
} catch (e) {}
|
||||
try {
|
||||
editPopup.test.disconnect(editPopup._testSlot);
|
||||
} catch (e) {}
|
||||
|
||||
// Define slots
|
||||
editPopup._savedSlot = onSave;
|
||||
editPopup._testSlot = onTest;
|
||||
|
||||
// Connect new signals
|
||||
editPopup.saved.connect(editPopup._savedSlot);
|
||||
editPopup.test.connect(editPopup._testSlot);
|
||||
|
||||
editPopup.open();
|
||||
}
|
||||
|
||||
// Startup Hook
|
||||
HookRow {
|
||||
label: I18n.tr("panels.hooks.noctalia-started-label")
|
||||
description: I18n.tr("panels.hooks.noctalia-started-description")
|
||||
value: Settings.data.hooks.startup
|
||||
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.noctalia-started-placeholder"), value, newValue => {
|
||||
Settings.data.hooks.startup = newValue;
|
||||
Settings.saveImmediate();
|
||||
}, val => {
|
||||
HooksService.executeStartupHook();
|
||||
})
|
||||
}
|
||||
|
||||
// Wallpaper Hook
|
||||
HookRow {
|
||||
label: I18n.tr("panels.hooks.wallpaper-changed-label")
|
||||
description: I18n.tr("panels.hooks.wallpaper-changed-description")
|
||||
value: Settings.data.hooks.wallpaperChange
|
||||
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.wallpaper-changed-placeholder"), value, newValue => {
|
||||
Settings.data.hooks.wallpaperChange = newValue;
|
||||
Settings.saveImmediate();
|
||||
}, val => {
|
||||
if (val)
|
||||
Quickshell.execDetached(["sh", "-lc", val.replace("$1", "test_wallpaper_path").replace("$2", "test_screen").replace("$3", "dark")]);
|
||||
})
|
||||
}
|
||||
|
||||
// Color Generation Hook
|
||||
HookRow {
|
||||
label: I18n.tr("panels.hooks.color-generation-label")
|
||||
description: I18n.tr("panels.hooks.color-generation-description")
|
||||
value: Settings.data.hooks.colorGeneration
|
||||
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.color-generation-placeholder"), value, newValue => {
|
||||
Settings.data.hooks.colorGeneration = newValue;
|
||||
Settings.saveImmediate();
|
||||
}, val => {
|
||||
if (val)
|
||||
Quickshell.execDetached(["sh", "-lc", val.replace("$1", "dark")]);
|
||||
})
|
||||
}
|
||||
|
||||
// Theme Hook
|
||||
HookRow {
|
||||
label: I18n.tr("panels.hooks.theme-changed-label")
|
||||
description: I18n.tr("panels.hooks.theme-changed-description")
|
||||
value: Settings.data.hooks.darkModeChange
|
||||
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.theme-changed-placeholder"), value, newValue => {
|
||||
Settings.data.hooks.darkModeChange = newValue;
|
||||
Settings.saveImmediate();
|
||||
}, val => {
|
||||
if (val)
|
||||
Quickshell.execDetached(["sh", "-lc", val.replace("$1", "true")]);
|
||||
})
|
||||
}
|
||||
|
||||
// Screen Lock Hook
|
||||
HookRow {
|
||||
label: I18n.tr("panels.hooks.screen-lock-label")
|
||||
description: I18n.tr("panels.hooks.screen-lock-description")
|
||||
value: Settings.data.hooks.screenLock
|
||||
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.screen-lock-placeholder"), value, newValue => {
|
||||
Settings.data.hooks.screenLock = newValue;
|
||||
Settings.saveImmediate();
|
||||
}, val => {
|
||||
if (val)
|
||||
Quickshell.execDetached(["sh", "-lc", val]);
|
||||
})
|
||||
}
|
||||
|
||||
// Screen Unlock Hook
|
||||
HookRow {
|
||||
label: I18n.tr("panels.hooks.screen-unlock-label")
|
||||
description: I18n.tr("panels.hooks.screen-unlock-description")
|
||||
value: Settings.data.hooks.screenUnlock
|
||||
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.screen-unlock-placeholder"), value, newValue => {
|
||||
Settings.data.hooks.screenUnlock = newValue;
|
||||
Settings.saveImmediate();
|
||||
}, val => {
|
||||
if (val)
|
||||
Quickshell.execDetached(["sh", "-lc", val]);
|
||||
})
|
||||
}
|
||||
|
||||
// Performance Mode Enabled Hook
|
||||
HookRow {
|
||||
label: I18n.tr("panels.hooks.performance-mode-enabled-label")
|
||||
description: I18n.tr("panels.hooks.performance-mode-enabled-description")
|
||||
value: Settings.data.hooks.performanceModeEnabled
|
||||
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.performance-mode-enabled-placeholder"), value, newValue => {
|
||||
Settings.data.hooks.performanceModeEnabled = newValue;
|
||||
Settings.saveImmediate();
|
||||
}, val => {
|
||||
if (val)
|
||||
Quickshell.execDetached(["sh", "-lc", val]);
|
||||
})
|
||||
}
|
||||
|
||||
// Performance Mode Disabled Hook
|
||||
HookRow {
|
||||
label: I18n.tr("panels.hooks.performance-mode-disabled-label")
|
||||
description: I18n.tr("panels.hooks.performance-mode-disabled-description")
|
||||
value: Settings.data.hooks.performanceModeDisabled
|
||||
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.performance-mode-disabled-placeholder"), value, newValue => {
|
||||
Settings.data.hooks.performanceModeDisabled = newValue;
|
||||
Settings.saveImmediate();
|
||||
}, val => {
|
||||
if (val)
|
||||
Quickshell.execDetached(["sh", "-lc", val]);
|
||||
})
|
||||
}
|
||||
|
||||
// Session Hook
|
||||
HookRow {
|
||||
label: I18n.tr("panels.hooks.session-label")
|
||||
description: I18n.tr("panels.hooks.session-description")
|
||||
value: Settings.data.hooks.session
|
||||
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.session-placeholder"), value, newValue => {
|
||||
Settings.data.hooks.session = newValue;
|
||||
Settings.saveImmediate();
|
||||
}, val => {
|
||||
if (val)
|
||||
Quickshell.execDetached(["sh", "-lc", val + " test"]);
|
||||
})
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.general")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("panels.hooks.title")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginS
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
GeneralSubTab {}
|
||||
HooksListSubTab {}
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Power
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Master enable
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.idle.enable-label")
|
||||
description: I18n.tr("panels.idle.enable-description")
|
||||
checked: Settings.data.idle.enabled
|
||||
defaultValue: Settings.getDefaultValue("idle.enabled")
|
||||
onToggled: checked => Settings.data.idle.enabled = checked
|
||||
}
|
||||
|
||||
// Live idle status
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
enabled: Settings.data.idle.enabled
|
||||
visible: IdleService.nativeIdleMonitorAvailable
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.idle.status-label")
|
||||
description: I18n.tr("panels.idle.status-description")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
|
||||
text: IdleService.idleSeconds > 0 ? I18n.trp("common.second", IdleService.idleSeconds) : I18n.tr("common.active")
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: Style.fontSizeM
|
||||
color: IdleService.idleSeconds > 0 ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
NLabel {
|
||||
visible: !IdleService.nativeIdleMonitorAvailable
|
||||
description: I18n.tr("panels.idle.unavailable")
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
IdleCommandEditPopup {
|
||||
id: editPopup
|
||||
parent: Overlay.overlay
|
||||
}
|
||||
|
||||
function openEdit(actionName, cmdVal, resumeCmdVal, onSaveCmd, onSaveResume) {
|
||||
editPopup.editIndex = -1;
|
||||
editPopup.showCommand = true;
|
||||
editPopup.showTimeout = false;
|
||||
editPopup.titleText = I18n.tr("common.edit") + " " + actionName;
|
||||
editPopup.timeoutValue = 0;
|
||||
editPopup.commandValue = cmdVal;
|
||||
editPopup.resumeCommandValue = resumeCmdVal;
|
||||
|
||||
try {
|
||||
editPopup.saved.disconnect(editPopup._savedSlot);
|
||||
} catch (e) {}
|
||||
|
||||
editPopup._savedSlot = function (timeout, cmd, resumeCmd, name) {
|
||||
onSaveCmd(cmd);
|
||||
onSaveResume(resumeCmd);
|
||||
};
|
||||
|
||||
editPopup.saved.connect(editPopup._savedSlot);
|
||||
editPopup.open();
|
||||
}
|
||||
|
||||
// Timeout spinboxes and resume commands
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginL
|
||||
enabled: Settings.data.idle.enabled
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.idle.timeouts-label")
|
||||
description: I18n.tr("panels.idle.timeouts-description")
|
||||
}
|
||||
|
||||
DefaultActionRow {
|
||||
actionName: I18n.tr("panels.idle.screen-off-label")
|
||||
actionDescription: I18n.tr("panels.idle.screen-off-description")
|
||||
timeoutValue: Settings.data.idle.screenOffTimeout
|
||||
defaultValue: Settings.getDefaultValue("idle.screenOffTimeout")
|
||||
command: Settings.data.idle.screenOffCommand
|
||||
resumeCommand: Settings.data.idle.resumeScreenOffCommand
|
||||
onActionTimeoutChanged: val => Settings.data.idle.screenOffTimeout = val
|
||||
onActionCommandChanged: cmd => {
|
||||
Settings.data.idle.screenOffCommand = cmd;
|
||||
Settings.saveImmediate();
|
||||
}
|
||||
onActionResumeCommandChanged: cmd => {
|
||||
Settings.data.idle.resumeScreenOffCommand = cmd;
|
||||
Settings.saveImmediate();
|
||||
}
|
||||
}
|
||||
|
||||
DefaultActionRow {
|
||||
actionName: I18n.tr("panels.idle.lock-label")
|
||||
actionDescription: I18n.tr("panels.idle.lock-description")
|
||||
timeoutValue: Settings.data.idle.lockTimeout
|
||||
defaultValue: Settings.getDefaultValue("idle.lockTimeout")
|
||||
command: Settings.data.idle.lockCommand
|
||||
resumeCommand: Settings.data.idle.resumeLockCommand
|
||||
onActionTimeoutChanged: val => Settings.data.idle.lockTimeout = val
|
||||
onActionCommandChanged: cmd => {
|
||||
Settings.data.idle.lockCommand = cmd;
|
||||
Settings.saveImmediate();
|
||||
}
|
||||
onActionResumeCommandChanged: cmd => {
|
||||
Settings.data.idle.resumeLockCommand = cmd;
|
||||
Settings.saveImmediate();
|
||||
}
|
||||
}
|
||||
|
||||
DefaultActionRow {
|
||||
actionName: I18n.tr("common.suspend")
|
||||
actionDescription: I18n.tr("panels.idle.suspend-description")
|
||||
timeoutValue: Settings.data.idle.suspendTimeout
|
||||
defaultValue: Settings.getDefaultValue("idle.suspendTimeout")
|
||||
command: Settings.data.idle.suspendCommand
|
||||
resumeCommand: Settings.data.idle.resumeSuspendCommand
|
||||
onActionTimeoutChanged: val => Settings.data.idle.suspendTimeout = val
|
||||
onActionCommandChanged: cmd => {
|
||||
Settings.data.idle.suspendCommand = cmd;
|
||||
Settings.saveImmediate();
|
||||
}
|
||||
onActionResumeCommandChanged: cmd => {
|
||||
Settings.data.idle.resumeSuspendCommand = cmd;
|
||||
Settings.saveImmediate();
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
label: I18n.tr("panels.idle.fade-duration-label")
|
||||
description: I18n.tr("panels.idle.fade-duration-description")
|
||||
from: 1
|
||||
to: 60
|
||||
suffix: "s"
|
||||
value: Settings.data.idle.fadeDuration
|
||||
defaultValue: Settings.getDefaultValue("idle.fadeDuration")
|
||||
onValueChanged: Settings.data.idle.fadeDuration = value
|
||||
}
|
||||
}
|
||||
|
||||
component DefaultActionRow: RowLayout {
|
||||
id: rowRoot
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
property string actionName
|
||||
property string actionDescription
|
||||
property alias timeoutValue: spinBox.value
|
||||
property int defaultValue
|
||||
property string command
|
||||
property string resumeCommand
|
||||
|
||||
signal actionTimeoutChanged(int newValue)
|
||||
signal actionCommandChanged(string newCmd)
|
||||
signal actionResumeCommandChanged(string newCmd)
|
||||
|
||||
NSpinBox {
|
||||
id: spinBox
|
||||
Layout.fillWidth: true
|
||||
label: rowRoot.actionName
|
||||
description: rowRoot.actionDescription
|
||||
from: 0
|
||||
to: 86400
|
||||
suffix: "s"
|
||||
defaultValue: rowRoot.defaultValue
|
||||
onValueChanged: rowRoot.actionTimeoutChanged(value)
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: "settings"
|
||||
tooltipText: I18n.tr("common.edit")
|
||||
onClicked: root.openEdit(rowRoot.actionName, rowRoot.command, rowRoot.resumeCommand, rowRoot.actionCommandChanged, rowRoot.actionResumeCommandChanged)
|
||||
}
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Power
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
enabled: Settings.data.idle.enabled
|
||||
|
||||
property bool _saving: false
|
||||
|
||||
ListModel {
|
||||
id: entriesModel
|
||||
}
|
||||
|
||||
function _loadToModel() {
|
||||
if (_saving)
|
||||
return;
|
||||
entriesModel.clear();
|
||||
var entries = [];
|
||||
try {
|
||||
entries = JSON.parse(Settings.data.idle.customCommands);
|
||||
} catch (e) {
|
||||
Logger.w("CustomSubTab", "Failed to parse customCommands:", e);
|
||||
}
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
entriesModel.append({
|
||||
"name": String(entries[i].name || ""),
|
||||
"timeout": parseInt(entries[i].timeout) || 60,
|
||||
"command": String(entries[i].command || ""),
|
||||
"resumeCommand": String(entries[i].resumeCommand || "")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _saveFromModel() {
|
||||
_saving = true;
|
||||
var arr = [];
|
||||
for (var i = 0; i < entriesModel.count; i++) {
|
||||
var item = entriesModel.get(i);
|
||||
arr.push({
|
||||
"name": item.name,
|
||||
"timeout": item.timeout,
|
||||
"command": item.command,
|
||||
"resumeCommand": item.resumeCommand
|
||||
});
|
||||
}
|
||||
Settings.data.idle.customCommands = JSON.stringify(arr);
|
||||
_saving = false;
|
||||
}
|
||||
|
||||
function _removeEntry(index) {
|
||||
entriesModel.remove(index, 1);
|
||||
_saveFromModel();
|
||||
}
|
||||
|
||||
Component.onCompleted: Qt.callLater(_loadToModel)
|
||||
|
||||
Connections {
|
||||
target: Settings.data.idle
|
||||
function onCustomCommandsChanged() {
|
||||
root._loadToModel();
|
||||
}
|
||||
}
|
||||
|
||||
// Shared Edit Popup
|
||||
IdleCommandEditPopup {
|
||||
id: editPopup
|
||||
parent: Overlay.overlay
|
||||
}
|
||||
|
||||
function openEdit(index, nameVal, timeoutVal, cmdVal, resumeCmdVal) {
|
||||
editPopup.editIndex = index;
|
||||
editPopup.nameValue = nameVal;
|
||||
editPopup.timeoutValue = timeoutVal;
|
||||
editPopup.commandValue = cmdVal;
|
||||
editPopup.resumeCommandValue = resumeCmdVal;
|
||||
editPopup.showName = true;
|
||||
|
||||
try {
|
||||
editPopup.saved.disconnect(editPopup._savedSlot);
|
||||
} catch (e) {}
|
||||
|
||||
editPopup._savedSlot = function (timeout, cmd, resumeCmd, name) {
|
||||
if (index >= 0 && index < entriesModel.count) {
|
||||
entriesModel.setProperty(index, "name", name);
|
||||
entriesModel.setProperty(index, "timeout", timeout);
|
||||
entriesModel.setProperty(index, "command", cmd);
|
||||
entriesModel.setProperty(index, "resumeCommand", resumeCmd);
|
||||
} else {
|
||||
entriesModel.append({
|
||||
"name": name,
|
||||
"timeout": timeout,
|
||||
"command": cmd,
|
||||
"resumeCommand": resumeCmd
|
||||
});
|
||||
}
|
||||
root._saveFromModel();
|
||||
};
|
||||
|
||||
editPopup.saved.connect(editPopup._savedSlot);
|
||||
editPopup.open();
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.idle.custom-label")
|
||||
description: I18n.tr("panels.idle.custom-description")
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginS
|
||||
Layout.bottomMargin: Style.marginS
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: entriesModel
|
||||
|
||||
delegate: RowLayout {
|
||||
id: entryDelegate
|
||||
required property int index
|
||||
required property string name
|
||||
required property int timeout
|
||||
required property string command
|
||||
required property string resumeCommand
|
||||
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
Layout.fillWidth: true
|
||||
label: entryDelegate.name || I18n.tr("panels.idle.custom-entry-unnamed")
|
||||
description: I18n.trp("common.second", entryDelegate.timeout)
|
||||
labelColor: (entryDelegate.command || entryDelegate.resumeCommand) ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: I18n.tr("common.edit")
|
||||
onClicked: root.openEdit(entryDelegate.index, entryDelegate.name, entryDelegate.timeout, entryDelegate.command, entryDelegate.resumeCommand)
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "trash"
|
||||
tooltipText: I18n.tr("panels.idle.custom-entry-delete")
|
||||
onClicked: root._removeEntry(entryDelegate.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("panels.idle.custom-add")
|
||||
icon: "add"
|
||||
enabled: Settings.data.idle.enabled
|
||||
onClicked: {
|
||||
root.openEdit(-1, "", 60, "", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
dim: true
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: Math.min(600 * Style.uiScaleRatio, parent.width * 0.9)
|
||||
height: Math.min(contentLayout.implicitHeight + padding * 2, parent.height * 0.9)
|
||||
padding: Style.marginL
|
||||
|
||||
property int editIndex: -1
|
||||
property int timeoutValue: 60
|
||||
property string commandValue: ""
|
||||
property string resumeCommandValue: ""
|
||||
property string nameValue: ""
|
||||
property bool showCommand: true
|
||||
property bool showTimeout: true
|
||||
property bool showName: false
|
||||
property string titleText: root.editIndex >= 0 ? I18n.tr("panels.idle.custom-entry-edit") : I18n.tr("panels.idle.custom-entry-new")
|
||||
|
||||
signal saved(int timeout, string command, string resumeCommand, string name)
|
||||
|
||||
property var _savedSlot: null
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
nameInput.text = nameValue;
|
||||
timeoutSpinBox.value = timeoutValue;
|
||||
commandInput.text = commandValue;
|
||||
resumeCommandInput.text = resumeCommandValue;
|
||||
if (showName) {
|
||||
nameInput.forceActiveFocus();
|
||||
} else {
|
||||
timeoutSpinBox.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: contentLayout
|
||||
spacing: Style.marginL
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
NText {
|
||||
text: root.titleText
|
||||
font.weight: Style.fontWeightBold
|
||||
pointSize: Style.fontSizeL
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Input Area
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NTextInput {
|
||||
id: nameInput
|
||||
visible: root.showName
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.idle.custom-entry-name")
|
||||
placeholderText: I18n.tr("panels.idle.custom-entry-name-placeholder")
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
id: timeoutSpinBox
|
||||
visible: root.showTimeout
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.idle.custom-entry-timeout")
|
||||
from: 0
|
||||
to: 86400
|
||||
suffix: "s"
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: commandInput
|
||||
visible: root.showCommand
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.idle.custom-entry-command")
|
||||
placeholderText: "notify-send \"Idle\""
|
||||
fontFamily: Settings.data.ui.fontFixed
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: resumeCommandInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.idle.resume-command-label")
|
||||
placeholderText: "notify-send \"Welcome back!\""
|
||||
fontFamily: Settings.data.ui.fontFixed
|
||||
}
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
} // Spacer
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.cancel")
|
||||
outlined: true
|
||||
onClicked: root.close()
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.save")
|
||||
icon: "check"
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
onClicked: {
|
||||
root.saved(timeoutSpinBox.value, commandInput.text, resumeCommandInput.text, nameInput.text);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
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
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("panels.idle.tab-behavior")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("panels.idle.tab-custom")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
BehaviorSubTab {}
|
||||
CustomSubTab {}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-clipboard-history-label")
|
||||
description: I18n.tr("panels.launcher.settings-clipboard-history-description")
|
||||
checked: Settings.data.appLauncher.enableClipboardHistory
|
||||
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardHistory")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-clip-preview-label")
|
||||
description: I18n.tr("panels.launcher.settings-clip-preview-description")
|
||||
checked: Settings.data.appLauncher.enableClipPreview
|
||||
onToggled: checked => Settings.data.appLauncher.enableClipPreview = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.enableClipPreview")
|
||||
enabled: Settings.data.appLauncher.enableClipboardHistory
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-clip-wrap-text-label")
|
||||
description: I18n.tr("panels.launcher.settings-clip-wrap-text-description")
|
||||
checked: Settings.data.appLauncher.clipboardWrapText
|
||||
onToggled: checked => Settings.data.appLauncher.clipboardWrapText = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.clipboardWrapText")
|
||||
enabled: Settings.data.appLauncher.enableClipboardHistory
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-auto-paste-label")
|
||||
description: I18n.tr("panels.launcher.settings-auto-paste-description")
|
||||
checked: Settings.data.appLauncher.autoPasteClipboard
|
||||
onToggled: checked => Settings.data.appLauncher.autoPasteClipboard = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.autoPasteClipboard")
|
||||
enabled: Settings.data.appLauncher.enableClipboardHistory && ProgramCheckerService.wtypeAvailable
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-clip-smart-icons-label")
|
||||
description: I18n.tr("panels.launcher.settings-clip-smart-icons-description")
|
||||
checked: Settings.data.appLauncher.enableClipboardSmartIcons
|
||||
onToggled: checked => Settings.data.appLauncher.enableClipboardSmartIcons = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardSmartIcons")
|
||||
enabled: Settings.data.appLauncher.enableClipboardHistory
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-clip-chips-label")
|
||||
description: I18n.tr("panels.launcher.settings-clip-chips-description")
|
||||
checked: Settings.data.appLauncher.enableClipboardChips
|
||||
onToggled: checked => Settings.data.appLauncher.enableClipboardChips = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardChips")
|
||||
enabled: Settings.data.appLauncher.enableClipboardHistory
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.appLauncher.enableClipboardHistory
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
label: I18n.tr("panels.launcher.settings-clipboard-watch-text-label")
|
||||
description: I18n.tr("panels.launcher.settings-clipboard-watch-text-description")
|
||||
Layout.fillWidth: true
|
||||
text: Settings.data.appLauncher.clipboardWatchTextCommand
|
||||
onTextChanged: Settings.data.appLauncher.clipboardWatchTextCommand = text
|
||||
enabled: Settings.data.appLauncher.enableClipboardHistory
|
||||
visible: Settings.data.appLauncher.enableClipboardHistory
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
label: I18n.tr("panels.launcher.settings-clipboard-watch-image-label")
|
||||
description: I18n.tr("panels.launcher.settings-clipboard-watch-image-description")
|
||||
Layout.fillWidth: true
|
||||
text: Settings.data.appLauncher.clipboardWatchImageCommand
|
||||
onTextChanged: Settings.data.appLauncher.clipboardWatchImageCommand = text
|
||||
enabled: Settings.data.appLauncher.enableClipboardHistory
|
||||
visible: Settings.data.appLauncher.enableClipboardHistory
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NTextInput {
|
||||
label: I18n.tr("panels.launcher.settings-terminal-command-label")
|
||||
description: I18n.tr("panels.launcher.settings-terminal-command-description")
|
||||
Layout.fillWidth: true
|
||||
text: Settings.data.appLauncher.terminalCommand
|
||||
onTextChanged: {
|
||||
Settings.data.appLauncher.terminalCommand = text;
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-custom-launch-prefix-enabled-label")
|
||||
description: I18n.tr("panels.launcher.settings-custom-launch-prefix-enabled-description")
|
||||
checked: Settings.data.appLauncher.customLaunchPrefixEnabled
|
||||
onToggled: checked => Settings.data.appLauncher.customLaunchPrefixEnabled = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.customLaunchPrefixEnabled")
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
label: I18n.tr("panels.launcher.settings-custom-launch-prefix-label")
|
||||
description: I18n.tr("panels.launcher.settings-custom-launch-prefix-description")
|
||||
Layout.fillWidth: true
|
||||
text: Settings.data.appLauncher.customLaunchPrefix
|
||||
enabled: Settings.data.appLauncher.customLaunchPrefixEnabled
|
||||
visible: Settings.data.appLauncher.customLaunchPrefixEnabled
|
||||
onTextChanged: Settings.data.appLauncher.customLaunchPrefix = text
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
label: I18n.tr("panels.launcher.settings-annotation-tool-label")
|
||||
description: I18n.tr("panels.launcher.settings-annotation-tool-description")
|
||||
Layout.fillWidth: true
|
||||
text: Settings.data.appLauncher.screenshotAnnotationTool
|
||||
placeholderText: I18n.tr("panels.launcher.settings-annotation-tool-placeholder")
|
||||
onTextChanged: Settings.data.appLauncher.screenshotAnnotationTool = text
|
||||
}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
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("common.position")
|
||||
description: I18n.tr("panels.launcher.settings-position-description")
|
||||
Layout.fillWidth: true
|
||||
model: [
|
||||
{
|
||||
"key": "follow_bar",
|
||||
"name": I18n.tr("positions.follow-bar")
|
||||
},
|
||||
{
|
||||
"key": "center",
|
||||
"name": I18n.tr("positions.center")
|
||||
},
|
||||
{
|
||||
"key": "top_center",
|
||||
"name": I18n.tr("positions.top-center")
|
||||
},
|
||||
{
|
||||
"key": "top_left",
|
||||
"name": I18n.tr("positions.top-left")
|
||||
},
|
||||
{
|
||||
"key": "top_right",
|
||||
"name": I18n.tr("positions.top-right")
|
||||
},
|
||||
{
|
||||
"key": "bottom_left",
|
||||
"name": I18n.tr("positions.bottom-left")
|
||||
},
|
||||
{
|
||||
"key": "bottom_right",
|
||||
"name": I18n.tr("positions.bottom-right")
|
||||
},
|
||||
{
|
||||
"key": "bottom_center",
|
||||
"name": I18n.tr("positions.bottom-center")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.appLauncher.position
|
||||
onSelected: function (key) {
|
||||
Settings.data.appLauncher.position = key;
|
||||
}
|
||||
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.position")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-overlay-layer-label")
|
||||
description: I18n.tr("panels.launcher.settings-overlay-layer-description")
|
||||
checked: Settings.data.appLauncher.overviewLayer
|
||||
onToggled: checked => Settings.data.appLauncher.overviewLayer = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.overviewLayer")
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.launcher.settings-view-mode-label")
|
||||
description: I18n.tr("panels.launcher.settings-view-mode-description")
|
||||
Layout.fillWidth: true
|
||||
model: [
|
||||
{
|
||||
"key": "list",
|
||||
"name": I18n.tr("options.launcher-view-mode.list")
|
||||
},
|
||||
{
|
||||
"key": "grid",
|
||||
"name": I18n.tr("options.launcher-view-mode.grid")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.appLauncher.viewMode
|
||||
onSelected: function (key) {
|
||||
Settings.data.appLauncher.viewMode = key;
|
||||
}
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.viewMode")
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.launcher.settings-density-label")
|
||||
description: I18n.tr("panels.launcher.settings-density-description")
|
||||
Layout.fillWidth: true
|
||||
model: [
|
||||
{
|
||||
"key": "compact",
|
||||
"name": I18n.tr("options.launcher-density.compact")
|
||||
},
|
||||
{
|
||||
"key": "default",
|
||||
"name": I18n.tr("options.launcher-density.default")
|
||||
},
|
||||
{
|
||||
"key": "comfortable",
|
||||
"name": I18n.tr("options.launcher-density.comfortable")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.appLauncher.density || "compact"
|
||||
onSelected: function (key) {
|
||||
Settings.data.appLauncher.density = key;
|
||||
}
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.density")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-show-categories-label")
|
||||
description: I18n.tr("panels.launcher.settings-show-categories-description")
|
||||
checked: Settings.data.appLauncher.showCategories
|
||||
onToggled: checked => Settings.data.appLauncher.showCategories = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.showCategories")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-icon-mode-label")
|
||||
description: I18n.tr("panels.launcher.settings-icon-mode-description")
|
||||
checked: Settings.data.appLauncher.iconMode === "native"
|
||||
onToggled: checked => Settings.data.appLauncher.iconMode = checked ? "native" : "tabler"
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.iconMode") === "native"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-show-icon-background-label")
|
||||
description: I18n.tr("panels.launcher.settings-show-icon-background-description")
|
||||
checked: Settings.data.appLauncher.showIconBackground
|
||||
onToggled: checked => Settings.data.appLauncher.showIconBackground = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.showIconBackground")
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-sort-by-usage-label")
|
||||
description: I18n.tr("panels.launcher.settings-sort-by-usage-description")
|
||||
checked: Settings.data.appLauncher.sortByMostUsed
|
||||
onToggled: checked => Settings.data.appLauncher.sortByMostUsed = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.sortByMostUsed")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-enable-settings-search-label")
|
||||
description: I18n.tr("panels.launcher.settings-enable-settings-search-description")
|
||||
checked: Settings.data.appLauncher.enableSettingsSearch
|
||||
onToggled: checked => Settings.data.appLauncher.enableSettingsSearch = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.enableSettingsSearch")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-enable-windows-search-label")
|
||||
description: I18n.tr("panels.launcher.settings-enable-windows-search-description")
|
||||
checked: Settings.data.appLauncher.enableWindowsSearch
|
||||
onToggled: checked => Settings.data.appLauncher.enableWindowsSearch = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.enableWindowsSearch")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-enable-session-search-label")
|
||||
description: I18n.tr("panels.launcher.settings-enable-session-search-description")
|
||||
checked: Settings.data.appLauncher.enableSessionSearch
|
||||
onToggled: checked => Settings.data.appLauncher.enableSessionSearch = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.enableSessionSearch")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.launcher.settings-ignore-mouse-input-label")
|
||||
description: I18n.tr("panels.launcher.settings-ignore-mouse-input-description")
|
||||
checked: Settings.data.appLauncher.ignoreMouseInput
|
||||
onToggled: checked => Settings.data.appLauncher.ignoreMouseInput = checked
|
||||
defaultValue: Settings.getDefaultValue("appLauncher.ignoreMouseInput")
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
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
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.general")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.clipboard")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.execute")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
GeneralSubTab {}
|
||||
ClipboardSubTab {}
|
||||
ExecuteSubTab {}
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
function insertToken(token) {
|
||||
if (formatInput.inputItem) {
|
||||
var input = formatInput.inputItem;
|
||||
var cursorPos = input.cursorPosition;
|
||||
var currentText = input.text;
|
||||
var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos);
|
||||
input.text = newText + " ";
|
||||
input.cursorPosition = cursorPos + token.length + 1;
|
||||
input.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.lock-screen.clock-style-label")
|
||||
description: I18n.tr("panels.lock-screen.clock-style-description")
|
||||
model: [
|
||||
{
|
||||
"key": "analog",
|
||||
"name": I18n.tr("panels.lock-screen.clock-style-analog")
|
||||
},
|
||||
{
|
||||
"key": "digital",
|
||||
"name": I18n.tr("panels.lock-screen.clock-style-digital")
|
||||
},
|
||||
{
|
||||
"key": "custom",
|
||||
"name": I18n.tr("panels.lock-screen.clock-style-custom")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.general.clockStyle
|
||||
onSelected: key => Settings.data.general.clockStyle = key
|
||||
defaultValue: Settings.getDefaultValue("general.clockStyle")
|
||||
z: 10
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: formatInput
|
||||
label: I18n.tr("panels.lock-screen.clock-format-label")
|
||||
description: I18n.tr("panels.lock-screen.clock-format-description")
|
||||
text: Settings.data.general.clockFormat
|
||||
onTextChanged: Settings.data.general.clockFormat = text
|
||||
visible: Settings.data.general.clockStyle === "custom"
|
||||
defaultValue: Settings.getDefaultValue("general.clockFormat")
|
||||
}
|
||||
|
||||
NDateTimeTokens {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 300
|
||||
visible: Settings.data.general.clockStyle === "custom"
|
||||
onTokenClicked: token => root.insertToken(token)
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.lock-screen.password-chars-label")
|
||||
description: I18n.tr("panels.lock-screen.password-chars-description")
|
||||
checked: Settings.data.general.passwordChars
|
||||
onToggled: checked => Settings.data.general.passwordChars = checked
|
||||
defaultValue: Settings.getDefaultValue("general.passwordChars")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.lock-screen.compact-lockscreen-label")
|
||||
description: I18n.tr("panels.lock-screen.compact-lockscreen-description")
|
||||
checked: Settings.data.general.compactLockScreen
|
||||
onToggled: checked => Settings.data.general.compactLockScreen = checked
|
||||
defaultValue: Settings.getDefaultValue("general.compactLockScreen")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.lock-screen.enable-lockscreen-media-controls-label")
|
||||
description: I18n.tr("panels.lock-screen.enable-lockscreen-media-controls-description")
|
||||
checked: Settings.data.general.enableLockScreenMediaControls
|
||||
onToggled: checked => Settings.data.general.enableLockScreenMediaControls = checked
|
||||
visible: !Settings.data.general.compactLockScreen
|
||||
defaultValue: Settings.getDefaultValue("general.enableLockScreenMediaControls")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.lock-screen.lock-screen-animations-label")
|
||||
description: I18n.tr("panels.lock-screen.lock-screen-animations-description")
|
||||
checked: Settings.data.general.lockScreenAnimations
|
||||
onToggled: checked => Settings.data.general.lockScreenAnimations = checked
|
||||
defaultValue: Settings.getDefaultValue("general.lockScreenAnimations")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.lock-screen.lock-screen-blur-strength-label")
|
||||
description: I18n.tr("panels.lock-screen.lock-screen-blur-strength-description")
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.general.lockScreenBlur
|
||||
onMoved: value => Settings.data.general.lockScreenBlur = value
|
||||
text: ((Settings.data.general.lockScreenBlur) * 100).toFixed(0) + "%"
|
||||
defaultValue: Settings.getDefaultValue("general.lockScreenBlur")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.lock-screen.lock-screen-tint-strength-label")
|
||||
description: I18n.tr("panels.lock-screen.lock-screen-tint-strength-description")
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.general.lockScreenTint
|
||||
onMoved: value => Settings.data.general.lockScreenTint = value
|
||||
text: ((Settings.data.general.lockScreenTint) * 100).toFixed(0) + "%"
|
||||
defaultValue: Settings.getDefaultValue("general.lockScreenTint")
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.lock-screen.lock-on-suspend-label")
|
||||
description: I18n.tr("panels.lock-screen.lock-on-suspend-description")
|
||||
checked: Settings.data.general.lockOnSuspend
|
||||
onToggled: checked => Settings.data.general.lockOnSuspend = checked
|
||||
defaultValue: Settings.getDefaultValue("general.lockOnSuspend")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.lock-screen.auto-start-auth-label")
|
||||
description: I18n.tr("panels.lock-screen.auto-start-auth-description")
|
||||
checked: Settings.data.general.autoStartAuth
|
||||
onToggled: checked => Settings.data.general.autoStartAuth = checked
|
||||
defaultValue: Settings.getDefaultValue("general.autoStartAuth")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.lock-screen.allow-password-with-fprintd-label")
|
||||
description: I18n.tr("panels.lock-screen.allow-password-with-fprintd-description")
|
||||
checked: Settings.data.general.allowPasswordWithFprintd
|
||||
onToggled: checked => Settings.data.general.allowPasswordWithFprintd = checked
|
||||
defaultValue: Settings.getDefaultValue("general.allowPasswordWithFprintd")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.lock-screen.show-session-buttons-label")
|
||||
description: I18n.tr("panels.lock-screen.show-session-buttons-description")
|
||||
checked: Settings.data.general.showSessionButtonsOnLockScreen
|
||||
onToggled: checked => Settings.data.general.showSessionButtonsOnLockScreen = checked
|
||||
defaultValue: Settings.getDefaultValue("general.showSessionButtonsOnLockScreen")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.lock-screen.show-hibernate-label")
|
||||
description: I18n.tr("panels.lock-screen.show-hibernate-description")
|
||||
checked: Settings.data.general.showHibernateOnLockScreen
|
||||
onToggled: checked => Settings.data.general.showHibernateOnLockScreen = checked
|
||||
visible: Settings.data.general.showSessionButtonsOnLockScreen
|
||||
defaultValue: Settings.getDefaultValue("general.showSessionButtonsOnLockScreen")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.session-menu.enable-countdown-label")
|
||||
description: I18n.tr("panels.session-menu.enable-countdown-description")
|
||||
checked: Settings.data.general.enableLockScreenCountdown
|
||||
onToggled: checked => Settings.data.general.enableLockScreenCountdown = checked
|
||||
visible: Settings.data.general.showSessionButtonsOnLockScreen
|
||||
defaultValue: Settings.getDefaultValue("general.enableLockScreenCountdown")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
visible: Settings.data.general.showSessionButtonsOnLockScreen && Settings.data.general.enableLockScreenCountdown
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.session-menu.countdown-duration-label")
|
||||
description: I18n.tr("panels.session-menu.countdown-duration-description")
|
||||
from: 1000
|
||||
to: 30000
|
||||
stepSize: 1000
|
||||
showReset: true
|
||||
value: Settings.data.general.lockScreenCountdownDuration
|
||||
onMoved: value => Settings.data.general.lockScreenCountdownDuration = value
|
||||
text: Math.round(Settings.data.general.lockScreenCountdownDuration / 1000) + "s"
|
||||
defaultValue: Settings.getDefaultValue("general.lockScreenCountdownDuration")
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
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
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.appearance")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.behavior")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.monitors")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
AppearanceSubTab {}
|
||||
BehaviorSubTab {}
|
||||
MonitorsSubTab {}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
enabled: true
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.lock-screen.monitors-desc")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Quickshell.screens || []
|
||||
delegate: NCheckbox {
|
||||
Layout.fillWidth: true
|
||||
readonly property real compositorScale: {
|
||||
const info = CompositorService.displayScales[modelData.name];
|
||||
return (info && info.scale) ? info.scale : 1.0;
|
||||
}
|
||||
label: modelData.name || "Unknown"
|
||||
description: {
|
||||
I18n.tr("system.monitor-description", {
|
||||
"model": modelData.model,
|
||||
"width": modelData.width * compositorScale,
|
||||
"height": modelData.height * compositorScale,
|
||||
"scale": compositorScale
|
||||
});
|
||||
}
|
||||
checked: (Settings.data.general.lockScreenMonitors || []).indexOf(modelData.name) !== -1
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
Settings.data.general.lockScreenMonitors = root.addMonitor(Settings.data.general.lockScreenMonitors, modelData.name);
|
||||
} else {
|
||||
Settings.data.general.lockScreenMonitors = root.removeMonitor(Settings.data.general.lockScreenMonitors, modelData.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
enabled: Settings.data.notifications.enabled
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.notifications.duration-respect-expire-label")
|
||||
description: I18n.tr("panels.notifications.duration-respect-expire-description")
|
||||
checked: Settings.data.notifications.respectExpireTimeout
|
||||
onToggled: checked => Settings.data.notifications.respectExpireTimeout = checked
|
||||
defaultValue: Settings.getDefaultValue("notifications.respectExpireTimeout")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.notifications.duration-low-urgency-label")
|
||||
description: I18n.tr("panels.notifications.duration-low-urgency-description")
|
||||
from: 1
|
||||
to: 30
|
||||
stepSize: 1
|
||||
showReset: true
|
||||
value: Settings.data.notifications.lowUrgencyDuration
|
||||
onMoved: value => Settings.data.notifications.lowUrgencyDuration = value
|
||||
text: Settings.data.notifications.lowUrgencyDuration + "s"
|
||||
defaultValue: Settings.getDefaultValue("notifications.lowUrgencyDuration")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.notifications.duration-normal-urgency-label")
|
||||
description: I18n.tr("panels.notifications.duration-normal-urgency-description")
|
||||
from: 1
|
||||
to: 30
|
||||
stepSize: 1
|
||||
showReset: true
|
||||
value: Settings.data.notifications.normalUrgencyDuration
|
||||
onMoved: value => Settings.data.notifications.normalUrgencyDuration = value
|
||||
text: Settings.data.notifications.normalUrgencyDuration + "s"
|
||||
defaultValue: Settings.getDefaultValue("notifications.normalUrgencyDuration")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.notifications.duration-critical-urgency-label")
|
||||
description: I18n.tr("panels.notifications.duration-critical-urgency-description")
|
||||
from: 1
|
||||
to: 30
|
||||
stepSize: 1
|
||||
showReset: true
|
||||
value: Settings.data.notifications.criticalUrgencyDuration
|
||||
onMoved: value => Settings.data.notifications.criticalUrgencyDuration = value
|
||||
text: Settings.data.notifications.criticalUrgencyDuration + "s"
|
||||
defaultValue: Settings.getDefaultValue("notifications.criticalUrgencyDuration")
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property var addMonitor
|
||||
property var removeMonitor
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.notifications.settings-enabled-label")
|
||||
description: I18n.tr("panels.notifications.settings-enabled-description")
|
||||
checked: Settings.data.notifications.enabled !== false
|
||||
onToggled: checked => Settings.data.notifications.enabled = checked
|
||||
defaultValue: Settings.getDefaultValue("notifications.enabled")
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL
|
||||
enabled: Settings.data.notifications.enabled
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.notifications.settings-density-label")
|
||||
description: I18n.tr("panels.notifications.settings-density-description")
|
||||
model: [
|
||||
{
|
||||
"key": "default",
|
||||
"name": I18n.tr("options.notification-density.default")
|
||||
},
|
||||
{
|
||||
"key": "compact",
|
||||
"name": I18n.tr("options.notification-density.compact")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.notifications.density || "default"
|
||||
onSelected: key => Settings.data.notifications.density = key
|
||||
defaultValue: Settings.getDefaultValue("notifications.density")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("tooltips.do-not-disturb-enabled")
|
||||
description: I18n.tr("panels.notifications.settings-do-not-disturb-description")
|
||||
checked: NotificationService.doNotDisturb
|
||||
onToggled: checked => NotificationService.doNotDisturb = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("common.position")
|
||||
description: I18n.tr("panels.notifications.settings-location-description")
|
||||
model: [
|
||||
{
|
||||
"key": "top",
|
||||
"name": I18n.tr("positions.top-center")
|
||||
},
|
||||
{
|
||||
"key": "top_left",
|
||||
"name": I18n.tr("positions.top-left")
|
||||
},
|
||||
{
|
||||
"key": "top_right",
|
||||
"name": I18n.tr("positions.top-right")
|
||||
},
|
||||
{
|
||||
"key": "bottom",
|
||||
"name": I18n.tr("positions.bottom-center")
|
||||
},
|
||||
{
|
||||
"key": "bottom_left",
|
||||
"name": I18n.tr("positions.bottom-left")
|
||||
},
|
||||
{
|
||||
"key": "bottom_right",
|
||||
"name": I18n.tr("positions.bottom-right")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.notifications.location || "top_right"
|
||||
onSelected: key => Settings.data.notifications.location = key
|
||||
defaultValue: Settings.getDefaultValue("notifications.location")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.osd.always-on-top-label")
|
||||
description: I18n.tr("panels.notifications.settings-always-on-top-description")
|
||||
checked: Settings.data.notifications.overlayLayer
|
||||
onToggled: checked => Settings.data.notifications.overlayLayer = checked
|
||||
defaultValue: Settings.getDefaultValue("notifications.overlayLayer")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.osd.background-opacity-label")
|
||||
description: I18n.tr("panels.notifications.settings-background-opacity-description")
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.notifications.backgroundOpacity
|
||||
onMoved: value => Settings.data.notifications.backgroundOpacity = value
|
||||
text: Math.round(Settings.data.notifications.backgroundOpacity * 100) + "%"
|
||||
defaultValue: Settings.getDefaultValue("notifications.backgroundOpacity")
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.notifications.monitors-desc")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Quickshell.screens || []
|
||||
delegate: NCheckbox {
|
||||
Layout.fillWidth: true
|
||||
readonly property real compositorScale: {
|
||||
const info = CompositorService.displayScales[modelData.name];
|
||||
return (info && info.scale) ? info.scale : 1.0;
|
||||
}
|
||||
label: modelData.name || I18n.tr("common.unknown")
|
||||
description: {
|
||||
I18n.tr("system.monitor-description", {
|
||||
"model": modelData.model,
|
||||
"width": modelData.width * compositorScale,
|
||||
"height": modelData.height * compositorScale,
|
||||
"scale": compositorScale
|
||||
});
|
||||
}
|
||||
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
Settings.data.notifications.monitors = root.addMonitor(Settings.data.notifications.monitors, modelData.name);
|
||||
} else {
|
||||
Settings.data.notifications.monitors = root.removeMonitor(Settings.data.notifications.monitors, modelData.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
enabled: Settings.data.notifications.enabled
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.notifications.history-clear-dismiss-label")
|
||||
description: I18n.tr("panels.notifications.history-clear-dismiss-description")
|
||||
checked: Settings.data.notifications.clearDismissed
|
||||
onToggled: checked => Settings.data.notifications.clearDismissed = checked
|
||||
defaultValue: Settings.getDefaultValue("notifications.clearDismissed")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.notifications.settings-markdown-label")
|
||||
description: I18n.tr("panels.notifications.settings-markdown-description")
|
||||
checked: Settings.data.notifications.enableMarkdown
|
||||
onToggled: checked => Settings.data.notifications.enableMarkdown = checked
|
||||
defaultValue: Settings.getDefaultValue("notifications.enableMarkdown")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.notifications.history-low-urgency-label")
|
||||
description: I18n.tr("panels.notifications.history-low-urgency-description")
|
||||
checked: Settings.data.notifications?.saveToHistory?.low !== false
|
||||
onToggled: checked => Settings.data.notifications.saveToHistory.low = checked
|
||||
defaultValue: Settings.getDefaultValue("notifications.saveToHistory.low")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.notifications.history-normal-urgency-label")
|
||||
description: I18n.tr("panels.notifications.history-normal-urgency-description")
|
||||
checked: Settings.data.notifications?.saveToHistory?.normal !== false
|
||||
onToggled: checked => Settings.data.notifications.saveToHistory.normal = checked
|
||||
defaultValue: Settings.getDefaultValue("notifications.saveToHistory.normal")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.notifications.history-critical-urgency-label")
|
||||
description: I18n.tr("panels.notifications.history-critical-urgency-description")
|
||||
checked: Settings.data.notifications?.saveToHistory?.critical !== false
|
||||
onToggled: checked => Settings.data.notifications.saveToHistory.critical = checked
|
||||
defaultValue: Settings.getDefaultValue("notifications.saveToHistory.critical")
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
dim: true
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: Math.min(500 * Style.uiScaleRatio, parent.width * 0.9)
|
||||
padding: Style.marginL
|
||||
|
||||
property int editIndex: -1
|
||||
property string patternValue: ""
|
||||
property string actionValue: "block"
|
||||
|
||||
signal saved(string pattern, string action)
|
||||
|
||||
property var _savedSlot: null
|
||||
property string _selectedAction: "block"
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
patternInput.text = patternValue;
|
||||
actionCombo.currentKey = actionValue;
|
||||
_selectedAction = actionValue;
|
||||
patternInput.forceActiveFocus();
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: contentLayout
|
||||
spacing: Style.marginL
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
NText {
|
||||
text: editIndex >= 0 ? I18n.tr("panels.notifications.rules-edit") : I18n.tr("panels.notifications.rules-add")
|
||||
font.weight: Style.fontWeightBold
|
||||
pointSize: Style.fontSizeL
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NTextInput {
|
||||
id: patternInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.notifications.rules-pattern-label")
|
||||
placeholderText: I18n.tr("panels.notifications.rules-pattern-placeholder")
|
||||
fontFamily: Settings.data.ui.fontFixed
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
id: actionCombo
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.notifications.rules-action-label")
|
||||
model: [
|
||||
{
|
||||
"key": "block",
|
||||
"name": I18n.tr("panels.notifications.rules-action-block")
|
||||
},
|
||||
{
|
||||
"key": "mute",
|
||||
"name": I18n.tr("panels.notifications.rules-action-mute")
|
||||
},
|
||||
{
|
||||
"key": "hide",
|
||||
"name": I18n.tr("panels.notifications.rules-action-hide")
|
||||
}
|
||||
]
|
||||
currentKey: actionValue
|
||||
onSelected: key => {
|
||||
actionValue = key;
|
||||
_selectedAction = key;
|
||||
}
|
||||
}
|
||||
|
||||
NLabel {
|
||||
Layout.fillWidth: true
|
||||
label: _selectedAction === "block" ? I18n.tr("panels.notifications.rules-action-block-desc") : (_selectedAction === "mute" ? I18n.tr("panels.notifications.rules-action-mute-desc") : I18n.tr("panels.notifications.rules-action-hide-desc"))
|
||||
labelColor: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.cancel")
|
||||
outlined: true
|
||||
onClicked: root.close()
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.save")
|
||||
icon: "check"
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
enabled: patternInput.text.trim() !== ""
|
||||
onClicked: {
|
||||
root.saved(patternInput.text.trim(), _selectedAction || "block");
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// File pickers for sound sub-tab
|
||||
function openUnifiedSoundPicker() {
|
||||
unifiedSoundFilePicker.open();
|
||||
}
|
||||
function openLowSoundPicker() {
|
||||
lowSoundFilePicker.open();
|
||||
}
|
||||
function openNormalSoundPicker() {
|
||||
normalSoundFilePicker.open();
|
||||
}
|
||||
function openCriticalSoundPicker() {
|
||||
criticalSoundFilePicker.open();
|
||||
}
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: false // this is too cramped on this tab to split evenly
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.appearance")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.duration")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.history")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.sound")
|
||||
tabIndex: 3
|
||||
checked: subTabBar.currentIndex === 3
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.toast")
|
||||
tabIndex: 4
|
||||
checked: subTabBar.currentIndex === 4
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("panels.notifications.rules-tab")
|
||||
tabIndex: 5
|
||||
checked: subTabBar.currentIndex === 5
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
GeneralSubTab {
|
||||
addMonitor: root.addMonitor
|
||||
removeMonitor: root.removeMonitor
|
||||
}
|
||||
DurationSubTab {}
|
||||
HistorySubTab {}
|
||||
SoundSubTab {
|
||||
onOpenUnifiedPicker: root.openUnifiedSoundPicker()
|
||||
onOpenLowPicker: root.openLowSoundPicker()
|
||||
onOpenNormalPicker: root.openNormalSoundPicker()
|
||||
onOpenCriticalPicker: root.openCriticalSoundPicker()
|
||||
}
|
||||
ToastSubTab {}
|
||||
RulesSubTab {}
|
||||
}
|
||||
|
||||
// File Pickers for Sound Files
|
||||
NFilePicker {
|
||||
id: unifiedSoundFilePicker
|
||||
title: I18n.tr("panels.notifications.sounds-files-unified-select-title")
|
||||
selectionMode: "files"
|
||||
initialPath: Quickshell.env("HOME")
|
||||
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
const soundPath = paths[0];
|
||||
Settings.data.notifications.sounds.normalSoundFile = soundPath;
|
||||
Settings.data.notifications.sounds.lowSoundFile = soundPath;
|
||||
Settings.data.notifications.sounds.criticalSoundFile = soundPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: lowSoundFilePicker
|
||||
title: I18n.tr("panels.notifications.sounds-files-low-select-title")
|
||||
selectionMode: "files"
|
||||
initialPath: Quickshell.env("HOME")
|
||||
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
Settings.data.notifications.sounds.lowSoundFile = paths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: normalSoundFilePicker
|
||||
title: I18n.tr("panels.notifications.sounds-files-normal-select-title")
|
||||
selectionMode: "files"
|
||||
initialPath: Quickshell.env("HOME")
|
||||
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
Settings.data.notifications.sounds.normalSoundFile = paths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: criticalSoundFilePicker
|
||||
title: I18n.tr("panels.notifications.sounds-files-critical-select-title")
|
||||
selectionMode: "files"
|
||||
initialPath: Quickshell.env("HOME")
|
||||
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
Settings.data.notifications.sounds.criticalSoundFile = paths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
enabled: Settings.data.notifications.enabled
|
||||
|
||||
function _saveToService() {
|
||||
NotificationRulesService.save();
|
||||
}
|
||||
|
||||
function _removeRule(index) {
|
||||
var arr = (NotificationRulesService.rules || []).slice();
|
||||
arr.splice(index, 1);
|
||||
NotificationRulesService.rules = arr;
|
||||
_saveToService();
|
||||
}
|
||||
|
||||
NotificationRuleEditPopup {
|
||||
id: editPopup
|
||||
parent: Overlay.overlay
|
||||
}
|
||||
|
||||
function openEdit(index, patternVal, actionVal) {
|
||||
editPopup.editIndex = index;
|
||||
editPopup.patternValue = patternVal || "";
|
||||
editPopup.actionValue = actionVal || "block";
|
||||
|
||||
try {
|
||||
editPopup.saved.disconnect(editPopup._savedSlot);
|
||||
} catch (e) {}
|
||||
|
||||
editPopup._savedSlot = function (pattern, action) {
|
||||
const trimmed = (pattern || "").trim();
|
||||
if (trimmed === "")
|
||||
return;
|
||||
var arr = (NotificationRulesService.rules || []).slice();
|
||||
var rule = {
|
||||
"pattern": trimmed,
|
||||
"action": action
|
||||
};
|
||||
if (index >= 0 && index < arr.length) {
|
||||
arr[index] = rule;
|
||||
} else {
|
||||
arr.push(rule);
|
||||
}
|
||||
NotificationRulesService.rules = arr;
|
||||
_saveToService();
|
||||
};
|
||||
|
||||
editPopup.saved.connect(editPopup._savedSlot);
|
||||
editPopup.open();
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.notifications.rules-label")
|
||||
description: I18n.tr("panels.notifications.rules-description")
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginS
|
||||
Layout.bottomMargin: Style.marginS
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: NotificationRulesService.rules || []
|
||||
|
||||
delegate: RowLayout {
|
||||
id: entryDelegate
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
property string pattern: modelData.pattern || ""
|
||||
property string action: modelData.action || "block"
|
||||
property bool isRegex: pattern.length >= 3 && pattern.startsWith("/") && pattern.endsWith("/")
|
||||
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
Layout.fillWidth: true
|
||||
label: (entryDelegate.isRegex ? "regex: " : "") + entryDelegate.pattern
|
||||
description: entryDelegate.action === "block" ? I18n.tr("panels.notifications.rules-action-block") : (entryDelegate.action === "mute" ? I18n.tr("panels.notifications.rules-action-mute") : I18n.tr("panels.notifications.rules-action-hide"))
|
||||
labelColor: entryDelegate.pattern ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: I18n.tr("common.edit")
|
||||
onClicked: root.openEdit(entryDelegate.index, entryDelegate.pattern, entryDelegate.action)
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "trash"
|
||||
tooltipText: I18n.tr("panels.notifications.rules-delete")
|
||||
onClicked: root._removeRule(entryDelegate.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("panels.notifications.rules-add")
|
||||
icon: "add"
|
||||
enabled: Settings.data.notifications.enabled
|
||||
onClicked: root.openEdit(-1, "", "block")
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
enabled: Settings.data.notifications.enabled
|
||||
|
||||
signal openUnifiedPicker
|
||||
signal openLowPicker
|
||||
signal openNormalPicker
|
||||
signal openCriticalPicker
|
||||
|
||||
// QtMultimedia unavailable message
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
visible: !SoundService.multimediaAvailable
|
||||
implicitHeight: unavailableContent.implicitHeight + Style.margin2L
|
||||
|
||||
RowLayout {
|
||||
id: unavailableContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "warning"
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeXL
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NLabel {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.notifications.sounds-unavailable-label")
|
||||
description: I18n.tr("panels.notifications.sounds-unavailable-description")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
enabled: SoundService.multimediaAvailable
|
||||
label: I18n.tr("panels.notifications.sounds-enabled-label")
|
||||
description: I18n.tr("panels.notifications.sounds-enabled-description")
|
||||
checked: Settings.data.notifications?.sounds?.enabled ?? false
|
||||
onToggled: checked => Settings.data.notifications.sounds.enabled = checked
|
||||
defaultValue: Settings.getDefaultValue("notifications.sounds.enabled")
|
||||
}
|
||||
|
||||
// Sound Volume
|
||||
NValueSlider {
|
||||
enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.notifications.sounds-volume-label")
|
||||
description: I18n.tr("panels.notifications.sounds-volume-description")
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.notifications?.sounds?.volume ?? 0.5
|
||||
onMoved: value => Settings.data.notifications.sounds.volume = value
|
||||
text: Math.round((Settings.data.notifications?.sounds?.volume ?? 0.5) * 100) + "%"
|
||||
defaultValue: Settings.getDefaultValue("notifications.sounds.volume")
|
||||
}
|
||||
|
||||
// Separate Sounds Toggle
|
||||
NToggle {
|
||||
enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.notifications.sounds-separate-label")
|
||||
description: I18n.tr("panels.notifications.sounds-separate-description")
|
||||
checked: Settings.data.notifications?.sounds?.separateSounds ?? false
|
||||
onToggled: checked => Settings.data.notifications.sounds.separateSounds = checked
|
||||
defaultValue: Settings.getDefaultValue("notifications.sounds.separateSounds")
|
||||
}
|
||||
|
||||
// Unified Sound File (shown when separateSounds is false)
|
||||
ColumnLayout {
|
||||
enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
|
||||
visible: !(Settings.data.notifications?.sounds?.separateSounds ?? false)
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.notifications.sounds-files-unified-label")
|
||||
description: I18n.tr("panels.notifications.sounds-files-unified-description")
|
||||
}
|
||||
|
||||
NTextInputButton {
|
||||
enabled: parent.enabled
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder")
|
||||
text: Settings.data.notifications?.sounds?.normalSoundFile ?? ""
|
||||
buttonIcon: "folder-open"
|
||||
buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file")
|
||||
onInputTextChanged: text => {
|
||||
// When separate sounds are enabled, this row is hidden but still
|
||||
// bound to normalSoundFile; user edits to normal still update that
|
||||
// key and would re-trigger this handler and wipe low/critical.
|
||||
if (Settings.data.notifications.sounds.separateSounds) {
|
||||
return;
|
||||
}
|
||||
const soundPath = text;
|
||||
Settings.data.notifications.sounds.normalSoundFile = soundPath;
|
||||
Settings.data.notifications.sounds.lowSoundFile = soundPath;
|
||||
Settings.data.notifications.sounds.criticalSoundFile = soundPath;
|
||||
}
|
||||
onButtonClicked: root.openUnifiedPicker()
|
||||
}
|
||||
}
|
||||
|
||||
// Separate Sound Files (shown when separateSounds is true)
|
||||
ColumnLayout {
|
||||
visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Low Urgency Sound File
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.notifications.sounds-files-low-label")
|
||||
description: I18n.tr("panels.notifications.sounds-files-low-description")
|
||||
}
|
||||
|
||||
NTextInputButton {
|
||||
enabled: parent.enabled
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder")
|
||||
text: Settings.data.notifications?.sounds?.lowSoundFile ?? ""
|
||||
buttonIcon: "folder-open"
|
||||
buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file")
|
||||
onInputTextChanged: text => Settings.data.notifications.sounds.lowSoundFile = text
|
||||
onButtonClicked: root.openLowPicker()
|
||||
}
|
||||
}
|
||||
|
||||
// Normal Urgency Sound File
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.notifications.sounds-files-normal-label")
|
||||
description: I18n.tr("panels.notifications.sounds-files-normal-description")
|
||||
}
|
||||
|
||||
NTextInputButton {
|
||||
enabled: parent.enabled
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder")
|
||||
text: Settings.data.notifications?.sounds?.normalSoundFile ?? ""
|
||||
buttonIcon: "folder-open"
|
||||
buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file")
|
||||
onInputTextChanged: text => Settings.data.notifications.sounds.normalSoundFile = text
|
||||
onButtonClicked: root.openNormalPicker()
|
||||
}
|
||||
}
|
||||
|
||||
// Critical Urgency Sound File
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.notifications.sounds-files-critical-label")
|
||||
description: I18n.tr("panels.notifications.sounds-files-critical-description")
|
||||
}
|
||||
|
||||
NTextInputButton {
|
||||
enabled: parent.enabled
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder")
|
||||
text: Settings.data.notifications?.sounds?.criticalSoundFile ?? ""
|
||||
buttonIcon: "folder-open"
|
||||
buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file")
|
||||
onInputTextChanged: text => Settings.data.notifications.sounds.criticalSoundFile = text
|
||||
onButtonClicked: root.openCriticalPicker()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Excluded Apps List
|
||||
ColumnLayout {
|
||||
enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.notifications.sounds-excluded-apps-label")
|
||||
description: I18n.tr("panels.notifications.sounds-excluded-apps-description")
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
enabled: parent.enabled
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("panels.notifications.sounds-excluded-apps-placeholder")
|
||||
text: Settings.data.notifications?.sounds?.excludedApps ?? ""
|
||||
onTextChanged: Settings.data.notifications.sounds.excludedApps = text
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NCheckbox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.notifications.toast-media-label")
|
||||
description: I18n.tr("panels.notifications.toast-media-description")
|
||||
checked: Settings.data.notifications.enableMediaToast
|
||||
onToggled: checked => Settings.data.notifications.enableMediaToast = checked
|
||||
}
|
||||
|
||||
NCheckbox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.notifications.toast-keyboard-label")
|
||||
description: I18n.tr("panels.notifications.toast-keyboard-description")
|
||||
checked: Settings.data.notifications.enableKeyboardLayoutToast
|
||||
onToggled: checked => Settings.data.notifications.enableKeyboardLayoutToast = checked
|
||||
}
|
||||
|
||||
NCheckbox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.notifications.toast-battery-label")
|
||||
description: I18n.tr("panels.notifications.toast-battery-description")
|
||||
checked: Settings.data.notifications.enableBatteryToast
|
||||
onToggled: checked => Settings.data.notifications.enableBatteryToast = checked
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
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: "types-volume"
|
||||
},
|
||||
{
|
||||
type: OSD.Type.InputVolume,
|
||||
key: "types-input-volume"
|
||||
},
|
||||
{
|
||||
type: OSD.Type.Brightness,
|
||||
key: "types-brightness"
|
||||
},
|
||||
{
|
||||
type: OSD.Type.LockKey,
|
||||
key: "types-lockkey"
|
||||
}
|
||||
]
|
||||
delegate: NCheckbox {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.osd." + modelData.key + "-label")
|
||||
description: I18n.tr("panels.osd." + 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
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("common.position")
|
||||
description: I18n.tr("panels.osd.location-description")
|
||||
model: [
|
||||
{
|
||||
"key": "top",
|
||||
"name": I18n.tr("positions.top-center")
|
||||
},
|
||||
{
|
||||
"key": "top_left",
|
||||
"name": I18n.tr("positions.top-left")
|
||||
},
|
||||
{
|
||||
"key": "top_right",
|
||||
"name": I18n.tr("positions.top-right")
|
||||
},
|
||||
{
|
||||
"key": "bottom",
|
||||
"name": I18n.tr("positions.bottom-center")
|
||||
},
|
||||
{
|
||||
"key": "bottom_left",
|
||||
"name": I18n.tr("positions.bottom-left")
|
||||
},
|
||||
{
|
||||
"key": "bottom_right",
|
||||
"name": I18n.tr("positions.bottom-right")
|
||||
},
|
||||
{
|
||||
"key": "left",
|
||||
"name": I18n.tr("positions.center-left")
|
||||
},
|
||||
{
|
||||
"key": "right",
|
||||
"name": I18n.tr("positions.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("panels.osd.enabled-label")
|
||||
description: I18n.tr("panels.osd.enabled-description")
|
||||
checked: Settings.data.osd.enabled
|
||||
defaultValue: Settings.getDefaultValue("osd.enabled")
|
||||
onToggled: checked => Settings.data.osd.enabled = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.osd.always-on-top-label")
|
||||
description: I18n.tr("panels.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("panels.osd.background-opacity-label")
|
||||
description: I18n.tr("panels.osd.background-opacity-description")
|
||||
from: 0
|
||||
to: 100
|
||||
stepSize: 1
|
||||
showReset: true
|
||||
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("panels.osd.duration-auto-hide-label")
|
||||
description: I18n.tr("panels.osd.duration-auto-hide-description")
|
||||
from: 500
|
||||
to: 5000
|
||||
stepSize: 100
|
||||
showReset: true
|
||||
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
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.osd.monitors-desc")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Quickshell.screens || []
|
||||
delegate: NCheckbox {
|
||||
Layout.fillWidth: true
|
||||
readonly property real compositorScale: {
|
||||
const info = CompositorService.displayScales[modelData.name];
|
||||
return (info && info.scale) ? info.scale : 1.0;
|
||||
}
|
||||
label: modelData.name || I18n.tr("common.unknown")
|
||||
description: {
|
||||
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,72 @@
|
||||
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
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.general")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.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
|
||||
}
|
||||
}
|
||||
}
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
import QtQuick
|
||||
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
|
||||
|
||||
property string pluginSearchText: ""
|
||||
property string selectedTag: ""
|
||||
property int tagsRefreshCounter: 0
|
||||
property int availablePluginsRefreshCounter: 0
|
||||
|
||||
// Pseudo tags for filtering
|
||||
readonly property var pseudoTags: ["official", "downloaded", "notDownloaded"]
|
||||
|
||||
readonly property var availableTags: {
|
||||
// Reference counter to force re-evaluation
|
||||
void (root.tagsRefreshCounter);
|
||||
var tags = {};
|
||||
var plugins = PluginService.availablePlugins || [];
|
||||
for (var i = 0; i < plugins.length; i++) {
|
||||
var pluginTags = plugins[i].tags || [];
|
||||
for (var j = 0; j < pluginTags.length; j++) {
|
||||
tags[pluginTags[j]] = true;
|
||||
}
|
||||
}
|
||||
return Object.keys(tags).sort();
|
||||
}
|
||||
|
||||
function stripAuthorEmail(author) {
|
||||
if (!author)
|
||||
return "";
|
||||
var lastBracket = author.lastIndexOf("<");
|
||||
if (lastBracket >= 0) {
|
||||
return author.substring(0, lastBracket).trim();
|
||||
}
|
||||
return author;
|
||||
}
|
||||
|
||||
// Tag filter chips in collapsible
|
||||
NTagFilter {
|
||||
tags: root.pseudoTags.concat(root.availableTags)
|
||||
selectedTag: root.selectedTag
|
||||
onSelectedTagChanged: root.selectedTag = selectedTag
|
||||
label: I18n.tr("panels.plugins.filter-tags-label")
|
||||
description: I18n.tr("panels.plugins.filter-tags-description")
|
||||
expanded: true
|
||||
|
||||
formatTag: function (tag) {
|
||||
if (tag === "")
|
||||
return I18n.tr("launcher.categories.all");
|
||||
if (tag === "official")
|
||||
return I18n.tr("common.official");
|
||||
if (tag === "downloaded")
|
||||
return I18n.tr("panels.plugins.filter-downloaded");
|
||||
if (tag === "notDownloaded")
|
||||
return I18n.tr("panels.plugins.filter-not-downloaded");
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
// Search input with refresh button
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NTextInput {
|
||||
placeholderText: I18n.tr("placeholders.search")
|
||||
inputIconName: "search"
|
||||
text: root.pluginSearchText
|
||||
onTextChanged: root.pluginSearchText = text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
tooltipText: I18n.tr("panels.plugins.refresh-tooltip")
|
||||
baseSize: Style.baseWidgetSize * 0.9
|
||||
onClicked: {
|
||||
PluginService.refreshAvailablePlugins();
|
||||
checkUpdatesTimer.restart();
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.refresh-refreshing"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Available plugins list
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
Repeater {
|
||||
id: availablePluginsRepeater
|
||||
|
||||
model: {
|
||||
// Reference counter to force re-evaluation when plugins are updated
|
||||
void (root.availablePluginsRefreshCounter);
|
||||
|
||||
var all = PluginService.availablePlugins || [];
|
||||
var filtered = [];
|
||||
|
||||
// Apply filter based on selectedTag
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
var plugin = all[i];
|
||||
var downloaded = plugin.downloaded || false;
|
||||
var pluginTags = plugin.tags || [];
|
||||
|
||||
if (root.selectedTag === "") {
|
||||
// "All" - no filter
|
||||
filtered.push(plugin);
|
||||
} else if (root.selectedTag === "official") {
|
||||
// Official (team-maintained) pseudo tag
|
||||
if (plugin.official === true)
|
||||
filtered.push(plugin);
|
||||
} else if (root.selectedTag === "downloaded") {
|
||||
// Downloaded pseudo tag
|
||||
if (downloaded)
|
||||
filtered.push(plugin);
|
||||
} else if (root.selectedTag === "notDownloaded") {
|
||||
// Not Downloaded pseudo tag
|
||||
if (!downloaded)
|
||||
filtered.push(plugin);
|
||||
} else {
|
||||
// Actual category tag
|
||||
if (pluginTags.indexOf(root.selectedTag) >= 0) {
|
||||
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"],
|
||||
"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;
|
||||
});
|
||||
}
|
||||
|
||||
// Move hello-world plugin to the end
|
||||
var helloWorldIndex = -1;
|
||||
for (var h = 0; h < filtered.length; h++) {
|
||||
if (filtered[h].id === "hello-world") {
|
||||
helloWorldIndex = h;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (helloWorldIndex >= 0) {
|
||||
var helloWorld = filtered.splice(helloWorldIndex, 1)[0];
|
||||
filtered.push(helloWorld);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
delegate: NBox {
|
||||
id: pluginBox
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.borderS
|
||||
Layout.rightMargin: Style.borderS
|
||||
implicitHeight: Math.round(contentColumn.implicitHeight + Style.margin2L)
|
||||
color: Color.mSurface
|
||||
|
||||
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.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.name
|
||||
color: Color.mPrimary
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Official badge (Noctalia Team maintained)
|
||||
Rectangle {
|
||||
visible: modelData.official === true
|
||||
color: Color.mSecondary
|
||||
radius: Style.radiusXS
|
||||
implicitWidth: officialBadgeRow.implicitWidth + Style.margin2S
|
||||
implicitHeight: officialBadgeRow.implicitHeight + Style.margin2XS
|
||||
|
||||
RowLayout {
|
||||
id: officialBadgeRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXS
|
||||
|
||||
NIcon {
|
||||
icon: "official-plugin"
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Color.mOnSecondary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("common.official")
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Open plugin page button
|
||||
NIconButton {
|
||||
icon: "external-link"
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
tooltipText: I18n.tr("panels.plugins.open-plugin-page")
|
||||
onClicked: {
|
||||
var sourceUrl = modelData.source?.url || "";
|
||||
Qt.openUrlExternally(sourceUrl && !PluginRegistry.isMainSource(sourceUrl) ? sourceUrl : "https://noctalia.dev/plugins/" + modelData.id + "/");
|
||||
}
|
||||
}
|
||||
|
||||
// Downloaded indicator
|
||||
NIcon {
|
||||
icon: "circle-check"
|
||||
pointSize: Style.baseWidgetSize * 0.5
|
||||
color: Color.mPrimary
|
||||
visible: modelData.downloaded === true
|
||||
}
|
||||
|
||||
// Install button (only shown when not downloaded and not installing)
|
||||
NIconButton {
|
||||
visible: modelData.downloaded === false && !PluginService.installingPlugins[modelData.id]
|
||||
icon: "download"
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
tooltipText: I18n.tr("common.install")
|
||||
onClicked: installPlugin(modelData)
|
||||
}
|
||||
|
||||
// Installing spinner
|
||||
NBusyIndicator {
|
||||
visible: !modelData.downloaded && (PluginService.installingPlugins[modelData.id] === true)
|
||||
size: Style.baseWidgetSize * 0.5
|
||||
running: visible
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
NText {
|
||||
visible: modelData.description
|
||||
text: modelData.description || ""
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Details row
|
||||
RowLayout {
|
||||
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
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: !!modelData.lastUpdated
|
||||
text: "•"
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: !!modelData.lastUpdated
|
||||
text: modelData.lastUpdated ? Time.formatRelativeTime(new Date(modelData.lastUpdated)) : ""
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NLabel {
|
||||
visible: availablePluginsRepeater.count === 0
|
||||
label: I18n.tr("panels.plugins.available-no-plugins-label")
|
||||
description: I18n.tr("panels.plugins.available-no-plugins-description")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to check for updates after refresh starts
|
||||
Timer {
|
||||
id: checkUpdatesTimer
|
||||
interval: 100
|
||||
onTriggered: {
|
||||
PluginService.checkForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
function installPlugin(pluginMetadata) {
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.installing", {
|
||||
"plugin": pluginMetadata.name
|
||||
}));
|
||||
|
||||
PluginService.installPlugin(pluginMetadata, false, function (success, error, registeredKey) {
|
||||
if (success) {
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.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("panels.plugins.title"), I18n.tr("panels.plugins.install-error", {
|
||||
"error": error || "Unknown error"
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Listen to plugin service signals
|
||||
Connections {
|
||||
target: PluginService
|
||||
|
||||
function onAvailablePluginsUpdated() {
|
||||
// Force tags and plugins model to re-evaluate
|
||||
root.tagsRefreshCounter++;
|
||||
root.availablePluginsRefreshCounter++;
|
||||
|
||||
// Manually trigger update check after a small delay to ensure all registries are loaded
|
||||
Qt.callLater(function () {
|
||||
PluginService.checkForUpdates();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+536
@@ -0,0 +1,536 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-update toggle
|
||||
NToggle {
|
||||
label: I18n.tr("panels.plugins.auto-update")
|
||||
description: I18n.tr("panels.plugins.auto-update-description")
|
||||
checked: Settings.data.plugins.autoUpdate
|
||||
onToggled: checked => Settings.data.plugins.autoUpdate = checked
|
||||
}
|
||||
|
||||
// Update notification toggle
|
||||
NToggle {
|
||||
label: I18n.tr("panels.plugins.notify-updates")
|
||||
description: I18n.tr("panels.plugins.notify-updates-description")
|
||||
checked: Settings.data.plugins.notifyUpdates
|
||||
onToggled: checked => Settings.data.plugins.notifyUpdates = checked
|
||||
}
|
||||
|
||||
// Check for updates button
|
||||
NButton {
|
||||
property bool isChecking: Object.keys(PluginService.activeFetches).length > 0
|
||||
|
||||
text: isChecking ? I18n.tr("panels.plugins.checking-for-updates") : I18n.tr("panels.plugins.check-for-updates")
|
||||
icon: "refresh"
|
||||
enabled: !isChecking
|
||||
visible: Object.keys(PluginService.pluginUpdates).length === 0
|
||||
Layout.fillWidth: true
|
||||
onClicked: PluginService.checkForUpdates()
|
||||
}
|
||||
|
||||
// Update All button
|
||||
NButton {
|
||||
property int updateCount: Object.keys(PluginService.pluginUpdates).length
|
||||
property bool isUpdating: false
|
||||
|
||||
text: I18n.tr("panels.plugins.update-all", {
|
||||
"count": updateCount
|
||||
})
|
||||
icon: "download"
|
||||
visible: (updateCount > 0)
|
||||
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("panels.plugins.title"), I18n.tr("panels.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.isFromOfficialRepo = parsed.isOfficial;
|
||||
if (!parsed.isOfficial) {
|
||||
pluginData.sourceName = PluginRegistry.getSourceNameByHash(parsed.sourceHash);
|
||||
}
|
||||
|
||||
// Look up "official" (team-maintained) status and lastUpdated from available plugins
|
||||
pluginData.official = false;
|
||||
pluginData.lastUpdated = null;
|
||||
var availablePlugins = PluginService.availablePlugins || [];
|
||||
for (var j = 0; j < availablePlugins.length; j++) {
|
||||
if (availablePlugins[j].id === manifest.id) {
|
||||
pluginData.official = availablePlugins[j].official === true;
|
||||
pluginData.lastUpdated = availablePlugins[j].lastUpdated || null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
plugins.push(pluginData);
|
||||
}
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
|
||||
delegate: NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.borderS
|
||||
Layout.rightMargin: Style.borderS
|
||||
implicitHeight: Math.round(contentColumn.implicitHeight + Style.margin2L)
|
||||
color: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginS
|
||||
|
||||
// Top row: icon, name, badge, spacer, action buttons
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
NIcon {
|
||||
icon: "plugin"
|
||||
pointSize: Style.fontSizeL
|
||||
color: PluginService.hasPluginError(modelData.compositeKey) ? Color.mError : Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.name
|
||||
color: Color.mPrimary
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Official badge (Noctalia Team maintained)
|
||||
Rectangle {
|
||||
visible: modelData.official === true
|
||||
color: Color.mSecondary
|
||||
radius: Style.radiusXS
|
||||
implicitWidth: officialBadgeRow.implicitWidth + Style.margin2S
|
||||
implicitHeight: officialBadgeRow.implicitHeight + Style.margin2XS
|
||||
|
||||
RowLayout {
|
||||
id: officialBadgeRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXS
|
||||
|
||||
NIcon {
|
||||
icon: "official-plugin"
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Color.mOnSecondary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("common.official")
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButtonHot {
|
||||
icon: "bug"
|
||||
hot: PluginService.isPluginHotReloadEnabled(modelData.id)
|
||||
tooltipText: PluginService.isPluginHotReloadEnabled(modelData.id) ? I18n.tr("panels.plugins.development-disable") : I18n.tr("panels.plugins.development-enable")
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
onClicked: PluginService.togglePluginHotReload(modelData.id)
|
||||
visible: Settings.isDebug
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: I18n.tr("panels.plugins.settings-tooltip")
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
visible: (modelData.entryPoints?.settings !== undefined)
|
||||
enabled: modelData.enabled
|
||||
onClicked: {
|
||||
pluginSettingsDialog.openPluginSettings(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "external-link"
|
||||
tooltipText: I18n.tr("panels.plugins.open-plugin-page")
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
visible: true
|
||||
onClicked: {
|
||||
var sourceUrl = PluginRegistry.getPluginSourceUrl(modelData.compositeKey) || "";
|
||||
Qt.openUrlExternally(sourceUrl && !PluginRegistry.isMainSource(sourceUrl) ? sourceUrl : "https://noctalia.dev/plugins/" + modelData.id);
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "trash"
|
||||
tooltipText: I18n.tr("common.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("panels.plugins.updating") : I18n.tr("common.update")
|
||||
icon: isUpdating ? "" : "download"
|
||||
visible: modelData.updateInfo !== undefined
|
||||
enabled: !isUpdating
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
fontSize: Style.fontSizeXXS
|
||||
fontWeight: Style.fontWeightMedium
|
||||
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("panels.plugins.title"), I18n.tr("panels.plugins.update-success", {
|
||||
"plugin": pname,
|
||||
"version": pversion
|
||||
}));
|
||||
} else {
|
||||
ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
NText {
|
||||
visible: modelData.description
|
||||
text: modelData.description || ""
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Details row
|
||||
RowLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: {
|
||||
if (modelData.updateInfo) {
|
||||
return I18n.tr("panels.plugins.update-version", {
|
||||
"current": modelData.version,
|
||||
"new": modelData.updateInfo.availableVersion
|
||||
});
|
||||
} else if (modelData.pendingUpdateInfo) {
|
||||
return I18n.tr("panels.plugins.update-pending", {
|
||||
"current": modelData.version,
|
||||
"new": modelData.pendingUpdateInfo.availableVersion,
|
||||
"required": modelData.pendingUpdateInfo.minNoctaliaVersion
|
||||
});
|
||||
}
|
||||
return "v" + modelData.version;
|
||||
}
|
||||
font.pointSize: Style.fontSizeXS
|
||||
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.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: stripAuthorEmail(modelData.author)
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
// Source indicator for plugins from non-official repos
|
||||
NText {
|
||||
visible: !modelData.isFromOfficialRepo
|
||||
text: "•"
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: !modelData.isFromOfficialRepo
|
||||
text: modelData.sourceName || I18n.tr("panels.plugins.source-custom")
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mTertiary
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: !!modelData.lastUpdated
|
||||
text: "•"
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: !!modelData.lastUpdated
|
||||
text: modelData.lastUpdated ? Time.formatRelativeTime(new Date(modelData.lastUpdated)) : ""
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NLabel {
|
||||
visible: PluginRegistry.getAllInstalledPluginIds().length === 0
|
||||
label: I18n.tr("panels.plugins.installed-no-plugins-label")
|
||||
description: I18n.tr("panels.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("panels.plugins.uninstall-dialog-title")
|
||||
description: I18n.tr("panels.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("common.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;
|
||||
|
||||
BarService.widgetsRevision++;
|
||||
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.uninstalling", {
|
||||
"plugin": pluginName
|
||||
}));
|
||||
|
||||
PluginService.uninstallPlugin(pluginId, function (success, error) {
|
||||
if (success) {
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.uninstall-success", {
|
||||
"plugin": pluginName
|
||||
}));
|
||||
} else {
|
||||
ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
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
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.installed")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.available")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.sources")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
InstalledSubTab {}
|
||||
AvailableSubTab {}
|
||||
SourcesSubTab {}
|
||||
}
|
||||
}
|
||||
+181
@@ -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.margin2L
|
||||
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("panels.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("panels.plugins.title"), I18n.tr("panels.plugins.refresh-refreshing"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add custom repository
|
||||
NButton {
|
||||
text: I18n.tr("panels.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("panels.plugins.sources-add-dialog-title")
|
||||
description: I18n.tr("panels.plugins.sources-add-dialog-description")
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: sourceNameInput
|
||||
label: I18n.tr("panels.plugins.sources-add-dialog-name")
|
||||
placeholderText: I18n.tr("panels.plugins.sources-add-dialog-name-placeholder")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: sourceUrlInput
|
||||
label: I18n.tr("panels.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("panels.plugins.title"), I18n.tr("panels.plugins.sources-add-dialog-success"));
|
||||
PluginService.refreshAvailablePlugins();
|
||||
addSourceDialog.close();
|
||||
sourceNameInput.text = "";
|
||||
sourceUrlInput.text = "";
|
||||
} else {
|
||||
ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.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 || [];
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property list<var> cardsModel: []
|
||||
property list<var> cardsDefault: [
|
||||
{
|
||||
"id": "calendar-header-card",
|
||||
"text": I18n.tr("panels.location.calendar-header-label"),
|
||||
"enabled": true,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "calendar-month-card",
|
||||
"text": I18n.tr("panels.location.calendar-month-label"),
|
||||
"enabled": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "weather-card",
|
||||
"text": I18n.tr("common.weather"),
|
||||
"enabled": true,
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
|
||||
function saveCards() {
|
||||
var toSave = [];
|
||||
for (var i = 0; i < cardsModel.length; i++) {
|
||||
toSave.push({
|
||||
"id": cardsModel[i].id,
|
||||
"enabled": cardsModel[i].enabled
|
||||
});
|
||||
}
|
||||
Settings.data.calendar.cards = toSave;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Starts empty
|
||||
cardsModel = [];
|
||||
|
||||
// Add the cards available in settings
|
||||
for (var i = 0; i < Settings.data.calendar.cards.length; i++) {
|
||||
const settingCard = Settings.data.calendar.cards[i];
|
||||
|
||||
for (var j = 0; j < cardsDefault.length; j++) {
|
||||
if (settingCard.id === cardsDefault[j].id) {
|
||||
var card = cardsDefault[j];
|
||||
card.enabled = settingCard.enabled;
|
||||
// Auto-disable weather card if weather is disabled
|
||||
if (card.id === "weather-card" && !Settings.data.location.weatherEnabled) {
|
||||
card.enabled = false;
|
||||
}
|
||||
cardsModel.push(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any missing cards from default
|
||||
for (var i = 0; i < cardsDefault.length; i++) {
|
||||
var found = false;
|
||||
for (var j = 0; j < cardsModel.length; j++) {
|
||||
if (cardsModel[j].id === cardsDefault[i].id) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
var card = cardsDefault[i];
|
||||
// Auto-disable weather card if weather is disabled
|
||||
if (card.id === "weather-card" && !Settings.data.location.weatherEnabled) {
|
||||
card.enabled = false;
|
||||
}
|
||||
cardsModel.push(card);
|
||||
}
|
||||
}
|
||||
|
||||
saveCards();
|
||||
}
|
||||
|
||||
// Clock style section
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.location.date-time-use-analog-label")
|
||||
description: I18n.tr("panels.location.date-time-use-analog-description")
|
||||
checked: Settings.data.location.analogClockInCalendar
|
||||
onToggled: checked => Settings.data.location.analogClockInCalendar = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.location.date-time-week-numbers-label")
|
||||
description: I18n.tr("panels.location.date-time-week-numbers-description")
|
||||
checked: Settings.data.location.showWeekNumberInCalendar
|
||||
onToggled: checked => Settings.data.location.showWeekNumberInCalendar = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.location.date-time-show-events-label")
|
||||
description: I18n.tr("panels.location.date-time-show-events-description")
|
||||
checked: Settings.data.location.showCalendarEvents
|
||||
onToggled: checked => Settings.data.location.showCalendarEvents = checked
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Calendar Cards Management Section
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
Connections {
|
||||
target: Settings.data.location
|
||||
function onWeatherEnabledChanged() {
|
||||
// Auto-disable weather card when weather is disabled
|
||||
var newModel = cardsModel.slice();
|
||||
for (var i = 0; i < newModel.length; i++) {
|
||||
if (newModel[i].id === "weather-card") {
|
||||
newModel[i] = Object.assign({}, newModel[i], {
|
||||
"enabled": Settings.data.location.weatherEnabled
|
||||
});
|
||||
cardsModel = newModel;
|
||||
saveCards();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NReorderCheckboxes {
|
||||
Layout.fillWidth: true
|
||||
model: cardsModel
|
||||
disabledIds: Settings.data.location.weatherEnabled ? [] : ["weather-card"]
|
||||
onItemToggled: function (index, enabled) {
|
||||
var newModel = cardsModel.slice();
|
||||
newModel[index] = Object.assign({}, newModel[index], {
|
||||
"enabled": enabled
|
||||
});
|
||||
cardsModel = newModel;
|
||||
saveCards();
|
||||
}
|
||||
onItemsReordered: function (fromIndex, toIndex) {
|
||||
var newModel = cardsModel.slice();
|
||||
var item = newModel.splice(fromIndex, 1)[0];
|
||||
newModel.splice(toIndex, 0, item);
|
||||
cardsModel = newModel;
|
||||
saveCards();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
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("panels.location.date-time-12hour-format-label")
|
||||
description: I18n.tr("panels.location.date-time-12hour-format-description")
|
||||
checked: Settings.data.location.use12hourFormat
|
||||
onToggled: checked => Settings.data.location.use12hourFormat = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.location.date-time-first-day-of-week-label")
|
||||
description: I18n.tr("panels.location.date-time-first-day-of-week-description")
|
||||
currentKey: Settings.data.location.firstDayOfWeek.toString()
|
||||
minimumWidth: 260 * Style.uiScaleRatio
|
||||
model: [
|
||||
{
|
||||
"key": "-1",
|
||||
"name": I18n.tr("panels.location.date-time-first-day-of-week-automatic")
|
||||
},
|
||||
{
|
||||
"key": "6",
|
||||
"name": I18n.locale.dayName(6, Locale.LongFormat).trim()
|
||||
} // Saturday
|
||||
,
|
||||
{
|
||||
"key": "0",
|
||||
"name": I18n.locale.dayName(0, Locale.LongFormat).trim()
|
||||
} // Sunday
|
||||
,
|
||||
{
|
||||
"key": "1",
|
||||
"name": I18n.locale.dayName(1, Locale.LongFormat).trim()
|
||||
} // Monday
|
||||
]
|
||||
onSelected: key => Settings.data.location.firstDayOfWeek = parseInt(key)
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Location
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Language
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.language-select-label")
|
||||
description: I18n.tr("panels.general.language-select-description")
|
||||
defaultValue: Settings.getDefaultValue("general.language")
|
||||
model: [
|
||||
{
|
||||
"key": "",
|
||||
"name": I18n.tr("panels.general.language-select-auto-detect") + " (" + I18n.systemDetectedLangCode + ")"
|
||||
}
|
||||
].concat(I18n.availableLanguages.map(function (langCode) {
|
||||
return {
|
||||
"key": langCode,
|
||||
"name": langCode
|
||||
};
|
||||
}))
|
||||
currentKey: Settings.data.general.language
|
||||
settingsPath: "general.language"
|
||||
onSelected: key => {
|
||||
// Need to change language on next frame using "callLater" or it will pull the rug below our feet: the NComboBox would be rebuilt immediately before it can close properly.
|
||||
Qt.callLater(() => {
|
||||
Settings.data.general.language = key;
|
||||
if (key === "") {
|
||||
I18n.detectLanguage(); // Re-detect system language if "Automatic" is selected
|
||||
} else {
|
||||
I18n.setLanguage(key); // Set specific language
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Location
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
// Auto-locate
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.location.auto-locate-label")
|
||||
description: I18n.tr("panels.location.auto-locate-description")
|
||||
checked: Settings.data.location.autoLocate
|
||||
onToggled: checked => Settings.data.location.autoLocate = checked
|
||||
defaultValue: Settings.getDefaultValue("location.autoLocate")
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("panels.location.geolocate-now-button")
|
||||
icon: "current-location"
|
||||
enabled: !LocationService.isFetchingWeather
|
||||
onClicked: LocationService.geolocateAndApply()
|
||||
}
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
visible: !Settings.data.location.autoLocate
|
||||
Layout.maximumWidth: root.width / 2
|
||||
label: I18n.tr("panels.location.location-search-label")
|
||||
description: I18n.tr("panels.location.location-search-description")
|
||||
text: Settings.data.location.name
|
||||
placeholderText: I18n.tr("panels.location.location-search-placeholder")
|
||||
onEditingFinished: {
|
||||
// Verify the location has really changed to avoid extra resets
|
||||
var newLocation = text.trim();
|
||||
if (newLocation != Settings.data.location.name) {
|
||||
Settings.data.location.name = newLocation;
|
||||
LocationService.resetWeather();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: LocationService.coordinatesReady ? I18n.tr("system.location-display", {
|
||||
"name": LocationService.stableName,
|
||||
"coordinates": LocationService.displayCoordinates
|
||||
}) : ""
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
font.italic: true
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.location.weather-enabled-label")
|
||||
description: I18n.tr("panels.location.weather-enabled-description")
|
||||
checked: Settings.data.location.weatherEnabled
|
||||
onToggled: checked => Settings.data.location.weatherEnabled = checked
|
||||
defaultValue: Settings.getDefaultValue("location.weatherEnabled")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.location.weather-fahrenheit-label")
|
||||
description: I18n.tr("panels.location.weather-fahrenheit-description")
|
||||
checked: Settings.data.location.useFahrenheit
|
||||
onToggled: checked => Settings.data.location.useFahrenheit = checked
|
||||
enabled: Settings.data.location.weatherEnabled
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.location.weather-show-effects-label")
|
||||
description: I18n.tr("panels.location.weather-show-effects-description")
|
||||
checked: Settings.data.location.weatherShowEffects
|
||||
onToggled: checked => Settings.data.location.weatherShowEffects = checked
|
||||
enabled: Settings.data.location.weatherEnabled
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.location.weather-talia-mascot-always-label")
|
||||
description: I18n.tr("panels.location.weather-talia-mascot-always-description")
|
||||
checked: Settings.data.location.weatherTaliaMascotAlways
|
||||
onToggled: checked => Settings.data.location.weatherTaliaMascotAlways = checked
|
||||
enabled: Settings.data.location.weatherEnabled
|
||||
defaultValue: Settings.getDefaultValue("location.weatherTaliaMascotAlways")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.location.weather-hide-city-label")
|
||||
description: I18n.tr("panels.location.weather-hide-city-description")
|
||||
checked: Settings.data.location.hideWeatherCityName
|
||||
onToggled: checked => Settings.data.location.hideWeatherCityName = checked
|
||||
enabled: Settings.data.location.weatherEnabled
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.location.weather-hide-timezone-label")
|
||||
description: I18n.tr("panels.location.weather-hide-timezone-description")
|
||||
checked: Settings.data.location.hideWeatherTimezone
|
||||
onToggled: checked => Settings.data.location.hideWeatherTimezone = checked
|
||||
enabled: Settings.data.location.weatherEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
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
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.location")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.date")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.calendar-panel")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
LocationSubTab {}
|
||||
DateSubTab {}
|
||||
ClockPanelSubTab {}
|
||||
}
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
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
|
||||
|
||||
NListView {
|
||||
id: listView
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginS
|
||||
interactive: false
|
||||
reserveScrollbarSpace: false
|
||||
model: root.entriesModel
|
||||
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
width: listView.availableWidth
|
||||
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 (only shown when global countdown is enabled)
|
||||
NIconButtonHot {
|
||||
visible: Settings.data.sessionMenu.enableCountdown
|
||||
icon: "clock"
|
||||
hot: modelData.countdownEnabled !== undefined ? modelData.countdownEnabled : true
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
tooltipText: I18n.tr("common.countdown")
|
||||
onClicked: root.updateEntry(delegateItem.index, {
|
||||
"countdownEnabled": !(modelData.countdownEnabled !== undefined ? modelData.countdownEnabled : true)
|
||||
})
|
||||
}
|
||||
|
||||
// Settings button (cogwheel)
|
||||
NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: I18n.tr("panels.session-menu.entry-settings-tooltip")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
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("panels.session-menu.large-buttons-style-label")
|
||||
description: I18n.tr("panels.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("panels.session-menu.large-buttons-layout-label")
|
||||
description: I18n.tr("panels.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
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("common.position")
|
||||
description: I18n.tr("panels.session-menu.position-description")
|
||||
Layout.fillWidth: true
|
||||
model: [
|
||||
{
|
||||
"key": "center",
|
||||
"name": I18n.tr("positions.center")
|
||||
},
|
||||
{
|
||||
"key": "top_center",
|
||||
"name": I18n.tr("positions.top-center")
|
||||
},
|
||||
{
|
||||
"key": "top_left",
|
||||
"name": I18n.tr("positions.top-left")
|
||||
},
|
||||
{
|
||||
"key": "top_right",
|
||||
"name": I18n.tr("positions.top-right")
|
||||
},
|
||||
{
|
||||
"key": "bottom_center",
|
||||
"name": I18n.tr("positions.bottom-center")
|
||||
},
|
||||
{
|
||||
"key": "bottom_left",
|
||||
"name": I18n.tr("positions.bottom-left")
|
||||
},
|
||||
{
|
||||
"key": "bottom_right",
|
||||
"name": I18n.tr("positions.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("panels.session-menu.show-header-label")
|
||||
description: I18n.tr("panels.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("panels.session-menu.show-keybinds-label")
|
||||
description: I18n.tr("panels.session-menu.show-keybinds-description")
|
||||
checked: Settings.data.sessionMenu.showKeybinds
|
||||
onToggled: checked => Settings.data.sessionMenu.showKeybinds = checked
|
||||
defaultValue: Settings.getDefaultValue("sessionMenu.showKeybinds")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.session-menu.enable-countdown-label")
|
||||
description: I18n.tr("panels.session-menu.enable-countdown-description")
|
||||
checked: Settings.data.sessionMenu.enableCountdown
|
||||
onToggled: checked => Settings.data.sessionMenu.enableCountdown = checked
|
||||
defaultValue: Settings.getDefaultValue("sessionMenu.enableCountdown")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
visible: Settings.data.sessionMenu.enableCountdown
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.session-menu.countdown-duration-label")
|
||||
description: I18n.tr("panels.session-menu.countdown-duration-description")
|
||||
from: 1000
|
||||
to: 30000
|
||||
stepSize: 1000
|
||||
showReset: true
|
||||
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")
|
||||
}
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
// Session Menu Entry Settings Dialog Component
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property int entryIndex: -1
|
||||
property var entryData: null
|
||||
property string entryId: ""
|
||||
property string entryText: ""
|
||||
property string keybindInputText: ""
|
||||
|
||||
signal updateEntryProperties(int index, var properties)
|
||||
|
||||
// Default commands mapping
|
||||
readonly property var defaultCommands: {
|
||||
"lock": I18n.tr("panels.session-menu.entry-settings-default-command-lock"),
|
||||
"suspend": "systemctl suspend || loginctl suspend",
|
||||
"hibernate": "systemctl hibernate || loginctl hibernate",
|
||||
"reboot": "systemctl reboot || loginctl reboot",
|
||||
"rebootToUefi": "systemctl reboot --firmware-setup || loginctl reboot --firmware-setup",
|
||||
"logout": I18n.tr("panels.session-menu.entry-settings-default-command-logout"),
|
||||
"shutdown": "systemctl poweroff || loginctl poweroff",
|
||||
"userspaceReboot": "systemctl soft-reboot"
|
||||
}
|
||||
|
||||
readonly property string defaultCommand: defaultCommands[entryId] || ""
|
||||
|
||||
width: Math.max(content.implicitWidth + padding * 2, 500)
|
||||
height: content.implicitHeight + padding * 2
|
||||
padding: Style.marginXL
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
dim: false
|
||||
anchors.centerIn: parent
|
||||
|
||||
onOpened: {
|
||||
// Load command when popup opens
|
||||
if (entryData) {
|
||||
commandInput.text = entryData.command || "";
|
||||
keybindInputText = entryData.keybind || "";
|
||||
}
|
||||
// Request focus to ensure keyboard input works
|
||||
forceActiveFocus();
|
||||
}
|
||||
|
||||
function save() {
|
||||
root.updateEntryProperties(root.entryIndex, {
|
||||
"command": commandInput.text,
|
||||
"keybind": keybindInputText
|
||||
});
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
id: bgRect
|
||||
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderM
|
||||
}
|
||||
|
||||
contentItem: FocusScope {
|
||||
id: focusScope
|
||||
focus: true
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginM
|
||||
|
||||
// Title
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.session-menu.entry-settings-title", {
|
||||
"entry": root.entryText
|
||||
})
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
onClicked: {
|
||||
root.save();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Color.mOutline
|
||||
}
|
||||
|
||||
// Command input
|
||||
NTextInput {
|
||||
id: commandInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("common.command")
|
||||
description: I18n.tr("panels.session-menu.entry-settings-command-description")
|
||||
placeholderText: I18n.tr("panels.session-menu.entry-settings-command-placeholder")
|
||||
onTextChanged: root.save()
|
||||
}
|
||||
|
||||
// Default command info
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXS
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.session-menu.entry-settings-default-info-label")
|
||||
description: I18n.tr("panels.session-menu.entry-settings-default-info-description")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Default command display
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: defaultCommandText.implicitHeight + Style.margin2M
|
||||
radius: Style.radiusM
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: "info"
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
|
||||
NText {
|
||||
id: defaultCommandText
|
||||
Layout.fillWidth: true
|
||||
text: root.defaultCommand
|
||||
color: Color.mOnSurfaceVariant
|
||||
font.family: "monospace"
|
||||
font.pointSize: Style.fontSizeS
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NKeybindRecorder {
|
||||
id: keybindRecorder
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("common.keybind")
|
||||
description: I18n.tr("placeholders.keybind-recording")
|
||||
allowEmpty: true
|
||||
maxKeybinds: 1
|
||||
requireModifierForNormalKeys: false
|
||||
currentKeybinds: keybindInputText ? [keybindInputText] : []
|
||||
settingsPath: "sessionMenu.powerOptions[" + root.entryIndex + "].keybind"
|
||||
onKeybindsChanged: newKeybinds => {
|
||||
keybindInputText = newKeybinds.length > 0 ? newKeybinds[0] : "";
|
||||
root.save();
|
||||
}
|
||||
}
|
||||
|
||||
// Hidden property to store the text since NKeybindRecorder manages its own state
|
||||
// but we need to initialize it and read from it
|
||||
|
||||
// Bottom spacer to maintain padding
|
||||
Item {
|
||||
Layout.preferredHeight: Style.marginS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
property var _activeDialog: null
|
||||
property list<var> entriesModel: []
|
||||
property list<var> entriesDefault: [
|
||||
{
|
||||
"id": "lock",
|
||||
"text": I18n.tr("common.lock"),
|
||||
"enabled": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "suspend",
|
||||
"text": I18n.tr("common.suspend"),
|
||||
"enabled": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "hibernate",
|
||||
"text": I18n.tr("common.hibernate"),
|
||||
"enabled": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "reboot",
|
||||
"text": I18n.tr("common.reboot"),
|
||||
"enabled": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "userspaceReboot",
|
||||
"text": I18n.tr("common.userspace-reboot"),
|
||||
"enabled": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "rebootToUefi",
|
||||
"text": I18n.tr("common.reboot-to-uefi"),
|
||||
"enabled": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "logout",
|
||||
"text": I18n.tr("common.logout"),
|
||||
"enabled": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "shutdown",
|
||||
"text": I18n.tr("common.shutdown"),
|
||||
"enabled": true,
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
|
||||
function saveEntries() {
|
||||
var toSave = [];
|
||||
var enabledNumber = 1;
|
||||
|
||||
for (var i = 0; i < entriesModel.length; i++) {
|
||||
var keybind = entriesModel[i].keybind || "";
|
||||
|
||||
if (entriesModel[i].enabled) {
|
||||
// For enabled entries with numeric or empty keybinds, assign sequential number
|
||||
if (keybind === "" || /^\d+$/.test(keybind)) {
|
||||
keybind = String(enabledNumber);
|
||||
}
|
||||
enabledNumber++;
|
||||
} else {
|
||||
// For disabled entries with numeric keybinds, clear them
|
||||
if (/^\d+$/.test(keybind)) {
|
||||
keybind = "";
|
||||
}
|
||||
}
|
||||
|
||||
toSave.push({
|
||||
"action": entriesModel[i].id,
|
||||
"enabled": entriesModel[i].enabled,
|
||||
"countdownEnabled": entriesModel[i].countdownEnabled !== undefined ? entriesModel[i].countdownEnabled : true,
|
||||
"command": entriesModel[i].command || "",
|
||||
"keybind": keybind
|
||||
});
|
||||
}
|
||||
|
||||
// Update local model with renumbered keybinds
|
||||
var newModel = [];
|
||||
for (var i = 0; i < entriesModel.length; i++) {
|
||||
newModel.push(Object.assign({}, entriesModel[i], {
|
||||
"keybind": toSave[i].keybind
|
||||
}));
|
||||
}
|
||||
entriesModel = newModel;
|
||||
|
||||
Settings.data.sessionMenu.powerOptions = toSave;
|
||||
}
|
||||
|
||||
function updateEntry(idx, properties) {
|
||||
var newModel = entriesModel.slice();
|
||||
newModel[idx] = Object.assign({}, newModel[idx], properties);
|
||||
entriesModel = newModel;
|
||||
saveEntries();
|
||||
}
|
||||
|
||||
function reorderEntries(fromIndex, toIndex) {
|
||||
var newModel = entriesModel.slice();
|
||||
var item = newModel.splice(fromIndex, 1)[0];
|
||||
newModel.splice(toIndex, 0, item);
|
||||
entriesModel = newModel;
|
||||
saveEntries();
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (_activeDialog && _activeDialog.close) {
|
||||
var dialog = _activeDialog;
|
||||
_activeDialog = null;
|
||||
dialog.close();
|
||||
dialog.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
function openEntrySettingsDialog(index) {
|
||||
if (index < 0 || index >= entriesModel.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = entriesModel[index];
|
||||
var component = Qt.createComponent(Quickshell.shellDir + "/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuEntrySettingsDialog.qml");
|
||||
|
||||
function instantiateAndOpen() {
|
||||
if (root._activeDialog) {
|
||||
root._activeDialog.close();
|
||||
root._activeDialog.destroy();
|
||||
root._activeDialog = null;
|
||||
}
|
||||
|
||||
var dialog = component.createObject(Overlay.overlay, {
|
||||
"entryIndex": index,
|
||||
"entryData": entry,
|
||||
"entryId": entry.id,
|
||||
"entryText": entry.text
|
||||
});
|
||||
|
||||
if (dialog) {
|
||||
root._activeDialog = dialog;
|
||||
dialog.updateEntryProperties.connect((idx, properties) => {
|
||||
root.updateEntry(idx, properties);
|
||||
});
|
||||
dialog.closed.connect(() => {
|
||||
if (root._activeDialog === dialog) {
|
||||
root._activeDialog = null;
|
||||
dialog.destroy();
|
||||
}
|
||||
});
|
||||
dialog.open();
|
||||
} else {
|
||||
Logger.e("SessionMenuTab", "Failed to create entry settings dialog");
|
||||
}
|
||||
}
|
||||
|
||||
if (component.status === Component.Ready) {
|
||||
instantiateAndOpen();
|
||||
} else if (component.status === Component.Error) {
|
||||
Logger.e("SessionMenuTab", "Error loading entry settings dialog:", component.errorString());
|
||||
} else {
|
||||
component.statusChanged.connect(function () {
|
||||
if (component.status === Component.Ready) {
|
||||
instantiateAndOpen();
|
||||
} else if (component.status === Component.Error) {
|
||||
Logger.e("SessionMenuTab", "Error loading entry settings dialog:", component.errorString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
entriesModel = [];
|
||||
|
||||
// Add the entries available in settings
|
||||
for (var i = 0; i < Settings.data.sessionMenu.powerOptions.length; i++) {
|
||||
const settingEntry = Settings.data.sessionMenu.powerOptions[i];
|
||||
|
||||
for (var j = 0; j < entriesDefault.length; j++) {
|
||||
if (settingEntry.action === entriesDefault[j].id) {
|
||||
var entry = entriesDefault[j];
|
||||
entry.enabled = settingEntry.enabled;
|
||||
// Default countdownEnabled to true for backward compatibility
|
||||
entry.countdownEnabled = settingEntry.countdownEnabled !== undefined ? settingEntry.countdownEnabled : true;
|
||||
// Load custom command if defined
|
||||
entry.command = settingEntry.command || "";
|
||||
entry.keybind = settingEntry.keybind || "";
|
||||
entriesModel.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any missing entries from default
|
||||
for (var i = 0; i < entriesDefault.length; i++) {
|
||||
var found = false;
|
||||
for (var j = 0; j < entriesModel.length; j++) {
|
||||
if (entriesModel[j].id === entriesDefault[i].id) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
var entry = entriesDefault[i];
|
||||
// Default countdownEnabled to true for new entries
|
||||
entry.countdownEnabled = true;
|
||||
// Default command to empty string for new entries
|
||||
entry.command = "";
|
||||
entry.keybind = "";
|
||||
entriesModel.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
saveEntries();
|
||||
}
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.general")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.actions")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
GeneralSubTab {}
|
||||
ActionsSubTab {
|
||||
entriesModel: root.entriesModel
|
||||
updateEntry: root.updateEntry
|
||||
reorderEntries: root.reorderEntries
|
||||
openEntrySettingsDialog: root.openEntrySettingsDialog
|
||||
}
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
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 screen
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginM
|
||||
label: I18n.tr("panels.system-monitor.enable-dgpu-monitoring-label")
|
||||
description: I18n.tr("panels.system-monitor.enable-dgpu-monitoring-description")
|
||||
checked: Settings.data.systemMonitor.enableDgpuMonitoring
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.enableDgpuMonitoring")
|
||||
onToggled: checked => Settings.data.systemMonitor.enableDgpuMonitoring = checked
|
||||
}
|
||||
|
||||
// Colors Section
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.system-monitor.use-custom-highlight-colors-label")
|
||||
description: I18n.tr("panels.system-monitor.use-custom-highlight-colors-description")
|
||||
checked: Settings.data.systemMonitor.useCustomColors
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.useCustomColors")
|
||||
onToggled: checked => {
|
||||
// If enabling custom colors and no custom color is saved, persist current theme colors
|
||||
if (checked) {
|
||||
if (!Settings.data.systemMonitor.warningColor || Settings.data.systemMonitor.warningColor === "") {
|
||||
Settings.data.systemMonitor.warningColor = Color.mTertiary.toString();
|
||||
}
|
||||
if (!Settings.data.systemMonitor.criticalColor || Settings.data.systemMonitor.criticalColor === "") {
|
||||
Settings.data.systemMonitor.criticalColor = Color.mError.toString();
|
||||
}
|
||||
}
|
||||
Settings.data.systemMonitor.useCustomColors = checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXL
|
||||
visible: Settings.data.systemMonitor.useCustomColors
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.system-monitor.warning-color-label")
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
|
||||
NColorPicker {
|
||||
screen: root.screen
|
||||
Layout.preferredWidth: Style.sliderWidth
|
||||
Layout.preferredHeight: Style.baseWidgetSize
|
||||
enabled: Settings.data.systemMonitor.useCustomColors
|
||||
selectedColor: Settings.data.systemMonitor.warningColor || Color.mTertiary
|
||||
onColorSelected: color => Settings.data.systemMonitor.warningColor = color
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.system-monitor.critical-color-label")
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
|
||||
NColorPicker {
|
||||
screen: root.screen
|
||||
Layout.preferredWidth: Style.sliderWidth
|
||||
Layout.preferredHeight: Style.baseWidgetSize
|
||||
enabled: Settings.data.systemMonitor.useCustomColors
|
||||
selectedColor: Settings.data.systemMonitor.criticalColor || Color.mError
|
||||
onColorSelected: color => Settings.data.systemMonitor.criticalColor = color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
label: I18n.tr("panels.system-monitor.external-monitor-label")
|
||||
description: I18n.tr("panels.system-monitor.external-monitor-description")
|
||||
placeholderText: I18n.tr("panels.system-monitor.external-monitor-placeholder")
|
||||
text: Settings.data.systemMonitor.externalMonitor
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.externalMonitor")
|
||||
onTextChanged: Settings.data.systemMonitor.externalMonitor = text
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
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("panels.system.noctalia-performance-disable-wallpaper-label")
|
||||
description: I18n.tr("panels.system.noctalia-performance-disable-wallpaper-description")
|
||||
checked: !Settings.data.noctaliaPerformance.disableWallpaper
|
||||
defaultValue: !Settings.getDefaultValue("noctaliaPerformance.disableWallpaper")
|
||||
onToggled: checked => Settings.data.noctaliaPerformance.disableWallpaper = !checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.system.noctalia-performance-disable-desktop-widgets-label")
|
||||
description: I18n.tr("panels.system.noctalia-performance-disable-desktop-widgets-description")
|
||||
checked: !Settings.data.noctaliaPerformance.disableDesktopWidgets
|
||||
defaultValue: !Settings.getDefaultValue("noctaliaPerformance.disableDesktopWidgets")
|
||||
onToggled: checked => Settings.data.noctaliaPerformance.disableDesktopWidgets = !checked
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
property var screen
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("system-monitor.title")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.thresholds")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.performance")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
GeneralSubTab {
|
||||
screen: root.screen
|
||||
}
|
||||
ThresholdsSubTab {}
|
||||
PerformanceSubTab {}
|
||||
}
|
||||
}
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
Layout.fillWidth: true
|
||||
description: I18n.tr("panels.system-monitor.thresholds-section-description")
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginM
|
||||
columns: 3
|
||||
columnSpacing: Style.marginM
|
||||
rowSpacing: Style.marginM
|
||||
|
||||
// Header row
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: I18n.tr("panels.system-monitor.threshold-warning")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: I18n.tr("panels.system-monitor.threshold-critical")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
// CPU Usage
|
||||
NText {
|
||||
text: I18n.tr("bar.system-monitor.cpu-usage-label")
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: 0
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.cpuWarningThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.cpuWarningThreshold")
|
||||
suffix: "%"
|
||||
onValueChanged: {
|
||||
Settings.data.systemMonitor.cpuWarningThreshold = value;
|
||||
if (Settings.data.systemMonitor.cpuCriticalThreshold < value) {
|
||||
Settings.data.systemMonitor.cpuCriticalThreshold = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: Settings.data.systemMonitor.cpuWarningThreshold
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.cpuCriticalThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.cpuCriticalThreshold")
|
||||
suffix: "%"
|
||||
onValueChanged: Settings.data.systemMonitor.cpuCriticalThreshold = value
|
||||
}
|
||||
|
||||
// CPU Temperature
|
||||
NText {
|
||||
text: I18n.tr("bar.system-monitor.cpu-temperature-label")
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: 0
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.tempWarningThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.tempWarningThreshold")
|
||||
suffix: "°C"
|
||||
onValueChanged: {
|
||||
Settings.data.systemMonitor.tempWarningThreshold = value;
|
||||
if (Settings.data.systemMonitor.tempCriticalThreshold < value) {
|
||||
Settings.data.systemMonitor.tempCriticalThreshold = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: Settings.data.systemMonitor.tempWarningThreshold
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.tempCriticalThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.tempCriticalThreshold")
|
||||
suffix: "°C"
|
||||
onValueChanged: Settings.data.systemMonitor.tempCriticalThreshold = value
|
||||
}
|
||||
|
||||
// GPU Temperature
|
||||
NText {
|
||||
visible: SystemStatService.gpuAvailable
|
||||
text: I18n.tr("panels.system-monitor.gpu-section-label")
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
visible: SystemStatService.gpuAvailable
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: 0
|
||||
to: 120
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.gpuWarningThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.gpuWarningThreshold")
|
||||
suffix: "°C"
|
||||
onValueChanged: {
|
||||
Settings.data.systemMonitor.gpuWarningThreshold = value;
|
||||
if (Settings.data.systemMonitor.gpuCriticalThreshold < value) {
|
||||
Settings.data.systemMonitor.gpuCriticalThreshold = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
visible: SystemStatService.gpuAvailable
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: Settings.data.systemMonitor.gpuWarningThreshold
|
||||
to: 120
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.gpuCriticalThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.gpuCriticalThreshold")
|
||||
suffix: "°C"
|
||||
onValueChanged: Settings.data.systemMonitor.gpuCriticalThreshold = value
|
||||
}
|
||||
|
||||
// Memory Usage
|
||||
NText {
|
||||
text: I18n.tr("bar.system-monitor.memory-usage-label")
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: 0
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.memWarningThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.memWarningThreshold")
|
||||
suffix: "%"
|
||||
onValueChanged: {
|
||||
Settings.data.systemMonitor.memWarningThreshold = value;
|
||||
if (Settings.data.systemMonitor.memCriticalThreshold < value) {
|
||||
Settings.data.systemMonitor.memCriticalThreshold = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: Settings.data.systemMonitor.memWarningThreshold
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.memCriticalThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.memCriticalThreshold")
|
||||
suffix: "%"
|
||||
onValueChanged: Settings.data.systemMonitor.memCriticalThreshold = value
|
||||
}
|
||||
|
||||
// Swap Usage
|
||||
NText {
|
||||
text: I18n.tr("bar.system-monitor.swap-usage-label")
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: 0
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.swapWarningThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.swapWarningThreshold")
|
||||
suffix: "%"
|
||||
onValueChanged: {
|
||||
Settings.data.systemMonitor.swapWarningThreshold = value;
|
||||
if (Settings.data.systemMonitor.swapCriticalThreshold < value) {
|
||||
Settings.data.systemMonitor.swapCriticalThreshold = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: Settings.data.systemMonitor.swapWarningThreshold
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.swapCriticalThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.swapCriticalThreshold")
|
||||
suffix: "%"
|
||||
onValueChanged: Settings.data.systemMonitor.swapCriticalThreshold = value
|
||||
}
|
||||
|
||||
// Disk Usage
|
||||
NText {
|
||||
text: I18n.tr("panels.system-monitor.disk-section-label")
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: 0
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.diskWarningThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.diskWarningThreshold")
|
||||
suffix: "%"
|
||||
onValueChanged: {
|
||||
Settings.data.systemMonitor.diskWarningThreshold = value;
|
||||
if (Settings.data.systemMonitor.diskCriticalThreshold < value) {
|
||||
Settings.data.systemMonitor.diskCriticalThreshold = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: Settings.data.systemMonitor.diskWarningThreshold
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.diskCriticalThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.diskCriticalThreshold")
|
||||
suffix: "%"
|
||||
onValueChanged: Settings.data.systemMonitor.diskCriticalThreshold = value
|
||||
}
|
||||
|
||||
// Disk Available
|
||||
NText {
|
||||
text: I18n.tr("panels.system-monitor.disk-available-label")
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: 0
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.diskAvailWarningThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.diskAvailWarningThreshold")
|
||||
suffix: "%"
|
||||
onValueChanged: {
|
||||
Settings.data.systemMonitor.diskAvailWarningThreshold = value;
|
||||
if (Settings.data.systemMonitor.diskAvailCriticalThreshold > value) {
|
||||
Settings.data.systemMonitor.diskAvailCriticalThreshold = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: 0
|
||||
to: 20
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.diskAvailCriticalThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.diskAvailCriticalThreshold")
|
||||
suffix: "%"
|
||||
onValueChanged: Settings.data.systemMonitor.diskAvailCriticalThreshold = value
|
||||
}
|
||||
|
||||
// Battery
|
||||
NText {
|
||||
text: I18n.tr("panels.notifications.toast-battery-label")
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: 0
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.batteryWarningThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.batteryWarningThreshold")
|
||||
suffix: "%"
|
||||
onValueChanged: {
|
||||
Settings.data.systemMonitor.batteryWarningThreshold = value;
|
||||
if (Settings.data.systemMonitor.batteryCriticalThreshold > value) {
|
||||
Settings.data.systemMonitor.batteryCriticalThreshold = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: 0
|
||||
to: Settings.data.systemMonitor.batteryWarningThreshold
|
||||
stepSize: 5
|
||||
value: Settings.data.systemMonitor.batteryCriticalThreshold
|
||||
defaultValue: Settings.getDefaultValue("systemMonitor.batteryCriticalThreshold")
|
||||
suffix: "%"
|
||||
onValueChanged: Settings.data.systemMonitor.batteryCriticalThreshold = value
|
||||
}
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
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("panels.user-interface.tooltips-label")
|
||||
description: I18n.tr("panels.user-interface.tooltips-description")
|
||||
checked: Settings.data.ui.tooltipsEnabled
|
||||
defaultValue: Settings.getDefaultValue("ui.tooltipsEnabled")
|
||||
onToggled: checked => Settings.data.ui.tooltipsEnabled = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.user-interface.box-border-label")
|
||||
description: I18n.tr("panels.user-interface.box-border-description")
|
||||
checked: Settings.data.ui.boxBorderEnabled
|
||||
defaultValue: Settings.getDefaultValue("ui.boxBorderEnabled")
|
||||
onToggled: checked => Settings.data.ui.boxBorderEnabled = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.user-interface.scrollbar-always-visible-label")
|
||||
description: I18n.tr("panels.user-interface.scrollbar-always-visible-description")
|
||||
checked: Settings.data.ui.scrollbarAlwaysVisible
|
||||
defaultValue: Settings.getDefaultValue("ui.scrollbarAlwaysVisible")
|
||||
onToggled: checked => Settings.data.ui.scrollbarAlwaysVisible = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.user-interface.shadows-label")
|
||||
description: I18n.tr("panels.user-interface.shadows-description")
|
||||
checked: Settings.data.general.enableShadows
|
||||
defaultValue: Settings.getDefaultValue("general.enableShadows")
|
||||
onToggled: checked => Settings.data.general.enableShadows = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.user-interface.blur-behind-label")
|
||||
description: I18n.tr("panels.user-interface.blur-behind-description")
|
||||
checked: Settings.data.general.enableBlurBehind
|
||||
defaultValue: Settings.getDefaultValue("general.enableBlurBehind")
|
||||
onToggled: checked => Settings.data.general.enableBlurBehind = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.user-interface.translucent-widgets-label")
|
||||
description: I18n.tr("panels.user-interface.translucent-widgets-description")
|
||||
checked: Settings.data.ui.translucentWidgets
|
||||
defaultValue: Settings.getDefaultValue("ui.translucentWidgets")
|
||||
onToggled: checked => Settings.data.ui.translucentWidgets = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
visible: Settings.data.general.enableShadows
|
||||
label: I18n.tr("panels.user-interface.shadows-direction-label")
|
||||
description: I18n.tr("panels.user-interface.shadows-direction-description")
|
||||
Layout.fillWidth: true
|
||||
|
||||
readonly property var shadowOptionsMap: ({
|
||||
"top_left": {
|
||||
"name": I18n.tr("positions.top-left"),
|
||||
"p": Qt.point(-2, -2)
|
||||
},
|
||||
"top": {
|
||||
"name": I18n.tr("positions.top"),
|
||||
"p": Qt.point(0, -3)
|
||||
},
|
||||
"top_right": {
|
||||
"name": I18n.tr("positions.top-right"),
|
||||
"p": Qt.point(2, -2)
|
||||
},
|
||||
"left": {
|
||||
"name": I18n.tr("positions.left"),
|
||||
"p": Qt.point(-3, 0)
|
||||
},
|
||||
"center": {
|
||||
"name": I18n.tr("positions.center"),
|
||||
"p": Qt.point(0, 0)
|
||||
},
|
||||
"right": {
|
||||
"name": I18n.tr("positions.right"),
|
||||
"p": Qt.point(3, 0)
|
||||
},
|
||||
"bottom_left": {
|
||||
"name": I18n.tr("positions.bottom-left"),
|
||||
"p": Qt.point(-2, 2)
|
||||
},
|
||||
"bottom": {
|
||||
"name": I18n.tr("positions.bottom"),
|
||||
"p": Qt.point(0, 3)
|
||||
},
|
||||
"bottom_right": {
|
||||
"name": I18n.tr("positions.bottom-right"),
|
||||
"p": Qt.point(2, 3)
|
||||
}
|
||||
})
|
||||
|
||||
model: Object.keys(shadowOptionsMap).map(function (k) {
|
||||
return {
|
||||
"key": k,
|
||||
"name": shadowOptionsMap[k].name
|
||||
};
|
||||
})
|
||||
|
||||
currentKey: Settings.data.general.shadowDirection
|
||||
defaultValue: Settings.getDefaultValue("general.shadowDirection")
|
||||
|
||||
onSelected: function (key) {
|
||||
var opt = shadowOptionsMap[key];
|
||||
if (opt) {
|
||||
Settings.data.general.shadowDirection = key;
|
||||
Settings.data.general.shadowOffsetX = opt.p.x;
|
||||
Settings.data.general.shadowOffsetY = opt.p.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.user-interface.scaling-label")
|
||||
description: I18n.tr("panels.user-interface.scaling-description")
|
||||
from: 0.8
|
||||
to: 1.2
|
||||
stepSize: 0.05
|
||||
showReset: true
|
||||
value: Settings.data.general.scaleRatio
|
||||
defaultValue: Settings.getDefaultValue("general.scaleRatio")
|
||||
onMoved: value => Settings.data.general.scaleRatio = value
|
||||
text: Math.floor(Settings.data.general.scaleRatio * 100) + "%"
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.user-interface.box-border-radius-label")
|
||||
description: I18n.tr("panels.user-interface.box-border-radius-description")
|
||||
from: 0
|
||||
to: 2
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.general.radiusRatio
|
||||
defaultValue: Settings.getDefaultValue("general.radiusRatio")
|
||||
onMoved: value => Settings.data.general.radiusRatio = value
|
||||
text: Math.floor(Settings.data.general.radiusRatio * 100) + "%"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.user-interface.control-border-radius-label")
|
||||
description: I18n.tr("panels.user-interface.control-border-radius-description")
|
||||
from: 0
|
||||
to: 2
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.general.iRadiusRatio
|
||||
defaultValue: Settings.getDefaultValue("general.iRadiusRatio")
|
||||
onMoved: value => Settings.data.general.iRadiusRatio = value
|
||||
text: Math.floor(Settings.data.general.iRadiusRatio * 100) + "%"
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.user-interface.animation-disable-label")
|
||||
description: I18n.tr("panels.user-interface.animation-disable-description")
|
||||
checked: Settings.data.general.animationDisabled
|
||||
defaultValue: Settings.getDefaultValue("general.animationDisabled")
|
||||
onToggled: checked => Settings.data.general.animationDisabled = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
visible: !Settings.data.general.animationDisabled
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.user-interface.animation-speed-label")
|
||||
description: I18n.tr("panels.user-interface.animation-speed-description")
|
||||
from: 0
|
||||
to: 2.0
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.general.animationSpeed
|
||||
defaultValue: Settings.getDefaultValue("general.animationSpeed")
|
||||
onMoved: value => Settings.data.general.animationSpeed = Math.max(value, 0.05)
|
||||
text: Math.round(Settings.data.general.animationSpeed * 100) + "%"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Panels.Settings // For SettingsPanel
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.user-interface.panels-attached-to-bar-label")
|
||||
description: I18n.tr("panels.user-interface.panels-attached-to-bar-description")
|
||||
checked: Settings.data.ui.panelsAttachedToBar
|
||||
defaultValue: Settings.getDefaultValue("ui.panelsAttachedToBar")
|
||||
onToggled: checked => Settings.data.ui.panelsAttachedToBar = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
visible: (Quickshell.screens.length > 1)
|
||||
label: I18n.tr("panels.user-interface.allow-panels-without-bar-label")
|
||||
description: I18n.tr("panels.user-interface.allow-panels-without-bar-description")
|
||||
checked: Settings.data.general.allowPanelsOnScreenWithoutBar
|
||||
defaultValue: Settings.getDefaultValue("general.allowPanelsOnScreenWithoutBar")
|
||||
onToggled: checked => Settings.data.general.allowPanelsOnScreenWithoutBar = checked
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.user-interface.panel-background-opacity-label")
|
||||
description: I18n.tr("panels.user-interface.panel-background-opacity-description")
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.ui.panelBackgroundOpacity
|
||||
defaultValue: Settings.getDefaultValue("ui.panelBackgroundOpacity")
|
||||
onMoved: value => Settings.data.ui.panelBackgroundOpacity = value
|
||||
text: Math.floor(Settings.data.ui.panelBackgroundOpacity * 100) + "%"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.user-interface.dimmer-opacity-label")
|
||||
description: I18n.tr("panels.user-interface.dimmer-opacity-description")
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.general.dimmerOpacity
|
||||
defaultValue: Settings.getDefaultValue("general.dimmerOpacity")
|
||||
onMoved: value => Settings.data.general.dimmerOpacity = value
|
||||
text: Math.floor(Settings.data.general.dimmerOpacity * 100) + "%"
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("panels.user-interface.settings-panel-header")
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.user-interface.settings-panel-mode-label")
|
||||
description: I18n.tr("panels.user-interface.settings-panel-mode-description")
|
||||
Layout.fillWidth: true
|
||||
minimumWidth: 220 * Style.uiScaleRatio
|
||||
model: [
|
||||
{
|
||||
"key": "attached",
|
||||
"name": I18n.tr("options.settings-panel-mode.attached")
|
||||
},
|
||||
{
|
||||
"key": "centered",
|
||||
"name": I18n.tr("options.settings-panel-mode.centered")
|
||||
},
|
||||
{
|
||||
"key": "window",
|
||||
"name": I18n.tr("options.settings-panel-mode.window")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.ui.settingsPanelMode
|
||||
defaultValue: Settings.getDefaultValue("ui.settingsPanelMode")
|
||||
onSelected: key => {
|
||||
// Defer setup to next update so close can do its work properly
|
||||
Qt.callLater(() => {
|
||||
Settings.data.ui.settingsPanelMode = key;
|
||||
});
|
||||
if (Settings.data.ui.settingsPanelMode === "window" || key === "window") {
|
||||
// Just switched from/to window, need to close panel
|
||||
var screen = PanelService.openedPanel?.screen || SettingsPanelService.settingsWindow?.screen || PanelService.findScreenForPanels();
|
||||
SettingsPanelService.close(screen);
|
||||
|
||||
Qt.callLater(() => {
|
||||
SettingsPanelService.openToTab(SettingsPanel.Tab.UserInterface, 1, screen);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.user-interface.settings-panel-sidebar-card-style-label")
|
||||
description: I18n.tr("panels.user-interface.settings-panel-sidebar-card-style-description")
|
||||
checked: Settings.data.ui.settingsPanelSideBarCardStyle
|
||||
defaultValue: Settings.getDefaultValue("ui.settingsPanelSideBarCardStyle")
|
||||
onToggled: checked => Settings.data.ui.settingsPanelSideBarCardStyle = checked
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
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("panels.general.screen-corners-show-corners-label")
|
||||
description: I18n.tr("panels.general.screen-corners-show-corners-description")
|
||||
checked: Settings.data.general.showScreenCorners
|
||||
defaultValue: Settings.getDefaultValue("general.showScreenCorners")
|
||||
onToggled: checked => Settings.data.general.showScreenCorners = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.general.screen-corners-solid-black-label")
|
||||
description: I18n.tr("panels.general.screen-corners-solid-black-description")
|
||||
checked: Settings.data.general.forceBlackScreenCorners
|
||||
defaultValue: Settings.getDefaultValue("general.forceBlackScreenCorners")
|
||||
onToggled: checked => Settings.data.general.forceBlackScreenCorners = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.screen-corners-radius-label")
|
||||
description: I18n.tr("panels.general.screen-corners-radius-description")
|
||||
from: 0
|
||||
to: 2
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.general.screenRadiusRatio
|
||||
defaultValue: Settings.getDefaultValue("general.screenRadiusRatio")
|
||||
onMoved: value => Settings.data.general.screenRadiusRatio = value
|
||||
text: Math.floor(Settings.data.general.screenRadiusRatio * 100) + "%"
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
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
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.appearance")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.panels")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.screen-corners")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
AppearanceSubTab {}
|
||||
PanelsSubTab {}
|
||||
ScreenCornersSubTab {}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
enabled: Settings.data.wallpaper.enabled
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.wallpaper.automation-scheduled-change-label")
|
||||
description: I18n.tr("panels.wallpaper.automation-scheduled-change-description")
|
||||
checked: Settings.data.wallpaper.automationEnabled
|
||||
onToggled: checked => Settings.data.wallpaper.automationEnabled = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
enabled: Settings.data.wallpaper.automationEnabled
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NComboBox {
|
||||
|
||||
label: I18n.tr("panels.wallpaper.automation-change-mode-label")
|
||||
description: I18n.tr("panels.wallpaper.automation-change-mode-description")
|
||||
Layout.fillWidth: true
|
||||
model: [
|
||||
{
|
||||
"key": "random",
|
||||
"name": I18n.tr("common.random")
|
||||
},
|
||||
{
|
||||
"key": "alphabetical",
|
||||
"name": I18n.tr("panels.wallpaper.automation-change-mode-alphabetical")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.wallpaper.wallpaperChangeMode || "random"
|
||||
onSelected: key => Settings.data.wallpaper.wallpaperChangeMode = key
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.wallpaperChangeMode")
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
label: I18n.tr("panels.wallpaper.automation-interval-label")
|
||||
description: I18n.tr("panels.wallpaper.automation-interval-description")
|
||||
Layout.fillWidth: true
|
||||
from: 1
|
||||
to: 1440
|
||||
stepSize: 1
|
||||
suffix: "m"
|
||||
value: Math.round(Settings.data.wallpaper.randomIntervalSec / 60)
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.randomIntervalSec") !== undefined ? Math.round(Settings.getDefaultValue("wallpaper.randomIntervalSec") / 60) : undefined
|
||||
onValueChanged: {
|
||||
let newSec = value * 60;
|
||||
if (newSec !== Settings.data.wallpaper.randomIntervalSec) {
|
||||
Settings.data.wallpaper.randomIntervalSec = newSec;
|
||||
WallpaperService.restartRandomWallpaperTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property var screen
|
||||
|
||||
signal openMainFolderPicker
|
||||
signal openMonitorFolderPicker(string monitorName)
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.wallpaper.settings-enable-management-label")
|
||||
description: I18n.tr("panels.wallpaper.settings-enable-management-description")
|
||||
checked: Settings.data.wallpaper.enabled
|
||||
onToggled: checked => Settings.data.wallpaper.enabled = checked
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.enabled")
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
enabled: Settings.data.wallpaper.enabled
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("tooltips.wallpaper-selector")
|
||||
description: I18n.tr("panels.wallpaper.settings-selector-description")
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "wallpaper-selector"
|
||||
tooltipText: I18n.tr("tooltips.wallpaper-selector")
|
||||
onClicked: PanelService.getPanel("wallpaperPanel", root.screen)?.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("common.position")
|
||||
description: I18n.tr("panels.wallpaper.settings-selector-position-description")
|
||||
Layout.fillWidth: true
|
||||
model: [
|
||||
{
|
||||
"key": "follow_bar",
|
||||
"name": I18n.tr("positions.follow-bar")
|
||||
},
|
||||
{
|
||||
"key": "center",
|
||||
"name": I18n.tr("positions.center")
|
||||
},
|
||||
{
|
||||
"key": "top_center",
|
||||
"name": I18n.tr("positions.top-center")
|
||||
},
|
||||
{
|
||||
"key": "top_left",
|
||||
"name": I18n.tr("positions.top-left")
|
||||
},
|
||||
{
|
||||
"key": "top_right",
|
||||
"name": I18n.tr("positions.top-right")
|
||||
},
|
||||
{
|
||||
"key": "bottom_left",
|
||||
"name": I18n.tr("positions.bottom-left")
|
||||
},
|
||||
{
|
||||
"key": "bottom_right",
|
||||
"name": I18n.tr("positions.bottom-right")
|
||||
},
|
||||
{
|
||||
"key": "bottom_center",
|
||||
"name": I18n.tr("positions.bottom-center")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.wallpaper.panelPosition
|
||||
onSelected: key => Settings.data.wallpaper.panelPosition = key
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.panelPosition")
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.wallpaper.settings-view-mode-label")
|
||||
description: I18n.tr("panels.wallpaper.settings-view-mode-description")
|
||||
Layout.fillWidth: true
|
||||
model: [
|
||||
{
|
||||
"key": "single",
|
||||
"name": I18n.tr("panels.wallpaper.view-mode-single")
|
||||
},
|
||||
{
|
||||
"key": "recursive",
|
||||
"name": I18n.tr("panels.wallpaper.view-mode-recursive")
|
||||
},
|
||||
{
|
||||
"key": "browse",
|
||||
"name": I18n.tr("panels.wallpaper.view-mode-browse")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.wallpaper.viewMode
|
||||
onSelected: key => Settings.data.wallpaper.viewMode = key
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.viewMode")
|
||||
}
|
||||
|
||||
NTextInputButton {
|
||||
id: wallpaperPathInput
|
||||
label: I18n.tr("panels.wallpaper.settings-folder-label")
|
||||
description: I18n.tr("panels.wallpaper.settings-folder-description")
|
||||
text: Settings.data.wallpaper.directory
|
||||
buttonIcon: "folder-open"
|
||||
buttonTooltip: I18n.tr("panels.wallpaper.settings-folder-label")
|
||||
Layout.fillWidth: true
|
||||
onInputTextChanged: text => Settings.data.wallpaper.directory = text
|
||||
onButtonClicked: root.openMainFolderPicker()
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.wallpaper.settings-monitor-specific-label")
|
||||
description: I18n.tr("panels.wallpaper.settings-monitor-specific-description")
|
||||
checked: Settings.data.wallpaper.enableMultiMonitorDirectories
|
||||
onToggled: checked => Settings.data.wallpaper.enableMultiMonitorDirectories = checked
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.enableMultiMonitorDirectories")
|
||||
}
|
||||
|
||||
NBox {
|
||||
visible: Settings.data.wallpaper.enableMultiMonitorDirectories
|
||||
Layout.fillWidth: true
|
||||
radius: Style.radiusM
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
implicitHeight: contentCol.implicitHeight + Style.margin2L
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
id: contentCol
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
Repeater {
|
||||
model: Quickshell.screens || []
|
||||
delegate: ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: (modelData.name || "Unknown")
|
||||
color: Color.mPrimary
|
||||
font.weight: Style.fontWeightBold
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
|
||||
NTextInputButton {
|
||||
id: monitorDirInput
|
||||
text: WallpaperService.getMonitorDirectory(modelData.name)
|
||||
buttonIcon: "folder-open"
|
||||
buttonTooltip: I18n.tr("panels.wallpaper.settings-monitor-specific-tooltip")
|
||||
Layout.fillWidth: true
|
||||
onInputEditingFinished: WallpaperService.setMonitorDirectory(modelData.name, monitorDirInput.text)
|
||||
onButtonClicked: root.openMonitorFolderPicker(modelData.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.wallpaper.settings-use-original-images-label")
|
||||
description: I18n.tr("panels.wallpaper.settings-use-original-images-description")
|
||||
checked: Settings.data.wallpaper.useOriginalImages
|
||||
enabled: Settings.data.wallpaper.enabled
|
||||
onToggled: checked => Settings.data.wallpaper.useOriginalImages = checked
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.useOriginalImages")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
enabled: Settings.data.wallpaper.enabled
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.wallpaper.settings-clear-cache-label")
|
||||
description: I18n.tr("panels.wallpaper.settings-clear-cache-description")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NButton {
|
||||
icon: "trash"
|
||||
text: I18n.tr("panels.wallpaper.settings-clear-cache-button")
|
||||
outlined: true
|
||||
onClicked: {
|
||||
ImageCacheService.clearLarge();
|
||||
ToastService.showNotice(I18n.tr("panels.wallpaper.settings-clear-cache-toast"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: CompositorService.isNiri
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: CompositorService.isNiri
|
||||
enabled: Settings.data.wallpaper.enabled
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.wallpaper.settings-enable-overview-label")
|
||||
description: I18n.tr("panels.wallpaper.settings-enable-overview-description")
|
||||
checked: Settings.data.wallpaper.enabled && Settings.data.wallpaper.overviewEnabled
|
||||
onToggled: checked => Settings.data.wallpaper.overviewEnabled = checked
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.overviewEnabled")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.wallpaper.overviewEnabled
|
||||
label: I18n.tr("panels.wallpaper.settings-overview-blur-strength-label")
|
||||
description: I18n.tr("panels.wallpaper.settings-overview-blur-strength-description")
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.wallpaper.overviewBlur
|
||||
onMoved: value => Settings.data.wallpaper.overviewBlur = value
|
||||
text: ((Settings.data.wallpaper.overviewBlur) * 100).toFixed(0) + "%"
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.overviewBlur")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.wallpaper.overviewEnabled
|
||||
label: I18n.tr("panels.wallpaper.settings-overview-tint-label")
|
||||
description: I18n.tr("panels.wallpaper.settings-overview-tint-description")
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.wallpaper.overviewTint
|
||||
onMoved: value => Settings.data.wallpaper.overviewTint = value
|
||||
text: ((Settings.data.wallpaper.overviewTint) * 100).toFixed(0) + "%"
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.overviewTint")
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
enabled: Settings.data.wallpaper.enabled
|
||||
|
||||
property var screen
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.wallpaper.look-feel-fill-mode-label")
|
||||
description: I18n.tr("panels.wallpaper.look-feel-fill-mode-description")
|
||||
model: WallpaperService.fillModeModel
|
||||
currentKey: Settings.data.wallpaper.fillMode
|
||||
onSelected: key => Settings.data.wallpaper.fillMode = key
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.fillMode")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
NLabel {
|
||||
label: I18n.tr("bar.audio-visualizer.color-name-label")
|
||||
description: I18n.tr("panels.wallpaper.look-feel-fill-color-description")
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
NColorPicker {
|
||||
screen: root.screen
|
||||
selectedColor: Settings.data.wallpaper.fillColor
|
||||
onColorSelected: color => Settings.data.wallpaper.fillColor = color
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.wallpaper.look-feel-transition-type-label")
|
||||
description: I18n.tr("panels.wallpaper.look-feel-transition-type-description")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: WallpaperService.allTransitions
|
||||
|
||||
NCheckbox {
|
||||
required property string modelData
|
||||
label: {
|
||||
var key = "wallpaper.transitions." + modelData;
|
||||
return I18n.tr(key);
|
||||
}
|
||||
labelSize: Style.fontSizeM
|
||||
checked: Settings.data.wallpaper.transitionType.includes(modelData)
|
||||
onToggled: checked => {
|
||||
var arr = Array.from(Settings.data.wallpaper.transitionType);
|
||||
if (checked) {
|
||||
if (!arr.includes(modelData))
|
||||
arr.push(modelData);
|
||||
} else {
|
||||
arr = arr.filter(k => k !== modelData);
|
||||
}
|
||||
Settings.data.wallpaper.transitionType = arr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.wallpaper.look-feel-skip-startup-transition-label")
|
||||
description: I18n.tr("panels.wallpaper.look-feel-skip-startup-transition-description")
|
||||
checked: Settings.data.wallpaper.skipStartupTransition
|
||||
onToggled: Settings.data.wallpaper.skipStartupTransition = checked
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.wallpaper.look-feel-transition-duration-label")
|
||||
description: I18n.tr("panels.wallpaper.look-feel-transition-duration-description")
|
||||
from: 500
|
||||
to: 10000
|
||||
stepSize: 100
|
||||
showReset: true
|
||||
value: Settings.data.wallpaper.transitionDuration
|
||||
onMoved: value => Settings.data.wallpaper.transitionDuration = value
|
||||
text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(1) + "s"
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.transitionDuration")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.wallpaper.look-feel-edge-smoothness-label")
|
||||
description: I18n.tr("panels.wallpaper.look-feel-edge-smoothness-description")
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
showReset: true
|
||||
value: Settings.data.wallpaper.transitionEdgeSmoothness
|
||||
onMoved: value => Settings.data.wallpaper.transitionEdgeSmoothness = value
|
||||
text: Math.round(Settings.data.wallpaper.transitionEdgeSmoothness * 100) + "%"
|
||||
defaultValue: Settings.getDefaultValue("wallpaper.transitionEdgeSmoothness")
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
property var screen
|
||||
|
||||
function openMainFolderPicker() {
|
||||
mainFolderPicker.open();
|
||||
}
|
||||
|
||||
function openMonitorFolderPicker(monitorName) {
|
||||
specificFolderMonitorName = monitorName;
|
||||
monitorFolderPicker.open();
|
||||
}
|
||||
|
||||
property string specificFolderMonitorName: ""
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.general")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.look")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.automation")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
GeneralSubTab {
|
||||
screen: root.screen
|
||||
onOpenMainFolderPicker: root.openMainFolderPicker()
|
||||
onOpenMonitorFolderPicker: monitorName => root.openMonitorFolderPicker(monitorName)
|
||||
}
|
||||
LookAndFeelSubTab {
|
||||
screen: root.screen
|
||||
}
|
||||
AutomationSubTab {}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: mainFolderPicker
|
||||
selectionMode: "folders"
|
||||
title: I18n.tr("setup.wallpaper.dir-select-title")
|
||||
initialPath: Settings.data.wallpaper.directory || Quickshell.env("HOME") + "/Pictures"
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
Settings.data.wallpaper.directory = paths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: monitorFolderPicker
|
||||
selectionMode: "folders"
|
||||
title: I18n.tr("panels.wallpaper.settings-select-monitor-folder")
|
||||
initialPath: WallpaperService.getMonitorDirectory(specificFolderMonitorName) || Quickshell.env("HOME") + "/Pictures"
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
WallpaperService.setMonitorDirectory(specificFolderMonitorName, paths[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user