Changements : - Detection des fichiers non suivis (untracked) dans chaque depot - Affichage "X fichier(s) en trop" dans le statut - Popup de confirmation listant les fichiers avant suppression (git clean -fd) - Suppression auto des fichiers en trop via "Tout mettre a jour" - Verification du depot distant via git ls-remote avant de proposer le clone - Affichage "Depot introuvable" si l'URL pointe vers un repo inexistant Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
693 lines
17 KiB
Go
693 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/lxn/walk"
|
|
. "github.com/lxn/walk/declarative"
|
|
)
|
|
|
|
// ── Modèle TableView ──────────────────────────────────────────────────────────
|
|
|
|
type RepoItem struct {
|
|
result RepoResult
|
|
progress float64 // 0.0 à 1.0
|
|
progressText string // ex: "Réception 45% (1.2 Go/2.5 Go)"
|
|
}
|
|
|
|
func (it *RepoItem) statusText() string {
|
|
r := it.result
|
|
if r.Pending {
|
|
return "Vérification..."
|
|
}
|
|
if r.Error != "" {
|
|
return r.Error
|
|
}
|
|
if r.NeedsClone {
|
|
return "À cloner"
|
|
}
|
|
if r.UpToDate {
|
|
return "À jour"
|
|
}
|
|
msg := ""
|
|
if r.NewCommits > 0 {
|
|
msg = fmt.Sprintf("%d commit(s)", r.NewCommits)
|
|
}
|
|
if r.LocalChanges > 0 {
|
|
if msg != "" {
|
|
msg += ", "
|
|
}
|
|
msg += fmt.Sprintf("%d modif. locale(s)", r.LocalChanges)
|
|
}
|
|
if r.UntrackedFiles > 0 {
|
|
if msg != "" {
|
|
msg += ", "
|
|
}
|
|
msg += fmt.Sprintf("%d fichier(s) en trop", r.UntrackedFiles)
|
|
}
|
|
if msg == "" {
|
|
return "À jour"
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// progressBarText génère une barre de progression visuelle en Unicode.
|
|
// Ex: "████████░░░░ 67% Réception objets"
|
|
func progressBarText(pct float64, width int, label string) string {
|
|
if width <= 0 {
|
|
width = 20
|
|
}
|
|
filled := int(pct * float64(width))
|
|
if filled > width {
|
|
filled = width
|
|
}
|
|
bar := strings.Repeat("█", filled) + strings.Repeat("░", width-filled)
|
|
pctInt := int(pct * 100)
|
|
if pctInt > 100 {
|
|
pctInt = 100
|
|
}
|
|
if label != "" {
|
|
return fmt.Sprintf("%s %3d%% %s", bar, pctInt, label)
|
|
}
|
|
return fmt.Sprintf("%s %3d%%", bar, pctInt)
|
|
}
|
|
|
|
func (it *RepoItem) textColor() walk.Color {
|
|
r := it.result
|
|
if r.Pending {
|
|
return walk.RGB(120, 120, 120)
|
|
}
|
|
if r.Error != "" {
|
|
return walk.RGB(200, 50, 50)
|
|
}
|
|
if r.NeedsClone {
|
|
return walk.RGB(180, 120, 0)
|
|
}
|
|
if r.UpToDate {
|
|
return walk.RGB(0, 150, 0)
|
|
}
|
|
return walk.RGB(180, 120, 0)
|
|
}
|
|
|
|
type RepoModel struct {
|
|
walk.TableModelBase
|
|
mu sync.RWMutex
|
|
items []*RepoItem
|
|
}
|
|
|
|
func newRepoModel(cfgs []RepoConfig) *RepoModel {
|
|
m := &RepoModel{}
|
|
m.items = make([]*RepoItem, len(cfgs))
|
|
for i, c := range cfgs {
|
|
m.items[i] = &RepoItem{result: RepoResult{Name: c.Name, Pending: true}}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (m *RepoModel) RowCount() int {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return len(m.items)
|
|
}
|
|
|
|
func (m *RepoModel) Value(row, col int) interface{} {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
if row >= len(m.items) {
|
|
return ""
|
|
}
|
|
it := m.items[row]
|
|
switch col {
|
|
case 0:
|
|
return it.result.Name
|
|
case 1:
|
|
return it.statusText()
|
|
case 2:
|
|
return it.progressText
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m *RepoModel) StyleCell(style *walk.CellStyle) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
row := style.Row()
|
|
if row >= len(m.items) {
|
|
return
|
|
}
|
|
col := style.Col()
|
|
if col == 1 {
|
|
style.TextColor = m.items[row].textColor()
|
|
}
|
|
if col == 2 {
|
|
it := m.items[row]
|
|
if it.progress > 0 && it.progress < 1.0 {
|
|
style.TextColor = walk.RGB(0, 100, 180)
|
|
} else if it.progress >= 1.0 {
|
|
style.TextColor = walk.RGB(0, 150, 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *RepoModel) setResult(row int, res RepoResult) {
|
|
m.mu.Lock()
|
|
if row < len(m.items) {
|
|
m.items[row].result = res
|
|
m.items[row].progress = 0
|
|
m.items[row].progressText = ""
|
|
}
|
|
m.mu.Unlock()
|
|
m.PublishRowChanged(row)
|
|
}
|
|
|
|
func (m *RepoModel) setProgress(row int, pct float64, text string) {
|
|
m.mu.Lock()
|
|
if row < len(m.items) {
|
|
m.items[row].progress = pct
|
|
m.items[row].progressText = text
|
|
}
|
|
m.mu.Unlock()
|
|
m.PublishRowChanged(row)
|
|
}
|
|
|
|
func (m *RepoModel) reset(cfgs []RepoConfig) {
|
|
m.mu.Lock()
|
|
m.items = make([]*RepoItem, len(cfgs))
|
|
for i, c := range cfgs {
|
|
m.items[i] = &RepoItem{result: RepoResult{Name: c.Name, Pending: true}}
|
|
}
|
|
m.mu.Unlock()
|
|
m.PublishRowsReset()
|
|
}
|
|
|
|
func (m *RepoModel) getResult(row int) (RepoResult, bool) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
if row < 0 || row >= len(m.items) {
|
|
return RepoResult{}, false
|
|
}
|
|
return m.items[row].result, true
|
|
}
|
|
|
|
func (m *RepoModel) hasUpdates() bool {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
for _, it := range m.items {
|
|
r := it.result
|
|
if !r.Pending && r.Error == "" && !r.Offline && (r.NewCommits > 0 || r.LocalChanges > 0 || r.UntrackedFiles > 0 || r.NeedsClone) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ── Application ───────────────────────────────────────────────────────────────
|
|
|
|
type App struct {
|
|
mw *walk.MainWindow
|
|
statusLabel *walk.Label
|
|
tv *walk.TableView
|
|
model *RepoModel
|
|
logEdit *walk.TextEdit
|
|
btnRefresh *walk.PushButton
|
|
btnUpdateAll *walk.PushButton
|
|
btnAction *walk.PushButton
|
|
|
|
reposConfig []RepoConfig
|
|
suConfig SelfUpdateConfig
|
|
checking atomic.Bool
|
|
}
|
|
|
|
func runApp() error {
|
|
app := &App{}
|
|
var err error
|
|
app.reposConfig, app.suConfig, err = loadConfig()
|
|
if err != nil {
|
|
logWarn("Config: " + err.Error())
|
|
}
|
|
app.model = newRepoModel(app.reposConfig)
|
|
return app.buildAndRun()
|
|
}
|
|
|
|
func (a *App) buildAndRun() error {
|
|
if err := (MainWindow{
|
|
AssignTo: &a.mw,
|
|
Title: "Git Update Checker v" + VERSION,
|
|
MinSize: Size{Width: 750, Height: 400},
|
|
Size: Size{Width: 950, Height: 600},
|
|
Layout: VBox{Margins: Margins{Left: 10, Top: 10, Right: 10, Bottom: 10}},
|
|
Children: []Widget{
|
|
// En-tête
|
|
Composite{
|
|
Layout: HBox{MarginsZero: true},
|
|
Children: []Widget{
|
|
Label{
|
|
Text: "Git Update Checker v" + VERSION,
|
|
Font: Font{Bold: true, PointSize: 12},
|
|
},
|
|
HSpacer{},
|
|
Label{AssignTo: &a.statusLabel, Text: "Démarrage..."},
|
|
},
|
|
},
|
|
// Zone principale : table + log
|
|
VSplitter{
|
|
Children: []Widget{
|
|
TableView{
|
|
AssignTo: &a.tv,
|
|
AlternatingRowBG: true,
|
|
ColumnsOrderable: false,
|
|
Columns: []TableViewColumn{
|
|
{Title: "Dépôt", Width: 150},
|
|
{Title: "Statut", Width: 200},
|
|
{Title: "Progression", Width: 350},
|
|
},
|
|
Model: a.model,
|
|
OnCurrentIndexChanged: a.onSelectionChanged,
|
|
OnItemActivated: a.doAction,
|
|
},
|
|
Composite{
|
|
Layout: VBox{MarginsZero: true},
|
|
Children: []Widget{
|
|
Composite{
|
|
Layout: HBox{MarginsZero: true},
|
|
Children: []Widget{
|
|
Label{Text: "Journal", Font: Font{Bold: true}},
|
|
HSpacer{},
|
|
PushButton{
|
|
Text: "Effacer",
|
|
MaxSize: Size{Width: 65},
|
|
OnClicked: func() { a.logEdit.SetText("") },
|
|
},
|
|
},
|
|
},
|
|
TextEdit{
|
|
AssignTo: &a.logEdit,
|
|
ReadOnly: true,
|
|
VScroll: true,
|
|
Font: Font{Family: "Consolas", PointSize: 9},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
// Boutons
|
|
Composite{
|
|
Layout: HBox{MarginsZero: true},
|
|
Children: []Widget{
|
|
PushButton{
|
|
AssignTo: &a.btnRefresh,
|
|
Text: "Rafraîchir",
|
|
OnClicked: a.startCheck,
|
|
},
|
|
PushButton{
|
|
AssignTo: &a.btnUpdateAll,
|
|
Text: "Tout mettre à jour",
|
|
Enabled: false,
|
|
OnClicked: a.updateAll,
|
|
},
|
|
PushButton{
|
|
AssignTo: &a.btnAction,
|
|
Text: "Mettre à jour",
|
|
Enabled: false,
|
|
OnClicked: a.doAction,
|
|
},
|
|
HSpacer{},
|
|
PushButton{Text: "config.ini", OnClicked: a.openConfig},
|
|
PushButton{Text: "Logs", OnClicked: a.openLogs},
|
|
},
|
|
},
|
|
},
|
|
}.Create()); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Icône fenêtre
|
|
if icoPath := filepath.Join(exeDir(), "icon.ico"); fileExists(icoPath) {
|
|
if icon, err := walk.NewIconFromFile(icoPath); err == nil {
|
|
a.mw.SetIcon(icon)
|
|
}
|
|
}
|
|
|
|
// Lancer la vérification au démarrage
|
|
go func() {
|
|
time.Sleep(150 * time.Millisecond)
|
|
a.mw.Synchronize(a.checkSelfUpdateThenRepos)
|
|
}()
|
|
|
|
a.mw.Run()
|
|
return nil
|
|
}
|
|
|
|
// ── Vérifications ─────────────────────────────────────────────────────────────
|
|
|
|
func (a *App) checkSelfUpdateThenRepos() {
|
|
a.setStatus("Vérification auto-update...")
|
|
go func() {
|
|
needs, info, err := checkSelfUpdate(a.suConfig)
|
|
a.mw.Synchronize(func() {
|
|
if err != nil {
|
|
logWarn("Auto-update: " + err.Error())
|
|
}
|
|
if needs {
|
|
ans := walk.MsgBox(a.mw,
|
|
"Mise à jour disponible",
|
|
info+"\n\nTélécharger maintenant ?",
|
|
walk.MsgBoxYesNo|walk.MsgBoxIconQuestion,
|
|
)
|
|
if ans == walk.DlgCmdYes {
|
|
a.doSelfUpdate()
|
|
return
|
|
}
|
|
}
|
|
a.startCheck()
|
|
})
|
|
}()
|
|
}
|
|
|
|
func (a *App) startCheck() {
|
|
if !a.checking.CompareAndSwap(false, true) {
|
|
return
|
|
}
|
|
a.btnRefresh.SetEnabled(false)
|
|
a.btnUpdateAll.SetEnabled(false)
|
|
a.btnAction.SetEnabled(false)
|
|
a.model.reset(a.reposConfig)
|
|
a.setStatus(fmt.Sprintf("Vérification 0/%d...", len(a.reposConfig)))
|
|
logInfo("Vérification des dépôts...")
|
|
|
|
done := atomic.Int32{}
|
|
total := int32(len(a.reposConfig))
|
|
|
|
for i, cfg := range a.reposConfig {
|
|
i, cfg := i, cfg
|
|
go func() {
|
|
res := checkRepo(cfg)
|
|
a.mw.Synchronize(func() {
|
|
a.model.setResult(i, res)
|
|
a.appendLog(logLineForResult(res))
|
|
logInfo(fmt.Sprintf("[%s] %s", res.Name, logLineForResult(res)))
|
|
|
|
n := done.Add(1)
|
|
if int32(n) == total {
|
|
a.onCheckDone()
|
|
} else {
|
|
a.setStatus(fmt.Sprintf("Vérification %d/%d...", n, total))
|
|
}
|
|
})
|
|
}()
|
|
}
|
|
|
|
if total == 0 {
|
|
a.onCheckDone()
|
|
}
|
|
}
|
|
|
|
func (a *App) onCheckDone() {
|
|
a.checking.Store(false)
|
|
a.btnRefresh.SetEnabled(true)
|
|
a.btnUpdateAll.SetEnabled(a.model.hasUpdates())
|
|
a.setStatus(fmt.Sprintf("Dernière vérification : %s", time.Now().Format("15:04:05")))
|
|
}
|
|
|
|
func logLineForResult(r RepoResult) string {
|
|
if r.Error != "" {
|
|
return r.Error
|
|
}
|
|
if r.NeedsClone {
|
|
return "À cloner"
|
|
}
|
|
if r.UpToDate {
|
|
return "À jour"
|
|
}
|
|
msg := ""
|
|
if r.NewCommits > 0 {
|
|
msg += fmt.Sprintf("%d commit(s) disponible(s)", r.NewCommits)
|
|
}
|
|
if r.LocalChanges > 0 {
|
|
if msg != "" {
|
|
msg += ", "
|
|
}
|
|
msg += fmt.Sprintf("%d modif. locale(s)", r.LocalChanges)
|
|
}
|
|
if r.UntrackedFiles > 0 {
|
|
if msg != "" {
|
|
msg += ", "
|
|
}
|
|
msg += fmt.Sprintf("%d fichier(s) en trop", r.UntrackedFiles)
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// recheckOne re-vérifie un seul dépôt sans toucher aux autres.
|
|
func (a *App) recheckOne(idx int) {
|
|
if idx < 0 || idx >= len(a.reposConfig) {
|
|
return
|
|
}
|
|
cfg := a.reposConfig[idx]
|
|
a.model.setResult(idx, RepoResult{Name: cfg.Name, Pending: true})
|
|
go func() {
|
|
res := checkRepo(cfg)
|
|
a.mw.Synchronize(func() {
|
|
a.model.setResult(idx, res)
|
|
a.btnUpdateAll.SetEnabled(a.model.hasUpdates())
|
|
a.onSelectionChanged()
|
|
})
|
|
}()
|
|
}
|
|
|
|
// proposeClean affiche un popup listant les fichiers non suivis et propose de les supprimer.
|
|
func (a *App) proposeClean(idx int, res RepoResult) {
|
|
// Construire la liste des fichiers (max 30 affichés)
|
|
list := ""
|
|
for i, f := range res.UntrackedList {
|
|
if i >= 30 {
|
|
list += fmt.Sprintf("\n... et %d autre(s)", len(res.UntrackedList)-30)
|
|
break
|
|
}
|
|
list += "\n - " + f
|
|
}
|
|
msg := fmt.Sprintf("[%s] %d fichier(s) non suivi(s) détecté(s) :%s\n\nSupprimer ces fichiers ?",
|
|
res.Name, res.UntrackedFiles, list)
|
|
|
|
ans := walk.MsgBox(a.mw, "Fichiers en trop", msg, walk.MsgBoxYesNo|walk.MsgBoxIconQuestion)
|
|
if ans == walk.DlgCmdYes {
|
|
a.appendLog(fmt.Sprintf("[%s] Nettoyage de %d fichier(s)...", res.Name, res.UntrackedFiles))
|
|
go func() {
|
|
err := doClean(res)
|
|
a.mw.Synchronize(func() {
|
|
if err != nil {
|
|
a.appendLog(fmt.Sprintf("[%s] Erreur nettoyage: %v", res.Name, err))
|
|
logError(fmt.Sprintf("[%s] clean: %v", res.Name, err))
|
|
} else {
|
|
a.appendLog(fmt.Sprintf("[%s] %d fichier(s) supprimé(s)", res.Name, res.UntrackedFiles))
|
|
logInfo(fmt.Sprintf("[%s] %d fichier(s) supprimé(s)", res.Name, res.UntrackedFiles))
|
|
}
|
|
a.recheckOne(idx)
|
|
})
|
|
}()
|
|
} else {
|
|
a.recheckOne(idx)
|
|
}
|
|
}
|
|
|
|
// ── Progression ───────────────────────────────────────────────────────────────
|
|
|
|
// makeProgressCB crée un callback de progression pour la ligne row du tableau.
|
|
// Le callback est appelé depuis un goroutine git et synchronise l'UI via mw.Synchronize.
|
|
func (a *App) makeProgressCB(row int) ProgressCallback {
|
|
// Limiter les mises à jour UI (max ~10/s) pour ne pas surcharger
|
|
var lastUpdate time.Time
|
|
return func(info ProgressInfo) {
|
|
now := time.Now()
|
|
if now.Sub(lastUpdate) < 100*time.Millisecond && info.Percent < 1.0 {
|
|
return
|
|
}
|
|
lastUpdate = now
|
|
|
|
label := info.Phase
|
|
if info.Speed != "" {
|
|
label += " " + info.Speed
|
|
}
|
|
text := progressBarText(info.Percent, 20, label)
|
|
a.mw.Synchronize(func() {
|
|
a.model.setProgress(row, info.Percent, text)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Actions dépôt ─────────────────────────────────────────────────────────────
|
|
|
|
func (a *App) onSelectionChanged() {
|
|
idx := a.tv.CurrentIndex()
|
|
res, ok := a.model.getResult(idx)
|
|
if !ok || res.Pending || res.Offline || res.Error != "" {
|
|
a.btnAction.SetEnabled(false)
|
|
return
|
|
}
|
|
if res.NeedsClone {
|
|
a.btnAction.SetText("Cloner")
|
|
a.btnAction.SetEnabled(true)
|
|
} else if res.NewCommits > 0 || res.LocalChanges > 0 || res.UntrackedFiles > 0 {
|
|
a.btnAction.SetText("Mettre à jour")
|
|
a.btnAction.SetEnabled(true)
|
|
} else {
|
|
a.btnAction.SetEnabled(false)
|
|
}
|
|
}
|
|
|
|
func (a *App) doAction() {
|
|
idx := a.tv.CurrentIndex()
|
|
res, ok := a.model.getResult(idx)
|
|
if !ok {
|
|
return
|
|
}
|
|
cfg := a.reposConfig[idx]
|
|
|
|
// Si uniquement des fichiers en trop, proposer directement le nettoyage
|
|
if res.UntrackedFiles > 0 && res.NewCommits == 0 && res.LocalChanges == 0 && !res.NeedsClone {
|
|
a.proposeClean(idx, res)
|
|
return
|
|
}
|
|
|
|
a.btnAction.SetEnabled(false)
|
|
a.appendLog(fmt.Sprintf("[%s] Mise à jour en cours...", res.Name))
|
|
|
|
cb := a.makeProgressCB(idx)
|
|
go func() {
|
|
var err error
|
|
if res.NeedsClone {
|
|
err = doCloneWithProgress(cfg, cb)
|
|
} else {
|
|
if res.LocalChanges > 0 {
|
|
err = doCheckout(res)
|
|
}
|
|
if err == nil && res.NewCommits > 0 {
|
|
err = doPullWithProgress(res, cb)
|
|
}
|
|
}
|
|
a.mw.Synchronize(func() {
|
|
if err != nil {
|
|
a.model.setProgress(idx, 0, "Erreur")
|
|
a.appendLog(fmt.Sprintf("[%s] Erreur: %v", res.Name, err))
|
|
logError(fmt.Sprintf("[%s] %v", res.Name, err))
|
|
} else {
|
|
a.model.setProgress(idx, 1.0, progressBarText(1.0, 20, "Terminé"))
|
|
a.appendLog(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
|
logInfo(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
|
}
|
|
// Si des fichiers en trop après la mise à jour, proposer le nettoyage
|
|
if res.UntrackedFiles > 0 {
|
|
a.proposeClean(idx, res)
|
|
return
|
|
}
|
|
// Re-vérifier uniquement ce dépôt, pas tous
|
|
a.recheckOne(idx)
|
|
})
|
|
}()
|
|
}
|
|
|
|
func (a *App) updateAll() {
|
|
a.btnUpdateAll.SetEnabled(false)
|
|
a.btnRefresh.SetEnabled(false)
|
|
pending := atomic.Int32{}
|
|
|
|
for i, cfg := range a.reposConfig {
|
|
res, ok := a.model.getResult(i)
|
|
if !ok || res.Pending || res.UpToDate || res.Offline || res.Error != "" {
|
|
continue
|
|
}
|
|
pending.Add(1)
|
|
i, cfg, res := i, cfg, res
|
|
cb := a.makeProgressCB(i)
|
|
go func() {
|
|
var err error
|
|
if res.NeedsClone {
|
|
err = doCloneWithProgress(cfg, cb)
|
|
} else {
|
|
if res.LocalChanges > 0 {
|
|
err = doCheckout(res)
|
|
}
|
|
if err == nil && res.NewCommits > 0 {
|
|
err = doPullWithProgress(res, cb)
|
|
}
|
|
if err == nil && res.UntrackedFiles > 0 {
|
|
err = doClean(res)
|
|
}
|
|
}
|
|
a.mw.Synchronize(func() {
|
|
if err != nil {
|
|
a.model.setProgress(i, 0, "Erreur")
|
|
logError(fmt.Sprintf("[%s] %v", res.Name, err))
|
|
a.appendLog(fmt.Sprintf("[%s] Erreur: %v", res.Name, err))
|
|
} else {
|
|
a.model.setProgress(i, 1.0, progressBarText(1.0, 20, "Terminé"))
|
|
logInfo(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
|
a.appendLog(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
|
}
|
|
if pending.Add(-1) == 0 {
|
|
a.startCheck()
|
|
}
|
|
})
|
|
}()
|
|
}
|
|
if pending.Load() == 0 {
|
|
a.startCheck()
|
|
}
|
|
}
|
|
|
|
// ── Auto-update programme ─────────────────────────────────────────────────────
|
|
|
|
func (a *App) doSelfUpdate() {
|
|
a.setStatus("Téléchargement de la mise à jour...")
|
|
go func() {
|
|
err := doSelfUpdate(a.suConfig)
|
|
a.mw.Synchronize(func() {
|
|
if err != nil {
|
|
walk.MsgBox(a.mw, "Erreur", "Mise à jour échouée :\n"+err.Error(), walk.MsgBoxIconError)
|
|
logError("Auto-update: " + err.Error())
|
|
a.startCheck()
|
|
return
|
|
}
|
|
logInfo("Auto-update: mise à jour appliquée, redémarrage...")
|
|
walk.MsgBox(a.mw, "Mise à jour", "Mise à jour installée.\nLe programme va redémarrer.", walk.MsgBoxIconInformation)
|
|
exePath, _ := os.Executable()
|
|
relaunchAfterUpdate(exePath)
|
|
a.mw.Close()
|
|
})
|
|
}()
|
|
}
|
|
|
|
// ── Utilitaires GUI ───────────────────────────────────────────────────────────
|
|
|
|
func (a *App) setStatus(text string) {
|
|
a.statusLabel.SetText(text)
|
|
}
|
|
|
|
func (a *App) appendLog(line string) {
|
|
ts := time.Now().Format("15:04:05")
|
|
current := a.logEdit.Text()
|
|
if current != "" {
|
|
current += "\r\n"
|
|
}
|
|
a.logEdit.SetText(current + "[" + ts + "] " + line)
|
|
// Scroller en bas
|
|
a.logEdit.SendMessage(0x0115 /*WM_VSCROLL*/, 7 /*SB_BOTTOM*/, 0)
|
|
}
|
|
|
|
func (a *App) openConfig() {
|
|
p := filepath.Join(exeDir(), "config.ini")
|
|
exec.Command("notepad", p).Start()
|
|
}
|
|
|
|
func (a *App) openLogs() {
|
|
p := filepath.Join(exeDir(), "log")
|
|
exec.Command("explorer", p).Start()
|
|
}
|