Files

813 lines
37 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.DCommon.Widgets
import qs.Modals.Common
import qs.Modules.Settings.Widgets
import qs.Modules.SystemUpdate
import "../../Common/Format.js" as Format
Item {
id: root
property var parentModal: null
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
Ref {
service: SystemUpdateService
modules: ["releases"]
}
readonly property var intervalOptions: [
{
label: I18n.tr("Every %1", "update check interval option, %1 is a duration such as 30 minutes").arg(I18n.duration(900)),
seconds: 900
},
{
label: I18n.tr("Every %1", "update check interval option, %1 is a duration such as 30 minutes").arg(I18n.duration(1800)),
seconds: 1800
},
{
label: I18n.tr("Every hour"),
seconds: 3600
},
{
label: I18n.tr("Every %1", "update check interval option, %1 is a duration such as 30 minutes").arg(I18n.duration(14400)),
seconds: 14400
},
{
label: I18n.tr("Once a day"),
seconds: 86400
}
]
readonly property string customIntervalLabel: I18n.tr("Custom")
property bool customIntervalSelected: false
property bool packagesExpanded: false
property int nowUnix: Math.floor(Date.now() / 1000)
readonly property int packageListCap: 60
readonly property int logTailLines: 20
readonly property real logViewHeight: Theme.listItemHeight * 5
readonly property bool upgradeRunsInTerminal: SystemUpdateService.useCustomCommand || (SystemUpdateService.backends || []).some(b => b.runsInTerminal === true)
readonly property bool upgradeInWindow: SettingsData.updaterUpgradeInWindow && !upgradeRunsInTerminal
readonly property int systemCount: SystemUpdateService.systemUpdates.length
readonly property int flatpakCount: SystemUpdateService.systemUpdates.filter(p => p.repo === "flatpak").length
readonly property bool busy: SystemUpdateService.isChecking || SystemUpdateService.isUpgrading
// A refresh clears the log, so a log next to an error means the upgrade itself failed.
readonly property bool upgradeFailed: SystemUpdateService.hasError && !busy && (SystemUpdateService.recentLog || []).length > 0
readonly property var installablePackages: SystemUpdateService.availableUpdates.filter(p => SettingsData.updaterIncludeFlatpak || p.repo !== "flatpak")
readonly property bool anythingToInstall: installablePackages.length > 0 && SystemUpdateService.helperAvailable
readonly property var notesRelease: SystemUpdateService.notesRelease
readonly property int serviceLastCheckUnix: SystemUpdateService.lastCheckUnix
onServiceLastCheckUnixChanged: nowUnix = Math.floor(Date.now() / 1000)
Component.onCompleted: {
customIntervalSelected = !intervalOptions.some(o => o.seconds === SettingsData.updaterIntervalSeconds);
nowUnix = Math.floor(Date.now() / 1000);
}
readonly property var notifyOptions: [
{
label: I18n.tr("Every check", "update notification frequency option, notify on every check"),
seconds: 0
},
{
label: I18n.tr("Every %1", "update check interval option, %1 is a duration such as 30 minutes").arg(I18n.duration(3600)),
seconds: 3600
},
{
label: I18n.tr("Every %1", "update check interval option, %1 is a duration such as 30 minutes").arg(I18n.duration(4 * 3600)),
seconds: 4 * 3600
},
{
label: I18n.tr("Once a day"),
seconds: 86400
},
{
label: I18n.tr("Once a week", "update notification frequency option"),
seconds: 7 * 86400
}
]
function intervalLabelFor(seconds) {
for (const opt of intervalOptions) {
if (opt.seconds === seconds)
return opt.label;
}
return customIntervalLabel;
}
function intervalSecondsFor(label) {
for (const opt of intervalOptions) {
if (opt.label === label)
return opt.seconds;
}
return 86400;
}
function lastCheckedText() {
const last = SystemUpdateService.lastCheckUnix;
if (!last)
return "";
const delta = Math.max(0, nowUnix - last);
if (delta < 90)
return I18n.tr("checked just now");
if (delta < 3600)
return I18n.tr("checked %1m ago", "system update last check time, %1 is minutes").arg(Math.round(delta / 60));
if (delta < 86400)
return I18n.tr("checked %1h ago", "system update last check time, %1 is hours").arg(Math.round(delta / 3600));
return I18n.tr("checked %1d ago", "system update last check time, %1 is days").arg(Math.round(delta / 86400));
}
function countText(count) {
return count === 1 ? I18n.tr("%1 update", "singular, %1 is 1, available system update count").arg(count) : I18n.tr("%1 updates", "plural, %1 is a count of available system updates").arg(count);
}
function installMethodLabel() {
switch (SystemUpdateService.shellInstallMethod) {
case "pacman":
case "rpm":
case "dpkg":
case "xbps":
return SystemUpdateService.shellPackageName || SystemUpdateService.shellInstallMethod;
case "nix":
return "Nix";
default:
return "";
}
}
function channelLabel() {
switch (SystemUpdateService.shellChannel) {
case "stable":
return I18n.tr("Stable", "release channel");
case "git":
return I18n.tr("Git", "release channel that follows the master branch");
default:
return I18n.tr("Unknown");
}
}
function statusTitle() {
switch (true) {
case !SystemUpdateService.sysupdateAvailable:
return I18n.tr("Unavailable");
case SystemUpdateService.isUpgrading:
return I18n.tr("Upgrading...", "system update popout status while packages upgrade");
case SystemUpdateService.isChecking:
return I18n.tr("Checking for updates...");
case SystemUpdateService.hasError:
return I18n.tr("Failed: %1", "system update error status, %1 is the error message").arg(SystemUpdateService.errorMessage);
case !SystemUpdateService.helperAvailable:
return I18n.tr("No supported package manager found.");
case SystemUpdateService.shellUpdateAvailable:
{
const shell = I18n.tr("DMS %1 available", "software updates hero, %1 is the new DMS version").arg(SystemUpdateService.shellUpdateVersion);
return root.systemCount > 0 ? shell + " · " + root.countText(root.systemCount) : shell;
}
case root.systemCount > 0:
return root.countText(root.systemCount);
default:
return I18n.tr("Up to date");
}
}
function statusSubtitle() {
if (SystemUpdateService.rebootRecommended)
return I18n.tr("Reboot recommended", "chip shown after a kernel or systemd upgrade") + " · " + SystemUpdateService.rebootPackages.join(", ");
if (SystemUpdateService.restartPending)
return I18n.tr("Requires restart");
return root.lastCheckedText();
}
readonly property bool offerReboot: SystemUpdateService.rebootRecommended && !SystemUpdateService.isUpgrading && !root.anythingToInstall
readonly property bool needsRestart: SystemUpdateService.restartPending || SystemUpdateService.rebootRecommended
readonly property bool primaryIsCheck: !SystemUpdateService.isUpgrading && !root.anythingToInstall && !root.needsRestart
function primaryLabel() {
switch (true) {
case SystemUpdateService.isUpgrading:
return I18n.tr("Cancel");
case root.anythingToInstall:
return I18n.tr("Install", "install action button");
case root.offerReboot:
return I18n.tr("Reboot", "verb, power menu action");
case SystemUpdateService.restartPending:
return I18n.tr("Restart DMS");
default:
return I18n.tr("Check for updates");
}
}
function primaryIcon() {
switch (true) {
case SystemUpdateService.isUpgrading:
return "stop";
case root.anythingToInstall:
return "system_update_alt";
case root.offerReboot:
case SystemUpdateService.restartPending:
return "restart_alt";
default:
return "refresh";
}
}
function primaryAction() {
switch (true) {
case SystemUpdateService.isUpgrading:
SystemUpdateService.cancelUpdates();
return;
case root.anythingToInstall:
installConfirm.showWithOptions({
title: I18n.tr("Install system updates?", "confirmation dialog title before running the system upgrade"),
message: root.installSummary(),
confirmText: I18n.tr("Install", "install action button"),
onConfirm: () => root.runUpdateAll()
});
return;
case root.offerReboot:
installConfirm.showWithOptions({
title: I18n.tr("Reboot now?", "confirmation dialog title, reboot to finish a system update"),
message: I18n.tr("Save your work first. Open apps will close.", "reboot confirmation after a system update"),
confirmText: I18n.tr("Reboot", "verb, power menu action"),
onConfirm: () => SessionService.reboot()
});
return;
case SystemUpdateService.restartPending:
SystemUpdateService.restartShell();
return;
default:
SystemUpdateService.checkForUpdates();
}
}
function installSummary() {
const pkgs = root.installablePackages;
const bytes = pkgs.reduce((sum, p) => sum + (p.sizeBytes || 0), 0);
const via = SystemUpdateService.useCustomCommand ? SettingsData.updaterCustomCommand.trim().split(/\s+/)[0] : (SystemUpdateService.backends || []).filter(b => pkgs.some(p => p.backend === b.id)).map(b => b.displayName).join(", ");
let text;
if (bytes > 0)
text = I18n.tr("%1 will be downloaded and installed via %2.", "system update confirmation, %1 is a download size, %2 the package managers").arg(Format.formatBytes(bytes)).arg(via);
else if (pkgs.length === 1)
text = I18n.tr("%1 package will be installed via %2.", "singular, %1 is 1, %2 the package managers").arg(1).arg(via);
else
text = I18n.tr("%1 packages will be installed via %2.", "plural, %1 is a package count, %2 the package managers").arg(pkgs.length).arg(via);
if (!root.upgradeRunsInTerminal)
return text;
return text + " " + I18n.tr("Runs in a terminal.", "system update confirmation, the upgrade opens a terminal window");
}
function runUpdateAll(interactive) {
SystemUpdateService.runUpdates({
includeFlatpak: SettingsData.updaterIncludeFlatpak,
includeAUR: SettingsData.updaterAllowAUR,
terminal: SessionData.terminalOverride,
interactive: interactive === true
});
packagesExpanded = false;
if (upgradeInWindow && interactive !== true)
PopoutService.showSystemUpdateModal();
}
SettingsPage {
id: mainColumn
SettingsHeroCard {
M3WaveProgress {
width: parent.width
height: Theme.spacingM
visible: SystemUpdateService.isUpgrading
isPlaying: visible
value: 1
trackColor: "transparent"
playheadColor: "transparent"
}
}
SettingsCard {
width: parent.width
title: "DankMaterialShell"
iconName: "deployed_code"
settingKey: "softwareUpdatesShell"
tags: ["dms", "shell", "version", "channel", "restart"]
SettingsRow {
readonly property bool failed: SystemUpdateService.hasError && !root.busy
title: root.statusTitle()
titleColor: failed ? Theme.error : contentColor
subtitle: root.statusSubtitle()
iconName: failed ? "error_outline" : root.needsRestart ? "restart_alt" : "system_update_alt"
iconColor: failed ? Theme.error : root.needsRestart ? Theme.warning : Theme.primary
DActionButton {
anchors.verticalCenter: parent.verticalCenter
visible: SystemUpdateService.sysupdateAvailable && !root.primaryIsCheck
enabled: !root.busy
buttonSize: Theme.buttonHeightS
iconName: "refresh"
iconColor: Theme.surfaceText
backgroundColor: SettingsMetrics.controlSurface
tooltipText: I18n.tr("Check for updates")
onClicked: SystemUpdateService.checkForUpdates()
}
DButton {
anchors.verticalCenter: parent.verticalCenter
visible: SystemUpdateService.sysupdateAvailable
buttonHeight: Theme.buttonHeightS
text: root.primaryLabel()
iconName: root.primaryIcon()
busy: SystemUpdateService.isChecking
enabled: !SystemUpdateService.isChecking
backgroundColor: Theme.primary
textColor: Theme.onPrimary
onClicked: root.primaryAction()
}
}
SettingsRow {
title: I18n.tr("Version")
iconName: "tag"
subtitle: {
const running = SystemUpdateService.shellRunning || ShellVersionService.shellVersion;
const to = SystemUpdateService.shellUpdateVersion;
if (to)
return running + " → " + to;
return running;
}
trailingBadge: root.installMethodLabel()
}
SettingsRow {
title: I18n.tr("Channel")
iconName: "alt_route"
subtitle: root.channelLabel()
trailingBadge: SystemUpdateService.shellManagedExternally ? I18n.tr("Managed by Nix", "badge on the DMS update row when the shell is installed through Nix") : ""
}
SettingsRow {
readonly property var master: SystemUpdateService.releases?.master ?? null
visible: SystemUpdateService.shellChannel === "git" && SystemUpdateService.commitsBehind >= 0
title: I18n.tr("Master branch", "git channel row, the branch the build tracks")
subtitle: {
const date = new Date(master?.date ?? NaN);
return isNaN(date) ? "" : I18n.tr("Last commit %1", "git channel row, %1 is a localized date").arg(date.toLocaleDateString(Qt.locale(), Locale.ShortFormat));
}
iconName: "commit"
// Drawn in the slot so it precedes the button; trailingBadge would land after it
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: SystemUpdateService.commitsBehind === 0 ? I18n.tr("Up to date") : I18n.tr("%1 behind", "git channel badge, %1 is the number of commits master is ahead").arg(SystemUpdateService.commitsBehind)
font.pixelSize: Theme.fontSizeSmall
color: SystemUpdateService.commitsBehind === 0 ? Theme.surfaceVariantText : Theme.primary
}
DActionButton {
anchors.verticalCenter: parent.verticalCenter
buttonSize: Theme.buttonHeightS
iconName: "open_in_browser"
iconColor: Theme.primary
backgroundColor: SettingsMetrics.controlSurface
tooltipText: I18n.tr("View on GitHub", "link to the release on GitHub")
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/DankMaterialShell/commits/master")
}
}
SettingsNavRow {
visible: root.notesRelease !== null
title: I18n.tr("Release notes", "settings page and card title, notes for a DMS release")
iconName: "auto_awesome"
hint: root.notesRelease?.codename ? "v" + root.notesRelease.version + " · " + root.notesRelease.codename : "v" + (root.notesRelease?.version ?? "")
onClicked: keyboard => root.parentModal?.navigateTo("updater_changelog", keyboard)
}
SettingsRow {
visible: SystemUpdateService.restartPending
title: I18n.tr("Requires restart")
iconName: "restart_alt"
iconColor: Theme.warning
subtitle: SystemUpdateService.shellInstalled ? I18n.tr("Installed %1, still running %2", "%1 is the installed version, %2 the running one").arg(SystemUpdateService.shellInstalled).arg(SystemUpdateService.shellRunning) : I18n.tr("A newer dms binary is installed.", "software updates row subtitle, the running shell is older than the installed binary")
}
}
SettingsCard {
width: parent.width
visible: SystemUpdateService.sysupdateAvailable
title: I18n.tr("System packages", "software updates card title, package manager updates")
iconName: "inventory_2"
settingKey: "softwareUpdatesSystem"
tags: ["system", "packages", "flatpak", "aur"]
SettingsRow {
title: {
if (!SystemUpdateService.helperAvailable)
return I18n.tr("No supported package manager found.");
if (root.systemCount === 0)
return I18n.tr("Up to date");
const base = root.countText(root.systemCount);
return root.flatpakCount > 0 ? base + " · " + I18n.tr("%1 Flatpak", "count of flatpak updates, %1 is a number").arg(root.flatpakCount) : base;
}
subtitle: {
const distro = SystemUpdateService.distributionPretty || SystemUpdateService.distribution;
const names = (SystemUpdateService.backends || []).map(b => b.displayName).join(", ");
return distro && names ? distro + " · " + names : distro || names;
}
iconName: !SystemUpdateService.helperAvailable ? "error_outline" : root.systemCount === 0 ? "check_circle" : "package_2"
iconColor: SystemUpdateService.helperAvailable ? Theme.primary : Theme.error
clickable: root.systemCount > 0
onClicked: root.packagesExpanded = !root.packagesExpanded
DIcon {
anchors.verticalCenter: parent.verticalCenter
visible: root.systemCount > 0
name: "expand_more"
size: Theme.iconSize
color: Theme.surfaceVariantText
rotation: root.packagesExpanded ? 180 : 0
Behavior on rotation {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
}
}
Column {
width: parent?.width ?? 0
spacing: Theme.groupedListGap
Repeater {
model: root.packagesExpanded && !SystemUpdateService.isUpgrading ? SystemUpdateService.systemUpdates.slice(0, root.packageListCap) : []
delegate: SettingsRow {
id: packageRow
required property var modelData
title: modelData.name || ""
subtitle: {
const from = modelData.fromVersion || "";
const to = modelData.toVersion || "";
const version = from && to ? from + " → " + to : to || from;
const repo = modelData.repo || "";
return repo && version ? repo + " · " + version : repo || version;
}
DActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "visibility_off"
visible: SystemUpdateService.canIgnorePackage(packageRow.modelData)
tooltipText: I18n.tr("Ignore package", "tooltip, exclude a package from system updates")
onClicked: SystemUpdateService.ignorePackage(packageRow.modelData.name)
}
}
}
}
SettingsRow {
visible: root.packagesExpanded && !SystemUpdateService.isUpgrading && root.systemCount > root.packageListCap
title: I18n.tr("and %1 more", "%1 is the number of update rows not shown").arg(root.systemCount - root.packageListCap)
titleColor: Theme.surfaceVariantText
}
SettingsRow {
visible: root.upgradeInWindow && SystemUpdateService.isUpgrading
title: I18n.tr("Upgrading...", "system update popout status while packages upgrade")
iconName: "open_in_new"
DButton {
anchors.verticalCenter: parent.verticalCenter
buttonHeight: Theme.buttonHeightS
text: I18n.tr("Show window", "button, brings back the floating system update window")
iconName: "open_in_new"
backgroundColor: SettingsMetrics.controlSurface
textColor: Theme.surfaceText
onClicked: PopoutService.showSystemUpdateModal()
}
}
SettingsRow {
visible: (SystemUpdateService.isUpgrading && !root.upgradeInWindow) || (root.upgradeFailed && !root.upgradeRunsInTerminal)
body: Item {
readonly property real lineHeight: logText.implicitHeight / Math.max(1, logText.lineCount)
width: parent.width
height: Math.min(logText.implicitHeight, Math.floor(root.logViewHeight / lineHeight) * lineHeight)
clip: true
UpgradeLogText {
id: logText
anchors.bottom: parent.bottom
width: parent.width
placeholder: root.upgradeRunsInTerminal ? I18n.tr("Running in terminal") : ""
lines: (SystemUpdateService.recentLog || []).slice(-root.logTailLines)
}
}
}
SettingsRow {
visible: SystemUpdateService.hasError && !root.busy
title: I18n.tr("Failed: %1", "system update error status, %1 is the error message").arg(SystemUpdateService.errorMessage)
titleColor: Theme.error
subtitle: SystemUpdateService.errorHint
iconName: "error_outline"
iconColor: Theme.error
DButton {
anchors.verticalCenter: parent.verticalCenter
visible: root.upgradeFailed && !root.upgradeRunsInTerminal
text: I18n.tr("Open in terminal")
iconName: "terminal"
backgroundColor: SettingsMetrics.controlSurface
textColor: Theme.surfaceText
onClicked: root.runUpdateAll(true)
}
}
}
SettingsCard {
width: parent.width
title: I18n.tr("Options")
iconName: "tune"
settingKey: "systemUpdater"
collapsible: true
expanded: false
SettingsDropdownRow {
settingKey: "systemUpdaterCheckInterval"
resetKeys: ["updaterIntervalSeconds"]
resetByKeys: false
onResetRequested: {
root.customIntervalSelected = false;
SettingsData.resetToDefault(["updaterIntervalSeconds"]);
SystemUpdateService.setInterval(SettingsData.updaterIntervalSeconds);
}
tags: ["interval", "poll", "frequency"]
text: I18n.tr("Check interval")
options: root.intervalOptions.map(o => o.label).concat([root.customIntervalLabel])
currentValue: root.customIntervalSelected ? root.customIntervalLabel : root.intervalLabelFor(SettingsData.updaterIntervalSeconds)
onValueChanged: label => {
if (label === root.customIntervalLabel) {
root.customIntervalSelected = true;
return;
}
root.customIntervalSelected = false;
const secs = root.intervalSecondsFor(label);
SettingsData.set("updaterIntervalSeconds", secs);
SystemUpdateService.setInterval(secs);
}
}
SettingsTextFieldRow {
id: customIntervalField
leftIconName: "timer"
visible: root.customIntervalSelected
text: I18n.tr("Custom interval in minutes (minimum 5)")
placeholderText: "1440"
value: Math.round(SettingsData.updaterIntervalSeconds / 60).toString()
validator: IntValidator {
bottom: 5
}
onValueEdited: value => {
const minutes = parseInt(value, 10);
if (isNaN(minutes) || minutes < 5)
return;
const seconds = minutes * 60;
SettingsData.set("updaterIntervalSeconds", seconds);
SystemUpdateService.setInterval(seconds);
}
}
SettingsToggleRow {
settingKey: "systemUpdaterCheckOnStart"
tags: ["startup", "check", "boot"]
text: I18n.tr("Check on startup")
checked: SettingsData.updaterCheckOnStart
onToggled: checked => SettingsData.set("updaterCheckOnStart", checked)
}
SettingsToggleRow {
settingKey: "systemUpdaterPauseOnBattery"
tags: ["battery", "power", "pause"]
visible: BatteryService.batteryAvailable
text: I18n.tr("Pause checks on battery", "software updates toggle, skip background checks on battery power")
checked: SettingsData.updaterPauseOnBattery
onToggled: checked => SettingsData.set("updaterPauseOnBattery", checked)
}
SettingsToggleRow {
settingKey: "systemUpdaterNotify"
tags: ["notify", "notification", "alert"]
text: I18n.tr("Notify me on new updates", "software updates toggle")
description: I18n.tr("Checks in the background at the check interval. Notifies only when the count grows.", "notify on new updates toggle description")
checked: SettingsData.updaterNotify
onToggled: checked => SettingsData.set("updaterNotify", checked)
}
SettingsDropdownRow {
settingKey: "systemUpdaterNotifyMin"
tags: ["notify", "notification", "frequency", "throttle"]
resetKeys: ["updaterNotifyMinSeconds"]
visible: SettingsData.updaterNotify
text: I18n.tr("Notify at most", "software updates dropdown label, how often update notifications may repeat")
options: root.notifyOptions.map(o => o.label)
currentValue: (root.notifyOptions.find(o => o.seconds === SettingsData.updaterNotifyMinSeconds) ?? root.notifyOptions[root.notifyOptions.length - 1]).label
onValueChanged: label => {
const opt = root.notifyOptions.find(o => o.label === label);
if (opt)
SettingsData.set("updaterNotifyMinSeconds", opt.seconds);
}
}
SettingsToggleRow {
settingKey: "systemUpdaterFlatpak"
tags: ["flatpak", "include"]
text: I18n.tr("Include Flatpak updates")
visible: (SystemUpdateService.backends || []).some(b => b.repo === "flatpak")
checked: SettingsData.updaterIncludeFlatpak
onToggled: checked => SettingsData.set("updaterIncludeFlatpak", checked)
}
SettingsToggleRow {
settingKey: "systemUpdaterAUR"
tags: ["aur", "paru", "yay", "shelly"]
text: I18n.tr("Include AUR updates")
visible: (SystemUpdateService.backends || []).some(b => ["paru", "yay", "shelly"].includes(b.id))
checked: SettingsData.updaterAllowAUR
onToggled: checked => SettingsData.set("updaterAllowAUR", checked)
}
SettingsToggleRow {
settingKey: "systemUpdaterReopenAfterUpgrade"
tags: ["reopen", "popout", "terminal", "upgrade"]
text: I18n.tr("Reopen panel after update")
visible: root.upgradeRunsInTerminal
checked: SettingsData.updaterReopenAfterUpgrade
onToggled: checked => SettingsData.set("updaterReopenAfterUpgrade", checked)
}
SettingsToggleRow {
settingKey: "systemUpdaterUpgradeInWindow"
tags: ["window", "popout", "floating", "log", "output"]
text: I18n.tr("Show upgrade in a window", "software updates toggle")
description: I18n.tr("Opens a floating window with the live output", "show upgrade in a window toggle description")
visible: !root.upgradeRunsInTerminal
checked: SettingsData.updaterUpgradeInWindow
onToggled: checked => SettingsData.set("updaterUpgradeInWindow", checked)
}
TerminalPickerRow {}
}
SettingsCard {
id: ignoredPackagesCard
width: parent.width
iconName: "visibility_off"
title: I18n.tr("Ignored packages")
settingKey: "systemUpdaterIgnoredPackages"
tags: ["system", "update", "package", "ignore"]
collapsible: true
expanded: false
property bool errorIsInvalidName: false
function addIgnoredPackage() {
const name = newIgnoredPackageField.value.trim();
if (name === "")
return;
errorIsInvalidName = !SystemUpdateService.isValidIgnoredName(name);
if (errorIsInvalidName) {
ignoredPackageError.visible = true;
return;
}
ignoredPackageError.visible = !SystemUpdateService.ignorePackage(name);
if (ignoredPackageError.visible)
return;
newIgnoredPackageField.value = "";
}
SettingsTextFieldRow {
id: newIgnoredPackageField
leftIconName: "inventory_2"
text: I18n.tr("Name")
description: {
if (SettingsData.updaterUseCustomCommand)
return I18n.tr("Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.");
if (SystemUpdateService.pkgManager === "shelly")
return I18n.tr("With Shelly, only Flatpak packages in the current update list can be ignored.");
return (SettingsData.updaterIgnoredPackages || []).length > 0 ? I18n.tr("Ignored packages are hidden from the updater and skipped by 'Update All'.") : I18n.tr("No packages ignored. Add one here or hover an update in the popout and click the hide button.");
}
placeholderText: I18n.tr("Package name (e.g., docker)")
onAccepted: ignoredPackagesCard.addIgnoredPackage()
onValueEdited: ignoredPackageError.visible = false
actions: DIconButton {
variant: "filled"
iconName: "add"
tooltipText: I18n.tr("Ignore package", "tooltip, exclude a package from system updates")
enabled: newIgnoredPackageField.value.trim() !== ""
onClicked: ignoredPackagesCard.addIgnoredPackage()
}
}
SettingsRow {
id: ignoredPackageError
visible: false
title: ignoredPackagesCard.errorIsInvalidName ? I18n.tr("Invalid package name — letters, digits and @._+:/- only.", "ignored packages validation error") : I18n.tr("With Shelly, only Flatpak packages in the current update list can be ignored.")
titleColor: Theme.error
}
Repeater {
model: SettingsData.updaterIgnoredPackages
delegate: SettingsRow {
id: ignoredRow
required property string modelData
required property int index
title: modelData
iconName: "visibility_off"
DActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "delete"
iconColor: Theme.error
tooltipText: I18n.tr("Stop ignoring %1", "system updater button tooltip, %1 is the package name").arg(ignoredRow.modelData)
onClicked: SystemUpdateService.unignorePackage(ignoredRow.modelData)
}
}
}
}
SettingsCard {
width: parent.width
iconName: "terminal"
title: I18n.tr("Advanced")
settingKey: "systemUpdaterAdvanced"
collapsible: true
expanded: false
SettingsToggleRow {
settingKey: "systemUpdaterCustomCommand"
tags: ["custom", "command", "terminal"]
text: I18n.tr("Use custom command")
checked: SettingsData.updaterUseCustomCommand
onToggled: checked => {
if (!checked) {
updaterCustomCommand.value = "";
updaterTerminalCustomClass.value = "";
SettingsData.set("updaterCustomCommand", "");
SettingsData.set("updaterTerminalAdditionalParams", "");
}
SettingsData.set("updaterUseCustomCommand", checked);
}
}
SettingsNoteRow {
enabled: SettingsData.updaterUseCustomCommand
text: I18n.tr("Custom command and terminal params are split on whitespace; paths with spaces will break.")
}
SettingsTextFieldRow {
id: updaterCustomCommand
leftIconName: "terminal"
visible: SettingsData.updaterUseCustomCommand
resetKeys: ["updaterCustomCommand"]
text: I18n.tr("Custom update command")
placeholderText: "topgrade --no-retry"
value: SettingsData.updaterCustomCommand
onValueEdited: value => SettingsData.set("updaterCustomCommand", value.trim())
}
SettingsTextFieldRow {
id: updaterTerminalCustomClass
leftIconName: "terminal"
visible: SettingsData.updaterUseCustomCommand
resetKeys: ["updaterTerminalAdditionalParams"]
text: I18n.tr("Terminal additional parameters")
placeholderText: "-T updater"
value: SettingsData.updaterTerminalAdditionalParams
onValueEdited: value => SettingsData.set("updaterTerminalAdditionalParams", value.trim())
}
}
}
ConfirmDialogOverlay {
id: installConfirm
parent: root.parentModal?.modalFocusScope ?? root
}
}