The very basic client app, not adapted for mobiles.
This commit is contained in:
		
							
								
								
									
										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 | ||||
		Reference in New Issue
	
	Block a user