diff --git a/quickshell/Modals/Greeter/GreeterModal.qml b/quickshell/Modals/Greeter/GreeterModal.qml index c959457f0..b33d387cf 100644 --- a/quickshell/Modals/Greeter/GreeterModal.qml +++ b/quickshell/Modals/Greeter/GreeterModal.qml @@ -1,5 +1,4 @@ import QtQuick -import Quickshell import Quickshell.Io import qs.Common import qs.Services @@ -10,8 +9,8 @@ DankFloatingWindow { readonly property var log: Log.scoped("GreeterModal") property int currentPage: 0 - readonly property int totalPages: 3 - readonly property var pageComponents: [welcomePage, doctorPage, completePage] + readonly property int totalPages: 4 + readonly property var pageComponents: [welcomePage, doctorPage, pluginsPage, completePage] property var cheatsheetData: ({}) property bool cheatsheetLoaded: false @@ -278,7 +277,7 @@ DankFloatingWindow { text: I18n.tr("Skip", "greeter skip button") backgroundColor: "transparent" textColor: Theme.surfaceVariantText - onClicked: root.skip() + onClicked: root.currentPage === 1 ? root.nextPage() : root.skip() } DankButton { @@ -327,6 +326,11 @@ DankFloatingWindow { GreeterDoctorPage {} } + Component { + id: pluginsPage + GreeterPluginsPage {} + } + Component { id: completePage GreeterCompletePage {} diff --git a/quickshell/Modals/Greeter/GreeterPluginsPage.qml b/quickshell/Modals/Greeter/GreeterPluginsPage.qml new file mode 100644 index 000000000..4b89cdd03 --- /dev/null +++ b/quickshell/Modals/Greeter/GreeterPluginsPage.qml @@ -0,0 +1,220 @@ +import QtQuick +import qs.Common +import qs.Modules.Settings +import qs.Services +import qs.Widgets + +Item { + id: root + + property var greeterRoot: parent ? parent.greeterRoot : null + + readonly property var curatedPluginIds: ["quickCapture", "dankKDEConnect", "wallpaperCarousel", "calculator", "emojiLauncher", "dockerManager", "amdGpuMonitor", "bongoCat"] + + readonly property real headerIconContainerSize: Math.round(Theme.iconSize * 2) + + property var curatedPlugins: [] + property bool isLoading: false + property string loadError: "" + + function refresh() { + isLoading = true; + loadError = ""; + DMSService.listPlugins(response => { + isLoading = false; + if (response.error) { + loadError = response.error; + return; + } + curatedPlugins = curate(response.result || []); + }); + if (DMSService.apiVersion >= 8) + DMSService.listInstalled(); + } + + function curate(registry) { + const byId = {}; + for (let i = 0; i < registry.length; i++) + byId[registry[i].id] = registry[i]; + const result = []; + for (let i = 0; i < curatedPluginIds.length; i++) { + const plugin = byId[curatedPluginIds[i]]; + if (plugin) + result.push(plugin); + } + return result; + } + + function markInstalled(installedList) { + const installedKeys = {}; + for (let i = 0; i < installedList.length; i++) { + const plugin = installedList[i]; + if (plugin.id) + installedKeys[plugin.id] = true; + if (plugin.name) + installedKeys[plugin.name] = true; + } + curatedPlugins = curatedPlugins.map(p => Object.assign({}, p, { + "installed": installedKeys[p.id] || installedKeys[p.name] || false + })); + } + + Component.onCompleted: refresh() + + Connections { + target: DMSService + + function onInstalledPluginsReceived(plugins) { + root.markInstalled(plugins); + } + + function onIsConnectedChanged() { + if (DMSService.isConnected && root.curatedPlugins.length === 0 && !root.isLoading) + root.refresh(); + } + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingL * 2 + contentWidth: width + + Column { + id: mainColumn + anchors.horizontalCenter: parent.horizontalCenter + width: Math.min(640, parent.width - Theme.spacingXL * 2) + topPadding: Theme.spacingL + spacing: Theme.spacingL + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + Rectangle { + width: root.headerIconContainerSize + height: root.headerIconContainerSize + radius: Math.round(root.headerIconContainerSize * 0.29) + color: Theme.primaryContainer + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "extension" + size: Theme.iconSize + 4 + color: Theme.primary + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXXS + + StyledText { + text: I18n.tr("Popular Plugins", "greeter plugins page title") + font.pixelSize: Theme.fontSizeXLarge + font.weight: Font.Bold + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("Community favorites to get you started", "greeter plugins page subtitle") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + } + } + + StyledText { + width: parent.width + text: I18n.tr("You can extend DMS with more applets, launcher features, and widgets through the plugin system. Hundreds are available on the official DMS registry, built by the community and the DMS team. Install some now, or later through the DMS CLI or the Settings window.", "greeter plugins page intro") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + Item { + width: parent.width + height: Math.round(Theme.fontSizeMedium * 8) + visible: root.isLoading + + DankSpinner { + anchors.centerIn: parent + running: root.isLoading + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: !root.isLoading && root.loadError !== "" + + DankIcon { + anchors.horizontalCenter: parent.horizontalCenter + name: "cloud_off" + size: Theme.iconSize + 16 + color: Theme.outline + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: I18n.tr("Couldn't load plugins", "plugin registry fetch error") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: root.loadError + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + } + + DankButton { + anchors.horizontalCenter: parent.horizontalCenter + text: I18n.tr("Retry", "retry failed action button") + iconName: "refresh" + backgroundColor: Theme.surfaceContainerHighest + textColor: Theme.surfaceText + onClicked: root.refresh() + } + } + + Grid { + width: parent.width + columns: 2 + rowSpacing: Theme.spacingS + columnSpacing: Theme.spacingS + visible: !root.isLoading && root.curatedPlugins.length > 0 + + Repeater { + model: root.curatedPlugins + + PluginCard { + required property var modelData + + width: (parent.width - Theme.spacingS) / 2 + previewHeight: Math.round(width * 0.45) + plugin: modelData + installed: modelData.installed || false + onClicked: { + if (modelData.repo) + Qt.openUrlExternally(modelData.repo); + } + onInstallRequested: PluginService.installFromRegistry(modelData.name, modelData.type === "desktop") + } + } + } + + GreeterSettingsCard { + width: parent.width + iconName: "explore" + title: I18n.tr("Browse Plugins", "plugin browser window title") + description: I18n.tr("Browse or search plugins") + visible: !root.isLoading && root.loadError === "" + onClicked: PopoutService.openSettingsWithTab("plugins") + } + } + } +} diff --git a/quickshell/Modules/Settings/PluginBrowser.qml b/quickshell/Modules/Settings/PluginBrowser.qml index a5bbdbdb3..8a07e25b6 100644 --- a/quickshell/Modules/Settings/PluginBrowser.qml +++ b/quickshell/Modules/Settings/PluginBrowser.qml @@ -24,8 +24,8 @@ DankFloatingWindow { property var categoryFilterOptions: [] property var availableLetters: [] property string detailPluginId: "" + property string loadError: "" - readonly property string previewApiBase: "https://api.danklinux.com/previews/" readonly property var detailPlugin: resolveDetailPlugin(detailPluginId, allPlugins) readonly property bool activeCategorySort: normalizedSortMode(SessionData.pluginBrowserSortMode) === "category" readonly property bool showCategoryFilters: activeCategorySort && categoryFilterOptions.length > 1 @@ -98,99 +98,6 @@ DankFloatingWindow { return (plugin.status || []).indexOf("reviewed") !== -1; } - function statusTone(status) { - switch (status) { - case "broken": - return "error"; - case "unmaintained": - return "warning"; - case "reviewed": - return "info"; - default: - return "outline"; - } - } - - function badgeTone(tone) { - switch (tone) { - case "secondary": - return Theme.secondary; - case "warning": - return Theme.warning; - case "error": - return Theme.error; - case "info": - return Theme.info; - case "outline": - return Theme.outline; - default: - return Theme.primary; - } - } - - function statusLabel(status) { - switch (status) { - case "broken": - return I18n.tr("broken", "plugin status"); - case "unmaintained": - return I18n.tr("unmaintained", "plugin status"); - case "deprecated": - return I18n.tr("deprecated", "plugin status"); - case "reviewed": - return I18n.tr("reviewed", "plugin status"); - default: - return status; - } - } - - function badgeModel(plugin) { - if (!plugin || (!plugin.id && !plugin.name)) - return []; - var badges = []; - if (plugin.featured) - badges.push({ - label: I18n.tr("featured"), - icon: "star", - tone: "secondary" - }); - if (plugin.firstParty) - badges.push({ - label: I18n.tr("official"), - icon: "verified", - tone: "primary" - }); - else - badges.push({ - label: I18n.tr("3rd party"), - icon: "", - tone: "warning" - }); - var status = plugin.status || []; - for (var i = 0; i < status.length; i++) - badges.push({ - label: statusLabel(status[i]), - icon: "", - tone: statusTone(status[i]) - }); - return badges; - } - - function previewUrl(plugin) { - if (!plugin) - return ""; - if (plugin.previewUrl) - return plugin.previewUrl; - if (plugin.id) - return previewApiBase + plugin.id; - return plugin.screenshot || ""; - } - - function heroUrl(plugin) { - if (!plugin) - return ""; - return plugin.screenshot || previewUrl(plugin); - } - function relatedPlugins(plugin) { if (!plugin || !plugin.similar || plugin.similar.length === 0) return []; @@ -528,32 +435,27 @@ DankFloatingWindow { } function installPlugin(pluginName, enableAfterInstall) { - ToastService.showInfo(I18n.tr("Installing: %1", "installation progress").arg(pluginName)); - DMSService.install(pluginName, response => { - if (response.error) { - ToastService.showError(I18n.tr("Install failed: %1", "installation error").arg(response.error)); + PluginService.installFromRegistry(pluginName, enableAfterInstall, success => { + if (!success) return; - } - ToastService.showInfo(I18n.tr("Installed: %1", "installation success").arg(pluginName)); - PluginService.scanPlugins(); refreshPlugins(); - if (enableAfterInstall) { - Qt.callLater(() => { - PluginService.enablePlugin(pluginName); - const plugin = PluginService.availablePlugins[pluginName]; - if (plugin?.type === "desktop") { - const defaultConfig = DesktopWidgetRegistry.getDefaultConfig(pluginName); - SettingsData.createDesktopWidgetInstance(pluginName, plugin.name || pluginName, defaultConfig); - } - hide(); - }); - } + if (enableAfterInstall) + hide(); }); } function refreshPlugins() { isLoading = true; - DMSService.listPlugins(); + loadError = ""; + DMSService.listPlugins(response => { + isLoading = false; + if (response.error) { + loadError = response.error; + return; + } + allPlugins = response.result || []; + updateFilteredPlugins(); + }); if (DMSService.apiVersion >= 8) DMSService.listInstalled(); } @@ -627,18 +529,13 @@ DankFloatingWindow { selectedIndex = -1; keyboardNavigationActive = false; isLoading = false; + loadError = ""; detailPluginId = ""; } Connections { target: DMSService - function onPluginsListReceived(plugins) { - root.isLoading = false; - root.allPlugins = plugins; - root.updateFilteredPlugins(); - } - function onInstalledPluginsReceived(plugins) { var pluginMap = {}; for (var i = 0; i < plugins.length; i++) { @@ -1037,245 +934,18 @@ DankFloatingWindow { required property var modelData required property int index - property bool isInstalled: modelData.installed || false - property bool isCompatible: PluginService.checkPluginCompatibility(modelData.requires_dms) - property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex - width: pluginGrid.cellWidth height: pluginGrid.cellHeight - Rectangle { - id: card + PluginCard { anchors.fill: parent anchors.margins: pluginGrid.cardSpacing / 2 - radius: Theme.cornerRadius - color: cardMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceVariant, 0.5) : Theme.withAlpha(Theme.surfaceVariant, 0.3) - border.color: cardCell.isSelected ? Theme.primary : Theme.withAlpha(Theme.outline, 0.15) - border.width: cardCell.isSelected ? 2 : 1 - scale: cardMouseArea.containsMouse ? 1.012 : 1 - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - - Behavior on scale { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - - MouseArea { - id: cardMouseArea - z: 0 - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: mouse => cardRipple.trigger(mouse.x, mouse.y) - onClicked: root.openPluginDetail(cardCell.modelData) - } - - DankRipple { - id: cardRipple - cornerRadius: card.radius - rippleColor: Theme.surfaceVariantText - } - - Item { - id: previewArea - z: 1 - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: Theme.spacingS - height: pluginGrid.previewHeight - - ClippingRectangle { - anchors.fill: parent - radius: Theme.cornerRadius - 2 - color: Theme.floatingWindowNestedSurface - - CachingImage { - id: cardPreview - anchors.fill: parent - imagePath: root.previewUrl(cardCell.modelData) - maxCacheSize: 640 - fillMode: Image.PreserveAspectCrop - animate: false - visible: status === Image.Ready - } - - DankIcon { - anchors.centerIn: parent - name: cardCell.modelData.icon || "extension" - size: Theme.iconSize + 12 - color: Theme.withAlpha(Theme.outline, 0.6) - visible: cardPreview.status !== Image.Ready - } - - DankSpinner { - anchors.centerIn: parent - running: cardPreview.status === Image.Loading - visible: running - } - } - - Row { - anchors.top: parent.top - anchors.left: parent.left - anchors.margins: Theme.spacingXS - spacing: Theme.spacingXXS - - Repeater { - model: root.badgeModel(cardCell.modelData) - - PluginBadge { - required property var modelData - label: modelData.label - iconName: modelData.icon - tone: root.badgeTone(modelData.tone) - onImage: true - } - } - } - - PluginBadge { - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: Theme.spacingXS - iconName: "thumb_up" - label: cardCell.modelData.upvotes || 0 - tone: Theme.primary - onImage: true - visible: !!cardCell.modelData.issueUrl - } - } - - Column { - z: 1 - anchors.top: previewArea.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: Theme.spacingS - anchors.leftMargin: Theme.spacingM - anchors.rightMargin: Theme.spacingM - spacing: Theme.spacingXXS - - Row { - width: parent.width - spacing: Theme.spacingS - - DankIcon { - id: cardIcon - name: cardCell.modelData.icon || "extension" - size: Theme.iconSize - 4 - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - width: parent.width - cardIcon.width - installAction.width - Theme.spacingS * 2 - text: cardCell.modelData.name || "" - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - elide: Text.ElideRight - maximumLineCount: 1 - anchors.verticalCenter: parent.verticalCenter - } - - Rectangle { - id: installAction - - property string buttonState: { - if (cardCell.isInstalled) - return "installed"; - if (!cardCell.isCompatible) - return "incompatible"; - return "available"; - } - - width: 28 - height: 28 - radius: 14 - anchors.verticalCenter: parent.verticalCenter - color: { - switch (buttonState) { - case "installed": - return Theme.surfaceVariant; - case "incompatible": - return Theme.withAlpha(Theme.warning, 0.15); - default: - return Theme.primary; - } - } - opacity: buttonState === "available" && installMouseArea.containsMouse ? 0.85 : 1 - - Behavior on opacity { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - - DankIcon { - anchors.centerIn: parent - size: 15 - name: { - switch (installAction.buttonState) { - case "installed": - return "check"; - case "incompatible": - return "warning"; - default: - return "download"; - } - } - color: { - switch (installAction.buttonState) { - case "installed": - return Theme.surfaceText; - case "incompatible": - return Theme.warning; - default: - return Theme.surface; - } - } - } - - MouseArea { - id: installMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: installAction.buttonState === "available" ? Qt.PointingHandCursor : Qt.ArrowCursor - enabled: installAction.buttonState === "available" - onClicked: root.installPlugin(cardCell.modelData.name, cardCell.modelData.type === "desktop") - } - } - } - - StyledText { - width: parent.width - text: I18n.tr("by %1", "author attribution").arg(cardCell.modelData.author || I18n.tr("Unknown", "unknown author")) - font.pixelSize: Theme.fontSizeSmall - color: Theme.outline - elide: Text.ElideRight - maximumLineCount: 1 - } - - StyledText { - width: parent.width - text: cardCell.modelData.description || "" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - wrapMode: Text.WordWrap - elide: Text.ElideRight - maximumLineCount: 2 - } - } + plugin: cardCell.modelData + previewHeight: pluginGrid.previewHeight + installed: cardCell.modelData.installed || false + selected: root.keyboardNavigationActive && cardCell.index === root.selectedIndex + onClicked: root.openPluginDetail(cardCell.modelData) + onInstallRequested: root.installPlugin(cardCell.modelData.name, cardCell.modelData.type === "desktop") } } } @@ -1319,7 +989,7 @@ DankFloatingWindow { Column { anchors.centerIn: parent spacing: Theme.spacingS - visible: !root.isLoading && root.filteredPlugins.length === 0 + visible: !root.isLoading && root.loadError === "" && root.filteredPlugins.length === 0 DankIcon { anchors.horizontalCenter: parent.horizontalCenter @@ -1335,6 +1005,42 @@ DankFloatingWindow { color: Theme.surfaceVariantText } } + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + visible: !root.isLoading && root.loadError !== "" + + DankIcon { + anchors.horizontalCenter: parent.horizontalCenter + name: "cloud_off" + size: Theme.iconSize + 16 + color: Theme.outline + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: I18n.tr("Couldn't load plugins", "plugin registry fetch error") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: root.loadError + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + } + + DankButton { + anchors.horizontalCenter: parent.horizontalCenter + text: I18n.tr("Retry", "retry failed action button") + iconName: "refresh" + backgroundColor: Theme.surfaceContainerHighest + textColor: Theme.surfaceText + onClicked: root.refreshPlugins() + } + } } Rectangle { @@ -1562,7 +1268,7 @@ DankFloatingWindow { CachingImage { id: heroImage anchors.fill: parent - imagePath: detailPane.heroFallback ? root.previewUrl(detailPane.plugin) : root.heroUrl(detailPane.plugin) + imagePath: detailPane.heroFallback ? PluginService.previewUrl(detailPane.plugin) : PluginService.heroUrl(detailPane.plugin) maxCacheSize: 1600 fillMode: Image.PreserveAspectFit visible: status === Image.Ready @@ -1606,13 +1312,13 @@ DankFloatingWindow { spacing: Theme.spacingXS Repeater { - model: root.badgeModel(detailPane.plugin) + model: PluginService.badgeModel(detailPane.plugin) PluginBadge { required property var modelData label: modelData.label iconName: modelData.icon - tone: root.badgeTone(modelData.tone) + tone: PluginService.badgeTone(modelData.tone) } } diff --git a/quickshell/Modules/Settings/PluginCard.qml b/quickshell/Modules/Settings/PluginCard.qml new file mode 100644 index 000000000..fee00e2ef --- /dev/null +++ b/quickshell/Modules/Settings/PluginCard.qml @@ -0,0 +1,249 @@ +import QtQuick +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property var plugin: ({}) + property bool installed: false + property bool selected: false + property real previewHeight: Math.round((width - Theme.spacingS * 2) * 0.52) + readonly property int infoHeight: 100 + readonly property bool compatible: PluginService.checkPluginCompatibility(plugin.requires_dms) + + signal clicked + signal installRequested + + implicitHeight: previewHeight + infoHeight + Theme.spacingS * 2 + Theme.spacingM + radius: Theme.cornerRadius + color: cardMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceVariant, 0.5) : Theme.withAlpha(Theme.surfaceVariant, 0.3) + border.color: selected ? Theme.primary : Theme.withAlpha(Theme.outline, 0.15) + border.width: selected ? 2 : 1 + scale: cardMouseArea.containsMouse ? 1.012 : 1 + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + MouseArea { + id: cardMouseArea + z: 0 + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => cardRipple.trigger(mouse.x, mouse.y) + onClicked: root.clicked() + } + + DankRipple { + id: cardRipple + cornerRadius: root.radius + rippleColor: Theme.surfaceVariantText + } + + Item { + id: previewArea + z: 1 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingS + height: root.previewHeight + + ClippingRectangle { + anchors.fill: parent + radius: Theme.cornerRadius - 2 + color: Theme.floatingWindowNestedSurface + + CachingImage { + id: cardPreview + anchors.fill: parent + imagePath: PluginService.previewUrl(root.plugin) + maxCacheSize: 640 + fillMode: Image.PreserveAspectCrop + animate: false + visible: status === Image.Ready + } + + DankIcon { + anchors.centerIn: parent + name: root.plugin.icon || "extension" + size: Theme.iconSize + 12 + color: Theme.withAlpha(Theme.outline, 0.6) + visible: cardPreview.status !== Image.Ready + } + + DankSpinner { + anchors.centerIn: parent + running: cardPreview.status === Image.Loading + visible: running + } + } + + Row { + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: Theme.spacingXS + spacing: Theme.spacingXXS + + Repeater { + model: PluginService.badgeModel(root.plugin) + + PluginBadge { + required property var modelData + label: modelData.label + iconName: modelData.icon + tone: PluginService.badgeTone(modelData.tone) + onImage: true + } + } + } + + PluginBadge { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Theme.spacingXS + iconName: "thumb_up" + label: root.plugin.upvotes || 0 + tone: Theme.primary + onImage: true + visible: !!root.plugin.issueUrl + } + } + + Column { + z: 1 + anchors.top: previewArea.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: Theme.spacingS + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingXXS + + Row { + width: parent.width + spacing: Theme.spacingS + + DankIcon { + id: cardIcon + name: root.plugin.icon || "extension" + size: Theme.iconSize - 4 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + width: parent.width - cardIcon.width - installAction.width - Theme.spacingS * 2 + text: root.plugin.name || "" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + maximumLineCount: 1 + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + id: installAction + + property string buttonState: { + if (root.installed) + return "installed"; + if (!root.compatible) + return "incompatible"; + return "available"; + } + + width: 28 + height: 28 + radius: 14 + anchors.verticalCenter: parent.verticalCenter + color: { + switch (buttonState) { + case "installed": + return Theme.surfaceVariant; + case "incompatible": + return Theme.withAlpha(Theme.warning, 0.15); + default: + return Theme.primary; + } + } + opacity: buttonState === "available" && installMouseArea.containsMouse ? 0.85 : 1 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + DankIcon { + anchors.centerIn: parent + size: 15 + name: { + switch (installAction.buttonState) { + case "installed": + return "check"; + case "incompatible": + return "warning"; + default: + return "download"; + } + } + color: { + switch (installAction.buttonState) { + case "installed": + return Theme.surfaceText; + case "incompatible": + return Theme.warning; + default: + return Theme.surface; + } + } + } + + MouseArea { + id: installMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: installAction.buttonState === "available" ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: installAction.buttonState === "available" + onClicked: root.installRequested() + } + } + } + + StyledText { + width: parent.width + text: I18n.tr("by %1", "author attribution").arg(root.plugin.author || I18n.tr("Unknown", "unknown author")) + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + elide: Text.ElideRight + maximumLineCount: 1 + } + + StyledText { + width: parent.width + text: root.plugin.description || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + elide: Text.ElideRight + maximumLineCount: 2 + } + } +} diff --git a/quickshell/Services/PluginService.qml b/quickshell/Services/PluginService.qml index 612f28442..b39e27e8c 100644 --- a/quickshell/Services/PluginService.qml +++ b/quickshell/Services/PluginService.qml @@ -23,6 +23,7 @@ Singleton { property bool pluginDirectoryExists: false property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins" + readonly property string previewApiBase: "https://api.danklinux.com/previews/" property var knownManifests: ({}) property var pathToPluginId: ({}) @@ -1183,6 +1184,128 @@ Singleton { globalVarChanged(pluginId, varName); } + function previewUrl(plugin) { + if (!plugin) + return ""; + if (plugin.previewUrl) + return plugin.previewUrl; + if (plugin.id) + return previewApiBase + plugin.id; + return plugin.screenshot || ""; + } + + function heroUrl(plugin) { + if (!plugin) + return ""; + return plugin.screenshot || previewUrl(plugin); + } + + function statusTone(status) { + switch (status) { + case "broken": + return "error"; + case "unmaintained": + return "warning"; + case "reviewed": + return "info"; + default: + return "outline"; + } + } + + function statusLabel(status) { + switch (status) { + case "broken": + return I18n.tr("broken", "plugin status"); + case "unmaintained": + return I18n.tr("unmaintained", "plugin status"); + case "deprecated": + return I18n.tr("deprecated", "plugin status"); + case "reviewed": + return I18n.tr("reviewed", "plugin status"); + default: + return status; + } + } + + function badgeTone(tone) { + switch (tone) { + case "secondary": + return Theme.secondary; + case "warning": + return Theme.warning; + case "error": + return Theme.error; + case "info": + return Theme.info; + case "outline": + return Theme.outline; + default: + return Theme.primary; + } + } + + function badgeModel(plugin) { + if (!plugin || (!plugin.id && !plugin.name)) + return []; + var badges = []; + if (plugin.featured) + badges.push({ + label: I18n.tr("featured"), + icon: "star", + tone: "secondary" + }); + if (plugin.firstParty) + badges.push({ + label: I18n.tr("official"), + icon: "verified", + tone: "primary" + }); + else + badges.push({ + label: I18n.tr("3rd party"), + icon: "", + tone: "warning" + }); + var status = plugin.status || []; + for (var i = 0; i < status.length; i++) + badges.push({ + label: statusLabel(status[i]), + icon: "", + tone: statusTone(status[i]) + }); + return badges; + } + + function installFromRegistry(pluginName, enableAfterInstall, onDone) { + ToastService.showInfo(I18n.tr("Installing: %1", "installation progress").arg(pluginName)); + DMSService.install(pluginName, response => { + if (response.error) { + ToastService.showError(I18n.tr("Install failed: %1", "installation error").arg(response.error)); + if (onDone) + onDone(false); + return; + } + ToastService.showInfo(I18n.tr("Installed: %1", "installation success").arg(pluginName)); + scanPlugins(); + if (!enableAfterInstall) { + if (onDone) + onDone(true); + return; + } + Qt.callLater(() => { + enablePlugin(pluginName); + const plugin = availablePlugins[pluginName]; + if (plugin?.type === "desktop") { + const defaultConfig = DesktopWidgetRegistry.getDefaultConfig(pluginName); + SettingsData.createDesktopWidgetInstance(pluginName, plugin.name || pluginName, defaultConfig); + } + if (onDone) + onDone(true); + }); + }); + } + function checkPluginCompatibility(requiresDms) { if (!requiresDms) return true;