diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index 834440fc9..205191f10 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -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 diff --git a/quickshell/Modals/DankLauncherV2/ControllerUtils.js b/quickshell/Modals/DankLauncherV2/ControllerUtils.js index f68f95392..b1ee37bd9 100644 --- a/quickshell/Modals/DankLauncherV2/ControllerUtils.js +++ b/quickshell/Modals/DankLauncherV2/ControllerUtils.js @@ -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; diff --git a/quickshell/Modals/DankLauncherV2/ItemTransformers.js b/quickshell/Modals/DankLauncherV2/ItemTransformers.js index f5a6eb244..f8f0f6ded 100644 --- a/quickshell/Modals/DankLauncherV2/ItemTransformers.js +++ b/quickshell/Modals/DankLauncherV2/ItemTransformers.js @@ -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", diff --git a/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml b/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml index 7021006a1..4b816f6b1 100644 --- a/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml +++ b/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml @@ -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 } diff --git a/quickshell/Services/AppSearchService.qml b/quickshell/Services/AppSearchService.qml index 7b3e2253f..b2ad9aadc 100644 --- a/quickshell/Services/AppSearchService.qml +++ b/quickshell/Services/AppSearchService.qml @@ -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)); + } }