Compare commits
9 Commits
1a11944423
..
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 70f18e89bf | |||
| c361ec5ba3 | |||
| 0c6d707325 | |||
| 1c72a5009f | |||
| 9aeb98da58 | |||
| c19bdd48e2 | |||
| e5f42924f8 | |||
| 13eb9028c7 | |||
| 5fa79bdf08 |
@@ -107,6 +107,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
app_version="$(git describe --tags --always --dirty)"
|
||||
# Gio needs a Linux ARM64 cgo cross-toolchain for desktop builds.
|
||||
# Keep the CI matrix to targets this runner can build reproducibly.
|
||||
for target in \
|
||||
@@ -126,14 +127,19 @@ jobs:
|
||||
ext=".exe"
|
||||
fi
|
||||
out="${DIST_DIR}/keepassgo-${goos}-${goarch}${ext}"
|
||||
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED="${cgo_enabled}" go build -o "${out}" .
|
||||
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED="${cgo_enabled}" \
|
||||
go build -ldflags "-X main.appVersion=${app_version}" -o "${out}" .
|
||||
done
|
||||
|
||||
- name: Build APK
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
make apk
|
||||
signkey_path="$(mktemp)"
|
||||
trap 'rm -f -- "$signkey_path"' EXIT
|
||||
printf '%s' '${{ secrets.APK_SIGNKEY_B64 }}' | base64 -d > "$signkey_path"
|
||||
export APP_VERSION="$(git describe --tags --always --dirty)"
|
||||
make apk SIGNKEY="$signkey_path" SIGNPASS='${{ secrets.APK_SIGNPASS }}'
|
||||
cp build/keepassgo.apk "${DIST_DIR}/keepassgo.apk"
|
||||
|
||||
- name: Upload CI artifacts
|
||||
|
||||
@@ -12,6 +12,7 @@ Environment:
|
||||
- `ANDROID_NDK_ROOT` defaults to `/opt/android-ndk`.
|
||||
- `JAVA_HOME` defaults to `/usr/lib/jvm/java-25-openjdk`.
|
||||
- `APP_ID` overrides the Android application id.
|
||||
- `APP_VERSION` overrides the version shown inside KeePassGO itself.
|
||||
- `APK_OUT` overrides the output path.
|
||||
- `APK_VERSION` overrides the packaged app version.
|
||||
- `ANDROID_MIN_SDK` overrides the minimum supported Android SDK.
|
||||
|
||||
@@ -5,8 +5,20 @@ PATH := $(JAVA_HOME)/bin:$(ANDROID_SDK_ROOT)/cmdline-tools/latest/bin:$(ANDROID_
|
||||
APP_ID ?= org.julianfamily.keepassgo
|
||||
APK_OUT ?= build/keepassgo.apk
|
||||
APK_VERSION ?= 0.1.0.1
|
||||
APP_VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||
GO_LDFLAGS ?= -X main.appVersion=$(APP_VERSION)
|
||||
ANDROID_MIN_SDK ?= 28
|
||||
ANDROID_TARGET_SDK ?= 35
|
||||
SIGNKEY ?=
|
||||
SIGNPASS ?=
|
||||
|
||||
GOGIO_SIGN_FLAGS :=
|
||||
ifneq ($(strip $(SIGNKEY)),)
|
||||
GOGIO_SIGN_FLAGS += -signkey $(SIGNKEY)
|
||||
endif
|
||||
ifneq ($(strip $(SIGNPASS)),)
|
||||
GOGIO_SIGN_FLAGS += -signpass $(SIGNPASS)
|
||||
endif
|
||||
|
||||
.PHONY: apk
|
||||
apk: android/keepassgo-android.jar
|
||||
@@ -24,6 +36,8 @@ apk: android/keepassgo-android.jar
|
||||
go tool gogio -target android \
|
||||
-buildmode exe \
|
||||
-appid $(APP_ID) \
|
||||
-ldflags "$(GO_LDFLAGS)" \
|
||||
$(GOGIO_SIGN_FLAGS) \
|
||||
-o $(APK_OUT) \
|
||||
-version $(APK_VERSION) \
|
||||
-minsdk $(ANDROID_MIN_SDK) \
|
||||
|
||||
@@ -41,6 +41,31 @@ Desktop build:
|
||||
go build ./...
|
||||
```
|
||||
|
||||
By default, build outputs stamp the app version from `git describe --tags --always --dirty`.
|
||||
You can override the version shown in KeePassGO with:
|
||||
|
||||
```bash
|
||||
go build -ldflags "-X main.appVersion=v0.0.1" ./...
|
||||
```
|
||||
|
||||
## Arch Linux Package
|
||||
|
||||
An AUR-style package definition for the Linux desktop client lives under:
|
||||
|
||||
- `packaging/archlinux/keepassgo-git/`
|
||||
|
||||
From that directory you can build and install it with:
|
||||
|
||||
```bash
|
||||
makepkg -si
|
||||
```
|
||||
|
||||
The package installs:
|
||||
|
||||
- `/usr/bin/keepassgo`
|
||||
- a desktop entry at `/usr/share/applications/keepassgo.desktop`
|
||||
- application icons under the hicolor theme
|
||||
|
||||
## Android Packaging
|
||||
|
||||
KeePassGO uses Gio, so Android packaging is done with `gogio`.
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ const (
|
||||
SectionRecycleBin Section = "recycle-bin"
|
||||
SectionAPITokens Section = "api-tokens"
|
||||
SectionAPIAudit Section = "api-audit"
|
||||
SectionAbout Section = "about"
|
||||
)
|
||||
|
||||
type CurrentSession interface {
|
||||
@@ -376,7 +377,7 @@ func (s *State) entriesForSection(model vault.Model) []vault.Entry {
|
||||
return slices.Clone(model.Templates)
|
||||
case SectionRecycleBin:
|
||||
return slices.Clone(model.RecycleBin)
|
||||
case SectionAPITokens, SectionAPIAudit:
|
||||
case SectionAPITokens, SectionAPIAudit, SectionAbout:
|
||||
return nil
|
||||
default:
|
||||
return slices.Clone(model.Entries)
|
||||
|
||||
@@ -13,6 +13,7 @@ const (
|
||||
DefaultAppID = "org.julianfamily.keepassgo"
|
||||
DefaultAPKOut = "build/keepassgo.apk"
|
||||
DefaultVersion = "0.1.0.1"
|
||||
DefaultLdflags = "-X main.appVersion=dev"
|
||||
DefaultMinSDK = "28"
|
||||
DefaultTargetSDK = "35"
|
||||
DefaultIconPath = "assets/keepassgo-icon.png"
|
||||
@@ -25,6 +26,7 @@ type Config struct {
|
||||
AppID string
|
||||
APKOut string
|
||||
Version string
|
||||
Ldflags string
|
||||
MinSDK string
|
||||
TargetSDK string
|
||||
IconPath string
|
||||
@@ -38,6 +40,7 @@ func DefaultConfig() Config {
|
||||
AppID: DefaultAppID,
|
||||
APKOut: DefaultAPKOut,
|
||||
Version: DefaultVersion,
|
||||
Ldflags: DefaultLdflags,
|
||||
MinSDK: DefaultMinSDK,
|
||||
TargetSDK: DefaultTargetSDK,
|
||||
IconPath: DefaultIconPath,
|
||||
@@ -49,6 +52,7 @@ func (c Config) GogioArgs() []string {
|
||||
"-target", "android",
|
||||
"-buildmode", "exe",
|
||||
"-appid", c.AppID,
|
||||
"-ldflags", c.Ldflags,
|
||||
"-o", c.APKOut,
|
||||
"-version", c.Version,
|
||||
"-minsdk", c.MinSDK,
|
||||
|
||||
@@ -15,6 +15,7 @@ func TestDefaultConfigGogioArgs(t *testing.T) {
|
||||
"-target", "android",
|
||||
"-buildmode", "exe",
|
||||
"-appid", DefaultAppID,
|
||||
"-ldflags", DefaultLdflags,
|
||||
"-o", DefaultAPKOut,
|
||||
"-version", DefaultVersion,
|
||||
"-minsdk", DefaultMinSDK,
|
||||
@@ -52,6 +53,7 @@ func TestValidateAcceptsCompleteAndroidToolchainLayout(t *testing.T) {
|
||||
AppID: DefaultAppID,
|
||||
APKOut: DefaultAPKOut,
|
||||
Version: DefaultVersion,
|
||||
Ldflags: DefaultLdflags,
|
||||
MinSDK: DefaultMinSDK,
|
||||
TargetSDK: DefaultTargetSDK,
|
||||
IconPath: filepath.Join(root, "icon.png"),
|
||||
@@ -77,6 +79,7 @@ func TestValidateRejectsMissingPrerequisites(t *testing.T) {
|
||||
AppID: DefaultAppID,
|
||||
APKOut: DefaultAPKOut,
|
||||
Version: DefaultVersion,
|
||||
Ldflags: DefaultLdflags,
|
||||
MinSDK: DefaultMinSDK,
|
||||
TargetSDK: DefaultTargetSDK,
|
||||
}
|
||||
|
||||
@@ -43,6 +43,15 @@ import (
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
var appVersion = "dev"
|
||||
|
||||
func currentAppVersion() string {
|
||||
if strings.TrimSpace(appVersion) == "" {
|
||||
return "dev"
|
||||
}
|
||||
return strings.TrimSpace(appVersion)
|
||||
}
|
||||
|
||||
type entry = vault.Entry
|
||||
|
||||
const (
|
||||
@@ -334,6 +343,7 @@ type ui struct {
|
||||
showRecycle widget.Clickable
|
||||
showAPITokens widget.Clickable
|
||||
showAPIAudit widget.Clickable
|
||||
showAbout widget.Clickable
|
||||
showLocalLifecycle widget.Clickable
|
||||
showRemoteLifecycle widget.Clickable
|
||||
showSyncLocal widget.Clickable
|
||||
@@ -422,6 +432,7 @@ type ui struct {
|
||||
syncDialogOpen bool
|
||||
syncMenuOpen bool
|
||||
mainMenuOpen bool
|
||||
selectedRemoteConnection bool
|
||||
securityDialogOpen bool
|
||||
remotePrefsDialogOpen bool
|
||||
showSyncPassword bool
|
||||
@@ -685,6 +696,8 @@ func (u *ui) searchPlaceholder() string {
|
||||
return "Search audit log"
|
||||
case appstate.SectionRecycleBin:
|
||||
return "Search recycle bin"
|
||||
case appstate.SectionAbout:
|
||||
return "Search disabled on About"
|
||||
default:
|
||||
return "Search vault"
|
||||
}
|
||||
@@ -814,6 +827,14 @@ func (u *ui) showAPIAuditSection() {
|
||||
u.filter()
|
||||
}
|
||||
|
||||
func (u *ui) showAboutSection() {
|
||||
u.resetPasswordPeek()
|
||||
u.rememberEntriesSectionState()
|
||||
u.state.ShowSection(appstate.SectionAbout)
|
||||
u.mainMenuOpen = false
|
||||
u.filter()
|
||||
}
|
||||
|
||||
func (u *ui) returnToMainEntries() {
|
||||
u.clearDeleteGroupConfirmation()
|
||||
u.showEntriesSection()
|
||||
@@ -1752,12 +1773,16 @@ func (u *ui) restoreStartupLifecycleTarget() {
|
||||
func (u *ui) hasSelectedLifecycleTarget() bool {
|
||||
switch strings.TrimSpace(u.lifecycleMode) {
|
||||
case "remote":
|
||||
return strings.TrimSpace(u.remoteBaseURL.Text()) != "" && strings.TrimSpace(u.remotePath.Text()) != ""
|
||||
return u.hasSelectedRemoteTarget()
|
||||
default:
|
||||
return strings.TrimSpace(u.vaultPath.Text()) != ""
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) hasSelectedRemoteTarget() bool {
|
||||
return u.selectedRemoteConnection
|
||||
}
|
||||
|
||||
func (u *ui) latestRecentVault() (string, time.Time) {
|
||||
for _, path := range u.recentVaults {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
@@ -1776,6 +1801,50 @@ func (u *ui) showLocalVaultChooser() bool {
|
||||
return u.lifecycleMode != "local" || !u.hasSelectedVaultPath()
|
||||
}
|
||||
|
||||
func (u *ui) showRemoteConnectionChooser() bool {
|
||||
return u.lifecycleMode != "remote" || !u.hasSelectedRemoteTarget()
|
||||
}
|
||||
|
||||
func (u *ui) switchToLifecycleSelection(mode string) {
|
||||
u.state.Session = &session.Manager{}
|
||||
u.state.CurrentPath = nil
|
||||
u.state.SelectedEntryID = ""
|
||||
u.state.Section = appstate.SectionEntries
|
||||
u.state.Dirty = false
|
||||
u.state.ErrorMessage = ""
|
||||
u.state.StatusMessage = ""
|
||||
u.loadingMessage = ""
|
||||
u.loadingActionLabel = ""
|
||||
u.lastLifecycleAction = ""
|
||||
u.lifecycleMode = mode
|
||||
u.editingEntry = false
|
||||
u.currentPath = nil
|
||||
u.syncedPath = nil
|
||||
u.clearMasterPassword()
|
||||
u.keyFilePath.SetText("")
|
||||
u.search.SetText("")
|
||||
switch mode {
|
||||
case "remote":
|
||||
u.vaultPath.SetText("")
|
||||
u.remoteBaseURL.SetText("")
|
||||
u.remotePath.SetText("")
|
||||
u.remoteUsername.SetText("")
|
||||
u.remotePassword.SetText("")
|
||||
u.rememberRemoteAuth.Value = false
|
||||
u.selectedRemoteConnection = false
|
||||
default:
|
||||
u.vaultPath.SetText("")
|
||||
u.remoteBaseURL.SetText("")
|
||||
u.remotePath.SetText("")
|
||||
u.remoteUsername.SetText("")
|
||||
u.remotePassword.SetText("")
|
||||
u.rememberRemoteAuth.Value = false
|
||||
u.selectedRemoteConnection = false
|
||||
}
|
||||
u.requestMasterPassFocus = u.hasSelectedLifecycleTarget()
|
||||
u.filter()
|
||||
}
|
||||
|
||||
func (u *ui) latestRecentRemote() (recentRemoteRecord, bool, time.Time) {
|
||||
for _, record := range u.recentRemotes {
|
||||
if strings.TrimSpace(record.BaseURL) == "" || strings.TrimSpace(record.Path) == "" {
|
||||
@@ -1819,6 +1888,7 @@ func (u *ui) applyRecentRemoteRecord(record recentRemoteRecord) {
|
||||
u.remotePassword.SetText(record.Password)
|
||||
u.remotePassword.Mask = '•'
|
||||
u.rememberRemoteAuth.Value = strings.TrimSpace(record.Username) != "" || record.Password != ""
|
||||
u.selectedRemoteConnection = true
|
||||
}
|
||||
|
||||
func (u *ui) remotePreferencesCurrentSummary() string {
|
||||
@@ -2706,6 +2776,11 @@ func (u *ui) listEmptyState() emptyState {
|
||||
Title: "No API audit events yet",
|
||||
Body: "Connect a trusted client, respond to approval prompts, or issue a token to start recording activity.",
|
||||
}
|
||||
case appstate.SectionAbout:
|
||||
return emptyState{
|
||||
Title: "About KeePassGO",
|
||||
Body: "Product details, compatibility notes, and platform targets appear in the detail pane.",
|
||||
}
|
||||
case appstate.SectionTemplates:
|
||||
return emptyState{
|
||||
Title: "Templates unavailable",
|
||||
@@ -2744,6 +2819,8 @@ func (u *ui) detailPlaceholderMessage() string {
|
||||
return "Select an API token, issue a new one, or search to narrow the list."
|
||||
case appstate.SectionAPIAudit:
|
||||
return "Select an audit event to inspect it, or use Search audit log or the quick filters above."
|
||||
case appstate.SectionAbout:
|
||||
return "Review the product overview, platform support, and compatibility goals."
|
||||
case appstate.SectionTemplates:
|
||||
return "Select a template or start a reusable entry."
|
||||
case appstate.SectionRecycleBin:
|
||||
@@ -2961,6 +3038,10 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
u.clearDeleteGroupConfirmation()
|
||||
u.showAPIAuditSection()
|
||||
}
|
||||
for u.showAbout.Clicked(gtx) {
|
||||
u.clearDeleteGroupConfirmation()
|
||||
u.showAboutSection()
|
||||
}
|
||||
for u.showLocalLifecycle.Clicked(gtx) {
|
||||
if u.lifecycleBusy() {
|
||||
continue
|
||||
@@ -2973,6 +3054,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
continue
|
||||
}
|
||||
u.lifecycleMode = "remote"
|
||||
u.selectedRemoteConnection = false
|
||||
u.requestMasterPassFocus = true
|
||||
}
|
||||
for u.toggleLifecycleAdvanced.Clicked(gtx) {
|
||||
@@ -3153,6 +3235,10 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
if u.lifecycleBusy() {
|
||||
continue
|
||||
}
|
||||
if u.shouldUseLockedSinglePane() {
|
||||
u.switchToLifecycleSelection("local")
|
||||
continue
|
||||
}
|
||||
u.vaultPath.SetText("")
|
||||
u.state.ErrorMessage = ""
|
||||
u.state.StatusMessage = ""
|
||||
@@ -3162,6 +3248,11 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
if u.lifecycleBusy() {
|
||||
continue
|
||||
}
|
||||
if u.shouldUseLockedSinglePane() {
|
||||
u.switchToLifecycleSelection("remote")
|
||||
continue
|
||||
}
|
||||
u.selectedRemoteConnection = false
|
||||
u.remoteBaseURL.SetText("")
|
||||
u.remotePath.SetText("")
|
||||
u.remoteUsername.SetText("")
|
||||
@@ -4137,6 +4228,10 @@ func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.showAbout, "About")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings")
|
||||
}),
|
||||
@@ -4144,6 +4239,70 @@ func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions {
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) aboutDetailPanel(gtx layout.Context) layout.Dimensions {
|
||||
rows := []layout.Widget{
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(22), "KeePassGO")
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(15), "A KeePass-compatible password manager built in Go.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(14)}.Layout,
|
||||
aboutFact(u.theme, "Compatibility", "KeePass and KDBX interoperability", "Designed to coexist with desktop KeePass and KeePass2Android workflows."),
|
||||
layout.Spacer{Height: unit.Dp(10)}.Layout,
|
||||
aboutFact(u.theme, "Platforms", "Windows and Linux first, Android supported", "Desktop remains the primary product surface while Android stays compatible."),
|
||||
layout.Spacer{Height: unit.Dp(10)}.Layout,
|
||||
aboutFact(u.theme, "Sync", "Local files and direct WebDAV", "Remote-file workflows are first-class and avoid browser-stack dependencies."),
|
||||
layout.Spacer{Height: unit.Dp(10)}.Layout,
|
||||
aboutFact(u.theme, "Programmatic Access", "Secure local gRPC API", "Built for trusted clients such as browser extensions and automation."),
|
||||
layout.Spacer{Height: unit.Dp(14)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Version")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(14), currentAppVersion())
|
||||
lbl.Color = u.theme.Palette.Fg
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
}
|
||||
return material.List(u.theme, &u.detailList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
return rows[i](gtx)
|
||||
})
|
||||
}
|
||||
|
||||
func aboutFact(theme *material.Theme, title, primary, secondary string) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(theme, unit.Sp(12), strings.ToUpper(title))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(theme, unit.Sp(16), primary)
|
||||
lbl.Color = theme.Palette.Fg
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(theme, unit.Sp(13), secondary)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions {
|
||||
label := "Sync"
|
||||
spacing := unit.Dp(4)
|
||||
@@ -4254,21 +4413,23 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
return panel(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
visibleEntries, entryClicks := u.visibleEntrySnapshot()
|
||||
rows := make([]layout.Widget, 0, 16+len(visibleEntries))
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
|
||||
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder())
|
||||
editor.Color = u.theme.Palette.Fg
|
||||
editor.HintColor = mutedColor
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
|
||||
if u.state.Section != appstate.SectionAbout {
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
|
||||
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder())
|
||||
editor.Color = u.theme.Palette.Fg
|
||||
editor.HintColor = mutedColor
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
|
||||
})
|
||||
})
|
||||
})
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Height: spacing}.Layout(gtx)
|
||||
})
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Height: spacing}.Layout(gtx)
|
||||
})
|
||||
}
|
||||
if !u.isVaultLocked() {
|
||||
rows = append(rows, u.navigationHeader)
|
||||
if u.state.Section == appstate.SectionEntries {
|
||||
if u.state.Section == appstate.SectionEntries || u.state.Section == appstate.SectionAbout {
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Height: spacing}.Layout(gtx)
|
||||
})
|
||||
@@ -4298,6 +4459,8 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
return btn.Layout(gtx)
|
||||
case appstate.SectionAPITokens:
|
||||
return tonedButton(gtx, u.theme, &u.issueAPIToken, "Issue API Token")
|
||||
case appstate.SectionAbout:
|
||||
return emptyStatePanel(gtx, u.theme, u.listEmptyState())
|
||||
default:
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
@@ -4311,6 +4474,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
rows = append(rows, u.apiTokenListPanel)
|
||||
case u.state.Section == appstate.SectionAPIAudit:
|
||||
rows = append(rows, u.apiAuditListPanel)
|
||||
case u.state.Section == appstate.SectionAbout:
|
||||
case len(visibleEntries) == 0:
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
return emptyStatePanel(gtx, u.theme, u.listEmptyState())
|
||||
@@ -4337,6 +4501,18 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
return u.navigationHeader(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.state.Section == appstate.SectionAbout {
|
||||
return emptyStatePanel(gtx, u.theme, u.listEmptyState())
|
||||
}
|
||||
return layout.Dimensions{}
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.state.Section == appstate.SectionAbout {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: spacing}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.isVaultLocked() || (u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionRecycleBin) {
|
||||
return layout.Dimensions{}
|
||||
@@ -4359,6 +4535,9 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.state.Section == appstate.SectionAbout {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.mode == "phone" {
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
}
|
||||
@@ -4371,6 +4550,9 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.state.Section == appstate.SectionAbout {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.isVaultLocked() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
@@ -4396,6 +4578,9 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
if u.state.Section == appstate.SectionAPIAudit {
|
||||
return u.apiAuditListPanel(gtx)
|
||||
}
|
||||
if u.state.Section == appstate.SectionAbout {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if len(u.visible) == 0 {
|
||||
return emptyStatePanel(gtx, u.theme, u.listEmptyState())
|
||||
}
|
||||
@@ -4411,13 +4596,23 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions {
|
||||
if u.mode == "phone" {
|
||||
if u.state.Section != appstate.SectionEntries {
|
||||
if u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionAbout {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.state.Section == appstate.SectionAbout {
|
||||
lbl := material.Label(u.theme, unit.Sp(18), "About")
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}
|
||||
return u.groupControlsDisclosure(gtx)
|
||||
}
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
if u.state.Section == appstate.SectionAbout {
|
||||
lbl := material.Label(u.theme, unit.Sp(18), "About")
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}
|
||||
return u.sectionBar(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -4692,7 +4887,6 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
|
||||
_ = panel
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
|
||||
if u.isVaultLocked() {
|
||||
summary := u.currentVaultSummary()
|
||||
return []layout.FlexChild{
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault")
|
||||
@@ -4705,47 +4899,6 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if strings.TrimSpace(summary.Title) == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "UNLOCK TARGET")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(15), summary.Title)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if strings.TrimSpace(summary.Detail) == "" || summary.Detail == summary.Title {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), summary.Detail)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if strings.TrimSpace(summary.Context) == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), summary.Context)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(u.unlockPanel),
|
||||
}
|
||||
@@ -4760,6 +4913,11 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
|
||||
layout.Flexed(1, u.apiAuditDetailPanel),
|
||||
}
|
||||
}
|
||||
if u.state.Section == appstate.SectionAbout {
|
||||
return []layout.FlexChild{
|
||||
layout.Flexed(1, u.aboutDetailPanel),
|
||||
}
|
||||
}
|
||||
item, ok := u.selectedEntry()
|
||||
if !ok && !u.editingEntry {
|
||||
return []layout.FlexChild{
|
||||
@@ -5472,32 +5630,22 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
|
||||
if atRoot {
|
||||
u.phoneGroupBrowserExpanded = true
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if len(groups) == 0 {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
maxY := gtx.Dp(unit.Dp(168))
|
||||
if gtx.Constraints.Max.Y > maxY {
|
||||
gtx.Constraints.Max.Y = maxY
|
||||
}
|
||||
if gtx.Constraints.Min.Y > gtx.Constraints.Max.Y {
|
||||
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||
}
|
||||
return material.List(u.theme, &u.groupList).Layout(gtx, len(groups), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
idx := i
|
||||
name := groups[i]
|
||||
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
for u.groupClicks[idx].Clicked(gtx) {
|
||||
u.state.EnterGroup(name)
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.filter()
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
|
||||
})
|
||||
children := make([]layout.FlexChild, 0, len(groups))
|
||||
for i := range groups {
|
||||
idx := i
|
||||
name := groups[i]
|
||||
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
for u.groupClicks[idx].Clicked(gtx) {
|
||||
u.state.EnterGroup(name)
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.filter()
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}))
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
+239
@@ -775,6 +775,37 @@ func TestUIPhoneGroupBrowserToggleDoesNotChangeCurrentGroupToolsState(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIPhoneGroupBarDoesNotClampScrollableContentHeight(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("phone", vault.Model{
|
||||
Groups: [][]string{
|
||||
{"Crew"},
|
||||
{"Crew", "One"},
|
||||
{"Crew", "Two"},
|
||||
{"Crew", "Three"},
|
||||
{"Crew", "Four"},
|
||||
{"Crew", "Five"},
|
||||
{"Crew", "Six"},
|
||||
{"Crew", "Seven"},
|
||||
{"Crew", "Eight"},
|
||||
},
|
||||
})
|
||||
u.setCurrentPath([]string{"Crew"})
|
||||
|
||||
ops := new(op.Ops)
|
||||
gtx := layout.Context{
|
||||
Ops: ops,
|
||||
Constraints: layout.Exact(image.Pt(1080, 2400)),
|
||||
}
|
||||
|
||||
dims := u.groupBar(gtx)
|
||||
minOldCap := gtx.Dp(unit.Dp(220))
|
||||
if dims.Size.Y <= minOldCap {
|
||||
t.Fatalf("groupBar() phone height = %d, want > %d to avoid nested-scroll clamp", dims.Size.Y, minOldCap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIPhoneStartsWithGroupToolsCollapsed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -4377,6 +4408,165 @@ func TestShowLocalVaultChooser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowRemoteConnectionChooser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("desktop", &session.Manager{})
|
||||
u.lifecycleMode = "remote"
|
||||
u.remoteBaseURL.SetText("")
|
||||
u.remotePath.SetText("")
|
||||
if got := u.showRemoteConnectionChooser(); !got {
|
||||
t.Fatal("showRemoteConnectionChooser() = false, want true when no remote connection is selected")
|
||||
}
|
||||
|
||||
u.remoteBaseURL.SetText("https://dav.crew.example.invalid")
|
||||
u.remotePath.SetText("vaults/bellagio.kdbx")
|
||||
if got := u.showRemoteConnectionChooser(); !got {
|
||||
t.Fatal("showRemoteConnectionChooser() = false, want true while manually entering a remote connection")
|
||||
}
|
||||
|
||||
u.applyRecentRemoteRecord(recentRemoteRecord{
|
||||
BaseURL: "https://dav.crew.example.invalid",
|
||||
Path: "vaults/bellagio.kdbx",
|
||||
})
|
||||
if got := u.showRemoteConnectionChooser(); got {
|
||||
t.Fatal("showRemoteConnectionChooser() = true, want false after selecting a saved remote connection")
|
||||
}
|
||||
|
||||
u.lifecycleMode = "local"
|
||||
if got := u.showRemoteConnectionChooser(); !got {
|
||||
t.Fatal("showRemoteConnectionChooser() = false, want true outside remote lifecycle mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyingRecentRemoteRecordMarksSelectedRemoteConnection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("desktop", &session.Manager{})
|
||||
if u.hasSelectedRemoteTarget() {
|
||||
t.Fatal("hasSelectedRemoteTarget() = true, want false before selecting a saved remote connection")
|
||||
}
|
||||
|
||||
u.applyRecentRemoteRecord(recentRemoteRecord{
|
||||
BaseURL: "https://dav.crew.example.invalid",
|
||||
Path: "vaults/bellagio.kdbx",
|
||||
Username: "dannyocean",
|
||||
Password: "topsecret",
|
||||
})
|
||||
|
||||
if !u.hasSelectedRemoteTarget() {
|
||||
t.Fatal("hasSelectedRemoteTarget() = false, want true after selecting a saved remote connection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchToLifecycleSelectionResetsLockedLocalSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("desktop", summarySession{hasVault: true, locked: true})
|
||||
u.lifecycleMode = "local"
|
||||
u.vaultPath.SetText("/vaults/bellagio.kdbx")
|
||||
u.remoteBaseURL.SetText("https://dav.crew.example.invalid")
|
||||
u.remotePath.SetText("vaults/remote.kdbx")
|
||||
u.remoteUsername.SetText("dannyocean")
|
||||
u.remotePassword.SetText("topsecret")
|
||||
u.rememberRemoteAuth.Value = true
|
||||
u.masterPassword.SetText("correct horse battery staple")
|
||||
u.keyFilePath.SetText("/vaults/keyfile.keyx")
|
||||
u.search.SetText("crew")
|
||||
u.state.CurrentPath = []string{"Crew"}
|
||||
u.state.SelectedEntryID = "entry-1"
|
||||
u.state.Section = appstate.SectionTemplates
|
||||
u.state.Dirty = true
|
||||
|
||||
u.switchToLifecycleSelection("local")
|
||||
|
||||
if !u.shouldShowLifecycleSetup() {
|
||||
t.Fatal("shouldShowLifecycleSetup() = false, want true after switching away from locked local vault")
|
||||
}
|
||||
if got := u.lifecycleMode; got != "local" {
|
||||
t.Fatalf("lifecycleMode = %q, want local", got)
|
||||
}
|
||||
if got := u.vaultPath.Text(); got != "" {
|
||||
t.Fatalf("vaultPath = %q, want empty", got)
|
||||
}
|
||||
if got := u.remoteBaseURL.Text(); got != "" {
|
||||
t.Fatalf("remoteBaseURL = %q, want empty", got)
|
||||
}
|
||||
if got := u.remotePath.Text(); got != "" {
|
||||
t.Fatalf("remotePath = %q, want empty", got)
|
||||
}
|
||||
if got := u.remoteUsername.Text(); got != "" {
|
||||
t.Fatalf("remoteUsername = %q, want empty", got)
|
||||
}
|
||||
if got := u.remotePassword.Text(); got != "" {
|
||||
t.Fatalf("remotePassword = %q, want empty", got)
|
||||
}
|
||||
if u.rememberRemoteAuth.Value {
|
||||
t.Fatal("rememberRemoteAuth = true, want false")
|
||||
}
|
||||
if got := u.masterPassword.Text(); got != "" {
|
||||
t.Fatalf("masterPassword = %q, want empty", got)
|
||||
}
|
||||
if got := u.keyFilePath.Text(); got != "" {
|
||||
t.Fatalf("keyFilePath = %q, want empty", got)
|
||||
}
|
||||
if got := u.search.Text(); got != "" {
|
||||
t.Fatalf("search = %q, want empty", got)
|
||||
}
|
||||
if got := u.state.Section; got != appstate.SectionEntries {
|
||||
t.Fatalf("state.Section = %q, want %q", got, appstate.SectionEntries)
|
||||
}
|
||||
if len(u.state.CurrentPath) != 0 {
|
||||
t.Fatalf("state.CurrentPath = %v, want empty", u.state.CurrentPath)
|
||||
}
|
||||
if got := u.state.SelectedEntryID; got != "" {
|
||||
t.Fatalf("state.SelectedEntryID = %q, want empty", got)
|
||||
}
|
||||
if u.state.Dirty {
|
||||
t.Fatal("state.Dirty = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchToLifecycleSelectionResetsLockedRemoteSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("desktop", summarySession{hasVault: true, locked: true, remote: true})
|
||||
u.lifecycleMode = "local"
|
||||
u.vaultPath.SetText("/vaults/bellagio.kdbx")
|
||||
u.remoteBaseURL.SetText("https://dav.crew.example.invalid")
|
||||
u.remotePath.SetText("vaults/remote.kdbx")
|
||||
u.remoteUsername.SetText("rustyryan")
|
||||
u.remotePassword.SetText("topsecret")
|
||||
u.rememberRemoteAuth.Value = true
|
||||
|
||||
u.switchToLifecycleSelection("remote")
|
||||
|
||||
if !u.shouldShowLifecycleSetup() {
|
||||
t.Fatal("shouldShowLifecycleSetup() = false, want true after switching away from locked remote vault")
|
||||
}
|
||||
if got := u.lifecycleMode; got != "remote" {
|
||||
t.Fatalf("lifecycleMode = %q, want remote", got)
|
||||
}
|
||||
if got := u.vaultPath.Text(); got != "" {
|
||||
t.Fatalf("vaultPath = %q, want empty", got)
|
||||
}
|
||||
if got := u.remoteBaseURL.Text(); got != "" {
|
||||
t.Fatalf("remoteBaseURL = %q, want empty", got)
|
||||
}
|
||||
if got := u.remotePath.Text(); got != "" {
|
||||
t.Fatalf("remotePath = %q, want empty", got)
|
||||
}
|
||||
if got := u.remoteUsername.Text(); got != "" {
|
||||
t.Fatalf("remoteUsername = %q, want empty", got)
|
||||
}
|
||||
if got := u.remotePassword.Text(); got != "" {
|
||||
t.Fatalf("remotePassword = %q, want empty", got)
|
||||
}
|
||||
if u.rememberRemoteAuth.Value {
|
||||
t.Fatal("rememberRemoteAuth = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectingRecentRemoteSwitchesToRemoteMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -5354,6 +5544,55 @@ func TestUISearchPlaceholderIsContextual(t *testing.T) {
|
||||
if got := u.searchPlaceholder(); got != "Search audit log" {
|
||||
t.Fatalf("api audit searchPlaceholder() = %q, want %q", got, "Search audit log")
|
||||
}
|
||||
u.showAboutSection()
|
||||
if got := u.searchPlaceholder(); got != "Search disabled on About" {
|
||||
t.Fatalf("about searchPlaceholder() = %q, want %q", got, "Search disabled on About")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowAboutSection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Entries: []vault.Entry{{ID: "entry-1", Title: "Bellagio", Path: []string{"Crew"}}},
|
||||
})
|
||||
u.mainMenuOpen = true
|
||||
u.state.CurrentPath = []string{"Crew"}
|
||||
u.state.SelectedEntryID = "entry-1"
|
||||
|
||||
u.showAboutSection()
|
||||
|
||||
if got := u.state.Section; got != appstate.SectionAbout {
|
||||
t.Fatalf("state.Section = %q, want %q", got, appstate.SectionAbout)
|
||||
}
|
||||
if u.mainMenuOpen {
|
||||
t.Fatal("mainMenuOpen = true, want false")
|
||||
}
|
||||
if len(u.state.CurrentPath) != 0 {
|
||||
t.Fatalf("state.CurrentPath = %v, want empty", u.state.CurrentPath)
|
||||
}
|
||||
if got := u.state.SelectedEntryID; got != "" {
|
||||
t.Fatalf("state.SelectedEntryID = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentAppVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
previous := appVersion
|
||||
t.Cleanup(func() {
|
||||
appVersion = previous
|
||||
})
|
||||
|
||||
appVersion = ""
|
||||
if got := currentAppVersion(); got != "dev" {
|
||||
t.Fatalf("currentAppVersion() with empty version = %q, want dev", got)
|
||||
}
|
||||
|
||||
appVersion = " v0.0.1 "
|
||||
if got := currentAppVersion(); got != "v0.0.1" {
|
||||
t.Fatalf("currentAppVersion() with linker version = %q, want v0.0.1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIAPIPolicyTargetActionsUseCurrentContext(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
pkgbase = keepassgo-git
|
||||
pkgdesc = KeePass-compatible password manager written in Go
|
||||
pkgver = r160.5fa79bd
|
||||
pkgrel = 1
|
||||
url = https://git.julianfamily.org/joejulian/keepassgo
|
||||
arch = x86_64
|
||||
arch = aarch64
|
||||
license = custom
|
||||
makedepends = git
|
||||
makedepends = go
|
||||
makedepends = pkgconf
|
||||
depends = glibc
|
||||
depends = hicolor-icon-theme
|
||||
depends = libx11
|
||||
depends = libxcursor
|
||||
depends = libxfixes
|
||||
depends = libxkbcommon
|
||||
depends = libxkbcommon-x11
|
||||
depends = mesa
|
||||
depends = wayland
|
||||
provides = keepassgo
|
||||
conflicts = keepassgo
|
||||
source = git+https://git.julianfamily.org/joejulian/keepassgo.git
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = keepassgo-git
|
||||
@@ -0,0 +1,65 @@
|
||||
pkgname=keepassgo-git
|
||||
pkgver=r0.0000000
|
||||
pkgrel=1
|
||||
pkgdesc='KeePass-compatible password manager written in Go'
|
||||
arch=('x86_64' 'aarch64')
|
||||
url='https://git.julianfamily.org/joejulian/keepassgo'
|
||||
license=('custom')
|
||||
depends=(
|
||||
'glibc'
|
||||
'hicolor-icon-theme'
|
||||
'libx11'
|
||||
'libxcursor'
|
||||
'libxfixes'
|
||||
'libxkbcommon'
|
||||
'libxkbcommon-x11'
|
||||
'mesa'
|
||||
'wayland'
|
||||
)
|
||||
makedepends=(
|
||||
'git'
|
||||
'go'
|
||||
'pkgconf'
|
||||
)
|
||||
provides=('keepassgo')
|
||||
conflicts=('keepassgo')
|
||||
source=('git+https://git.julianfamily.org/joejulian/keepassgo.git')
|
||||
sha256sums=('SKIP')
|
||||
|
||||
_repo_dir() {
|
||||
if [[ -d "${srcdir}/keepassgo/.git" ]]; then
|
||||
printf '%s\n' "${srcdir}/keepassgo"
|
||||
return
|
||||
fi
|
||||
|
||||
cd "${startdir}/../../.." || exit 1
|
||||
pwd
|
||||
}
|
||||
|
||||
pkgver() {
|
||||
cd "$(_repo_dir)"
|
||||
printf 'r%s.%s' "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$(_repo_dir)"
|
||||
export CGO_ENABLED=1
|
||||
export GOFLAGS="-trimpath"
|
||||
local app_version
|
||||
app_version="$(git describe --tags --always --dirty)"
|
||||
go build -ldflags "-X main.appVersion=${app_version}" -o keepassgo .
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$(_repo_dir)"
|
||||
|
||||
install -Dm755 keepassgo "${pkgdir}/usr/bin/keepassgo"
|
||||
install -Dm644 assets/keepassgo-icon.png \
|
||||
"${pkgdir}/usr/share/icons/hicolor/512x512/apps/keepassgo.png"
|
||||
install -Dm644 assets/keepassgo-icon.svg \
|
||||
"${pkgdir}/usr/share/icons/hicolor/scalable/apps/keepassgo.svg"
|
||||
install -Dm644 packaging/archlinux/keepassgo-git/keepassgo.desktop \
|
||||
"${pkgdir}/usr/share/applications/keepassgo.desktop"
|
||||
install -Dm644 README.md \
|
||||
"${pkgdir}/usr/share/licenses/${pkgname}/README.md"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=KeePassGO
|
||||
Comment=KeePass-compatible password manager
|
||||
Exec=keepassgo
|
||||
Icon=keepassgo
|
||||
Terminal=false
|
||||
Categories=Utility;Security;
|
||||
Keywords=keepass;password;vault;kdbx;
|
||||
StartupNotify=true
|
||||
+264
-130
@@ -21,6 +21,8 @@ import (
|
||||
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||
busy := u.lifecycleBusy()
|
||||
showLocalChooser := u.showLocalVaultChooser()
|
||||
showRemoteChooser := u.showRemoteConnectionChooser()
|
||||
selectedLocalPath := strings.TrimSpace(u.vaultPath.Text())
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "OPEN A VAULT")
|
||||
@@ -60,104 +62,121 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||
if u.lifecycleMode == "remote" {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "LOCATION")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if strings.TrimSpace(u.remoteBaseURL.Text()) == "" || strings.TrimSpace(u.remotePath.Text()) == "" {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
record := u.currentRemoteRecord()
|
||||
lastGroup := u.recentRemoteGroup(record.BaseURL, record.Path)
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "SELECTED CONNECTION")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(14), friendlyRecentRemoteLabel(record))
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Path: "+strings.TrimSpace(record.Path))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Server: "+strings.TrimSpace(record.BaseURL))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Auth: "+recentRemoteStoredAuthSummary(recentRemoteRecord{
|
||||
Username: strings.TrimSpace(u.remoteUsername.Text()),
|
||||
Password: u.remotePassword.Text(),
|
||||
}))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if len(lastGroup) == 0 {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, "Change...")
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return u.recentRemoteList(gtx)
|
||||
return labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if showRemoteChooser || !u.hasSelectedRemoteTarget() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Dimensions{}
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if showRemoteChooser && !busy {
|
||||
return u.recentRemoteList(gtx)
|
||||
}
|
||||
return layout.Dimensions{}
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
box := material.CheckBox(u.theme, &u.rememberRemoteAuth, "Remember sign-in on this device")
|
||||
box.Color = accentColor
|
||||
return box.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Inset{Top: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openRemotePrefsHelp, "Settings & Help")
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showRemoteChooser {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
@@ -202,52 +221,13 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
selectedPath := strings.TrimSpace(u.vaultPath.Text())
|
||||
switch {
|
||||
case busy:
|
||||
return labeledEditorHelp(u.theme, "Vault Path", localVaultPathHelp(), &u.vaultPath, false)(gtx)
|
||||
case selectedPath == "":
|
||||
case selectedLocalPath == "":
|
||||
return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx)
|
||||
default:
|
||||
lastGroup := u.recentVaultGroup(selectedPath)
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "SELECTED VAULT")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(16), friendlyRecentVaultLabel(selectedPath))
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
dir := compactPathDirectorySummary(selectedPath)
|
||||
if dir == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(11), dir)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if len(lastGroup) == 0 {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Open Different Vault")
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -318,10 +298,26 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.lifecycleMode == "remote" {
|
||||
label := u.remoteOpenButtonLabel()
|
||||
if busy {
|
||||
return passiveTonedButton(gtx, u.theme, label)
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.openRemote, label)
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy {
|
||||
return passiveTonedButton(gtx, u.theme, label)
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.openRemote, label)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy || !u.hasSelectedRemoteTarget() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy || !u.hasSelectedRemoteTarget() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return u.selectedRemoteConnectionCard(gtx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -347,11 +343,118 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy || selectedLocalPath == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy || selectedLocalPath == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return u.selectedLocalVaultCard(gtx, selectedLocalPath)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (u *ui) selectedRemoteConnectionCard(gtx layout.Context) layout.Dimensions {
|
||||
record := u.currentRemoteRecord()
|
||||
lastGroup := u.recentRemoteGroup(record.BaseURL, record.Path)
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "SELECTED CONNECTION")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(14), friendlyRecentRemoteLabel(record))
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Path: "+strings.TrimSpace(record.Path))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Server: "+strings.TrimSpace(record.BaseURL))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Auth: "+recentRemoteStoredAuthSummary(recentRemoteRecord{
|
||||
Username: strings.TrimSpace(u.remoteUsername.Text()),
|
||||
Password: u.remotePassword.Text(),
|
||||
}))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if len(lastGroup) == 0 {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, "Open Different Connection")
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) selectedLocalVaultCard(gtx layout.Context, path string) layout.Dimensions {
|
||||
lastGroup := u.recentVaultGroup(path)
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "SELECTED VAULT")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(16), friendlyRecentVaultLabel(path))
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
dir := compactPathDirectorySummary(path)
|
||||
if dir == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(11), dir)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if len(lastGroup) == 0 {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Open Different Vault")
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) lifecycleSecuritySettingsSummary() string {
|
||||
return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices."
|
||||
}
|
||||
@@ -1234,12 +1337,14 @@ func selectorEditorHelp(th *material.Theme, label, help string, editor *widget.E
|
||||
func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
|
||||
targetLabel := "Locked vault"
|
||||
targetValue := "Unlock the active vault to continue."
|
||||
changeLabel := "Open Different Vault"
|
||||
if u.state.Session != nil {
|
||||
if strings.TrimSpace(u.remoteBaseURL.Text()) != "" || strings.TrimSpace(u.remotePath.Text()) != "" {
|
||||
baseURL := strings.TrimSpace(u.remoteBaseURL.Text())
|
||||
path := strings.TrimSpace(u.remotePath.Text())
|
||||
targetLabel = "Remote vault"
|
||||
targetValue = friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: path})
|
||||
changeLabel = "Open Different Connection"
|
||||
if strings.TrimSpace(targetValue) == "" {
|
||||
targetValue = "Remote WebDAV vault"
|
||||
}
|
||||
@@ -1255,25 +1360,42 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
}
|
||||
}
|
||||
targetCard := func(gtx layout.Context) layout.Dimensions {
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), strings.ToUpper(targetLabel))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Body1(u.theme, targetValue)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !u.shouldUseLockedSinglePane() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Inset{Top: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
if targetLabel == "Remote vault" {
|
||||
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, changeLabel)
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.clearVaultSelection, changeLabel)
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), strings.ToUpper(targetLabel))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Body1(u.theme, targetValue)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
if u.mode == "desktop" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return targetCard(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -1285,6 +1407,18 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock")
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.mode != "desktop" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.mode != "desktop" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return targetCard(gtx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user