native dialogs

This commit is contained in:
Gregory Bednov 2025-09-21 15:18:18 +03:00
commit fda504005e
3 changed files with 87 additions and 73 deletions

150
main.go
View file

@ -13,25 +13,22 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/widget"
simpledialog "github.com/sqweek/dialog"
_ "embed"
"github.com/emersion/go-autostart"
"github.com/gofrs/flock"
"github.com/gregorybednov/lbc/cli"
toml "github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2"
)
//go:embed icon.png
var iconPNG []byte
func appIcon() fyne.Resource {
return fyne.NewStaticResource("icon.png", iconPNG)
}
/* --------------------------- Конфиг лончера --------------------------- */
type LauncherCfg struct {
@ -101,8 +98,18 @@ func (w *uiWriter) Write(p []byte) (int, error) {
func main() {
st := &AppState{}
must(initConfig(st))
must(acquireSingleInstanceLock(st))
err := initConfig(st)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
err = acquireSingleInstanceLock(st)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
defer releaseSingleInstanceLock(st)
exe, _ := os.Executable()
@ -115,6 +122,7 @@ func main() {
a := app.New()
w := a.NewWindow("LBС Launcher")
st.app, st.win = a, w
// Статус + логи
@ -124,18 +132,15 @@ func main() {
st.logArea.Disable() // только для чтения
homeEntry := widget.NewEntry()
homeEntry.SetPlaceHolder("Каталог HOME узла (будут data/, config/)")
homeEntry.SetPlaceHolder("Полный путь до каталога (там будут данные и настройки)")
homeEntry.SetText(st.cfg.NodeHome)
btnPickHome := widget.NewButton("Выбрать HOME…", func() {
dd := dialog.NewFolderOpen(func(lu fyne.ListableURI, err error) {
if lu == nil || err != nil {
return
}
st.cfg.NodeHome = lu.Path()
btnPickHome := widget.NewButton("Выбрать каталог…", func() {
directory, err := simpledialog.Directory().Title("Выбрать каталог с настройками и данными").Browse()
if err == nil {
st.cfg.NodeHome = directory
homeEntry.SetText(st.cfg.NodeHome)
_ = saveConfig(st)
}, w)
dd.Show()
}
})
topControls := container.NewVBox(
@ -151,7 +156,7 @@ func main() {
go func() {
w := &uiWriter{st}
if err := cli.ExecuteWithArgs([]string{"init", "genesis"}, homeEntry.Text, w, w); err != nil {
st.alert("Ошибка: " + err.Error())
simpledialog.Message("%s", err.Error()).Title("Произошла ошибка").Error()
} else {
st.notify("init genesis: готово")
}
@ -170,33 +175,45 @@ func main() {
exportBtn := widget.NewButton("Экспортировать genesis.json…", func() {
// genesis лежит в HOME/config/genesis.json
if homeEntry.Text == "" {
st.alert("Сначала укажи HOME каталóg узла.")
simpledialog.Message("%s", "Сначала укажите домашний каталог узла").Title("Не найден HOME").Error()
return
}
src := filepath.Join(homeEntry.Text, "config", "genesis.json")
if _, err := os.Stat(src); err != nil {
st.alert("Не найден файл: " + src)
simpledialog.Message("%s", "Не найден файл"+src).Title("Файл не найден").Error()
return
}
fd := dialog.NewFileSave(func(uw fyne.URIWriteCloser, err error) {
if uw == nil || err != nil {
return
}
defer uw.Close()
f, e := os.Open(src)
if e != nil {
st.alert("Ошибка чтения genesis: " + e.Error())
return
}
defer f.Close()
if _, e = io.Copy(uw, f); e != nil {
st.alert("Ошибка записи: " + e.Error())
return
}
st.notify("Готово: экспортирован genesis.json")
}, w)
fd.SetFileName("genesis.json")
fd.Show()
savePath, err := simpledialog.File().Filter("JSON files", "json").Title("Экспорт данных о сети").Save()
if err != nil {
return
}
// открыть исходный файл
f, e := os.Open(src)
if e != nil {
simpledialog.Message("Ошибка чтения genesis: %s", e.Error()).Title("Ошибка").Error()
return
}
defer f.Close()
// создать файл для сохранения
uw, e := os.Create(savePath)
if e != nil {
simpledialog.Message("Ошибка создания файла: %s", e.Error()).Title("Ошибка").Error()
return
}
defer uw.Close()
// копируем содержимое
if _, e = io.Copy(uw, f); e != nil {
simpledialog.Message("Ошибка записи: %s", e.Error()).Title("Ошибка").Error()
return
}
st.notify(fmt.Sprintf("Готово: экспортирован genesis.json -> %s", savePath))
//fd.SetFileName("genesis.json")
//fd.Show()
})
tabExportGenesis := container.NewVBox(
widget.NewLabel("Экспорт genesis.json из текущего узла"),
@ -207,13 +224,11 @@ func main() {
genesisPathEntry := widget.NewEntry()
genesisPathEntry.SetPlaceHolder("Путь к существующему genesis.json")
pickGenesis := widget.NewButton("Выбрать genesis.json…", func() {
fd := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
if uc == nil || err != nil {
return
}
genesisPathEntry.SetText(uc.URI().Path())
}, w)
fd.Show()
filename, err := simpledialog.File().Filter("Genesis JSON file", "json").Load()
if err == nil {
genesisPathEntry.SetText(filename)
}
})
joinBtn := widget.NewButton("Создать узел-присоединение (init joiner)", func() {
if !st.ensurePathsOk(homeEntry.Text) {
@ -222,21 +237,21 @@ func main() {
// 1) скопировать genesis.json в HOME/config/genesis.json
src := genesisPathEntry.Text
if src == "" {
st.alert("Укажи путь к genesis.json")
simpledialog.Message("%s", "Укажите путь к genesis.json").Title("Не найден файл").Error()
return
}
dest := filepath.Join(homeEntry.Text, "config")
_ = os.MkdirAll(dest, 0o755)
destFile := filepath.Join(dest, "genesis.json")
if err := fileCopy(src, destFile); err != nil {
st.alert("Не удалось скопировать genesis.json: " + err.Error())
simpledialog.Message("%s", "Не удалось скопировать genesis.json: "+err.Error()).Title("Ошибка генерации").Error()
return
}
// 2) lbc init joiner
go func() {
w := &uiWriter{st}
if err := cli.ExecuteWithArgs([]string{"init", "joiner", genesisPathEntry.Text}, homeEntry.Text, w, w); err != nil {
st.alert("Ошибка: " + err.Error())
simpledialog.Message("%s", err.Error()).Title("Произошла ошибка").Error()
} else {
st.notify("init joiner: готово")
}
@ -258,12 +273,12 @@ func main() {
loadCfgBtn := widget.NewButton("Загрузить config.toml", func() {
if homeEntry.Text == "" {
st.alert("Укажи HOME узла.")
simpledialog.Message("%s", "Укажите домашнюю директорию узла").Title("Ошибка сохранения").Error()
return
}
cfgp := filepath.Join(homeEntry.Text, "config", "config.toml")
if cfg, err := loadTOML(cfgp); err != nil {
st.alert("Чтение TOML: " + err.Error())
simpledialog.Message("%s", err.Error()).Title("Ошибка чтения TOML").Error()
} else {
cfgPathLbl.SetText("Файл: " + cfgp)
monikerEntry.SetText(cfg.Moniker)
@ -278,13 +293,13 @@ func main() {
})
saveCfgBtn := widget.NewButton("Сохранить", func() {
if homeEntry.Text == "" {
st.alert("Укажи HOME узла.")
simpledialog.Message("%s", "Укажите домашнюю директорию узла").Title("Ошибка сохранения").Error()
return
}
cfgp := filepath.Join(homeEntry.Text, "config", "config.toml")
cfg, err := loadTOML(cfgp) // мягко читаем всё, меняем только интересующие поля
cfg, err := loadTOML(cfgp)
if err != nil {
st.alert("Чтение TOML: " + err.Error())
simpledialog.Message("%s", err.Error()).Title("Ошибка чтения TOML").Error()
return
}
cfg.Moniker = monikerEntry.Text
@ -292,7 +307,7 @@ func main() {
cfg.P2P.PersistentPeers = ppeersEntry.Text
cfg.LogLevel = logLevelSel.Selected
if err := saveTOML(cfgp, cfg); err != nil {
st.alert("Запись TOML: " + err.Error())
simpledialog.Message("%s", err.Error()).Title("Ошибка записи TOML").Error()
return
}
st.notify("config.toml сохранён")
@ -326,7 +341,7 @@ func main() {
cbAutostart := widget.NewCheck("Запускать при входе в систему", func(v bool) {
if v {
if err := autoApp.Enable(); err != nil {
st.alert("Автозапуск: " + err.Error())
simpledialog.Message("%s", err.Error()).Title("Произошла ошибка автозапуска").Error()
return
}
} else {
@ -352,7 +367,7 @@ func main() {
// Трей-меню
if desk, ok := a.(desktop.App); ok {
desk.SetSystemTrayIcon(appIcon())
desk.SetSystemTrayIcon(fyne.NewStaticResource("icon.png", iconPNG))
openItem := fyne.NewMenuItem("Открыть", func() { w.Show(); w.RequestFocus() })
autoItem := fyne.NewMenuItem("Автозапуск", nil)
autoItem.Checked = autoApp.IsEnabled()
@ -360,7 +375,7 @@ func main() {
ns := !autoItem.Checked
if ns {
if err := autoApp.Enable(); err != nil {
st.alert(err.Error())
simpledialog.Message("%s", err.Error()).Title("Произошла ошибка").Error()
return
}
} else {
@ -370,7 +385,7 @@ func main() {
st.cfg.AutoStart = ns
_ = saveConfig(st)
}
quitItem := fyne.NewMenuItem("Выйти", func() { releaseSingleInstanceLock(st); a.Quit() })
quitItem := fyne.NewMenuItem("Выход", func() { releaseSingleInstanceLock(st); a.Quit() })
desk.SetSystemTrayMenu(fyne.NewMenu("", openItem, autoItem, fyne.NewMenuItemSeparator(), quitItem))
}
@ -503,21 +518,21 @@ func (st *AppState) appendLog(s string) {
st.logArea.CursorRow = 999999
}()
}
func (st *AppState) alert(msg string) {
dialog.ShowInformation("Сообщение", msg, st.win)
}
func (st *AppState) notify(msg string) {
st.app.SendNotification(&fyne.Notification{Title: "LBClient", Content: msg})
}
func (st *AppState) ensurePathsOk(home string) bool {
if home == "" {
st.alert("Укажи HOME каталог узла")
simpledialog.Message("%s", "Каталог узла не найден").Title("Не найден HOME").Error()
return false
}
st.cfg.NodeHome = home
_ = saveConfig(st)
return true
}
func fileCopy(src, dst string) error {
in, err := os.Open(src)
if err != nil {
@ -537,12 +552,3 @@ func fileCopy(src, dst string) error {
}
return out.Close()
}
/* --------------------------------- misc -------------------------------- */
func must(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}