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)) } }