native dialogs
This commit is contained in:
parent
47e7c30604
commit
fda504005e
3 changed files with 87 additions and 73 deletions
4
go.mod
4
go.mod
|
|
@ -6,7 +6,7 @@ require (
|
|||
fyne.io/fyne/v2 v2.6.1
|
||||
github.com/emersion/go-autostart v0.0.0-20250403115856-34830d6457d2
|
||||
github.com/gofrs/flock v0.12.1
|
||||
github.com/gregorybednov/lbc v0.0.0-20250901080446-e7a9e1092be5
|
||||
github.com/gregorybednov/lbc v0.0.0-20250917212825-f35c528ba65f
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
)
|
||||
|
||||
|
|
@ -20,6 +20,7 @@ require (
|
|||
github.com/DataDog/zstd v1.4.5 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf // indirect
|
||||
github.com/Workiva/go-datastructures v1.0.53 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.14.3 // indirect
|
||||
|
|
@ -111,6 +112,7 @@ require (
|
|||
github.com/spf13/cobra v1.8.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.19.0 // indirect
|
||||
github.com/sqweek/dialog v0.0.0-20240226140203-065105509627 // indirect
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
|
|
|
|||
6
go.sum
6
go.sum
|
|
@ -31,6 +31,8 @@ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE
|
|||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf h1:FPsprx82rdrX2jiKyS17BH6IrTmUBYqZa/CXT4uvb+I=
|
||||
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I=
|
||||
github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
|
||||
|
|
@ -231,6 +233,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
|||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregorybednov/lbc v0.0.0-20250901080446-e7a9e1092be5 h1:J8IkR4rM3xQ/p0iCArRA4u3IcqczTvrpIuuKtUd2KCw=
|
||||
github.com/gregorybednov/lbc v0.0.0-20250901080446-e7a9e1092be5/go.mod h1:6JpnLLSnq0Lc/w4kYThFeksLfNGTDbKvpiS4vvWs9is=
|
||||
github.com/gregorybednov/lbc v0.0.0-20250917212825-f35c528ba65f h1:TxTIxFNokRmFrVv97rWEwJqPXUz89UJPG6FwlxkMdbw=
|
||||
github.com/gregorybednov/lbc v0.0.0-20250917212825-f35c528ba65f/go.mod h1:6JpnLLSnq0Lc/w4kYThFeksLfNGTDbKvpiS4vvWs9is=
|
||||
github.com/gregorybednov/lbc_sdk v0.0.0-20250810123844-a90b874431fa h1:8EuqAmsS94ju83o4aEIV8e2fdocdAJ9xVerKRSI+nDA=
|
||||
github.com/gregorybednov/lbc_sdk v0.0.0-20250810123844-a90b874431fa/go.mod h1:DBE00+SaYBtD4qw+nOtSTLuF6h9Ia4TkuBMJB+6krik=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
|
|
@ -388,6 +392,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
|||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/sqweek/dialog v0.0.0-20240226140203-065105509627 h1:2JL2wmHXWIAxDofCK+AdkFi1KEg3dgkefCsm7isADzQ=
|
||||
github.com/sqweek/dialog v0.0.0-20240226140203-065105509627/go.mod h1:/qNPSY91qTz/8TgHEMioAUc6q7+3SOybeKczHMXFcXw=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
|
|
|
|||
130
main.go
130
main.go
|
|
@ -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 {
|
||||
|
||||
savePath, err := simpledialog.File().Filter("JSON files", "json").Title("Экспорт данных о сети").Save()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer uw.Close()
|
||||
|
||||
// открыть исходный файл
|
||||
f, e := os.Open(src)
|
||||
if e != nil {
|
||||
st.alert("Ошибка чтения genesis: " + e.Error())
|
||||
simpledialog.Message("Ошибка чтения genesis: %s", e.Error()).Title("Ошибка").Error()
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
if _, e = io.Copy(uw, f); e != nil {
|
||||
st.alert("Ошибка записи: " + e.Error())
|
||||
|
||||
// создать файл для сохранения
|
||||
uw, e := os.Create(savePath)
|
||||
if e != nil {
|
||||
simpledialog.Message("Ошибка создания файла: %s", e.Error()).Title("Ошибка").Error()
|
||||
return
|
||||
}
|
||||
st.notify("Готово: экспортирован genesis.json")
|
||||
}, w)
|
||||
fd.SetFileName("genesis.json")
|
||||
fd.Show()
|
||||
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
|
||||
filename, err := simpledialog.File().Filter("Genesis JSON file", "json").Load()
|
||||
if err == nil {
|
||||
genesisPathEntry.SetText(filename)
|
||||
}
|
||||
genesisPathEntry.SetText(uc.URI().Path())
|
||||
}, w)
|
||||
fd.Show()
|
||||
|
||||
})
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue