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:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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,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 {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: directoryColumn.implicitHeight + Theme.spacingL * 2
|
height: directoryColumn.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -533,4 +552,8 @@ FocusScope {
|
|||||||
if (pluginBrowserLoader.item)
|
if (pluginBrowserLoader.item)
|
||||||
pluginBrowserLoader.item.show();
|
pluginBrowserLoader.item.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showPluginUpdatesDialog() {
|
||||||
|
pluginUpdatesDialogItem.show(pluginsTab.pluginsWithUpdates);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user