1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-10 15:38:31 -04:00

feat(plugins): add update all CLI flag and settings updates dialog (#2682)

* feat(plugins): add update all CLI flag and settings update dialog

* feat(plugins): add comparison diff URL support and update button styling

* feat(plugins): skip system plugins in bulk CLI update

* fix(plugins): remove check shorthand to resolve conflict with config flag

* feat(plugins): inline update dialog, show version tags, restructure buttons
This commit is contained in:
Huỳnh Thiện Lộc
2026-07-04 08:51:39 +07:00
committed by GitHub
parent 6e3e178721
commit 8465ed4311
8 changed files with 554 additions and 26 deletions
+197 -5
View File
@@ -106,6 +106,8 @@ func init() {
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
printIPCHelp() printIPCHelp()
}) })
pluginsUpdateCmd.Flags().BoolP("all", "a", false, "Update all installed plugins")
pluginsUpdateCmd.Flags().Bool("check", false, "Check for available updates without applying them")
} }
var debugSrvCmd = &cobra.Command{ var debugSrvCmd = &cobra.Command{
@@ -184,10 +186,22 @@ var pluginsUninstallCmd = &cobra.Command{
} }
var pluginsUpdateCmd = &cobra.Command{ var pluginsUpdateCmd = &cobra.Command{
Use: "update <plugin-id>", Use: "update [plugin-id]",
Short: "Update a plugin by ID", Short: "Update a plugin by ID, or all plugins",
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.", Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). If --all or -a is specified, all installed plugins will be updated.",
Args: cobra.ExactArgs(1), Args: func(cmd *cobra.Command, args []string) error {
updateAll, _ := cmd.Flags().GetBool("all")
if updateAll {
if len(args) > 0 {
return fmt.Errorf("cannot specify plugin ID when using --all/-a")
}
return nil
}
if len(args) != 1 {
return fmt.Errorf("requires exactly 1 arg (plugin ID) or use --all/-a")
}
return nil
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 { if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
@@ -195,6 +209,26 @@ var pluginsUpdateCmd = &cobra.Command{
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
checkOnly, _ := cmd.Flags().GetBool("check")
updateAll, _ := cmd.Flags().GetBool("all")
if checkOnly {
if updateAll {
if err := checkAllPluginsCLI(); err != nil {
log.Fatalf("Error checking updates: %v", err)
}
return
}
if err := checkPluginCLI(args[0]); err != nil {
log.Fatalf("Error checking update: %v", err)
}
return
}
if updateAll {
if err := updateAllPluginsCLI(); err != nil {
log.Fatalf("Error updating plugins: %v", err)
}
return
}
if err := updatePluginCLI(args[0]); err != nil { if err := updatePluginCLI(args[0]); err != nil {
log.Fatalf("Error updating plugin: %v", err) log.Fatalf("Error updating plugin: %v", err)
} }
@@ -370,7 +404,11 @@ func listInstalledPlugins() error {
fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames)) fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames))
for _, id := range installedNames { for _, id := range installedNames {
if plugin, ok := pluginMap[id]; ok { if plugin, ok := pluginMap[id]; ok {
fmt.Printf(" %s\n", plugin.Name) hasUpdateStr := ""
if hasUpdates, _, err := manager.HasUpdates(id, plugin); err == nil && hasUpdates {
hasUpdateStr = " (update available)"
}
fmt.Printf(" %s%s\n", plugin.Name, hasUpdateStr)
fmt.Printf(" ID: %s\n", plugin.ID) fmt.Printf(" ID: %s\n", plugin.ID)
fmt.Printf(" Category: %s\n", plugin.Category) fmt.Printf(" Category: %s\n", plugin.Category)
fmt.Printf(" Author: %s\n", plugin.Author) fmt.Printf(" Author: %s\n", plugin.Author)
@@ -550,6 +588,160 @@ func updatePluginCLI(idOrName string) error {
return nil return nil
} }
func updateAllPluginsCLI() error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
installed, err := manager.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list installed plugins: %w", err)
}
pluginList, _ := registry.List()
var errs []error
for _, pluginID := range installed {
plugin := plugins.FindByIDOrName(pluginID, pluginList)
if plugin != nil {
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Update(*plugin); err != nil {
if strings.Contains(err.Error(), "cannot update system plugin") {
fmt.Printf("Skipping system plugin: %s\n", plugin.Name)
} else {
errs = append(errs, fmt.Errorf("failed to update %s: %w", plugin.Name, err))
}
} else {
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
}
} else {
fmt.Printf("Updating plugin: %s\n", pluginID)
if err := manager.UpdateByIDOrName(pluginID); err != nil {
if strings.Contains(err.Error(), "cannot update system plugin") {
fmt.Printf("Skipping system plugin: %s\n", pluginID)
} else {
errs = append(errs, fmt.Errorf("failed to update %s: %w", pluginID, err))
}
} else {
fmt.Printf("Plugin updated successfully: %s\n", pluginID)
}
}
}
if len(errs) > 0 {
for _, err := range errs {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
return fmt.Errorf("failed to update some plugins")
}
return nil
}
func checkPluginCLI(idOrName string) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(idOrName, pluginList)
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
}
hasUpdates, _, err := manager.HasUpdates(plugin.ID, *plugin)
if err != nil {
return fmt.Errorf("failed to check updates: %w", err)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
} else {
fmt.Printf("Plugin is up to date: %s\n", plugin.Name)
}
return nil
}
dummyPlugin := plugins.Plugin{ID: idOrName}
hasUpdates, _, err := manager.HasUpdates(idOrName, dummyPlugin)
if err != nil {
return fmt.Errorf("failed to check updates: %w", err)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s\n", idOrName)
} else {
fmt.Printf("Plugin is up to date: %s\n", idOrName)
}
return nil
}
func checkAllPluginsCLI() error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
installed, err := manager.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list installed plugins: %w", err)
}
pluginList, _ := registry.List()
var count int
for _, pluginID := range installed {
plugin := plugins.FindByIDOrName(pluginID, pluginList)
var hasUpdates bool
var name string
if plugin != nil {
name = plugin.Name
hasUpdates, _, _ = manager.HasUpdates(pluginID, *plugin)
} else {
name = pluginID
dummyPlugin := plugins.Plugin{ID: pluginID}
hasUpdates, _, _ = manager.HasUpdates(pluginID, dummyPlugin)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s (ID: %s)\n", name, pluginID)
count++
}
}
if count > 0 {
fmt.Printf("\nFound %d plugin(s) with available updates.\n", count)
} else {
fmt.Println("All plugins are up to date.")
}
return nil
}
func getCommonCommands() []*cobra.Command { func getCommonCommands() []*cobra.Command {
return []*cobra.Command{ return []*cobra.Command{
versionCmd, versionCmd,
+23 -8
View File
@@ -554,35 +554,50 @@ func (m *Manager) findInDirByIDOrName(dir, idOrName string) (string, error) {
return "", nil return "", nil
} }
func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (bool, error) { func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (hasUpdates bool, diffURL string, err error) {
pluginPath, err := m.findInstalledPath(pluginID) pluginPath, err := m.findInstalledPath(pluginID)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to find plugin: %w", err) return false, "", fmt.Errorf("failed to find plugin: %w", err)
} }
if pluginPath == "" { if pluginPath == "" {
return false, fmt.Errorf("plugin not installed: %s", pluginID) return false, "", fmt.Errorf("plugin not installed: %s", pluginID)
} }
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") { if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
return false, nil return false, "", nil
} }
metaPath := pluginPath + ".meta" metaPath := pluginPath + ".meta"
metaExists, err := afero.Exists(m.fs, metaPath) metaExists, err := afero.Exists(m.fs, metaPath)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to check metadata: %w", err) return false, "", fmt.Errorf("failed to check metadata: %w", err)
} }
var hasUp bool
var localHash, remoteHash string
if metaExists { if metaExists {
// Plugin is from a monorepo, check the repo directory // Plugin is from a monorepo, check the repo directory
reposDir := filepath.Join(m.pluginsDir, ".repos") reposDir := filepath.Join(m.pluginsDir, ".repos")
repoName := m.getRepoName(plugin.Repo) repoName := m.getRepoName(plugin.Repo)
repoPath := filepath.Join(reposDir, repoName) repoPath := filepath.Join(reposDir, repoName)
return m.gitClient.HasUpdates(repoPath) hasUp, localHash, remoteHash, err = m.gitClient.HasUpdates(repoPath)
} else {
// Plugin is a standalone repo
hasUp, localHash, remoteHash, err = m.gitClient.HasUpdates(pluginPath)
} }
// Plugin is a standalone repo if err != nil {
return m.gitClient.HasUpdates(pluginPath) return false, "", err
}
diffURL = plugin.Repo
if diffURL != "" {
diffURL = strings.TrimSuffix(diffURL, ".git")
if hasUp && len(localHash) >= 7 && len(remoteHash) >= 7 {
diffURL = fmt.Sprintf("%s/compare/%s...%s", diffURL, localHash[:7], remoteHash[:7])
}
}
return hasUp, diffURL, nil
} }
+10 -9
View File
@@ -33,7 +33,7 @@ type Plugin struct {
type GitClient interface { type GitClient interface {
PlainClone(path string, url string) error PlainClone(path string, url string) error
Pull(path string) error Pull(path string) error
HasUpdates(path string) (bool, error) HasUpdates(path string) (hasUpdates bool, localHash string, remoteHash string, err error)
} }
type realGitClient struct{} type realGitClient struct{}
@@ -65,10 +65,10 @@ func (g *realGitClient) Pull(path string) error {
return nil return nil
} }
func (g *realGitClient) HasUpdates(path string) (bool, error) { func (g *realGitClient) HasUpdates(path string) (bool, string, string, error) {
repo, err := git.PlainOpen(path) repo, err := git.PlainOpen(path)
if err != nil { if err != nil {
return false, err return false, "", "", err
} }
// Fetch remote changes // Fetch remote changes
@@ -76,24 +76,24 @@ func (g *realGitClient) HasUpdates(path string) (bool, error) {
if err != nil && err.Error() != "already up-to-date" { if err != nil && err.Error() != "already up-to-date" {
// If fetch fails, we can't determine if there are updates // If fetch fails, we can't determine if there are updates
// Return false and the error // Return false and the error
return false, err return false, "", "", err
} }
// Get the HEAD reference // Get the HEAD reference
head, err := repo.Head() head, err := repo.Head()
if err != nil { if err != nil {
return false, err return false, "", "", err
} }
// Get the remote HEAD reference (typically origin/HEAD or origin/main or origin/master) // Get the remote HEAD reference (typically origin/HEAD or origin/main or origin/master)
remote, err := repo.Remote("origin") remote, err := repo.Remote("origin")
if err != nil { if err != nil {
return false, err return false, "", "", err
} }
refs, err := remote.List(&git.ListOptions{}) refs, err := remote.List(&git.ListOptions{})
if err != nil { if err != nil {
return false, err return false, "", "", err
} }
// Find the default branch remote ref // Find the default branch remote ref
@@ -108,13 +108,14 @@ func (g *realGitClient) HasUpdates(path string) (bool, error) {
} }
} }
localHash := head.Hash().String()
// If we couldn't find a remote HEAD, assume no updates // If we couldn't find a remote HEAD, assume no updates
if remoteHead == "" { if remoteHead == "" {
return false, nil return false, localHash, "", nil
} }
// Compare local HEAD with remote HEAD // Compare local HEAD with remote HEAD
return head.Hash().String() != remoteHead, nil return localHash != remoteHead, localHash, remoteHead, nil
} }
type Registry struct { type Registry struct {
+3 -3
View File
@@ -13,7 +13,7 @@ import (
type mockGitClient struct { type mockGitClient struct {
cloneFunc func(path string, url string) error cloneFunc func(path string, url string) error
pullFunc func(path string) error pullFunc func(path string) error
hasUpdatesFunc func(path string) (bool, error) hasUpdatesFunc func(path string) (bool, string, string, error)
} }
func (m *mockGitClient) PlainClone(path string, url string) error { func (m *mockGitClient) PlainClone(path string, url string) error {
@@ -30,11 +30,11 @@ func (m *mockGitClient) Pull(path string) error {
return nil return nil
} }
func (m *mockGitClient) HasUpdates(path string) (bool, error) { func (m *mockGitClient) HasUpdates(path string) (bool, string, string, error) {
if m.hasUpdatesFunc != nil { if m.hasUpdatesFunc != nil {
return m.hasUpdatesFunc(path) return m.hasUpdatesFunc(path)
} }
return false, nil return false, "", "", nil
} }
func TestNewRegistry(t *testing.T) { func TestNewRegistry(t *testing.T) {
@@ -42,12 +42,15 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
for _, id := range installedNames { for _, id := range installedNames {
if plugin, ok := pluginMap[id]; ok { if plugin, ok := pluginMap[id]; ok {
hasUpdate := false hasUpdate := false
if hasUpdates, err := manager.HasUpdates(id, plugin); err == nil { diffURL := plugin.Repo
if hasUpdates, dURL, err := manager.HasUpdates(id, plugin); err == nil {
hasUpdate = hasUpdates hasUpdate = hasUpdates
diffURL = dURL
} }
info := pluginInfoFromPlugin(plugin) info := pluginInfoFromPlugin(plugin)
info.HasUpdate = hasUpdate info.HasUpdate = hasUpdate
info.DiffURL = diffURL
result = append(result, info) result = append(result, info)
} else { } else {
result = append(result, PluginInfo{ result = append(result, PluginInfo{
+1
View File
@@ -18,6 +18,7 @@ type PluginInfo struct {
Note string `json:"note,omitempty"` Note string `json:"note,omitempty"`
HasUpdate bool `json:"hasUpdate,omitempty"` HasUpdate bool `json:"hasUpdate,omitempty"`
RequiresDMS string `json:"requires_dms,omitempty"` RequiresDMS string `json:"requires_dms,omitempty"`
DiffURL string `json:"diffUrl,omitempty"`
Upvotes int `json:"upvotes,omitempty"` Upvotes int `json:"upvotes,omitempty"`
Status []string `json:"status,omitempty"` Status []string `json:"status,omitempty"`
IssueURL string `json:"issueUrl,omitempty"` IssueURL string `json:"issueUrl,omitempty"`
@@ -0,0 +1,293 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Services
StyledRect {
id: root
property var updatesList: []
property bool isUpdating: false
property string currentUpdatingPlugin: ""
width: parent.width
height: visible ? Math.max(200, innerColumn.implicitHeight + Theme.spacingL * 2) : 0
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.width: 0
clip: true
visible: false
Behavior on height {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
}
}
function show(list) {
updatesList = list || [];
visible = true;
}
function hide() {
if (!isUpdating) {
visible = false;
updatesList = [];
}
}
function updateSingle(plugin) {
if (isUpdating) return;
isUpdating = true;
currentUpdatingPlugin = plugin.name;
DMSService.update(plugin.name, response => {
isUpdating = false;
currentUpdatingPlugin = "";
if (response.error) {
ToastService.showError(I18n.tr("Failed to update %1: %2").arg(plugin.name).arg(response.error));
} else {
ToastService.showInfo(I18n.tr("Plugin updated: %1").arg(plugin.name));
PluginService.forceRescanPlugin(plugin.id);
DMSService.listInstalled();
updatesList = updatesList.filter(p => p.id !== plugin.id);
if (updatesList.length === 0) {
root.hide();
}
}
});
}
function updateAll() {
if (isUpdating) return;
isUpdating = true;
var list = updatesList.slice();
var idx = 0;
function updateNext() {
if (idx >= list.length) {
isUpdating = false;
currentUpdatingPlugin = "";
DMSService.listInstalled();
root.hide();
return;
}
var plugin = list[idx];
currentUpdatingPlugin = plugin.name;
DMSService.update(plugin.name, response => {
if (response.error) {
ToastService.showError(I18n.tr("Failed to update %1: %2").arg(plugin.name).arg(response.error));
} else {
PluginService.forceRescanPlugin(plugin.id);
updatesList = updatesList.filter(p => p.id !== plugin.id);
}
idx++;
updateNext();
});
}
updateNext();
}
Column {
id: innerColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "download"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Available Updates (%1)").arg(root.updatesList.length)
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - parent.spacing * 2 - Theme.iconSize - parent.children[1].implicitWidth - collapseBtn.width
height: 1
}
DankActionButton {
id: collapseBtn
iconName: "close"
iconSize: Theme.iconSize - 2
iconColor: Theme.outline
anchors.verticalCenter: parent.verticalCenter
enabled: !root.isUpdating
onClicked: root.hide()
}
}
Item {
width: parent.width
height: isUpdating ? 40 : 0
visible: isUpdating
clip: true
Behavior on height {
NumberAnimation { duration: Theme.shortDuration }
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingM
DankSpinner {
running: root.isUpdating
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.currentUpdatingPlugin ? I18n.tr("Updating %1...").arg(root.currentUpdatingPlugin) : I18n.tr("Updating plugins...")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
DankFlickable {
width: parent.width
height: Math.min(listCol.implicitHeight, 300)
clip: true
contentHeight: listCol.implicitHeight
visible: !isUpdating
Column {
id: listCol
width: parent.width
spacing: Theme.spacingM
Repeater {
model: root.updatesList
delegate: StyledRect {
width: parent.width
height: 64
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
border.width: 0
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: modelData.icon || "extension"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
width: parent.width - Theme.iconSize - Theme.spacingM - actionButtonsRow.width - Theme.spacingM
StyledText {
text: modelData.name || ""
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: modelData.author ? I18n.tr("By %1").arg(modelData.author) : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
horizontalAlignment: Text.AlignLeft
}
}
Row {
id: actionButtonsRow
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankButton {
text: I18n.tr("Diff")
iconName: "open_in_new"
visible: !!modelData.diffUrl || !!modelData.repo
backgroundColor: Theme.surfaceContainerHigh
textColor: Theme.surfaceText
onClicked: {
Qt.openUrlExternally(modelData.diffUrl || modelData.repo);
}
}
DankButton {
text: I18n.tr("Update")
iconName: "download"
enabled: !root.isUpdating
onClicked: {
root.updateSingle(modelData);
}
}
}
}
}
}
StyledText {
width: parent.width
text: I18n.tr("No updates available.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: root.updatesList.length === 0
}
}
}
Row {
anchors.right: parent.right
spacing: Theme.spacingM
visible: !isUpdating
DankButton {
text: I18n.tr("Cancel")
iconName: "close"
backgroundColor: Theme.surfaceContainerHighest
textColor: Theme.surfaceText
onClicked: root.hide()
}
DankButton {
text: I18n.tr("Update All")
iconName: "download"
enabled: root.updatesList.length > 0
onClicked: root.updateAll()
}
}
}
}
@@ -20,6 +20,11 @@ FocusScope {
property string searchQuery: "" property string searchQuery: ""
property var filteredPlugins: [] property var filteredPlugins: []
readonly property var pluginsWithUpdates: {
if (!DMSService.installedPlugins) return [];
return DMSService.installedPlugins.filter(p => p.hasUpdate === true);
}
function updateFilteredPlugins() { function updateFilteredPlugins() {
var query = searchQuery.toLowerCase(); var query = searchQuery.toLowerCase();
filteredPlugins = PluginService.availablePluginsList.filter(plugin => { filteredPlugins = PluginService.availablePluginsList.filter(plugin => {
@@ -261,9 +266,23 @@ FocusScope {
} }
} }
} }
DankButton {
text: I18n.tr("Update All")
iconName: "download"
enabled: DMSService.dmsAvailable && pluginsTab.pluginsWithUpdates.length > 0
onClicked: {
showPluginUpdatesDialog();
} }
} }
} }
}
}
PluginUpdatesDialog {
id: pluginUpdatesDialogItem
width: parent.width
}
StyledRect { StyledRect {
width: parent.width width: parent.width
@@ -533,4 +552,8 @@ FocusScope {
if (pluginBrowserLoader.item) if (pluginBrowserLoader.item)
pluginBrowserLoader.item.show(); pluginBrowserLoader.item.show();
} }
function showPluginUpdatesDialog() {
pluginUpdatesDialogItem.show(pluginsTab.pluginsWithUpdates);
}
} }