Commit bbfcabd7 authored by Roman Alifanov's avatar Roman Alifanov

Use real PlayListUpdates D-Bus method, filter progress from log

- Replace CheckPlayUpdates stub with real D-Bus call to PlayListUpdates - Add PlayUpdateApp model and display play apps count in category row - Pass EventType through store pipeline to UI - Only show NOTIFICATION events in log view, not PROGRESS
parent f58b96a0
package eepm
import (
"SystemUpdater/model"
"context"
"encoding/json"
"fmt"
"log"
"github.com/diamondburned/gotk4/pkg/gio/v2"
"github.com/diamondburned/gotk4/pkg/glib/v2"
)
// CheckPlayUpdates checks if play apps updates are available
func (c *Client) CheckPlayUpdates(ctx context.Context) (*model.PlayUpdateInfo, error) {
resultCh := make(chan *glib.Variant, 1)
errorCh := make(chan error, 1)
c.conn.QueryProxy.Call(
ctx,
"PlayListUpdates",
nil,
gio.DBusCallFlagsNone,
300000, // 5 minutes timeout
func(res gio.AsyncResulter) {
reply, err := c.conn.QueryProxy.CallFinish(res)
if err != nil {
errorCh <- err
return
}
resultCh <- reply
},
)
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-errorCh:
log.Printf("CheckPlayUpdates D-Bus error: %v", err)
return nil, err
case reply := <-resultCh:
var parsed PlayListUpdatesRootResponse
if err := json.Unmarshal([]byte(reply.ChildValue(0).String()), &parsed); err != nil {
log.Printf("Failed to parse PlayListUpdates response: %v", err)
return nil, err
}
info := &model.PlayUpdateInfo{
Available: parsed.Data.TotalCount > 0,
Message: parsed.Data.Message,
Apps: parsed.Data.Apps,
TotalCount: parsed.Data.TotalCount,
}
return info, nil
}
}
// RunPlayUpdate executes play apps update
// NOTE: Requires PlayUpdate method in eepm-dbus Manage interface
func (c *Client) RunPlayUpdate(ctx context.Context, transaction string) error {
errorCh := make(chan error, 1)
args := glib.NewVariantTuple([]*glib.Variant{
glib.NewVariantString("all"),
glib.NewVariantString(transaction),
})
// NOTE: This assumes PlayUpdate method exists in eepm-dbus
// If not implemented yet, this will fail with "unknown method"
c.conn.ManageProxy.Call(
ctx,
"PlayUpdate",
args,
gio.DBusCallFlagsNone,
2147483647, // Max int32 timeout
func(res gio.AsyncResulter) {
_, err := c.conn.ManageProxy.CallFinish(res)
if err != nil {
errorCh <- err
return
}
errorCh <- nil
},
)
select {
case <-ctx.Done():
return ctx.Err()
case err := <-errorCh:
if err != nil {
log.Printf("RunPlayUpdate error: %v", err)
return fmt.Errorf("play update failed: %w", err)
}
return nil
}
}
package eepm
import "SystemUpdater/model"
// API Response wrapper matching eepm-dbus-rs ApiResponse<T>
type ApiResponse[T any] struct {
Data T `json:"data"`
Error bool `json:"error"`
Message *string `json:"message"`
}
// InfoResponse for package info
type InfoResponse struct {
Message string `json:"message"`
Package model.Package `json:"package"`
}
// CheckResponse for check-upgrade, check-install, check-remove
type CheckResponse struct {
Message string `json:"message"`
Info model.PackageChanges `json:"info"`
}
// UpgradeResponse for upgrade operations
type UpgradeResponse struct {
Message string `json:"message"`
Info model.PackageChanges `json:"info"`
}
// KernelUpdateResponse for kernel operations
type KernelUpdateResponse struct {
Message string `json:"message"`
Info model.KernelUpdateInfo `json:"info"`
}
// PlayListUpdatesResponse for play list-updates
type PlayListUpdatesResponse struct {
Message string `json:"message"`
Apps []model.PlayUpdateApp `json:"apps"`
TotalCount uint32 `json:"total_count"`
}
// Root response types (wrapped in ApiResponse)
type InfoRootResponse = ApiResponse[InfoResponse]
type CheckUpgradeRootResponse = ApiResponse[CheckResponse]
type UpgradeRootResponse = ApiResponse[UpgradeResponse]
type KernelUpdateRootResponse = ApiResponse[KernelUpdateResponse]
type PlayListUpdatesRootResponse = ApiResponse[PlayListUpdatesResponse]
package model
// UpdateCategory represents a category of updates (System, Kernel, Play)
type UpdateCategory struct {
Name string
Enabled bool // User can toggle this category
Available bool // Has updates available
Packages []Package
DownloadSize uint64
InstallSize int64
LastChecked string // RFC3339 timestamp
}
// KernelUpdateInfo represents kernel update information
type KernelUpdateInfo struct {
RunningKernel string
AvailableKernel string
KernelType string
KernelVersion string
TotalModules uint32
AvailableModules []string
MissingModules []string
AutoSelectedModules []string
Attention []string
Packages PackageChanges
UpToDate bool
}
// PlayUpdateApp represents a play app with an available update
type PlayUpdateApp struct {
Name string `json:"name"`
InstalledVersion string `json:"installed_version"`
AvailableVersion string `json:"available_version"`
}
// PlayUpdateInfo represents play apps update information
type PlayUpdateInfo struct {
Available bool
Message string
Apps []PlayUpdateApp
TotalCount uint32
}
// ActiveUpdate represents an ongoing update operation
type ActiveUpdate struct {
Categories []string // Which categories are being updated
Transaction string
Progress float64 // 0.0 - 100.0
CurrentOp string
EventType string
Log []string
StartedAt string // RFC3339 timestamp
}
package service
import (
"SystemUpdater/backend/eepm"
"SystemUpdater/model"
"SystemUpdater/store"
"context"
"fmt"
"log"
"sync"
"time"
)
// UpdateService orchestrates update operations
type UpdateService struct {
store *store.Store
eepmClient *eepm.Client
history *HistoryService
}
// NewUpdateService creates a new update service
func NewUpdateService(st *store.Store, client *eepm.Client, history *HistoryService) *UpdateService {
return &UpdateService{
store: st,
eepmClient: client,
history: history,
}
}
// CheckAllUpdates checks for updates in all categories (parallel)
func (us *UpdateService) CheckAllUpdates(ctx context.Context) error {
log.Println("Checking for updates in all categories...")
us.store.Dispatch(&store.SetPhaseAction{Phase: store.PhaseLoading})
var wg sync.WaitGroup
var mu sync.Mutex
errors := []error{}
// Check System updates
wg.Add(1)
go func() {
defer wg.Done()
changes, err := us.eepmClient.CheckSystemUpdates(ctx)
if err != nil {
mu.Lock()
errors = append(errors, fmt.Errorf("system updates: %w", err))
mu.Unlock()
return
}
us.store.Dispatch(&store.LoadSystemUpdatesAction{Changes: *changes})
}()
// Check Kernel updates
wg.Add(1)
go func() {
defer wg.Done()
info, err := us.eepmClient.CheckKernelUpdates(ctx)
if err != nil {
mu.Lock()
errors = append(errors, fmt.Errorf("kernel updates: %w", err))
mu.Unlock()
return
}
us.store.Dispatch(&store.LoadKernelUpdatesAction{Info: *info})
}()
// Check Play updates
wg.Add(1)
go func() {
defer wg.Done()
info, err := us.eepmClient.CheckPlayUpdates(ctx)
if err != nil {
mu.Lock()
errors = append(errors, fmt.Errorf("play updates: %w", err))
mu.Unlock()
return
}
us.store.Dispatch(&store.LoadPlayUpdatesAction{Info: *info})
}()
wg.Wait()
if len(errors) > 0 {
errMsg := "Failed to check some updates"
for _, err := range errors {
log.Printf("Update check error: %v", err)
errMsg += "; " + err.Error()
}
us.store.Dispatch(&store.SetErrorAction{Error: errMsg})
return fmt.Errorf("%s", errMsg)
}
us.store.Dispatch(&store.SetPhaseAction{Phase: store.PhaseReady})
log.Println("Update check completed")
return nil
}
// RunUpdates executes updates for selected categories
func (us *UpdateService) RunUpdates(ctx context.Context) error {
state := us.store.GetState()
// Get enabled categories
categories := store.GetEnabledCategories(&state)
if len(categories) == 0 {
return fmt.Errorf("no categories enabled for update")
}
log.Printf("Starting update for categories: %v", categories)
transaction := fmt.Sprintf("update-%d", time.Now().UnixNano())
us.store.Dispatch(&store.StartUpdateAction{
Categories: categories,
Transaction: transaction,
})
progressCh := make(chan model.EventData, 100)
us.eepmClient.SubscribeProgress(transaction, progressCh)
defer us.eepmClient.UnsubscribeProgress(transaction)
progressDone := make(chan struct{})
go us.handleProgress(ctx, progressCh, progressDone)
startTime := time.Now()
packagesUpdated := 0
var updateErr error
// Execute updates sequentially
for _, category := range categories {
if err := us.runCategoryUpdate(ctx, category, transaction); err != nil {
updateErr = err
log.Printf("Update failed for %s: %v", category, err)
break
}
// Count packages
switch category {
case "System":
packagesUpdated += len(state.SystemUpdates.Packages)
case "Kernel":
packagesUpdated += len(state.KernelUpdates.Packages)
}
}
// Wait for progress handler to finish
close(progressDone)
// Finish update
success := updateErr == nil
us.store.Dispatch(&store.FinishUpdateAction{
Success: success,
Error: fmt.Sprintf("%v", updateErr),
})
// Record in history
if us.history != nil {
entry := model.HistoryEntry{
Timestamp: time.Now().Format(time.RFC3339),
Categories: categories,
Success: success,
DurationSeconds: int(time.Since(startTime).Seconds()),
PackagesUpdated: packagesUpdated,
}
if state.ActiveUpdate != nil {
entry.Log = state.ActiveUpdate.Log
}
if !success {
entry.ErrorMessage = fmt.Sprintf("%v", updateErr)
}
us.history.RecordUpdate(entry)
}
if updateErr != nil {
return updateErr
}
log.Println("Update completed successfully")
return nil
}
// runCategoryUpdate runs update for a specific category
func (us *UpdateService) runCategoryUpdate(ctx context.Context, category string, transaction string) error {
log.Printf("Updating %s...", category)
switch category {
case "System":
return us.eepmClient.RunSystemUpgrade(ctx, transaction)
case "Kernel":
return us.eepmClient.RunKernelUpgrade(ctx, transaction)
case "Play":
return us.eepmClient.RunPlayUpdate(ctx, transaction)
default:
return fmt.Errorf("unknown category: %s", category)
}
}
// handleProgress processes progress events
func (us *UpdateService) handleProgress(ctx context.Context, ch chan model.EventData, done chan struct{}) {
for {
select {
case <-ctx.Done():
return
case <-done:
return
case event := <-ch:
us.store.Dispatch(&store.UpdateProgressAction{
Transaction: event.Transaction,
Progress: event.Progress,
Message: event.Message,
EventType: event.EventType,
})
}
}
}
package store
import (
"SystemUpdater/model"
"time"
)
// Action represents a state modification action
type Action interface {
Apply(state *State) error
}
// SetPhaseAction sets the application phase
type SetPhaseAction struct {
Phase AppPhase
}
func (a *SetPhaseAction) Apply(state *State) error {
state.Phase = a.Phase
return nil
}
// LoadSystemUpdatesAction loads system updates into state
type LoadSystemUpdatesAction struct {
Changes model.PackageChanges
}
func (a *LoadSystemUpdatesAction) Apply(state *State) error {
state.SystemUpdates.Available = a.Changes.HasChanges()
state.SystemUpdates.Packages = a.Changes.Upgrade
state.SystemUpdates.Packages = append(state.SystemUpdates.Packages, a.Changes.Install...)
state.SystemUpdates.DownloadSize = a.Changes.DownloadSize
state.SystemUpdates.InstallSize = a.Changes.InstallSize
state.SystemUpdates.LastChecked = time.Now().Format(time.RFC3339)
return nil
}
// LoadKernelUpdatesAction loads kernel updates into state
type LoadKernelUpdatesAction struct {
Info model.KernelUpdateInfo
}
func (a *LoadKernelUpdatesAction) Apply(state *State) error {
state.KernelUpdates.Available = !a.Info.UpToDate && a.Info.Packages.HasChanges()
state.KernelUpdates.Packages = a.Info.Packages.Upgrade
state.KernelUpdates.Packages = append(state.KernelUpdates.Packages, a.Info.Packages.Install...)
state.KernelUpdates.DownloadSize = a.Info.Packages.DownloadSize
state.KernelUpdates.InstallSize = a.Info.Packages.InstallSize
state.KernelUpdates.LastChecked = time.Now().Format(time.RFC3339)
return nil
}
// LoadPlayUpdatesAction loads play apps updates into state
type LoadPlayUpdatesAction struct {
Info model.PlayUpdateInfo
}
func (a *LoadPlayUpdatesAction) Apply(state *State) error {
state.PlayUpdates.Available = a.Info.Available
state.PlayUpdates.LastChecked = time.Now().Format(time.RFC3339)
// Convert play apps to packages for display
var packages []model.Package
for _, app := range a.Info.Apps {
summary := app.InstalledVersion + " → " + app.AvailableVersion
packages = append(packages, model.Package{
Name: app.Name,
Version: app.AvailableVersion,
Summary: &summary,
})
}
state.PlayUpdates.Packages = packages
return nil
}
// ToggleCategoryAction toggles a category's enabled status
type ToggleCategoryAction struct {
Category string // "System", "Kernel", "Play"
Enabled bool
}
func (a *ToggleCategoryAction) Apply(state *State) error {
switch a.Category {
case "System":
state.SystemUpdates.Enabled = a.Enabled
case "Kernel":
state.KernelUpdates.Enabled = a.Enabled
case "Play":
state.PlayUpdates.Enabled = a.Enabled
}
return nil
}
// StartUpdateAction starts an update operation
type StartUpdateAction struct {
Categories []string
Transaction string
}
func (a *StartUpdateAction) Apply(state *State) error {
state.Phase = PhaseUpdating
state.ActiveUpdate = &model.ActiveUpdate{
Categories: a.Categories,
Transaction: a.Transaction,
Progress: 0.0,
CurrentOp: "Preparing...",
Log: []string{},
StartedAt: time.Now().Format(time.RFC3339),
}
return nil
}
// UpdateProgressAction updates progress of active update
type UpdateProgressAction struct {
Transaction string
Progress float64
Message string
EventType string
}
func (a *UpdateProgressAction) Apply(state *State) error {
if state.ActiveUpdate != nil && state.ActiveUpdate.Transaction == a.Transaction {
state.ActiveUpdate.Progress = a.Progress
state.ActiveUpdate.CurrentOp = a.Message
state.ActiveUpdate.EventType = a.EventType
if a.EventType != "PROGRESS" && a.Message != "" {
state.ActiveUpdate.Log = append(state.ActiveUpdate.Log, a.Message)
}
}
return nil
}
// FinishUpdateAction finishes an update operation
type FinishUpdateAction struct {
Success bool
Error string
}
func (a *FinishUpdateAction) Apply(state *State) error {
if a.Success {
state.Phase = PhaseReady
} else {
state.Phase = PhaseError
state.LastError = a.Error
}
state.ActiveUpdate = nil
return nil
}
// AddHistoryAction adds a history entry
type AddHistoryAction struct {
Entry model.HistoryEntry
}
func (a *AddHistoryAction) Apply(state *State) error {
state.History = append(state.History, a.Entry)
// Keep only last 100 entries
if len(state.History) > 100 {
state.History = state.History[len(state.History)-100:]
}
return nil
}
// LoadHistoryAction loads history from storage
type LoadHistoryAction struct {
Entries []model.HistoryEntry
}
func (a *LoadHistoryAction) Apply(state *State) error {
state.History = a.Entries
return nil
}
// UpdateSettingsAction updates application settings
type UpdateSettingsAction struct {
Settings AppSettings
}
func (a *UpdateSettingsAction) Apply(state *State) error {
state.Settings = a.Settings
return nil
}
// SetErrorAction sets an error state
type SetErrorAction struct {
Error string
}
func (a *SetErrorAction) Apply(state *State) error {
state.Phase = PhaseError
state.LastError = a.Error
return nil
}
package pages
import (
"SystemUpdater/model"
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
gtksbuilder "SystemUpdater/lib/gtks/builder"
)
type UpdateProcessPage struct {
*adw.NavigationPage
stack *gtk.Stack
statusPage *adw.StatusPage
progressBar *gtk.ProgressBar
logView *gtk.TextView
logBuffer *gtk.TextBuffer
rebootBtn *gtk.Button
}
func NewUpdateProcessPage(onReboot func()) *UpdateProcessPage {
b := gtksbuilder.NewBuilder("process-page.ui")
page := &UpdateProcessPage{
NavigationPage: gtksbuilder.GetObject[*adw.NavigationPage](b, "process_page"),
stack: gtksbuilder.GetObject[*gtk.Stack](b, "process_stack"),
statusPage: gtksbuilder.GetObject[*adw.StatusPage](b, "status_page"),
progressBar: gtksbuilder.GetObject[*gtk.ProgressBar](b, "progress_bar"),
logView: gtksbuilder.GetObject[*gtk.TextView](b, "log_view"),
rebootBtn: gtksbuilder.GetObject[*gtk.Button](b, "restart_button"),
}
page.logBuffer = page.logView.Buffer()
page.progressBar.SetShowText(true)
page.rebootBtn.ConnectClicked(func() {
if onReboot != nil {
onReboot()
}
})
return page
}
func (p *UpdateProcessPage) UpdateProgress(event model.EventData) {
if event.Progress > 0 {
p.progressBar.SetFraction(event.Progress / 100.0)
}
if event.Message != "" && event.EventType != "PROGRESS" {
endIter := p.logBuffer.EndIter()
p.logBuffer.Insert(endIter, event.Message+"\n")
mark := p.logBuffer.CreateMark("end", endIter, false)
p.logView.ScrollToMark(mark, 0.0, false, 0.0, 0.0)
}
}
func (p *UpdateProcessPage) ShowComplete(success bool) {
if success {
p.statusPage.SetTitle("Updating complete!")
p.statusPage.SetIconName("face-smile-big-symbolic")
p.statusPage.SetDescription("Click on button below to restart device")
} else {
p.statusPage.SetTitle("Update Failed")
p.statusPage.SetIconName("dialog-error-symbolic")
p.statusPage.SetDescription("The update encountered an error. Please check the logs.")
p.rebootBtn.SetVisible(false)
}
p.stack.SetVisibleChildName("finish")
}
func (p *UpdateProcessPage) Reset() {
start := p.logBuffer.StartIter()
end := p.logBuffer.EndIter()
p.logBuffer.Delete(start, end)
p.progressBar.SetFraction(0.0)
p.stack.SetVisibleChildName("default")
p.rebootBtn.SetVisible(true)
}
package ui
import (
"SystemUpdater/model"
"SystemUpdater/service"
"SystemUpdater/store"
"SystemUpdater/ui/pages"
"context"
"fmt"
"log"
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
"github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
)
// Window is the main application window
type Window struct {
Window *adw.ApplicationWindow
st *store.Store
updateSvc *service.UpdateService
historySvc *service.HistoryService
// State changes channel
stateChanges chan store.StateChange
ctx context.Context
cancel context.CancelFunc
// UI components
navView *adw.NavigationView
stack *gtk.Stack
headerBar *adw.HeaderBar
historyButton *gtk.Button
// Pages
loadingPage *pages.LoadingPage
noUpdatesPage *pages.NoUpdatesPage
updatesPage *pages.UpdatesListPage
processPage *pages.UpdateProcessPage
historyPage *pages.HistoryPage
}
// NewWindow creates a new main window
func NewWindow(app *adw.Application, st *store.Store, updateSvc *service.UpdateService, historySvc *service.HistoryService) *Window {
ctx, cancel := context.WithCancel(context.Background())
appWindow := adw.NewApplicationWindow(&app.Application)
w := &Window{
Window: appWindow,
st: st,
updateSvc: updateSvc,
historySvc: historySvc,
stateChanges: make(chan store.StateChange, 100),
ctx: ctx,
cancel: cancel,
}
w.Window.SetDefaultSize(360, 500)
w.Window.SetTitle("System Updater")
w.buildUI()
w.connectSignals()
st.Subscribe(w.stateChanges)
go w.handleStateChanges()
return w
}
func (w *Window) buildUI() {
w.navView = adw.NewNavigationView()
toolbarView := adw.NewToolbarView()
toolbarView.SetTopBarStyle(adw.ToolbarRaised)
w.headerBar = adw.NewHeaderBar()
w.historyButton = gtk.NewButtonFromIconName("document-open-recent-symbolic")
w.historyButton.SetTooltipText("Update History")
w.headerBar.PackEnd(w.historyButton)
aboutButton := gtk.NewButtonFromIconName("help-about-symbolic")
aboutButton.SetTooltipText("About")
w.headerBar.PackEnd(aboutButton)
toolbarView.AddTopBar(w.headerBar)
w.stack = gtk.NewStack()
w.stack.SetTransitionType(gtk.StackTransitionTypeCrossfade)
w.loadingPage = pages.NewLoadingPage()
w.noUpdatesPage = pages.NewNoUpdatesPage()
w.updatesPage = pages.NewUpdatesListPage(w.st, w.onUpdateClicked, w.onNavigateToCategory)
w.processPage = pages.NewUpdateProcessPage(w.onRebootClicked)
w.historyPage = pages.NewHistoryPage()
w.stack.AddNamed(w.loadingPage, "loading")
w.stack.AddNamed(w.noUpdatesPage, "noupdates")
w.stack.AddNamed(w.updatesPage, "main")
toolbarView.SetContent(w.stack)
mainPage := adw.NewNavigationPage(toolbarView, "System Updater")
w.navView.Add(mainPage)
w.Window.SetContent(w.navView)
w.stack.SetVisibleChildName("loading")
}
func (w *Window) connectSignals() {
w.historyButton.ConnectClicked(func() {
w.showHistory()
})
// Window close
w.Window.ConnectCloseRequest(func() bool {
w.Cleanup()
return false
})
}
// handleStateChanges handles state changes from store
func (w *Window) handleStateChanges() {
for {
select {
case <-w.ctx.Done():
return
case change := <-w.stateChanges:
// Update UI on main thread
glib.IdleAdd(func() {
w.onStateChange(change)
})
}
}
}
// onStateChange handles a state change
func (w *Window) onStateChange(change store.StateChange) {
state := w.st.GetState()
switch change.Type {
case store.ChangePhase:
w.updatePhase(state.Phase)
case store.ChangeSystemUpdates, store.ChangeKernelUpdates, store.ChangePlayUpdates:
// Only update the page content, don't change visibility yet
// Visibility will be changed when Phase becomes Ready
if state.Phase == store.PhaseReady {
w.updatesPage.Update()
}
case store.ChangeCategoryToggle:
w.updatesPage.Update()
case store.ChangeUpdateStart:
w.processPage.Reset()
w.navView.Push(w.processPage.NavigationPage)
case store.ChangeUpdateProgress:
if state.ActiveUpdate != nil {
event := model.EventData{
Progress: state.ActiveUpdate.Progress,
Message: state.ActiveUpdate.CurrentOp,
EventType: state.ActiveUpdate.EventType,
}
w.processPage.UpdateProgress(event)
}
case store.ChangeUpdateFinish:
action, ok := change.Data.(*store.FinishUpdateAction)
if ok {
w.processPage.ShowComplete(action.Success)
}
case store.ChangeHistory:
// Refresh history page if visible
w.historyPage.Update(state.History)
case store.ChangeError:
log.Printf("Error: %s", state.LastError)
}
}
// updatePhase updates UI based on phase
func (w *Window) updatePhase(phase store.AppPhase) {
switch phase {
case store.PhaseLoading:
w.stack.SetVisibleChildName("loading")
case store.PhaseReady:
state := w.st.GetState()
if store.HasAnyUpdates(&state) {
w.stack.SetVisibleChildName("main")
w.updatesPage.Update()
} else {
w.stack.SetVisibleChildName("noupdates")
}
}
}
// onUpdateClicked handles update button click
func (w *Window) onUpdateClicked() {
w.showUpdateDialog()
}
// showUpdateDialog shows dialog to select categories to update
func (w *Window) showUpdateDialog() {
state := w.st.GetState()
dialog := adw.NewMessageDialog(
nil,
"Select Components to Update",
"Choose which components you want to update:",
)
box := gtk.NewBox(gtk.OrientationVertical, 12)
box.SetMarginTop(12)
box.SetMarginBottom(12)
box.SetMarginStart(12)
box.SetMarginEnd(12)
var systemCheck, kernelCheck, playCheck *gtk.CheckButton
if state.SystemUpdates.Available {
systemCheck = gtk.NewCheckButtonWithLabel(
fmt.Sprintf("System (%d packages)", len(state.SystemUpdates.Packages)),
)
systemCheck.SetActive(true)
box.Append(systemCheck)
}
if state.KernelUpdates.Available {
kernelCheck = gtk.NewCheckButtonWithLabel(
fmt.Sprintf("Kernel (%d modules)", len(state.KernelUpdates.Packages)),
)
kernelCheck.SetActive(true)
box.Append(kernelCheck)
}
if state.PlayUpdates.Available {
playCheck = gtk.NewCheckButtonWithLabel("Play apps")
playCheck.SetActive(true)
box.Append(playCheck)
}
dialog.SetExtraChild(box)
dialog.AddResponse("cancel", "Cancel")
dialog.AddResponse("update", "Update")
dialog.SetResponseAppearance("update", adw.ResponseSuggested)
dialog.SetDefaultResponse("update")
dialog.ConnectResponse(func(response string) {
if response == "update" {
categories := []string{}
if systemCheck != nil && systemCheck.Active() {
categories = append(categories, "System")
}
if kernelCheck != nil && kernelCheck.Active() {
categories = append(categories, "Kernel")
}
if playCheck != nil && playCheck.Active() {
categories = append(categories, "Play")
}
if len(categories) == 0 {
w.showError("No Selection", "Please select at least one component to update")
return
}
w.runUpdate(categories)
}
})
dialog.Present()
}
func (w *Window) runUpdate(categories []string) {
log.Printf("Starting update for: %v", categories)
for _, cat := range categories {
w.st.Dispatch(&store.ToggleCategoryAction{
Category: cat,
Enabled: true,
})
}
allCategories := []string{"System", "Kernel", "Play"}
for _, cat := range allCategories {
found := false
for _, selected := range categories {
if cat == selected {
found = true
break
}
}
if !found {
w.st.Dispatch(&store.ToggleCategoryAction{
Category: cat,
Enabled: false,
})
}
}
// Run update in background
go func() {
if err := w.updateSvc.RunUpdates(w.ctx); err != nil {
log.Printf("Update failed: %v", err)
glib.IdleAdd(func() {
w.showError("Update Failed", err.Error())
})
}
}()
}
// onRebootClicked handles reboot button click
func (w *Window) onRebootClicked() {
dialog := adw.NewMessageDialog(
nil,
"Reboot System?",
"The system needs to be rebooted to complete the update.",
)
dialog.AddResponse("cancel", "Cancel")
dialog.AddResponse("reboot", "Reboot")
dialog.SetResponseAppearance("reboot", adw.ResponseDestructive)
dialog.SetDefaultResponse("cancel")
dialog.ConnectResponse(func(response string) {
if response == "reboot" {
// TODO: Implement reboot via systemd or similar
log.Println("Reboot requested")
}
})
dialog.Present()
}
// showHistory shows the history page
func (w *Window) showHistory() {
state := w.st.GetState()
w.historyPage.Update(state.History)
w.navView.Push(w.historyPage.NavigationPage)
}
// onNavigateToCategory navigates to package list for a category
func (w *Window) onNavigateToCategory(category string) {
state := w.st.GetState()
var packages []model.Package
switch category {
case "System":
packages = state.SystemUpdates.Packages
case "Kernel":
packages = state.KernelUpdates.Packages
case "Play":
// Play apps don't have detailed list yet
packages = []model.Package{}
}
packageListPage := pages.NewPackageListPage(category, packages)
w.navView.Push(packageListPage.NavigationPage)
}
// showError shows an error dialog
func (w *Window) showError(title, message string) {
dialog := adw.NewMessageDialog(nil, title, message)
dialog.AddResponse("ok", "OK")
dialog.SetDefaultResponse("ok")
dialog.Present()
}
// Cleanup cleans up resources
func (w *Window) Cleanup() {
w.cancel()
w.st.Unsubscribe(w.stateChanges)
log.Println("Window cleanup done")
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment