pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell import qs.Common import qs.Services import qs.DCommon.Widgets import qs.Widgets import "../../Common/QmlUtils.js" as QmlUtils import qs.Modals.Common import qs.Modules.Settings.Widgets Column { id: root property var parentModal: null property bool _pluginBrowserShowQueued: false property string searchQuery: "" property string filterOverride: "" readonly property var filterKeys: ["all", "enabled", "disabled", "updates"] readonly property int filterIndex: filterKeys.indexOf(filterOverride || CacheData.pluginViewFilter) property string actionError: "" readonly property bool checkingUpdates: DMSService.checkingPluginUpdates readonly property var sortKeys: ["name", "author", "modified"] readonly property int sortIndex: sortKeys.indexOf(CacheData.pluginViewSort.by) readonly property bool descending: CacheData.pluginViewSort.descending property var uninstalling: ({}) property var uninstallErrors: ({}) property var updateErrors: ({}) property string updatingPluginId: "" readonly property string pendingRevealPluginId: PluginService.pendingSettingsRevealPluginId readonly property var sortOptions: [I18n.tr("Name"), I18n.tr("Author", "installed plugins sort option, plugin author"), I18n.tr("Last modified", "plugin directory modification time")] readonly property var filterOptions: [I18n.tr("All"), I18n.tr("Enabled"), I18n.tr("Disabled"), I18n.tr("Update available", "plugin row badge")] FontMetrics { id: pluginTitleMetrics font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeMedium font.weight: Theme.fontWeightMedium } FontMetrics { id: pluginDescriptionMetrics font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall } readonly property var filteredPlugins: plugins.filter(row => { const loaded = PluginService.loadedPlugins[row.pluginId] !== undefined; if (filterIndex === 1 && !loaded) return false; if (filterIndex === 2 && loaded) return false; if (filterIndex === 3 && !hasUpdate(row)) return false; const plugin = PluginService.availablePlugins[row.pluginId]; return [row.text, row.hint, plugin?.author, row.pluginId].join(" ").toLowerCase().includes(searchQuery.trim().toLowerCase()); }).sort((a, b) => { let comparison = 0; switch (sortIndex) { case 1: comparison = (PluginService.availablePlugins[a.pluginId]?.author || "").localeCompare(PluginService.availablePlugins[b.pluginId]?.author || ""); break; case 2: comparison = modifiedAt(a) - modifiedAt(b); break; default: comparison = a.text.localeCompare(b.text); } if (comparison !== 0) return descending ? -comparison : comparison; return a.text.localeCompare(b.text) || a.pluginId.localeCompare(b.pluginId); }) readonly property var plugins: SettingsTabs.pluginHubRows readonly property var updateCheckErrors: (DMSService.installedPlugins || []).filter(plugin => plugin.updateError).reduce((errors, plugin) => { errors[plugin.id] = plugin.updateError; return errors; }, {}) readonly property var pluginsWithUpdates: (DMSService.installedPlugins || []).filter(plugin => plugin.hasUpdate === true) readonly property var incompatiblePlugins: { PluginService.loadedPlugins; ShellVersionService.semverVersion; return PluginService.getIncompatiblePlugins(); } width: parent?.width ?? 0 LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.childrenInherit: true spacing: Theme.spacingL onPendingRevealPluginIdChanged: revealTimer.restart() onVisibleChanged: { if (!visible) filterOverride = ""; } Component.onCompleted: { revealTimer.restart(); if (DMSService.dmsAvailable && DMSService.apiVersion >= 8) DMSService.listInstalled(); if (PopoutService.pendingPluginInstall) Qt.callLater(showPluginBrowser); } function revealPlugin(pluginId) { PluginService.pendingSettingsRevealPluginId = pluginId; revealTimer.restart(); } function setFilter(index) { if (index < 0 || index >= filterKeys.length) return; CacheData.set("pluginViewFilter", filterKeys[index]); filterOverride = ""; } Timer { id: revealTimer interval: 0 onTriggered: { if (!root.visible || !root.pendingRevealPluginId || pluginBrowserLoader.item?.visible) return; if (!PluginService.availablePlugins[root.pendingRevealPluginId]) return; root.searchQuery = ""; root.filterOverride = "all"; const key = "installedPlugin:" + root.pendingRevealPluginId; const entry = SettingsSearchService.registeredCards[key]; if (!entry) return; SettingsSearchService.navigateToSection(key); entry.item.focusTarget.forceActiveFocus(Qt.TabFocusReason); PluginService.pendingSettingsRevealPluginId = ""; } } function requestUpdate(row) { if (updatingPluginId || pluginUpdatesDialogItem.isUpdating || uninstalling[row.pluginId]) return; const plugin = DMSService.installedPlugins.find(plugin => plugin.id === row.pluginId || plugin.name === row.text); confirmUpdates([plugin || { id: row.pluginId, name: row.text }]); } function confirmUpdates(plugins) { if (!plugins.length || updatingPluginId || pluginUpdatesDialogItem.isUpdating || Object.keys(uninstalling).length) return; const single = plugins.length === 1; updateConfirm.showWithOptions({ title: single ? I18n.tr("Update %1?", "plugin update confirmation").arg(plugins[0].name) : I18n.tr("Update All"), message: I18n.tr("Plugin updates can change the code running in your session. Review the changes before updating.", "plugin update audit reminder"), reviewUrl: single ? plugins[0].diffUrl || plugins[0].repo || "" : "", confirmText: single ? I18n.tr("Update") : I18n.tr("Update All"), onConfirm: () => { if (single) { updatePlugin({ pluginId: plugins[0].id, text: plugins[0].name }); return; } pluginUpdatesDialogItem.updatePlugins(plugins); } }); } ConfirmDialogOverlay { id: updateConfirm parent: root.parentModal?.modalFocusScope ?? root.parent } function updatePlugin(row) { if (updatingPluginId || pluginUpdatesDialogItem.isUpdating || uninstalling[row.pluginId]) return; const id = row.pluginId; updatingPluginId = id; updateErrors = Object.assign({}, updateErrors, { [id]: "" }); PluginService.updatePlugin(id, response => { updatingPluginId = ""; if (response.error) { updateErrors = Object.assign({}, updateErrors, { [id]: response.error }); revealPlugin(id); return; } pluginUpdatesDialogItem.updatesList = pluginUpdatesDialogItem.updatesList.filter(plugin => plugin.id !== id); revealPlugin(id); }); } function setSort(index) { if (index < 0 || index >= sortKeys.length) return; CacheData.set("pluginViewSort", { by: sortKeys[index], descending: index === sortIndex ? descending : index === 2 }); } function modifiedAt(row) { const directory = PluginService.availablePlugins[row.pluginId]?.pluginDirectory; return PluginService.directoryModifiedTimes[directory] || 0; } function checkUpdates() { if (!DMSService.dmsAvailable || DMSService.apiVersion < 8) return; actionError = ""; DMSService.listInstalled(undefined, true); } function requestUninstall(row) { if (uninstalling[row.pluginId] || updatingPluginId || pluginUpdatesDialogItem.isUpdating) return; uninstallConfirm.showWithOptions({ title: I18n.tr("Uninstall"), message: I18n.tr("Uninstall %1?", "plugin removal confirmation").arg(row.text), confirmText: I18n.tr("Uninstall"), confirmColor: Theme.error, onConfirm: () => uninstall(row) }); } function uninstall(row) { const id = row.pluginId; uninstalling = Object.assign({}, uninstalling, { [id]: true }); uninstallErrors = Object.assign({}, uninstallErrors, { [id]: "" }); DMSService.uninstall(id, response => { const pending = Object.assign({}, uninstalling); delete pending[id]; uninstalling = pending; if (response.error) { uninstallErrors = Object.assign({}, uninstallErrors, { [id]: response.error }); return; } PluginService.scanPlugins(); }); } ConfirmDialogOverlay { id: uninstallConfirm parent: root.parentModal?.modalFocusScope ?? root.parent } function showPluginBrowser() { pluginBrowserLoader.active = true; if (pluginBrowserLoader.item) { pluginBrowserLoader.item.show(); return; } _pluginBrowserShowQueued = true; } function hasUpdate(row) { return pluginsWithUpdates.some(plugin => plugin.id === row.pluginId || plugin.name === row.text); } function setEnabled(row, enabled) { actionError = ""; if (enabled) { PluginService.enablePlugin(row.pluginId, ok => { if (ok) return; const error = PluginService.pluginLoadErrors[row.pluginId]; actionError = error ? [error.title, error.details].filter(Boolean).join("\n") : I18n.tr("Failed to enable plugin: %1").arg(row.text); }); return; } if (!PluginService.disablePlugin(row.pluginId)) actionError = I18n.tr("Failed to disable plugin: %1").arg(row.text); } Connections { target: PluginService function onRegistryInstallFinished(pluginId, success) { if (success || PluginService.availablePlugins[pluginId]) root.revealPlugin(pluginId); } function onPluginListUpdated() { revealTimer.restart(); } } Connections { target: PopoutService function onPendingPluginInstallChanged() { if (PopoutService.pendingPluginInstall) root.showPluginBrowser(); } } Connections { target: pluginBrowserLoader.item function onVisibleChanged() { if (!pluginBrowserLoader.item.visible) revealTimer.restart(); } } Connections { target: pluginBrowserLoader function onItemChanged() { if (!root._pluginBrowserShowQueued || !pluginBrowserLoader.item) return; root._pluginBrowserShowQueued = false; pluginBrowserLoader.item.show(); } } LazyLoader { id: pluginBrowserLoader active: false PluginBrowser { id: pluginBrowserItem Component.onCompleted: { pluginBrowserItem.parentModal = root.parentModal; } } } PluginUpdatesDialog { id: pluginUpdatesDialogItem width: parent.width operationsBlocked: Object.keys(root.uninstalling).length > 0 || root.updatingPluginId !== "" onUpdatesRequested: plugins => root.confirmUpdates(plugins) onPluginUpdated: pluginId => root.revealPlugin(pluginId) } SettingsCard { settingKey: "pluginManagerUnavailable" visible: !DMSService.dmsAvailable SettingsRow { iconName: "warning" title: I18n.tr("Plugin manager unavailable") subtitle: I18n.tr("The DMS_SOCKET environment variable is not set or the socket is unavailable. Automated plugin management requires the DMS_SOCKET.") } } SettingsCard { settingKey: "pluginsIncompatible" visible: root.incompatiblePlugins.length > 0 SettingsRow { iconName: "error" title: I18n.tr("Incompatible plugins loaded") subtitle: I18n.tr("Some plugins require a newer version of DMS:") + " " + root.incompatiblePlugins.map(plugin => plugin.name + " (" + plugin.requires_dms + ")").join(", ") subtitleColor: Theme.error } } RowLayout { width: parent.width spacing: Theme.spacingS visible: root.plugins.length > 0 Flow { Layout.fillWidth: true Layout.preferredHeight: implicitHeight spacing: Theme.spacingS DButton { text: root.pluginsWithUpdates.length ? I18n.tr("Update All") + " (" + root.pluginsWithUpdates.length + ")" : I18n.tr("Check for updates") iconName: root.pluginsWithUpdates.length ? "download" : "refresh" busy: root.checkingUpdates || pluginUpdatesDialogItem.isUpdating backgroundColor: Theme.secondaryContainer textColor: Theme.onSecondaryContainer maximumWidth: parent.width wrapText: true enabled: DMSService.dmsAvailable && !pluginUpdatesDialogItem.isUpdating && !root.updatingPluginId && Object.keys(root.uninstalling).length === 0 Accessible.description: root.checkingUpdates ? I18n.tr("Checking for updates...") : "" onClicked: { if (root.checkingUpdates) return; if (root.pluginsWithUpdates.length) { pluginUpdatesDialogItem.show(root.pluginsWithUpdates); return; } root.checkUpdates(); } } DIconButton { iconName: "refresh" widthMode: "narrow" tooltipText: I18n.tr("Check for updates") visible: root.pluginsWithUpdates.length > 0 enabled: DMSService.dmsAvailable && !root.checkingUpdates && !pluginUpdatesDialogItem.isUpdating && !root.updatingPluginId onClicked: root.checkUpdates() } } DButton { text: I18n.tr("Manage Registries", "plugin registry management") buttonHeight: Theme.buttonHeightXS horizontalPadding: Theme.spacingS backgroundColor: "transparent" textColor: Theme.primary maximumWidth: root.width / 2 wrapText: true Layout.alignment: Qt.AlignTop Layout.topMargin: (Theme.buttonHeightS - Theme.buttonHeightXS) / 2 onClicked: keyboard => root.parentModal?.navigateTo("plugins_manage", keyboard) } } StyledText { width: parent.width visible: text !== "" text: [root.actionError, DMSService.pluginUpdateCheckError].filter(Boolean).join("\n") color: Theme.error wrapMode: Text.Wrap } DCard { id: storeCard color: SettingsMetrics.rowColor width: parent.width implicitHeight: storeContent.implicitHeight + pad * 2 visible: root.plugins.length === 0 pad: Theme.spacingL ColumnLayout { id: storeContent width: parent.width spacing: Theme.spacingM DIcon { name: "store" size: Theme.iconSizeLarge color: storeCard.accentColor } StyledText { Layout.fillWidth: true text: I18n.tr("Explore Plugins") font.pixelSize: Theme.fontSizeLarge font.weight: Theme.fontWeightMedium color: storeCard.contentColor wrapMode: Text.Wrap } StyledText { Layout.fillWidth: true text: I18n.tr("Extend DankMaterialShell with powerful plugins for widgets, launchers, and more") color: storeCard.mutedColor wrapMode: Text.Wrap } Flow { Layout.fillWidth: true Layout.preferredHeight: implicitHeight spacing: Theme.spacingS DButton { text: I18n.tr("Browse") iconName: "store" backgroundColor: Theme.primary textColor: Theme.onPrimary maximumWidth: parent.width wrapText: true enabled: DMSService.dmsAvailable onClicked: root.showPluginBrowser() } DButton { text: I18n.tr("Manage Registries", "plugin registry management") backgroundColor: "transparent" textColor: storeCard.accentColor maximumWidth: parent.width wrapText: true onClicked: keyboard => root.parentModal?.navigateTo("plugins_manage", keyboard) } } } } Column { width: parent.width spacing: Theme.spacingS visible: root.plugins.length > 0 DSearchField { width: parent.width text: root.searchQuery placeholderText: I18n.tr("Search plugins...", "plugin search placeholder") onTextEdited: root.searchQuery = text } Flow { width: parent.width spacing: Theme.spacingS DSplitButton { id: filterButton text: I18n.tr("Filter") + ": " + root.filterOptions[root.filterIndex] iconName: "filter_list" size: "xs" variant: root.filterIndex > 0 ? "tonal" : "outlined" maximumWidth: parent.width expanded: filterMenu.menuVisible tooltipText: root.filterOptions[(root.filterIndex + 1) % root.filterKeys.length] menuTooltipText: I18n.tr("Filter") onClicked: root.setFilter((root.filterIndex + 1) % root.filterKeys.length) onMenuClicked: { filterMenu.currentValue = root.filterOptions[root.filterIndex]; filterMenu.openDropdownMenu(); } DDropdown { id: filterMenu showTrigger: false popupAnchorItem: filterButton.trailingButton focusReturnTarget: filterButton.trailingButton alignPopupRight: !I18n.isRtl popupWidth: Math.min(root.width, Theme.smallBreakpoint / 2) options: root.filterOptions onValueChanged: value => root.setFilter(options.indexOf(value)) } } DSplitButton { id: sortButton text: I18n.tr("Sort by") + ": " + root.sortOptions[root.sortIndex] iconName: root.descending ? "arrow_downward" : "arrow_upward" size: "xs" variant: "tonal" maximumWidth: parent.width expanded: sortMenu.menuVisible tooltipText: root.sortOptions[(root.sortIndex + 1) % root.sortKeys.length] menuTooltipText: I18n.tr("Sort by") Accessible.description: [root.sortOptions[root.sortIndex], root.descending ? I18n.tr("Descending") : I18n.tr("Ascending")].join(" ยท ") onClicked: root.setSort((root.sortIndex + 1) % root.sortKeys.length) onMenuClicked: { sortMenu.currentValue = root.sortOptions[root.sortIndex]; sortMenu.openDropdownMenu(); } DDropdown { id: sortMenu showTrigger: false popupAnchorItem: sortButton.trailingButton focusReturnTarget: sortButton.trailingButton alignPopupRight: !I18n.isRtl popupWidth: Math.min(root.width, Theme.smallBreakpoint / 2) options: root.sortOptions.concat([I18n.tr("Ascending"), I18n.tr("Descending")]) optionIcons: ["sort_by_alpha", "person", "schedule", root.descending ? "arrow_upward" : "check", root.descending ? "check" : "arrow_downward"] onValueChanged: value => { const index = options.indexOf(value); if (index < 0) return; if (index < root.sortKeys.length) { root.setSort(index); return; } CacheData.set("pluginViewSort", { by: root.sortKeys[root.sortIndex], descending: index === root.sortKeys.length + 1 }); } } } } } StyledText { width: parent.width visible: root.plugins.length > 0 && root.filteredPlugins.length === 0 text: I18n.tr("No plugins found", "empty plugin list") color: Theme.onSurfaceVariant wrapMode: Text.Wrap } GridLayout { id: installedGrid width: parent.width columns: width >= Theme.mediumBreakpoint ? 3 : width >= Theme.smallBreakpoint ? 2 : 1 columnSpacing: Theme.spacingM rowSpacing: Theme.spacingM Repeater { model: root.filteredPlugins DCard { id: installedCard required property var modelData readonly property bool loaded: PluginService.loadedPlugins[modelData.pluginId] !== undefined readonly property var plugin: PluginService.availablePlugins[modelData.pluginId] || ({}) readonly property var loadError: PluginService.pluginLoadErrors[modelData.pluginId] readonly property bool compatible: PluginService.checkPluginCompatibility(plugin.requires_dms) readonly property string problem: root.uninstallErrors[modelData.pluginId] || root.updateErrors[modelData.pluginId] || root.updateCheckErrors[modelData.pluginId] || (loadError ? [loadError.title, loadError.details].filter(Boolean).join("\n") : !compatible ? I18n.tr("Requires %1", "version requirement").arg(plugin.requires_dms) : "") Layout.fillWidth: true Layout.minimumWidth: 0 Layout.preferredWidth: (installedGrid.width - installedGrid.columnSpacing * (installedGrid.columns - 1)) / installedGrid.columns Layout.fillHeight: true implicitHeight: cardContent.implicitHeight + pad * 2 pad: Theme.spacingL radius: Theme.cornerRadiusM readonly property string highlightKey: "installedPlugin:" + modelData.pluginId color: SettingsSearchService.highlightSection === highlightKey ? Theme.blend(SettingsMetrics.rowColor, Theme.primary, SettingsMetrics.highlightBlend) : SettingsMetrics.rowColor clickable: true Accessible.name: modelData.text onClicked: keyboard => root.parentModal?.navigateTo(modelData.id, keyboard) Timer { interval: 0 running: true onTriggered: { const flickable = QmlUtils.findParentFlickable(installedCard.parent); if (!flickable) return; SettingsSearchService.registerCard(installedCard.highlightKey, installedCard, flickable); revealTimer.restart(); } } Component.onDestruction: SettingsSearchService.unregisterCard(highlightKey, installedCard) ColumnLayout { id: cardContent anchors.fill: parent spacing: Theme.spacingS RowLayout { Layout.fillWidth: true Layout.fillHeight: false Layout.minimumHeight: Math.ceil(pluginTitleMetrics.height) * 2 + Theme.spacingXXS + pluginMetadata.implicitHeight Layout.maximumHeight: Layout.minimumHeight spacing: Theme.spacingS DIcon { id: pluginIcon Layout.alignment: Qt.AlignTop Layout.topMargin: Math.max(0, (pluginTitle.implicitHeight - height) / 2) name: installedCard.modelData.icon size: Theme.iconSize color: Theme.primary } ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignTop Layout.topMargin: Math.max(0, (pluginIcon.height - pluginTitle.implicitHeight) / 2) spacing: Theme.spacingXXS StyledText { id: pluginTitle Layout.fillWidth: true text: installedCard.modelData.text maximumLineCount: 2 wrapMode: Text.Wrap font.pixelSize: Theme.fontSizeMedium font.weight: Theme.fontWeightMedium color: Theme.onSurface elide: Text.ElideRight verticalAlignment: Text.AlignTop } Item { id: pluginMetadata Layout.fillWidth: true Layout.minimumWidth: 0 implicitHeight: Math.max(Theme.spacingL, Math.ceil(pluginDescriptionMetrics.height), versionBadge.implicitHeight) DBadge { id: versionBadge anchors.left: parent.left anchors.top: parent.top text: installedCard.plugin.version || "" visible: text !== "" maximumWidth: parent.width color: installedCard.chipColor textColor: Theme.onSurfaceVariant Accessible.name: I18n.tr("Version %1", "accessible name of plugin version badge, %1 is the version number").arg(text) } StyledText { anchors.left: versionBadge.visible ? versionBadge.right : parent.left anchors.leftMargin: versionBadge.visible ? Theme.spacingXS : 0 anchors.baseline: versionBadge.visible ? versionBadge.baseline : undefined width: Math.max(0, parent.width - (versionBadge.visible ? versionBadge.width + Theme.spacingXS : 0)) visible: root.sortKeys[root.sortIndex] === "author" text: I18n.tr("by %1", "author attribution").arg(installedCard.plugin.author || I18n.tr("Unknown", "unknown author")) font.pixelSize: Theme.fontSizeSmall color: Theme.onSurfaceVariant wrapMode: Text.NoWrap elide: Text.ElideRight } } } DToggle { Layout.alignment: Qt.AlignTop enabled: !root.updatingPluginId && !pluginUpdatesDialogItem.isUpdating && !root.uninstalling[installedCard.modelData.pluginId] checked: installedCard.loaded Accessible.name: installedCard.modelData.text onToggled: value => root.setEnabled(installedCard.modelData, value) } } StyledText { Layout.fillWidth: true Layout.minimumHeight: Math.ceil(pluginDescriptionMetrics.height) * 2 Layout.maximumHeight: Layout.minimumHeight text: installedCard.modelData.hint wrapMode: Text.Wrap maximumLineCount: 2 color: Theme.onSurfaceVariant font.pixelSize: Theme.fontSizeSmall elide: Text.ElideRight verticalAlignment: Text.AlignTop } RowLayout { Layout.fillWidth: true Layout.fillHeight: false visible: installedCard.problem !== "" || !!root.uninstalling[installedCard.modelData.pluginId] spacing: Theme.spacingS DActionButton { visible: installedCard.problem !== "" buttonSize: Theme.buttonHeightXS iconName: "error" iconColor: Theme.error tooltipText: installedCard.problem onClicked: root.actionError = installedCard.modelData.text + ": " + installedCard.problem } StyledText { Layout.fillWidth: true text: root.uninstalling[installedCard.modelData.pluginId] ? I18n.tr("Uninstalling: %1", "uninstallation progress").arg(installedCard.modelData.text) : installedCard.problem color: installedCard.problem ? Theme.error : Theme.onSurfaceVariant font.pixelSize: Theme.fontSizeSmall wrapMode: Text.Wrap maximumLineCount: 2 elide: Text.ElideRight Accessible.description: installedCard.problem } } Item { Layout.fillWidth: true implicitHeight: Math.max(settingsButton.implicitHeight, cardActions.implicitHeight) DIconButton { id: settingsButton anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter iconName: "settings" variant: "tonal" Accessible.name: I18n.tr("Settings") onClicked: keyboard => root.parentModal?.navigateTo(installedCard.modelData.id, keyboard) } Row { id: cardActions anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingXS DIconButton { iconName: "download" variant: "tonal" tooltipText: root.updatingPluginId === installedCard.modelData.pluginId ? I18n.tr("Updating...", "plugin update button tooltip while the update runs") : I18n.tr("Update available", "plugin row badge") visible: root.hasUpdate(installedCard.modelData) || root.updatingPluginId === installedCard.modelData.pluginId enabled: DMSService.dmsAvailable && !root.checkingUpdates && !pluginUpdatesDialogItem.isUpdating && !root.updatingPluginId && !root.uninstalling[installedCard.modelData.pluginId] onClicked: root.requestUpdate(installedCard.modelData) } DIconButton { iconName: "delete" Accessible.name: I18n.tr("Uninstall") visible: installedCard.plugin.source !== "system" enabled: DMSService.dmsAvailable && !pluginUpdatesDialogItem.isUpdating && !root.updatingPluginId && !root.uninstalling[installedCard.modelData.pluginId] onClicked: root.requestUninstall(installedCard.modelData) } } } } } } } SettingsFabBar { shown: root.plugins.length > 0 && DMSService.dmsAvailable DFab { text: I18n.tr("Browse") iconName: "store" onClicked: root.showPluginBrowser() } } }