package mujica import ( "fmt" "log/slog" "github.com/egoist/mygo/ui" "mujicaui/icons" "mujicaui/internal" "mujicaui/theme" ) // TransferStatus is where a transfer is. type TransferStatus int const ( TransferQueued TransferStatus = iota TransferActive TransferPaused TransferFailed TransferDone ) // QueuedTransfer is one upload or download the caller runs. type QueuedTransfer struct { ID, Name string Upload bool Status TransferStatus Done, Total int64 // Error explains a failure. Error string } // TransferAction is what the user asked of a transfer. type TransferAction int const ( TransferNone TransferAction = iota TransferPause TransferResume TransferRetry TransferRemove ) // TransferQueueOptions configure a TransferQueue. type TransferQueueOptions struct { Disabled bool // Revision changes when items are edited in place. Revision uint64 } // TransferQueueResult exposes the queue and requested item action. type TransferQueueResult struct { Element *ui.Element Action TransferAction // ID is the transfer the action is for. ID string } // TransferQueueState is the queue's place. type TransferQueueState struct { rows ui.ListState data *QueuedTransfer n int revision uint64 active, failed int started bool } // TransferQueue virtualizes uploads, downloads and their available actions. func TransferQueue(c *ui.Context, items []QueuedTransfer, s *TransferQueueState, opts TransferQueueOptions) TransferQueueResult { if s == nil { panic("mujica: TransferQueue needs a state") } var first *QueuedTransfer if len(items) > 0 { first = &items[0] } if !s.started || s.data != first || s.n != len(items) || s.revision != opts.Revision { s.active, s.failed = 0, 0 ids := make(map[string]bool, len(items)) for _, t := range items { if t.ID == "" || ids[t.ID] { panic("mujica: Transfer needs a unique ID") } ids[t.ID] = true if t.Status < TransferQueued || t.Status > TransferDone || t.Done < 0 || t.Total < 0 || t.Total > 0 && t.Done > t.Total { panic("mujica: Transfer " + t.ID + " has an invalid status or size") } switch t.Status { case TransferActive, TransferQueued: s.active++ case TransferFailed: s.failed++ } } s.data, s.n, s.revision, s.started = first, len(items), opts.Revision, true } d := Density(c) var res TransferQueueResult res.Element = codePanel(c).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() res.Element.Children(func() { codeHeader(c, func() { ui.Text(c, fmt.Sprintf(msg(c, "transfer_queue.summary"), s.active, s.failed)).Role(ui.RoleStatus) }) if len(items) == 0 { codeCaption(c, msg(c, "transfer_queue.empty")).Padding(d.Unit() * 4) return } s.rows.Key = func(i int) any { return items[i].ID } s.rows.Label = func(i int) string { return items[i].Name } ui.List(c, &s.rows, len(items), func(i int) { if a := transferRow(c, items[i]); a != TransferNone && !disabled { res.Action, res.ID = a, items[i].ID slog.Debug("mujica: transfer_queue action", "action", a, "id", items[i].ID) } }).Grow(1).MinHeight(d.ControlHeight() * 3) }) return res } // transferRow builds one transfer and returns the action clicked. func transferRow(c *ui.Context, t QueuedTransfer) TransferAction { k := Tokens(c) d := Density(c) u := d.Unit() action := TransferNone ui.Row(c).MinHeight(d.ControlHeight()+2*u).Padding(u, u*3).Gap(u * 3).AlignItems(ui.Center).Children(func() { FileIcon(c, t.Name, FileIconOptions{}) ui.Column(c).Grow(1).MinWidth(0).Gap(u).Children(func() { ui.Row(c).Gap(u).AlignItems(ui.Center).Children(func() { dir, key := "arrow-down", "transfer_queue.download" if t.Upload { dir, key = "arrow-up", "transfer_queue.upload" } ui.Icon(c, icons.Must(dir)).Size(14, 14).Shrink(0).TextColor(k.TextMuted).Label(msg(c, key)).Role(ui.RoleImage) ui.Text(c, t.Name).SingleLine().MinWidth(0) }) frac := float32(0) if t.Total > 0 { frac = float32(t.Done) / float32(t.Total) } if t.Status == TransferDone { frac = 1 } ui.Box(c).Height(4).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(r, k.Border, 2) fill := k.AccentText if t.Status == TransferFailed { fill = k.Danger } if frac > 0 { p.Fill(ui.Rect{X: internal.Snap(p, r.X), Y: internal.Snap(p, r.Y), W: internal.Snap(p, max(r.W*frac, r.H)), H: r.H}, fill, 2) } }) transferStatusText(c, t, frac) }) var acts []TransferAction switch t.Status { case TransferActive, TransferQueued: acts = []TransferAction{TransferPause} case TransferPaused: acts = []TransferAction{TransferResume} case TransferFailed: acts = []TransferAction{TransferRetry} } acts = append(acts, TransferRemove) for _, a := range acts { icon, key := transferActionIcon(a) if iconAction(c, icon, fmt.Sprintf(msg(c, key), t.Name)).Clicked() { action = a } } }) return action } func transferStatusText(c *ui.Context, t QueuedTransfer, frac float32) { k := Tokens(c) text, col := "", k.TextMuted switch t.Status { case TransferQueued: text = msg(c, "transfer_queue.queued") case TransferActive: text = fmt.Sprintf("%.0f%% ยท %s / %s", frac*100, processListBytes(uint64(t.Done)), processListBytes(uint64(t.Total))) case TransferPaused: text = fmt.Sprintf(msg(c, "transfer_queue.paused"), frac*100) case TransferFailed: text, col = fmt.Sprintf(msg(c, "transfer_queue.failed"), t.Error), k.Danger case TransferDone: text, col = msg(c, "transfer_queue.done"), k.Success } ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)).TextColor(col).SingleLine() } var transferPauseIcon = ui.MustParseSVG([]byte(``)) var transferPlayIcon = ui.MustParseSVG([]byte(``)) func transferActionIcon(a TransferAction) (*ui.SVG, string) { switch a { case TransferPause: return transferPauseIcon, "transfer_queue.pause" case TransferResume: return transferPlayIcon, "transfer_queue.resume" case TransferRetry: return icons.Must("refresh-cw"), "transfer_queue.retry" } return icons.Must("x"), "transfer_queue.remove" }