mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 04:09:15 -04:00
feat(settings): Added Settings Tab Autostart App (XDG Autostart) (#2535)
* feat(Autostart): add Autostart tab and application selection popup * fix(AutoStartTab): update systemdUserDir property to use XDG_CONFIG_HOME * fix(AutoStartTab): update autostartDir and systemdUserDir to use StandardPaths for config home * refactor(AutoStartTab): use FileView & FolderListModel * refactor(AutoStartTab): implement systemd override generation for autostart applications using FileView * feat(AutoStartTab): add systemd check to determine environment and update tray icon visibility * feat(SettingsSidebar, AutoStartTab, DesktopService): add autostart functionality and systemd checks * feat(AutoStartTab): add hidden property support for desktop entries and toggle functionality * feat(AutoStartTab): add initialize autostart directory and add toast if writer failed * add(AutoStartTab): logging for scoped log tracking ---------
This commit is contained in:
@@ -0,0 +1,751 @@
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Qt.labs.folderlistmodel
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property var log: Log.scoped("AutoStartTab")
|
||||
property var parentModal: null
|
||||
property var entries: []
|
||||
property var desktopApps: []
|
||||
property string newEntryType: "desktop"
|
||||
property string newEntryName: ""
|
||||
property string newEntryExec: ""
|
||||
property string newEntryDesktopId: ""
|
||||
property string newEntryCommandWrapper: "%command%"
|
||||
|
||||
readonly property string autostartDir: {
|
||||
const configHome = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
|
||||
return configHome + "/autostart";
|
||||
}
|
||||
|
||||
function lookupDesktopIcon(name, exec, fileName) {
|
||||
const appId = fileName ? fileName.replace(/\.desktop$/, "") : "";
|
||||
let entry = appId ? DesktopEntries.heuristicLookup(appId) : null;
|
||||
if (entry && entry.icon) return entry.icon;
|
||||
if (exec) {
|
||||
const cmdBase = exec.split(" ")[0].split("/").pop();
|
||||
for (let i = 0; i < root.desktopApps.length; i++) {
|
||||
const app = root.desktopApps[i];
|
||||
if (app.icon) {
|
||||
const appExec = (app.exec || app.execString || "").split(" ")[0].split("/").pop();
|
||||
if (appExec === cmdBase) return app.icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function parseDesktopFile(content, filePath) {
|
||||
if (!content || content.length === 0) return null;
|
||||
const lines = content.split("\n");
|
||||
let name = "";
|
||||
let execCmd = "";
|
||||
let icon = "";
|
||||
let hidden = false;
|
||||
let isDesktopEntry = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line === "[Desktop Entry]") {
|
||||
isDesktopEntry = true;
|
||||
} else if (isDesktopEntry) {
|
||||
if (line.startsWith("[")) break;
|
||||
const nameMatch = line.match(/^Name=(.+)$/);
|
||||
if (nameMatch) name = nameMatch[1];
|
||||
const execMatch = line.match(/^Exec=(.+)$/);
|
||||
if (execMatch) execCmd = execMatch[1];
|
||||
const iconMatch = line.match(/^Icon=(.+)$/);
|
||||
if (iconMatch) icon = iconMatch[1];
|
||||
const hiddenMatch = line.match(/^Hidden=(true|false)$/);
|
||||
if (hiddenMatch) hidden = hiddenMatch[1] === "true";
|
||||
}
|
||||
}
|
||||
if (!isDesktopEntry || !name || !execCmd) return null;
|
||||
const fileName = filePath.split("/").pop();
|
||||
if (!icon) icon = root.lookupDesktopIcon(name, execCmd, fileName);
|
||||
return { name: name, exec: execCmd, icon: icon, hidden: hidden, filePath: filePath, fileName: fileName, content: content };
|
||||
}
|
||||
|
||||
function addEntry() {
|
||||
if (newEntryType === "desktop") {
|
||||
if (!newEntryDesktopId) return;
|
||||
const app = desktopApps.find(a => (a.id || a.execString) === newEntryDesktopId);
|
||||
if (!app) return;
|
||||
const entryName = app.name || newEntryDesktopId;
|
||||
const appExec = app.exec || app.execString || "";
|
||||
const execCmd = root.newEntryCommandWrapper.replace("%command%", appExec);
|
||||
const appIcon = app.icon || "";
|
||||
const fileName = entryName.toLowerCase().replace(/[^a-z0-9]/g, "-") + ".desktop";
|
||||
writeDesktopFile(fileName, entryName, execCmd, appIcon);
|
||||
} else {
|
||||
if (!newEntryName || !newEntryExec) return;
|
||||
const fileName = newEntryName.toLowerCase().replace(/[^a-z0-9]/g, "-") + ".desktop";
|
||||
writeDesktopFile(fileName, newEntryName, newEntryExec, "");
|
||||
}
|
||||
}
|
||||
|
||||
function writeDesktopFile(fileName, name, execCmd, icon) {
|
||||
let content = "[Desktop Entry]\nType=Application\nName=" + name + "\nExec=" + execCmd + "\n";
|
||||
if (icon) content += "Icon=" + icon + "\n";
|
||||
writerFileView.path = root.autostartDir + "/" + fileName;
|
||||
writerFileView.setText(content);
|
||||
root.resetNewEntry();
|
||||
}
|
||||
|
||||
function setHidden(entry, hidden) {
|
||||
if (!entry || !entry.content) return;
|
||||
const lines = entry.content.split("\n");
|
||||
const hiddenValue = hidden ? "true" : "false";
|
||||
let found = false;
|
||||
const merged = lines.map(line => {
|
||||
const m = line.match(/^Hidden=(true|false)\s*$/);
|
||||
if (m) {
|
||||
found = true;
|
||||
return "Hidden=" + hiddenValue;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
if (!found) {
|
||||
const idx = merged.findIndex(l => l.trim() === "[Desktop Entry]");
|
||||
if (idx >= 0)
|
||||
merged.splice(idx + 1, 0, "Hidden=" + hiddenValue);
|
||||
else
|
||||
merged.unshift("Hidden=" + hiddenValue);
|
||||
}
|
||||
writerFileView.path = entry.filePath;
|
||||
writerFileView.setText(merged.join("\n"));
|
||||
}
|
||||
|
||||
function removeEntry(filePath) {
|
||||
const proc = removeFileComponent.createObject(root, {
|
||||
targetPath: filePath,
|
||||
running: true
|
||||
});
|
||||
}
|
||||
|
||||
function resetNewEntry() {
|
||||
newEntryType = "desktop";
|
||||
newEntryName = "";
|
||||
newEntryExec = "";
|
||||
newEntryDesktopId = "";
|
||||
newEntryCommandWrapper = "%command%";
|
||||
}
|
||||
|
||||
function addOrUpdateEntry(entry) {
|
||||
var list = root.entries.slice();
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i].filePath === entry.filePath) {
|
||||
list[i] = entry;
|
||||
root.entries = list;
|
||||
return;
|
||||
}
|
||||
}
|
||||
list.push(entry);
|
||||
list.sort((a, b) => a.fileName.localeCompare(b.fileName));
|
||||
root.entries = list;
|
||||
}
|
||||
|
||||
function removeEntryByPath(filePath) {
|
||||
var list = root.entries.filter(e => e.filePath !== filePath);
|
||||
root.entries = list;
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: writerFileView
|
||||
blockLoading: true
|
||||
atomicWrites: true
|
||||
onSaveFailed: error => {
|
||||
ToastService.showError(I18n.tr("Failed to write autostart entry"))
|
||||
log.warn("Failed to write autostart entry to " + writerFileView.path + ": " + error);
|
||||
}
|
||||
}
|
||||
|
||||
FolderListModel {
|
||||
id: folderModel
|
||||
nameFilters: ["*.desktop"]
|
||||
showDirs: false
|
||||
showDotAndDotDot: false
|
||||
showHidden: false
|
||||
sortField: FolderListModel.Name
|
||||
|
||||
onStatusChanged: {
|
||||
if (status !== FolderListModel.Ready) return;
|
||||
// rebuild entries
|
||||
const validPaths = new Set();
|
||||
for (let i = 0; i < folderModel.count; i++) {
|
||||
const fp = folderModel.get(i, "filePath") || "";
|
||||
validPaths.add(fp.startsWith("file://") ? fp.substring(7) : fp);
|
||||
}
|
||||
const filtered = root.entries.filter(e => validPaths.has(e.filePath));
|
||||
if (filtered.length !== root.entries.length) {
|
||||
root.entries = filtered;
|
||||
}
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
fileReaderRepeater.model = count;
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: fileReaderRepeater
|
||||
model: 0
|
||||
|
||||
Item {
|
||||
required property int index
|
||||
|
||||
readonly property string filePath: {
|
||||
const fp = folderModel.get(index, "filePath") || "";
|
||||
return fp.startsWith("file://") ? fp.substring(7) : fp;
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: fileView
|
||||
path: filePath ? "file://" + filePath : ""
|
||||
watchChanges: true
|
||||
|
||||
onLoaded: {
|
||||
const entry = root.parseDesktopFile(fileView.text(), filePath);
|
||||
if (entry) {
|
||||
root.addOrUpdateEntry(entry);
|
||||
} else {
|
||||
root.removeEntryByPath(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
onFileChanged: reload()
|
||||
|
||||
onLoadFailed: {
|
||||
root.removeEntryByPath(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: removeFileComponent
|
||||
Process {
|
||||
property string targetPath: ""
|
||||
command: ["rm", "-f", targetPath]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.removeEntryByPath(targetPath);
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateTrayIconFixSystemdOverride() {
|
||||
const configHome = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
|
||||
const dir = configHome + "/systemd/user/app-@autostart.service.d";
|
||||
const proc = systemdOverrideMkDirComp.createObject(root, { targetPath: dir, running: true });
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: systemdOverrideWriter
|
||||
atomicWrites: true
|
||||
|
||||
// make sure we don't overwrite an existing override with a default one, in case the user has already customized it
|
||||
function buildOverrideContent(existing) {
|
||||
if (!existing) return "[Unit]\nAfter=dms.service\n";
|
||||
const lines = existing.split("\n");
|
||||
const hasAfter = lines.some(l => l.trim() === "After=dms.service");
|
||||
if (hasAfter) return existing;
|
||||
const unitIdx = lines.findIndex(l => l.trim() === "[Unit]");
|
||||
if (unitIdx >= 0) {
|
||||
lines.splice(unitIdx + 1, 0, "After=dms.service");
|
||||
} else {
|
||||
lines.push("[Unit]", "After=dms.service");
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
const merged = buildOverrideContent(text());
|
||||
if (merged !== text()) setText(merged);
|
||||
ToastService.showInfo(I18n.tr("Systemd Override generated"));
|
||||
}
|
||||
|
||||
onLoadFailed: {
|
||||
setText("[Unit]\nAfter=dms.service\n");
|
||||
ToastService.showInfo(I18n.tr("Systemd Override generated"));
|
||||
}
|
||||
|
||||
onSaveFailed: error => {
|
||||
ToastService.showError(I18n.tr("Failed to generate systemd override"));
|
||||
log.warn("Failed to write systemd override to " + systemdOverrideWriter.path + ": " + error);
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: systemdOverrideMkDirComp
|
||||
Process {
|
||||
property string targetPath: ""
|
||||
command: ["mkdir", "-p", targetPath]
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
systemdOverrideWriter.path = targetPath + "/override.conf";
|
||||
} else {
|
||||
ToastService.showError(I18n.tr("Failed to generate systemd override"));
|
||||
}
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: autostartInitMkDirComp
|
||||
Process {
|
||||
command: ["mkdir", "-p", root.autostartDir]
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
folderModel.folder = "file://" + root.autostartDir;
|
||||
}
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
desktopApps = AppSearchService.getVisibleApplications() || [];
|
||||
autostartInitMkDirComp.createObject(root, { running: true });
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
desktopApps = [];
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentHeight: mainColumn.height + Theme.spacingXL
|
||||
contentWidth: width
|
||||
|
||||
AppBrowserPopup {
|
||||
id: appBrowserPopup
|
||||
appsModel: root.desktopApps
|
||||
parentModal: root.parentModal
|
||||
onAppSelected: appId => root.newEntryDesktopId = appId
|
||||
}
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
topPadding: 4
|
||||
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
visible: DesktopService.autostartAvailable
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "add_circle"
|
||||
title: I18n.tr("Add Entry")
|
||||
|
||||
SettingsDropdownRow {
|
||||
width: parent.width
|
||||
text: I18n.tr("Entry Type")
|
||||
description: I18n.tr("Choose whether to launch a desktop app or a command")
|
||||
currentValue: root.newEntryType === "desktop" ? I18n.tr("Desktop Application") : I18n.tr("Command Line")
|
||||
options: [I18n.tr("Desktop Application"), I18n.tr("Command Line")]
|
||||
onValueChanged: val => {
|
||||
root.newEntryType = val === I18n.tr("Desktop Application") ? "desktop" : "command";
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
visible: root.newEntryType === "desktop"
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: appLabelColumn.height
|
||||
|
||||
Column {
|
||||
id: appLabelColumn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Application")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Select a desktop application")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledRect {
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: root.newEntryDesktopId ? Theme.surfaceContainerHigh : Theme.withAlpha(Theme.surfaceContainerHigh, 0.5)
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
readonly property string selectedName: {
|
||||
if (!root.newEntryDesktopId) return "";
|
||||
const app = root.desktopApps.find(a => (a.id || a.execString) === root.newEntryDesktopId);
|
||||
return app ? (app.name || app.id || "") : root.newEntryDesktopId;
|
||||
}
|
||||
|
||||
width: parent.width - browseButton.width - Theme.spacingM
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
visible: root.newEntryDesktopId !== ""
|
||||
|
||||
Image {
|
||||
width: 24
|
||||
height: 24
|
||||
source: {
|
||||
const app = root.desktopApps.find(a => (a.id || a.execString) === root.newEntryDesktopId);
|
||||
return Paths.resolveIconUrl(app?.icon || "application-x-executable");
|
||||
}
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error)
|
||||
source = "image://icon/application-x-executable";
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: parent.parent.selectedName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("No application selected")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: root.newEntryDesktopId === ""
|
||||
}
|
||||
}
|
||||
|
||||
DankButton {
|
||||
id: browseButton
|
||||
text: I18n.tr("Browse")
|
||||
iconName: "search"
|
||||
onClicked: appBrowserPopup.show()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: wrapperLabelColumn.height
|
||||
|
||||
Column {
|
||||
id: wrapperLabelColumn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Wrap the app command. %command% is replaced with the actual executable")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
width: parent.width
|
||||
placeholderText: I18n.tr("%command%")
|
||||
text: root.newEntryCommandWrapper
|
||||
onTextChanged: root.newEntryCommandWrapper = text
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
visible: root.newEntryType === "command"
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: labelColumn.height
|
||||
|
||||
Column {
|
||||
id: labelColumn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Name")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Display name for this entry")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
width: parent.width
|
||||
placeholderText: I18n.tr("e.g. My Script")
|
||||
text: root.newEntryName
|
||||
onTextChanged: root.newEntryName = text
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: labelColumn2.height
|
||||
|
||||
Column {
|
||||
id: labelColumn2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Full command to execute")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
width: parent.width
|
||||
placeholderText: I18n.tr("e.g. /usr/bin/my-script --flag")
|
||||
text: root.newEntryExec
|
||||
onTextChanged: root.newEntryExec = text
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("These add entries to the XDG autostart directory (~/.config/autostart/*.desktop)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Theme.spacingM
|
||||
}
|
||||
|
||||
DankButton {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: I18n.tr("Add to Autostart")
|
||||
iconName: "add"
|
||||
enabled: {
|
||||
if (root.newEntryType === "desktop") return root.newEntryDesktopId !== "";
|
||||
return root.newEntryName !== "" && root.newEntryExec !== "";
|
||||
}
|
||||
onClicked: root.addEntry()
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
id: entriesCard
|
||||
width: parent.width
|
||||
iconName: "line_start"
|
||||
title: I18n.tr("Autostart Entries")
|
||||
settingKey: "autostartEntries"
|
||||
collapsible: true
|
||||
expanded: true
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
width: parent.width - clearAllButton.width - Theme.spacingM
|
||||
text: I18n.tr("Applications and commands to start automatically when you log in")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: clearAllButton
|
||||
iconName: "delete_sweep"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.error
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
for (let i = 0; i < root.entries.length; i++) {
|
||||
root.removeEntry(root.entries[i].filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: entriesList
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.entries
|
||||
|
||||
delegate: Rectangle {
|
||||
width: entriesList.width
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: (index + 1).toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.primary
|
||||
width: 20
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Image {
|
||||
width: 24
|
||||
height: 24
|
||||
source: Paths.resolveIconUrl(modelData.icon || "application-x-executable")
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error)
|
||||
source = "image://icon/application-x-executable";
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - 20 - Theme.spacingM - 24 - Theme.spacingM - Theme.spacingM - 60 - Theme.spacingM - 32 - Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: modelData.hidden ? Theme.surfaceVariantText : Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
opacity: modelData.hidden ? 0.6 : 1.0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: modelData.hidden ? I18n.tr("Disabled") : modelData.exec
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: !modelData.hidden
|
||||
onToggled: checked => root.setHidden(modelData, !checked)
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "close"
|
||||
iconSize: 16
|
||||
buttonSize: 32
|
||||
circular: true
|
||||
iconColor: Theme.error
|
||||
onClicked: root.removeEntry(modelData.filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("No autostart entries")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: root.entries.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "system_tray"
|
||||
title: I18n.tr("Tray Icon Fix")
|
||||
visible: DesktopService.isSystemd
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("If autostart app icons don't appear in the system tray, generate a systemd override to ensure DMS starts before autostart apps")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
DankButton {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: I18n.tr("Generate Override")
|
||||
iconName: "build"
|
||||
onClicked: root.generateTrayIconFixSystemdOverride()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user