add noctalia and fuzzel
This commit is contained in:
+393
@@ -0,0 +1,393 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property string pluginSearchText: ""
|
||||
property string selectedTag: ""
|
||||
property int tagsRefreshCounter: 0
|
||||
property int availablePluginsRefreshCounter: 0
|
||||
|
||||
// Pseudo tags for filtering
|
||||
readonly property var pseudoTags: ["official", "downloaded", "notDownloaded"]
|
||||
|
||||
readonly property var availableTags: {
|
||||
// Reference counter to force re-evaluation
|
||||
void (root.tagsRefreshCounter);
|
||||
var tags = {};
|
||||
var plugins = PluginService.availablePlugins || [];
|
||||
for (var i = 0; i < plugins.length; i++) {
|
||||
var pluginTags = plugins[i].tags || [];
|
||||
for (var j = 0; j < pluginTags.length; j++) {
|
||||
tags[pluginTags[j]] = true;
|
||||
}
|
||||
}
|
||||
return Object.keys(tags).sort();
|
||||
}
|
||||
|
||||
function stripAuthorEmail(author) {
|
||||
if (!author)
|
||||
return "";
|
||||
var lastBracket = author.lastIndexOf("<");
|
||||
if (lastBracket >= 0) {
|
||||
return author.substring(0, lastBracket).trim();
|
||||
}
|
||||
return author;
|
||||
}
|
||||
|
||||
// Tag filter chips in collapsible
|
||||
NTagFilter {
|
||||
tags: root.pseudoTags.concat(root.availableTags)
|
||||
selectedTag: root.selectedTag
|
||||
onSelectedTagChanged: root.selectedTag = selectedTag
|
||||
label: I18n.tr("panels.plugins.filter-tags-label")
|
||||
description: I18n.tr("panels.plugins.filter-tags-description")
|
||||
expanded: true
|
||||
|
||||
formatTag: function (tag) {
|
||||
if (tag === "")
|
||||
return I18n.tr("launcher.categories.all");
|
||||
if (tag === "official")
|
||||
return I18n.tr("common.official");
|
||||
if (tag === "downloaded")
|
||||
return I18n.tr("panels.plugins.filter-downloaded");
|
||||
if (tag === "notDownloaded")
|
||||
return I18n.tr("panels.plugins.filter-not-downloaded");
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
// Search input with refresh button
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NTextInput {
|
||||
placeholderText: I18n.tr("placeholders.search")
|
||||
inputIconName: "search"
|
||||
text: root.pluginSearchText
|
||||
onTextChanged: root.pluginSearchText = text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
tooltipText: I18n.tr("panels.plugins.refresh-tooltip")
|
||||
baseSize: Style.baseWidgetSize * 0.9
|
||||
onClicked: {
|
||||
PluginService.refreshAvailablePlugins();
|
||||
checkUpdatesTimer.restart();
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.refresh-refreshing"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Available plugins list
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
Repeater {
|
||||
id: availablePluginsRepeater
|
||||
|
||||
model: {
|
||||
// Reference counter to force re-evaluation when plugins are updated
|
||||
void (root.availablePluginsRefreshCounter);
|
||||
|
||||
var all = PluginService.availablePlugins || [];
|
||||
var filtered = [];
|
||||
|
||||
// Apply filter based on selectedTag
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
var plugin = all[i];
|
||||
var downloaded = plugin.downloaded || false;
|
||||
var pluginTags = plugin.tags || [];
|
||||
|
||||
if (root.selectedTag === "") {
|
||||
// "All" - no filter
|
||||
filtered.push(plugin);
|
||||
} else if (root.selectedTag === "official") {
|
||||
// Official (team-maintained) pseudo tag
|
||||
if (plugin.official === true)
|
||||
filtered.push(plugin);
|
||||
} else if (root.selectedTag === "downloaded") {
|
||||
// Downloaded pseudo tag
|
||||
if (downloaded)
|
||||
filtered.push(plugin);
|
||||
} else if (root.selectedTag === "notDownloaded") {
|
||||
// Not Downloaded pseudo tag
|
||||
if (!downloaded)
|
||||
filtered.push(plugin);
|
||||
} else {
|
||||
// Actual category tag
|
||||
if (pluginTags.indexOf(root.selectedTag) >= 0) {
|
||||
filtered.push(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then apply fuzzy search if there's search text
|
||||
var query = root.pluginSearchText.trim();
|
||||
if (query !== "") {
|
||||
var results = FuzzySort.go(query, filtered, {
|
||||
"keys": ["name", "description"],
|
||||
"limit": 50
|
||||
});
|
||||
filtered = [];
|
||||
for (var j = 0; j < results.length; j++) {
|
||||
filtered.push(results[j].obj);
|
||||
}
|
||||
} else {
|
||||
// Sort by lastUpdated (most recent first) when not searching
|
||||
filtered.sort(function (a, b) {
|
||||
var dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
|
||||
var dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
}
|
||||
|
||||
// Move hello-world plugin to the end
|
||||
var helloWorldIndex = -1;
|
||||
for (var h = 0; h < filtered.length; h++) {
|
||||
if (filtered[h].id === "hello-world") {
|
||||
helloWorldIndex = h;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (helloWorldIndex >= 0) {
|
||||
var helloWorld = filtered.splice(helloWorldIndex, 1)[0];
|
||||
filtered.push(helloWorld);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
delegate: NBox {
|
||||
id: pluginBox
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.borderS
|
||||
Layout.rightMargin: Style.borderS
|
||||
implicitHeight: Math.round(contentColumn.implicitHeight + Style.margin2L)
|
||||
color: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginS
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
NIcon {
|
||||
icon: "plugin"
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.name
|
||||
color: Color.mPrimary
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Official badge (Noctalia Team maintained)
|
||||
Rectangle {
|
||||
visible: modelData.official === true
|
||||
color: Color.mSecondary
|
||||
radius: Style.radiusXS
|
||||
implicitWidth: officialBadgeRow.implicitWidth + Style.margin2S
|
||||
implicitHeight: officialBadgeRow.implicitHeight + Style.margin2XS
|
||||
|
||||
RowLayout {
|
||||
id: officialBadgeRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXS
|
||||
|
||||
NIcon {
|
||||
icon: "official-plugin"
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Color.mOnSecondary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("common.official")
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Open plugin page button
|
||||
NIconButton {
|
||||
icon: "external-link"
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
tooltipText: I18n.tr("panels.plugins.open-plugin-page")
|
||||
onClicked: {
|
||||
var sourceUrl = modelData.source?.url || "";
|
||||
Qt.openUrlExternally(sourceUrl && !PluginRegistry.isMainSource(sourceUrl) ? sourceUrl : "https://noctalia.dev/plugins/" + modelData.id + "/");
|
||||
}
|
||||
}
|
||||
|
||||
// Downloaded indicator
|
||||
NIcon {
|
||||
icon: "circle-check"
|
||||
pointSize: Style.baseWidgetSize * 0.5
|
||||
color: Color.mPrimary
|
||||
visible: modelData.downloaded === true
|
||||
}
|
||||
|
||||
// Install button (only shown when not downloaded and not installing)
|
||||
NIconButton {
|
||||
visible: modelData.downloaded === false && !PluginService.installingPlugins[modelData.id]
|
||||
icon: "download"
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
tooltipText: I18n.tr("common.install")
|
||||
onClicked: installPlugin(modelData)
|
||||
}
|
||||
|
||||
// Installing spinner
|
||||
NBusyIndicator {
|
||||
visible: !modelData.downloaded && (PluginService.installingPlugins[modelData.id] === true)
|
||||
size: Style.baseWidgetSize * 0.5
|
||||
running: visible
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
NText {
|
||||
visible: modelData.description
|
||||
text: modelData.description || ""
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Details row
|
||||
RowLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: "v" + modelData.version
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "•"
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: stripAuthorEmail(modelData.author)
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "•"
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.source ? modelData.source.name : ""
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: !!modelData.lastUpdated
|
||||
text: "•"
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: !!modelData.lastUpdated
|
||||
text: modelData.lastUpdated ? Time.formatRelativeTime(new Date(modelData.lastUpdated)) : ""
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NLabel {
|
||||
visible: availablePluginsRepeater.count === 0
|
||||
label: I18n.tr("panels.plugins.available-no-plugins-label")
|
||||
description: I18n.tr("panels.plugins.available-no-plugins-description")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to check for updates after refresh starts
|
||||
Timer {
|
||||
id: checkUpdatesTimer
|
||||
interval: 100
|
||||
onTriggered: {
|
||||
PluginService.checkForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
function installPlugin(pluginMetadata) {
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.installing", {
|
||||
"plugin": pluginMetadata.name
|
||||
}));
|
||||
|
||||
PluginService.installPlugin(pluginMetadata, false, function (success, error, registeredKey) {
|
||||
if (success) {
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.install-success", {
|
||||
"plugin": pluginMetadata.name
|
||||
}));
|
||||
// Auto-enable the plugin after installation (use registered key which may be composite)
|
||||
PluginService.enablePlugin(registeredKey);
|
||||
} else {
|
||||
ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.install-error", {
|
||||
"error": error || "Unknown error"
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Listen to plugin service signals
|
||||
Connections {
|
||||
target: PluginService
|
||||
|
||||
function onAvailablePluginsUpdated() {
|
||||
// Force tags and plugins model to re-evaluate
|
||||
root.tagsRefreshCounter++;
|
||||
root.availablePluginsRefreshCounter++;
|
||||
|
||||
// Manually trigger update check after a small delay to ensure all registries are loaded
|
||||
Qt.callLater(function () {
|
||||
PluginService.checkForUpdates();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+536
@@ -0,0 +1,536 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Track which plugins are currently updating
|
||||
property var updatingPlugins: ({})
|
||||
property int installedPluginsRefreshCounter: 0
|
||||
|
||||
function stripAuthorEmail(author) {
|
||||
if (!author)
|
||||
return "";
|
||||
var lastBracket = author.lastIndexOf("<");
|
||||
if (lastBracket >= 0) {
|
||||
return author.substring(0, lastBracket).trim();
|
||||
}
|
||||
return author;
|
||||
}
|
||||
|
||||
// Check for updates when tab becomes visible
|
||||
onVisibleChanged: {
|
||||
if (visible && PluginService.pluginsFullyLoaded) {
|
||||
PluginService.checkForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-update toggle
|
||||
NToggle {
|
||||
label: I18n.tr("panels.plugins.auto-update")
|
||||
description: I18n.tr("panels.plugins.auto-update-description")
|
||||
checked: Settings.data.plugins.autoUpdate
|
||||
onToggled: checked => Settings.data.plugins.autoUpdate = checked
|
||||
}
|
||||
|
||||
// Update notification toggle
|
||||
NToggle {
|
||||
label: I18n.tr("panels.plugins.notify-updates")
|
||||
description: I18n.tr("panels.plugins.notify-updates-description")
|
||||
checked: Settings.data.plugins.notifyUpdates
|
||||
onToggled: checked => Settings.data.plugins.notifyUpdates = checked
|
||||
}
|
||||
|
||||
// Check for updates button
|
||||
NButton {
|
||||
property bool isChecking: Object.keys(PluginService.activeFetches).length > 0
|
||||
|
||||
text: isChecking ? I18n.tr("panels.plugins.checking-for-updates") : I18n.tr("panels.plugins.check-for-updates")
|
||||
icon: "refresh"
|
||||
enabled: !isChecking
|
||||
visible: Object.keys(PluginService.pluginUpdates).length === 0
|
||||
Layout.fillWidth: true
|
||||
onClicked: PluginService.checkForUpdates()
|
||||
}
|
||||
|
||||
// Update All button
|
||||
NButton {
|
||||
property int updateCount: Object.keys(PluginService.pluginUpdates).length
|
||||
property bool isUpdating: false
|
||||
|
||||
text: I18n.tr("panels.plugins.update-all", {
|
||||
"count": updateCount
|
||||
})
|
||||
icon: "download"
|
||||
visible: (updateCount > 0)
|
||||
enabled: !isUpdating
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
isUpdating = true;
|
||||
var pluginIds = Object.keys(PluginService.pluginUpdates);
|
||||
var currentIndex = 0;
|
||||
|
||||
function updateNext() {
|
||||
if (currentIndex >= pluginIds.length) {
|
||||
isUpdating = false;
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.update-all-success"));
|
||||
return;
|
||||
}
|
||||
|
||||
var pluginId = pluginIds[currentIndex];
|
||||
currentIndex++;
|
||||
|
||||
PluginService.updatePlugin(pluginId, function (success, error) {
|
||||
if (!success) {
|
||||
Logger.w("InstalledSubTab", "Failed to update", pluginId + ":", error);
|
||||
}
|
||||
Qt.callLater(updateNext);
|
||||
});
|
||||
}
|
||||
|
||||
updateNext();
|
||||
}
|
||||
}
|
||||
|
||||
// Installed plugins list
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
Repeater {
|
||||
id: installedPluginsRepeater
|
||||
|
||||
model: {
|
||||
// Force refresh when counter changes
|
||||
var _ = root.installedPluginsRefreshCounter;
|
||||
|
||||
var allIds = PluginRegistry.getAllInstalledPluginIds();
|
||||
var plugins = [];
|
||||
for (var i = 0; i < allIds.length; i++) {
|
||||
var compositeKey = allIds[i];
|
||||
var manifest = PluginRegistry.getPluginManifest(compositeKey);
|
||||
if (manifest) {
|
||||
// Create a copy of manifest and include update info, enabled state, and source info
|
||||
var pluginData = JSON.parse(JSON.stringify(manifest));
|
||||
pluginData.compositeKey = compositeKey;
|
||||
pluginData.updateInfo = PluginService.pluginUpdates[compositeKey];
|
||||
pluginData.pendingUpdateInfo = PluginService.pluginUpdatesPending[compositeKey];
|
||||
pluginData.enabled = PluginRegistry.isPluginEnabled(compositeKey);
|
||||
|
||||
// Add source info
|
||||
var parsed = PluginRegistry.parseCompositeKey(compositeKey);
|
||||
pluginData.isFromOfficialRepo = parsed.isOfficial;
|
||||
if (!parsed.isOfficial) {
|
||||
pluginData.sourceName = PluginRegistry.getSourceNameByHash(parsed.sourceHash);
|
||||
}
|
||||
|
||||
// Look up "official" (team-maintained) status and lastUpdated from available plugins
|
||||
pluginData.official = false;
|
||||
pluginData.lastUpdated = null;
|
||||
var availablePlugins = PluginService.availablePlugins || [];
|
||||
for (var j = 0; j < availablePlugins.length; j++) {
|
||||
if (availablePlugins[j].id === manifest.id) {
|
||||
pluginData.official = availablePlugins[j].official === true;
|
||||
pluginData.lastUpdated = availablePlugins[j].lastUpdated || null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
plugins.push(pluginData);
|
||||
}
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
|
||||
delegate: NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.borderS
|
||||
Layout.rightMargin: Style.borderS
|
||||
implicitHeight: Math.round(contentColumn.implicitHeight + Style.margin2L)
|
||||
color: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginS
|
||||
|
||||
// Top row: icon, name, badge, spacer, action buttons
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
NIcon {
|
||||
icon: "plugin"
|
||||
pointSize: Style.fontSizeL
|
||||
color: PluginService.hasPluginError(modelData.compositeKey) ? Color.mError : Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.name
|
||||
color: Color.mPrimary
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Official badge (Noctalia Team maintained)
|
||||
Rectangle {
|
||||
visible: modelData.official === true
|
||||
color: Color.mSecondary
|
||||
radius: Style.radiusXS
|
||||
implicitWidth: officialBadgeRow.implicitWidth + Style.margin2S
|
||||
implicitHeight: officialBadgeRow.implicitHeight + Style.margin2XS
|
||||
|
||||
RowLayout {
|
||||
id: officialBadgeRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXS
|
||||
|
||||
NIcon {
|
||||
icon: "official-plugin"
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Color.mOnSecondary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("common.official")
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButtonHot {
|
||||
icon: "bug"
|
||||
hot: PluginService.isPluginHotReloadEnabled(modelData.id)
|
||||
tooltipText: PluginService.isPluginHotReloadEnabled(modelData.id) ? I18n.tr("panels.plugins.development-disable") : I18n.tr("panels.plugins.development-enable")
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
onClicked: PluginService.togglePluginHotReload(modelData.id)
|
||||
visible: Settings.isDebug
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: I18n.tr("panels.plugins.settings-tooltip")
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
visible: (modelData.entryPoints?.settings !== undefined)
|
||||
enabled: modelData.enabled
|
||||
onClicked: {
|
||||
pluginSettingsDialog.openPluginSettings(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "external-link"
|
||||
tooltipText: I18n.tr("panels.plugins.open-plugin-page")
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
visible: true
|
||||
onClicked: {
|
||||
var sourceUrl = PluginRegistry.getPluginSourceUrl(modelData.compositeKey) || "";
|
||||
Qt.openUrlExternally(sourceUrl && !PluginRegistry.isMainSource(sourceUrl) ? sourceUrl : "https://noctalia.dev/plugins/" + modelData.id);
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "trash"
|
||||
tooltipText: I18n.tr("common.uninstall")
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
onClicked: {
|
||||
uninstallDialog.pluginToUninstall = modelData;
|
||||
uninstallDialog.open();
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
id: updateButton
|
||||
property string pluginId: modelData.compositeKey
|
||||
property bool isUpdating: root.updatingPlugins[pluginId] === true
|
||||
|
||||
text: isUpdating ? I18n.tr("panels.plugins.updating") : I18n.tr("common.update")
|
||||
icon: isUpdating ? "" : "download"
|
||||
visible: modelData.updateInfo !== undefined
|
||||
enabled: !isUpdating
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
fontSize: Style.fontSizeXXS
|
||||
fontWeight: Style.fontWeightMedium
|
||||
onClicked: {
|
||||
var pid = pluginId;
|
||||
var pname = modelData.name;
|
||||
var pversion = modelData.updateInfo?.availableVersion || "";
|
||||
var rootRef = root;
|
||||
var updates = Object.assign({}, rootRef.updatingPlugins);
|
||||
updates[pid] = true;
|
||||
rootRef.updatingPlugins = updates;
|
||||
|
||||
PluginService.updatePlugin(pid, function (success, error) {
|
||||
var updates2 = Object.assign({}, rootRef.updatingPlugins);
|
||||
updates2[pid] = false;
|
||||
rootRef.updatingPlugins = updates2;
|
||||
|
||||
if (success) {
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.update-success", {
|
||||
"plugin": pname,
|
||||
"version": pversion
|
||||
}));
|
||||
} else {
|
||||
ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.update-error", {
|
||||
"plugin": pname,
|
||||
"error": error || "Unknown error"
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
checked: modelData.enabled
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
PluginService.enablePlugin(modelData.compositeKey);
|
||||
} else {
|
||||
PluginService.disablePlugin(modelData.compositeKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
NText {
|
||||
visible: modelData.description
|
||||
text: modelData.description || ""
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Details row
|
||||
RowLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: {
|
||||
if (modelData.updateInfo) {
|
||||
return I18n.tr("panels.plugins.update-version", {
|
||||
"current": modelData.version,
|
||||
"new": modelData.updateInfo.availableVersion
|
||||
});
|
||||
} else if (modelData.pendingUpdateInfo) {
|
||||
return I18n.tr("panels.plugins.update-pending", {
|
||||
"current": modelData.version,
|
||||
"new": modelData.pendingUpdateInfo.availableVersion,
|
||||
"required": modelData.pendingUpdateInfo.minNoctaliaVersion
|
||||
});
|
||||
}
|
||||
return "v" + modelData.version;
|
||||
}
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: modelData.updateInfo ? Color.mPrimary : (modelData.pendingUpdateInfo ? Color.mTertiary : Color.mOnSurfaceVariant)
|
||||
font.weight: (modelData.updateInfo || modelData.pendingUpdateInfo) ? Style.fontWeightMedium : Style.fontWeightRegular
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "•"
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: stripAuthorEmail(modelData.author)
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
// Source indicator for plugins from non-official repos
|
||||
NText {
|
||||
visible: !modelData.isFromOfficialRepo
|
||||
text: "•"
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: !modelData.isFromOfficialRepo
|
||||
text: modelData.sourceName || I18n.tr("panels.plugins.source-custom")
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mTertiary
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: !!modelData.lastUpdated
|
||||
text: "•"
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: !!modelData.lastUpdated
|
||||
text: modelData.lastUpdated ? Time.formatRelativeTime(new Date(modelData.lastUpdated)) : ""
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Error indicator
|
||||
RowLayout {
|
||||
spacing: Style.marginS
|
||||
visible: PluginService.hasPluginError(modelData.compositeKey)
|
||||
|
||||
NIcon {
|
||||
icon: "alert-triangle"
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mError
|
||||
}
|
||||
|
||||
NText {
|
||||
property var errorInfo: PluginService.getPluginError(modelData.compositeKey)
|
||||
text: errorInfo ? errorInfo.error : ""
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
color: Color.mError
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NLabel {
|
||||
visible: PluginRegistry.getAllInstalledPluginIds().length === 0
|
||||
label: I18n.tr("panels.plugins.installed-no-plugins-label")
|
||||
description: I18n.tr("panels.plugins.installed-no-plugins-description")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Uninstall confirmation dialog
|
||||
Popup {
|
||||
id: uninstallDialog
|
||||
parent: Overlay.overlay
|
||||
modal: true
|
||||
dim: false
|
||||
anchors.centerIn: parent
|
||||
width: 400 * Style.uiScaleRatio
|
||||
padding: Style.marginL
|
||||
|
||||
property var pluginToUninstall: null
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusS
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderM
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: Style.marginL
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("panels.plugins.uninstall-dialog-title")
|
||||
description: I18n.tr("panels.plugins.uninstall-dialog-description", {
|
||||
"plugin": uninstallDialog.pluginToUninstall?.name || ""
|
||||
})
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.cancel")
|
||||
onClicked: uninstallDialog.close()
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.uninstall")
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
onClicked: {
|
||||
if (uninstallDialog.pluginToUninstall) {
|
||||
root.uninstallPlugin(uninstallDialog.pluginToUninstall.compositeKey);
|
||||
uninstallDialog.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin settings popup
|
||||
NPluginSettingsPopup {
|
||||
id: pluginSettingsDialog
|
||||
parent: Overlay.overlay
|
||||
showToastOnSave: true
|
||||
}
|
||||
|
||||
function uninstallPlugin(pluginId) {
|
||||
var manifest = PluginRegistry.getPluginManifest(pluginId);
|
||||
var pluginName = manifest?.name || pluginId;
|
||||
|
||||
BarService.widgetsRevision++;
|
||||
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.uninstalling", {
|
||||
"plugin": pluginName
|
||||
}));
|
||||
|
||||
PluginService.uninstallPlugin(pluginId, function (success, error) {
|
||||
if (success) {
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.uninstall-success", {
|
||||
"plugin": pluginName
|
||||
}));
|
||||
} else {
|
||||
ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.uninstall-error", {
|
||||
"error": error || "Unknown error"
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Listen to plugin registry changes
|
||||
Connections {
|
||||
target: PluginRegistry
|
||||
|
||||
function onPluginsChanged() {
|
||||
root.installedPluginsRefreshCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
// Listen to plugin service signals
|
||||
Connections {
|
||||
target: PluginService
|
||||
|
||||
function onPluginUpdatesChanged() {
|
||||
root.installedPluginsRefreshCounter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.installed")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.available")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.sources")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
InstalledSubTab {}
|
||||
AvailableSubTab {}
|
||||
SourcesSubTab {}
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
// List of plugin sources
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
Repeater {
|
||||
id: pluginSourcesRepeater
|
||||
model: PluginRegistry.pluginSources || []
|
||||
|
||||
delegate: NBox {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: sourceRow.implicitHeight + Style.margin2L
|
||||
color: Color.mSurface
|
||||
|
||||
RowLayout {
|
||||
id: sourceRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "brand-github"
|
||||
pointSize: Style.fontSizeL
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: modelData.name
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.url
|
||||
font.pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "trash"
|
||||
tooltipText: I18n.tr("panels.plugins.sources-remove-tooltip")
|
||||
visible: index !== 0 // Cannot remove official source
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
onClicked: {
|
||||
PluginRegistry.removePluginSource(modelData.url);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable/Disable a source
|
||||
NToggle {
|
||||
checked: modelData.enabled !== false // Default to true if not set
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
onToggled: checked => {
|
||||
PluginRegistry.setSourceEnabled(modelData.url, checked);
|
||||
PluginService.refreshAvailablePlugins();
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.refresh-refreshing"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add custom repository
|
||||
NButton {
|
||||
text: I18n.tr("panels.plugins.sources-add-custom")
|
||||
icon: "plus"
|
||||
onClicked: {
|
||||
addSourceDialog.open();
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Add source dialog
|
||||
Popup {
|
||||
id: addSourceDialog
|
||||
parent: Overlay.overlay
|
||||
modal: true
|
||||
dim: false
|
||||
anchors.centerIn: parent
|
||||
width: 500
|
||||
padding: Style.marginL
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusS
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderM
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: Style.marginL
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("panels.plugins.sources-add-dialog-title")
|
||||
description: I18n.tr("panels.plugins.sources-add-dialog-description")
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: sourceNameInput
|
||||
label: I18n.tr("panels.plugins.sources-add-dialog-name")
|
||||
placeholderText: I18n.tr("panels.plugins.sources-add-dialog-name-placeholder")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: sourceUrlInput
|
||||
label: I18n.tr("panels.plugins.sources-add-dialog-url")
|
||||
placeholderText: "https://github.com/user/repo"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.cancel")
|
||||
onClicked: addSourceDialog.close()
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.add")
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
enabled: sourceNameInput.text.length > 0 && sourceUrlInput.text.length > 0
|
||||
onClicked: {
|
||||
if (PluginRegistry.addPluginSource(sourceNameInput.text, sourceUrlInput.text)) {
|
||||
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.sources-add-dialog-success"));
|
||||
PluginService.refreshAvailablePlugins();
|
||||
addSourceDialog.close();
|
||||
sourceNameInput.text = "";
|
||||
sourceUrlInput.text = "";
|
||||
} else {
|
||||
ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.sources-add-dialog-error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen to plugin registry changes
|
||||
Connections {
|
||||
target: PluginRegistry
|
||||
|
||||
function onPluginsChanged() {
|
||||
// Force model refresh for plugin sources
|
||||
pluginSourcesRepeater.model = undefined;
|
||||
Qt.callLater(function () {
|
||||
pluginSourcesRepeater.model = Qt.binding(function () {
|
||||
return PluginRegistry.pluginSources || [];
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user