Simplify KeePassGO desktop vault workflow
This commit is contained in:
@@ -7,6 +7,8 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -57,102 +59,111 @@ type uiSurface struct {
|
||||
Locked bool
|
||||
}
|
||||
|
||||
type sessionStatus interface {
|
||||
HasVault() bool
|
||||
IsLocked() bool
|
||||
IsRemote() bool
|
||||
}
|
||||
|
||||
type attachmentItem struct {
|
||||
Name string
|
||||
Size int
|
||||
}
|
||||
|
||||
type ui struct {
|
||||
mode string
|
||||
theme *material.Theme
|
||||
search widget.Editor
|
||||
vaultPath widget.Editor
|
||||
saveAsPath widget.Editor
|
||||
remoteBaseURL widget.Editor
|
||||
remotePath widget.Editor
|
||||
remoteUsername widget.Editor
|
||||
remotePassword widget.Editor
|
||||
masterPassword widget.Editor
|
||||
keyFilePath widget.Editor
|
||||
entryID widget.Editor
|
||||
entryTitle widget.Editor
|
||||
entryUsername widget.Editor
|
||||
entryPassword widget.Editor
|
||||
entryURL widget.Editor
|
||||
entryNotes widget.Editor
|
||||
entryTags widget.Editor
|
||||
entryPath widget.Editor
|
||||
entryFields widget.Editor
|
||||
historyIndex widget.Editor
|
||||
groupName widget.Editor
|
||||
passwordProfile widget.Editor
|
||||
attachmentName widget.Editor
|
||||
attachmentPath widget.Editor
|
||||
exportAttachmentPath widget.Editor
|
||||
list widget.List
|
||||
detailList widget.List
|
||||
copyUser widget.Clickable
|
||||
copyPass widget.Clickable
|
||||
copyURL widget.Clickable
|
||||
lockVault widget.Clickable
|
||||
unlockVault widget.Clickable
|
||||
createVault widget.Clickable
|
||||
openVault widget.Clickable
|
||||
saveVault widget.Clickable
|
||||
saveAsVault widget.Clickable
|
||||
openRemote widget.Clickable
|
||||
changeMasterKey widget.Clickable
|
||||
addEntry widget.Clickable
|
||||
saveEntry widget.Clickable
|
||||
duplicateEntry widget.Clickable
|
||||
deleteEntry widget.Clickable
|
||||
restoreEntry widget.Clickable
|
||||
saveTemplate widget.Clickable
|
||||
deleteTemplate widget.Clickable
|
||||
instantiateTemplate widget.Clickable
|
||||
addAttachment widget.Clickable
|
||||
replaceAttachment widget.Clickable
|
||||
removeAttachment widget.Clickable
|
||||
exportAttachment widget.Clickable
|
||||
restoreHistory widget.Clickable
|
||||
generatePassword widget.Clickable
|
||||
createGroup widget.Clickable
|
||||
renameGroup widget.Clickable
|
||||
deleteGroup widget.Clickable
|
||||
togglePasswordInline widget.Clickable
|
||||
showEntries widget.Clickable
|
||||
showTemplates widget.Clickable
|
||||
showRecycle widget.Clickable
|
||||
showLocalLifecycle widget.Clickable
|
||||
showRemoteLifecycle widget.Clickable
|
||||
masterKeyPasswordOnly widget.Clickable
|
||||
masterKeyKeyFileOnly widget.Clickable
|
||||
masterKeyComposite widget.Clickable
|
||||
entryClicks []widget.Clickable
|
||||
historyClicks []widget.Clickable
|
||||
attachmentClicks []widget.Clickable
|
||||
breadcrumbs []widget.Clickable
|
||||
groupClicks []widget.Clickable
|
||||
state appstate.State
|
||||
masterKeyMode vault.MasterKeyMode
|
||||
visible []entry
|
||||
currentPath []string
|
||||
syncedPath []string
|
||||
selectedHistoryIndex int
|
||||
showPassword bool
|
||||
togglePassword widget.Clickable
|
||||
phoneSplit widget.Float
|
||||
splitDrag gesture.Drag
|
||||
splitBase float32
|
||||
splitStartY float32
|
||||
phoneSpan int
|
||||
eyeIcon *widget.Icon
|
||||
eyeOffIcon *widget.Icon
|
||||
copyIcon *widget.Icon
|
||||
clipboardWriter clipboard.Writer
|
||||
loadingMessage string
|
||||
lifecycleMode string
|
||||
keyboardFocus focusID
|
||||
mode string
|
||||
theme *material.Theme
|
||||
search widget.Editor
|
||||
vaultPath widget.Editor
|
||||
saveAsPath widget.Editor
|
||||
remoteBaseURL widget.Editor
|
||||
remotePath widget.Editor
|
||||
remoteUsername widget.Editor
|
||||
remotePassword widget.Editor
|
||||
masterPassword widget.Editor
|
||||
keyFilePath widget.Editor
|
||||
entryID widget.Editor
|
||||
entryTitle widget.Editor
|
||||
entryUsername widget.Editor
|
||||
entryPassword widget.Editor
|
||||
entryURL widget.Editor
|
||||
entryNotes widget.Editor
|
||||
entryTags widget.Editor
|
||||
entryPath widget.Editor
|
||||
entryFields widget.Editor
|
||||
historyIndex widget.Editor
|
||||
groupName widget.Editor
|
||||
passwordProfile widget.Editor
|
||||
attachmentName widget.Editor
|
||||
attachmentPath widget.Editor
|
||||
exportAttachmentPath widget.Editor
|
||||
list widget.List
|
||||
detailList widget.List
|
||||
copyUser widget.Clickable
|
||||
copyPass widget.Clickable
|
||||
copyURL widget.Clickable
|
||||
lockVault widget.Clickable
|
||||
unlockVault widget.Clickable
|
||||
createVault widget.Clickable
|
||||
openVault widget.Clickable
|
||||
saveVault widget.Clickable
|
||||
saveAsVault widget.Clickable
|
||||
openRemote widget.Clickable
|
||||
changeMasterKey widget.Clickable
|
||||
synchronizeVault widget.Clickable
|
||||
editEntry widget.Clickable
|
||||
cancelEdit widget.Clickable
|
||||
pickVaultPath widget.Clickable
|
||||
pickKeyFile widget.Clickable
|
||||
addEntry widget.Clickable
|
||||
saveEntry widget.Clickable
|
||||
duplicateEntry widget.Clickable
|
||||
deleteEntry widget.Clickable
|
||||
restoreEntry widget.Clickable
|
||||
saveTemplate widget.Clickable
|
||||
deleteTemplate widget.Clickable
|
||||
instantiateTemplate widget.Clickable
|
||||
addAttachment widget.Clickable
|
||||
replaceAttachment widget.Clickable
|
||||
removeAttachment widget.Clickable
|
||||
exportAttachment widget.Clickable
|
||||
restoreHistory widget.Clickable
|
||||
generatePassword widget.Clickable
|
||||
createGroup widget.Clickable
|
||||
renameGroup widget.Clickable
|
||||
deleteGroup widget.Clickable
|
||||
togglePasswordInline widget.Clickable
|
||||
showEntries widget.Clickable
|
||||
showTemplates widget.Clickable
|
||||
showRecycle widget.Clickable
|
||||
showLocalLifecycle widget.Clickable
|
||||
showRemoteLifecycle widget.Clickable
|
||||
entryClicks []widget.Clickable
|
||||
historyClicks []widget.Clickable
|
||||
attachmentClicks []widget.Clickable
|
||||
breadcrumbs []widget.Clickable
|
||||
groupClicks []widget.Clickable
|
||||
state appstate.State
|
||||
visible []entry
|
||||
currentPath []string
|
||||
syncedPath []string
|
||||
selectedHistoryIndex int
|
||||
showPassword bool
|
||||
togglePassword widget.Clickable
|
||||
phoneSplit widget.Float
|
||||
splitDrag gesture.Drag
|
||||
splitBase float32
|
||||
splitStartY float32
|
||||
phoneSpan int
|
||||
eyeIcon *widget.Icon
|
||||
eyeOffIcon *widget.Icon
|
||||
copyIcon *widget.Icon
|
||||
clipboardWriter clipboard.Writer
|
||||
loadingMessage string
|
||||
lifecycleMode string
|
||||
keyboardFocus focusID
|
||||
defaultSaveAsPath string
|
||||
editingEntry bool
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -225,9 +236,9 @@ func newUIWithState(mode string, sess appstate.CurrentSession) *ui {
|
||||
List: layout.List{Axis: layout.Vertical},
|
||||
},
|
||||
state: appstate.State{},
|
||||
masterKeyMode: vault.MasterKeyModePasswordOnly,
|
||||
selectedHistoryIndex: -1,
|
||||
lifecycleMode: "local",
|
||||
defaultSaveAsPath: defaultSaveAsPath(),
|
||||
}
|
||||
u.state.Session = sess
|
||||
u.phoneSplit.Value = 0.46
|
||||
@@ -253,6 +264,14 @@ func (u *ui) filter() {
|
||||
}
|
||||
}
|
||||
|
||||
func defaultSaveAsPath() string {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil || strings.TrimSpace(cacheDir) == "" {
|
||||
cacheDir = os.TempDir()
|
||||
}
|
||||
return filepath.Join(cacheDir, "keepassgo", "vault.kdbx")
|
||||
}
|
||||
|
||||
func (u *ui) selectedAttachmentItems() []attachmentItem {
|
||||
item, ok := u.selectedEntry()
|
||||
if !ok || len(item.Attachments) == 0 {
|
||||
@@ -370,26 +389,11 @@ func (u *ui) ensureHistoryClickables() {
|
||||
func (u *ui) currentMasterKey() (vault.MasterKey, error) {
|
||||
password := u.masterPassword.Text()
|
||||
path := strings.TrimSpace(u.keyFilePath.Text())
|
||||
|
||||
switch u.masterKeyMode {
|
||||
case vault.MasterKeyModeKeyFileOnly:
|
||||
if path == "" {
|
||||
return vault.MasterKey{}, fmt.Errorf("key file is required")
|
||||
}
|
||||
case vault.MasterKeyModePasswordAndKeyFile:
|
||||
if password == "" {
|
||||
return vault.MasterKey{}, fmt.Errorf("master password is required")
|
||||
}
|
||||
if path == "" {
|
||||
return vault.MasterKey{}, fmt.Errorf("key file is required")
|
||||
}
|
||||
default:
|
||||
if path == "" {
|
||||
if password == "" {
|
||||
return vault.MasterKey{}, fmt.Errorf("master password is required")
|
||||
}
|
||||
return vault.MasterKey{Password: password}, nil
|
||||
}
|
||||
if password == "" && path == "" {
|
||||
return vault.MasterKey{}, fmt.Errorf("master password or key file is required")
|
||||
}
|
||||
if path == "" {
|
||||
return vault.MasterKey{Password: password}, nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
@@ -406,9 +410,7 @@ func (u *ui) currentMasterKey() (vault.MasterKey, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *ui) setMasterKeyMode(mode vault.MasterKeyMode) {
|
||||
u.masterKeyMode = mode
|
||||
}
|
||||
func (u *ui) setMasterKeyMode(vault.MasterKeyMode) {}
|
||||
|
||||
func (u *ui) createVaultAction() error {
|
||||
key, err := u.currentMasterKey()
|
||||
@@ -418,7 +420,14 @@ func (u *ui) createVaultAction() error {
|
||||
if err := u.state.CreateVault(key); err != nil {
|
||||
return err
|
||||
}
|
||||
if u.lifecycleMode == "local" {
|
||||
if err := u.state.SaveAs(u.saveAsTargetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
u.vaultPath.SetText(u.saveAsTargetPath())
|
||||
}
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
@@ -436,6 +445,7 @@ func (u *ui) openVaultAction() error {
|
||||
return err
|
||||
}
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
@@ -449,14 +459,11 @@ func (u *ui) saveAction() error {
|
||||
}
|
||||
|
||||
func (u *ui) saveAsAction() error {
|
||||
path := strings.TrimSpace(u.saveAsPath.Text())
|
||||
if path == "" {
|
||||
return errors.New(errSaveAsPathRequired)
|
||||
}
|
||||
|
||||
path := u.saveAsTargetPath()
|
||||
if err := u.state.SaveAs(path); err != nil {
|
||||
return err
|
||||
}
|
||||
u.vaultPath.SetText(path)
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
@@ -474,6 +481,7 @@ func (u *ui) openRemoteAction() error {
|
||||
if err := u.state.OpenRemoteVault(client, strings.TrimSpace(u.remotePath.Text()), key); err != nil {
|
||||
return err
|
||||
}
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
@@ -484,6 +492,7 @@ func (u *ui) lockAction() error {
|
||||
}
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.showPassword = false
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
@@ -497,6 +506,7 @@ func (u *ui) unlockAction() error {
|
||||
return err
|
||||
}
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
@@ -509,6 +519,22 @@ func (u *ui) changeMasterKeyAction() error {
|
||||
return u.state.ChangeMasterKey(key)
|
||||
}
|
||||
|
||||
func (u *ui) synchronizeAction() error {
|
||||
if err := u.state.Synchronize(); err != nil {
|
||||
return err
|
||||
}
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) saveAsTargetPath() string {
|
||||
path := strings.TrimSpace(u.saveAsPath.Text())
|
||||
if path != "" {
|
||||
return path
|
||||
}
|
||||
return u.defaultSaveAsPath
|
||||
}
|
||||
|
||||
func (u *ui) runAction(label string, action func() error) {
|
||||
u.loadingMessage = actionLoadingLabel(label)
|
||||
if err := action(); err != nil {
|
||||
@@ -572,6 +598,37 @@ func (u *ui) sessionSurface() uiSurface {
|
||||
return uiSurface{}
|
||||
}
|
||||
|
||||
func (u *ui) hasOpenVault() bool {
|
||||
status, ok := u.state.Session.(sessionStatus)
|
||||
if ok {
|
||||
return status.HasVault()
|
||||
}
|
||||
_, err := u.state.Session.Current()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (u *ui) isVaultLocked() bool {
|
||||
status, ok := u.state.Session.(sessionStatus)
|
||||
if ok {
|
||||
return status.IsLocked()
|
||||
}
|
||||
_, err := u.state.Session.Current()
|
||||
return errors.Is(err, session.ErrLocked)
|
||||
}
|
||||
|
||||
func (u *ui) shouldShowLifecycleSetup() bool {
|
||||
return !u.hasOpenVault()
|
||||
}
|
||||
|
||||
func (u *ui) chooseExistingFileAction(target *widget.Editor) error {
|
||||
path, err := pickExistingFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target.SetText(path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) listEmptyMessage() string {
|
||||
if surface := u.sessionSurface(); surface.Locked {
|
||||
return "Unlock the vault to browse entries and groups."
|
||||
@@ -661,18 +718,12 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
for u.changeMasterKey.Clicked(gtx) {
|
||||
u.runAction("change master key", u.changeMasterKeyAction)
|
||||
}
|
||||
for u.synchronizeVault.Clicked(gtx) {
|
||||
u.runAction("synchronize vault", u.synchronizeAction)
|
||||
}
|
||||
for u.unlockVault.Clicked(gtx) {
|
||||
u.runAction("unlock vault", u.unlockAction)
|
||||
}
|
||||
for u.masterKeyPasswordOnly.Clicked(gtx) {
|
||||
u.setMasterKeyMode(vault.MasterKeyModePasswordOnly)
|
||||
}
|
||||
for u.masterKeyKeyFileOnly.Clicked(gtx) {
|
||||
u.setMasterKeyMode(vault.MasterKeyModeKeyFileOnly)
|
||||
}
|
||||
for u.masterKeyComposite.Clicked(gtx) {
|
||||
u.setMasterKeyMode(vault.MasterKeyModePasswordAndKeyFile)
|
||||
}
|
||||
for u.showEntries.Clicked(gtx) {
|
||||
u.showEntriesSection()
|
||||
}
|
||||
@@ -691,10 +742,25 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
for u.lockVault.Clicked(gtx) {
|
||||
u.runAction("lock vault", u.lockAction)
|
||||
}
|
||||
for u.editEntry.Clicked(gtx) {
|
||||
u.editingEntry = true
|
||||
u.loadSelectedEntryIntoEditor()
|
||||
}
|
||||
for u.cancelEdit.Clicked(gtx) {
|
||||
u.editingEntry = false
|
||||
u.loadSelectedEntryIntoEditor()
|
||||
}
|
||||
for u.pickVaultPath.Clicked(gtx) {
|
||||
u.runAction("choose vault path", func() error { return u.chooseExistingFileAction(&u.vaultPath) })
|
||||
}
|
||||
for u.pickKeyFile.Clicked(gtx) {
|
||||
u.runAction("choose key file", func() error { return u.chooseExistingFileAction(&u.keyFilePath) })
|
||||
}
|
||||
for u.addEntry.Clicked(gtx) {
|
||||
u.state.BeginNewEntry()
|
||||
u.loadSelectedEntryIntoEditor()
|
||||
u.entryPath.SetText(strings.Join(u.state.CurrentPath, " / "))
|
||||
u.editingEntry = true
|
||||
}
|
||||
for u.saveEntry.Clicked(gtx) {
|
||||
u.runAction("save entry", u.saveEntryAction)
|
||||
@@ -778,6 +844,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
if u.shouldShowLifecycleSetup() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.mode == "phone" || gtx.Constraints.Max.X < gtx.Dp(unit.Dp(720)) {
|
||||
u.phoneSpan = gtx.Constraints.Max.Y
|
||||
listHeight := int(float32(gtx.Constraints.Max.Y) * u.phoneSplit.Value)
|
||||
@@ -821,15 +890,19 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions {
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
btn := material.Button(u.theme, &u.lockVault, "Lock")
|
||||
return btn.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(u.headerActions),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !u.shouldShowLifecycleSetup() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(u.lifecycleControls),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(u.lifecycleControls),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(u.feedbackBanner),
|
||||
)
|
||||
})
|
||||
@@ -843,20 +916,45 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions {
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
btn := material.Button(u.theme, &u.lockVault, "Lock")
|
||||
return btn.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(u.headerActions),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !u.shouldShowLifecycleSetup() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(u.lifecycleControls),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(u.lifecycleControls),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(u.feedbackBanner),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
|
||||
if u.shouldShowLifecycleSetup() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.isVaultLocked() {
|
||||
btn := material.Button(u.theme, &u.unlockVault, "Unlock")
|
||||
return btn.Layout(gtx)
|
||||
}
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
btn := material.Button(u.theme, &u.synchronizeVault, "Synchronize")
|
||||
return btn.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
btn := material.Button(u.theme, &u.lockVault, "Lock")
|
||||
return btn.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
panel := card
|
||||
spacing := unit.Dp(12)
|
||||
@@ -867,13 +965,33 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
u.ensureNavClickables()
|
||||
return panel(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(u.sectionBar),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.isVaultLocked() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return u.sectionBar(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
|
||||
layout.Rigid(u.pathBar),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.isVaultLocked() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return u.pathBar(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
|
||||
layout.Rigid(u.groupBar),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.isVaultLocked() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return u.groupBar(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
|
||||
layout.Rigid(u.groupControls),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.isVaultLocked() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return u.groupControls(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.mode == "phone" {
|
||||
@@ -888,6 +1006,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.isVaultLocked() || u.state.Section != appstate.SectionEntries {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
label := "Add Entry"
|
||||
if u.mode == "phone" {
|
||||
label = "+ Add Entry"
|
||||
@@ -1065,16 +1186,28 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions {
|
||||
panel = compactCard
|
||||
}
|
||||
return panel(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
item, ok := u.selectedEntry()
|
||||
if !ok {
|
||||
if u.isVaultLocked() {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
surface := u.sessionSurface()
|
||||
title := surface.Title
|
||||
if title == "" {
|
||||
title = "Entry details"
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(18), title)
|
||||
lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault")
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(14), "Enter the master password, choose a key file, or provide both.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(u.unlockPanel),
|
||||
)
|
||||
}
|
||||
item, ok := u.selectedEntry()
|
||||
if !ok && !u.editingEntry {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(18), "Entry details")
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
@@ -1084,7 +1217,20 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions {
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
)
|
||||
}
|
||||
if u.editingEntry {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
title := "New Entry"
|
||||
if ok {
|
||||
title = "Edit Entry"
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(18), title)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(u.entryEditorPanel),
|
||||
)
|
||||
}
|
||||
@@ -1156,7 +1302,40 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions {
|
||||
layout.Spacer{Height: unit.Dp(12)}.Layout,
|
||||
u.historyPanel,
|
||||
layout.Spacer{Height: unit.Dp(12)}.Layout,
|
||||
u.entryEditorPanel,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
switch u.state.Section {
|
||||
case appstate.SectionTemplates:
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.editEntry, "Edit Template")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.instantiateTemplate, "Instantiate")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.deleteTemplate, "Delete Template")
|
||||
}),
|
||||
)
|
||||
case appstate.SectionRecycleBin:
|
||||
return tonedButton(gtx, u.theme, &u.restoreEntry, "Restore Entry")
|
||||
default:
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.editEntry, "Edit")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.duplicateEntry, "Duplicate")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.deleteEntry, "Delete")
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
return material.List(u.theme, &u.detailList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
return rows[i](gtx)
|
||||
@@ -1310,7 +1489,7 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
|
||||
u.syncCurrentPath()
|
||||
crumbs := append([]string{"Vault"}, append([]string{}, u.currentPath...)...)
|
||||
crumbs := append([]string{"All Entries"}, append([]string{}, u.currentPath...)...)
|
||||
if u.state.Section == appstate.SectionTemplates {
|
||||
crumbs = append([]string{"Templates"}, append([]string{}, u.currentPath...)...)
|
||||
}
|
||||
@@ -1365,7 +1544,7 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.filter()
|
||||
}
|
||||
btn := material.Button(u.theme, &u.groupClicks[idx], "Folder: "+name)
|
||||
btn := material.Button(u.theme, &u.groupClicks[idx], name)
|
||||
btn.Background = color.NRGBA{R: 241, G: 236, B: 227, A: 255}
|
||||
btn.Color = accentColor
|
||||
btn.TextSize = unit.Sp(12)
|
||||
@@ -1575,6 +1754,18 @@ type uiSession struct {
|
||||
locked bool
|
||||
}
|
||||
|
||||
func (s *uiSession) HasVault() bool {
|
||||
return len(s.model.Entries) > 0 || len(s.model.Templates) > 0 || len(s.model.RecycleBin) > 0 || len(s.model.Groups) > 0 || s.locked
|
||||
}
|
||||
|
||||
func (s *uiSession) IsLocked() bool {
|
||||
return s.locked
|
||||
}
|
||||
|
||||
func (s *uiSession) IsRemote() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *uiSession) Current() (vault.Model, error) {
|
||||
if s.locked {
|
||||
return vault.Model{}, session.ErrLocked
|
||||
@@ -1598,3 +1789,25 @@ func (s *uiSession) Unlock(vault.MasterKey) error {
|
||||
s.locked = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func pickExistingFile() (string, error) {
|
||||
if path, err := runFilePicker("kdialog", "--getopenfilename", "--title", "Choose KeePass file"); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
if path, err := runFilePicker("zenity", "--file-selection", "--title=Choose KeePass file"); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
return "", fmt.Errorf("no supported file picker found; install kdialog or zenity")
|
||||
}
|
||||
|
||||
func runFilePicker(name string, args ...string) (string, error) {
|
||||
if _, err := exec.LookPath(name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
cmd := exec.Command(name, args...)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user