mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -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:
@@ -106,6 +106,8 @@ func init() {
|
||||
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||
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{
|
||||
@@ -184,10 +186,22 @@ var pluginsUninstallCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var pluginsUpdateCmd = &cobra.Command{
|
||||
Use: "update <plugin-id>",
|
||||
Short: "Update a plugin by ID",
|
||||
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Use: "update [plugin-id]",
|
||||
Short: "Update a plugin by ID, or all plugins",
|
||||
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: 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) {
|
||||
if len(args) != 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
@@ -195,6 +209,26 @@ var pluginsUpdateCmd = &cobra.Command{
|
||||
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
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 {
|
||||
log.Fatalf("Error updating plugin: %v", err)
|
||||
}
|
||||
@@ -370,7 +404,11 @@ func listInstalledPlugins() error {
|
||||
fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames))
|
||||
for _, id := range installedNames {
|
||||
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(" Category: %s\n", plugin.Category)
|
||||
fmt.Printf(" Author: %s\n", plugin.Author)
|
||||
@@ -550,6 +588,160 @@ func updatePluginCLI(idOrName string) error {
|
||||
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 {
|
||||
return []*cobra.Command{
|
||||
versionCmd,
|
||||
|
||||
@@ -554,35 +554,50 @@ func (m *Manager) findInDirByIDOrName(dir, idOrName string) (string, error) {
|
||||
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)
|
||||
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 == "" {
|
||||
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") {
|
||||
return false, nil
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
metaPath := pluginPath + ".meta"
|
||||
metaExists, err := afero.Exists(m.fs, metaPath)
|
||||
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 {
|
||||
// Plugin is from a monorepo, check the repo directory
|
||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||
repoName := m.getRepoName(plugin.Repo)
|
||||
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
|
||||
return m.gitClient.HasUpdates(pluginPath)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ type Plugin struct {
|
||||
type GitClient interface {
|
||||
PlainClone(path string, url 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{}
|
||||
@@ -65,10 +65,10 @@ func (g *realGitClient) Pull(path string) error {
|
||||
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)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, "", "", err
|
||||
}
|
||||
|
||||
// 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 fetch fails, we can't determine if there are updates
|
||||
// Return false and the error
|
||||
return false, err
|
||||
return false, "", "", err
|
||||
}
|
||||
|
||||
// Get the HEAD reference
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, "", "", err
|
||||
}
|
||||
|
||||
// Get the remote HEAD reference (typically origin/HEAD or origin/main or origin/master)
|
||||
remote, err := repo.Remote("origin")
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, "", "", err
|
||||
}
|
||||
|
||||
refs, err := remote.List(&git.ListOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, "", "", err
|
||||
}
|
||||
|
||||
// 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 remoteHead == "" {
|
||||
return false, nil
|
||||
return false, localHash, "", nil
|
||||
}
|
||||
|
||||
// Compare local HEAD with remote HEAD
|
||||
return head.Hash().String() != remoteHead, nil
|
||||
return localHash != remoteHead, localHash, remoteHead, nil
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
type mockGitClient struct {
|
||||
cloneFunc func(path string, url 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 {
|
||||
@@ -30,11 +30,11 @@ func (m *mockGitClient) Pull(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockGitClient) HasUpdates(path string) (bool, error) {
|
||||
func (m *mockGitClient) HasUpdates(path string) (bool, string, string, error) {
|
||||
if m.hasUpdatesFunc != nil {
|
||||
return m.hasUpdatesFunc(path)
|
||||
}
|
||||
return false, nil
|
||||
return false, "", "", nil
|
||||
}
|
||||
|
||||
func TestNewRegistry(t *testing.T) {
|
||||
|
||||
@@ -42,12 +42,15 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
|
||||
for _, id := range installedNames {
|
||||
if plugin, ok := pluginMap[id]; ok {
|
||||
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
|
||||
diffURL = dURL
|
||||
}
|
||||
|
||||
info := pluginInfoFromPlugin(plugin)
|
||||
info.HasUpdate = hasUpdate
|
||||
info.DiffURL = diffURL
|
||||
result = append(result, info)
|
||||
} else {
|
||||
result = append(result, PluginInfo{
|
||||
|
||||
@@ -18,6 +18,7 @@ type PluginInfo struct {
|
||||
Note string `json:"note,omitempty"`
|
||||
HasUpdate bool `json:"hasUpdate,omitempty"`
|
||||
RequiresDMS string `json:"requires_dms,omitempty"`
|
||||
DiffURL string `json:"diffUrl,omitempty"`
|
||||
Upvotes int `json:"upvotes,omitempty"`
|
||||
Status []string `json:"status,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 var filteredPlugins: []
|
||||
|
||||
readonly property var pluginsWithUpdates: {
|
||||
if (!DMSService.installedPlugins) return [];
|
||||
return DMSService.installedPlugins.filter(p => p.hasUpdate === true);
|
||||
}
|
||||
|
||||
function updateFilteredPlugins() {
|
||||
var query = searchQuery.toLowerCase();
|
||||
filteredPlugins = PluginService.availablePluginsList.filter(plugin => {
|
||||
@@ -261,10 +266,24 @@ 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 {
|
||||
width: parent.width
|
||||
height: directoryColumn.implicitHeight + Theme.spacingL * 2
|
||||
@@ -533,4 +552,8 @@ FocusScope {
|
||||
if (pluginBrowserLoader.item)
|
||||
pluginBrowserLoader.item.show();
|
||||
}
|
||||
|
||||
function showPluginUpdatesDialog() {
|
||||
pluginUpdatesDialogItem.show(pluginsTab.pluginsWithUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user