From 6c04569123e8b448442cc5d5caa293dbef98ffd4 Mon Sep 17 00:00:00 2001 From: lx1056758714-glitch Date: Fri, 19 Dec 2025 16:33:22 +0800 Subject: [PATCH] Refactor UI: replace dashboard and sidebar with infobar Removed dashboard, sidebar, layout, and logs components, consolidating status and account information into a new infobar component. Updated app structure to use tabbed pages and the infobar for displaying key information. Simplified menu and settings handling, and removed legacy UI code for a more streamlined interface. --- internal/chatlog/app.go | 615 ++++++++++++++++------------- internal/ui/dashboard/dashboard.go | 59 --- internal/ui/infobar/infobar.go | 230 +++++++++++ internal/ui/layout/layout.go | 29 -- internal/ui/logs/logs.go | 43 -- internal/ui/menu/menu.go | 2 +- internal/ui/sidebar/sidebar.go | 34 -- 7 files changed, 581 insertions(+), 431 deletions(-) delete mode 100644 internal/ui/dashboard/dashboard.go create mode 100644 internal/ui/infobar/infobar.go delete mode 100644 internal/ui/layout/layout.go delete mode 100644 internal/ui/logs/logs.go delete mode 100644 internal/ui/sidebar/sidebar.go diff --git a/internal/chatlog/app.go b/internal/chatlog/app.go index d8c598f..3dbdd37 100644 --- a/internal/chatlog/app.go +++ b/internal/chatlog/app.go @@ -5,15 +5,13 @@ import ( "path/filepath" "time" + "github.com/rs/zerolog/log" "github.com/sjzar/chatlog/internal/chatlog/ctx" - "github.com/sjzar/chatlog/internal/ui/dashboard" "github.com/sjzar/chatlog/internal/ui/footer" "github.com/sjzar/chatlog/internal/ui/form" "github.com/sjzar/chatlog/internal/ui/help" - "github.com/sjzar/chatlog/internal/ui/layout" - "github.com/sjzar/chatlog/internal/ui/logs" + "github.com/sjzar/chatlog/internal/ui/infobar" "github.com/sjzar/chatlog/internal/ui/menu" - "github.com/sjzar/chatlog/internal/ui/sidebar" "github.com/sjzar/chatlog/internal/wechat" "github.com/gdamore/tcell/v2" @@ -31,76 +29,59 @@ type App struct { m *Manager stopRefresh chan struct{} - // UI Components - layout *layout.Layout - sidebar *sidebar.Sidebar - dashboard *dashboard.Dashboard - logs *logs.Logs + // page + mainPages *tview.Pages + infoBar *infobar.InfoBar + tabPages *tview.Pages footer *footer.Footer - // Page Managers - rootPages *tview.Pages // Handles Main Layout + Modals - contentPages *tview.Pages // Handles Content (Dashboard, Actions, Settings, etc.) - - // Specific Pages - actionsMenu *menu.Menu - help *help.Help + // tab + menu *menu.Menu + help *help.Help + activeTab int + tabCount int } func NewApp(ctx *ctx.Context, m *Manager) *App { app := &App{ - ctx: ctx, - m: m, - Application: tview.NewApplication(), - rootPages: tview.NewPages(), - contentPages: tview.NewPages(), - dashboard: dashboard.New(), - logs: logs.New(), - footer: footer.New(), - actionsMenu: menu.New("操作菜单"), - help: help.New(), + ctx: ctx, + m: m, + Application: tview.NewApplication(), + mainPages: tview.NewPages(), + infoBar: infobar.New(), + tabPages: tview.NewPages(), + footer: footer.New(), + menu: menu.New("主菜单"), + help: help.New(), } - // Initialize Sidebar - app.sidebar = sidebar.New(app.onSidebarSelected) - app.sidebar.AddItem("概览", '1') - app.sidebar.AddItem("操作", '2') - app.sidebar.AddItem("设置", '3') - app.sidebar.AddItem("日志", '4') - app.sidebar.AddItem("帮助", '5') - - // Initialize Layout - app.layout = layout.New(app.sidebar, app.contentPages) - - // Build the main view with Footer - flex := tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(app.layout, 0, 1, true). - AddItem(app.footer, 1, 1, false) - - app.rootPages.AddPage("main", flex, true, true) - - // Initialize Pages - app.contentPages.AddPage("概览", app.dashboard, true, true) - app.contentPages.AddPage("操作", app.actionsMenu, true, false) - app.contentPages.AddPage("日志", app.logs, true, false) - app.contentPages.AddPage("帮助", app.help, true, false) - - // Settings page will be dynamic (using form) but for now let's add a placeholder or the sub-menu logic - app.initSettingsPage() - app.initMenu() + app.updateMenuItemsState() return app } func (a *App) Run() error { + + flex := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(a.infoBar, infobar.InfoBarViewHeight, 0, false). + AddItem(a.tabPages, 0, 1, true). + AddItem(a.footer, 1, 1, false) + + a.mainPages.AddPage("main", flex, true, true) + + a.tabPages. + AddPage("0", a.menu, true, true). + AddPage("1", a.help, true, false) + a.tabCount = 2 + a.SetInputCapture(a.inputCapture) go a.refresh() - if err := a.SetRoot(a.rootPages, true).EnableMouse(true).Run(); err != nil { + if err := a.SetRoot(a.mainPages, true).EnableMouse(false).Run(); err != nil { return err } @@ -108,70 +89,17 @@ func (a *App) Run() error { } func (a *App) Stop() { + // 添加一个通道用于停止刷新 goroutine if a.stopRefresh != nil { close(a.stopRefresh) } a.Application.Stop() } -func (a *App) onSidebarSelected(index int, mainText string, secondaryText string, shortcut rune) { - if a.contentPages.HasPage(mainText) { - a.contentPages.SwitchToPage(mainText) - } -} - -func (a *App) initSettingsPage() { - // For settings, we can reuse the Menu structure as a list of settings categories - settingsMenu := menu.New("系统设置") - - settings := []settingItem{ - { - name: "设置 HTTP 服务地址", - description: "配置 HTTP 服务监听的地址", - action: a.settingHTTPPort, - }, - { - name: "设置工作目录", - description: "配置数据解密后的存储目录", - action: a.settingWorkDir, - }, - { - name: "设置数据密钥", - description: "配置数据解密密钥", - action: a.settingDataKey, - }, - { - name: "设置图片密钥", - description: "配置图片解密密钥", - action: a.settingImgKey, - }, - { - name: "设置数据目录", - description: "配置微信数据文件所在目录", - action: a.settingDataDir, - }, - } - - for idx, setting := range settings { - item := &menu.Item{ - Index: idx + 1, - Name: setting.name, - Description: setting.description, - Selected: func(action func()) func(*menu.Item) { - return func(*menu.Item) { - action() - } - }(setting.action), - } - settingsMenu.AddItem(item) - } - - a.contentPages.AddPage("设置", settingsMenu, true, false) -} - func (a *App) updateMenuItemsState() { - for _, item := range a.actionsMenu.GetItems() { - // Auto Decrypt + // 查找并更新自动解密菜单项 + for _, item := range a.menu.GetItems() { + // 更新自动解密菜单项 if item.Index == 6 { if a.ctx.AutoDecrypt { item.Name = "停止自动解密" @@ -182,7 +110,7 @@ func (a *App) updateMenuItemsState() { } } - // HTTP Service + // 更新HTTP服务菜单项 if item.Index == 5 { if a.ctx.HTTPEnabled { item.Name = "停止 HTTP 服务" @@ -195,6 +123,15 @@ func (a *App) updateMenuItemsState() { } } +func (a *App) switchTab(step int) { + index := (a.activeTab + step) % a.tabCount + if index < 0 { + index = a.tabCount - 1 + } + a.activeTab = index + a.tabPages.SwitchToPage(fmt.Sprint(a.activeTab)) +} + func (a *App) refresh() { tick := time.NewTicker(RefreshInterval) defer tick.Stop() @@ -204,16 +141,18 @@ func (a *App) refresh() { case <-a.stopRefresh: return case <-tick.C: - // Auto-detect account if nil + // 如果当前账号为空,尝试查找微信进程 if a.ctx.Current == nil { + // 获取微信实例 instances := a.m.wechat.GetWeChatInstances() if len(instances) > 0 { + // 找到微信进程,设置第一个为当前账号 a.ctx.SwitchCurrent(instances[0]) - a.logs.AddLog(fmt.Sprintf("检测到微信进程,PID: %d,已设置为当前账号", instances[0].PID)) + log.Info().Msgf("检测到微信进程,PID: %d,已设置为当前账号", instances[0].PID) } } - // Refresh account status + // 刷新当前账号状态(如果存在) if a.ctx.Current != nil { originalName := a.ctx.Current.Name a.ctx.Current.RefreshStatus() @@ -227,36 +166,27 @@ func (a *App) refresh() { if a.ctx.AutoDecrypt || a.ctx.HTTPEnabled { a.m.RefreshSession() } - - // Update Dashboard - dashboardData := map[string]string{ - "Account": a.ctx.Account, - "PID": fmt.Sprintf("%d", a.ctx.PID), - "Status": a.ctx.Status, - "ExePath": a.ctx.ExePath, - "Platform": a.ctx.Platform, - "Version": a.ctx.FullVersion, - "Session": "", - "Data Key": a.ctx.DataKey, - "Image Key": a.ctx.ImgKey, - "Data Usage": a.ctx.DataUsage, - "Data Dir": a.ctx.DataDir, - "Work Usage": a.ctx.WorkUsage, - "Work Dir": a.ctx.WorkDir, - "HTTP Server": "[未启动]", - "Auto Decrypt": "[未开启]", - } - + a.infoBar.UpdateAccount(a.ctx.Account) + a.infoBar.UpdateBasicInfo(a.ctx.PID, a.ctx.FullVersion, a.ctx.ExePath) + a.infoBar.UpdateStatus(a.ctx.Status) + a.infoBar.UpdateDataKey(a.ctx.DataKey) + a.infoBar.UpdateImageKey(a.ctx.ImgKey) + a.infoBar.UpdatePlatform(a.ctx.Platform) + a.infoBar.UpdateDataUsageDir(a.ctx.DataUsage, a.ctx.DataDir) + a.infoBar.UpdateWorkUsageDir(a.ctx.WorkUsage, a.ctx.WorkDir) if a.ctx.LastSession.Unix() > 1000000000 { - dashboardData["Session"] = a.ctx.LastSession.Format("2006-01-02 15:04:05") + a.infoBar.UpdateSession(a.ctx.LastSession.Format("2006-01-02 15:04:05")) } if a.ctx.HTTPEnabled { - dashboardData["HTTP Server"] = fmt.Sprintf("[green][已启动][white] [%s]", a.ctx.HTTPAddr) + a.infoBar.UpdateHTTPServer(fmt.Sprintf("[green][已启动][white] [%s]", a.ctx.HTTPAddr)) + } else { + a.infoBar.UpdateHTTPServer("[未启动]") } if a.ctx.AutoDecrypt { - dashboardData["Auto Decrypt"] = "[green][已开启][white]" + a.infoBar.UpdateAutoDecrypt("[green][已开启][white]") + } else { + a.infoBar.UpdateAutoDecrypt("[未开启]") } - a.dashboard.Update(dashboardData) // Update latest message in footer if session, err := a.m.GetLatestSession(); err == nil && session != nil { @@ -264,7 +194,7 @@ func (a *App) refresh() { if sender == "" { sender = session.UserName } - a.footer.UpdateLatestMessage(sender, session.NTime.Format("15:04:05"), session.Content) + a.footer.UpdateLatestMessage(sender, session.NTime.Format("15:04:05"), session.Content) } a.Draw() @@ -273,26 +203,28 @@ func (a *App) refresh() { } func (a *App) inputCapture(event *tcell.EventKey) *tcell.EventKey { - // If a modal is open (like settings form), let it handle input - // Simple check: if rootPages front page is not "main" - name, _ := a.rootPages.GetFrontPage() - if name != "main" { - return event + + // 如果当前页面不是主页面,ESC 键返回主页面 + if a.mainPages.HasPage("submenu") && event.Key() == tcell.KeyEscape { + a.mainPages.RemovePage("submenu") + a.mainPages.SwitchToPage("main") + return nil + } + + if a.tabPages.HasFocus() { + switch event.Key() { + case tcell.KeyLeft: + a.switchTab(-1) + return nil + case tcell.KeyRight: + a.switchTab(1) + return nil + } } switch event.Key() { case tcell.KeyCtrlC: a.Stop() - case tcell.KeyTab: - // Switch focus between sidebar and content - if a.sidebar.HasFocus() { - _, item := a.contentPages.GetFrontPage() - if item != nil { - a.SetFocus(item) - } - } else { - a.SetFocus(a.sidebar) - } } return event @@ -304,27 +236,28 @@ func (a *App) initMenu() { Name: "获取图片密钥", Description: "扫描内存获取图片密钥(需微信V4)", Selected: func(i *menu.Item) { - a.logs.AddLog("开始扫描内存获取图片密钥...") modal := tview.NewModal() modal.SetText("正在扫描内存获取图片密钥...\n请确保微信已登录并浏览过图片") - a.rootPages.AddPage("modal", modal, true, true) + a.mainPages.AddPage("modal", modal, true, true) a.SetFocus(modal) go func() { err := a.m.GetImageKey() + // 在主线程中更新UI a.QueueUpdateDraw(func() { if err != nil { - a.logs.AddLog("获取图片密钥失败: " + err.Error()) + // 解密失败 modal.SetText("获取图片密钥失败: " + err.Error()) } else { - a.logs.AddLog("获取图片密钥成功") + // 解密成功 modal.SetText("获取图片密钥成功") } + // 添加确认按钮 modal.AddButtons([]string{"OK"}) modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { - a.rootPages.RemovePage("modal") + a.mainPages.RemovePage("modal") }) a.SetFocus(modal) }) @@ -337,16 +270,15 @@ func (a *App) initMenu() { Name: "重启并获取密钥", Description: "结束当前微信进程,重启后获取密钥", Selected: func(i *menu.Item) { - a.logs.AddLog("准备重启微信获取密钥...") modal := tview.NewModal().SetText("正在准备重启微信...") - a.rootPages.AddPage("modal", modal, true, true) + a.mainPages.AddPage("modal", modal, true, true) a.SetFocus(modal) go func() { + // 定义状态更新回调 onStatus := func(msg string) { a.QueueUpdateDraw(func() { modal.SetText(msg) - a.logs.AddLog(msg) }) } @@ -354,16 +286,14 @@ func (a *App) initMenu() { a.QueueUpdateDraw(func() { if err != nil { - a.logs.AddLog("重启获取密钥失败: " + err.Error()) modal.SetText("操作失败: " + err.Error()) } else { - a.logs.AddLog("重启获取密钥成功") modal.SetText("操作成功,请检查密钥是否已更新") } modal.AddButtons([]string{"OK"}) modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { - a.rootPages.RemovePage("modal") + a.mainPages.RemovePage("modal") }) a.SetFocus(modal) }) @@ -376,26 +306,32 @@ func (a *App) initMenu() { Name: "解密数据", Description: "解密数据文件", Selected: func(i *menu.Item) { - a.logs.AddLog("开始解密数据...") - modal := tview.NewModal().SetText("解密中...") - a.rootPages.AddPage("modal", modal, true, true) + // 创建一个没有按钮的模态框,显示"解密中..." + modal := tview.NewModal(). + SetText("解密中...") + + a.mainPages.AddPage("modal", modal, true, true) a.SetFocus(modal) + // 在后台执行解密操作 go func() { + // 执行解密 err := a.m.DecryptDBFiles() + // 在主线程中更新UI a.QueueUpdateDraw(func() { if err != nil { - a.logs.AddLog("解密失败: " + err.Error()) + // 解密失败 modal.SetText("解密失败: " + err.Error()) } else { - a.logs.AddLog("解密数据成功") + // 解密成功 modal.SetText("解密数据成功") } + // 添加确认按钮 modal.AddButtons([]string{"OK"}) modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { - a.rootPages.RemovePage("modal") + a.mainPages.RemovePage("modal") }) a.SetFocus(modal) }) @@ -410,56 +346,65 @@ func (a *App) initMenu() { Selected: func(i *menu.Item) { modal := tview.NewModal() + // 根据当前服务状态执行不同操作 if !a.ctx.HTTPEnabled { - a.logs.AddLog("正在启动 HTTP 服务...") + // HTTP 服务未启动,启动服务 modal.SetText("正在启动 HTTP 服务...") - a.rootPages.AddPage("modal", modal, true, true) + a.mainPages.AddPage("modal", modal, true, true) a.SetFocus(modal) + // 在后台启动服务 go func() { err := a.m.StartService() + // 在主线程中更新UI a.QueueUpdateDraw(func() { if err != nil { - a.logs.AddLog("启动 HTTP 服务失败: " + err.Error()) + // 启动失败 modal.SetText("启动 HTTP 服务失败: " + err.Error()) } else { - a.logs.AddLog("已启动 HTTP 服务") + // 启动成功 modal.SetText("已启动 HTTP 服务") } + // 更改菜单项名称 a.updateMenuItemsState() + // 添加确认按钮 modal.AddButtons([]string{"OK"}) modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { - a.rootPages.RemovePage("modal") + a.mainPages.RemovePage("modal") }) a.SetFocus(modal) }) }() } else { - a.logs.AddLog("正在停止 HTTP 服务...") + // HTTP 服务已启动,停止服务 modal.SetText("正在停止 HTTP 服务...") - a.rootPages.AddPage("modal", modal, true, true) + a.mainPages.AddPage("modal", modal, true, true) a.SetFocus(modal) + // 在后台停止服务 go func() { err := a.m.StopService() + // 在主线程中更新UI a.QueueUpdateDraw(func() { if err != nil { - a.logs.AddLog("停止 HTTP 服务失败: " + err.Error()) + // 停止失败 modal.SetText("停止 HTTP 服务失败: " + err.Error()) } else { - a.logs.AddLog("已停止 HTTP 服务") + // 停止成功 modal.SetText("已停止 HTTP 服务") } + // 更改菜单项名称 a.updateMenuItemsState() + // 添加确认按钮 modal.AddButtons([]string{"OK"}) modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { - a.rootPages.RemovePage("modal") + a.mainPages.RemovePage("modal") }) a.SetFocus(modal) }) @@ -475,54 +420,65 @@ func (a *App) initMenu() { Selected: func(i *menu.Item) { modal := tview.NewModal() + // 根据当前自动解密状态执行不同操作 if !a.ctx.AutoDecrypt { - a.logs.AddLog("开启自动解密...") + // 自动解密未开启,开启自动解密 modal.SetText("正在开启自动解密...") - a.rootPages.AddPage("modal", modal, true, true) + a.mainPages.AddPage("modal", modal, true, true) a.SetFocus(modal) + // 在后台开启自动解密 go func() { err := a.m.StartAutoDecrypt() + // 在主线程中更新UI a.QueueUpdateDraw(func() { if err != nil { - a.logs.AddLog("开启自动解密失败: " + err.Error()) + // 开启失败 modal.SetText("开启自动解密失败: " + err.Error()) } else { - a.logs.AddLog("已开启自动解密") + // 开启成功 modal.SetText("已开启自动解密") } + // 更改菜单项名称 a.updateMenuItemsState() + + // 添加确认按钮 modal.AddButtons([]string{"OK"}) modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { - a.rootPages.RemovePage("modal") + a.mainPages.RemovePage("modal") }) a.SetFocus(modal) }) }() } else { - a.logs.AddLog("停止自动解密...") + // 自动解密已开启,停止自动解密 modal.SetText("正在停止自动解密...") - a.rootPages.AddPage("modal", modal, true, true) + a.mainPages.AddPage("modal", modal, true, true) a.SetFocus(modal) + // 在后台停止自动解密 go func() { err := a.m.StopAutoDecrypt() + // 在主线程中更新UI a.QueueUpdateDraw(func() { if err != nil { - a.logs.AddLog("停止自动解密失败: " + err.Error()) + // 停止失败 modal.SetText("停止自动解密失败: " + err.Error()) } else { - a.logs.AddLog("已停止自动解密") + // 停止成功 modal.SetText("已停止自动解密") } + // 更改菜单项名称 a.updateMenuItemsState() + + // 添加确认按钮 modal.AddButtons([]string{"OK"}) modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { - a.rootPages.RemovePage("modal") + a.mainPages.RemovePage("modal") }) a.SetFocus(modal) }) @@ -531,6 +487,13 @@ func (a *App) initMenu() { }, } + setting := &menu.Item{ + Index: 7, + Name: "设置", + Description: "设置应用程序选项", + Selected: a.settingSelected, + } + selectAccount := &menu.Item{ Index: 8, Name: "切换账号", @@ -578,22 +541,23 @@ func (a *App) initMenu() { SetText(text). AddButtons([]string{"返回"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { - a.rootPages.RemovePage("modal") + a.mainPages.RemovePage("modal") }) - a.rootPages.AddPage("modal", modal, true, true) + a.mainPages.AddPage("modal", modal, true, true) a.SetFocus(modal) }, } - a.actionsMenu.AddItem(getDataKey) - a.actionsMenu.AddItem(restartAndGetDataKey) - a.actionsMenu.AddItem(decryptData) - a.actionsMenu.AddItem(httpServer) - a.actionsMenu.AddItem(autoDecrypt) - a.actionsMenu.AddItem(selectAccount) - a.actionsMenu.AddItem(mcpSubscriptions) + a.menu.AddItem(getDataKey) + a.menu.AddItem(restartAndGetDataKey) + a.menu.AddItem(decryptData) + a.menu.AddItem(httpServer) + a.menu.AddItem(autoDecrypt) + a.menu.AddItem(setting) + a.menu.AddItem(selectAccount) + a.menu.AddItem(mcpSubscriptions) - a.actionsMenu.AddItem(&menu.Item{ + a.menu.AddItem(&menu.Item{ Index: 10, Name: "退出", Description: "退出程序", @@ -603,85 +567,150 @@ func (a *App) initMenu() { }) } -// settingItem represents a setting item +// settingItem 表示一个设置项 type settingItem struct { name string description string action func() } -// settingHTTPPort Sets HTTP Port +func (a *App) settingSelected(i *menu.Item) { + + settings := []settingItem{ + { + name: "设置 HTTP 服务地址", + description: "配置 HTTP 服务监听的地址", + action: a.settingHTTPPort, + }, + { + name: "设置工作目录", + description: "配置数据解密后的存储目录", + action: a.settingWorkDir, + }, + { + name: "设置数据密钥", + description: "配置数据解密密钥", + action: a.settingDataKey, + }, + { + name: "设置图片密钥", + description: "配置图片解密密钥", + action: a.settingImgKey, + }, + { + name: "设置数据目录", + description: "配置微信数据文件所在目录", + action: a.settingDataDir, + }, + } + + subMenu := menu.NewSubMenu("设置") + for idx, setting := range settings { + item := &menu.Item{ + Index: idx + 1, + Name: setting.name, + Description: setting.description, + Selected: func(action func()) func(*menu.Item) { + return func(*menu.Item) { + action() + } + }(setting.action), + } + subMenu.AddItem(item) + } + + a.mainPages.AddPage("submenu", subMenu, true, true) + a.SetFocus(subMenu) +} + +// settingHTTPPort 设置 HTTP 端口 func (a *App) settingHTTPPort() { + // 使用我们的自定义表单组件 formView := form.NewForm("设置 HTTP 地址") + + // 临时存储用户输入的值 tempHTTPAddr := a.ctx.HTTPAddr + // 添加输入字段 - 不再直接设置HTTP地址,而是更新临时变量 formView.AddInputField("地址", tempHTTPAddr, 0, nil, func(text string) { - tempHTTPAddr = text + tempHTTPAddr = text // 只更新临时变量 }) + // 添加按钮 - 点击保存时才设置HTTP地址 formView.AddButton("保存", func() { - a.m.SetHTTPAddr(tempHTTPAddr) - a.rootPages.RemovePage("form") + a.m.SetHTTPAddr(tempHTTPAddr) // 在这里设置HTTP地址 + a.mainPages.RemovePage("submenu2") a.showInfo("HTTP 地址已设置为 " + a.ctx.HTTPAddr) }) formView.AddButton("取消", func() { - a.rootPages.RemovePage("form") + a.mainPages.RemovePage("submenu2") }) - a.rootPages.AddPage("form", formView, true, true) + a.mainPages.AddPage("submenu2", formView, true, true) a.SetFocus(formView) } -// settingWorkDir Sets Work Dir +// settingWorkDir 设置工作目录 func (a *App) settingWorkDir() { + // 使用我们的自定义表单组件 formView := form.NewForm("设置工作目录") + + // 临时存储用户输入的值 tempWorkDir := a.ctx.WorkDir + // 添加输入字段 - 不再直接设置工作目录,而是更新临时变量 formView.AddInputField("工作目录", tempWorkDir, 0, nil, func(text string) { - tempWorkDir = text + tempWorkDir = text // 只更新临时变量 }) + // 添加按钮 - 点击保存时才设置工作目录 formView.AddButton("保存", func() { - a.ctx.SetWorkDir(tempWorkDir) - a.rootPages.RemovePage("form") + a.ctx.SetWorkDir(tempWorkDir) // 在这里设置工作目录 + a.mainPages.RemovePage("submenu2") a.showInfo("工作目录已设置为 " + a.ctx.WorkDir) }) formView.AddButton("取消", func() { - a.rootPages.RemovePage("form") + a.mainPages.RemovePage("submenu2") }) - a.rootPages.AddPage("form", formView, true, true) + a.mainPages.AddPage("submenu2", formView, true, true) a.SetFocus(formView) } -// settingDataKey Sets Data Key +// settingDataKey 设置数据密钥 func (a *App) settingDataKey() { + // 使用我们的自定义表单组件 formView := form.NewForm("设置数据密钥") + + // 临时存储用户输入的值 tempDataKey := a.ctx.DataKey + // 添加输入字段 - 不直接设置数据密钥,而是更新临时变量 formView.AddInputField("数据密钥", tempDataKey, 0, nil, func(text string) { - tempDataKey = text + tempDataKey = text // 只更新临时变量 }) + // 添加按钮 - 点击保存时才设置数据密钥 formView.AddButton("保存", func() { - a.ctx.DataKey = tempDataKey - a.rootPages.RemovePage("form") + a.ctx.DataKey = tempDataKey // 设置数据密钥 + a.mainPages.RemovePage("submenu2") a.showInfo("数据密钥已设置") }) formView.AddButton("取消", func() { - a.rootPages.RemovePage("form") + a.mainPages.RemovePage("submenu2") }) - a.rootPages.AddPage("form", formView, true, true) + a.mainPages.AddPage("submenu2", formView, true, true) a.SetFocus(formView) } -// settingImgKey Sets Image Key +// settingImgKey 设置图片密钥 (ImgKey) func (a *App) settingImgKey() { formView := form.NewForm("设置图片密钥") + tempImgKey := a.ctx.ImgKey formView.AddInputField("图片密钥", tempImgKey, 0, nil, func(text string) { @@ -690,58 +719,75 @@ func (a *App) settingImgKey() { formView.AddButton("保存", func() { a.ctx.SetImgKey(tempImgKey) - a.rootPages.RemovePage("form") + a.mainPages.RemovePage("submenu2") a.showInfo("图片密钥已设置") }) formView.AddButton("取消", func() { - a.rootPages.RemovePage("form") + a.mainPages.RemovePage("submenu2") }) - a.rootPages.AddPage("form", formView, true, true) + a.mainPages.AddPage("submenu2", formView, true, true) a.SetFocus(formView) } -// settingDataDir Sets Data Dir +// settingDataDir 设置数据目录 func (a *App) settingDataDir() { + // 使用我们的自定义表单组件 formView := form.NewForm("设置数据目录") + + // 临时存储用户输入的值 tempDataDir := a.ctx.DataDir + // 添加输入字段 - 不直接设置数据目录,而是更新临时变量 formView.AddInputField("数据目录", tempDataDir, 0, nil, func(text string) { - tempDataDir = text + tempDataDir = text // 只更新临时变量 }) + // 添加按钮 - 点击保存时才设置数据目录 formView.AddButton("保存", func() { - a.ctx.DataDir = tempDataDir - a.rootPages.RemovePage("form") + a.ctx.DataDir = tempDataDir // 设置数据目录 + a.mainPages.RemovePage("submenu2") a.showInfo("数据目录已设置为 " + a.ctx.DataDir) }) formView.AddButton("取消", func() { - a.rootPages.RemovePage("form") + a.mainPages.RemovePage("submenu2") }) - a.rootPages.AddPage("form", formView, true, true) + a.mainPages.AddPage("submenu2", formView, true, true) a.SetFocus(formView) } -// selectAccountSelected Handles account switch selection +// selectAccountSelected 处理切换账号菜单项的选择事件 func (a *App) selectAccountSelected(i *menu.Item) { - // Create sub-menu for account selection + // 创建子菜单 subMenu := menu.NewSubMenu("切换账号") - // Add instances + // 添加微信进程 instances := a.m.wechat.GetWeChatInstances() if len(instances) > 0 { - subMenu.AddItem(&menu.Item{Index: 0, Name: "--- 微信进程 ---", Description: "", Hidden: false, Selected: nil}) + // 添加实例标题 + subMenu.AddItem(&menu.Item{ + Index: 0, + Name: "--- 微信进程 ---", + Description: "", + Hidden: false, + Selected: nil, + }) + // 添加实例列表 for idx, instance := range instances { + // 创建一个实例描述 description := fmt.Sprintf("版本: %s 目录: %s", instance.FullVersion, instance.DataDir) + + // 标记当前选中的实例 name := fmt.Sprintf("%s [%d]", instance.Name, instance.PID) if a.ctx.Current != nil && a.ctx.Current.PID == instance.PID { name = name + " [当前]" } + // 创建菜单项 instanceItem := &menu.Item{ Index: idx + 1, Name: name, @@ -749,27 +795,36 @@ func (a *App) selectAccountSelected(i *menu.Item) { Hidden: false, Selected: func(instance *wechat.Account) func(*menu.Item) { return func(*menu.Item) { + // 如果是当前账号,则无需切换 if a.ctx.Current != nil && a.ctx.Current.PID == instance.PID { - a.rootPages.RemovePage("submenu") + a.mainPages.RemovePage("submenu") a.showInfo("已经是当前账号") return } + // 显示切换中的模态框 modal := tview.NewModal().SetText("正在切换账号...") - a.rootPages.AddPage("modal", modal, true, true) + a.mainPages.AddPage("modal", modal, true, true) a.SetFocus(modal) + // 在后台执行切换操作 go func() { err := a.m.Switch(instance, "") + + // 在主线程中更新UI a.QueueUpdateDraw(func() { - a.rootPages.RemovePage("modal") - a.rootPages.RemovePage("submenu") + a.mainPages.RemovePage("modal") + a.mainPages.RemovePage("submenu") + if err != nil { - a.showError(fmt.Errorf("切换账号失败: %v", err)) - } else { - a.showInfo("切换账号成功") - a.updateMenuItemsState() - } + // 切换失败 + a.showError(fmt.Errorf("切换账号失败: %v", err)) + } else { + // 切换成功 + a.showInfo("切换账号成功") + // 更新菜单状态 + a.updateMenuItemsState() + } }) }() } @@ -779,12 +834,24 @@ func (a *App) selectAccountSelected(i *menu.Item) { } } - // Add History + // 添加历史账号 if len(a.ctx.History) > 0 { - subMenu.AddItem(&menu.Item{Index: 100, Name: "--- 历史账号 ---", Description: "", Hidden: false, Selected: nil}) + // 添加历史账号标题 + subMenu.AddItem(&menu.Item{ + Index: 100, + Name: "--- 历史账号 ---", + Description: "", + Hidden: false, + Selected: nil, + }) + + // 添加历史账号列表 idx := 101 for account, hist := range a.ctx.History { + // 创建一个账号描述 description := fmt.Sprintf("版本: %s 目录: %s", hist.FullVersion, hist.DataDir) + + // 标记当前选中的账号 name := account if name == "" { name = filepath.Base(hist.DataDir) @@ -793,6 +860,7 @@ func (a *App) selectAccountSelected(i *menu.Item) { name = name + " [当前]" } + // 创建菜单项 histItem := &menu.Item{ Index: idx, Name: name, @@ -800,27 +868,36 @@ func (a *App) selectAccountSelected(i *menu.Item) { Hidden: false, Selected: func(account string) func(*menu.Item) { return func(*menu.Item) { + // 如果是当前账号,则无需切换 if a.ctx.Current != nil && a.ctx.DataDir == a.ctx.History[account].DataDir { - a.rootPages.RemovePage("submenu") + a.mainPages.RemovePage("submenu") a.showInfo("已经是当前账号") return } + // 显示切换中的模态框 modal := tview.NewModal().SetText("正在切换账号...") - a.rootPages.AddPage("modal", modal, true, true) + a.mainPages.AddPage("modal", modal, true, true) a.SetFocus(modal) + // 在后台执行切换操作 go func() { err := a.m.Switch(nil, account) + + // 在主线程中更新UI a.QueueUpdateDraw(func() { - a.rootPages.RemovePage("modal") - a.rootPages.RemovePage("submenu") + a.mainPages.RemovePage("modal") + a.mainPages.RemovePage("submenu") + if err != nil { - a.showError(fmt.Errorf("切换账号失败: %v", err)) - } else { - a.showInfo("切换账号成功") - a.updateMenuItemsState() - } + // 切换失败 + a.showError(fmt.Errorf("切换账号失败: %v", err)) + } else { + // 切换成功 + a.showInfo("切换账号成功") + // 更新菜单状态 + a.updateMenuItemsState() + } }) }() } @@ -831,35 +908,43 @@ func (a *App) selectAccountSelected(i *menu.Item) { } } + // 如果没有账号可选择 if len(a.ctx.History) == 0 && len(instances) == 0 { - subMenu.AddItem(&menu.Item{Index: 1, Name: "无可用账号", Description: "未检测到微信进程或历史账号", Hidden: false, Selected: nil}) + subMenu.AddItem(&menu.Item{ + Index: 1, + Name: "无可用账号", + Description: "未检测到微信进程或历史账号", + Hidden: false, + Selected: nil, + }) } - a.rootPages.AddPage("submenu", subMenu, true, true) + // 显示子菜单 + a.mainPages.AddPage("submenu", subMenu, true, true) a.SetFocus(subMenu) } -// showModal Shows a modal +// showModal 显示一个模态对话框 func (a *App) showModal(text string, buttons []string, doneFunc func(buttonIndex int, buttonLabel string)) { modal := tview.NewModal(). SetText(text). AddButtons(buttons). SetDoneFunc(doneFunc) - a.rootPages.AddPage("modal", modal, true, true) + a.mainPages.AddPage("modal", modal, true, true) a.SetFocus(modal) } -// showError Shows an error modal +// showError 显示错误对话框 func (a *App) showError(err error) { a.showModal(err.Error(), []string{"OK"}, func(buttonIndex int, buttonLabel string) { - a.rootPages.RemovePage("modal") + a.mainPages.RemovePage("modal") }) } -// showInfo Shows an info modal +// showInfo 显示信息对话框 func (a *App) showInfo(text string) { a.showModal(text, []string{"OK"}, func(buttonIndex int, buttonLabel string) { - a.rootPages.RemovePage("modal") + a.mainPages.RemovePage("modal") }) -} \ No newline at end of file +} diff --git a/internal/ui/dashboard/dashboard.go b/internal/ui/dashboard/dashboard.go deleted file mode 100644 index bed185c..0000000 --- a/internal/ui/dashboard/dashboard.go +++ /dev/null @@ -1,59 +0,0 @@ -package dashboard - -import ( - "fmt" - - "github.com/rivo/tview" - "github.com/sjzar/chatlog/internal/ui/style" -) - -type Dashboard struct { - *tview.Table -} - -func New() *Dashboard { - d := &Dashboard{ - Table: tview.NewTable(), - } - - d.SetBorders(false) - d.SetBorder(true) - d.SetTitle(" 状态概览 ") - d.SetBorderColor(style.BorderColor) - d.SetBackgroundColor(style.BgColor) - - return d -} - -func (d *Dashboard) Update(data map[string]string) { - d.Clear() - - row := 0 - headerColor := style.InfoBarItemFgColor - - keys := []string{ - "Account", "PID", "Status", "ExePath", - "Platform", "Version", "Session", "Data Key", - "Image Key", "Data Usage", "Data Dir", - "Work Usage", "Work Dir", "HTTP Server", "Auto Decrypt", - } - - for _, key := range keys { - val, ok := data[key] - if !ok { - continue - } - - d.SetCell(row, 0, tview.NewTableCell(fmt.Sprintf(" [%s::b]%s", headerColor, key)). - SetAlign(tview.AlignRight). - SetExpansion(1). - SetTextColor(style.FgColor)) - - d.SetCell(row, 1, tview.NewTableCell(fmt.Sprintf(" %s", val)). - SetAlign(tview.AlignLeft). - SetExpansion(3). - SetTextColor(style.FgColor)) - - row++ - } -} diff --git a/internal/ui/infobar/infobar.go b/internal/ui/infobar/infobar.go new file mode 100644 index 0000000..63f1f92 --- /dev/null +++ b/internal/ui/infobar/infobar.go @@ -0,0 +1,230 @@ +package infobar + +import ( + "fmt" + + "github.com/sjzar/chatlog/internal/ui/style" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +const ( + Title = "infobar" +) + +// InfoBarViewHeight info bar height. +const ( + InfoBarViewHeight = 8 + accountRow = 0 + statusRow = 1 + platformRow = 2 + sessionRow = 3 + dataUsageRow = 4 + workUsageRow = 5 + httpServerRow = 6 + autoDecryptRow = 7 + + // 列索引 + labelCol1 = 0 // 第一列标签 + valueCol1 = 1 // 第一列值 + labelCol2 = 2 // 第二列标签 + valueCol2 = 3 // 第二列值 + totalCols = 4 +) + +// InfoBar implements the info bar primitive. +type InfoBar struct { + *tview.Box + title string + table *tview.Table +} + +// NewInfoBar returns info bar view. +func New() *InfoBar { + table := tview.NewTable() + headerColor := style.InfoBarItemFgColor + + // Account 和 PID 行 + table.SetCell( + accountRow, + labelCol1, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Account:")), + ) + table.SetCell(accountRow, valueCol1, tview.NewTableCell("")) + + table.SetCell( + accountRow, + labelCol2, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "PID:")), + ) + table.SetCell(accountRow, valueCol2, tview.NewTableCell("")) + + // Status 和 ExePath 行 + table.SetCell( + statusRow, + labelCol1, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Status:")), + ) + table.SetCell(statusRow, valueCol1, tview.NewTableCell("")) + + table.SetCell( + statusRow, + labelCol2, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "ExePath:")), + ) + table.SetCell(statusRow, valueCol2, tview.NewTableCell("")) + + // Platform 和 Version 行 + table.SetCell( + platformRow, + labelCol1, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Platform:")), + ) + table.SetCell(platformRow, valueCol1, tview.NewTableCell("")) + + table.SetCell( + platformRow, + labelCol2, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Version:")), + ) + table.SetCell(platformRow, valueCol2, tview.NewTableCell("")) + + // Session 和 Data Key 行 + table.SetCell( + sessionRow, + labelCol1, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Session:")), + ) + table.SetCell(sessionRow, valueCol1, tview.NewTableCell("")) + + table.SetCell( + sessionRow, + labelCol2, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Data Key:")), + ) + table.SetCell(sessionRow, valueCol2, tview.NewTableCell("")) + + // Data Usage 和 Data Dir 行 + table.SetCell( + dataUsageRow, + labelCol1, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Data Usage:")), + ) + table.SetCell(dataUsageRow, valueCol1, tview.NewTableCell("")) + + table.SetCell( + dataUsageRow, + labelCol2, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Data Dir:")), + ) + table.SetCell(dataUsageRow, valueCol2, tview.NewTableCell("")) + + // Work Usage 和 Work Dir 行 + table.SetCell( + workUsageRow, + labelCol1, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Work Usage:")), + ) + table.SetCell(workUsageRow, valueCol1, tview.NewTableCell("")) + + table.SetCell( + workUsageRow, + labelCol2, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Work Dir:")), + ) + table.SetCell(workUsageRow, valueCol2, tview.NewTableCell("")) + + // HTTP Server 行 + table.SetCell( + httpServerRow, + labelCol1, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "HTTP Server:")), + ) + table.SetCell(httpServerRow, valueCol1, tview.NewTableCell("")) + + table.SetCell( + httpServerRow, + labelCol2, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Image Key:")), + ) + table.SetCell(httpServerRow, valueCol2, tview.NewTableCell("")) + + table.SetCell( + autoDecryptRow, + labelCol1, + tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Auto Decrypt:")), + ) + table.SetCell(autoDecryptRow, valueCol1, tview.NewTableCell("")) + + // infobar + infoBar := &InfoBar{ + Box: tview.NewBox(), + title: Title, + table: table, + } + + return infoBar +} + +func (info *InfoBar) UpdateAccount(account string) { + info.table.GetCell(accountRow, valueCol1).SetText(account) +} + +func (info *InfoBar) UpdateBasicInfo(pid int, version string, exePath string) { + info.table.GetCell(accountRow, valueCol2).SetText(fmt.Sprintf("%d", pid)) + info.table.GetCell(statusRow, valueCol2).SetText(exePath) + info.table.GetCell(platformRow, valueCol2).SetText(version) +} + +func (info *InfoBar) UpdateStatus(status string) { + info.table.GetCell(statusRow, valueCol1).SetText(status) +} + +func (info *InfoBar) UpdatePlatform(text string) { + info.table.GetCell(platformRow, valueCol1).SetText(text) +} + +func (info *InfoBar) UpdateSession(text string) { + info.table.GetCell(sessionRow, valueCol1).SetText(text) +} + +func (info *InfoBar) UpdateDataKey(key string) { + info.table.GetCell(sessionRow, valueCol2).SetText(key) +} + +func (info *InfoBar) UpdateDataUsageDir(dataUsage string, dataDir string) { + info.table.GetCell(dataUsageRow, valueCol1).SetText(dataUsage) + info.table.GetCell(dataUsageRow, valueCol2).SetText(dataDir) +} + +func (info *InfoBar) UpdateWorkUsageDir(workUsage string, workDir string) { + info.table.GetCell(workUsageRow, valueCol1).SetText(workUsage) + info.table.GetCell(workUsageRow, valueCol2).SetText(workDir) +} + +// UpdateHTTPServer updates HTTP Server value. +func (info *InfoBar) UpdateHTTPServer(server string) { + info.table.GetCell(httpServerRow, valueCol1).SetText(server) +} + +func (info *InfoBar) UpdateImageKey(key string) { + info.table.GetCell(httpServerRow, valueCol2).SetText(key) +} + +// UpdateAutoDecrypt updates Auto Decrypt value. +func (info *InfoBar) UpdateAutoDecrypt(text string) { + info.table.GetCell(autoDecryptRow, valueCol1).SetText(text) +} + +// Draw draws this primitive onto the screen. +func (info *InfoBar) Draw(screen tcell.Screen) { + info.Box.DrawForSubclass(screen, info) + info.Box.SetBorder(false) + + x, y, width, height := info.GetInnerRect() + + info.table.SetRect(x, y, width, height) + info.table.SetBorder(false) + info.table.Draw(screen) +} diff --git a/internal/ui/layout/layout.go b/internal/ui/layout/layout.go deleted file mode 100644 index 67ed18c..0000000 --- a/internal/ui/layout/layout.go +++ /dev/null @@ -1,29 +0,0 @@ -package layout - -import ( - "github.com/rivo/tview" - "github.com/sjzar/chatlog/internal/ui/sidebar" -) - -type Layout struct { - *tview.Flex - Sidebar *sidebar.Sidebar - Pages *tview.Pages -} - -func New(s *sidebar.Sidebar, pages *tview.Pages) *Layout { - l := &Layout{ - Flex: tview.NewFlex(), - Sidebar: s, - Pages: pages, - } - - l.AddItem(s, 20, 0, true). - AddItem(pages, 0, 1, false) - - return l -} - -func (l *Layout) FocusSidebar() { - l.AddItem(l.Sidebar, 20, 0, true) -} diff --git a/internal/ui/logs/logs.go b/internal/ui/logs/logs.go deleted file mode 100644 index fa4723d..0000000 --- a/internal/ui/logs/logs.go +++ /dev/null @@ -1,43 +0,0 @@ -package logs - -import ( - "fmt" - "time" - - "github.com/rivo/tview" - "github.com/sjzar/chatlog/internal/ui/style" -) - -type Logs struct { - *tview.TextView -} - -func New() *Logs { - l := &Logs{ - TextView: tview.NewTextView(), - } - - l.SetDynamicColors(true) - l.SetScrollable(true) - l.SetWrap(true) - l.SetBorder(true) - l.SetTitle(" 日志 ") - l.SetBorderColor(style.BorderColor) - l.SetBackgroundColor(style.BgColor) - l.SetTextColor(style.FgColor) - - return l -} - -func (l *Logs) AddLog(msg string) { - fmt.Fprintf(l, "[%s]%s[white] %s\n", - time.Now().Format("15:04:05"), - "", - msg) - l.ScrollToEnd() -} - -func (l *Logs) Write(p []byte) (n int, err error) { - l.AddLog(string(p)) - return len(p), nil -} diff --git a/internal/ui/menu/menu.go b/internal/ui/menu/menu.go index 766de19..352bb8b 100644 --- a/internal/ui/menu/menu.go +++ b/internal/ui/menu/menu.go @@ -159,4 +159,4 @@ func (l SortItems) Less(i, j int) bool { func (l SortItems) Swap(i, j int) { l[i], l[j] = l[j], l[i] -} +} \ No newline at end of file diff --git a/internal/ui/sidebar/sidebar.go b/internal/ui/sidebar/sidebar.go deleted file mode 100644 index c6bcca7..0000000 --- a/internal/ui/sidebar/sidebar.go +++ /dev/null @@ -1,34 +0,0 @@ -package sidebar - -import ( - "github.com/rivo/tview" - "github.com/sjzar/chatlog/internal/ui/style" -) - -type Sidebar struct { - *tview.List -} - -func New(onSelected func(int, string, string, rune)) *Sidebar { - s := &Sidebar{ - List: tview.NewList(), - } - - s.ShowSecondaryText(false) - s.SetBackgroundColor(style.BgColor) - s.SetMainTextColor(style.FgColor) - s.SetSelectedBackgroundColor(style.MenuBgColor) - s.SetSelectedTextColor(style.PageHeaderFgColor) - s.SetBorder(true) - s.SetTitle(" 导航 ") - s.SetTitleAlign(tview.AlignLeft) - s.SetBorderColor(style.BorderColor) - - s.SetSelectedFunc(onSelected) - - return s -} - -func (s *Sidebar) AddItem(text string, shortcut rune) { - s.List.AddItem(text, "", shortcut, nil) -}