436 lines
10 KiB
Go
436 lines
10 KiB
Go
package chatlog
|
||
|
||
import (
|
||
"fmt"
|
||
"runtime"
|
||
"time"
|
||
|
||
"github.com/sjzar/chatlog/internal/chatlog/ctx"
|
||
"github.com/sjzar/chatlog/internal/ui/footer"
|
||
"github.com/sjzar/chatlog/internal/ui/help"
|
||
"github.com/sjzar/chatlog/internal/ui/infobar"
|
||
"github.com/sjzar/chatlog/internal/ui/menu"
|
||
|
||
"github.com/gdamore/tcell/v2"
|
||
"github.com/rivo/tview"
|
||
)
|
||
|
||
const (
|
||
RefreshInterval = 1000 * time.Millisecond
|
||
)
|
||
|
||
type App struct {
|
||
*tview.Application
|
||
|
||
ctx *ctx.Context
|
||
m *Manager
|
||
stopRefresh chan struct{}
|
||
|
||
// page
|
||
mainPages *tview.Pages
|
||
infoBar *infobar.InfoBar
|
||
tabPages *tview.Pages
|
||
footer *footer.Footer
|
||
|
||
// 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(),
|
||
mainPages: tview.NewPages(),
|
||
infoBar: infobar.New(),
|
||
tabPages: tview.NewPages(),
|
||
footer: footer.New(),
|
||
menu: menu.New("主菜单"),
|
||
help: help.New(),
|
||
}
|
||
|
||
app.initMenu()
|
||
|
||
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.mainPages, true).EnableMouse(false).Run(); err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (a *App) Stop() {
|
||
// 添加一个通道用于停止刷新 goroutine
|
||
if a.stopRefresh != nil {
|
||
close(a.stopRefresh)
|
||
}
|
||
a.Application.Stop()
|
||
}
|
||
|
||
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()
|
||
|
||
for {
|
||
select {
|
||
case <-a.stopRefresh:
|
||
return
|
||
case <-tick.C:
|
||
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.UpdateDataUsageDir(a.ctx.DataUsage, a.ctx.DataDir)
|
||
a.infoBar.UpdateWorkUsageDir(a.ctx.WorkUsage, a.ctx.WorkDir)
|
||
if a.ctx.HTTPEnabled {
|
||
a.infoBar.UpdateHTTPServer(fmt.Sprintf("[green][已启动][white] [%s]", a.ctx.HTTPAddr))
|
||
} else {
|
||
a.infoBar.UpdateHTTPServer("[未启动]")
|
||
}
|
||
|
||
a.Draw()
|
||
}
|
||
}
|
||
}
|
||
|
||
func (a *App) inputCapture(event *tcell.EventKey) *tcell.EventKey {
|
||
|
||
// 如果当前页面不是主页面,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()
|
||
}
|
||
|
||
return event
|
||
}
|
||
|
||
func (a *App) initMenu() {
|
||
getDataKey := &menu.Item{
|
||
Index: 2,
|
||
Name: "获取数据密钥",
|
||
Description: "从进程获取数据密钥",
|
||
Selected: func(i *menu.Item) {
|
||
modal := tview.NewModal()
|
||
if runtime.GOOS == "darwin" {
|
||
modal.SetText("获取数据密钥中...\n预计需要 20 秒左右的时间,期间微信会卡住,请耐心等待")
|
||
} else {
|
||
modal.SetText("获取数据密钥中...")
|
||
}
|
||
a.mainPages.AddPage("modal", modal, true, true)
|
||
a.SetFocus(modal)
|
||
|
||
go func() {
|
||
err := a.m.GetDataKey()
|
||
|
||
// 在主线程中更新UI
|
||
a.QueueUpdateDraw(func() {
|
||
if err != nil {
|
||
// 解密失败
|
||
modal.SetText("获取数据密钥失败: " + err.Error())
|
||
} else {
|
||
// 解密成功
|
||
modal.SetText("获取数据密钥成功")
|
||
}
|
||
|
||
// 添加确认按钮
|
||
modal.AddButtons([]string{"OK"})
|
||
modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||
a.mainPages.RemovePage("modal")
|
||
})
|
||
a.SetFocus(modal)
|
||
})
|
||
}()
|
||
},
|
||
}
|
||
|
||
decryptData := &menu.Item{
|
||
Index: 3,
|
||
Name: "解密数据",
|
||
Description: "解密数据文件",
|
||
Selected: func(i *menu.Item) {
|
||
// 创建一个没有按钮的模态框,显示"解密中..."
|
||
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 {
|
||
// 解密失败
|
||
modal.SetText("解密失败: " + err.Error())
|
||
} else {
|
||
// 解密成功
|
||
modal.SetText("解密数据成功")
|
||
}
|
||
|
||
// 添加确认按钮
|
||
modal.AddButtons([]string{"OK"})
|
||
modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||
a.mainPages.RemovePage("modal")
|
||
})
|
||
a.SetFocus(modal)
|
||
})
|
||
}()
|
||
},
|
||
}
|
||
|
||
httpServer := &menu.Item{
|
||
Index: 4,
|
||
Name: "启动 HTTP 服务",
|
||
Description: "启动本地 HTTP & MCP 服务器",
|
||
Selected: func(i *menu.Item) {
|
||
modal := tview.NewModal()
|
||
|
||
// 根据当前服务状态执行不同操作
|
||
if !a.ctx.HTTPEnabled {
|
||
// HTTP 服务未启动,启动服务
|
||
modal.SetText("正在启动 HTTP 服务...")
|
||
a.mainPages.AddPage("modal", modal, true, true)
|
||
a.SetFocus(modal)
|
||
|
||
// 在后台启动服务
|
||
go func() {
|
||
err := a.m.StartService()
|
||
|
||
// 在主线程中更新UI
|
||
a.QueueUpdateDraw(func() {
|
||
if err != nil {
|
||
// 启动失败
|
||
modal.SetText("启动 HTTP 服务失败: " + err.Error())
|
||
} else {
|
||
// 启动成功
|
||
modal.SetText("已启动 HTTP 服务")
|
||
// 更改菜单项名称
|
||
i.Name = "停止 HTTP 服务"
|
||
i.Description = "停止本地 HTTP & MCP 服务器"
|
||
}
|
||
|
||
// 添加确认按钮
|
||
modal.AddButtons([]string{"OK"})
|
||
modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||
a.mainPages.RemovePage("modal")
|
||
})
|
||
a.SetFocus(modal)
|
||
})
|
||
}()
|
||
} else {
|
||
// HTTP 服务已启动,停止服务
|
||
modal.SetText("正在停止 HTTP 服务...")
|
||
a.mainPages.AddPage("modal", modal, true, true)
|
||
a.SetFocus(modal)
|
||
|
||
// 在后台停止服务
|
||
go func() {
|
||
err := a.m.StopService()
|
||
|
||
// 在主线程中更新UI
|
||
a.QueueUpdateDraw(func() {
|
||
if err != nil {
|
||
// 停止失败
|
||
modal.SetText("停止 HTTP 服务失败: " + err.Error())
|
||
} else {
|
||
// 停止成功
|
||
modal.SetText("已停止 HTTP 服务")
|
||
// 更改菜单项名称
|
||
i.Name = "启动 HTTP 服务"
|
||
i.Description = "启动本地 HTTP & MCP 服务器"
|
||
}
|
||
|
||
// 添加确认按钮
|
||
modal.AddButtons([]string{"OK"})
|
||
modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||
a.mainPages.RemovePage("modal")
|
||
})
|
||
a.SetFocus(modal)
|
||
})
|
||
}()
|
||
}
|
||
},
|
||
}
|
||
|
||
setting := &menu.Item{
|
||
Index: 5,
|
||
Name: "设置",
|
||
Description: "设置应用程序选项",
|
||
Selected: a.settingSelected,
|
||
}
|
||
|
||
a.menu.AddItem(setting)
|
||
a.menu.AddItem(getDataKey)
|
||
a.menu.AddItem(decryptData)
|
||
a.menu.AddItem(httpServer)
|
||
|
||
a.menu.AddItem(&menu.Item{
|
||
Index: 6,
|
||
Name: "退出",
|
||
Description: "退出程序",
|
||
Selected: func(i *menu.Item) {
|
||
a.Stop()
|
||
},
|
||
})
|
||
}
|
||
|
||
// settingItem 表示一个设置项
|
||
type settingItem struct {
|
||
name string
|
||
description string
|
||
action func()
|
||
}
|
||
|
||
func (a *App) settingSelected(i *menu.Item) {
|
||
|
||
settings := []settingItem{
|
||
{
|
||
name: "设置 HTTP 服务端口",
|
||
description: "配置 HTTP 服务监听的端口",
|
||
action: a.settingHTTPPort,
|
||
},
|
||
{
|
||
name: "设置工作目录",
|
||
description: "配置数据解密后的存储目录",
|
||
action: a.settingWorkDir,
|
||
},
|
||
}
|
||
|
||
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() {
|
||
// 实现端口设置逻辑
|
||
// 这里可以使用 tview.InputField 让用户输入端口
|
||
form := tview.NewForm().
|
||
AddInputField("端口", a.ctx.HTTPAddr, 20, nil, func(text string) {
|
||
a.ctx.SetHTTPAddr(text)
|
||
}).
|
||
AddButton("保存", func() {
|
||
a.mainPages.RemovePage("submenu2")
|
||
a.showInfo("HTTP 端口已设置为 " + a.ctx.HTTPAddr)
|
||
}).
|
||
AddButton("取消", func() {
|
||
a.mainPages.RemovePage("submenu2")
|
||
})
|
||
form.SetBorder(true).SetTitle("设置 HTTP 端口")
|
||
|
||
a.mainPages.AddPage("submenu2", form, true, true)
|
||
a.SetFocus(form)
|
||
}
|
||
|
||
// settingWorkDir 设置工作目录
|
||
func (a *App) settingWorkDir() {
|
||
// 实现工作目录设置逻辑
|
||
form := tview.NewForm().
|
||
AddInputField("工作目录", a.ctx.WorkDir, 40, nil, func(text string) {
|
||
a.ctx.SetWorkDir(text)
|
||
}).
|
||
AddButton("保存", func() {
|
||
a.mainPages.RemovePage("submenu2")
|
||
a.showInfo("工作目录已设置为 " + a.ctx.WorkDir)
|
||
}).
|
||
AddButton("取消", func() {
|
||
a.mainPages.RemovePage("submenu2")
|
||
})
|
||
form.SetBorder(true).SetTitle("设置工作目录")
|
||
|
||
a.mainPages.AddPage("submenu2", form, true, true)
|
||
a.SetFocus(form)
|
||
}
|
||
|
||
// 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.mainPages.AddPage("modal", modal, true, true)
|
||
a.SetFocus(modal)
|
||
}
|
||
|
||
// showError 显示错误对话框
|
||
func (a *App) showError(err error) {
|
||
a.showModal(err.Error(), []string{"OK"}, func(buttonIndex int, buttonLabel string) {
|
||
a.mainPages.RemovePage("modal")
|
||
})
|
||
}
|
||
|
||
// showInfo 显示信息对话框
|
||
func (a *App) showInfo(text string) {
|
||
a.showModal(text, []string{"OK"}, func(buttonIndex int, buttonLabel string) {
|
||
a.mainPages.RemovePage("modal")
|
||
})
|
||
}
|