feat(ui): content-measured History columns, shared caption widths, settings_view split

Stage 5: generalize logColumnWidth into textColumnWidth so History's
Trigger/Job/State/Detail/Log columns size from measured text instead of
pixel constants that clipped at larger text sizes (F6, F14).

Stage 6: captionColumnWidth replaces detailCaptionWidth and
settingsLabelWidth with one theme-derived helper; jobs_view_details.go
now builds its metadata rows and their width from a single
metadataRows() list instead of two hand-kept ones (F10); the Settings
button row drops its transparent-rectangle spacers for a
CustomPaddedLayout (F8); the remaining eight fyne.TextTruncate call
sites move to the non-deprecated Truncation field (N1).

Stage 7: settings_view.go split into settings_view.go (field
construction/save/load/validate), settings_view_layout.go (the
two-column layout and settingsSection/settingsRow), and
settings_view_helpers.go (fyneVersion, dialogs, path helpers),
mirroring the jobs_view.go split. Along the way, Queue/Storage's inline
VBox and Application/About's settingsSection collapse into one
settingsSection(title, spacing, rows...) constructor, and
chooseFile/chooseJSONFile merge into one function with a filter
argument.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mixeme
2026-07-27 16:26:13 +03:00
parent 57e6fe410e
commit cebd41a5ac
12 changed files with 621 additions and 269 deletions
+107
View File
@@ -0,0 +1,107 @@
package ui
import (
"errors"
"net/url"
"runtime/debug"
"strings"
"gitea.mixdep.ru/mix/gosentry/src/platform/filemanager"
"gitea.mixdep.ru/mix/gosentry/src/storage"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/dialog"
fynestorage "fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/widget"
)
func fyneVersion() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return "unknown"
}
for _, dependency := range info.Deps {
if dependency.Path == "fyne.io/fyne/v2" {
if dependency.Replace != nil && dependency.Replace.Version != "" {
return dependency.Replace.Version
}
if dependency.Version != "" {
return dependency.Version
}
return "local"
}
}
return "unknown"
}
func mustParseURL(raw string) *url.URL {
parsed, err := url.Parse(raw)
if err != nil {
return &url.URL{}
}
return parsed
}
// chooseFile opens a file picker that writes the chosen path into target. A
// nil filter offers every file, as job_dialog.go's command browser wants;
// chooseJSONFile passed an extension filter here before the two were merged,
// since they differed only by that one call.
func chooseFile(w fyne.Window, target *widget.Entry, filter fynestorage.FileFilter) {
fileDialog := dialog.NewFileOpen(func(uri fyne.URIReadCloser, err error) {
if err != nil || uri == nil {
return
}
target.SetText(uri.URI().Path())
}, w)
if filter != nil {
fileDialog.SetFilter(filter)
}
fileDialog.Resize(fyne.NewSize(900, 640))
fileDialog.Show()
}
// chooseJSONFile is chooseFile restricted to .json files, used for the jobs
// file so the picker does not list every file in the folder. The entry stays
// editable, which is how a path to a file that does not exist yet is entered.
func chooseJSONFile(w fyne.Window, target *widget.Entry) {
chooseFile(w, target, fynestorage.NewExtensionFileFilter([]string{".json"}))
}
func chooseFolder(w fyne.Window, target *widget.Entry) {
folderDialog := dialog.NewFolderOpen(func(uri fyne.ListableURI, err error) {
if err != nil || uri == nil {
return
}
target.SetText(uri.Path())
}, w)
// The default folder picker can be cramped on Windows. A larger size makes
// long paths readable and avoids forcing the user to resize it every time.
folderDialog.Resize(fyne.NewSize(900, 640))
folderDialog.Show()
}
// settingsFolderPath resolves what a directory field currently points at,
// applying the same relative-path rule the store uses when it loads the config
// so the folder that opens is the one the setting would use. Blank text has no
// folder to open and yields an empty path.
func settingsFolderPath(appDir string, text string) string {
trimmed := strings.TrimSpace(text)
if trimmed == "" {
return ""
}
return storage.ResolveConfiguredPath(appDir, trimmed)
}
// openFolder reveals dir in the desktop file manager. A folder that is not set
// or cannot be opened (most often: it does not exist yet, because the logs
// directory is created on the first run) is reported in a dialog rather than
// leaving the button looking dead.
func openFolder(w fyne.Window, dir string) {
if dir == "" {
dialog.ShowError(errors.New("no folder is set"), w)
return
}
if err := filemanager.Open(dir); err != nil {
dialog.ShowError(err, w)
}
}