mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 15:07:49 -04:00
plugins: discover shelves, quick filters and typo-tolerant search in the browser (2/3) (#3752)
This commit is contained in:
1 parent
c0c054552e
commit
2ad442fb37
8 files changed
+742
-22
No files matched your search
@@ -7,6 +7,7 @@ import qs.Services
|
||||
import qs.DCommon.Widgets
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
import "../../Common/PluginRanking.js" as PluginRanking
|
||||
|
||||
RegistryBrowserWindow {
|
||||
id: root
|
||||
@@ -23,8 +24,22 @@ RegistryBrowserWindow {
|
||||
property string operationMessage: ""
|
||||
property bool operationFailed: false
|
||||
property bool operationPending: false
|
||||
property var discoverPicks: []
|
||||
// Quick filters of this visit: usable (on the running compositor), fresh (new or updated), official.
|
||||
property var quickFilters: ({})
|
||||
property var searchSuggestions: []
|
||||
property var discoverShelves: []
|
||||
|
||||
readonly property var detailPlugin: resolveDetailPlugin(detailPluginId, allPlugins)
|
||||
property bool discoverCollapsed: false
|
||||
readonly property bool discoverVisible: {
|
||||
if (isLoading || loadError !== "" || searchQuery !== "" || typeFilter !== "")
|
||||
return false;
|
||||
if (discoverPicks.length === 0 && discoverShelves.length === 0)
|
||||
return false;
|
||||
var mode = normalizedSortMode(SessionData.pluginBrowserSortMode);
|
||||
return mode === "default" || mode === "recommended";
|
||||
}
|
||||
readonly property bool activeCategorySort: normalizedSortMode(SessionData.pluginBrowserSortMode) === "category"
|
||||
readonly property bool showCategoryFilters: activeCategorySort && categoryFilterOptions.length > 1
|
||||
readonly property bool showLetterIndex: {
|
||||
@@ -32,6 +47,25 @@ RegistryBrowserWindow {
|
||||
return (mode === "name" || mode === "author") && availableLetters.length > 1;
|
||||
}
|
||||
|
||||
readonly property var quickFilterOptions: [
|
||||
{
|
||||
value: "usable",
|
||||
label: I18n.tr("Works here", "plugin browser filter chip, plugins that run on the current compositor"),
|
||||
icon: "desktop_windows"
|
||||
},
|
||||
{
|
||||
value: "fresh",
|
||||
label: I18n.tr("New & updated", "plugin browser filter chip"),
|
||||
icon: "fiber_new"
|
||||
},
|
||||
{
|
||||
value: "official",
|
||||
label: I18n.tr("Official only", "plugin browser filter chip, first party plugins"),
|
||||
icon: "verified"
|
||||
}
|
||||
]
|
||||
showLoadingSpinner: false
|
||||
|
||||
readonly property var sortChipOptions: [
|
||||
{
|
||||
id: "hideInstalled",
|
||||
@@ -48,6 +82,11 @@ RegistryBrowserWindow {
|
||||
label: I18n.tr("Votes", "plugin browser sort option"),
|
||||
toggle: false
|
||||
},
|
||||
{
|
||||
id: "recommended",
|
||||
label: I18n.tr("For you", "plugin browser sort option, plugins ranked for this user"),
|
||||
toggle: false
|
||||
},
|
||||
{
|
||||
id: "name",
|
||||
label: I18n.tr("Name", "plugin browser sort option"),
|
||||
@@ -68,11 +107,97 @@ RegistryBrowserWindow {
|
||||
function normalizedSortMode(mode) {
|
||||
if (mode === "type" || mode === "contributor")
|
||||
return "author";
|
||||
if (mode === "name" || mode === "author" || mode === "category")
|
||||
if (mode === "name" || mode === "author" || mode === "category" || mode === "recommended")
|
||||
return mode;
|
||||
return "default";
|
||||
}
|
||||
|
||||
function rankingCompositor() {
|
||||
if (CompositorService.isNiri)
|
||||
return "niri";
|
||||
if (CompositorService.isHyprland)
|
||||
return "hyprland";
|
||||
if (CompositorService.isSway)
|
||||
return "sway";
|
||||
if (CompositorService.isMango)
|
||||
return "mangowc";
|
||||
if (CompositorService.isLabwc)
|
||||
return "labwc";
|
||||
if (CompositorService.isMiracle)
|
||||
return "miracle-wm";
|
||||
return "";
|
||||
}
|
||||
|
||||
function recommendationReason(key) {
|
||||
switch (key) {
|
||||
case "similar":
|
||||
return I18n.tr("Like your plugins", "plugin recommendation reason, similar to installed plugins");
|
||||
case "featured":
|
||||
return I18n.tr("Featured plugin", "plugin recommendation reason");
|
||||
case "new":
|
||||
return I18n.tr("New", "plugin recommendation reason, recently published");
|
||||
case "trending":
|
||||
return I18n.tr("Trending", "plugin recommendation reason, gaining votes quickly");
|
||||
case "category":
|
||||
return I18n.tr("Fits your setup", "plugin recommendation reason, same category as installed plugins");
|
||||
default:
|
||||
return I18n.tr("Popular", "plugin recommendation reason, many votes");
|
||||
}
|
||||
}
|
||||
|
||||
function updateDiscover(pool) {
|
||||
var now = Date.now();
|
||||
var taste = PluginRanking.buildTaste(allPlugins);
|
||||
var options = {
|
||||
now: now,
|
||||
taste: taste,
|
||||
compositor: rankingCompositor(),
|
||||
limit: 6
|
||||
};
|
||||
|
||||
var picks = [];
|
||||
var week = PluginRanking.pickOfTheWeek(pool, now);
|
||||
if (week)
|
||||
picks.push({
|
||||
label: I18n.tr("Pick of the week", "plugin browser highlighted plugin"),
|
||||
plugin: week
|
||||
});
|
||||
var day = PluginRanking.pickOfTheDay(pool, now);
|
||||
if (day)
|
||||
picks.push({
|
||||
label: I18n.tr("Pick of the day", "plugin browser highlighted plugin"),
|
||||
plugin: day
|
||||
});
|
||||
|
||||
options.exclude = picks.map(pick => pick.plugin.id);
|
||||
|
||||
var shelves = [];
|
||||
var recommended = PluginRanking.recommend(pool, options);
|
||||
if (recommended.length > 0)
|
||||
shelves.push({
|
||||
label: taste.count > 0 ? I18n.tr("For you", "plugin browser shelf, plugins ranked for this user") : I18n.tr("Popular", "plugin browser shelf"),
|
||||
plugins: recommended,
|
||||
captions: recommended.map(p => recommendationReason(PluginRanking.reason(p, taste, now)))
|
||||
});
|
||||
var trending = PluginRanking.trendingNow(pool, options);
|
||||
if (trending.length > 0)
|
||||
shelves.push({
|
||||
label: I18n.tr("Trending", "plugin browser shelf, gaining votes quickly"),
|
||||
plugins: trending,
|
||||
captions: []
|
||||
});
|
||||
var arrivals = PluginRanking.newArrivals(pool, options);
|
||||
if (arrivals.length > 0)
|
||||
shelves.push({
|
||||
label: I18n.tr("New", "plugin browser shelf, recently published"),
|
||||
plugins: arrivals,
|
||||
captions: []
|
||||
});
|
||||
|
||||
discoverPicks = picks;
|
||||
discoverShelves = shelves;
|
||||
}
|
||||
|
||||
function isSortChipSelected(chipId, toggle) {
|
||||
if (toggle) {
|
||||
if (chipId === "hideInstalled")
|
||||
@@ -230,7 +355,8 @@ RegistryBrowserWindow {
|
||||
if (!pluginGrid)
|
||||
return;
|
||||
pluginGrid.cancelFlick();
|
||||
pluginGrid.contentY = 0;
|
||||
discoverCollapsed = false;
|
||||
pluginGrid.positionViewAtBeginning();
|
||||
Qt.callLater(() => {
|
||||
if (pluginGrid)
|
||||
pluginGrid.forceLayout();
|
||||
@@ -250,7 +376,8 @@ RegistryBrowserWindow {
|
||||
|
||||
function updateFilteredPlugins() {
|
||||
var baseFiltered = [];
|
||||
var query = searchQuery ? searchQuery.toLowerCase() : "";
|
||||
var visiblePlugins = SessionData.showThirdPartyPlugins ? allPlugins : allPlugins.filter(p => p.firstParty || false);
|
||||
var plan = PluginRanking.searchPlan(visiblePlugins, searchQuery);
|
||||
|
||||
for (var i = 0; i < allPlugins.length; i++) {
|
||||
var plugin = allPlugins[i];
|
||||
@@ -264,19 +391,14 @@ RegistryBrowserWindow {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (query.length === 0) {
|
||||
baseFiltered.push(plugin);
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = plugin.name ? plugin.name.toLowerCase() : "";
|
||||
var description = plugin.description ? plugin.description.toLowerCase() : "";
|
||||
var author = plugin.author ? plugin.author.toLowerCase() : "";
|
||||
|
||||
if (name.indexOf(query) !== -1 || description.indexOf(query) !== -1 || author.indexOf(query) !== -1)
|
||||
if (PluginRanking.searchRank(plugin, plan) > 0)
|
||||
baseFiltered.push(plugin);
|
||||
}
|
||||
var compositor = rankingCompositor();
|
||||
var filterNow = Date.now();
|
||||
baseFiltered = baseFiltered.filter(p => PluginRanking.matchesFilters(p, quickFilters, compositor, filterNow));
|
||||
|
||||
updateDiscover(baseFiltered);
|
||||
categoryFilterOptions = buildCategoryFilterOptions(baseFiltered);
|
||||
if (categoryFilter !== "all") {
|
||||
var filterStillValid = false;
|
||||
@@ -290,7 +412,7 @@ RegistryBrowserWindow {
|
||||
categoryFilter = "all";
|
||||
}
|
||||
|
||||
var filtered = baseFiltered.slice();
|
||||
var filtered = baseFiltered;
|
||||
if (SessionData.pluginBrowserHideInstalled)
|
||||
filtered = filtered.filter(p => !(p.installed || false));
|
||||
if (activeCategorySort && categoryFilter !== "all") {
|
||||
@@ -302,6 +424,15 @@ RegistryBrowserWindow {
|
||||
});
|
||||
}
|
||||
|
||||
var sortMode = normalizedSortMode(SessionData.pluginBrowserSortMode);
|
||||
var scores = {};
|
||||
if (sortMode === "recommended") {
|
||||
var now = Date.now();
|
||||
var taste = PluginRanking.buildTaste(allPlugins);
|
||||
for (var s = 0; s < filtered.length; s++)
|
||||
scores[filtered[s].id] = PluginRanking.score(filtered[s], taste, now);
|
||||
}
|
||||
|
||||
filtered.sort((a, b) => {
|
||||
if (SessionData.pluginBrowserInstalledFirst) {
|
||||
var instA = a.installed || false;
|
||||
@@ -309,7 +440,13 @@ RegistryBrowserWindow {
|
||||
if (instA !== instB)
|
||||
return instA ? -1 : 1;
|
||||
}
|
||||
var sortMode = normalizedSortMode(SessionData.pluginBrowserSortMode);
|
||||
if (sortMode === "recommended") {
|
||||
var scoreA = scores[a.id] || 0;
|
||||
var scoreB = scores[b.id] || 0;
|
||||
if (scoreA !== scoreB)
|
||||
return scoreB - scoreA;
|
||||
return comparePluginName(a, b);
|
||||
}
|
||||
if (sortMode === "name")
|
||||
return comparePluginName(a, b);
|
||||
if (sortMode === "author")
|
||||
@@ -328,6 +465,7 @@ RegistryBrowserWindow {
|
||||
});
|
||||
|
||||
filteredPlugins = filtered;
|
||||
searchSuggestions = filtered.length === 0 && plan.length > 0 ? PluginRanking.closest(visiblePlugins, searchQuery, 4) : [];
|
||||
updateAvailableLetters(filtered);
|
||||
selectedIndex = -1;
|
||||
keyboardNavigationActive = false;
|
||||
@@ -546,6 +684,8 @@ RegistryBrowserWindow {
|
||||
function resetContent() {
|
||||
allPlugins = [];
|
||||
filteredPlugins = [];
|
||||
discoverPicks = [];
|
||||
discoverShelves = [];
|
||||
loadError = "";
|
||||
detailPluginId = "";
|
||||
}
|
||||
@@ -664,14 +804,19 @@ RegistryBrowserWindow {
|
||||
model: root.sortChipOptions.slice(0, 2).map(option => ({
|
||||
label: option.label,
|
||||
value: option.id
|
||||
}))
|
||||
})).concat(root.quickFilterOptions)
|
||||
multiSelect: true
|
||||
selectedValues: [SessionData.pluginBrowserHideInstalled ? "hideInstalled" : "", SessionData.pluginBrowserInstalledFirst ? "installed" : ""]
|
||||
selectedValues: [SessionData.pluginBrowserHideInstalled ? "hideInstalled" : "", SessionData.pluginBrowserInstalledFirst ? "installed" : ""].concat(Object.keys(root.quickFilters).filter(key => root.quickFilters[key]))
|
||||
onSelectionToggled: (index, selected) => {
|
||||
if (index === 0)
|
||||
if (index === 0) {
|
||||
SessionData.setPluginBrowserHideInstalled(selected);
|
||||
else
|
||||
} else if (index === 1) {
|
||||
SessionData.setPluginBrowserInstalledFirst(selected);
|
||||
} else {
|
||||
var next = Object.assign({}, root.quickFilters);
|
||||
next[model[index].value] = selected;
|
||||
root.quickFilters = next;
|
||||
}
|
||||
root.updateFilteredPlugins();
|
||||
}
|
||||
}
|
||||
@@ -717,6 +862,39 @@ RegistryBrowserWindow {
|
||||
]
|
||||
|
||||
listContent: [
|
||||
Item {
|
||||
id: discoverPane
|
||||
|
||||
readonly property bool fits: discover.height + pluginGrid.cellHeight / 3 <= parent.height
|
||||
readonly property bool shown: root.discoverVisible && fits && !root.discoverCollapsed
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: pluginGrid.anchors.rightMargin
|
||||
height: shown ? discover.height + Theme.spacingM : 0
|
||||
clip: true
|
||||
visible: height > 0 && pluginGrid.visible
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
PluginDiscover {
|
||||
id: discover
|
||||
x: pluginGrid.cardSpacing / 2
|
||||
width: pluginGrid.cellWidth * pluginGrid.columns - pluginGrid.cardSpacing
|
||||
picks: root.discoverPicks
|
||||
shelves: root.discoverShelves
|
||||
columns: pluginGrid.columns
|
||||
cardSpacing: pluginGrid.cardSpacing
|
||||
onPluginOpened: plugin => root.openPluginDetail(plugin)
|
||||
onInstallRequested: plugin => root.installPlugin(plugin.id, plugin.name, plugin.type === "desktop")
|
||||
}
|
||||
},
|
||||
DGridView {
|
||||
id: pluginGrid
|
||||
|
||||
@@ -726,6 +904,7 @@ RegistryBrowserWindow {
|
||||
readonly property int infoHeight: Theme.iconButtonSize + Theme.fontSizeSmall * 4 + Theme.spacingS
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: discoverPane.height
|
||||
anchors.rightMargin: root.showLetterIndex ? Theme.iconButtonSize : 0
|
||||
cellWidth: Math.floor(width / columns)
|
||||
cellHeight: previewHeight + infoHeight + Math.round(cardSpacing) + Theme.spacingS * 2 + Theme.spacingM
|
||||
@@ -733,6 +912,15 @@ RegistryBrowserWindow {
|
||||
clip: true
|
||||
visible: !root.isLoading && root.loadError === ""
|
||||
cacheBuffer: cellHeight * 2
|
||||
onContentYChanged: {
|
||||
if (!root.discoverCollapsed) {
|
||||
if (contentY > cellHeight / 4)
|
||||
root.discoverCollapsed = true;
|
||||
return;
|
||||
}
|
||||
if (contentY <= 0 && contentHeight > height + cellHeight)
|
||||
root.discoverCollapsed = false;
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: cardCell
|
||||
@@ -751,6 +939,7 @@ RegistryBrowserWindow {
|
||||
plugin: cardCell.modelData
|
||||
busy: !!PluginService.installingPlugins[cardCell.modelData.id]
|
||||
previewHeight: pluginGrid.previewHeight
|
||||
compositor: root.rankingCompositor()
|
||||
installed: cardCell.modelData.installed || false
|
||||
selected: root.keyboardNavigationActive && cardCell.index === root.selectedIndex
|
||||
onClicked: root.openPluginDetail(cardCell.modelData)
|
||||
@@ -758,6 +947,84 @@ RegistryBrowserWindow {
|
||||
}
|
||||
}
|
||||
},
|
||||
Grid {
|
||||
id: loadingGrid
|
||||
anchors.fill: pluginGrid
|
||||
columns: pluginGrid.columns
|
||||
clip: true
|
||||
visible: root.isLoading
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: loadingGrid.visible && Theme.longDuration > 0
|
||||
loops: Animation.Infinite
|
||||
onRunningChanged: {
|
||||
if (!running)
|
||||
loadingGrid.opacity = 1;
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.45
|
||||
duration: Theme.longDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1
|
||||
duration: Theme.longDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.isLoading ? pluginGrid.columns * Math.max(1, Math.ceil(pluginGrid.height / pluginGrid.cellHeight)) : 0
|
||||
|
||||
Item {
|
||||
width: pluginGrid.cellWidth
|
||||
height: pluginGrid.cellHeight
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: pluginGrid.cardSpacing / 2
|
||||
radius: Theme.cornerRadiusM
|
||||
color: SettingsMetrics.rowColor
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
|
||||
Rectangle {
|
||||
id: ghostPreview
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingS
|
||||
height: pluginGrid.previewHeight
|
||||
radius: Theme.cornerRadiusS
|
||||
color: SettingsMetrics.controlColor
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.top: ghostPreview.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Theme.spacingM
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: [0.55, 0.3, 0.9]
|
||||
|
||||
Rectangle {
|
||||
required property real modelData
|
||||
width: parent.width * modelData
|
||||
height: Theme.fontSizeSmall
|
||||
radius: height / 2
|
||||
color: SettingsMetrics.controlColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
DFlickable {
|
||||
id: letterIndex
|
||||
anchors.right: parent.right
|
||||
@@ -806,6 +1073,33 @@ RegistryBrowserWindow {
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.searchSuggestions.length > 0
|
||||
text: I18n.tr("Did you mean", "plugin search, introduces plugins with a close name")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXS
|
||||
visible: root.searchSuggestions.length > 0
|
||||
|
||||
Repeater {
|
||||
model: root.searchSuggestions
|
||||
|
||||
DButton {
|
||||
required property var modelData
|
||||
text: modelData.name || modelData.id
|
||||
iconName: modelData.icon || "extension"
|
||||
backgroundColor: Theme.secondaryContainer
|
||||
textColor: Theme.onSecondaryContainer
|
||||
onClicked: root.openPluginDetail(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -4,6 +4,7 @@ import qs.Common
|
||||
import qs.Modules.Settings.Widgets
|
||||
import qs.Services
|
||||
import qs.DCommon.Widgets
|
||||
import "../../Common/PluginRanking.js" as PluginRanking
|
||||
|
||||
DCard {
|
||||
id: root
|
||||
@@ -13,14 +14,17 @@ DCard {
|
||||
property bool installed: false
|
||||
property bool selected: false
|
||||
property string fallbackIcon: "extension"
|
||||
property string highlight: ""
|
||||
property string previewSource: PluginService.thumbUrl(plugin)
|
||||
property string previewFallback: PluginService.previewUrl(plugin)
|
||||
property var badges: PluginService.badgeModel(plugin)
|
||||
property bool allowUninstall: false
|
||||
property var palette: null
|
||||
property string compositor: ""
|
||||
property real previewHeight: Math.round((width - Theme.spacingS * 2) * SettingsMetrics.choiceCardPreviewRatio)
|
||||
readonly property int infoHeight: Theme.iconButtonSize + Theme.fontSizeSmall * 4 + Theme.spacingS
|
||||
readonly property bool compatible: PluginService.checkPluginCompatibility(plugin.requires_dms)
|
||||
readonly property var otherCompositors: PluginRanking.unsupportedOn(plugin, compositor)
|
||||
readonly property bool focusRingShown: selected && Theme.focusRingWidth > 0
|
||||
|
||||
signal installRequested
|
||||
@@ -60,6 +64,7 @@ DCard {
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
animate: false
|
||||
visible: status === Image.Ready
|
||||
opacity: root.otherCompositors.length > 0 ? 0.5 : 1
|
||||
onStatusChanged: {
|
||||
if (status !== Image.Error || useFallback || !root.previewFallback || root.previewFallback === root.previewSource)
|
||||
return;
|
||||
@@ -124,6 +129,26 @@ DCard {
|
||||
tone: Theme.primary
|
||||
visible: !!root.plugin.issueUrl
|
||||
}
|
||||
|
||||
PluginBadge {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Theme.spacingXS
|
||||
iconName: "auto_awesome"
|
||||
label: root.highlight
|
||||
tone: Theme.primary
|
||||
visible: root.highlight.length > 0
|
||||
}
|
||||
|
||||
PluginBadge {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingXS
|
||||
iconName: root.installed ? "check_circle" : "desktop_windows"
|
||||
label: root.installed ? I18n.tr("Installed", "adjective, plugin or theme is already installed, button state and filter chip") : I18n.tr("%1 only", "lowercase plugin badge, compositors the plugin is limited to").arg(root.otherCompositors.join(", "))
|
||||
tone: root.installed ? Theme.primary : Theme.warning
|
||||
visible: root.installed || root.otherCompositors.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.DCommon.Widgets
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property var picks: []
|
||||
property var shelves: []
|
||||
property int columns: 1
|
||||
property real cardSpacing: Theme.spacingM
|
||||
property int shelfIndex: 0
|
||||
|
||||
readonly property var shelf: shelves[Math.min(shelfIndex, shelves.length - 1)] || null
|
||||
readonly property real cardWidth: Math.floor((width - cardSpacing * (columns - 1)) / columns)
|
||||
readonly property int pickColumns: Math.min(Math.max(1, picks.length), columns > 1 ? 2 : 1)
|
||||
readonly property int pickHeight: Theme.fontSizeMedium + Theme.fontSizeSmall * 6 + Theme.spacingM * 2
|
||||
|
||||
signal pluginOpened(var plugin)
|
||||
signal installRequested(var plugin)
|
||||
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Grid {
|
||||
width: parent.width
|
||||
columns: root.pickColumns
|
||||
spacing: root.cardSpacing
|
||||
visible: root.picks.length > 0
|
||||
|
||||
Repeater {
|
||||
model: root.picks
|
||||
|
||||
DCard {
|
||||
id: pickCard
|
||||
|
||||
required property var modelData
|
||||
readonly property var plugin: modelData.plugin
|
||||
|
||||
width: Math.floor((root.width - root.cardSpacing * (root.pickColumns - 1)) / root.pickColumns)
|
||||
height: root.pickHeight
|
||||
radius: Theme.cornerRadiusM
|
||||
color: SettingsMetrics.rowColor
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
pad: 0
|
||||
clickable: true
|
||||
Accessible.name: modelData.label + ": " + (plugin.name || "")
|
||||
onClicked: root.pluginOpened(plugin)
|
||||
|
||||
ClippingRectangle {
|
||||
id: pickPreview
|
||||
z: 1
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Theme.spacingS
|
||||
width: Math.round(height / SettingsMetrics.choiceCardPreviewRatio)
|
||||
radius: Theme.cornerRadiusS
|
||||
color: SettingsMetrics.controlColor
|
||||
|
||||
CachingImage {
|
||||
id: pickImage
|
||||
anchors.fill: parent
|
||||
imagePath: PluginService.thumbUrl(pickCard.plugin)
|
||||
maxCacheSize: 640
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
animate: false
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
DIcon {
|
||||
anchors.centerIn: parent
|
||||
name: pickCard.plugin.icon || "extension"
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.onSurfaceVariant
|
||||
visible: pickImage.status !== Image.Ready
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
z: 1
|
||||
anchors.left: pickPreview.right
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
DIcon {
|
||||
name: "award_star"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: pickCard.modelData.label
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: pickCard.plugin.name || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("by %1", "author attribution").arg(pickCard.plugin.author || I18n.tr("Unknown", "unknown author"))
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: pickCard.plugin.description || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DFilterChips {
|
||||
width: parent.width
|
||||
visible: root.shelves.length > 0
|
||||
model: root.shelves.map(shelf => shelf.label)
|
||||
currentIndex: Math.min(root.shelfIndex, Math.max(0, root.shelves.length - 1))
|
||||
onSelectionChanged: index => root.shelfIndex = index
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: root.cardSpacing
|
||||
visible: !!root.shelf
|
||||
|
||||
Repeater {
|
||||
model: root.shelf ? root.shelf.plugins.slice(0, root.columns) : []
|
||||
|
||||
PluginCard {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: root.cardWidth
|
||||
plugin: modelData
|
||||
busy: !!PluginService.installingPlugins[modelData.id]
|
||||
installed: modelData.installed || false
|
||||
highlight: root.shelf.captions[index] || ""
|
||||
onClicked: root.pluginOpened(modelData)
|
||||
onInstallRequested: root.installRequested(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("All plugins", "plugin browser section title above the full list")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
@@ -499,6 +499,8 @@ Column {
|
||||
width: parent.width
|
||||
text: root.searchQuery
|
||||
placeholderText: I18n.tr("Search plugins...", "plugin search placeholder")
|
||||
// matches MAX_QUERY_LENGTH in PluginRanking.js
|
||||
maximumLength: 100
|
||||
onTextEdited: root.searchQuery = text
|
||||
}
|
||||
Flow {
|
||||
|
||||
@@ -14,6 +14,7 @@ DFloatingWindow {
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool isLoading: false
|
||||
property bool showLoadingSpinner: true
|
||||
property bool pendingInstallHandled: false
|
||||
property string headerTitle: title
|
||||
property string searchPlaceholder: ""
|
||||
@@ -243,11 +244,11 @@ DFloatingWindow {
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: root.isLoading
|
||||
visible: root.isLoading && root.showLoadingSpinner
|
||||
|
||||
DSpinner {
|
||||
anchors.centerIn: parent
|
||||
running: root.isLoading
|
||||
running: root.isLoading && root.showLoadingSpinner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user