338 lines
8.9 KiB
Go
338 lines
8.9 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/charmbracelet/bubbles/list"
|
||
"github.com/charmbracelet/bubbles/progress"
|
||
"github.com/charmbracelet/bubbles/spinner"
|
||
tea "github.com/charmbracelet/bubbletea"
|
||
)
|
||
|
||
// --- Version Select Page ---
|
||
|
||
func updateVersionSelect(m model, msg tea.Msg) (model, tea.Cmd) {
|
||
switch msg := msg.(type) {
|
||
case versionsFoundMsg:
|
||
items := make([]list.Item, len(msg))
|
||
for i, v := range msg {
|
||
items[i] = versionItem{name: v.name, path: v.path}
|
||
}
|
||
m.logLines = nil // Bug3: 清除旧的扫描错误,否则 view 永远显示错误
|
||
m.versionList.SetItems(items)
|
||
return m, nil
|
||
|
||
case scanErrorMsg:
|
||
m.logLines = []string{errorStyle.Render("扫描版本目录失败: " + msg.err.Error())}
|
||
return m, nil
|
||
|
||
case tea.KeyMsg:
|
||
if msg.String() == "enter" {
|
||
selected, ok := m.versionList.SelectedItem().(versionItem)
|
||
if ok {
|
||
m.versionDir = selected.path
|
||
m.versionName = selected.name
|
||
m.currentPage = pageMainMenu
|
||
m.logLines = nil // Bug1: 清除版本选择页的 logLines
|
||
m.menuList.SetItems([]list.Item{
|
||
menuItem{title: "输入数字码", desc: "输入工具码以执行操作"},
|
||
menuItem{title: "切换版本", desc: "选择其他游戏版本"},
|
||
})
|
||
return m, nil
|
||
}
|
||
}
|
||
}
|
||
|
||
var cmd tea.Cmd
|
||
m.versionList, cmd = m.versionList.Update(msg)
|
||
return m, cmd
|
||
}
|
||
|
||
func viewVersionSelect(m model) string {
|
||
if len(m.logLines) > 0 {
|
||
return "\n" + strings.Join(m.logLines, "\n") + "\n\n" + subtleStyle.Render("按 ctrl+c 退出")
|
||
}
|
||
return m.versionList.View()
|
||
}
|
||
|
||
// --- Main Menu Page ---
|
||
|
||
func updateMainMenu(m model, msg tea.Msg) (model, tea.Cmd) {
|
||
switch msg := msg.(type) {
|
||
case tea.KeyMsg:
|
||
switch msg.String() {
|
||
case "enter":
|
||
idx := m.menuList.Index()
|
||
switch idx {
|
||
case 0:
|
||
m.currentPage = pageCodeInput
|
||
m.codeInput.SetValue("")
|
||
return m, m.codeInput.Focus()
|
||
case 1:
|
||
m.currentPage = pageVersionSelect
|
||
m.logLines = nil // Bug2: 清除残留 logLines,否则版本列表被错误文本遮挡
|
||
m.versionList.SetItems(nil) // Bug10: 清除旧列表,避免短暂显示过时数据
|
||
return m, scanVersions(m.exeDir)
|
||
}
|
||
case "esc", "q":
|
||
return m, tea.Quit
|
||
}
|
||
}
|
||
|
||
var cmd tea.Cmd
|
||
m.menuList, cmd = m.menuList.Update(msg)
|
||
return m, cmd
|
||
}
|
||
|
||
func viewMainMenu(m model) string {
|
||
header := subtleStyle.Render(fmt.Sprintf("当前版本: %s", m.versionName))
|
||
return header + "\n" + m.menuList.View()
|
||
}
|
||
|
||
// --- Code Input Page ---
|
||
|
||
func updateCodeInput(m model, msg tea.Msg) (model, tea.Cmd) {
|
||
switch msg := msg.(type) {
|
||
case tea.KeyMsg:
|
||
switch msg.String() {
|
||
case "enter":
|
||
code := m.codeInput.Value()
|
||
if len(code) == 4 {
|
||
m.currentPage = pageExecuting
|
||
m = resetExecState(m)
|
||
return m, tea.Batch(m.spinner.Tick, fetchActions(code))
|
||
}
|
||
case "esc":
|
||
m.currentPage = pageMainMenu
|
||
return m, nil
|
||
}
|
||
}
|
||
|
||
var cmd tea.Cmd
|
||
m.codeInput, cmd = m.codeInput.Update(msg)
|
||
return m, cmd
|
||
}
|
||
|
||
func viewCodeInput(m model) string {
|
||
return fmt.Sprintf(
|
||
"%s\n\n 输入4位数字码:\n\n %s\n\n %s\n",
|
||
appTitle,
|
||
m.codeInput.View(),
|
||
subtleStyle.Render("enter 确认 | esc 返回"),
|
||
)
|
||
}
|
||
|
||
// --- Executing Page ---
|
||
|
||
type actionProgressMsg struct {
|
||
percent float64
|
||
message string
|
||
}
|
||
type actionCompleteMsg struct{ index int }
|
||
type actionErrorMsg struct {
|
||
index int
|
||
err error
|
||
}
|
||
type mirrorChoiceMsg struct {
|
||
index int
|
||
mirrors []string
|
||
action Action
|
||
}
|
||
|
||
func updateExecuting(m model, msg tea.Msg) (model, tea.Cmd) {
|
||
if m.choosingMirror {
|
||
return updateMirrorChoice(m, msg)
|
||
}
|
||
|
||
switch msg := msg.(type) {
|
||
case actionsReceivedMsg:
|
||
m.actions = msg.actions
|
||
m.actionIdx = 0
|
||
if len(m.actions) == 0 {
|
||
m.execDone = true
|
||
m.logLines = append(m.logLines, successStyle.Render("没有需要执行的操作"))
|
||
return m, nil
|
||
}
|
||
m.backupDir = fmt.Sprintf("amt/backup/%s", time.Now().Format("20060102_150405"))
|
||
m.logLines = append(m.logLines, boldStyle.Render(fmt.Sprintf("共 %d 个操作", len(m.actions))))
|
||
m.logLines = append(m.logLines, describeAction(m.actions[0], 0, len(m.actions)))
|
||
return m, tea.Batch(m.spinner.Tick, executeAction(m.versionDir, m.actions[0], 0, m.backupDir))
|
||
|
||
case apiErrorMsg:
|
||
m.execErr = msg.err
|
||
m.logLines = append(m.logLines, crossMark+" "+errorStyle.Render("API 请求失败: "+msg.err.Error()))
|
||
return m, nil
|
||
|
||
case actionProgressMsg:
|
||
if msg.message != "" {
|
||
m.logLines = append(m.logLines, msg.message)
|
||
}
|
||
cmd := m.progress.SetPercent(msg.percent)
|
||
var cmds []tea.Cmd
|
||
cmds = append(cmds, cmd)
|
||
if m.progressCh != nil {
|
||
cmds = append(cmds, waitForProgress(m.progressCh, m.actionIdx))
|
||
}
|
||
return m, tea.Batch(cmds...)
|
||
|
||
case actionCompleteMsg:
|
||
m.progressCh = nil
|
||
total := len(m.actions)
|
||
m.logLines = append(m.logLines, fmt.Sprintf("%s [%d/%d] 完成", checkMark, msg.index+1, total))
|
||
|
||
if msg.index+1 >= total {
|
||
m.execDone = true
|
||
m.logLines = append(m.logLines, "\n"+successStyle.Render("全部操作完成!"))
|
||
return m, m.progress.SetPercent(1.0)
|
||
}
|
||
|
||
m.actionIdx = msg.index + 1
|
||
m.logLines = append(m.logLines, describeAction(m.actions[m.actionIdx], m.actionIdx, total))
|
||
progressCmd := m.progress.SetPercent(float64(m.actionIdx) / float64(total))
|
||
return m, tea.Batch(
|
||
progressCmd,
|
||
executeAction(m.versionDir, m.actions[m.actionIdx], m.actionIdx, m.backupDir),
|
||
)
|
||
|
||
case actionErrorMsg:
|
||
m.progressCh = nil
|
||
m.execErr = msg.err
|
||
m.logLines = append(m.logLines, fmt.Sprintf("%s [%d/%d] 失败: %s", crossMark, msg.index+1, len(m.actions), msg.err.Error()))
|
||
return m, nil
|
||
|
||
case mirrorChoiceMsg:
|
||
m.progressCh = nil
|
||
m.choosingMirror = true
|
||
m.pendingAction = &msg.action
|
||
items := make([]list.Item, len(msg.mirrors))
|
||
for i, u := range msg.mirrors {
|
||
items[i] = mirrorItem{url: u}
|
||
}
|
||
delegate := list.NewDefaultDelegate()
|
||
l := list.New(items, delegate, m.width, m.height-6)
|
||
l.Title = "主 URL 下载失败,请选择镜像源"
|
||
l.SetFilteringEnabled(false)
|
||
l.SetShowStatusBar(false)
|
||
m.mirrorList = l
|
||
return m, nil
|
||
|
||
case tea.KeyMsg:
|
||
if m.execDone || m.execErr != nil {
|
||
m.currentPage = pageMainMenu
|
||
m = resetExecState(m)
|
||
return m, nil
|
||
}
|
||
|
||
case spinner.TickMsg:
|
||
var cmd tea.Cmd
|
||
m.spinner, cmd = m.spinner.Update(msg)
|
||
return m, cmd
|
||
|
||
case progress.FrameMsg:
|
||
p, cmd := m.progress.Update(msg)
|
||
m.progress = p.(progress.Model)
|
||
return m, cmd
|
||
}
|
||
|
||
return m, nil
|
||
}
|
||
|
||
func updateMirrorChoice(m model, msg tea.Msg) (model, tea.Cmd) {
|
||
switch msg := msg.(type) {
|
||
case tea.KeyMsg:
|
||
if msg.String() == "enter" {
|
||
selected, ok := m.mirrorList.SelectedItem().(mirrorItem)
|
||
if ok && m.pendingAction != nil {
|
||
m.choosingMirror = false
|
||
action := *m.pendingAction
|
||
action.URL = selected.url
|
||
m.pendingAction = nil
|
||
m.logLines = append(m.logLines, subtleStyle.Render("使用镜像: "+selected.url))
|
||
return m, executeAction(m.versionDir, action, m.actionIdx, m.backupDir)
|
||
}
|
||
}
|
||
if msg.String() == "esc" {
|
||
m.choosingMirror = false
|
||
m.pendingAction = nil
|
||
m.execErr = fmt.Errorf("用户取消镜像选择")
|
||
m.logLines = append(m.logLines, crossMark+" "+errorStyle.Render("已取消"))
|
||
return m, nil
|
||
}
|
||
}
|
||
|
||
var cmd tea.Cmd
|
||
m.mirrorList, cmd = m.mirrorList.Update(msg)
|
||
return m, cmd
|
||
}
|
||
|
||
func viewExecuting(m model) string {
|
||
if m.choosingMirror {
|
||
return m.mirrorList.View()
|
||
}
|
||
|
||
var sb strings.Builder
|
||
sb.WriteString(appTitle)
|
||
sb.WriteString("\n\n")
|
||
|
||
for _, line := range m.logLines {
|
||
sb.WriteString(" ")
|
||
sb.WriteString(line)
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
if !m.execDone && m.execErr == nil && len(m.actions) > 0 {
|
||
sb.WriteString("\n")
|
||
sb.WriteString(fmt.Sprintf(" %s 正在执行 [%d/%d]...\n", m.spinner.View(), m.actionIdx+1, len(m.actions)))
|
||
sb.WriteString("\n ")
|
||
sb.WriteString(m.progress.View())
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
if m.execDone || m.execErr != nil {
|
||
sb.WriteString("\n ")
|
||
sb.WriteString(subtleStyle.Render("按任意键返回主菜单"))
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
return sb.String()
|
||
}
|
||
|
||
func resetExecState(m model) model {
|
||
m.actions = nil
|
||
m.actionIdx = 0
|
||
m.logLines = nil
|
||
m.execErr = nil
|
||
m.execDone = false
|
||
m.choosingMirror = false
|
||
m.pendingAction = nil
|
||
m.backupDir = ""
|
||
m.progressCh = nil
|
||
return m
|
||
}
|
||
|
||
func describeAction(a Action, idx, total int) string {
|
||
prefix := fmt.Sprintf("[%d/%d]", idx+1, total)
|
||
switch a.Type {
|
||
case "add":
|
||
if a.Unzip {
|
||
return subtleStyle.Render(fmt.Sprintf("%s 下载并解压 %s ...", prefix, a.Path))
|
||
}
|
||
return subtleStyle.Render(fmt.Sprintf("%s 下载 %s ...", prefix, a.Path))
|
||
case "delete":
|
||
return subtleStyle.Render(fmt.Sprintf("%s 删除 %s ...", prefix, a.Path))
|
||
case "copy":
|
||
return subtleStyle.Render(fmt.Sprintf("%s 复制 %s -> %s ...", prefix, a.Path, a.NewPath))
|
||
case "move":
|
||
return subtleStyle.Render(fmt.Sprintf("%s 移动 %s -> %s ...", prefix, a.Path, a.NewPath))
|
||
case "new":
|
||
if a.IsDir {
|
||
return subtleStyle.Render(fmt.Sprintf("%s 新建文件夹 %s ...", prefix, a.Path))
|
||
}
|
||
return subtleStyle.Render(fmt.Sprintf("%s 新建文件 %s ...", prefix, a.Path))
|
||
default:
|
||
return subtleStyle.Render(fmt.Sprintf("%s %s %s ...", prefix, a.Type, a.Path))
|
||
}
|
||
}
|