Files
2026-06-05 20:26:24 +08:00

341 lines
9.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"fmt"
"path/filepath"
"strings"
"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 = newBackupDir()
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()))
if m.backupDir != "" {
m.logLines = append(m.logLines, subtleStyle.Render("备份目录: "+filepath.Join(m.versionDir, m.backupDir)))
}
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))
}
}