firststart/greeter: add a step for plugins

This commit is contained in:
bbedward committed 2026-08-31 09:15:59 -04:00
1 parent 7372b79c2d
commit 4624ceb8f0
5 files changed
+663 -361

No files matched your search

+8 -4
View File
@@ -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 {}
@@ -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")
}
}
}
}
+63 -357
View File
@@ -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)
}
}
+249
View File
@@ -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
}
}
}
+123
View File
@@ -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;