package ui import ( "fmt" "go_dreamfactory/cmd/v2/lib/common" os_storage "go_dreamfactory/cmd/v2/lib/storage" "go_dreamfactory/cmd/v2/service" "go_dreamfactory/cmd/v2/service/observer" "image/color" "os/exec" "path/filepath" "runtime" "strings" "sync" "github.com/pkg/errors" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "github.com/sirupsen/logrus" "github.com/spf13/cast" ) type appTerm struct { appAdapter obs observer.Observer sshService *service.SSHService jsonList *common.ItemList //json列表 cProgress *widget.ProgressBarInfinite //连接进度条进度条 upProgress *widget.ProgressBar //上传进度条 endProgress sync.WaitGroup downloadList *common.ItemList //download列表 } func (app *appTerm) LazyInit(ptService service.PttService, obs observer.Observer) error { app.obs = obs app.sshService = &service.SSHService{} app.jsonList = common.NewItemList() //progress app.cProgress = widget.NewProgressBarInfinite() app.cProgress.Hide() app.upProgress = widget.NewProgressBar() app.upProgress.Hide() app.tabItem = container.NewTabItemWithIcon(common.TOOLBAR_TERM, theme.MailSendIcon(), nil) content := container.NewMax() content.Objects = []fyne.CanvasObject{} //load term conf storage, _ := os_storage.NewOSStorage() conf, err := storage.LoadConfig() if err != nil { return err } //left-top localDir := widget.NewEntry() localDir.PlaceHolder = `本地Json目录` remoteDir := widget.NewEntry() remoteDir.PlaceHolder = `远程目录` remoteDir.Text = "/home/liwei/go_dreamfactory/bin/json" //config ip := widget.NewEntry() ip.PlaceHolder = "10.0.0.9" port := widget.NewEntry() port.Text = "22" //default port port.PlaceHolder = "端口" userName := widget.NewEntry() userName.PlaceHolder = "账号" password := widget.NewPasswordEntry() password.PlaceHolder = "密码" passwordItem := &widget.FormItem{Text: "密码:", Widget: password} configForm := widget.NewForm( &widget.FormItem{Text: "IP:", Widget: container.NewBorder(nil, nil, nil, port, ip)}, &widget.FormItem{Text: "用户名:", Widget: userName}, passwordItem, widget.NewFormItem("Json目录", container.NewBorder(nil, nil, nil, widget.NewButtonWithIcon("", theme.FolderIcon(), func() { openFolder(localDir, toolWin.w) }), localDir)), widget.NewFormItem("远程目录", remoteDir), ) configForm.Items[4].HintText = "谨慎修改远程目录" //svn form lubanAddr := widget.NewEntry() lubanAddr.PlaceHolder = "Luban服务IP" workDir := widget.NewEntry() workDir.PlaceHolder = "工作目录" lubanCli := widget.NewEntry() lubanCli.PlaceHolder = "luban客户端可执行文件" //define define := widget.NewEntry() define.PlaceHolder = "定义文件" define.Text = `Defines\\__root__.xml` dataDir := widget.NewEntry() dataDir.PlaceHolder = "Data目录" jsonDir := widget.NewEntry() jsonDir.PlaceHolder = "Json目录" svnForm := widget.NewForm( &widget.FormItem{Text: "服务地址", Widget: lubanAddr}, &widget.FormItem{Text: "工作目录", Widget: container.NewBorder(nil, nil, nil, widget.NewButtonWithIcon("", theme.FolderIcon(), func() { openFolder(workDir, toolWin.w) }), workDir)}, &widget.FormItem{Text: "LubanCli", Widget: lubanCli}, &widget.FormItem{Text: "Data目录", Widget: dataDir}, &widget.FormItem{Text: "JSON目录", Widget: jsonDir}, ) //set input entry if conf.SyncConf != nil { ip.Text = conf.SyncConf.Ip port.Text = conf.SyncConf.Port userName.Text = conf.SyncConf.UserName password.Text = conf.SyncConf.Password localDir.Text = conf.SyncConf.LocalDir remoteDir.Text = conf.SyncConf.RemoteDir // // lubanAddr.Text = conf.SyncConf.ServerIp workDir.Text = conf.SyncConf.WorkDir lubanCli.Text = conf.SyncConf.LubanCli dataDir.Text = conf.SyncConf.DataDir jsonDir.Text = conf.SyncConf.JsonDir } else { conf.SyncConf = &os_storage.SyncConfig{ Ip: "10.0.0.9", UserName: "root", Password: "Legu.cc()123", RemoteDir: "/home/liwei/go_dreamfactory/bin/json/", ServerIp: "10.0.1.11", LubanCli: `Luban.Client\Luban.Client.exe`, DataDir: "Datas", JsonDir: `output\json`, } } // 解决文本没显示的问题 passwordItem.Widget.Refresh() ip.Refresh() localDir.Refresh() workDir.Refresh() // save func saveFunc := func() { syncConf := &os_storage.SyncConfig{ Ip: ip.Text, UserName: userName.Text, Password: password.Text, Port: port.Text, LocalDir: localDir.Text, RemoteDir: remoteDir.Text, ServerIp: lubanAddr.Text, WorkDir: workDir.Text, LubanCli: lubanCli.Text, DataDir: dataDir.Text, JsonDir: jsonDir.Text, } conf.SyncConf = syncConf if err := storage.StoreConfig(conf); err != nil { logrus.Error(err) } } saveBtn1 := widget.NewButtonWithIcon("保存配置", theme.DocumentSaveIcon(), saveFunc) saveBtn2 := widget.NewButtonWithIcon("保存配置", theme.DocumentSaveIcon(), saveFunc) // conn func connBtn := &widget.Button{Text: "连接", Icon: theme.MailForwardIcon()} disConnBtn := &widget.Button{Text: "断开", Icon: theme.CancelIcon()} syncBtn := &widget.Button{Text: "同步JSON", Icon: theme.ConfirmIcon()} refreshBtn := &widget.Button{Text: "刷新", Icon: theme.ViewRefreshIcon()} svnBtn := &widget.Button{Text: "SVN更新", Icon: theme.ConfirmIcon()} excelBtn := &widget.Button{Text: "更新Excel", Icon: theme.FolderIcon()} explorBtn := &widget.Button{Text: "资源管理器", Icon: theme.FolderIcon()} dlBtn := widget.NewButtonWithIcon("", theme.DownloadIcon(), nil) searchEntry := widget.NewEntry() // 全选/全取消 allSelBtn := &widget.Button{Icon: theme.CheckButtonCheckedIcon()} allCancelBtn := &widget.Button{Icon: theme.CheckButtonIcon()} allSelBtn.Hide() // 连接 connBtn.OnTapped = func() { app.cProgress.Show() app.cProgress.Start() ciphers := []string{} if ip.Text == "" { dialog.ShowError(errors.New("IP未填写"), toolWin.w) return } if userName.Text == "" { dialog.ShowError(errors.New("账号未填写"), toolWin.w) return } if password.Text == "" { dialog.ShowError(errors.New("密码未填写"), toolWin.w) return } if port.Text == "" { dialog.ShowError(errors.New("端口未填写"), toolWin.w) return } if localDir.Text == "" { dialog.ShowError(errors.New("JSON目录未填写"), toolWin.w) return } if remoteDir.Text == "" { dialog.ShowError(errors.New("远程目录未填写"), toolWin.w) return } connBtn.Disable() err := app.sshService.Connect(userName.Text, password.Text, ip.Text, "", cast.ToInt(port.Text), ciphers) if err != nil { dialog.ShowError(err, toolWin.w) app.cProgress.Stop() app.cProgress.Hide() connBtn.Enable() return } else { app.jsonList.LoadItem(localDir.Text) app.cProgress.Stop() app.cProgress.Hide() disConnBtn.Enable() syncBtn.Enable() app.upProgress.Hide() allCancelBtn.Show() allSelBtn.Hide() refreshBtn.Enable() dlBtn.Enable() } } // 刷新JSON列表 refreshBtn.Disable() reloadItem := func() { defer func() { syncBtn.Enable() allSelBtn.Hide() allCancelBtn.Show() }() app.jsonList.LoadItem(localDir.Text) } refreshBtn.OnTapped = reloadItem // 断开 disConnBtn.Disable() disConnBtn.OnTapped = func() { defer func() { app.jsonList.Reset() connBtn.Enable() disConnBtn.Disable() syncBtn.Disable() allSelBtn.Hide() allCancelBtn.Show() refreshBtn.Disable() dlBtn.Disable() }() app.sshService.Close() } //资源管理器 openExplor := func(dir string) { if runtime.GOOS == "windows" { if err := exec.Command("explorer", filepath.Join(dir)).Start(); err != nil { dialog.ShowError(errors.WithMessage(err, "请确认Json目录是否填写正确"), toolWin.w) return } } } explorBtn.OnTapped = func() { // logrus.Debug(localDir.Text) if localDir.Text == "" { common.ShowTip("Json目录必须填写") } else { openExplor(localDir.Text) } } //使用说明 helpBtn1 := widget.NewButtonWithIcon("", theme.QuestionIcon(), func() { quesWin := fyne.CurrentApp().NewWindow("同步JSON使用说明") quesWin.SetContent(widget.NewRichTextFromMarkdown( ` * 同步Json的操作仅限于数值热更,非结构热更 * 将json文件热更到内网测试环境 * 提示: * IP:10.0.0.9 22 (保持一致) * 用户名:root(保持一致) * 密码:(服务端同学提供) * Json目录:(json文件目录全路径) * 远程目录:/home/liwei/go_dreamfactory/bin/json/(保持一致) * 全部填写完点击【保存配置】 * 点击【连接】,右侧面板选择要同步的文件 `)) quesWin.Resize(fyne.NewSize(350, 200)) quesWin.SetFixedSize(true) quesWin.CenterOnScreen() quesWin.Show() }) helpBtn2 := widget.NewButtonWithIcon("", theme.QuestionIcon(), func() { quesWin := fyne.CurrentApp().NewWindow("SVN更新使用说明") c := widget.NewRichTextFromMarkdown( ` * 基于Luban工具生成json * 建议每次同步JSON之前先点击SVN更新 * 提示: * 服务地址:10.0.1.11 (保持一致) * 工作目录:svn下ExcelFile目录全路径 * LubanCli:Luban.Client\Luban.Client.exe (保持一致) * Data目录:Datas(保持一致) * JSON目录:output\json(保持一致) * 全部填写完点击【保存配置】 * 点击【SVN更新】生成json文件 `, ) quesWin.SetContent(c) quesWin.Resize(fyne.NewSize(350, 200)) quesWin.SetFixedSize(true) quesWin.CenterOnScreen() quesWin.Show() }) //同步JSON syncBtn.Disable() syncNext := func() { defer func() { syncBtn.Enable() app.upProgress.Hide() reloadItem() dialog.ShowInformation("提示", "所有文件均上传完毕,需等1-2分钟待文件热更", toolWin.w) }() syncBtn.Disable() app.upProgress.Show() app.upProgress.SetValue(0) len := len(app.jsonList.SelItemIds) num := 0.0 increment := func(wg *sync.WaitGroup) { num += float64(1) / float64(len) app.upProgress.SetValue(num) wg.Done() } for _, fileName := range app.jsonList.SelItemIds { app.endProgress.Add(1) go func(fn string) { // time.Sleep(time.Second * time.Duration(rand.Intn(3))) if err := app.sshService.ScpCopy(filepath.Join(localDir.Text, fn), remoteDir.Text); err != nil { logrus.WithField("err", err).Error("同步json") common.ShowTip(err.Error()) } increment(&app.endProgress) // 移除已上传的 app.jsonList.DeleteItem(fn) common.ShowTip(fmt.Sprintf("%s 成功上传", fn)) }(fileName) } app.endProgress.Wait() app.upProgress.SetValue(1) } syncBtn.OnTapped = func() { if app.sshService.Client == nil { dialog.ShowError(errors.New("请先连接终端"), toolWin.w) return } //SVN更新提示 dc := dialog.NewConfirm("提示", "是否要进行SVN更新?", func(b bool) { if b { common.ShowTip("单击【SVN更新】按钮进行更新") svnBtn.FocusGained() return } else { if len(app.jsonList.SelItemIds) == 0 { common.ShowTip("没有选择任何文件,或尝试点击【刷新】") return } syncNext() } }, toolWin.w) dc.SetConfirmText("必须的") dc.SetDismissText("我拿生命担保无需更新") dc.Show() } // excel更新 excelBtn.OnTapped = func() { if workDir.Text == "" { common.ShowTip("工作目录必须填写") } else { openExplor(workDir.Text) } } // SVN更新 svnNext := func() { defer func() { app.cProgress.Hide() app.cProgress.Stop() svnBtn.Enable() }() svnBtn.Disable() app.cProgress.Show() app.cProgress.Start() commandStr := `%s -h %s -j cfg -- -d %s --input_data_dir %s --output_data_dir %s --gen_types data_json -s server` arg := fmt.Sprintf(commandStr, filepath.Join(workDir.Text, lubanCli.Text), lubanAddr.Text, filepath.Join(workDir.Text, define.Text), filepath.Join(workDir.Text, dataDir.Text), filepath.Join(workDir.Text, jsonDir.Text), ) logrus.Debug(arg) c := exec.Command("cmd.exe", "/c", arg) if err := c.Run(); err != nil { dialog.ShowError(err, toolWin.w) return } } svnBtn.OnTapped = func() { //提示更新excel dc := dialog.NewConfirm("提示", "是否要更新excel?", func(b bool) { if b { //打开资源管理器 openExplor(workDir.Text) return } else { svnNext() } }, toolWin.w) dc.SetConfirmText("必须的") dc.SetDismissText("我拿生命担保无需更新") dc.Show() } // 全部取消 allCancelBtn.OnTapped = func() { defer func() { allCancelBtn.Hide() allSelBtn.Show() }() for i, v := range app.jsonList.CachedList.Items { app.jsonList.CachedList.Items[i].Checked = true app.jsonList.SelItemIds = append(app.jsonList.SelItemIds, v.Text) app.jsonList.ItemList.UpdateItem(i, widget.NewCheck(v.Text, nil)) } app.jsonList.ItemList.Refresh() } // 全选 allSelBtn.OnTapped = func() { defer func() { allCancelBtn.Show() allSelBtn.Hide() }() app.jsonList.SelItemIds = []string{} for i, v := range app.jsonList.CachedList.Items { app.jsonList.CachedList.Items[i].Checked = false app.jsonList.ItemList.UpdateItem(i, widget.NewCheck(v.Text, nil)) } app.jsonList.ItemList.Refresh() } // 搜索 searchEntry.PlaceHolder = "搜索" searchEntry.OnChanged = func(s string) { if app.sshService.Client == nil { dialog.ShowError(errors.New("请先连接终端"), toolWin.w) return } if s == "" { reloadItem() } else { // go func() { newList := []common.Item{} for _, v := range app.jsonList.SearchItem { if strings.Contains(v.Text, s) { newList = append(newList, v) } } app.jsonList.CachedList.Items = newList app.jsonList.ItemList.Refresh() // }() } } // 下载日志 dlBtn.Disable() dlBtn.OnTapped = func() { w := app.createDownloadWindow() w.Show() w.SetCloseIntercept(func() { dlBtn.Enable() w.Close() }) dlBtn.Disable() } // 创建json列表 app.jsonList.ItemList = app.jsonList.CreateDefaultCheckList() btns1 := container.NewHBox(helpBtn1, dlBtn, &layout.Spacer{}, saveBtn1, connBtn, disConnBtn) btns2 := container.NewHBox(helpBtn2, &layout.Spacer{}, saveBtn2, excelBtn, svnBtn) split := container.NewHSplit(container.NewVBox(canvas.NewText("---只能在非上产环境!!!同步Json的操作仅限于数值热更,非结构热更---", color.RGBA{255, 0, 0, 255}), configForm, btns1, svnForm, btns2, app.cProgress), container.NewBorder( container.NewBorder(nil, nil, container.NewHBox(allCancelBtn, allSelBtn, syncBtn, refreshBtn), container.NewHBox(explorBtn), searchEntry), nil, nil, nil, app.jsonList.ItemList)) split.Offset = 0.45 content.Objects = append(content.Objects, container.NewBorder(nil, app.upProgress, nil, nil, split)) app.tabItem.Content = content return nil } func (a *appTerm) GetAppName() string { return common.TOOLBAR_TERM } func (a *appTerm) Icon() fyne.Resource { return theme.MailSendIcon() } func OpenExplor(dir string) { if dir == "" { common.ShowTip("资源管理器路径错误") return } if runtime.GOOS == "windows" { if err := exec.Command("explorer", filepath.Join(dir)).Start(); err != nil { dialog.ShowError(errors.WithMessage(err, "请确认Json目录是否填写正确"), toolWin.w) return } } } func (a *appTerm) OnClose() bool { dialog.ShowConfirm("关闭终端", "你希望断开连接吗?", func(b bool) { if !b { return } if a.sshService.Client != nil { a.sshService.Client.Close() } }, toolWin.w) return true } func (a *appTerm) createDownloadWindow() fyne.Window { w := toolWin.app.NewWindow("日志") a.downloadList = common.NewItemList() a.downloadList.ItemList = a.downloadList.CreateDownloadList() remoteLogDirEntry := widget.NewEntry() remoteLogDirEntry.PlaceHolder = "下载到" remoteLogDirEntry.Text = "/home/liwei/go_dreamfactory/bin/log/" saveDirEntry := widget.NewEntry() saveDirEntry.PlaceHolder = "保存目录" editorEntry := widget.NewEntry() editorEntry.PlaceHolder = "编辑器可执行文件路径" // config confForm := widget.NewForm( widget.NewFormItem("远程目录", remoteLogDirEntry), widget.NewFormItem("下载目录", saveDirEntry), widget.NewFormItem("编辑器", container.NewBorder(nil, nil, nil, widget.NewButtonWithIcon("", theme.FolderIcon(), func() { openFile(editorEntry, w) }), editorEntry)), ) // 进度条 downloadProgress := widget.NewProgressBarInfinite() var ( storage os_storage.Storage conf *os_storage.Config err error ) defer func() { downloadProgress.Hide() //加载配置 storage, _ = os_storage.NewOSStorage() conf, err = storage.LoadConfig() if err != nil { return } // sshConf = service.GetDbService().GetSSHConf(common.BUCKET_SSHCONF) if conf.SyncConf != nil { if conf.SyncConf.LogDir == "" { remoteLogDirEntry.Text = "/home/liwei/go_dreamfactory/bin/log/" } else { remoteLogDirEntry.Text = conf.SyncConf.LogDir } saveDirEntry.Text = conf.SyncConf.SaveDir editorEntry.Text = conf.SyncConf.Editor } else { conf.SyncConf = &os_storage.SyncConfig{} } remoteLogDirEntry.Refresh() saveDirEntry.Refresh() editorEntry.Refresh() //load list if remoteLogDirEntry.Text == "" { return } files, err := a.sshService.GetRemoteDir(remoteLogDirEntry.Text) if err != nil { dialog.ShowError(err, toolWin.w) return } for _, f := range files { a.downloadList.AddItem(common.Item{Id: f.FileName, Text: f.FileName, Size: f.Size}) } }() //下载 allDownloadBtn := widget.NewButtonWithIcon("下载", theme.DownloadIcon(), nil) DownloadDirBtn := widget.NewButtonWithIcon("打开下载目录", theme.FolderIcon(), nil) EditorBtn := widget.NewButtonWithIcon("打开编辑器", theme.FolderIcon(), nil) saveConfBtn := widget.NewButtonWithIcon("保存配置", theme.DocumentSaveIcon(), func() { // 保存配置 conf.SyncConf.LogDir = remoteLogDirEntry.Text conf.SyncConf.SaveDir = saveDirEntry.Text conf.SyncConf.Editor = editorEntry.Text if err := storage.StoreConfig(conf); err != nil { logrus.Error(err) } }) allSelBtn := &widget.Button{Icon: theme.CheckButtonCheckedIcon()} allCancelBtn := &widget.Button{Icon: theme.CheckButtonIcon()} allSelBtn.Hide() // 全部取消 allCancelBtn.OnTapped = func() { defer func() { allCancelBtn.Hide() allSelBtn.Show() }() dlist := a.downloadList for i, v := range dlist.CachedList.Items { dlist.CachedList.Items[i].Checked = true dlist.SelItemIds = append(dlist.SelItemIds, v.Text) dlist.ItemList.UpdateItem(i, container.NewHBox( widget.NewCheck(v.Text, nil), &layout.Spacer{}, widget.NewLabel(common.ConvertFileSize(v.Size)))) } dlist.ItemList.Refresh() } // 全选 allSelBtn.OnTapped = func() { defer func() { allCancelBtn.Show() allSelBtn.Hide() }() dlist := a.downloadList dlist.SelItemIds = []string{} for i, v := range dlist.CachedList.Items { dlist.CachedList.Items[i].Checked = false dlist.ItemList.UpdateItem(i, container.NewHBox( widget.NewCheck(v.Text, nil), &layout.Spacer{}, widget.NewLabel(common.ConvertFileSize(v.Size)))) } dlist.ItemList.Refresh() } // 打开下载目录 DownloadDirBtn.OnTapped = func() { OpenExplor(saveDirEntry.Text) } EditorBtn.OnTapped = func() { if editorEntry.Text == "" { common.ShowTip("请配置编辑器") return } if runtime.GOOS == "windows" { for _, v := range a.downloadList.SelItemIds { if err := exec.Command(editorEntry.Text, filepath.Join(saveDirEntry.Text, v)).Start(); err != nil { dialog.ShowError(errors.WithMessage(err, "请确认编辑器目录是否填写正确"), toolWin.w) return } } } } //使用说明 helpBtn := widget.NewButtonWithIcon("", theme.QuestionIcon(), func() { quesWin := fyne.CurrentApp().NewWindow("日志使用说明") quesWin.SetContent(widget.NewRichTextFromMarkdown( ` * 下载远程服务日志到本地分析 * 提示: * 远程目录 (保持一致) * 下载目录:填写本地任意目录 * 编辑器:选择已安装的编辑器(例如:notepad++、Sublime) * 操作: * 选择文件,点击【打开编辑器】 `)) quesWin.Resize(fyne.NewSize(350, 200)) quesWin.SetFixedSize(true) quesWin.CenterOnScreen() quesWin.Show() }) var wg sync.WaitGroup //download allDownloadBtn.OnTapped = func() { downloadProgress.Show() downloadProgress.Start() selItems := a.downloadList.SelItemIds if len(selItems) == 0 { common.ShowTip("请选择下载的文件") return } else { for _, item := range selItems { wg.Add(1) go func(name string) { defer func() { downloadProgress.Hide() wg.Done() }() logrus.WithField("filepath", saveDirEntry.Text+name).Debug("下载") if err := a.sshService.ScpDownload(saveDirEntry.Text, remoteLogDirEntry.Text+name); err != nil { common.ShowTip(name + " 下载失败") } }(item) } wg.Wait() } } btns := container.NewHBox(allSelBtn, allCancelBtn, allDownloadBtn, DownloadDirBtn, EditorBtn, helpBtn) //toolbar toolbar := container.NewBorder(nil, btns, nil, saveConfBtn, confForm) //layout w.SetContent(container.NewBorder(toolbar, downloadProgress, nil, nil, a.downloadList.ItemList)) w.Resize(fyne.NewSize(800, 450)) w.CenterOnScreen() return w }