130 lines
4.7 KiB
Go
130 lines
4.7 KiB
Go
package files
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/code"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/feedback"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/icons"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/input"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
|
|
)
|
|
|
|
// FileOperationKind is what a file operation does.
|
|
type FileOperationKind int
|
|
|
|
const (
|
|
FileCopy FileOperationKind = iota
|
|
FileMove
|
|
FileDelete
|
|
)
|
|
|
|
// FileOperation is the caller's running operation; counts are of items.
|
|
type FileOperation struct {
|
|
Kind FileOperationKind
|
|
Done, Total int
|
|
// BytesDone and BytesTotal show the amount when BytesTotal is set.
|
|
BytesDone, BytesTotal int64
|
|
// Current is the item in progress.
|
|
Current string
|
|
// Conflict names an existing target awaiting a decision.
|
|
Conflict string
|
|
}
|
|
|
|
// ConflictChoice answers a name conflict.
|
|
type ConflictChoice int
|
|
|
|
const (
|
|
ConflictNone ConflictChoice = iota
|
|
ConflictReplace
|
|
ConflictKeepBoth
|
|
ConflictSkip
|
|
)
|
|
|
|
// FileOperationResult is the panel and the user's answer this frame.
|
|
type FileOperationResult struct {
|
|
Element *ui.Element
|
|
Canceled bool
|
|
// Resolved answers a conflict; ApplyAll extends that decision.
|
|
Resolved ConflictChoice
|
|
ApplyAll bool
|
|
}
|
|
|
|
// FileOperationProgress displays progress, cancellation and conflict choices.
|
|
func FileOperationProgress(c *ui.Context, op FileOperation, opts FileOperationOptions) FileOperationResult {
|
|
if op.Total < 0 || op.Done < 0 || op.Done > op.Total || op.BytesTotal < 0 || op.BytesDone < 0 || op.BytesTotal > 0 && op.BytesDone > op.BytesTotal {
|
|
panic("mujica: FileOperation counts out of range")
|
|
}
|
|
if op.Kind < FileCopy || op.Kind > FileDelete {
|
|
panic("mujica: FileOperation kind out of range")
|
|
}
|
|
k := core.Tokens(c)
|
|
u := core.Density(c).Unit()
|
|
var res FileOperationResult
|
|
res.Element = code.CodePanel(c).Padding(u * 4).Gap(u * 3).Disabled(opts.Disabled).Role(ui.RoleGroup)
|
|
all := ui.Local(res.Element, "file_operation_progress.all", func() bool { return false })
|
|
disabled := res.Element.IsDisabled()
|
|
verb := core.Msg(c, [...]string{"file_operation_progress.copying", "file_operation_progress.moving", "file_operation_progress.deleting"}[op.Kind])
|
|
title := fmt.Sprintf(verb, op.Done, op.Total)
|
|
res.Element.Label(title)
|
|
frac := 0.0
|
|
switch {
|
|
case op.BytesTotal > 0:
|
|
frac = float64(op.BytesDone) / float64(op.BytesTotal)
|
|
case op.Total > 0:
|
|
frac = float64(op.Done) / float64(op.Total)
|
|
}
|
|
res.Element.Children(func() {
|
|
feedback.Progress(c, frac, feedback.ProgressOptions{Label: title, ShowValue: true})
|
|
ui.Row(c).Gap(u * 3).AlignItems(ui.Center).Children(func() {
|
|
ui.Column(c).Grow(1).MinWidth(0).Children(func() {
|
|
if op.Current != "" {
|
|
ui.Text(c, op.Current).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine()
|
|
}
|
|
if op.BytesTotal > 0 {
|
|
ui.Text(c, fmt.Sprintf(core.Msg(c, "file_operation_progress.bytes"), code.ProcessListBytes(uint64(op.BytesDone)), code.ProcessListBytes(uint64(op.BytesTotal)))).
|
|
FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted)
|
|
}
|
|
})
|
|
if input.Button(c, core.Msg(c, "file_operation_progress.cancel"), input.ButtonOptions{Variant: input.Secondary}).Clicked() && !disabled {
|
|
res.Canceled = true
|
|
}
|
|
})
|
|
if op.Conflict == "" {
|
|
return
|
|
}
|
|
ui.Column(c).Gap(u*2).Padding(u*3).Radius(theme.ControlRadius).Border(theme.BorderWidth, k.Warning).Children(func() {
|
|
ui.Row(c).Gap(u * 2).AlignItems(ui.Center).TextColor(k.Warning).Children(func() {
|
|
ui.Icon(c, icons.Must("triangle-alert")).Size(16, 16)
|
|
ui.Text(c, fmt.Sprintf(core.Msg(c, "file_operation_progress.conflict"), op.Conflict)).TextColor(k.Text).Role(ui.RoleStatus)
|
|
})
|
|
input.Checkbox(c, all, core.Msg(c, "file_operation_progress.all"), input.CheckboxOptions{Disabled: opts.Disabled})
|
|
ui.Row(c).Gap(u * 2).Justify(ui.End).Children(func() {
|
|
for _, b := range []struct {
|
|
key string
|
|
v input.ButtonVariant
|
|
cho ConflictChoice
|
|
}{{"file_operation_progress.skip", input.Ghost, ConflictSkip}, {"file_operation_progress.keepBoth", input.Secondary, ConflictKeepBoth}, {"file_operation_progress.replace", input.Danger, ConflictReplace}} {
|
|
if input.Button(c, core.Msg(c, b.key), input.ButtonOptions{Variant: b.v}).Clicked() && !disabled {
|
|
res.Resolved, res.ApplyAll = b.cho, *all
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
if res.Canceled || res.Resolved != ConflictNone {
|
|
slog.Debug("mujica: file_operation_progress answer", "canceled", res.Canceled, "choice", res.Resolved, "all", res.ApplyAll)
|
|
*all = false
|
|
}
|
|
return res
|
|
}
|
|
|
|
// FileOperationOptions configure a FileOperationProgress.
|
|
type FileOperationOptions struct {
|
|
Disabled bool
|
|
}
|