363 lines
14 KiB
Go
363 lines
14 KiB
Go
package appui
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget"
|
|
"gioui.org/widget/material"
|
|
"git.julianfamily.org/keepassgo/internal/appstate"
|
|
syncmodel "git.julianfamily.org/keepassgo/internal/appui/sync"
|
|
"git.julianfamily.org/keepassgo/internal/vault"
|
|
)
|
|
|
|
func (u *ui) syncButtonGroupWithMetrics(gtx layout.Context) (layout.Dimensions, layout.Dimensions, layout.Dimensions) {
|
|
spacing := unit.Dp(4)
|
|
if u.usesCompactViewport() {
|
|
spacing = unit.Dp(3)
|
|
}
|
|
var primaryDims layout.Dimensions
|
|
var toggleDims layout.Dimensions
|
|
groupDims := layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
primaryDims = syncPrimaryButton(gtx, u.theme, &u.synchronizeVault, "Sync", u.usesCompactViewport())
|
|
return primaryDims
|
|
}),
|
|
layout.Rigid(layout.Spacer{Width: spacing}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
toggleDims = u.syncMenuToggle(gtx)
|
|
return toggleDims
|
|
}),
|
|
)
|
|
return groupDims, primaryDims, toggleDims
|
|
}
|
|
|
|
func (u *ui) syncMenuToggle(gtx layout.Context) layout.Dimensions {
|
|
btn := material.IconButton(u.theme, &u.toggleSyncMenu, u.chevronDownIcon, "More synchronize actions")
|
|
if u.syncMenuOpen {
|
|
btn.Background = accentColor
|
|
btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
|
|
} else {
|
|
btn.Background = color.NRGBA{R: 231, G: 236, B: 232, A: 255}
|
|
btn.Color = accentColor
|
|
}
|
|
btn.Size = unit.Dp(18)
|
|
btn.Inset = layout.UniformInset(unit.Dp(8))
|
|
if u.usesCompactViewport() {
|
|
btn.Size = unit.Dp(16)
|
|
btn.Inset = layout.UniformInset(unit.Dp(7))
|
|
}
|
|
return btn.Layout(gtx)
|
|
}
|
|
|
|
func (u *ui) syncMenuWidget() layout.Widget {
|
|
if !u.syncMenuOpen {
|
|
return nil
|
|
}
|
|
return u.syncMenu
|
|
}
|
|
|
|
func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions {
|
|
model := u.buildSyncMenuModel()
|
|
profiles := u.availableRemoteProfiles()
|
|
credentials := u.availableRemoteCredentialEntries()
|
|
if len(u.vaultRemoteProfileClicks) < len(profiles) {
|
|
u.vaultRemoteProfileClicks = make([]widget.Clickable, len(profiles))
|
|
}
|
|
if len(u.vaultRemoteCredentialClicks) < len(credentials) {
|
|
u.vaultRemoteCredentialClicks = make([]widget.Clickable, len(credentials))
|
|
}
|
|
actionRows := u.syncMenuActionRows(model)
|
|
actionWidth := menuActionWidth(gtx, actionRows)
|
|
menu := func(gtx layout.Context) layout.Dimensions {
|
|
return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, u.syncMenuRows(model, profiles, credentials, actionWidth)...)
|
|
})
|
|
}
|
|
reserveWidth := u.syncMenuTrailingReserveWidth(gtx)
|
|
if reserveWidth <= 0 {
|
|
return menu(gtx)
|
|
}
|
|
return layout.Flex{}.Layout(gtx,
|
|
layout.Rigid(menu),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Dimensions{Size: image.Pt(reserveWidth, 0)}
|
|
}),
|
|
)
|
|
}
|
|
|
|
func (u *ui) syncMenuTrailingReserveWidth(gtx layout.Context) int {
|
|
spacing := gtx.Dp(unit.Dp(8))
|
|
if u.usesCompactViewport() {
|
|
spacing = gtx.Dp(unit.Dp(8))
|
|
}
|
|
|
|
measureGTX := gtx
|
|
measureGTX.Constraints.Min = image.Point{}
|
|
|
|
lockOps := op.Record(gtx.Ops)
|
|
lockDims := func(gtx layout.Context) layout.Dimensions {
|
|
btn := material.Button(u.theme, &u.lockVault, "Lock")
|
|
return btn.Layout(gtx)
|
|
}(measureGTX)
|
|
_ = lockOps.Stop()
|
|
|
|
menuOps := op.Record(gtx.Ops)
|
|
menuDims := u.mainMenuButtonGroup(measureGTX)
|
|
_ = menuOps.Stop()
|
|
|
|
return spacing + lockDims.Size.X + spacing + menuDims.Size.X
|
|
}
|
|
|
|
func (u *ui) syncMenuActionRows(model syncmodel.MenuModel) []layout.Widget {
|
|
rows := []layout.Widget{
|
|
func(gtx layout.Context) layout.Dimensions {
|
|
return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync")
|
|
},
|
|
}
|
|
if model.ShowShare {
|
|
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
|
return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault")
|
|
})
|
|
}
|
|
if model.ShowRemoteSyncSetupShortcut() {
|
|
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
|
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.RemoteSyncSetupShortcutLabel())
|
|
})
|
|
}
|
|
if model.ShowDirectRemoteSyncShortcut() {
|
|
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
|
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, model.DirectRemoteSyncShortcutLabel())
|
|
})
|
|
}
|
|
if model.ShowRemoteSyncSettingsShortcut() {
|
|
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
|
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.RemoteSyncSettingsShortcutLabel())
|
|
})
|
|
}
|
|
if model.ShowRemoveRemoteSyncShortcut() {
|
|
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
|
return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, model.RemoveRemoteSyncShortcutLabel())
|
|
})
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func (u *ui) syncMenuRows(model syncmodel.MenuModel, profiles []vault.RemoteProfile, credentials []vault.Entry, actionWidth int) []layout.FlexChild {
|
|
rows := u.syncMenuPrimaryRows(model, actionWidth)
|
|
rows = append(rows, u.syncMenuSavedBindingRows(model, profiles, credentials)...)
|
|
if model.ShowSaveCurrentBinding {
|
|
rows = append(rows, u.syncMenuSaveBindingRows(model)...)
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func (u *ui) syncMenuPrimaryRows(model syncmodel.MenuModel, actionWidth int) []layout.FlexChild {
|
|
rows := []layout.FlexChild{}
|
|
if model.ShowShare {
|
|
rows = append(rows, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
|
return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault")
|
|
})
|
|
}),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
)
|
|
}))
|
|
}
|
|
rows = append(rows, u.syncMenuActionRow(actionWidth, &u.openAdvancedSync, "Open Advanced Sync"))
|
|
if model.ShowRemoteSyncSetupShortcut() {
|
|
rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
|
rows = append(rows, u.syncMenuActionRow(actionWidth, &u.useSavedAdvancedSyncRemote, model.RemoteSyncSetupShortcutLabel()))
|
|
}
|
|
if model.ShowDirectRemoteSyncShortcut() {
|
|
rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
|
rows = append(rows, u.syncMenuActionRow(actionWidth, &u.openSelectedVaultRemote, model.DirectRemoteSyncShortcutLabel()))
|
|
}
|
|
if model.ShowRemoteSyncSettingsShortcut() {
|
|
rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
|
rows = append(rows, u.syncMenuActionRow(actionWidth, &u.useSavedAdvancedSyncRemote, model.RemoteSyncSettingsShortcutLabel()))
|
|
}
|
|
if model.ShowRemoveRemoteSyncShortcut() {
|
|
rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
|
rows = append(rows, u.syncMenuActionRow(actionWidth, &u.removeSelectedRemoteBinding, model.RemoveRemoteSyncShortcutLabel()))
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func (u *ui) syncMenuActionRow(actionWidth int, click *widget.Clickable, label string) layout.FlexChild {
|
|
return layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
|
return tonedButton(gtx, u.theme, click, label)
|
|
})
|
|
})
|
|
}
|
|
|
|
func (u *ui) syncMenuSavedBindingRows(model syncmodel.MenuModel, profiles []vault.RemoteProfile, credentials []vault.Entry) []layout.FlexChild {
|
|
if !u.hasOpenVault() || len(profiles) == 0 || len(credentials) == 0 {
|
|
return nil
|
|
}
|
|
rows := []layout.FlexChild{
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
lbl := material.Label(u.theme, unit.Sp(11), model.SavedBindingHeading())
|
|
lbl.Color = mutedColor
|
|
return lbl.Layout(gtx)
|
|
}),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
}
|
|
if !model.ShowSelectors {
|
|
rows = append(rows, layout.Rigid(u.syncMenuSavedBindingSummary(model)))
|
|
} else {
|
|
rows = append(rows, u.syncMenuSelectorRows(model, profiles, credentials)...)
|
|
}
|
|
if _, ok := u.selectedVaultRemoteProfile(); ok {
|
|
if _, ok := u.selectedVaultRemoteCredentialEntry(); ok {
|
|
rows = append(rows,
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.openSelectedVaultRemoteButtonLabel())
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func (u *ui) syncMenuSavedBindingSummary(model syncmodel.MenuModel) layout.Widget {
|
|
return func(gtx layout.Context) layout.Dimensions {
|
|
summary := model.SavedBindingSummary
|
|
if !summary.OK {
|
|
return layout.Dimensions{}
|
|
}
|
|
return layout.Background{}.Layout(gtx, fill(color.NRGBA{R: 242, G: 245, B: 240, A: 255}), 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(13), summary.ProfileLabel)
|
|
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(12), "Credential: "+summary.CredentialLabel)
|
|
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(12), summary.SyncLabel)
|
|
lbl.Color = mutedColor
|
|
return lbl.Layout(gtx)
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func (u *ui) syncMenuSaveBindingRows(model syncmodel.MenuModel) []layout.FlexChild {
|
|
return []layout.FlexChild{
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
lbl := material.Label(u.theme, unit.Sp(11), model.SaveCurrentRemoteBindingHeading())
|
|
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.saveCurrentRemoteBinding, model.SaveCurrentRemoteBindingButtonLabel())
|
|
}),
|
|
}
|
|
}
|
|
|
|
func (u *ui) syncMenuSelectorRows(_ syncmodel.MenuModel, profiles []vault.RemoteProfile, credentials []vault.Entry) []layout.FlexChild {
|
|
rows := make([]layout.FlexChild, 0, len(profiles)+len(credentials)+4)
|
|
for i, profile := range profiles {
|
|
i := i
|
|
profile := profile
|
|
rows = append(rows, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
selected := u.selectedVaultRemoteProfileID == profile.ID
|
|
return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions {
|
|
return u.vaultRemoteProfileClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
lbl := material.Label(u.theme, unit.Sp(13), profile.Name)
|
|
lbl.Color = accentColor
|
|
return lbl.Layout(gtx)
|
|
})
|
|
})
|
|
})
|
|
}))
|
|
}
|
|
rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
|
for i, entry := range credentials {
|
|
i := i
|
|
entry := entry
|
|
rows = append(rows, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
selected := u.selectedVaultRemoteCredentialEntryID == entry.ID
|
|
label := entry.Title
|
|
if entry.Username != "" {
|
|
label += " · " + entry.Username
|
|
}
|
|
return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions {
|
|
return u.vaultRemoteCredentialClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
lbl := material.Label(u.theme, unit.Sp(13), label)
|
|
lbl.Color = accentColor
|
|
return lbl.Layout(gtx)
|
|
})
|
|
})
|
|
})
|
|
}))
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func (u *ui) buildSyncMenuModel() syncmodel.MenuModel {
|
|
model := syncmodel.MenuModel{
|
|
HasOpenVault: u.hasOpenVault(),
|
|
ShowSelectors: u.shouldShowSavedRemoteBindingSelectors(),
|
|
ShowShare: supportsVaultShare(runtime.GOOS) && u.vaultSharer != nil && strings.TrimSpace(u.currentShareableVaultPath()) != "",
|
|
RemoteBaseURL: strings.TrimSpace(u.remoteBaseURL.Text()),
|
|
RemotePath: strings.TrimSpace(u.remotePath.Text()),
|
|
RemoteUsername: strings.TrimSpace(u.remoteUsername.Text()),
|
|
RemotePassword: u.remotePassword.Text(),
|
|
SelectedVaultSyncMode: normalizeUISyncMode(u.selectedVaultRemoteSyncMode),
|
|
}
|
|
_, model.HasSelectedBinding = u.selectedVaultRemoteBinding()
|
|
model.SavedBindingSummary = u.computeSavedRemoteBindingSummary()
|
|
model.ShowSaveCurrentBinding = model.HasOpenVault && model.RemoteBaseURL != "" && model.RemotePath != "" && model.RemoteUsername != "" && model.RemotePassword != ""
|
|
return model
|
|
}
|
|
|
|
func (u *ui) computeSavedRemoteBindingSummary() syncmodel.MenuBindingSummary {
|
|
profile, ok := u.selectedVaultRemoteProfile()
|
|
if !ok {
|
|
return syncmodel.MenuBindingSummary{}
|
|
}
|
|
entry, ok := u.selectedVaultRemoteCredentialEntry()
|
|
if !ok {
|
|
return syncmodel.MenuBindingSummary{}
|
|
}
|
|
credentialLabel := entry.Title
|
|
if strings.TrimSpace(entry.Username) != "" {
|
|
credentialLabel += " · " + strings.TrimSpace(entry.Username)
|
|
}
|
|
syncLabel := "Sync manually when you choose Use Remote Sync."
|
|
if normalizeUISyncMode(u.selectedVaultRemoteSyncMode) == appstate.SyncModeAutomaticOnOpenSave {
|
|
syncLabel = "Syncs automatically on open and save."
|
|
}
|
|
return syncmodel.MenuBindingSummary{
|
|
ProfileLabel: profile.Name,
|
|
CredentialLabel: credentialLabel,
|
|
SyncLabel: syncLabel,
|
|
OK: true,
|
|
}
|
|
}
|