package gui import ( "os/exec" "runtime" "stream-bot/internal/logger" "time" "github.com/lxn/walk" . "github.com/lxn/walk/declarative" ) const appVersion = "11.0.43" func Run(webURL string, getChatStatus func() bool, getEventSubStatus func() (connected bool, subscriptions []string)) { if runtime.GOOS != "windows" { logger.Warn("GUI only supported on Windows, running without window") return } var urlLE *walk.LineEdit var openBtn *walk.PushButton var exitBtn *walk.PushButton var chatStatusLabel, eventSubLabel *walk.Label var mw *walk.MainWindow exitWithoutMinimize := false // флаг для полного закрытия err := MainWindow{ AssignTo: &mw, Title: "TTW_Bot v" + appVersion, MinSize: Size{Width: 450, Height: 280}, Size: Size{Width: 500, Height: 300}, Layout: VBox{}, Children: []Widget{ Label{Text: "Веб-интерфейс бота доступен по адресу:"}, LineEdit{ AssignTo: &urlLE, Text: webURL, ReadOnly: true, }, PushButton{ AssignTo: &openBtn, Text: "🌐 Открыть в браузере", OnClicked: func() { openBrowser(webURL) }, }, Label{Text: "─────────────────────────────────"}, Label{Text: "Статус Twitch чата:"}, Label{AssignTo: &chatStatusLabel, Text: "загрузка..."}, Label{Text: "Статус EventSub:"}, Label{AssignTo: &eventSubLabel, Text: "загрузка..."}, Label{Text: "─────────────────────────────────"}, PushButton{ AssignTo: &exitBtn, Text: "❌ Завершить работу бота", OnClicked: func() { exitWithoutMinimize = true // запоминаем, что хотим выйти _ = mw.Close() }, }, }, }.Create() if err != nil { msg := "Failed to create GUI window: " + err.Error() logger.Error(msg) walk.MsgBox(nil, "Ошибка", msg, walk.MsgBoxIconError) return } // При закрытии окна – либо сворачиваем в трей, либо завершаем программу mw.Closing().Attach(func(cancel *bool, reason walk.CloseReason) { if exitWithoutMinimize { // полное завершение – не отменяем закрытие return } // иначе – сворачиваем в трей *cancel = true mw.Hide() }) // Создаём иконку в трее ni, err := walk.NewNotifyIcon(mw) if err != nil { logger.Error("Failed to create notify icon: %v", err) } else { _ = ni.SetToolTip("TTW_Bot") // Устанавливаем иконку (системная иконка информации) if err := ni.SetIcon(walk.IconInformation()); err != nil { logger.Warn("Failed to set icon: %v", err) } showAction := walk.NewAction() _ = showAction.SetText("Показать окно") showAction.Triggered().Attach(func() { mw.Show() mw.SetVisible(true) }) exitAction := walk.NewAction() _ = exitAction.SetText("Выход") exitAction.Triggered().Attach(func() { exitWithoutMinimize = true _ = mw.Close() }) menu := ni.ContextMenu() _ = menu.Actions().Add(showAction) _ = menu.Actions().Add(exitAction) ni.MouseDown().Attach(func(x, y int, button walk.MouseButton) { if button == walk.LeftButton { mw.Show() mw.SetVisible(true) } }) if err := ni.SetVisible(true); err != nil { logger.Error("Failed to set tray icon visible: %v", err) } } // Запускаем периодическое обновление статусов ticker := time.NewTicker(5 * time.Second) go func() { for range ticker.C { mw.Synchronize(func() { chatConnected := getChatStatus() if chatConnected { _ = chatStatusLabel.SetText("✅ Подключен") } else { _ = chatStatusLabel.SetText("❌ Отключен") } esConnected, subs := getEventSubStatus() if esConnected { _ = eventSubLabel.SetText("✅ Подключен (" + itoa(len(subs)) + " подписок)") } else { _ = eventSubLabel.SetText("❌ Отключен") } }) } }() mw.Run() ticker.Stop() } func openBrowser(url string) { var cmd *exec.Cmd switch runtime.GOOS { case "windows": cmd = exec.Command("cmd", "/c", "start", url) case "darwin": cmd = exec.Command("open", url) default: cmd = exec.Command("xdg-open", url) } if err := cmd.Start(); err != nil { logger.Error("Failed to open browser: %v", err) walk.MsgBox(nil, "Ошибка", "Не удалось открыть браузер: "+err.Error(), walk.MsgBoxIconError) } } func itoa(i int) string { if i == 0 { return "0" } digits := "" for i > 0 { digits = string(rune('0'+i%10)) + digits i /= 10 } return digits }