mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 04:09:15 -04:00
d2905072c0
* 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 ---------
752 lines
30 KiB
QML
752 lines
30 KiB
QML
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|