feat(launcher): add flatpak uninstall option in context menu (#3340)

* feat(launcher): add flatpak uninstall option to context menu

* fix(launcher): reuse existing catalog terms for uninstall toasts
This commit is contained in:
Huỳnh Thiện Lộc authored and GitHub committed 2026-09-10 10:12:25 -04:00
1 parent cf74c10561
commit 3adfa918c1
5 files changed
+168 -6

No files matched your search

+33
View File
@@ -381,6 +381,39 @@ Item {
}
}
LazyLoader {
id: uninstallAppConfirmLoader
active: false
readonly property ConfirmModal loadedModal: item as ConfirmModal
ConfirmModal {
id: uninstallAppConfirm
}
}
Connections {
target: AppSearchService
function onUninstallAppConfirmRequested(appId, appName, flatpakId) {
uninstallAppConfirmLoader.active = true;
const showDialog = () => {
if (uninstallAppConfirmLoader.loadedModal) {
uninstallAppConfirmLoader.loadedModal.showWithOptions({
title: I18n.tr("Uninstall %1?", "modal title for app uninstallation").arg(appName),
message: I18n.tr("%1 will be removed from your system.", "modal confirmation text for uninstalling an app").arg(appName),
confirmText: I18n.tr("Uninstall", "confirm button label to proceed with uninstallation"),
cancelText: I18n.tr("Cancel"),
confirmColor: Theme.error,
onConfirm: () => AppSearchService.uninstallFlatpak(appId, appName, flatpakId)
});
}
};
if (uninstallAppConfirmLoader.loadedModal)
showDialog();
else
Qt.callLater(showDialog);
}
}
LazyLoader {
id: notificationCenterLoader
@@ -134,6 +134,37 @@ function classifyAppSource(app) {
return "system";
}
function extractFlatpakAppId(app) {
if (!app)
return "";
var execRaw = app.execString || app.exec || "";
if (app.command && app.command.length > 0) {
var isRun = false;
for (var i = 0; i < app.command.length; i++) {
var arg = String(app.command[i]);
if (arg === "run") {
isRun = true;
continue;
}
if (isRun && !arg.startsWith("-")) {
return arg;
}
}
}
if (execRaw) {
var match = execRaw.match(/\bflatpak\s+run\s+(?:--?[^\s]+\s+)*([a-zA-Z0-9_\-\.]+)/);
if (match && match[1])
return match[1];
}
var id = app.id || "";
if (id.endsWith(".desktop"))
return id.slice(0, -8);
return id;
}
function sortPluginIdsByOrder(pluginIds, order) {
if (!order || order.length === 0)
return pluginIds;
@@ -28,6 +28,7 @@ function transformApp(app, override, defaultActions, primaryActionLabel) {
keywords: app.keywords || [],
actions: actions,
source: Utils.classifyAppSource(app),
flatpakId: Utils.classifyAppSource(app) === "flatpak" ? Utils.extractFlatpakAppId(app) : "",
primaryAction: {
name: primaryActionLabel,
icon: "open_in_new",
@@ -6,6 +6,7 @@ import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
import "ControllerUtils.js" as Utils
Item {
id: root
@@ -99,6 +100,8 @@ Item {
}
readonly property bool isPinned: appId ? SessionData.isPinnedApp(appId) : false
readonly property bool isRegularApp: item?.type === "app" && !item.isCore && desktopEntry
readonly property bool isFlatpakApp: isRegularApp && (item?.source === "flatpak" || !!item?.flatpakId || Utils.classifyAppSource(desktopEntry) === "flatpak")
readonly property string flatpakAppId: item?.flatpakId || (isFlatpakApp ? Utils.extractFlatpakAppId(desktopEntry) : "")
readonly property bool isPluginItem: item?.type === "plugin"
function getPluginContextMenuActions() {
@@ -231,6 +234,19 @@ Item {
action: launchApp
});
if (isFlatpakApp && flatpakAppId) {
items.push({
type: "separator"
});
items.push({
type: "item",
icon: "delete",
text: I18n.tr("Uninstall…", "context menu action to uninstall app"),
isDestructive: true,
action: uninstallCurrentApp
});
}
return items;
}
@@ -307,6 +323,18 @@ Item {
hide();
}
function uninstallCurrentApp() {
if (!isFlatpakApp || !flatpakAppId)
return;
const targetFlatpakId = flatpakAppId;
const appName = item?.name || desktopEntry?.name || flatpakAppId;
const currentAppId = appId;
hide();
if (controller)
controller.itemExecuted();
AppSearchService.requestUninstallFlatpak(currentAppId, appName, targetFlatpakId);
}
function launchApp() {
if (isCoreApp) {
if (!coreAppData)
@@ -619,10 +647,14 @@ Item {
height: parent.height
radius: Theme.cornerRadius
color: {
const isDestructive = menuItemDelegate.modelData?.isDestructive ?? false;
if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) {
return Theme.primaryPressed;
return isDestructive ? Theme.withAlpha(Theme.error, 0.25) : Theme.primaryPressed;
}
return itemMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(BlurService.hoverColor(Theme.widgetBaseHoverColor), 0);
if (itemMouseArea.containsMouse) {
return isDestructive ? Theme.withAlpha(Theme.error, 0.15) : BlurService.hoverColor(Theme.widgetBaseHoverColor);
}
return Theme.withAlpha(BlurService.hoverColor(Theme.widgetBaseHoverColor), 0);
}
Row {
@@ -642,8 +674,8 @@ Item {
visible: (menuItemDelegate.modelData?.icon ?? "").length > 0
name: menuItemDelegate.modelData?.icon ?? ""
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
color: (menuItemDelegate.modelData?.isDestructive ?? false) ? Theme.error : Theme.surfaceText
opacity: (menuItemDelegate.modelData?.isDestructive ?? false) ? 1.0 : 0.7
anchors.verticalCenter: parent.verticalCenter
}
}
@@ -651,7 +683,7 @@ Item {
StyledText {
text: menuItemDelegate.modelData.text || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
color: (menuItemDelegate.modelData?.isDestructive ?? false) ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
@@ -661,7 +693,7 @@ Item {
DankRipple {
id: menuItemRipple
rippleColor: Theme.surfaceText
rippleColor: (menuItemDelegate.modelData?.isDestructive ?? false) ? Theme.error : Theme.surfaceText
cornerRadius: Theme.cornerRadius
}
+65
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
@@ -1004,4 +1005,68 @@ Singleton {
log.warn("Error setting category on plugin", pluginId, ":", e);
}
}
signal uninstallAppConfirmRequested(string appId, string appName, string flatpakId)
property string _uninstallingAppId: ""
property string _uninstallingAppName: ""
property string _uninstallingStderr: ""
Process {
id: flatpakUninstallProc
running: false
command: []
stderr: StdioCollector {
onStreamFinished: {
root._uninstallingStderr = (text || "").trim();
}
}
onExited: exitCode => {
const appName = root._uninstallingAppName;
const appId = root._uninstallingAppId;
const err = root._uninstallingStderr;
root._uninstallingAppName = "";
root._uninstallingAppId = "";
root._uninstallingStderr = "";
if (exitCode === 0) {
ToastService.showInfo(I18n.tr("Uninstalled: %1", "uninstallation success").arg(appName));
if (appId) {
SessionData.removePinnedApp(appId);
SessionData.removeBarPinnedApp(appId);
}
root.refreshApplications();
} else {
ToastService.showError(
I18n.tr("Uninstall failed: %1", "uninstallation error").arg(appName),
err
);
}
}
}
function requestUninstallFlatpak(appId, appName, flatpakId) {
if (!flatpakId)
return;
uninstallAppConfirmRequested(appId, appName, flatpakId);
}
function uninstallFlatpak(appId, appName, flatpakId) {
if (!flatpakId)
return;
if (flatpakUninstallProc.running) {
ToastService.showWarning(I18n.tr("An uninstallation is already in progress", "toast warning message"));
return;
}
_uninstallingAppId = appId || "";
_uninstallingAppName = appName || flatpakId;
_uninstallingStderr = "";
flatpakUninstallProc.command = ["flatpak", "uninstall", "-y", "--app", flatpakId];
flatpakUninstallProc.running = true;
ToastService.showInfo(I18n.tr("Uninstalling: %1", "uninstallation progress").arg(_uninstallingAppName));
}
}