The very basic client app, not adapted for mobiles.
This commit is contained in:
parent
b65b8a9e72
commit
e3b9c9ae40
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*DS_Store*
|
||||
fyne-cross
|
||||
.build
|
6
FyneApp.toml
Normal file
6
FyneApp.toml
Normal file
@ -0,0 +1,6 @@
|
||||
Website = "https://bunker.pztrn.name"
|
||||
|
||||
[Details]
|
||||
Icon = "Icon.png"
|
||||
Name = "pztrn's Bunker"
|
||||
ID = "name.pztrn.bunker.client"
|
29
Taskfile.yml
Normal file
29
Taskfile.yml
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
version: "3"
|
||||
|
||||
vars:
|
||||
VERSION:
|
||||
sh: scripts/version_generator.sh
|
||||
CLIENT_VERSION:
|
||||
sh: scripts/version_generator.sh client
|
||||
BRANCH:
|
||||
sh: git rev-parse --abbrev-ref HEAD
|
||||
COMMIT:
|
||||
sh: git rev-parse --short HEAD
|
||||
BUILD:
|
||||
sh: git rev-list --count HEAD
|
||||
BUILD_DATE:
|
||||
sh: TZ=UTC date +'%Y-%m-%d %H:%M:%S %Z'
|
||||
BASIC_LDFLAGS: "-X 'bunker/commons/constants.Version={{.VERSION}}' -X 'bunker/commons/constants.Branch={{.BRANCH}}' -X 'bunker/commons/constants.Commit={{.COMMIT}}' -X 'bunker/commons/constants.Build={{.BUILD}}' -X 'bunker/commons/constants.BuildDate={{.BUILD_DATE}}'"
|
||||
|
||||
env:
|
||||
GOFLAGS: "-trimpath"
|
||||
|
||||
includes:
|
||||
client: ./client
|
||||
|
||||
tasks:
|
||||
ensure-builddir:
|
||||
internal: true
|
||||
cmds:
|
||||
- mkdir -p .build
|
6
client/Taskfile.yml
Normal file
6
client/Taskfile.yml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
version: "3"
|
||||
|
||||
includes:
|
||||
client: ./cmd/client
|
||||
translations_checker: ./cmd/translations_checker
|
88
client/cmd/client/Taskfile.yml
Normal file
88
client/cmd/client/Taskfile.yml
Normal file
@ -0,0 +1,88 @@
|
||||
---
|
||||
version: "3"
|
||||
|
||||
vars:
|
||||
ClientBinary: name.pztrn.bunker.client{{exeExt}}
|
||||
MetadataParams: --metadata "Branch={{ .BRANCH }}" --metadata "Build={{ .BUILD }}" --metadata "BuildDate={{ .BUILD_DATE }}" --metadata "Commit={{ .COMMIT }}" --metadata "Version={{ .VERSION }}"
|
||||
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: "Build client."
|
||||
dir: ./client/cmd/client
|
||||
internal: true
|
||||
cmds:
|
||||
- task: ::ensure-builddir
|
||||
- task: cleanup
|
||||
- fyne build --release -o ../../../.build/{{ .ClientBinary }} --pprof --pprof-port 6060 {{ .MetadataParams }}
|
||||
|
||||
build-debug:
|
||||
desc: "Build client in debug mode."
|
||||
internal: true
|
||||
dir: ./client/cmd/client
|
||||
cmds:
|
||||
- task: ::ensure-builddir
|
||||
- task: cleanup
|
||||
- fyne build -o ../../../.build/{{ .ClientBinary }} --tags debug {{ .MetadataParams }}
|
||||
|
||||
# build-production:
|
||||
# desc: "Build production package for current OS."
|
||||
# dir: ./client/cmd/client
|
||||
# cmds:
|
||||
# - task: ::ensure-builddir
|
||||
# - task: cleanup
|
||||
# - fyne package --release --app-id name.pztrn.bunker.client -executable name.pztrn.bunker.client -icon ../../Icon.png --release --name "pztrn's Bunker" --app-version "{{ .CLIENT_VERSION }}" --app-build "{{ .BUILD }}" {{ .MetadataParams }}
|
||||
|
||||
# build-web:
|
||||
# desc: "Build web version."
|
||||
# dir: ./client/cmd/client
|
||||
# cmds:
|
||||
# - fyne package -os web --app-id name.pztrn.bunker.client -icon ../../Icon.png --release --name "pztrn's Bunker" --app-version "{{ .CLIENT_VERSION }}" --app-build "{{ .BUILD }}" {{ .MetadataParams }}
|
||||
|
||||
build-darwin-amd64:
|
||||
desc: "Build darwin/arm64 using fyne-cross"
|
||||
cmds:
|
||||
- task: ::ensure-builddir
|
||||
- task: cleanup
|
||||
- fyne-cross darwin -app-version="{{ .CLIENT_VERSION }}" -app-build="{{ .BUILD }}" -arch=amd64 -category=6007 -pull client/cmd/client
|
||||
|
||||
build-darwin-arm64:
|
||||
desc: "Build darwin/arm64 using fyne-cross"
|
||||
cmds:
|
||||
- task: ::ensure-builddir
|
||||
- task: cleanup
|
||||
- fyne-cross darwin -app-version="{{ .CLIENT_VERSION }}" -app-build="{{ .BUILD }}" -arch=arm64 -category=6007 -pull client/cmd/client
|
||||
|
||||
build-linux-amd64:
|
||||
desc: "Build linux/amd64 using fyne-cross"
|
||||
cmds:
|
||||
- task: ::ensure-builddir
|
||||
- task: cleanup
|
||||
- fyne-cross linux -app-id=name.pztrn.bunker.client -app-version="{{ .CLIENT_VERSION }}" -app-build="{{ .BUILD }}" -icon=client/Icon.png -arch=amd64 -name="pztrn's Bunker" -pull client/cmd/client
|
||||
|
||||
build-windows-amd64:
|
||||
desc: "Build windows/amd64 using fyne-cross"
|
||||
cmds:
|
||||
- task: ::ensure-builddir
|
||||
- task: cleanup
|
||||
- fyne-cross windows -app-id=name.pztrn.bunker.client -app-version="{{ .CLIENT_VERSION }}" -app-build="{{ .BUILD }}" -icon=client/Icon.png -arch=amd64 -name="pztrn's Bunker" -pull client/cmd/client
|
||||
|
||||
cleanup:
|
||||
desc: "Cleanup build environment."
|
||||
cmds:
|
||||
- rm .build/{{ .ClientBinary }}
|
||||
ignore_error: true
|
||||
|
||||
run:
|
||||
desc: "Launch client."
|
||||
cmds:
|
||||
- task: build
|
||||
- .build/{{ .ClientBinary }}
|
||||
|
||||
run-debug:
|
||||
desc: "Launch client in debug mode."
|
||||
cmds:
|
||||
- task: build-debug
|
||||
- .build/{{ .ClientBinary }}
|
69
client/cmd/client/main.go
Normal file
69
client/cmd/client/main.go
Normal file
@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"bunker/client/internal/application"
|
||||
"bunker/client/internal/services/core/database"
|
||||
"bunker/client/internal/services/core/mainwindow"
|
||||
"bunker/client/internal/services/core/options"
|
||||
"bunker/client/internal/services/core/translations"
|
||||
"bunker/commons"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
func main() {
|
||||
slog.Info("Starting Bunker client...")
|
||||
|
||||
_ = slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
|
||||
app := application.New()
|
||||
|
||||
checkError(translations.Initialize(app))
|
||||
checkError(database.Initialize(app))
|
||||
checkError(options.Initialize(app))
|
||||
checkError(mainwindow.Initialize(app))
|
||||
|
||||
checkError(app.Start())
|
||||
}
|
||||
|
||||
func checkError(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fyneApp := app.NewWithID(commons.ClientAppID)
|
||||
|
||||
wnd := fyneApp.NewWindow("pztrn's Bunker - Error occurred!")
|
||||
//nolint:mnd
|
||||
wnd.Resize(fyne.NewSize(550, 300))
|
||||
|
||||
lbl := widget.NewLabel("Error appeared while starting pztrn's Bunker:")
|
||||
lbl.Wrapping = fyne.TextWrapWord
|
||||
|
||||
errLabel := widget.NewLabel(err.Error())
|
||||
errLabel.Wrapping = fyne.TextWrapWord
|
||||
|
||||
lbl2 := widget.NewLabel("Please, report this to developer!")
|
||||
lbl2.Wrapping = fyne.TextWrapWord
|
||||
|
||||
copyToClipboardButton := widget.NewButton("Copy to clipboard", func() {
|
||||
fyneApp.Clipboard().SetContent(err.Error())
|
||||
})
|
||||
|
||||
wnd.SetContent(container.NewVBox(
|
||||
lbl,
|
||||
errLabel,
|
||||
lbl2,
|
||||
copyToClipboardButton,
|
||||
))
|
||||
|
||||
wnd.ShowAndRun()
|
||||
|
||||
os.Exit(1)
|
||||
}
|
26
client/cmd/translations_checker/Taskfile.yml
Normal file
26
client/cmd/translations_checker/Taskfile.yml
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
version: "3"
|
||||
|
||||
vars:
|
||||
TCClientBinary: name.pztrn.bunker.app.translations_checker{{exeExt}}
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: "Build translations checker."
|
||||
dir: ./client/cmd/translations_checker
|
||||
cmds:
|
||||
- task: ::ensure-builddir
|
||||
- task: cleanup
|
||||
- go build -o ../../../.build/{{ .TCClientBinary }} main.go
|
||||
|
||||
cleanup:
|
||||
desc: "Cleanup build environment for translations checker."
|
||||
cmds:
|
||||
- rm .build/{{ .TCClientBinary }}
|
||||
ignore_error: true
|
||||
|
||||
run:
|
||||
desc: "Launch translations checker."
|
||||
cmds:
|
||||
- task: build
|
||||
- .build/{{ .TCClientBinary }} -translations-path client/internal/langfiles/files
|
128
client/cmd/translations_checker/main.go
Normal file
128
client/cmd/translations_checker/main.go
Normal file
@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
exitCodeFailedToReadEnglishFile = 3
|
||||
exitCodeFailedToReadTranslationsFile = 6
|
||||
exitCodeFailedToNormalizePath = 2
|
||||
exitCodeFailedToUnmarshalEnglishFile = 4
|
||||
exitCodeFailedToUnmarshalTranslationsFile = 7
|
||||
exitCodeFailedToWalkTranslationsPath = 5
|
||||
exitCodeNoTranslationsPath = 1
|
||||
)
|
||||
|
||||
var translationsPath string
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&translationsPath, "translations-path", "", "Path to translations JSON files.")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if translationsPath == "" {
|
||||
slog.Error("No -translations-path specified.")
|
||||
os.Exit(exitCodeNoTranslationsPath)
|
||||
}
|
||||
|
||||
slog.Info(
|
||||
"Checking translations file for missing translations, using 'en.json' as original source...",
|
||||
"path", translationsPath,
|
||||
)
|
||||
|
||||
dirPath, err := filepath.Abs(translationsPath)
|
||||
if err != nil {
|
||||
slog.Error("Failed to normalize translations files path!", "error", err.Error())
|
||||
os.Exit(exitCodeFailedToNormalizePath)
|
||||
}
|
||||
|
||||
sourceDataAsBytes, err := os.ReadFile(filepath.Join(dirPath, "en.json"))
|
||||
if err != nil {
|
||||
slog.Error("Failed to read 'en.json' file!", "error", err.Error())
|
||||
os.Exit(exitCodeFailedToReadEnglishFile)
|
||||
}
|
||||
|
||||
sourceData := make(map[string]string)
|
||||
|
||||
if err := json.Unmarshal(sourceDataAsBytes, &sourceData); err != nil {
|
||||
slog.Error("Failed to unmarshal 'en.json' file!", "error", err.Error())
|
||||
os.Exit(exitCodeFailedToUnmarshalEnglishFile)
|
||||
}
|
||||
|
||||
langFilesToCheck := make([]string, 0)
|
||||
|
||||
if err := filepath.WalkDir(dirPath, func(path string, _ fs.DirEntry, _ error) error {
|
||||
if strings.Contains(path, ".json") && !strings.Contains(path, "en.json") {
|
||||
langFilesToCheck = append(langFilesToCheck, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
slog.Error("Failed to walk translations path!", "error", err.Error())
|
||||
os.Exit(exitCodeFailedToWalkTranslationsPath)
|
||||
}
|
||||
|
||||
slog.Info("Got langfiles to check", "count", len(langFilesToCheck))
|
||||
|
||||
missingTranslations := make(map[string][]string)
|
||||
|
||||
for _, langFile := range langFilesToCheck {
|
||||
slog.Info("Checking language file...", "file", langFile)
|
||||
|
||||
langFileBytes, err := os.ReadFile(langFile)
|
||||
if err != nil {
|
||||
slog.Error("Failed to read language file!", "file", langFile, "error", err.Error())
|
||||
os.Exit(exitCodeFailedToReadTranslationsFile)
|
||||
}
|
||||
|
||||
langFileData := make(map[string]string)
|
||||
|
||||
if err := json.Unmarshal(langFileBytes, &langFileData); err != nil {
|
||||
slog.Error("Failed to parse language file!", "file", langFile, "error", err.Error())
|
||||
os.Exit(exitCodeFailedToUnmarshalTranslationsFile)
|
||||
}
|
||||
|
||||
for originalTranslation := range sourceData {
|
||||
if _, found := langFileData[originalTranslation]; !found {
|
||||
if _, langFound := missingTranslations[langFile]; !langFound {
|
||||
missingTranslations[langFile] = make([]string, 0)
|
||||
}
|
||||
|
||||
missingTranslations[langFile] = append(missingTranslations[langFile], originalTranslation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingTranslations) == 0 {
|
||||
slog.Info("Yay, no missing translations!")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
for langFile, missing := range missingTranslations {
|
||||
_, fileName := filepath.Split(langFile)
|
||||
|
||||
slog.Info("Found missing translations for file.", "file", fileName)
|
||||
|
||||
sort.Strings(missing)
|
||||
|
||||
//nolint:forbidigo
|
||||
fmt.Println("============================== " + fileName + " MISSING START")
|
||||
|
||||
for _, name := range missing {
|
||||
//nolint:forbidigo
|
||||
fmt.Println(name)
|
||||
}
|
||||
|
||||
//nolint:forbidigo
|
||||
fmt.Println("============================== " + fileName + " MISSING END")
|
||||
}
|
||||
}
|
214
client/internal/application/application.go
Normal file
214
client/internal/application/application.go
Normal file
@ -0,0 +1,214 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
errApplication = errors.New("application")
|
||||
errNoMainWindow = errors.New("no main window service registered")
|
||||
)
|
||||
|
||||
// Application is a lifecycle controlling structure for application.
|
||||
type Application struct {
|
||||
fyneApp fyne.App
|
||||
services []Service
|
||||
}
|
||||
|
||||
// New creates new instance of lifecycle controlling structure.
|
||||
func New() *Application {
|
||||
appl := &Application{}
|
||||
|
||||
appl.initialize()
|
||||
|
||||
return appl
|
||||
}
|
||||
|
||||
func (a *Application) configure() error {
|
||||
// First iteration - core services.
|
||||
for _, service := range a.services {
|
||||
if !strings.Contains(service.Name(), "core/") {
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Debug("Launching configuration procedure for service", "service", service.Name())
|
||||
|
||||
if err := service.Configure(); err != nil {
|
||||
return fmt.Errorf("configure service '%s': %w", service.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Second iteration - rest of the services.
|
||||
for _, service := range a.services {
|
||||
if strings.Contains(service.Name(), "core/") {
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Debug("Launching configuration procedure for service", "service", service.Name())
|
||||
|
||||
if err := service.Configure(); err != nil {
|
||||
return fmt.Errorf("configure service '%s': %w", service.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) connectDependencies() error {
|
||||
// First iteration - core services.
|
||||
for _, service := range a.services {
|
||||
if !strings.Contains(service.Name(), "core/") {
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Debug("Connecting dependencies for service.", "service", service.Name())
|
||||
|
||||
if err := service.ConnectDependencies(); err != nil {
|
||||
return fmt.Errorf("connect dependencies for service '%s': %w", service.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Second iteration - rest of the services.
|
||||
for _, service := range a.services {
|
||||
if strings.Contains(service.Name(), "core/") {
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Debug("Connecting dependencies for service.", "service", service.Name())
|
||||
|
||||
if err := service.ConnectDependencies(); err != nil {
|
||||
return fmt.Errorf("connect dependencies for service '%s': %w", service.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContextWithTimeout returns context.Context with requested timeout.
|
||||
func (a *Application) ContextWithTimeout(timeout time.Duration) context.Context {
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
||||
|
||||
// As we do not need to call cancelFunc - make linter happy.
|
||||
// This probably will lead to context leak, so it should be investigated.
|
||||
go func(_ context.CancelFunc) {}(cancelFunc)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (a *Application) initialize() {
|
||||
a.services = make([]Service, 0)
|
||||
|
||||
a.initializeFyne()
|
||||
}
|
||||
|
||||
func (a *Application) launchStartupTasks() error {
|
||||
for _, service := range a.services {
|
||||
if strings.Contains(service.Name(), "mainwindow") {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(service.Name(), "core/") {
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Debug("Launching startup tasks for service.", "service", service.Name())
|
||||
|
||||
if err := service.LaunchStartupTasks(); err != nil {
|
||||
return fmt.Errorf("launch startup tasks for core/%s: %w", service.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, service := range a.services {
|
||||
if strings.Contains(service.Name(), "core/") {
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Debug("Launching startup tasks for service.", "service", service.Name())
|
||||
|
||||
if err := service.LaunchStartupTasks(); err != nil {
|
||||
return fmt.Errorf("launch startup tasks for core/%s: %w", service.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
var mainWindowService Service
|
||||
|
||||
for _, srv := range a.services {
|
||||
if srv.Name() == "core/mainwindow" {
|
||||
mainWindowService = srv
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if mainWindowService == nil {
|
||||
return fmt.Errorf("launch startup tasks: %w", errNoMainWindow)
|
||||
}
|
||||
|
||||
if err := mainWindowService.LaunchStartupTasks(); err != nil {
|
||||
return fmt.Errorf("launch startup tasks for %s: %w", mainWindowService.Name(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown stops application
|
||||
func (a *Application) Shutdown() error {
|
||||
slog.Info("Stopping pztrn's Bunker...")
|
||||
|
||||
// Сначала тушим фичи.
|
||||
for _, service := range a.services {
|
||||
if !strings.Contains(service.Name(), "features/") {
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Debug("Shutting down service.", "service", service.Name())
|
||||
|
||||
if err := service.Shutdown(); err != nil {
|
||||
return fmt.Errorf("shutting down service '%s': %w", service.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Потом тушим ядро.
|
||||
for _, service := range a.services {
|
||||
if !strings.Contains(service.Name(), "core/") {
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Debug("Shutting down service.", "service", service.Name())
|
||||
|
||||
if err := service.Shutdown(); err != nil {
|
||||
return fmt.Errorf("shutting down service '%s': %w", service.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start запускает приложение.
|
||||
func (a *Application) Start() error {
|
||||
if err := a.connectDependencies(); err != nil {
|
||||
return fmt.Errorf("%w: %w", errApplication, err)
|
||||
}
|
||||
|
||||
if err := a.configure(); err != nil {
|
||||
return fmt.Errorf("%w: %w", errApplication, err)
|
||||
}
|
||||
|
||||
if err := a.launchStartupTasks(); err != nil {
|
||||
return fmt.Errorf("%w: %w", errApplication, err)
|
||||
}
|
||||
|
||||
a.fyneApp.Run()
|
||||
|
||||
return nil
|
||||
}
|
17
client/internal/application/fyne.go
Normal file
17
client/internal/application/fyne.go
Normal file
@ -0,0 +1,17 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"bunker/commons"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
)
|
||||
|
||||
// Fyne возвращает экземпляр Fyne для взаимодействия с ним.
|
||||
func (a *Application) Fyne() fyne.App {
|
||||
return a.fyneApp
|
||||
}
|
||||
|
||||
func (a *Application) initializeFyne() {
|
||||
a.fyneApp = app.NewWithID(commons.ClientAppID)
|
||||
}
|
80
client/internal/application/service.go
Normal file
80
client/internal/application/service.go
Normal file
@ -0,0 +1,80 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrServiceAlreadyRegistered returns if trying to register a service with name already taken by other service.
|
||||
ErrServiceAlreadyRegistered = errors.New("service with such name already registered")
|
||||
// ErrServiceNotFound returns if trying to gather service with unknown name.
|
||||
ErrServiceNotFound = errors.New("service with such name wasn't found")
|
||||
)
|
||||
|
||||
// Service is an interface every service should conform to. Specific services will have own interface for
|
||||
// cross-service interation.
|
||||
type Service interface {
|
||||
// Configure configures service. Called after ConnectDependencies and before LaunchStartupTasks.
|
||||
Configure() error
|
||||
// ConnectDependencies gets neccessary dependencies.
|
||||
ConnectDependencies() error
|
||||
// Initialize initializes service's internal state. Called while registering service with Application
|
||||
// lifecycle controller.
|
||||
Initialize() error
|
||||
// Name returns service name.
|
||||
Name() string
|
||||
// LaunchStartupTasks launches tasks on application start. Called after ConnectDependencies and Configure.
|
||||
LaunchStartupTasks() error
|
||||
// Shutdown stops service.
|
||||
Shutdown() error
|
||||
}
|
||||
|
||||
// RegisterService registering service with lifecycle controller for later use in any other service.
|
||||
func (a *Application) RegisterService(srv Service) error {
|
||||
var found bool
|
||||
|
||||
for _, knownService := range a.services {
|
||||
if srv.Name() == knownService.Name() {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
return fmt.Errorf(
|
||||
"%w: RegisterService: check for service '%s' registration: %w",
|
||||
errApplication,
|
||||
srv.Name(),
|
||||
ErrServiceAlreadyRegistered,
|
||||
)
|
||||
}
|
||||
|
||||
if err := srv.Initialize(); err != nil {
|
||||
return fmt.Errorf("%w: RegisterService: initialize service '%s': %w", errApplication, srv.Name(), err)
|
||||
}
|
||||
|
||||
a.services = append(a.services, srv)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Service returns requested service.
|
||||
func (a *Application) Service(name string) Service {
|
||||
var srv Service
|
||||
|
||||
for _, knownService := range a.services {
|
||||
if knownService.Name() == name {
|
||||
srv = knownService
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if srv == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
13
client/internal/helpers/sqlx.go
Normal file
13
client/internal/helpers/sqlx.go
Normal file
@ -0,0 +1,13 @@
|
||||
package helpers
|
||||
|
||||
import "strings"
|
||||
|
||||
// IsConstraintFailed возвращает true если ограничение по индексу было нарушено.
|
||||
func IsConstraintFailed(err error) bool {
|
||||
return strings.Contains(err.Error(), "constraint failed")
|
||||
}
|
||||
|
||||
// IsEmptySliceForIn возвращает true если sqlx вернул ошибку подготовки слайла параметров в sqlx.In.
|
||||
func IsEmptySliceForIn(err error) bool {
|
||||
return strings.Contains(err.Error(), "empty slice passed to 'in' query")
|
||||
}
|
30
client/internal/langfiles/files/en.json
Normal file
30
client/internal/langfiles/files/en.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"about_dialog.about_tab": "About",
|
||||
"about_dialog.about_tab.copyright": "Copyright © {{ .Years }}, Stanislav Nikitin (a.k.a. pztrn). All rights reserved.",
|
||||
"about_dialog.about_tab.summary": "pztrn's Bunker is a self-hosted family or company intranet.",
|
||||
"about_dialog.licenses_tab": "Licenses",
|
||||
"about_dialog.licenses_tab.dependency.fyne": "Cross platform GUI toolkit in Go inspired by Material Design.",
|
||||
"about_dialog.licenses_tab.dependency.goose": "A database migration tool.",
|
||||
"about_dialog.licenses_tab.dependency.gopsutil": "psutil for golang. Used for getting information about Bunker's process and machine it launched on.",
|
||||
"about_dialog.licenses_tab.dependency.modernc_sqlite": "CGo-free port of SQLite/SQLite3.\n\nSQLite is an in-process implementation of a self-contained, serverless, zero-configuration, transactional SQL database engine.",
|
||||
"about_dialog.licenses_tab.dependency.sqlx": "General purpose extensions to golang's database/sql.",
|
||||
"about_dialog.licenses_tab.summary": "This software uses these third-party libraries:",
|
||||
"about_dialog.close_button": "Close",
|
||||
"about_dialog.sysinfo_tab": "System info",
|
||||
"about_dialog.sysinfo_tab.copy_to_clipboard.notification.content": "Copied to clipboard!",
|
||||
"about_dialog.sysinfo_tab.copy_to_clipboard.notification.title": "System information data",
|
||||
"about_dialog.sysinfo_tab.summary": "This tab contains system information that will be needed for bug reporting and features requests.",
|
||||
"about_dialog.title": "About pztrn's Bunker",
|
||||
"about_dialog.version": "Version",
|
||||
"main_menu.about": "?",
|
||||
"main_menu.about.about_bunker_menu_item": "About pztrn's Bunker...",
|
||||
"main_menu.file": "File",
|
||||
"main_menu.file.exit_menu_item": "Exit",
|
||||
"main_menu.file.options_menu_item": "Options",
|
||||
"options_dialog.no_settings": "No settings found for selected section.",
|
||||
"options_dialog.select_section": "Select section on left to see settings.",
|
||||
"search.placeholder": "Enter text to search and press ENTER.",
|
||||
"window.lorem_ipsum.tab_name": "Welcome!",
|
||||
"window.lorem_ipsum.text": "Welcome to pztrn's Bunker!\n\nUnfortunately, something went wrong and while application was successfully initialized and started you see this message.\n\nPlease, restart application using terminal and send logs to developer!",
|
||||
"window.title": "pztrn's Bunker"
|
||||
}
|
30
client/internal/langfiles/files/ru.json
Normal file
30
client/internal/langfiles/files/ru.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"about_dialog.about_tab": "О программе",
|
||||
"about_dialog.about_tab.copyright": "Copyright © {{ .Years }}, Stanislav Nikitin (a.k.a. pztrn). Все права защищены.",
|
||||
"about_dialog.about_tab.summary": "pztrn's Bunker это интранет для вашей семьи или компании.",
|
||||
"about_dialog.licenses_tab": "Лицензии",
|
||||
"about_dialog.licenses_tab.dependency.fyne": "Cross platform GUI toolkit in Go inspired by Material Design.",
|
||||
"about_dialog.licenses_tab.dependency.goose": "A database migration tool.",
|
||||
"about_dialog.licenses_tab.dependency.gopsutil": "psutil for golang. Used for getting information about Bunker's process and machine it launched on.",
|
||||
"about_dialog.licenses_tab.dependency.modernc_sqlite": "CGo-free port of SQLite/SQLite3.\n\nSQLite is an in-process implementation of a self-contained, serverless, zero-configuration, transactional SQL database engine.",
|
||||
"about_dialog.licenses_tab.dependency.sqlx": "General purpose extensions to golang's database/sql.",
|
||||
"about_dialog.licenses_tab.summary": "Это приложение использует следующие сторонние библиотеки:",
|
||||
"about_dialog.close_button": "Закрыть",
|
||||
"about_dialog.sysinfo_tab": "Информация о системе",
|
||||
"about_dialog.sysinfo_tab.copy_to_clipboard.notification.content": "Скопировано в буфер обмена!",
|
||||
"about_dialog.sysinfo_tab.copy_to_clipboard.notification.title": "Информация о системе",
|
||||
"about_dialog.sysinfo_tab.summary": "Эта вкладка содержит информацию, которую необходимо предоставить разработчикам при сообщении об ошибке или при запросе новой функциональности.",
|
||||
"about_dialog.title": "О pztrn's Bunker",
|
||||
"about_dialog.version": "Версия",
|
||||
"main_menu.about": "?",
|
||||
"main_menu.about.about_bunker_menu_item": "О pztrn's Bunker...",
|
||||
"main_menu.file": "Файл",
|
||||
"main_menu.file.exit_menu_item": "Выход",
|
||||
"main_menu.file.options_menu_item": "Настройки",
|
||||
"options_dialog.no_settings": "Для выбранной секции настройки отсутствуют.",
|
||||
"options_dialog.select_section": "Выберите секцию слева для отображения настроек.",
|
||||
"search.placeholder": "Введите текст для поиска и нажмите ENTER.",
|
||||
"window.lorem_ipsum.tab_name": "Добро пожаловать!",
|
||||
"window.lorem_ipsum.text": "Welcome to pztrn's Bunker!\n\nUnfortunately, something went wrong and while application was successfully initialized and started you see this message.\n\nPlease, restart application using terminal and send logs to developer!",
|
||||
"window.title": "pztrn's Bunker"
|
||||
}
|
8
client/internal/langfiles/langfiles.go
Normal file
8
client/internal/langfiles/langfiles.go
Normal file
@ -0,0 +1,8 @@
|
||||
package langfiles
|
||||
|
||||
import "embed"
|
||||
|
||||
// LangFiles это встроенная ФС, содержащая переводы приложения.
|
||||
//
|
||||
//go:embed files
|
||||
var LangFiles embed.FS
|
44
client/internal/services/core/database.go
Normal file
44
client/internal/services/core/database.go
Normal file
@ -0,0 +1,44 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/fs"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// ServiceNameDatabase is a name for database service.
|
||||
const ServiceNameDatabase = "core/database"
|
||||
|
||||
var (
|
||||
// ErrDatabase indicates that error appeared somewhere in database service.
|
||||
ErrDatabase = errors.New("database service")
|
||||
// ErrDatabaseIsInvalid indicates that database service implementation is invalid.
|
||||
ErrDatabaseIsInvalid = errors.New("database service implementation is invalid")
|
||||
)
|
||||
|
||||
// Database is an interface for database service.
|
||||
type Database interface {
|
||||
// Exec is a proxy for ExecContext from sqlx.
|
||||
Exec(ctx context.Context, query string, params ...interface{}) error
|
||||
// Get is a proxy for GetContext from sqlx.
|
||||
Get(ctx context.Context, target interface{}, query string, params ...interface{}) error
|
||||
// NamedExec is a proxy for NamedExecContext from sqlx.
|
||||
NamedExec(ctx context.Context, query string, param interface{}) error
|
||||
// RegisterMigrations registers migrations for applying from other services. Migrations should reside
|
||||
// in "migrations" directory in passed filesystem.
|
||||
RegisterMigrations(moduleName string, fs fs.FS) error
|
||||
// Select is a proxy for SelectContext from sqlx.
|
||||
Select(ctx context.Context, target interface{}, query string, params ...interface{}) error
|
||||
// Transaction is a wrapper for transactions processing which wraps sqlx's transactions.
|
||||
Transaction(ctx context.Context) (DatabaseTransaction, error)
|
||||
}
|
||||
|
||||
// DatabaseTransaction is an interface for database transactions controllers implementations.
|
||||
type DatabaseTransaction interface {
|
||||
Apply(steps ...TransactionFunc) error
|
||||
}
|
||||
|
||||
// TransactionFunc is a function that is used in transactions to mangle with data.
|
||||
type TransactionFunc func(*sqlx.Tx) error
|
116
client/internal/services/core/database/database.go
Normal file
116
client/internal/services/core/database/database.go
Normal file
@ -0,0 +1,116 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
|
||||
"bunker/client/internal/application"
|
||||
"bunker/client/internal/services/core"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
// sqlite adapter.
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
var _ = core.Database(&database{})
|
||||
|
||||
type database struct {
|
||||
mainWindow core.MainWindow
|
||||
options core.Options
|
||||
app *application.Application
|
||||
db *sqlx.DB
|
||||
migrations map[string]fs.FS
|
||||
dbPath string
|
||||
version int64
|
||||
}
|
||||
|
||||
// Initialize initializes service.
|
||||
func Initialize(app *application.Application) error {
|
||||
db := &database{
|
||||
app: app,
|
||||
}
|
||||
|
||||
if err := app.RegisterService(db); err != nil {
|
||||
return fmt.Errorf("%w: %w", core.ErrMainWindow, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) Configure() error {
|
||||
if err := d.configureDBPath(); err != nil {
|
||||
return fmt.Errorf("configure: %w", err)
|
||||
}
|
||||
|
||||
db, err := sqlx.Open("sqlite", d.dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configure: open database: %w", err)
|
||||
}
|
||||
|
||||
d.db = db
|
||||
|
||||
slog.Info("Database opened.", "path", d.dbPath)
|
||||
|
||||
if err := d.initializeSysInfoHandler(); err != nil {
|
||||
return fmt.Errorf("configure: %w", err)
|
||||
}
|
||||
|
||||
if err := d.initializeOptions(); err != nil {
|
||||
return fmt.Errorf("configure: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) ConnectDependencies() error {
|
||||
mainWindowRaw := d.app.Service(core.ServiceNameMainWindow)
|
||||
if mainWindowRaw == nil {
|
||||
return fmt.Errorf("connect dependencies: get main window: %w", application.ErrServiceNotFound)
|
||||
}
|
||||
|
||||
mainWindow, valid := mainWindowRaw.(core.MainWindow)
|
||||
if !valid {
|
||||
return fmt.Errorf("connect dependencies: type assert main window: %w", core.ErrMainWindowIsInvalid)
|
||||
}
|
||||
|
||||
d.mainWindow = mainWindow
|
||||
|
||||
optionsRaw := d.app.Service(core.ServiceNameOptions)
|
||||
if optionsRaw == nil {
|
||||
return fmt.Errorf("connect dependencies: get options: %w", application.ErrServiceNotFound)
|
||||
}
|
||||
|
||||
options, valid := optionsRaw.(core.Options)
|
||||
if !valid {
|
||||
return fmt.Errorf("connect dependencies: type assert options: %w", core.ErrOptionsIsInvalid)
|
||||
}
|
||||
|
||||
d.options = options
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) Initialize() error {
|
||||
d.migrations = make(map[string]fs.FS, 0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) Name() string {
|
||||
return core.ServiceNameDatabase
|
||||
}
|
||||
|
||||
func (d *database) LaunchStartupTasks() error {
|
||||
// Запускаем миграции.
|
||||
if err := d.applyMigrations(); err != nil {
|
||||
return fmt.Errorf("launch startup tasks: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) Shutdown() error {
|
||||
return nil
|
||||
}
|
30
client/internal/services/core/database/dbpath.go
Normal file
30
client/internal/services/core/database/dbpath.go
Normal file
@ -0,0 +1,30 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"bunker/commons"
|
||||
)
|
||||
|
||||
func (d *database) configureDBPath() error {
|
||||
rootDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("configure DB path: get config dir: %w", err)
|
||||
}
|
||||
|
||||
rootDir = filepath.Join(rootDir, commons.ClientAppID)
|
||||
|
||||
//nolint:mnd
|
||||
if err := os.MkdirAll(rootDir, 0o700); err != nil {
|
||||
return fmt.Errorf("configure DB path: create dir: %w", err)
|
||||
}
|
||||
|
||||
d.dbPath = filepath.Join(rootDir, "database.sqlite3")
|
||||
|
||||
slog.Info("Database path configured.", "path", d.dbPath)
|
||||
|
||||
return nil
|
||||
}
|
78
client/internal/services/core/database/migrations.go
Normal file
78
client/internal/services/core/database/migrations.go
Normal file
@ -0,0 +1,78 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"bunker/client/internal/services/core"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
var errMigrationsAlreadyRegistered = errors.New("migrations already registered")
|
||||
|
||||
func (d *database) applyMigrations() error {
|
||||
slog.Info("Migrating database...")
|
||||
|
||||
modules := make([]string, 0)
|
||||
|
||||
for module := range d.migrations {
|
||||
modules = append(modules, module)
|
||||
}
|
||||
|
||||
sort.Strings(modules)
|
||||
|
||||
_ = goose.SetDialect(string(goose.DialectSQLite3))
|
||||
|
||||
for _, module := range modules {
|
||||
slog.Info("Migrating database for module...", "module", module)
|
||||
|
||||
goose.SetBaseFS(d.migrations[module])
|
||||
goose.SetTableName(strings.ReplaceAll(module, "/", "_") + "_migrations")
|
||||
|
||||
if err := goose.Up(d.db.DB, "migrations"); err != nil {
|
||||
return fmt.Errorf("%w: applying migrations for module '%s': %w", core.ErrDatabase, module, err)
|
||||
}
|
||||
|
||||
moduleDBVersion, err := goose.GetDBVersion(d.db.DB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: get database version for module '%s': %w", core.ErrDatabase, module, err)
|
||||
}
|
||||
|
||||
d.version += moduleDBVersion
|
||||
|
||||
slog.Info(
|
||||
"Database for module migrated to latest version",
|
||||
"module", module,
|
||||
"module_db_version", moduleDBVersion,
|
||||
"db_version", d.version,
|
||||
)
|
||||
}
|
||||
|
||||
slog.Info("Database migrated.", "version", d.version)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) RegisterMigrations(moduleName string, fSys fs.FS) error {
|
||||
slog.Debug("Registering migrations for service.", "service", moduleName)
|
||||
|
||||
if _, found := d.migrations[moduleName]; found {
|
||||
return fmt.Errorf(
|
||||
"%w: RegisterMigrations: module '%s': %w",
|
||||
core.ErrDatabase,
|
||||
moduleName,
|
||||
errMigrationsAlreadyRegistered,
|
||||
)
|
||||
}
|
||||
|
||||
d.migrations[moduleName] = fSys
|
||||
|
||||
slog.Debug("Migrations for service successfully registered.", "service", moduleName)
|
||||
|
||||
return nil
|
||||
}
|
36
client/internal/services/core/database/options.go
Normal file
36
client/internal/services/core/database/options.go
Normal file
@ -0,0 +1,36 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"bunker/client/internal/services/core/options/dto"
|
||||
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
func (d *database) initializeOptions() error {
|
||||
databaseMaxIdleConnsEntry := widget.NewEntry()
|
||||
databaseMaxIdleConns := widget.NewFormItem("Maximum idle connections", databaseMaxIdleConnsEntry)
|
||||
|
||||
databaseMaxOpenedConnsEntry := widget.NewEntry()
|
||||
databaseMaxOpenedConns := widget.NewFormItem("Maximum opened connections", databaseMaxOpenedConnsEntry)
|
||||
|
||||
optionsWidgetForm := widget.NewForm(
|
||||
databaseMaxIdleConns,
|
||||
databaseMaxOpenedConns,
|
||||
)
|
||||
|
||||
if err := d.options.RegisterOptionsWidget(&dto.OptionPane{
|
||||
Widget: optionsWidgetForm,
|
||||
SaveHandler: d.saveOptions,
|
||||
Name: "Database",
|
||||
}); err != nil {
|
||||
return fmt.Errorf("register options widget: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) saveOptions() error {
|
||||
return nil
|
||||
}
|
80
client/internal/services/core/database/queries.go
Normal file
80
client/internal/services/core/database/queries.go
Normal file
@ -0,0 +1,80 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"bunker/client/internal/services/core"
|
||||
)
|
||||
|
||||
// Exec is a proxy for ExecContext from sqlx.
|
||||
func (d *database) Exec(ctx context.Context, query string, params ...interface{}) error {
|
||||
if strings.Contains(query, "?") {
|
||||
query = d.db.Rebind(query)
|
||||
}
|
||||
|
||||
slog.Debug("Executing query.", "query", query, "params", fmt.Sprintf("%+v", params), "module", "core/database")
|
||||
|
||||
if _, err := d.db.ExecContext(ctx, query, params...); err != nil {
|
||||
return fmt.Errorf("%w: failed to Exec(): %w", core.ErrDatabase, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get is a proxy for GetContext from sqlx.
|
||||
func (d *database) Get(ctx context.Context, target interface{}, query string, params ...interface{}) error {
|
||||
if strings.Contains(query, "?") {
|
||||
query = d.db.Rebind(query)
|
||||
}
|
||||
|
||||
slog.Debug(
|
||||
"Getting single data from database with query.",
|
||||
"query", query,
|
||||
"params", fmt.Sprintf("%+v", params),
|
||||
"module", "core/database",
|
||||
)
|
||||
|
||||
if err := d.db.GetContext(ctx, target, query, params...); err != nil {
|
||||
return fmt.Errorf("%w: failed to Get(): %w", core.ErrDatabase, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NamedExec is a proxy for NamedExecContext from sqlx.
|
||||
func (d *database) NamedExec(ctx context.Context, query string, param interface{}) error {
|
||||
if strings.Contains(query, "?") {
|
||||
query = d.db.Rebind(query)
|
||||
}
|
||||
|
||||
slog.Debug("Executing named query.", "query", query, "params", fmt.Sprintf("%+v", param), "module", "core/database")
|
||||
|
||||
if _, err := d.db.NamedExecContext(ctx, query, param); err != nil {
|
||||
return fmt.Errorf("%w: failed to NamedExec(): %w", core.ErrDatabase, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Select is a proxy for SelectContext from sqlx.
|
||||
func (d *database) Select(ctx context.Context, target interface{}, query string, params ...interface{}) error {
|
||||
if strings.Contains(query, "?") {
|
||||
query = d.db.Rebind(query)
|
||||
}
|
||||
|
||||
slog.Debug(
|
||||
"Selecting from database with query.",
|
||||
"query", query,
|
||||
"params", fmt.Sprintf("%+v", params),
|
||||
"module", "core/database",
|
||||
)
|
||||
|
||||
if err := d.db.SelectContext(ctx, target, query, params...); err != nil {
|
||||
return fmt.Errorf("%w: failed to Select(): %w", core.ErrDatabase, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
51
client/internal/services/core/database/sys_info_handler.go
Normal file
51
client/internal/services/core/database/sys_info_handler.go
Normal file
@ -0,0 +1,51 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func (d *database) initializeSysInfoHandler() error {
|
||||
if err := d.mainWindow.RegisterAboutWindowSysInfoHandler(
|
||||
"database_data",
|
||||
d.sysInfoHandlerDatabaseVersion,
|
||||
); err != nil {
|
||||
return fmt.Errorf("register database_data handler: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) sysInfoHandlerDatabaseVersion() string {
|
||||
var dbFileSize, sizeDivisionsCount int64
|
||||
|
||||
var databaseSizeString string
|
||||
|
||||
fileInfo, err := os.Stat(d.dbPath)
|
||||
if err == nil {
|
||||
dbFileSize = fileInfo.Size()
|
||||
}
|
||||
|
||||
for {
|
||||
//nolint:mnd
|
||||
if dbFileSize > 1024 {
|
||||
dbFileSize /= 1024
|
||||
sizeDivisionsCount++
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
switch sizeDivisionsCount {
|
||||
case 1:
|
||||
databaseSizeString = fmt.Sprintf("- Database file size: %d KB", dbFileSize)
|
||||
case 2: //nolint:mnd
|
||||
databaseSizeString = fmt.Sprintf("- Database file size: %d MB", dbFileSize)
|
||||
default:
|
||||
databaseSizeString = fmt.Sprintf("- Database file size: %d GB", dbFileSize)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("### Database\n\n- Database version: %d\n%s\n", d.version, databaseSizeString)
|
||||
}
|
79
client/internal/services/core/database/transaction.go
Normal file
79
client/internal/services/core/database/transaction.go
Normal file
@ -0,0 +1,79 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"bunker/client/internal/services/core"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type transaction struct {
|
||||
transaction *sqlx.Tx
|
||||
}
|
||||
|
||||
func (d *database) Transaction(ctx context.Context) (core.DatabaseTransaction, error) {
|
||||
txn, err := d.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: starting transaction: %w", core.ErrDatabase, err)
|
||||
}
|
||||
|
||||
txHandler := &transaction{
|
||||
transaction: txn,
|
||||
}
|
||||
|
||||
return txHandler, nil
|
||||
}
|
||||
|
||||
func (t *transaction) Apply(steps ...core.TransactionFunc) error {
|
||||
for stepNumber, stepFunc := range steps {
|
||||
if err := stepFunc(t.transaction); err != nil {
|
||||
slog.Error(
|
||||
"Error occurred.",
|
||||
"step", stepNumber,
|
||||
"error", err.Error(),
|
||||
"module", "core/database",
|
||||
"subsystem", "transaction",
|
||||
)
|
||||
|
||||
if rollbackErr := t.transaction.Rollback(); rollbackErr != nil {
|
||||
slog.Error(
|
||||
"Transaction rollback failed.",
|
||||
"error", err.Error(),
|
||||
"module", "core/database",
|
||||
"subsystem", "transaction",
|
||||
)
|
||||
|
||||
return fmt.Errorf("%w: transaction rollback: %w", core.ErrDatabase, rollbackErr)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := t.transaction.Commit(); err != nil {
|
||||
slog.Error(
|
||||
"Transaction commit failed.",
|
||||
"error", err.Error(),
|
||||
"module", "core/database",
|
||||
"subsystem", "transaction",
|
||||
)
|
||||
|
||||
if rollbackErr := t.transaction.Rollback(); rollbackErr != nil {
|
||||
slog.Error(
|
||||
"Transaction rollback failed.",
|
||||
"error", err.Error(),
|
||||
"module", "core/database",
|
||||
"subsystem", "transaction",
|
||||
)
|
||||
|
||||
return fmt.Errorf("%w: transaction rollback: %w", core.ErrDatabase, rollbackErr)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: transaction commit: %w", core.ErrDatabase, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
32
client/internal/services/core/mainwindow.go
Normal file
32
client/internal/services/core/mainwindow.go
Normal file
@ -0,0 +1,32 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
)
|
||||
|
||||
// ServiceNameMainWindow is a name for main window service.
|
||||
const ServiceNameMainWindow = "core/mainwindow"
|
||||
|
||||
var (
|
||||
// ErrMainWindow indicates that error appeared somewhere in main window service.
|
||||
ErrMainWindow = errors.New("main window service")
|
||||
// ErrMainWindowIsInvalid indicates that main window service implementation is invalid.
|
||||
ErrMainWindowIsInvalid = errors.New("main window service implementation is invalid")
|
||||
)
|
||||
|
||||
// MainWindow is an interface for main window service.
|
||||
type MainWindow interface {
|
||||
// AddTab adds tab in main window.
|
||||
AddTab(tab *container.TabItem)
|
||||
// MainWindow returns main window instance (e.g. for using as parent with dialogs).
|
||||
MainWindow() fyne.Window
|
||||
// RegisterAboutWindowSysInfoHandler registers handler for System Info tab in About dialog.
|
||||
RegisterAboutWindowSysInfoHandler(name string, hndl SysInfoHandler) error
|
||||
}
|
||||
|
||||
// SysInfoHandler is a function signature for registering with additional system information handler for About dialog.
|
||||
// It should return valid markdown and should end with "\n".
|
||||
type SysInfoHandler func() string
|
54
client/internal/services/core/mainwindow/about_dialog.go
Normal file
54
client/internal/services/core/mainwindow/about_dialog.go
Normal file
@ -0,0 +1,54 @@
|
||||
package mainwindow
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/lang"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
)
|
||||
|
||||
const (
|
||||
minRowsVisibleInSysInfo = 10
|
||||
startYear = 2025
|
||||
)
|
||||
|
||||
func (m *mainWindow) showAboutDialog() {
|
||||
appNameLbl := canvas.NewText(
|
||||
"pztrn's Bunker",
|
||||
m.app.Fyne().Settings().Theme().Color(
|
||||
theme.ColorNameForeground,
|
||||
m.app.Fyne().Settings().ThemeVariant(),
|
||||
),
|
||||
)
|
||||
appNameLbl.TextSize = 16
|
||||
appNameLbl.TextStyle = fyne.TextStyle{Bold: true}
|
||||
appNameLbl.Alignment = fyne.TextAlignCenter
|
||||
|
||||
appVersionLbl := canvas.NewText(
|
||||
lang.L("about_dialog.version")+" "+m.app.Fyne().Metadata().Custom["Version"],
|
||||
m.app.Fyne().Settings().Theme().Color(
|
||||
theme.ColorNameForeground,
|
||||
m.app.Fyne().Settings().ThemeVariant(),
|
||||
),
|
||||
)
|
||||
appVersionLbl.TextSize = 16
|
||||
appVersionLbl.TextStyle = fyne.TextStyle{Bold: true}
|
||||
appVersionLbl.Alignment = fyne.TextAlignCenter
|
||||
|
||||
tabs := container.NewAppTabs(
|
||||
m.generateAboutTab(),
|
||||
m.generateLicensesTab(),
|
||||
m.generateSysInfoTab(),
|
||||
)
|
||||
|
||||
vbox := container.NewVBox(
|
||||
appNameLbl,
|
||||
appVersionLbl,
|
||||
tabs,
|
||||
)
|
||||
|
||||
dlg := dialog.NewCustom(lang.L("about_dialog.title"), lang.L("about_dialog.close_button"), vbox, m.window)
|
||||
dlg.Show()
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package mainwindow
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/lang"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
func (m *mainWindow) generateAboutTab() *container.TabItem {
|
||||
copyrightYears := strconv.Itoa(startYear)
|
||||
if time.Now().Year() > startYear {
|
||||
copyrightYears = strconv.Itoa(startYear) + " - " + strconv.Itoa(time.Now().Year())
|
||||
}
|
||||
|
||||
aboutLabel := widget.NewLabel(
|
||||
lang.L("about_dialog.about_tab.summary") +
|
||||
"\n\n" +
|
||||
lang.L("about_dialog.about_tab.copyright", map[string]any{"Years": copyrightYears}),
|
||||
)
|
||||
aboutLabel.Wrapping = fyne.TextWrapWord
|
||||
|
||||
aboutVBox := container.NewVBox(aboutLabel)
|
||||
aboutScroll := container.NewVScroll(aboutVBox)
|
||||
|
||||
aboutScroll.SetMinSize(fyne.NewSize(
|
||||
m.window.Content().Size().Width-m.window.Content().Size().Width/3,
|
||||
m.window.Content().Size().Height-m.window.Content().Size().Height/2.5,
|
||||
))
|
||||
|
||||
return container.NewTabItem(lang.L("about_dialog.about_tab"), aboutScroll)
|
||||
}
|
@ -0,0 +1,293 @@
|
||||
package mainwindow
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/lang"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
func (m *mainWindow) generateLicensesTab() *container.TabItem {
|
||||
licensesLabel := widget.NewLabel(lang.L("about_dialog.licenses_tab.summary"))
|
||||
licensesLabel.Wrapping = fyne.TextWrapWord
|
||||
|
||||
accordion := widget.NewAccordion(
|
||||
m.generateFyneAccordionItem(),
|
||||
m.generateGooseAccordionItem(),
|
||||
m.generateGopsutilAccordionItem(),
|
||||
m.generateModerncSqliteAccordionItem(),
|
||||
m.generateSqlxAccrodionItem(),
|
||||
)
|
||||
|
||||
licensesVBox := container.NewVBox(licensesLabel, accordion)
|
||||
licensesScroll := container.NewVScroll(licensesVBox)
|
||||
|
||||
return container.NewTabItem(lang.L("about_dialog.licenses_tab"), licensesScroll)
|
||||
}
|
||||
|
||||
func (m *mainWindow) generateFyneAccordionItem() *widget.AccordionItem {
|
||||
summary := widget.NewLabel(lang.L("about_dialog.licenses_tab.dependency.fyne"))
|
||||
summary.Wrapping = fyne.TextWrapWord
|
||||
|
||||
license := widget.NewLabel(`BSD 3-Clause License
|
||||
|
||||
Copyright (C) 2018 Fyne.io developers (see AUTHORS)
|
||||
All rights reserved.
|
||||
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Fyne.io nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.`)
|
||||
license.TextStyle = fyne.TextStyle{Monospace: true}
|
||||
license.Wrapping = fyne.TextWrapWord
|
||||
|
||||
url, _ := url.Parse("https://github.com/fyne-io/fyne")
|
||||
authorsURL, _ := url.Parse("https://raw.githubusercontent.com/fyne-io/fyne/refs/heads/master/AUTHORS")
|
||||
|
||||
return widget.NewAccordionItem("Fyne v2.6.1", container.NewVBox(
|
||||
summary,
|
||||
container.NewHBox(
|
||||
widget.NewHyperlink("Source", url),
|
||||
widget.NewLabel("|"),
|
||||
widget.NewHyperlink("Authors", authorsURL),
|
||||
),
|
||||
license,
|
||||
))
|
||||
}
|
||||
|
||||
func (m *mainWindow) generateGooseAccordionItem() *widget.AccordionItem {
|
||||
summary := widget.NewLabel(lang.L("about_dialog.licenses_tab.dependency.goose"))
|
||||
summary.Wrapping = fyne.TextWrapWord
|
||||
|
||||
license := widget.NewLabel(`MIT License
|
||||
|
||||
Original work Copyright (c) 2012 Liam Staskawicz
|
||||
Modified work Copyright (c) 2016 Vojtech Vitek
|
||||
Modified work Copyright (c) 2021 Michael Fridman, Vojtech Vitek
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`)
|
||||
license.TextStyle = fyne.TextStyle{Monospace: true}
|
||||
license.Wrapping = fyne.TextWrapWord
|
||||
|
||||
url, _ := url.Parse("https://github.com/pressly/goose")
|
||||
authorsURL, _ := url.Parse("https://github.com/pressly/goose/graphs/contributors")
|
||||
|
||||
return widget.NewAccordionItem("goose v3.24.3", container.NewVBox(
|
||||
summary,
|
||||
container.NewHBox(
|
||||
widget.NewHyperlink("Source", url),
|
||||
widget.NewLabel("|"),
|
||||
widget.NewHyperlink("Authors", authorsURL),
|
||||
),
|
||||
license,
|
||||
))
|
||||
}
|
||||
|
||||
func (m *mainWindow) generateGopsutilAccordionItem() *widget.AccordionItem {
|
||||
summary := widget.NewLabel(lang.L("about_dialog.licenses_tab.dependency.gopsutil"))
|
||||
summary.Wrapping = fyne.TextWrapWord
|
||||
|
||||
license := widget.NewLabel(`gopsutil is distributed under BSD license reproduced below.
|
||||
|
||||
Copyright (c) 2014, WAKAYAMA Shirou
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name of the gopsutil authors nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
-------
|
||||
internal/common/binary.go in the gopsutil is copied and modified from golang/encoding/binary.go.
|
||||
|
||||
|
||||
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.`)
|
||||
license.TextStyle = fyne.TextStyle{Monospace: true}
|
||||
license.Wrapping = fyne.TextWrapWord
|
||||
|
||||
url, _ := url.Parse("https://github.com/shirou/gopsutil")
|
||||
authorsURL, _ := url.Parse("https://github.com/shirou/gopsutil/graphs/contributors")
|
||||
|
||||
return widget.NewAccordionItem("gopsutil v3.24.5", container.NewVBox(
|
||||
summary,
|
||||
container.NewHBox(
|
||||
widget.NewHyperlink("Source", url),
|
||||
widget.NewLabel("|"),
|
||||
widget.NewHyperlink("Authors", authorsURL),
|
||||
),
|
||||
license,
|
||||
))
|
||||
}
|
||||
|
||||
func (m *mainWindow) generateModerncSqliteAccordionItem() *widget.AccordionItem {
|
||||
summary := widget.NewLabel(lang.L("about_dialog.licenses_tab.dependency.modernc_sqlite"))
|
||||
summary.Wrapping = fyne.TextWrapWord
|
||||
|
||||
license := widget.NewLabel(`Copyright (c) 2017 The Sqlite Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
`)
|
||||
license.TextStyle = fyne.TextStyle{Monospace: true}
|
||||
license.Wrapping = fyne.TextWrapWord
|
||||
|
||||
url, _ := url.Parse("https://gitlab.com/cznic/sqlite")
|
||||
authorsURL, _ := url.Parse("https://gitlab.com/cznic/sqlite/-/raw/master/AUTHORS")
|
||||
|
||||
return widget.NewAccordionItem("modernc/sqlite v1.37.1", container.NewVBox(
|
||||
summary,
|
||||
container.NewHBox(
|
||||
widget.NewHyperlink("Source", url),
|
||||
widget.NewLabel("|"),
|
||||
widget.NewHyperlink("Authors", authorsURL),
|
||||
),
|
||||
license,
|
||||
))
|
||||
}
|
||||
|
||||
func (m *mainWindow) generateSqlxAccrodionItem() *widget.AccordionItem {
|
||||
summary := widget.NewLabel(lang.L("about_dialog.licenses_tab.dependency.sqlx"))
|
||||
summary.Wrapping = fyne.TextWrapWord
|
||||
|
||||
license := widget.NewLabel(`Copyright (c) 2013, Jason Moiron
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.`)
|
||||
license.TextStyle = fyne.TextStyle{Monospace: true}
|
||||
license.Wrapping = fyne.TextWrapWord
|
||||
|
||||
url, _ := url.Parse("https://github.com/jmoiron/sqlx")
|
||||
authorsURL, _ := url.Parse("https://github.com/jmoiron/sqlx/graphs/contributors")
|
||||
|
||||
return widget.NewAccordionItem("sqlx v1.4.0", container.NewVBox(
|
||||
summary,
|
||||
container.NewHBox(
|
||||
widget.NewHyperlink("Source", url),
|
||||
widget.NewLabel("|"),
|
||||
widget.NewHyperlink("Authors", authorsURL),
|
||||
),
|
||||
license,
|
||||
))
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
package mainwindow
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
|
||||
"bunker/client/internal/services/core"
|
||||
"bunker/client/internal/services/core/mainwindow/models"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/lang"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
"github.com/shirou/gopsutil/v3/process"
|
||||
)
|
||||
|
||||
var errSysInfoHandlerAlreadyRegistered = errors.New("handler already registered")
|
||||
|
||||
func (m *mainWindow) generateSysInfoTab() *container.TabItem {
|
||||
lbl := widget.NewLabel(lang.L("about_dialog.sysinfo_tab.summary"))
|
||||
lbl.Wrapping = fyne.TextWrapWord
|
||||
|
||||
buildInfo, _ := debug.ReadBuildInfo()
|
||||
|
||||
var builtForOS, builtForArch string
|
||||
|
||||
for _, bi := range buildInfo.Settings {
|
||||
switch bi.Key {
|
||||
case "GOARCH":
|
||||
builtForArch = bi.Value
|
||||
case "GOOS":
|
||||
builtForOS = bi.Value
|
||||
}
|
||||
}
|
||||
|
||||
commons, _ := host.Info()
|
||||
|
||||
cpuInfo, _ := cpu.Info()
|
||||
|
||||
memInfo, _ := mem.VirtualMemory()
|
||||
|
||||
memoryTotal := memInfo.Total
|
||||
memoryTotalDivCount := 0
|
||||
|
||||
var memoryTotalString string
|
||||
|
||||
for {
|
||||
//nolint:mnd
|
||||
if memoryTotal > 1024 {
|
||||
memoryTotal = memoryTotal / 1024
|
||||
memoryTotalDivCount++
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
//nolint:mnd
|
||||
switch memoryTotalDivCount {
|
||||
case 1:
|
||||
memoryTotalString = fmt.Sprintf("%d KB", memoryTotal)
|
||||
case 2:
|
||||
memoryTotalString = fmt.Sprintf("%d MB", memoryTotal)
|
||||
default:
|
||||
memoryTotalString = fmt.Sprintf("%d GB", memoryTotal)
|
||||
}
|
||||
|
||||
memoryFree := memInfo.Available
|
||||
memoryFreeDivCount := 0
|
||||
|
||||
var memoryFreeString string
|
||||
|
||||
//nolint:mnd
|
||||
for {
|
||||
if memoryFree > 1024 {
|
||||
memoryFree = memoryFree / 1024
|
||||
memoryFreeDivCount++
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
//nolint:mnd
|
||||
switch memoryFreeDivCount {
|
||||
case 1:
|
||||
memoryFreeString = fmt.Sprintf("%d KB", memoryFree)
|
||||
case 2:
|
||||
memoryFreeString = fmt.Sprintf("%d MB", memoryFree)
|
||||
default:
|
||||
memoryFreeString = fmt.Sprintf("%d GB", memoryFree)
|
||||
}
|
||||
|
||||
var (
|
||||
conns int
|
||||
fds int32
|
||||
)
|
||||
|
||||
//nolint:gosec
|
||||
proc, err := process.NewProcess(int32(os.Getpid()))
|
||||
if err == nil {
|
||||
connections, _ := proc.Connections()
|
||||
conns = len(connections)
|
||||
|
||||
fds, _ = proc.NumFDs()
|
||||
}
|
||||
|
||||
sysInfoData := fmt.Sprintf(`## Build information
|
||||
|
||||
- Version: %s (#%s, from %s/%s, on %s)
|
||||
- Built for: %s/%s
|
||||
|
||||
## System information
|
||||
|
||||
- CPU: %s (%d cores)
|
||||
- RAM: %s (%s available)
|
||||
- OS: %s %s
|
||||
- Processes running: %d
|
||||
|
||||
## Launch information
|
||||
|
||||
- Running on: %s/%s
|
||||
- Opened network connections: %d
|
||||
- Opened file descriptors: %d
|
||||
|
||||
## Additional information
|
||||
|
||||
%s
|
||||
`,
|
||||
m.app.Fyne().Metadata().Custom["Version"],
|
||||
m.app.Fyne().Metadata().Custom["Build"],
|
||||
m.app.Fyne().Metadata().Custom["Branch"],
|
||||
m.app.Fyne().Metadata().Custom["Commit"],
|
||||
m.app.Fyne().Metadata().Custom["BuildDate"],
|
||||
builtForOS,
|
||||
builtForArch,
|
||||
cpuInfo[0].ModelName,
|
||||
cpuInfo[0].Cores,
|
||||
memoryTotalString,
|
||||
memoryFreeString,
|
||||
commons.OS,
|
||||
commons.PlatformVersion,
|
||||
commons.Procs,
|
||||
runtime.GOOS,
|
||||
runtime.GOARCH,
|
||||
conns,
|
||||
fds,
|
||||
m.prepareSysInfoAdditionalsForAbout(),
|
||||
)
|
||||
|
||||
sysInfo := widget.NewMultiLineEntry()
|
||||
sysInfo.OnChanged = func(_ string) {
|
||||
sysInfo.SetText(sysInfoData)
|
||||
}
|
||||
sysInfo.SetText(sysInfoData)
|
||||
sysInfo.SetMinRowsVisible(minRowsVisibleInSysInfo)
|
||||
|
||||
copyToClipboard := widget.NewButton("Copy to clipboard", func() {
|
||||
m.app.Fyne().Clipboard().SetContent(sysInfoData)
|
||||
|
||||
m.app.Fyne().SendNotification(&fyne.Notification{
|
||||
Title: lang.L("about_dialog.sysinfo_tab.copy_to_clipboard.notification.title"),
|
||||
Content: lang.L("about_dialog.sysinfo_tab.copy_to_clipboard.notification.content"),
|
||||
})
|
||||
})
|
||||
|
||||
sysInfoVBox := container.NewVBox(
|
||||
lbl,
|
||||
sysInfo,
|
||||
copyToClipboard,
|
||||
)
|
||||
|
||||
return container.NewTabItem(lang.L("about_dialog.sysinfo_tab"), sysInfoVBox)
|
||||
}
|
||||
|
||||
func (m *mainWindow) prepareSysInfoAdditionalsForAbout() string {
|
||||
additionals := ""
|
||||
|
||||
handlersNames := make([]string, 0, len(m.sysInfoHandlers))
|
||||
|
||||
for name := range m.sysInfoHandlers {
|
||||
handlersNames = append(handlersNames, name)
|
||||
}
|
||||
|
||||
sort.Strings(handlersNames)
|
||||
|
||||
for _, name := range handlersNames {
|
||||
additionals += m.sysInfoHandlers[name].Handler() + "\n"
|
||||
}
|
||||
|
||||
return additionals
|
||||
}
|
||||
|
||||
func (m *mainWindow) RegisterAboutWindowSysInfoHandler(name string, hndl core.SysInfoHandler) error {
|
||||
if _, found := m.sysInfoHandlers[name]; found {
|
||||
return fmt.Errorf(
|
||||
"%w: RegisterAboutWindowSysInfoHandler: register '%s': %w",
|
||||
core.ErrMainWindow,
|
||||
name,
|
||||
errSysInfoHandlerAlreadyRegistered,
|
||||
)
|
||||
}
|
||||
|
||||
sysInfoHandler := &models.SysInfoHandler{
|
||||
Name: name,
|
||||
Handler: hndl,
|
||||
}
|
||||
|
||||
m.sysInfoHandlers[name] = sysInfoHandler
|
||||
|
||||
return nil
|
||||
}
|
109
client/internal/services/core/mainwindow/mainwindow.go
Normal file
109
client/internal/services/core/mainwindow/mainwindow.go
Normal file
@ -0,0 +1,109 @@
|
||||
package mainwindow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"bunker/client/internal/application"
|
||||
"bunker/client/internal/services/core"
|
||||
"bunker/client/internal/services/core/mainwindow/models"
|
||||
"bunker/commons"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/lang"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
var _ = core.MainWindow(&mainWindow{})
|
||||
|
||||
type mainWindow struct {
|
||||
app *application.Application
|
||||
window fyne.Window
|
||||
options core.Options
|
||||
tabs *container.AppTabs
|
||||
sysInfoHandlers map[string]*models.SysInfoHandler
|
||||
}
|
||||
|
||||
// Initialize инициализирует сервис.
|
||||
func Initialize(app *application.Application) error {
|
||||
mainW := &mainWindow{
|
||||
app: app,
|
||||
}
|
||||
|
||||
if err := app.RegisterService(mainW); err != nil {
|
||||
return fmt.Errorf("%w: %w", core.ErrMainWindow, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mainWindow) Configure() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mainWindow) ConnectDependencies() error {
|
||||
optionsRaw := m.app.Service(core.ServiceNameOptions)
|
||||
if optionsRaw == nil {
|
||||
return fmt.Errorf("connect dependencies: get options service: %w", application.ErrServiceNotFound)
|
||||
}
|
||||
|
||||
options, valid := optionsRaw.(core.Options)
|
||||
if !valid {
|
||||
return fmt.Errorf("connect dependencies: type assert options service: %w", core.ErrOptionsIsInvalid)
|
||||
}
|
||||
|
||||
m.options = options
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mainWindow) Initialize() error {
|
||||
m.sysInfoHandlers = make(map[string]*models.SysInfoHandler)
|
||||
|
||||
m.window = m.app.Fyne().NewWindow(lang.L("window.title"))
|
||||
// ToDo: сохранение и восстановление размеров окна.
|
||||
//nolint:mnd
|
||||
m.window.Resize(fyne.NewSize(800, 650))
|
||||
|
||||
m.initializeMenu()
|
||||
|
||||
m.window.SetCloseIntercept(m.stopApp)
|
||||
|
||||
welcomeLabel := widget.NewLabel(lang.L("window.lorem_ipsum.text"))
|
||||
welcomeLabel.Wrapping = fyne.TextWrapWord
|
||||
|
||||
m.tabs = container.NewAppTabs(
|
||||
container.NewTabItem(lang.L("window.lorem_ipsum.tab_name"), welcomeLabel),
|
||||
)
|
||||
m.window.SetContent(m.tabs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mainWindow) MainWindow() fyne.Window {
|
||||
return m.window
|
||||
}
|
||||
|
||||
func (m *mainWindow) Name() string {
|
||||
return core.ServiceNameMainWindow
|
||||
}
|
||||
|
||||
func (m *mainWindow) LaunchStartupTasks() error {
|
||||
m.window.Show()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mainWindow) Shutdown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mainWindow) stopApp() {
|
||||
if err := m.app.Shutdown(); err != nil {
|
||||
slog.Error("Failed to stop Bunker!", "error", err.Error())
|
||||
|
||||
os.Exit(commons.ExitCodeAppStopFailed)
|
||||
}
|
||||
}
|
35
client/internal/services/core/mainwindow/mainwindow_menu.go
Normal file
35
client/internal/services/core/mainwindow/mainwindow_menu.go
Normal file
@ -0,0 +1,35 @@
|
||||
package mainwindow
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/lang"
|
||||
)
|
||||
|
||||
func (m *mainWindow) initializeMenu() {
|
||||
optionsMenuItem := fyne.NewMenuItem(lang.L("main_menu.file.options_menu_item"), func() {
|
||||
slog.Info("Opening options...")
|
||||
|
||||
m.options.ShowOptionsDialog()
|
||||
})
|
||||
|
||||
exitMenuItem := fyne.NewMenuItem(lang.L("main_menu.file.exit_menu_item"), func() {
|
||||
m.stopApp()
|
||||
})
|
||||
exitMenuItem.IsQuit = true
|
||||
|
||||
fileMenu := fyne.NewMenu(lang.L("main_menu.file"), optionsMenuItem, fyne.NewMenuItemSeparator(), exitMenuItem)
|
||||
|
||||
aboutMenuItem := fyne.NewMenuItem(lang.L("main_menu.about.about_bunker_menu_item"), func() {
|
||||
m.showAboutDialog()
|
||||
})
|
||||
|
||||
aboutMenu := fyne.NewMenu(lang.L("main_menu.about"), aboutMenuItem)
|
||||
|
||||
mainMenu := fyne.NewMainMenu(
|
||||
fileMenu,
|
||||
aboutMenu,
|
||||
)
|
||||
m.window.SetMainMenu(mainMenu)
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package models
|
||||
|
||||
// SysInfoHandler содержит в себе информацию об обработчике для Markdown системной информации.
|
||||
type SysInfoHandler struct {
|
||||
Handler func() string
|
||||
Name string
|
||||
}
|
14
client/internal/services/core/mainwindow/tabs.go
Normal file
14
client/internal/services/core/mainwindow/tabs.go
Normal file
@ -0,0 +1,14 @@
|
||||
package mainwindow
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/lang"
|
||||
)
|
||||
|
||||
func (m *mainWindow) AddTab(tab *container.TabItem) {
|
||||
if len(m.tabs.Items) == 1 && m.tabs.Items[0].Text == lang.L("window.lorem_ipsum.tab_name") {
|
||||
m.tabs.Remove(m.tabs.Items[0])
|
||||
}
|
||||
|
||||
m.tabs.Append(tab)
|
||||
}
|
25
client/internal/services/core/options.go
Normal file
25
client/internal/services/core/options.go
Normal file
@ -0,0 +1,25 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"bunker/client/internal/services/core/options/dto"
|
||||
)
|
||||
|
||||
// ServiceNameOptions это название для сервиса работы с настройками.
|
||||
const ServiceNameOptions = "core/options"
|
||||
|
||||
var (
|
||||
// ErrOptions говорит о возникновении ошибки в сервисе работы с настройками.
|
||||
ErrOptions = errors.New("options core service")
|
||||
// ErrOptionsIsInvalid говорит о неверной имплементации сервиса работы с настройками.
|
||||
ErrOptionsIsInvalid = errors.New("options service implementation is invalid")
|
||||
)
|
||||
|
||||
// Options это интерфейс для сервиса работы с настройками.
|
||||
type Options interface {
|
||||
// RegisterOptionsWidget регистрирует виджет настроек, а также необходимые дополнительные параметры.
|
||||
RegisterOptionsWidget(widgetData *dto.OptionPane) error
|
||||
// ShowOptionsDialog показывает диалог с настройками. Используется только главным окном!
|
||||
ShowOptionsDialog()
|
||||
}
|
82
client/internal/services/core/options/dialog.go
Normal file
82
client/internal/services/core/options/dialog.go
Normal file
@ -0,0 +1,82 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/lang"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
func (o *options) closeOptionsDialog(save bool) {
|
||||
slog.Debug("Closing options dialog.", "save", save)
|
||||
|
||||
if !save {
|
||||
return
|
||||
}
|
||||
|
||||
for _, widget := range o.widgets {
|
||||
if widget.SaveHandler == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := widget.SaveHandler(); err != nil {
|
||||
slog.Error("Failed to save options for module!", "module", widget.Name, "error", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *options) optionsListCreateItem() fyne.CanvasObject {
|
||||
return widget.NewLabel("")
|
||||
}
|
||||
|
||||
func (o *options) optionsListLength() int {
|
||||
return len(o.widgetsItems)
|
||||
}
|
||||
|
||||
func (o *options) optionsListUpdateItem(itemID widget.ListItemID, obj fyne.CanvasObject) {
|
||||
label, _ := obj.(*widget.Label)
|
||||
|
||||
label.SetText(o.widgets[o.widgetsItems[itemID]].Name)
|
||||
}
|
||||
|
||||
func (o *options) ShowOptionsDialog() {
|
||||
optionsWidgetScroll := container.NewScroll(widget.NewLabel(lang.L("options_dialog.select_section")))
|
||||
|
||||
optionsList := widget.NewList(o.optionsListLength, o.optionsListCreateItem, o.optionsListUpdateItem)
|
||||
optionsList.OnSelected = func(listID widget.ListItemID) {
|
||||
// listID должно быть всегда меньше длины, так как является индексом.
|
||||
// Эта проверка отрабатывает, если настроек по какой-то причине нет.
|
||||
if listID >= len(o.widgetsItems) {
|
||||
return
|
||||
}
|
||||
|
||||
optionsWidget := o.widgets[o.widgetsItems[listID]].Widget
|
||||
|
||||
if optionsWidget != nil {
|
||||
optionsWidgetScroll.Content = optionsWidget
|
||||
} else {
|
||||
optionsWidgetScroll.Content = widget.NewLabel(lang.L("options_dialog.no_settings"))
|
||||
}
|
||||
|
||||
optionsWidgetScroll.Refresh()
|
||||
}
|
||||
|
||||
optionsListSplitter := container.NewHSplit(optionsList, optionsWidgetScroll)
|
||||
//nolint:mnd
|
||||
optionsListSplitter.SetOffset(0.25)
|
||||
|
||||
optionsDlg := dialog.NewCustomConfirm(
|
||||
"Options",
|
||||
"Save",
|
||||
"Cancel",
|
||||
optionsListSplitter,
|
||||
o.closeOptionsDialog,
|
||||
o.mainWindow.MainWindow(),
|
||||
)
|
||||
optionsDlg.Resize(o.mainWindow.MainWindow().Canvas().Size())
|
||||
|
||||
optionsDlg.Show()
|
||||
}
|
14
client/internal/services/core/options/dto/option_pane.go
Normal file
14
client/internal/services/core/options/dto/option_pane.go
Normal file
@ -0,0 +1,14 @@
|
||||
package dto
|
||||
|
||||
import "fyne.io/fyne/v2"
|
||||
|
||||
// OptionPane is a DTO for registering options pane.
|
||||
type OptionPane struct {
|
||||
Widget fyne.CanvasObject
|
||||
SaveHandler OptionSaveHandler
|
||||
Name string
|
||||
}
|
||||
|
||||
// OptionSaveHandler is a signature for functions responsible for saving options, resides in service that registers own
|
||||
// options pane.
|
||||
type OptionSaveHandler func() error
|
17
client/internal/services/core/options/migrations.go
Normal file
17
client/internal/services/core/options/migrations.go
Normal file
@ -0,0 +1,17 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//go:embed migrations
|
||||
var migrations embed.FS
|
||||
|
||||
func (o *options) registerMigrations() error {
|
||||
if err := o.db.RegisterMigrations("core/options", migrations); err != nil {
|
||||
return fmt.Errorf("register migrations: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE IF NOT EXISTS options (
|
||||
key VARCHAR(1024) NOT NULL UNIQUE,
|
||||
value VARCHAR(8192)
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE IF EXISTS options;
|
23
client/internal/services/core/options/models/option_pane.go
Normal file
23
client/internal/services/core/options/models/option_pane.go
Normal file
@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"bunker/client/internal/services/core/options/dto"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
// OptionPane это описание одной панели настроек в окне настроек.
|
||||
type OptionPane struct {
|
||||
Widget fyne.CanvasObject
|
||||
SaveHandler func() error
|
||||
Name string
|
||||
}
|
||||
|
||||
// OptionPaneFromDTO конвертирует параметры панели настроек из DTO во внутреннее представление.
|
||||
func OptionPaneFromDTO(dtoData *dto.OptionPane) *OptionPane {
|
||||
return &OptionPane{
|
||||
Widget: dtoData.Widget,
|
||||
SaveHandler: dtoData.SaveHandler,
|
||||
Name: dtoData.Name,
|
||||
}
|
||||
}
|
88
client/internal/services/core/options/options.go
Normal file
88
client/internal/services/core/options/options.go
Normal file
@ -0,0 +1,88 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"bunker/client/internal/application"
|
||||
"bunker/client/internal/services/core"
|
||||
"bunker/client/internal/services/core/options/models"
|
||||
)
|
||||
|
||||
var _ = core.Options(&options{})
|
||||
|
||||
type options struct {
|
||||
app *application.Application
|
||||
db core.Database
|
||||
mainWindow core.MainWindow
|
||||
|
||||
widgets map[string]*models.OptionPane
|
||||
widgetsItems []string // для рисования списка Fyne.
|
||||
}
|
||||
|
||||
// Initialize инициализирует сервис.
|
||||
func Initialize(app *application.Application) error {
|
||||
opts := &options{
|
||||
app: app,
|
||||
}
|
||||
|
||||
if err := app.RegisterService(opts); err != nil {
|
||||
return fmt.Errorf("%w: %w", core.ErrOptions, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *options) Configure() error {
|
||||
if err := o.registerMigrations(); err != nil {
|
||||
return fmt.Errorf("configure: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *options) ConnectDependencies() error {
|
||||
databaseRaw := o.app.Service(core.ServiceNameDatabase)
|
||||
if databaseRaw == nil {
|
||||
return fmt.Errorf("connect dependencies: get database service: %w", application.ErrServiceNotFound)
|
||||
}
|
||||
|
||||
database, valid := databaseRaw.(core.Database)
|
||||
if !valid {
|
||||
return fmt.Errorf("connect dependencies: type assert database service: %w", core.ErrDatabaseIsInvalid)
|
||||
}
|
||||
|
||||
o.db = database
|
||||
|
||||
mainWindowRaw := o.app.Service(core.ServiceNameMainWindow)
|
||||
if mainWindowRaw == nil {
|
||||
return fmt.Errorf("connect dependencies: get main window: %w", application.ErrServiceNotFound)
|
||||
}
|
||||
|
||||
mainWindow, valid := mainWindowRaw.(core.MainWindow)
|
||||
if !valid {
|
||||
return fmt.Errorf("connect dependencies: type assert main window: %w", core.ErrMainWindowIsInvalid)
|
||||
}
|
||||
|
||||
o.mainWindow = mainWindow
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *options) Initialize() error {
|
||||
o.widgets = make(map[string]*models.OptionPane)
|
||||
o.widgetsItems = make([]string, 0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *options) Name() string {
|
||||
return core.ServiceNameOptions
|
||||
}
|
||||
|
||||
func (o *options) LaunchStartupTasks() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *options) Shutdown() error {
|
||||
return nil
|
||||
}
|
28
client/internal/services/core/options/widgets.go
Normal file
28
client/internal/services/core/options/widgets.go
Normal file
@ -0,0 +1,28 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"bunker/client/internal/services/core"
|
||||
"bunker/client/internal/services/core/options/dto"
|
||||
"bunker/client/internal/services/core/options/models"
|
||||
)
|
||||
|
||||
var errWidgetPaneAlreadyRegistered = errors.New("widget pane already registered")
|
||||
|
||||
func (o *options) RegisterOptionsWidget(widgetData *dto.OptionPane) error {
|
||||
if _, found := o.widgets[widgetData.Name]; found {
|
||||
return fmt.Errorf(
|
||||
"%w: RegisterOptionsWidget: '%s': %w",
|
||||
core.ErrOptions,
|
||||
widgetData.Name,
|
||||
errWidgetPaneAlreadyRegistered,
|
||||
)
|
||||
}
|
||||
|
||||
o.widgets[widgetData.Name] = models.OptionPaneFromDTO(widgetData)
|
||||
o.widgetsItems = append(o.widgetsItems, widgetData.Name)
|
||||
|
||||
return nil
|
||||
}
|
18
client/internal/services/core/translations.go
Normal file
18
client/internal/services/core/translations.go
Normal file
@ -0,0 +1,18 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// ServiceNameTranslations это название для сервиса работы с переводами.
|
||||
const ServiceNameTranslations = "core/translations"
|
||||
|
||||
var (
|
||||
// ErrTranslations говорит о возникновении ошибки в сервисе работы с настройками.
|
||||
ErrTranslations = errors.New("translations core service")
|
||||
// ErrTranslationsIsInvalid говорит о неверной имплементации сервиса работы с переводами.
|
||||
ErrTranslationsIsInvalid = errors.New("translations service implementation is invalid")
|
||||
)
|
||||
|
||||
// Translations это интерфейс для сервиса работы с переводами.
|
||||
type Translations interface{}
|
@ -0,0 +1,74 @@
|
||||
package translations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"bunker/client/internal/langfiles"
|
||||
)
|
||||
|
||||
func (t *translations) initializeSysInfoHandler() error {
|
||||
if err := t.mainWindow.RegisterAboutWindowSysInfoHandler(
|
||||
"translations_languages_and_strings_count",
|
||||
t.sysInfoHandlerTranslationsLanguagesAndStringsCount,
|
||||
); err != nil {
|
||||
return fmt.Errorf("register translations_languages_and_strings_count handler: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *translations) sysInfoHandlerTranslationsLanguagesAndStringsCount() string {
|
||||
langString := "- Error occurred while getting translations languages and strings counts. See console log for details."
|
||||
|
||||
// Эту всю информацию надо получать напрямую из файлов, пакет Fyne не предоставляет нужную информацию.
|
||||
languages := make([]string, 0)
|
||||
translationsStringsCount := 0
|
||||
|
||||
entries, err := langfiles.LangFiles.ReadDir("files")
|
||||
if err != nil {
|
||||
slog.Error("Failed to read translations filesystem entries.", "error", err.Error())
|
||||
|
||||
return langString
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasSuffix(entry.Name(), ".json") {
|
||||
fileData, err := langfiles.LangFiles.ReadFile(filepath.Join("files", entry.Name()))
|
||||
if err != nil {
|
||||
slog.Error("Failed to read translation file!", "file", entry.Name(), "error", err.Error())
|
||||
|
||||
return langString
|
||||
}
|
||||
|
||||
data := make(map[string]string)
|
||||
|
||||
if err := json.Unmarshal(fileData, &data); err != nil {
|
||||
slog.Error("Failed to unmarshal translation file!", "file", entry.Name(), "error", err.Error())
|
||||
|
||||
return langString
|
||||
}
|
||||
|
||||
_, fileName := filepath.Split(entry.Name())
|
||||
langName := strings.Split(fileName, ".")[0]
|
||||
|
||||
languages = append(languages, langName)
|
||||
translationsStringsCount += len(data)
|
||||
}
|
||||
}
|
||||
|
||||
langString = fmt.Sprintf(
|
||||
"### Translations\n\n- Languages: %d\n- Translations strings (in total): %d\n",
|
||||
len(languages),
|
||||
translationsStringsCount,
|
||||
)
|
||||
|
||||
return langString
|
||||
}
|
81
client/internal/services/core/translations/translations.go
Normal file
81
client/internal/services/core/translations/translations.go
Normal file
@ -0,0 +1,81 @@
|
||||
package translations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"bunker/client/internal/application"
|
||||
"bunker/client/internal/langfiles"
|
||||
"bunker/client/internal/services/core"
|
||||
|
||||
"fyne.io/fyne/v2/lang"
|
||||
)
|
||||
|
||||
var _ = core.Translations(&translations{})
|
||||
|
||||
type translations struct {
|
||||
app *application.Application
|
||||
mainWindow core.MainWindow
|
||||
}
|
||||
|
||||
// Initialize инициализирует сервис.
|
||||
func Initialize(app *application.Application) error {
|
||||
transl := &translations{
|
||||
app: app,
|
||||
}
|
||||
|
||||
if err := app.RegisterService(transl); err != nil {
|
||||
return fmt.Errorf("%w: %w", core.ErrOptions, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *translations) Configure() error {
|
||||
if err := t.initializeSysInfoHandler(); err != nil {
|
||||
return fmt.Errorf("configure: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *translations) ConnectDependencies() error {
|
||||
mainWindowRaw := t.app.Service(core.ServiceNameMainWindow)
|
||||
if mainWindowRaw == nil {
|
||||
return fmt.Errorf("connect dependencies: get main window: %w", application.ErrServiceNotFound)
|
||||
}
|
||||
|
||||
mainWindow, valid := mainWindowRaw.(core.MainWindow)
|
||||
if !valid {
|
||||
return fmt.Errorf("connect dependencies: type assert main window: %w", core.ErrMainWindowIsInvalid)
|
||||
}
|
||||
|
||||
t.mainWindow = mainWindow
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *translations) Initialize() error {
|
||||
langFromEnv, _ := os.LookupEnv("LANG")
|
||||
|
||||
slog.Info("Current system locale.", "locale", lang.SystemLocale().String(), "LANG", langFromEnv)
|
||||
|
||||
if err := lang.AddTranslationsFS(langfiles.LangFiles, "files"); err != nil {
|
||||
return fmt.Errorf("%w: load translations: %w", core.ErrTranslations, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *translations) Name() string {
|
||||
return core.ServiceNameTranslations
|
||||
}
|
||||
|
||||
func (t *translations) LaunchStartupTasks() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *translations) Shutdown() error {
|
||||
return nil
|
||||
}
|
8
commons/appids.go
Normal file
8
commons/appids.go
Normal file
@ -0,0 +1,8 @@
|
||||
package commons
|
||||
|
||||
var (
|
||||
// ClientAppID это идентификатор для клиента.
|
||||
ClientAppID = "name.pztrn.bunker.client"
|
||||
// ServerAppID это идентификатор для сервера.
|
||||
ServerAppID = "name.pztrn.bunker.server"
|
||||
)
|
6
commons/exit_codes.go
Normal file
6
commons/exit_codes.go
Normal file
@ -0,0 +1,6 @@
|
||||
package commons
|
||||
|
||||
const (
|
||||
// ExitCodeAppStopFailed указывает на ошибку остановки работы приложения.
|
||||
ExitCodeAppStopFailed = 2
|
||||
)
|
14
commons/version.go
Normal file
14
commons/version.go
Normal file
@ -0,0 +1,14 @@
|
||||
package commons
|
||||
|
||||
var (
|
||||
// Branch это ветка, из которой собрано приложение.
|
||||
Branch string
|
||||
// Build это порядковый номер сборки.
|
||||
Build string
|
||||
// BuildDate это таймштамп сборки приложения.
|
||||
BuildDate string
|
||||
// Commit это коммит, из которого собрано приложение.
|
||||
Commit string
|
||||
// Version это версия, из которой собрано приложение.
|
||||
Version string
|
||||
)
|
66
go.mod
Normal file
66
go.mod
Normal file
@ -0,0 +1,66 @@
|
||||
module bunker
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.6.3
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/pressly/goose/v3 v3.25.0
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
modernc.org/sqlite v1.38.2
|
||||
)
|
||||
|
||||
require (
|
||||
fyne.io/systray v1.11.0 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fredbi/uri v1.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fyne-io/gl-js v0.2.0 // indirect
|
||||
github.com/fyne-io/glfw-js v0.3.0 // indirect
|
||||
github.com/fyne-io/image v0.1.1 // indirect
|
||||
github.com/fyne-io/oksvg v0.1.0 // indirect
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-text/render v0.2.0 // indirect
|
||||
github.com/go-text/typesetting v0.2.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
||||
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rymdport/portal v0.4.1 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // 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.11.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/image v0.24.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
170
go.sum
Normal file
170
go.sum
Normal file
@ -0,0 +1,170 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
fyne.io/fyne/v2 v2.6.3 h1:cvtM2KHeRuH+WhtHiA63z5wJVBkQ9+Ay0UMl9PxFHyA=
|
||||
fyne.io/fyne/v2 v2.6.3/go.mod h1:NGSurpRElVoI1G3h+ab2df3O5KLGh1CGbsMMcX0bPIs=
|
||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
|
||||
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
|
||||
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
|
||||
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
|
||||
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
|
||||
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
|
||||
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
|
||||
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
|
||||
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
|
||||
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
|
||||
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
|
||||
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
||||
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
|
||||
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjGng=
|
||||
github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
|
||||
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
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=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
33
scripts/version_generator.sh
Executable file
33
scripts/version_generator.sh
Executable file
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Generates version based on git tag.
|
||||
LATEST_TAG=$(git tag | tail -n 1)
|
||||
|
||||
# Check latest tag commit. If it is equal to current - use that tag as version.
|
||||
if [ "${LATEST_TAG}" != "" ]; then
|
||||
LATEST_TAG_COMMIT=$(git rev-list -n 1 $LATEST_TAG)
|
||||
LATEST_COMMIT=$(git rev-list -n 1 HEAD)
|
||||
|
||||
if [ "${LATEST_TAG_COMMIT}" == "${LATEST_COMMIT}" ]; then
|
||||
echo "${LATEST_TAG}"
|
||||
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
# No tags in repo. Assuming latest tag is "v0.0.0".
|
||||
LATEST_TAG="0.0.0"
|
||||
fi
|
||||
|
||||
# If we're here, then latest tag commit is not latest commit on current branch.
|
||||
# We should increase second number and add "-dev" postfix.
|
||||
IFS=. read MAJOR MINOR PATCH <<<"${LATEST_TAG}"
|
||||
MINOR=$(($MINOR + 1))
|
||||
|
||||
case $1 in
|
||||
client)
|
||||
echo "${MAJOR}.${MINOR}.${PATCH}"
|
||||
;;
|
||||
*)
|
||||
echo "v${MAJOR}.${MINOR}.${PATCH}-dev"
|
||||
;;
|
||||
esac
|
Loading…
x
Reference in New Issue
Block a user