Normalize app UI pane packages
This commit is contained in:
@@ -1,95 +0,0 @@
|
|||||||
package actions
|
|
||||||
|
|
||||||
import "git.julianfamily.org/keepassgo/internal/appstate"
|
|
||||||
|
|
||||||
type SyncMenuModel struct {
|
|
||||||
HasOpenVault bool
|
|
||||||
HasSelectedBinding bool
|
|
||||||
ShowSelectors bool
|
|
||||||
ShowShare bool
|
|
||||||
ShowSaveCurrentBinding bool
|
|
||||||
SavedBindingSummary SyncMenuBindingSummary
|
|
||||||
RemoteBaseURL string
|
|
||||||
RemotePath string
|
|
||||||
RemoteUsername string
|
|
||||||
RemotePassword string
|
|
||||||
SelectedVaultSyncMode appstate.SyncMode
|
|
||||||
}
|
|
||||||
|
|
||||||
type SyncMenuBindingSummary struct {
|
|
||||||
ProfileLabel string
|
|
||||||
CredentialLabel string
|
|
||||||
SyncLabel string
|
|
||||||
OK bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) SavedBindingHeading() string {
|
|
||||||
if !m.ShowSelectors {
|
|
||||||
return "Use this vault's saved remote sync target"
|
|
||||||
}
|
|
||||||
return "Use a saved remote profile from this vault"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) OpenSelectedButtonLabel() string {
|
|
||||||
if !m.ShowSelectors {
|
|
||||||
return "Use Remote Sync"
|
|
||||||
}
|
|
||||||
return "Open Saved Remote"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) ShowDirectRemoteSyncShortcut() bool {
|
|
||||||
return m.HasOpenVault && m.HasSelectedBinding
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) DirectRemoteSyncShortcutLabel() string {
|
|
||||||
return "Use Remote Sync"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) ShowRemoteSyncSettingsShortcut() bool {
|
|
||||||
return m.HasOpenVault && m.HasSelectedBinding
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) RemoteSyncSettingsShortcutLabel() string {
|
|
||||||
return "Remote Sync Settings"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) ShowRemoveRemoteSyncShortcut() bool {
|
|
||||||
return m.ShowRemoteSyncSettingsShortcut()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) RemoveRemoteSyncShortcutLabel() string {
|
|
||||||
return "Stop Using Remote Sync"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) ShowRemoteSyncSetupShortcut() bool {
|
|
||||||
return m.HasOpenVault && !m.HasSelectedBinding
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) RemoteSyncSetupShortcutLabel() string {
|
|
||||||
return "Set Up Remote Sync"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) ActionLabels() []string {
|
|
||||||
labels := []string{"Open Advanced Sync"}
|
|
||||||
if m.ShowRemoteSyncSetupShortcut() {
|
|
||||||
labels = append(labels, m.RemoteSyncSetupShortcutLabel())
|
|
||||||
}
|
|
||||||
if m.ShowDirectRemoteSyncShortcut() {
|
|
||||||
labels = append(labels, m.DirectRemoteSyncShortcutLabel())
|
|
||||||
}
|
|
||||||
if m.ShowRemoteSyncSettingsShortcut() {
|
|
||||||
labels = append(labels, m.RemoteSyncSettingsShortcutLabel())
|
|
||||||
}
|
|
||||||
if m.ShowRemoveRemoteSyncShortcut() {
|
|
||||||
labels = append(labels, m.RemoveRemoteSyncShortcutLabel())
|
|
||||||
}
|
|
||||||
return labels
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) SaveCurrentRemoteBindingHeading() string {
|
|
||||||
return "Bind this local vault to the current remote target"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SyncMenuModel) SaveCurrentRemoteBindingButtonLabel() string {
|
|
||||||
return "Save Remote In Vault"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.julianfamily.org/keepassgo/internal/apiaudit"
|
||||||
|
"git.julianfamily.org/keepassgo/internal/apitokens"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuditQuickFilter struct {
|
||||||
|
Label string
|
||||||
|
Query string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Operations() []apitokens.Operation {
|
||||||
|
return []apitokens.Operation{
|
||||||
|
apitokens.OperationListEntries,
|
||||||
|
apitokens.OperationListGroups,
|
||||||
|
apitokens.OperationReadEntry,
|
||||||
|
apitokens.OperationCopyPassword,
|
||||||
|
apitokens.OperationCopyUsername,
|
||||||
|
apitokens.OperationCopyURL,
|
||||||
|
apitokens.OperationMutateEntry,
|
||||||
|
apitokens.OperationMutateGroup,
|
||||||
|
apitokens.OperationManageVault,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuditDecisionLabel(eventType apiaudit.EventType) string {
|
||||||
|
switch eventType {
|
||||||
|
case apiaudit.EventApprovalRequested:
|
||||||
|
return "Requested"
|
||||||
|
case apiaudit.EventApprovalAllowed:
|
||||||
|
return "Allowed"
|
||||||
|
case apiaudit.EventApprovalDenied:
|
||||||
|
return "Denied"
|
||||||
|
case apiaudit.EventApprovalCanceled:
|
||||||
|
return "Canceled"
|
||||||
|
case apiaudit.EventApprovalTimedOut:
|
||||||
|
return "Timed Out"
|
||||||
|
case apiaudit.EventAuthRejected:
|
||||||
|
return "Auth Rejected"
|
||||||
|
default:
|
||||||
|
return strings.ReplaceAll(string(eventType), "_", " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuditOperationLabel(operation apitokens.Operation) string {
|
||||||
|
if strings.TrimSpace(string(operation)) == "" {
|
||||||
|
return "Other"
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(string(operation), "_", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompactAuditFilterLabel(label string) string {
|
||||||
|
label = strings.TrimSpace(label)
|
||||||
|
if len(label) <= 22 {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
return label[:19] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuditEventSearchTerms(event apiaudit.Event) string {
|
||||||
|
parts := []string{
|
||||||
|
string(event.Type),
|
||||||
|
AuditDecisionLabel(event.Type),
|
||||||
|
event.TokenName,
|
||||||
|
event.ClientName,
|
||||||
|
string(event.Operation),
|
||||||
|
AuditOperationLabel(event.Operation),
|
||||||
|
strings.Join(event.Resource.Path, " / "),
|
||||||
|
event.Resource.EntryID,
|
||||||
|
event.Message,
|
||||||
|
}
|
||||||
|
switch event.Type {
|
||||||
|
case apiaudit.EventApprovalAllowed:
|
||||||
|
parts = append(parts, "allow approved")
|
||||||
|
case apiaudit.EventApprovalDenied:
|
||||||
|
parts = append(parts, "deny denied")
|
||||||
|
case apiaudit.EventApprovalRequested:
|
||||||
|
parts = append(parts, "prompt requested")
|
||||||
|
case apiaudit.EventApprovalCanceled:
|
||||||
|
parts = append(parts, "cancel canceled")
|
||||||
|
case apiaudit.EventApprovalTimedOut:
|
||||||
|
parts = append(parts, "timeout timed out")
|
||||||
|
case apiaudit.EventAuthRejected:
|
||||||
|
parts = append(parts, "rejected unauthorized")
|
||||||
|
}
|
||||||
|
return strings.ToLower(strings.Join(parts, " "))
|
||||||
|
}
|
||||||
@@ -12,89 +12,10 @@ import (
|
|||||||
"gioui.org/widget/material"
|
"gioui.org/widget/material"
|
||||||
"git.julianfamily.org/keepassgo/internal/apiaudit"
|
"git.julianfamily.org/keepassgo/internal/apiaudit"
|
||||||
"git.julianfamily.org/keepassgo/internal/apitokens"
|
"git.julianfamily.org/keepassgo/internal/apitokens"
|
||||||
|
apiui "git.julianfamily.org/keepassgo/internal/appui/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func apiOperations() []apitokens.Operation {
|
type apiAuditQuickFilter = apiui.AuditQuickFilter
|
||||||
return []apitokens.Operation{
|
|
||||||
apitokens.OperationListEntries,
|
|
||||||
apitokens.OperationListGroups,
|
|
||||||
apitokens.OperationReadEntry,
|
|
||||||
apitokens.OperationCopyPassword,
|
|
||||||
apitokens.OperationCopyUsername,
|
|
||||||
apitokens.OperationCopyURL,
|
|
||||||
apitokens.OperationMutateEntry,
|
|
||||||
apitokens.OperationMutateGroup,
|
|
||||||
apitokens.OperationManageVault,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiAuditQuickFilter struct {
|
|
||||||
Label string
|
|
||||||
Query string
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiAuditDecisionLabel(eventType apiaudit.EventType) string {
|
|
||||||
switch eventType {
|
|
||||||
case apiaudit.EventApprovalRequested:
|
|
||||||
return "Requested"
|
|
||||||
case apiaudit.EventApprovalAllowed:
|
|
||||||
return "Allowed"
|
|
||||||
case apiaudit.EventApprovalDenied:
|
|
||||||
return "Denied"
|
|
||||||
case apiaudit.EventApprovalCanceled:
|
|
||||||
return "Canceled"
|
|
||||||
case apiaudit.EventApprovalTimedOut:
|
|
||||||
return "Timed Out"
|
|
||||||
case apiaudit.EventAuthRejected:
|
|
||||||
return "Auth Rejected"
|
|
||||||
default:
|
|
||||||
return strings.ReplaceAll(string(eventType), "_", " ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiAuditOperationLabel(operation apitokens.Operation) string {
|
|
||||||
if strings.TrimSpace(string(operation)) == "" {
|
|
||||||
return "Other"
|
|
||||||
}
|
|
||||||
return strings.ReplaceAll(string(operation), "_", " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func compactAuditFilterLabel(label string) string {
|
|
||||||
label = strings.TrimSpace(label)
|
|
||||||
if len(label) <= 22 {
|
|
||||||
return label
|
|
||||||
}
|
|
||||||
return label[:19] + "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiAuditEventSearchTerms(event apiaudit.Event) string {
|
|
||||||
parts := []string{
|
|
||||||
string(event.Type),
|
|
||||||
apiAuditDecisionLabel(event.Type),
|
|
||||||
event.TokenName,
|
|
||||||
event.ClientName,
|
|
||||||
string(event.Operation),
|
|
||||||
apiAuditOperationLabel(event.Operation),
|
|
||||||
strings.Join(event.Resource.Path, " / "),
|
|
||||||
event.Resource.EntryID,
|
|
||||||
event.Message,
|
|
||||||
}
|
|
||||||
switch event.Type {
|
|
||||||
case apiaudit.EventApprovalAllowed:
|
|
||||||
parts = append(parts, "allow approved")
|
|
||||||
case apiaudit.EventApprovalDenied:
|
|
||||||
parts = append(parts, "deny denied")
|
|
||||||
case apiaudit.EventApprovalRequested:
|
|
||||||
parts = append(parts, "prompt requested")
|
|
||||||
case apiaudit.EventApprovalCanceled:
|
|
||||||
parts = append(parts, "cancel canceled")
|
|
||||||
case apiaudit.EventApprovalTimedOut:
|
|
||||||
parts = append(parts, "timeout timed out")
|
|
||||||
case apiaudit.EventAuthRejected:
|
|
||||||
parts = append(parts, "rejected unauthorized")
|
|
||||||
}
|
|
||||||
return strings.ToLower(strings.Join(parts, " "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiAuditFilterButtons(clicks *[]widget.Clickable, filters []apiAuditQuickFilter) []widget.Clickable {
|
func apiAuditFilterButtons(clicks *[]widget.Clickable, filters []apiAuditQuickFilter) []widget.Clickable {
|
||||||
if len(filters) == 0 {
|
if len(filters) == 0 {
|
||||||
@@ -126,7 +47,7 @@ func (u *ui) apiAuditQuickFilters(events []apiaudit.Event) ([]apiAuditQuickFilte
|
|||||||
}
|
}
|
||||||
if _, ok := decisionSeen[event.Type]; !ok {
|
if _, ok := decisionSeen[event.Type]; !ok {
|
||||||
decisionSeen[event.Type] = struct{}{}
|
decisionSeen[event.Type] = struct{}{}
|
||||||
label := apiAuditDecisionLabel(event.Type)
|
label := apiui.AuditDecisionLabel(event.Type)
|
||||||
decisions = append(decisions, apiAuditQuickFilter{Label: label, Query: label})
|
decisions = append(decisions, apiAuditQuickFilter{Label: label, Query: label})
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(string(event.Operation)) == "" {
|
if strings.TrimSpace(string(event.Operation)) == "" {
|
||||||
@@ -136,7 +57,7 @@ func (u *ui) apiAuditQuickFilters(events []apiaudit.Event) ([]apiAuditQuickFilte
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
operationSeen[event.Operation] = struct{}{}
|
operationSeen[event.Operation] = struct{}{}
|
||||||
label := apiAuditOperationLabel(event.Operation)
|
label := apiui.AuditOperationLabel(event.Operation)
|
||||||
operations = append(operations, apiAuditQuickFilter{Label: label, Query: label})
|
operations = append(operations, apiAuditQuickFilter{Label: label, Query: label})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +242,7 @@ func parseAPITokenExpiry(text string) (*time.Time, error) {
|
|||||||
|
|
||||||
func parseAPIPolicyOperation(text string) (apitokens.Operation, error) {
|
func parseAPIPolicyOperation(text string) (apitokens.Operation, error) {
|
||||||
value := apitokens.Operation(strings.TrimSpace(text))
|
value := apitokens.Operation(strings.TrimSpace(text))
|
||||||
for _, operation := range apiOperations() {
|
for _, operation := range apiui.Operations() {
|
||||||
if operation == value {
|
if operation == value {
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
@@ -450,7 +371,7 @@ func (u *ui) apiAuditEvents() []apiaudit.Event {
|
|||||||
}
|
}
|
||||||
filtered := make([]apiaudit.Event, 0, len(events))
|
filtered := make([]apiaudit.Event, 0, len(events))
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
haystack := apiAuditEventSearchTerms(event)
|
haystack := apiui.AuditEventSearchTerms(event)
|
||||||
if strings.Contains(haystack, query) {
|
if strings.Contains(haystack, query) {
|
||||||
filtered = append(filtered, event)
|
filtered = append(filtered, event)
|
||||||
}
|
}
|
||||||
@@ -785,7 +706,7 @@ func (u *ui) apiAuditQuickFilterRow(gtx layout.Context, title string, filters []
|
|||||||
click := &buttons[i]
|
click := &buttons[i]
|
||||||
selected := strings.EqualFold(strings.TrimSpace(u.search.Text()), strings.TrimSpace(filter.Query))
|
selected := strings.EqualFold(strings.TrimSpace(u.search.Text()), strings.TrimSpace(filter.Query))
|
||||||
column = append(column, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
column = append(column, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return u.auditQuickFilterButton(gtx, click, compactAuditFilterLabel(filter.Label), selected, filter.Query)
|
return u.auditQuickFilterButton(gtx, click, apiui.CompactAuditFilterLabel(filter.Label), selected, filter.Query)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, column...)
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, column...)
|
||||||
@@ -799,7 +720,7 @@ func (u *ui) apiAuditQuickFilterRow(gtx layout.Context, title string, filters []
|
|||||||
click := &buttons[i]
|
click := &buttons[i]
|
||||||
selected := strings.EqualFold(strings.TrimSpace(u.search.Text()), strings.TrimSpace(filter.Query))
|
selected := strings.EqualFold(strings.TrimSpace(u.search.Text()), strings.TrimSpace(filter.Query))
|
||||||
flexChildren = append(flexChildren, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
flexChildren = append(flexChildren, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return u.auditQuickFilterButton(gtx, click, compactAuditFilterLabel(filter.Label), selected, filter.Query)
|
return u.auditQuickFilterButton(gtx, click, apiui.CompactAuditFilterLabel(filter.Label), selected, filter.Query)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, flexChildren...)
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, flexChildren...)
|
||||||
@@ -1051,7 +972,7 @@ func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
return material.CheckBox(u.theme, &u.apiPolicyGroupScopeW, "Group scope (unchecked means exact entry scope)").Layout(gtx)
|
return material.CheckBox(u.theme, &u.apiPolicyGroupScopeW, "Group scope (unchecked means exact entry scope)").Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(labeledEditorHelp(u.theme, "Operation", "Valid operations: "+strings.Join(stringOps(apiOperations()), ", "), &u.apiPolicyOperation, false)),
|
layout.Rigid(labeledEditorHelp(u.theme, "Operation", "Valid operations: "+strings.Join(stringOps(apiui.Operations()), ", "), &u.apiPolicyOperation, false)),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if u.apiPolicyGroupScopeW.Value {
|
if u.apiPolicyGroupScopeW.Value {
|
||||||
+24
-46
@@ -26,9 +26,13 @@ import (
|
|||||||
"git.julianfamily.org/keepassgo/internal/apiaudit"
|
"git.julianfamily.org/keepassgo/internal/apiaudit"
|
||||||
"git.julianfamily.org/keepassgo/internal/apitokens"
|
"git.julianfamily.org/keepassgo/internal/apitokens"
|
||||||
"git.julianfamily.org/keepassgo/internal/appstate"
|
"git.julianfamily.org/keepassgo/internal/appstate"
|
||||||
detaillayout "git.julianfamily.org/keepassgo/internal/appui/layout/detail"
|
detailmodel "git.julianfamily.org/keepassgo/internal/appui/detail"
|
||||||
listlayout "git.julianfamily.org/keepassgo/internal/appui/layout/list"
|
detaillayout "git.julianfamily.org/keepassgo/internal/appui/detail/layout"
|
||||||
|
lifecyclemodel "git.julianfamily.org/keepassgo/internal/appui/lifecycle"
|
||||||
|
listmodel "git.julianfamily.org/keepassgo/internal/appui/list"
|
||||||
|
listlayout "git.julianfamily.org/keepassgo/internal/appui/list/layout"
|
||||||
"git.julianfamily.org/keepassgo/internal/appui/platform"
|
"git.julianfamily.org/keepassgo/internal/appui/platform"
|
||||||
|
syncmodel "git.julianfamily.org/keepassgo/internal/appui/sync"
|
||||||
keepassassets "git.julianfamily.org/keepassgo/internal/assets"
|
keepassassets "git.julianfamily.org/keepassgo/internal/assets"
|
||||||
"git.julianfamily.org/keepassgo/internal/clipboard"
|
"git.julianfamily.org/keepassgo/internal/clipboard"
|
||||||
"git.julianfamily.org/keepassgo/internal/passwords"
|
"git.julianfamily.org/keepassgo/internal/passwords"
|
||||||
@@ -105,24 +109,17 @@ type uiSurface struct {
|
|||||||
Locked bool
|
Locked bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type lifecycleOpenIntent string
|
type lifecycleOpenIntent = lifecyclemodel.OpenIntent
|
||||||
|
|
||||||
const (
|
const (
|
||||||
lifecycleOpenIntentNone lifecycleOpenIntent = ""
|
lifecycleOpenIntentNone = lifecyclemodel.OpenIntentNone
|
||||||
lifecycleOpenIntentRemoteSyncSetup lifecycleOpenIntent = "remote_sync_setup"
|
lifecycleOpenIntentRemoteSyncSetup = lifecyclemodel.OpenIntentRemoteSyncSetup
|
||||||
lifecycleOpenIntentRemoteSyncSettings lifecycleOpenIntent = "remote_sync_settings"
|
lifecycleOpenIntentRemoteSyncSettings = lifecyclemodel.OpenIntentRemoteSyncSettings
|
||||||
)
|
)
|
||||||
|
|
||||||
type emptyState struct {
|
type emptyState = detailmodel.EmptyState
|
||||||
Title string
|
|
||||||
Body string
|
|
||||||
}
|
|
||||||
|
|
||||||
type vaultSummary struct {
|
type vaultSummary = detailmodel.VaultSummary
|
||||||
Title string
|
|
||||||
Detail string
|
|
||||||
Context string
|
|
||||||
}
|
|
||||||
|
|
||||||
type sessionStatus interface {
|
type sessionStatus interface {
|
||||||
HasVault() bool
|
HasVault() bool
|
||||||
@@ -130,10 +127,7 @@ type sessionStatus interface {
|
|||||||
IsRemote() bool
|
IsRemote() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type attachmentItem struct {
|
type attachmentItem = detailmodel.AttachmentItem
|
||||||
Name string
|
|
||||||
Size int
|
|
||||||
}
|
|
||||||
|
|
||||||
type statePaths struct {
|
type statePaths struct {
|
||||||
DefaultSaveAsPath string
|
DefaultSaveAsPath string
|
||||||
@@ -195,32 +189,27 @@ const (
|
|||||||
autofillFirstFillApprovalBlock autofillFirstFillApprovalMode = "block"
|
autofillFirstFillApprovalBlock autofillFirstFillApprovalMode = "block"
|
||||||
)
|
)
|
||||||
|
|
||||||
type entriesSectionState struct {
|
type entriesSectionState = listmodel.EntriesSectionState
|
||||||
Path []string
|
|
||||||
SearchQuery string
|
|
||||||
SelectedEntryID string
|
|
||||||
Editing bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type syncSourceMode string
|
type syncSourceMode = syncmodel.SourceMode
|
||||||
|
|
||||||
const (
|
const (
|
||||||
syncSourceLocal syncSourceMode = "local"
|
syncSourceLocal = syncmodel.SourceLocal
|
||||||
syncSourceRemote syncSourceMode = "remote"
|
syncSourceRemote = syncmodel.SourceRemote
|
||||||
)
|
)
|
||||||
|
|
||||||
type syncDirection string
|
type syncDirection = syncmodel.Direction
|
||||||
|
|
||||||
const (
|
const (
|
||||||
syncDirectionPull syncDirection = "pull"
|
syncDirectionPull = syncmodel.DirectionPull
|
||||||
syncDirectionPush syncDirection = "push"
|
syncDirectionPush = syncmodel.DirectionPush
|
||||||
)
|
)
|
||||||
|
|
||||||
type syncDialogPurpose string
|
type syncDialogPurpose = syncmodel.DialogPurpose
|
||||||
|
|
||||||
const (
|
const (
|
||||||
syncDialogPurposeAdvanced syncDialogPurpose = "advanced"
|
syncDialogPurposeAdvanced = syncmodel.DialogPurposeAdvanced
|
||||||
syncDialogPurposeRemoteSetup syncDialogPurpose = "remote-setup"
|
syncDialogPurposeRemoteSetup = syncmodel.DialogPurposeRemoteSetup
|
||||||
)
|
)
|
||||||
|
|
||||||
type ui struct {
|
type ui struct {
|
||||||
@@ -3106,18 +3095,7 @@ func syncDialogSectionLabel(th *material.Theme, text string) layout.Widget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func syncDialogSummaryText(purpose syncDialogPurpose, source syncSourceMode, direction syncDirection) string {
|
func syncDialogSummaryText(purpose syncDialogPurpose, source syncSourceMode, direction syncDirection) string {
|
||||||
if purpose == syncDialogPurposeRemoteSetup {
|
return syncmodel.SummaryText(purpose, source, direction)
|
||||||
return "Push this local vault to a WebDAV target and save that target for future sync."
|
|
||||||
}
|
|
||||||
sourceLabel := "another local vault file"
|
|
||||||
if source == syncSourceRemote {
|
|
||||||
sourceLabel = "another WebDAV-backed vault"
|
|
||||||
}
|
|
||||||
action := "Pull changes from"
|
|
||||||
if direction == syncDirectionPush {
|
|
||||||
action = "Push the current vault into"
|
|
||||||
}
|
|
||||||
return action + " " + sourceLabel + "."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncDialogSummaryCard(gtx layout.Context, th *material.Theme, purpose syncDialogPurpose, source syncSourceMode, direction syncDirection) layout.Dimensions {
|
func syncDialogSummaryCard(gtx layout.Context, th *material.Theme, purpose syncDialogPurpose, source syncSourceMode, direction syncDirection) layout.Dimensions {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package detail
|
package layout
|
||||||
|
|
||||||
type Mode string
|
type Mode string
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package detail
|
||||||
|
|
||||||
|
type EmptyState struct {
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
type VaultSummary struct {
|
||||||
|
Title string
|
||||||
|
Detail string
|
||||||
|
Context string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttachmentItem struct {
|
||||||
|
Name string
|
||||||
|
Size int
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package editor
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type Field string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FieldID Field = "id"
|
||||||
|
FieldTitle Field = "title"
|
||||||
|
FieldUsername Field = "username"
|
||||||
|
FieldPassword Field = "password"
|
||||||
|
FieldURL Field = "url"
|
||||||
|
FieldPath Field = "path"
|
||||||
|
FieldTags Field = "tags"
|
||||||
|
FieldPasswordProfile Field = "password-profile"
|
||||||
|
FieldNotes Field = "notes"
|
||||||
|
FieldFields Field = "fields"
|
||||||
|
FieldHistoryIndex Field = "history-index"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Label(field Field) string {
|
||||||
|
switch field {
|
||||||
|
case FieldID:
|
||||||
|
return "ID"
|
||||||
|
case FieldTitle:
|
||||||
|
return "Title"
|
||||||
|
case FieldUsername:
|
||||||
|
return "Username"
|
||||||
|
case FieldPassword:
|
||||||
|
return "Password"
|
||||||
|
case FieldURL:
|
||||||
|
return "URL"
|
||||||
|
case FieldPath:
|
||||||
|
return "Path"
|
||||||
|
case FieldTags:
|
||||||
|
return "Tags"
|
||||||
|
case FieldPasswordProfile:
|
||||||
|
return "Password Profile"
|
||||||
|
case FieldNotes:
|
||||||
|
return "Notes"
|
||||||
|
case FieldFields:
|
||||||
|
return "Custom Fields"
|
||||||
|
case FieldHistoryIndex:
|
||||||
|
return "History Index"
|
||||||
|
default:
|
||||||
|
return strings.ReplaceAll(string(field), "-", " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FocusOrder() []Field {
|
||||||
|
return []Field{
|
||||||
|
FieldID,
|
||||||
|
FieldTitle,
|
||||||
|
FieldUsername,
|
||||||
|
FieldPassword,
|
||||||
|
FieldURL,
|
||||||
|
FieldPath,
|
||||||
|
FieldTags,
|
||||||
|
FieldPasswordProfile,
|
||||||
|
FieldNotes,
|
||||||
|
FieldFields,
|
||||||
|
FieldHistoryIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package appui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"gioui.org/layout"
|
||||||
|
"gioui.org/op"
|
||||||
|
"gioui.org/op/paint"
|
||||||
|
"gioui.org/unit"
|
||||||
|
"gioui.org/widget"
|
||||||
|
"gioui.org/widget/material"
|
||||||
|
headerlayout "git.julianfamily.org/keepassgo/internal/appui/header/layout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (u *ui) header(gtx layout.Context) layout.Dimensions {
|
||||||
|
if u.usesCompactViewport() {
|
||||||
|
if u.shouldShowLifecycleSetup() || u.isVaultLocked() {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||||
|
return u.headerActions(gtx)
|
||||||
|
}
|
||||||
|
if u.shouldShowDesktopWorkingHeader() {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return card(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||||
|
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return u.brandMark(gtx, 196, 56)
|
||||||
|
}),
|
||||||
|
layout.Rigid(u.headerActions),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
|
||||||
|
if u.shouldShowLifecycleSetup() || u.isVaultLocked() || u.shouldShowDesktopWorkingHeader() {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
spacing := gtx.Dp(unit.Dp(8))
|
||||||
|
metrics := headerlayout.ActionMetrics{Spacing: spacing}
|
||||||
|
row := func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
metrics.SyncDims = u.syncButtonGroup(gtx)
|
||||||
|
return metrics.SyncDims
|
||||||
|
}),
|
||||||
|
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")
|
||||||
|
metrics.LockDims = btn.Layout(gtx)
|
||||||
|
return metrics.LockDims
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
metrics.MainDims = u.mainMenuButtonGroup(gtx)
|
||||||
|
return metrics.MainDims
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
rowOps := op.Record(gtx.Ops)
|
||||||
|
metrics.RowDims = row(gtx)
|
||||||
|
rowCall := rowOps.Stop()
|
||||||
|
|
||||||
|
if u.usesCompactViewport() {
|
||||||
|
metrics.RowOriginX = max(0, gtx.Constraints.Max.X-metrics.RowDims.Size.X)
|
||||||
|
}
|
||||||
|
|
||||||
|
surface := headerlayout.DropdownSurface{ContainerWidth: gtx.Constraints.Max.X, LeftInset: 0, TopInset: 0}
|
||||||
|
|
||||||
|
rowStack := op.Offset(image.Pt(metrics.RowOriginX, 0)).Push(gtx.Ops)
|
||||||
|
rowCall.Add(gtx.Ops)
|
||||||
|
rowStack.Pop()
|
||||||
|
|
||||||
|
if u.usesCompactViewport() {
|
||||||
|
if u.syncMenuOpen {
|
||||||
|
u.phoneSyncMenuVisible = true
|
||||||
|
u.phoneSyncMenuAnchor = metrics.SyncAnchor().Point()
|
||||||
|
}
|
||||||
|
if u.mainMenuOpen {
|
||||||
|
u.phoneMainMenuVisible = true
|
||||||
|
u.phoneMainMenuAnchor = metrics.MainAnchor().Point()
|
||||||
|
}
|
||||||
|
return layout.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, metrics.RowDims.Size.Y)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.syncMenuOpen {
|
||||||
|
surface.Draw(gtx, metrics.SyncAnchor(), u.syncMenu)
|
||||||
|
}
|
||||||
|
if u.mainMenuOpen {
|
||||||
|
surface.Draw(gtx, metrics.MainAnchor(), u.mainMenu)
|
||||||
|
}
|
||||||
|
|
||||||
|
return layout.Dimensions{Size: image.Pt(metrics.RowDims.Size.X, metrics.RowDims.Size.Y)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) topRightActionOrder() []string {
|
||||||
|
if u.isVaultLocked() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{"Sync", "Lock", "Menu"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) phoneHeaderMenus(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !u.usesCompactViewport() || (!u.syncMenuVisibleOnPhone() && !u.mainMenuVisibleOnPhone()) {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
gtx.Constraints.Min = gtx.Constraints.Max
|
||||||
|
contentInsetPx := gtx.Dp(unit.Dp(16))
|
||||||
|
surface := headerlayout.DropdownSurface{
|
||||||
|
ContainerWidth: max(0, gtx.Constraints.Max.X-(contentInsetPx*2)),
|
||||||
|
LeftInset: contentInsetPx,
|
||||||
|
TopInset: contentInsetPx,
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.syncMenuVisibleOnPhone() {
|
||||||
|
surface.Draw(gtx, headerlayout.DropdownAnchor{TriggerRightX: u.phoneSyncMenuAnchor.X, TriggerBottomY: u.phoneSyncMenuAnchor.Y}, u.syncMenu)
|
||||||
|
}
|
||||||
|
if u.mainMenuVisibleOnPhone() {
|
||||||
|
surface.Draw(gtx, headerlayout.DropdownAnchor{TriggerRightX: u.phoneMainMenuAnchor.X, TriggerBottomY: u.phoneMainMenuAnchor.Y}, u.mainMenu)
|
||||||
|
}
|
||||||
|
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) syncMenuVisibleOnPhone() bool {
|
||||||
|
return u.usesCompactViewport() && u.phoneSyncMenuVisible && u.syncMenuOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) mainMenuVisibleOnPhone() bool {
|
||||||
|
return u.usesCompactViewport() && u.phoneMainMenuVisible && u.mainMenuOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) syncMenuDropsBelowTrigger() bool { return true }
|
||||||
|
|
||||||
|
func (u *ui) syncMenuRightAlignsToTrigger() bool { return true }
|
||||||
|
|
||||||
|
func (u *ui) headerMenusUseOverlayModel() bool { return true }
|
||||||
|
|
||||||
|
func (u *ui) mainMenuDropsBelowTrigger() bool { return true }
|
||||||
|
|
||||||
|
func (u *ui) mainMenuRightAlignsToTrigger() bool { return true }
|
||||||
|
|
||||||
|
func (u *ui) lifecycleBranding(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !u.usesCompactViewport() {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) brandMark(gtx layout.Context, widthDP, heightDP float32) layout.Dimensions {
|
||||||
|
if u.usesCompactViewport() {
|
||||||
|
return u.brandImage(gtx, u.splashSquare, widthDP, heightDP)
|
||||||
|
}
|
||||||
|
return u.brandImage(gtx, u.logoHorizontal, widthDP, heightDP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) brandImage(gtx layout.Context, src paint.ImageOp, widthDP, heightDP float32) layout.Dimensions {
|
||||||
|
width := gtx.Dp(unit.Dp(widthDP))
|
||||||
|
height := gtx.Dp(unit.Dp(heightDP))
|
||||||
|
if width > gtx.Constraints.Max.X {
|
||||||
|
width = gtx.Constraints.Max.X
|
||||||
|
}
|
||||||
|
if height > gtx.Constraints.Max.Y && gtx.Constraints.Max.Y > 0 {
|
||||||
|
height = gtx.Constraints.Max.Y
|
||||||
|
}
|
||||||
|
img := widget.Image{
|
||||||
|
Src: src,
|
||||||
|
Fit: widget.Contain,
|
||||||
|
Position: layout.W,
|
||||||
|
Scale: 1.0 / gtx.Metric.PxPerDp,
|
||||||
|
}
|
||||||
|
gtx.Constraints.Min = image.Point{}
|
||||||
|
gtx.Constraints.Max = image.Pt(width, height)
|
||||||
|
return img.Layout(gtx)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package header
|
package layout
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package appui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"gioui.org/layout"
|
||||||
|
"gioui.org/unit"
|
||||||
|
"gioui.org/widget/material"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions {
|
||||||
|
rows := []layout.Widget{
|
||||||
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.showEntries, "Entries")
|
||||||
|
},
|
||||||
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.showRecycle, "Recycle Bin")
|
||||||
|
},
|
||||||
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.showAPITokens, "API Tokens")
|
||||||
|
},
|
||||||
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit")
|
||||||
|
},
|
||||||
|
func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.showAbout, "About") },
|
||||||
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rowWidth := menuActionWidth(gtx, rows)
|
||||||
|
return intrinsicCompactCard(gtx, 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, rowWidth, rows[0]) }),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return rightAlignedMenuAction(gtx, rowWidth, rows[1]) }),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return rightAlignedMenuAction(gtx, rowWidth, rows[2]) }),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return rightAlignedMenuAction(gtx, rowWidth, rows[3]) }),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return rightAlignedMenuAction(gtx, rowWidth, rows[4]) }),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return rightAlignedMenuAction(gtx, rowWidth, rows[5]) }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) mainMenuButtonGroup(gtx layout.Context) layout.Dimensions {
|
||||||
|
icon := u.menuIcon
|
||||||
|
if icon == nil {
|
||||||
|
icon = u.settingsIcon
|
||||||
|
}
|
||||||
|
btn := material.IconButton(u.theme, &u.toggleMainMenu, icon, "Menu")
|
||||||
|
if u.mainMenuOpen {
|
||||||
|
btn.Background = accentColor
|
||||||
|
btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
|
||||||
|
} else {
|
||||||
|
btn.Background = selectedColor
|
||||||
|
btn.Color = accentColor
|
||||||
|
}
|
||||||
|
btn.Size = unit.Dp(18)
|
||||||
|
btn.Inset = layout.UniformInset(unit.Dp(8))
|
||||||
|
return btn.Layout(gtx)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package appui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"gioui.org/layout"
|
||||||
|
"gioui.org/op"
|
||||||
|
"gioui.org/unit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func intrinsicCompactCard(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
||||||
|
measureGTX := gtx
|
||||||
|
measureGTX.Constraints.Min = image.Point{}
|
||||||
|
measureGTX.Constraints.Max.X = gtx.Constraints.Max.X
|
||||||
|
macro := op.Record(gtx.Ops)
|
||||||
|
contentDims := w(measureGTX)
|
||||||
|
_ = macro.Stop()
|
||||||
|
width := contentDims.Size.X + gtx.Dp(unit.Dp(20))
|
||||||
|
maxWidth := gtx.Constraints.Max.X
|
||||||
|
if maxWidth > 0 && width > maxWidth {
|
||||||
|
width = maxWidth
|
||||||
|
}
|
||||||
|
if width > 0 {
|
||||||
|
gtx.Constraints.Min.X = width
|
||||||
|
gtx.Constraints.Max.X = width
|
||||||
|
}
|
||||||
|
return compactCard(gtx, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func menuActionWidth(gtx layout.Context, rows []layout.Widget) int {
|
||||||
|
width := 0
|
||||||
|
for _, row := range rows {
|
||||||
|
measureGTX := gtx
|
||||||
|
measureGTX.Constraints.Min = image.Point{}
|
||||||
|
macro := op.Record(gtx.Ops)
|
||||||
|
dims := row(measureGTX)
|
||||||
|
_ = macro.Stop()
|
||||||
|
if dims.Size.X > width {
|
||||||
|
width = dims.Size.X
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
func rightAlignedMenuAction(gtx layout.Context, width int, child layout.Widget) layout.Dimensions {
|
||||||
|
if width <= 0 {
|
||||||
|
return child(gtx)
|
||||||
|
}
|
||||||
|
gtx.Constraints.Min.X = width
|
||||||
|
gtx.Constraints.Max.X = width
|
||||||
|
return layout.E.Layout(gtx, child)
|
||||||
|
}
|
||||||
@@ -1,177 +1,33 @@
|
|||||||
package appui
|
package appui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op"
|
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"gioui.org/widget"
|
"gioui.org/widget"
|
||||||
"gioui.org/widget/material"
|
"gioui.org/widget/material"
|
||||||
"git.julianfamily.org/keepassgo/internal/appui/actions"
|
"git.julianfamily.org/keepassgo/internal/appstate"
|
||||||
headerlayout "git.julianfamily.org/keepassgo/internal/appui/layout/header"
|
syncmodel "git.julianfamily.org/keepassgo/internal/appui/sync"
|
||||||
"git.julianfamily.org/keepassgo/internal/vault"
|
"git.julianfamily.org/keepassgo/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (u *ui) header(gtx layout.Context) layout.Dimensions {
|
|
||||||
if u.usesCompactViewport() {
|
|
||||||
if u.shouldShowLifecycleSetup() || u.isVaultLocked() {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
|
||||||
return u.headerActions(gtx)
|
|
||||||
}
|
|
||||||
if u.shouldShowDesktopWorkingHeader() {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return card(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
|
||||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return u.brandMark(gtx, 196, 56)
|
|
||||||
}),
|
|
||||||
layout.Rigid(u.headerActions),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
|
|
||||||
if u.shouldShowLifecycleSetup() {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
if u.isVaultLocked() {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
if u.shouldShowDesktopWorkingHeader() {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
spacing := gtx.Dp(unit.Dp(8))
|
|
||||||
metrics := headerlayout.ActionMetrics{Spacing: spacing}
|
|
||||||
row := func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
metrics.SyncDims = u.syncButtonGroup(gtx)
|
|
||||||
return metrics.SyncDims
|
|
||||||
}),
|
|
||||||
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")
|
|
||||||
metrics.LockDims = btn.Layout(gtx)
|
|
||||||
return metrics.LockDims
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
metrics.MainDims = u.mainMenuButtonGroup(gtx)
|
|
||||||
return metrics.MainDims
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
rowOps := op.Record(gtx.Ops)
|
|
||||||
metrics.RowDims = row(gtx)
|
|
||||||
rowCall := rowOps.Stop()
|
|
||||||
|
|
||||||
if u.usesCompactViewport() {
|
|
||||||
metrics.RowOriginX = max(0, gtx.Constraints.Max.X-metrics.RowDims.Size.X)
|
|
||||||
}
|
|
||||||
|
|
||||||
surface := headerlayout.DropdownSurface{ContainerWidth: gtx.Constraints.Max.X, LeftInset: 0, TopInset: 0}
|
|
||||||
|
|
||||||
rowStack := op.Offset(image.Pt(metrics.RowOriginX, 0)).Push(gtx.Ops)
|
|
||||||
rowCall.Add(gtx.Ops)
|
|
||||||
rowStack.Pop()
|
|
||||||
|
|
||||||
if u.usesCompactViewport() {
|
|
||||||
if u.syncMenuOpen {
|
|
||||||
u.phoneSyncMenuVisible = true
|
|
||||||
u.phoneSyncMenuAnchor = metrics.SyncAnchor().Point()
|
|
||||||
}
|
|
||||||
if u.mainMenuOpen {
|
|
||||||
u.phoneMainMenuVisible = true
|
|
||||||
u.phoneMainMenuAnchor = metrics.MainAnchor().Point()
|
|
||||||
}
|
|
||||||
width := gtx.Constraints.Max.X
|
|
||||||
return layout.Dimensions{Size: image.Pt(width, metrics.RowDims.Size.Y)}
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.syncMenuOpen {
|
|
||||||
surface.Draw(gtx, metrics.SyncAnchor(), u.syncMenu)
|
|
||||||
}
|
|
||||||
if u.mainMenuOpen {
|
|
||||||
surface.Draw(gtx, metrics.MainAnchor(), u.mainMenu)
|
|
||||||
}
|
|
||||||
|
|
||||||
width := metrics.RowDims.Size.X
|
|
||||||
return layout.Dimensions{Size: image.Pt(width, metrics.RowDims.Size.Y)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions {
|
|
||||||
rows := []layout.Widget{
|
|
||||||
func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return tonedButton(gtx, u.theme, &u.showEntries, "Entries")
|
|
||||||
},
|
|
||||||
func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return tonedButton(gtx, u.theme, &u.showRecycle, "Recycle Bin")
|
|
||||||
},
|
|
||||||
func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return tonedButton(gtx, u.theme, &u.showAPITokens, "API Tokens")
|
|
||||||
},
|
|
||||||
func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit")
|
|
||||||
},
|
|
||||||
func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.showAbout, "About") },
|
|
||||||
func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
rowWidth := menuActionWidth(gtx, rows)
|
|
||||||
return intrinsicCompactCard(gtx, 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, rowWidth, rows[0])
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return rightAlignedMenuAction(gtx, rowWidth, rows[1])
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return rightAlignedMenuAction(gtx, rowWidth, rows[2])
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return rightAlignedMenuAction(gtx, rowWidth, rows[3])
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return rightAlignedMenuAction(gtx, rowWidth, rows[4])
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return rightAlignedMenuAction(gtx, rowWidth, rows[5])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions {
|
func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions {
|
||||||
label := "Sync"
|
|
||||||
spacing := unit.Dp(4)
|
spacing := unit.Dp(4)
|
||||||
if u.usesCompactViewport() {
|
if u.usesCompactViewport() {
|
||||||
spacing = unit.Dp(3)
|
spacing = unit.Dp(3)
|
||||||
}
|
}
|
||||||
row := func(gtx layout.Context) layout.Dimensions {
|
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
return syncPrimaryButton(gtx, u.theme, &u.synchronizeVault, "Sync", u.usesCompactViewport())
|
||||||
return syncPrimaryButton(gtx, u.theme, &u.synchronizeVault, label, u.usesCompactViewport())
|
}),
|
||||||
}),
|
layout.Rigid(layout.Spacer{Width: spacing}.Layout),
|
||||||
layout.Rigid(layout.Spacer{Width: spacing}.Layout),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
return u.syncMenuToggle(gtx)
|
||||||
return u.syncMenuToggle(gtx)
|
}),
|
||||||
}),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
return row(gtx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) syncMenuToggle(gtx layout.Context) layout.Dimensions {
|
func (u *ui) syncMenuToggle(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -205,12 +61,11 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions {
|
|||||||
actionRows := u.syncMenuActionRows(model)
|
actionRows := u.syncMenuActionRows(model)
|
||||||
actionWidth := menuActionWidth(gtx, actionRows)
|
actionWidth := menuActionWidth(gtx, actionRows)
|
||||||
return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
rows := u.syncMenuRows(model, profiles, credentials, actionWidth)
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, u.syncMenuRows(model, profiles, credentials, actionWidth)...)
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) syncMenuActionRows(model actions.SyncMenuModel) []layout.Widget {
|
func (u *ui) syncMenuActionRows(model syncmodel.MenuModel) []layout.Widget {
|
||||||
rows := []layout.Widget{
|
rows := []layout.Widget{
|
||||||
func(gtx layout.Context) layout.Dimensions {
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync")
|
return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync")
|
||||||
@@ -244,7 +99,7 @@ func (u *ui) syncMenuActionRows(model actions.SyncMenuModel) []layout.Widget {
|
|||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) syncMenuRows(model actions.SyncMenuModel, profiles []vault.RemoteProfile, credentials []vault.Entry, actionWidth int) []layout.FlexChild {
|
func (u *ui) syncMenuRows(model syncmodel.MenuModel, profiles []vault.RemoteProfile, credentials []vault.Entry, actionWidth int) []layout.FlexChild {
|
||||||
rows := u.syncMenuPrimaryRows(model, actionWidth)
|
rows := u.syncMenuPrimaryRows(model, actionWidth)
|
||||||
rows = append(rows, u.syncMenuSavedBindingRows(model, profiles, credentials)...)
|
rows = append(rows, u.syncMenuSavedBindingRows(model, profiles, credentials)...)
|
||||||
if model.ShowSaveCurrentBinding {
|
if model.ShowSaveCurrentBinding {
|
||||||
@@ -253,7 +108,7 @@ func (u *ui) syncMenuRows(model actions.SyncMenuModel, profiles []vault.RemotePr
|
|||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) syncMenuPrimaryRows(model actions.SyncMenuModel, actionWidth int) []layout.FlexChild {
|
func (u *ui) syncMenuPrimaryRows(model syncmodel.MenuModel, actionWidth int) []layout.FlexChild {
|
||||||
rows := []layout.FlexChild{
|
rows := []layout.FlexChild{
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
lbl := material.Label(u.theme, unit.Sp(11), "Need another source or direction?")
|
lbl := material.Label(u.theme, unit.Sp(11), "Need another source or direction?")
|
||||||
@@ -302,7 +157,7 @@ func (u *ui) syncMenuActionRow(actionWidth int, click *widget.Clickable, label s
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) syncMenuSavedBindingRows(model actions.SyncMenuModel, profiles []vault.RemoteProfile, credentials []vault.Entry) []layout.FlexChild {
|
func (u *ui) syncMenuSavedBindingRows(model syncmodel.MenuModel, profiles []vault.RemoteProfile, credentials []vault.Entry) []layout.FlexChild {
|
||||||
if !u.hasOpenVault() || len(profiles) == 0 || len(credentials) == 0 {
|
if !u.hasOpenVault() || len(profiles) == 0 || len(credentials) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -333,7 +188,7 @@ func (u *ui) syncMenuSavedBindingRows(model actions.SyncMenuModel, profiles []va
|
|||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) syncMenuSavedBindingSummary(model actions.SyncMenuModel) layout.Widget {
|
func (u *ui) syncMenuSavedBindingSummary(model syncmodel.MenuModel) layout.Widget {
|
||||||
return func(gtx layout.Context) layout.Dimensions {
|
return func(gtx layout.Context) layout.Dimensions {
|
||||||
summary := model.SavedBindingSummary
|
summary := model.SavedBindingSummary
|
||||||
if !summary.OK {
|
if !summary.OK {
|
||||||
@@ -365,7 +220,7 @@ func (u *ui) syncMenuSavedBindingSummary(model actions.SyncMenuModel) layout.Wid
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) syncMenuSaveBindingRows(model actions.SyncMenuModel) []layout.FlexChild {
|
func (u *ui) syncMenuSaveBindingRows(model syncmodel.MenuModel) []layout.FlexChild {
|
||||||
return []layout.FlexChild{
|
return []layout.FlexChild{
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -380,7 +235,7 @@ func (u *ui) syncMenuSaveBindingRows(model actions.SyncMenuModel) []layout.FlexC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) syncMenuSelectorRows(_ actions.SyncMenuModel, profiles []vault.RemoteProfile, credentials []vault.Entry) []layout.FlexChild {
|
func (u *ui) syncMenuSelectorRows(_ syncmodel.MenuModel, profiles []vault.RemoteProfile, credentials []vault.Entry) []layout.FlexChild {
|
||||||
rows := make([]layout.FlexChild, 0, len(profiles)+len(credentials)+4)
|
rows := make([]layout.FlexChild, 0, len(profiles)+len(credentials)+4)
|
||||||
for i, profile := range profiles {
|
for i, profile := range profiles {
|
||||||
i := i
|
i := i
|
||||||
@@ -422,125 +277,44 @@ func (u *ui) syncMenuSelectorRows(_ actions.SyncMenuModel, profiles []vault.Remo
|
|||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
func intrinsicCompactCard(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
func (u *ui) buildSyncMenuModel() syncmodel.MenuModel {
|
||||||
measureGTX := gtx
|
model := syncmodel.MenuModel{
|
||||||
measureGTX.Constraints.Min = image.Point{}
|
HasOpenVault: u.hasOpenVault(),
|
||||||
measureGTX.Constraints.Max.X = gtx.Constraints.Max.X
|
ShowSelectors: u.shouldShowSavedRemoteBindingSelectors(),
|
||||||
macro := op.Record(gtx.Ops)
|
ShowShare: supportsVaultShare(runtime.GOOS) && u.vaultSharer != nil && strings.TrimSpace(u.currentShareableVaultPath()) != "",
|
||||||
contentDims := w(measureGTX)
|
RemoteBaseURL: strings.TrimSpace(u.remoteBaseURL.Text()),
|
||||||
_ = macro.Stop()
|
RemotePath: strings.TrimSpace(u.remotePath.Text()),
|
||||||
width := contentDims.Size.X + gtx.Dp(unit.Dp(20))
|
RemoteUsername: strings.TrimSpace(u.remoteUsername.Text()),
|
||||||
maxWidth := gtx.Constraints.Max.X
|
RemotePassword: u.remotePassword.Text(),
|
||||||
if maxWidth > 0 && width > maxWidth {
|
SelectedVaultSyncMode: normalizeUISyncMode(u.selectedVaultRemoteSyncMode),
|
||||||
width = maxWidth
|
|
||||||
}
|
}
|
||||||
if width > 0 {
|
_, model.HasSelectedBinding = u.selectedVaultRemoteBinding()
|
||||||
gtx.Constraints.Min.X = width
|
model.SavedBindingSummary = u.computeSavedRemoteBindingSummary()
|
||||||
gtx.Constraints.Max.X = width
|
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{}
|
||||||
}
|
}
|
||||||
return compactCard(gtx, w)
|
entry, ok := u.selectedVaultRemoteCredentialEntry()
|
||||||
}
|
if !ok {
|
||||||
|
return syncmodel.MenuBindingSummary{}
|
||||||
func (u *ui) topRightActionOrder() []string {
|
|
||||||
if u.isVaultLocked() {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return []string{"Sync", "Lock", "Menu"}
|
credentialLabel := entry.Title
|
||||||
}
|
if strings.TrimSpace(entry.Username) != "" {
|
||||||
|
credentialLabel += " · " + strings.TrimSpace(entry.Username)
|
||||||
func (u *ui) mainMenuButtonGroup(gtx layout.Context) layout.Dimensions {
|
|
||||||
button := func(gtx layout.Context) layout.Dimensions {
|
|
||||||
icon := u.menuIcon
|
|
||||||
if icon == nil {
|
|
||||||
icon = u.settingsIcon
|
|
||||||
}
|
|
||||||
btn := material.IconButton(u.theme, &u.toggleMainMenu, icon, "Menu")
|
|
||||||
if u.mainMenuOpen {
|
|
||||||
btn.Background = accentColor
|
|
||||||
btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
|
|
||||||
} else {
|
|
||||||
btn.Background = selectedColor
|
|
||||||
btn.Color = accentColor
|
|
||||||
}
|
|
||||||
btn.Size = unit.Dp(18)
|
|
||||||
btn.Inset = layout.UniformInset(unit.Dp(8))
|
|
||||||
return btn.Layout(gtx)
|
|
||||||
}
|
}
|
||||||
return button(gtx)
|
syncLabel := "Sync manually when you choose Use Remote Sync."
|
||||||
}
|
if normalizeUISyncMode(u.selectedVaultRemoteSyncMode) == appstate.SyncModeAutomaticOnOpenSave {
|
||||||
|
syncLabel = "Syncs automatically on open and save."
|
||||||
func (u *ui) phoneHeaderMenus(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !u.usesCompactViewport() {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
}
|
||||||
if !u.syncMenuVisibleOnPhone() && !u.mainMenuVisibleOnPhone() {
|
return syncmodel.MenuBindingSummary{
|
||||||
return layout.Dimensions{}
|
ProfileLabel: profile.Name,
|
||||||
|
CredentialLabel: credentialLabel,
|
||||||
|
SyncLabel: syncLabel,
|
||||||
|
OK: true,
|
||||||
}
|
}
|
||||||
gtx.Constraints.Min = gtx.Constraints.Max
|
|
||||||
contentInsetPx := gtx.Dp(unit.Dp(16))
|
|
||||||
surface := headerlayout.DropdownSurface{
|
|
||||||
ContainerWidth: max(0, gtx.Constraints.Max.X-(contentInsetPx*2)),
|
|
||||||
LeftInset: contentInsetPx,
|
|
||||||
TopInset: contentInsetPx,
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.syncMenuVisibleOnPhone() {
|
|
||||||
surface.Draw(gtx, headerlayout.DropdownAnchor{TriggerRightX: u.phoneSyncMenuAnchor.X, TriggerBottomY: u.phoneSyncMenuAnchor.Y}, u.syncMenu)
|
|
||||||
}
|
|
||||||
if u.mainMenuVisibleOnPhone() {
|
|
||||||
surface.Draw(gtx, headerlayout.DropdownAnchor{TriggerRightX: u.phoneMainMenuAnchor.X, TriggerBottomY: u.phoneMainMenuAnchor.Y}, u.mainMenu)
|
|
||||||
}
|
|
||||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) syncMenuVisibleOnPhone() bool {
|
|
||||||
return u.usesCompactViewport() && u.phoneSyncMenuVisible && u.syncMenuOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) mainMenuVisibleOnPhone() bool {
|
|
||||||
return u.usesCompactViewport() && u.phoneMainMenuVisible && u.mainMenuOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) syncMenuDropsBelowTrigger() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) syncMenuRightAlignsToTrigger() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) headerMenusUseOverlayModel() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) mainMenuDropsBelowTrigger() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) mainMenuRightAlignsToTrigger() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func menuActionWidth(gtx layout.Context, rows []layout.Widget) int {
|
|
||||||
width := 0
|
|
||||||
for _, row := range rows {
|
|
||||||
measureGTX := gtx
|
|
||||||
measureGTX.Constraints.Min = image.Point{}
|
|
||||||
macro := op.Record(gtx.Ops)
|
|
||||||
dims := row(measureGTX)
|
|
||||||
_ = macro.Stop()
|
|
||||||
if dims.Size.X > width {
|
|
||||||
width = dims.Size.X
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return width
|
|
||||||
}
|
|
||||||
|
|
||||||
func rightAlignedMenuAction(gtx layout.Context, width int, child layout.Widget) layout.Dimensions {
|
|
||||||
if width <= 0 {
|
|
||||||
return child(gtx)
|
|
||||||
}
|
|
||||||
gtx.Constraints.Min.X = width
|
|
||||||
gtx.Constraints.Max.X = width
|
|
||||||
return layout.E.Layout(gtx, child)
|
|
||||||
}
|
}
|
||||||
@@ -5,28 +5,42 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
|
"gioui.org/layout"
|
||||||
"git.julianfamily.org/keepassgo/internal/appstate"
|
"git.julianfamily.org/keepassgo/internal/appstate"
|
||||||
|
editormodel "git.julianfamily.org/keepassgo/internal/appui/editor"
|
||||||
|
"git.julianfamily.org/keepassgo/internal/clipboard"
|
||||||
)
|
)
|
||||||
|
|
||||||
type focusID string
|
type focusID string
|
||||||
|
|
||||||
type detailField string
|
type detailField = editormodel.Field
|
||||||
|
|
||||||
const (
|
const (
|
||||||
focusSearch focusID = "search"
|
focusSearch focusID = "search"
|
||||||
|
|
||||||
detailFieldID detailField = "id"
|
detailFieldID = editormodel.FieldID
|
||||||
detailFieldTitle detailField = "title"
|
detailFieldTitle = editormodel.FieldTitle
|
||||||
detailFieldUsername detailField = "username"
|
detailFieldUsername = editormodel.FieldUsername
|
||||||
detailFieldPassword detailField = "password"
|
detailFieldPassword = editormodel.FieldPassword
|
||||||
detailFieldURL detailField = "url"
|
detailFieldURL = editormodel.FieldURL
|
||||||
detailFieldPath detailField = "path"
|
detailFieldPath = editormodel.FieldPath
|
||||||
detailFieldTags detailField = "tags"
|
detailFieldTags = editormodel.FieldTags
|
||||||
detailFieldPasswordProfile detailField = "password-profile"
|
detailFieldPasswordProfile = editormodel.FieldPasswordProfile
|
||||||
detailFieldNotes detailField = "notes"
|
detailFieldNotes = editormodel.FieldNotes
|
||||||
detailFieldFields detailField = "fields"
|
detailFieldFields = editormodel.FieldFields
|
||||||
detailFieldHistoryIndex detailField = "history-index"
|
detailFieldHistoryIndex = editormodel.FieldHistoryIndex
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
shortcutSearch = "search"
|
||||||
|
shortcutSave = "save"
|
||||||
|
shortcutLock = "lock"
|
||||||
|
shortcutNewEntry = "new-entry"
|
||||||
|
shortcutCopyUser = "copy-user"
|
||||||
|
shortcutCopyPassword = "copy-password"
|
||||||
|
shortcutCopyURL = "copy-url"
|
||||||
)
|
)
|
||||||
|
|
||||||
func breadcrumbFocusID(index int) focusID {
|
func breadcrumbFocusID(index int) focusID {
|
||||||
@@ -41,6 +55,68 @@ func detailFocusID(field detailField) focusID {
|
|||||||
return focusID("detail:" + string(field))
|
return focusID("detail:" + string(field))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) processShortcuts(gtx layout.Context) {
|
||||||
|
event.Op(gtx.Ops, u)
|
||||||
|
for {
|
||||||
|
ev, ok := gtx.Event(
|
||||||
|
key.Filter{Name: "F", Required: key.ModShortcut},
|
||||||
|
key.Filter{Name: "S", Required: key.ModShortcut},
|
||||||
|
key.Filter{Name: "L", Required: key.ModShortcut},
|
||||||
|
key.Filter{Name: "N", Required: key.ModShortcut},
|
||||||
|
key.Filter{Name: "U", Required: key.ModShortcut},
|
||||||
|
key.Filter{Name: "P", Required: key.ModShortcut},
|
||||||
|
key.Filter{Name: "O", Required: key.ModShortcut},
|
||||||
|
key.Filter{Name: key.NameTab, Optional: key.ModShift},
|
||||||
|
key.Filter{Name: key.NameLeftArrow},
|
||||||
|
key.Filter{Name: key.NameRightArrow},
|
||||||
|
key.Filter{Name: key.NameUpArrow},
|
||||||
|
key.Filter{Name: key.NameDownArrow},
|
||||||
|
key.Filter{Name: key.NameReturn},
|
||||||
|
key.Filter{Name: key.NameBack},
|
||||||
|
key.Filter{Name: key.NameEscape},
|
||||||
|
)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
ke, ok := ev.(key.Event)
|
||||||
|
if !ok || ke.State != key.Press {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u.handleKeyPress(ke.Name, ke.Modifiers)
|
||||||
|
if ke.Name == key.NameBack || ke.Name == key.NameEscape {
|
||||||
|
_ = u.handlePhoneBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) performShortcut(name string) error {
|
||||||
|
switch name {
|
||||||
|
case shortcutSearch:
|
||||||
|
u.keyboardFocus = focusSearch
|
||||||
|
return nil
|
||||||
|
case shortcutSave:
|
||||||
|
return u.saveAction()
|
||||||
|
case shortcutLock:
|
||||||
|
return u.lockAction()
|
||||||
|
case shortcutNewEntry:
|
||||||
|
u.state.BeginNewEntry()
|
||||||
|
u.loadSelectedEntryIntoEditor()
|
||||||
|
u.entryPath.SetText(strings.Join(u.state.CurrentPath, " / "))
|
||||||
|
u.keyboardFocus = detailFocusID(detailFieldTitle)
|
||||||
|
return nil
|
||||||
|
case shortcutCopyUser:
|
||||||
|
return u.copySelectedFieldAction(clipboard.TargetUsername)
|
||||||
|
case shortcutCopyPassword:
|
||||||
|
return u.copySelectedFieldAction(clipboard.TargetPassword)
|
||||||
|
case shortcutCopyURL:
|
||||||
|
return u.copySelectedFieldAction(clipboard.TargetURL)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) handleKeyPress(name key.Name, modifiers key.Modifiers) bool {
|
func (u *ui) handleKeyPress(name key.Name, modifiers key.Modifiers) bool {
|
||||||
if u.handleShortcutKey(name, modifiers) {
|
if u.handleShortcutKey(name, modifiers) {
|
||||||
return true
|
return true
|
||||||
@@ -336,19 +412,7 @@ func (u *ui) focusedDetailIndex() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func detailFocusOrder() []detailField {
|
func detailFocusOrder() []detailField {
|
||||||
return []detailField{
|
return editormodel.FocusOrder()
|
||||||
detailFieldID,
|
|
||||||
detailFieldTitle,
|
|
||||||
detailFieldUsername,
|
|
||||||
detailFieldPassword,
|
|
||||||
detailFieldURL,
|
|
||||||
detailFieldPath,
|
|
||||||
detailFieldTags,
|
|
||||||
detailFieldPasswordProfile,
|
|
||||||
detailFieldNotes,
|
|
||||||
detailFieldFields,
|
|
||||||
detailFieldHistoryIndex,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func canonicalFocusID(id focusID) focusID {
|
func canonicalFocusID(id focusID) focusID {
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package lifecycle
|
||||||
|
|
||||||
|
type OpenIntent string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OpenIntentNone OpenIntent = ""
|
||||||
|
OpenIntentRemoteSyncSetup OpenIntent = "remote_sync_setup"
|
||||||
|
OpenIntentRemoteSyncSettings OpenIntent = "remote_sync_settings"
|
||||||
|
)
|
||||||
@@ -18,6 +18,23 @@ import (
|
|||||||
"git.julianfamily.org/keepassgo/internal/appstate"
|
"git.julianfamily.org/keepassgo/internal/appstate"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (u *ui) lifecycleScreen(gtx layout.Context) layout.Dimensions {
|
||||||
|
panel := card
|
||||||
|
if u.usesCompactViewport() {
|
||||||
|
panel = compactCard
|
||||||
|
}
|
||||||
|
return panel(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
rows := []layout.Widget{
|
||||||
|
u.lifecycleBranding,
|
||||||
|
layout.Spacer{Height: unit.Dp(8)}.Layout,
|
||||||
|
u.lifecycleControls,
|
||||||
|
}
|
||||||
|
return material.List(u.theme, &u.lifecycleList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions {
|
||||||
|
return rows[i](gtx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||||
busy := u.lifecycleBusy()
|
busy := u.lifecycleBusy()
|
||||||
showLocalChooser := u.showLocalVaultChooser()
|
showLocalChooser := u.showLocalVaultChooser()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package list
|
package layout
|
||||||
|
|
||||||
type TopSection string
|
type TopSection string
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package list
|
||||||
|
|
||||||
|
type EntriesSectionState struct {
|
||||||
|
Path []string
|
||||||
|
SearchQuery string
|
||||||
|
SelectedEntryID string
|
||||||
|
Editing bool
|
||||||
|
}
|
||||||
@@ -25,8 +25,8 @@ import (
|
|||||||
"git.julianfamily.org/keepassgo/internal/apiaudit"
|
"git.julianfamily.org/keepassgo/internal/apiaudit"
|
||||||
"git.julianfamily.org/keepassgo/internal/apitokens"
|
"git.julianfamily.org/keepassgo/internal/apitokens"
|
||||||
"git.julianfamily.org/keepassgo/internal/appstate"
|
"git.julianfamily.org/keepassgo/internal/appstate"
|
||||||
headerlayout "git.julianfamily.org/keepassgo/internal/appui/layout/header"
|
headerlayout "git.julianfamily.org/keepassgo/internal/appui/header/layout"
|
||||||
listlayout "git.julianfamily.org/keepassgo/internal/appui/layout/list"
|
listlayout "git.julianfamily.org/keepassgo/internal/appui/list/layout"
|
||||||
"git.julianfamily.org/keepassgo/internal/clipboard"
|
"git.julianfamily.org/keepassgo/internal/clipboard"
|
||||||
"git.julianfamily.org/keepassgo/internal/passwords"
|
"git.julianfamily.org/keepassgo/internal/passwords"
|
||||||
"git.julianfamily.org/keepassgo/internal/session"
|
"git.julianfamily.org/keepassgo/internal/session"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package appui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -9,29 +11,28 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
|
"gioui.org/op/clip"
|
||||||
|
"gioui.org/op/paint"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"gioui.org/widget"
|
"gioui.org/widget"
|
||||||
"gioui.org/widget/material"
|
"gioui.org/widget/material"
|
||||||
|
editormodel "git.julianfamily.org/keepassgo/internal/appui/editor"
|
||||||
|
settingsmodel "git.julianfamily.org/keepassgo/internal/appui/settings"
|
||||||
"git.julianfamily.org/keepassgo/internal/vault"
|
"git.julianfamily.org/keepassgo/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
displayDensityDense = "dense"
|
displayDensityDense = settingsmodel.DisplayDensityDense
|
||||||
displayDensityComfortable = "comfortable"
|
displayDensityComfortable = settingsmodel.DisplayDensityComfortable
|
||||||
|
|
||||||
contrastStandard = "standard"
|
contrastStandard = settingsmodel.ContrastStandard
|
||||||
contrastHigh = "high"
|
contrastHigh = settingsmodel.ContrastHigh
|
||||||
|
|
||||||
keyboardFocusStandard = "standard"
|
keyboardFocusStandard = settingsmodel.KeyboardFocusStandard
|
||||||
keyboardFocusProminent = "prominent"
|
keyboardFocusProminent = settingsmodel.KeyboardFocusProminent
|
||||||
)
|
)
|
||||||
|
|
||||||
type accessibilityPreferences struct {
|
type accessibilityPreferences = settingsmodel.AccessibilityPreferences
|
||||||
DisplayDensity string
|
|
||||||
Contrast string
|
|
||||||
ReducedMotion bool
|
|
||||||
KeyboardFocus string
|
|
||||||
}
|
|
||||||
|
|
||||||
type settingsFile struct {
|
type settingsFile struct {
|
||||||
Sync syncSettings `json:"sync,omitempty"`
|
Sync syncSettings `json:"sync,omitempty"`
|
||||||
@@ -63,37 +64,23 @@ type choiceSpec struct {
|
|||||||
Active bool
|
Active bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type focusAppearance struct {
|
||||||
|
BorderColor color.NRGBA
|
||||||
|
OutlineColor color.NRGBA
|
||||||
|
OutlineWidth int
|
||||||
|
MinHeight int
|
||||||
|
}
|
||||||
|
|
||||||
func defaultAccessibilityPreferences() accessibilityPreferences {
|
func defaultAccessibilityPreferences() accessibilityPreferences {
|
||||||
return accessibilityPreferences{
|
return settingsmodel.DefaultAccessibilityPreferences()
|
||||||
DisplayDensity: displayDensityForDenseLayout(true),
|
|
||||||
Contrast: contrastStandard,
|
|
||||||
KeyboardFocus: keyboardFocusStandard,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayDensityForDenseLayout(dense bool) string {
|
func displayDensityForDenseLayout(dense bool) string {
|
||||||
if dense {
|
return settingsmodel.DisplayDensityForDenseLayout(dense)
|
||||||
return displayDensityDense
|
|
||||||
}
|
|
||||||
return displayDensityComfortable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeAccessibilityPreferences(prefs accessibilityPreferences) accessibilityPreferences {
|
func normalizeAccessibilityPreferences(prefs accessibilityPreferences) accessibilityPreferences {
|
||||||
normalized := defaultAccessibilityPreferences()
|
return settingsmodel.NormalizeAccessibilityPreferences(prefs)
|
||||||
switch prefs.DisplayDensity {
|
|
||||||
case displayDensityDense, displayDensityComfortable:
|
|
||||||
normalized.DisplayDensity = prefs.DisplayDensity
|
|
||||||
}
|
|
||||||
switch prefs.Contrast {
|
|
||||||
case contrastStandard, contrastHigh:
|
|
||||||
normalized.Contrast = prefs.Contrast
|
|
||||||
}
|
|
||||||
switch prefs.KeyboardFocus {
|
|
||||||
case keyboardFocusStandard, keyboardFocusProminent:
|
|
||||||
normalized.KeyboardFocus = prefs.KeyboardFocus
|
|
||||||
}
|
|
||||||
normalized.ReducedMotion = prefs.ReducedMotion
|
|
||||||
return normalized
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) applyAccessibilityPreferences(prefs accessibilityPreferences) {
|
func (u *ui) applyAccessibilityPreferences(prefs accessibilityPreferences) {
|
||||||
@@ -102,6 +89,96 @@ func (u *ui) applyAccessibilityPreferences(prefs accessibilityPreferences) {
|
|||||||
u.accessibilityPrefs = normalized
|
u.accessibilityPrefs = normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fieldFocusAppearance(metric unit.Metric, prefs accessibilityPreferences, focused bool) focusAppearance {
|
||||||
|
prefs = normalizeAccessibilityPreferences(prefs)
|
||||||
|
appearance := focusAppearance{
|
||||||
|
BorderColor: color.NRGBA{R: 202, G: 194, B: 180, A: 255},
|
||||||
|
OutlineColor: color.NRGBA{A: 0},
|
||||||
|
OutlineWidth: max(1, metric.Dp(unit.Dp(1))),
|
||||||
|
MinHeight: metric.Dp(unit.Dp(44)),
|
||||||
|
}
|
||||||
|
if prefs.DisplayDensity == displayDensityComfortable {
|
||||||
|
appearance.MinHeight = metric.Dp(unit.Dp(52))
|
||||||
|
}
|
||||||
|
if prefs.Contrast == contrastHigh {
|
||||||
|
appearance.BorderColor = color.NRGBA{R: 108, G: 101, B: 90, A: 255}
|
||||||
|
}
|
||||||
|
if focused {
|
||||||
|
appearance.BorderColor = accentColor
|
||||||
|
appearance.OutlineColor = color.NRGBA{R: 28, G: 83, B: 63, A: 72}
|
||||||
|
appearance.OutlineWidth = max(2, metric.Dp(unit.Dp(2)))
|
||||||
|
if prefs.Contrast == contrastHigh {
|
||||||
|
appearance.BorderColor = color.NRGBA{R: 16, G: 60, B: 44, A: 255}
|
||||||
|
appearance.OutlineColor = color.NRGBA{R: 20, G: 74, B: 55, A: 124}
|
||||||
|
}
|
||||||
|
if prefs.KeyboardFocus == keyboardFocusProminent {
|
||||||
|
appearance.OutlineWidth = max(3, metric.Dp(unit.Dp(3)))
|
||||||
|
appearance.OutlineColor = color.NRGBA{R: 20, G: 74, B: 55, A: 148}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appearance
|
||||||
|
}
|
||||||
|
|
||||||
|
func buttonFocusColors(prefs accessibilityPreferences, focused bool) (background color.NRGBA, text color.NRGBA) {
|
||||||
|
prefs = normalizeAccessibilityPreferences(prefs)
|
||||||
|
background = color.NRGBA{R: 231, G: 239, B: 235, A: 255}
|
||||||
|
text = accentColor
|
||||||
|
if prefs.Contrast == contrastHigh {
|
||||||
|
background = color.NRGBA{R: 225, G: 235, B: 230, A: 255}
|
||||||
|
text = color.NRGBA{R: 19, G: 57, B: 43, A: 255}
|
||||||
|
}
|
||||||
|
if focused {
|
||||||
|
background = color.NRGBA{R: 214, G: 229, B: 221, A: 255}
|
||||||
|
if prefs.Contrast == contrastHigh || prefs.KeyboardFocus == keyboardFocusProminent {
|
||||||
|
background = color.NRGBA{R: 202, G: 222, B: 212, A: 255}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return background, text
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) accessibilityLabel(id focusID) string {
|
||||||
|
switch {
|
||||||
|
case id == focusSearch:
|
||||||
|
return "Search vault"
|
||||||
|
case strings.HasPrefix(string(id), "breadcrumb:"):
|
||||||
|
index := focusIndex(id)
|
||||||
|
crumbs := u.breadcrumbLabels()
|
||||||
|
if index >= 0 && index < len(crumbs) {
|
||||||
|
return fmt.Sprintf("Navigate to %s", crumbs[index])
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(string(id), "list:"):
|
||||||
|
index := focusIndex(id)
|
||||||
|
if index >= 0 && index < len(u.visible) {
|
||||||
|
return fmt.Sprintf("Select entry %s", u.visible[index].Title)
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(string(id), "detail:"):
|
||||||
|
name := strings.TrimPrefix(string(id), "detail:")
|
||||||
|
return fmt.Sprintf("Edit %s", detailFieldLabel(detailField(name)))
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawFocusOutline(gtx layout.Context, appearance focusAppearance, size image.Point) layout.Dimensions {
|
||||||
|
if appearance.OutlineColor.A == 0 || appearance.OutlineWidth <= 0 {
|
||||||
|
return layout.Dimensions{Size: size}
|
||||||
|
}
|
||||||
|
|
||||||
|
width := appearance.OutlineWidth
|
||||||
|
paint.FillShape(gtx.Ops, appearance.OutlineColor, clip.Rect{Max: image.Pt(size.X, width)}.Op())
|
||||||
|
paint.FillShape(gtx.Ops, appearance.OutlineColor, clip.Rect{Min: image.Pt(0, size.Y-width), Max: image.Pt(size.X, size.Y)}.Op())
|
||||||
|
paint.FillShape(gtx.Ops, appearance.OutlineColor, clip.Rect{Max: image.Pt(width, size.Y)}.Op())
|
||||||
|
paint.FillShape(gtx.Ops, appearance.OutlineColor, clip.Rect{Min: image.Pt(size.X-width, 0), Max: image.Pt(size.X, size.Y)}.Op())
|
||||||
|
return layout.Dimensions{Size: size}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) isFocused(id focusID) bool {
|
||||||
|
return u.keyboardFocus == id
|
||||||
|
}
|
||||||
|
|
||||||
|
func detailFieldLabel(field detailField) string {
|
||||||
|
return editormodel.Label(field)
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) loadSettingsDraft() {
|
func (u *ui) loadSettingsDraft() {
|
||||||
u.settingsDraft = settingsDraft{
|
u.settingsDraft = settingsDraft{
|
||||||
Accessibility: accessibilityPreferences{
|
Accessibility: accessibilityPreferences{
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
type AccessibilityPreferences struct {
|
||||||
|
DisplayDensity string
|
||||||
|
Contrast string
|
||||||
|
ReducedMotion bool
|
||||||
|
KeyboardFocus string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DisplayDensityDense = "dense"
|
||||||
|
DisplayDensityComfortable = "comfortable"
|
||||||
|
ContrastStandard = "standard"
|
||||||
|
ContrastHigh = "high"
|
||||||
|
KeyboardFocusStandard = "standard"
|
||||||
|
KeyboardFocusProminent = "prominent"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DefaultAccessibilityPreferences() AccessibilityPreferences {
|
||||||
|
return AccessibilityPreferences{
|
||||||
|
DisplayDensity: DisplayDensityDense,
|
||||||
|
Contrast: ContrastStandard,
|
||||||
|
KeyboardFocus: KeyboardFocusStandard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DisplayDensityForDenseLayout(dense bool) string {
|
||||||
|
if dense {
|
||||||
|
return DisplayDensityDense
|
||||||
|
}
|
||||||
|
return DisplayDensityComfortable
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizeAccessibilityPreferences(prefs AccessibilityPreferences) AccessibilityPreferences {
|
||||||
|
normalized := DefaultAccessibilityPreferences()
|
||||||
|
switch prefs.DisplayDensity {
|
||||||
|
case DisplayDensityDense, DisplayDensityComfortable:
|
||||||
|
normalized.DisplayDensity = prefs.DisplayDensity
|
||||||
|
}
|
||||||
|
switch prefs.Contrast {
|
||||||
|
case ContrastStandard, ContrastHigh:
|
||||||
|
normalized.Contrast = prefs.Contrast
|
||||||
|
}
|
||||||
|
switch prefs.KeyboardFocus {
|
||||||
|
case KeyboardFocusStandard, KeyboardFocusProminent:
|
||||||
|
normalized.KeyboardFocus = prefs.KeyboardFocus
|
||||||
|
}
|
||||||
|
normalized.ReducedMotion = prefs.ReducedMotion
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package sync
|
||||||
|
|
||||||
|
import "git.julianfamily.org/keepassgo/internal/appstate"
|
||||||
|
|
||||||
|
type SourceMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SourceLocal SourceMode = "local"
|
||||||
|
SourceRemote SourceMode = "remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Direction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DirectionPull Direction = "pull"
|
||||||
|
DirectionPush Direction = "push"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DialogPurpose string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DialogPurposeAdvanced DialogPurpose = "advanced"
|
||||||
|
DialogPurposeRemoteSetup DialogPurpose = "remote-setup"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MenuModel struct {
|
||||||
|
HasOpenVault bool
|
||||||
|
HasSelectedBinding bool
|
||||||
|
ShowSelectors bool
|
||||||
|
ShowShare bool
|
||||||
|
ShowSaveCurrentBinding bool
|
||||||
|
SavedBindingSummary MenuBindingSummary
|
||||||
|
RemoteBaseURL string
|
||||||
|
RemotePath string
|
||||||
|
RemoteUsername string
|
||||||
|
RemotePassword string
|
||||||
|
SelectedVaultSyncMode appstate.SyncMode
|
||||||
|
}
|
||||||
|
|
||||||
|
type MenuBindingSummary struct {
|
||||||
|
ProfileLabel string
|
||||||
|
CredentialLabel string
|
||||||
|
SyncLabel string
|
||||||
|
OK bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MenuModel) SavedBindingHeading() string {
|
||||||
|
if !m.ShowSelectors {
|
||||||
|
return "Use this vault's saved remote sync target"
|
||||||
|
}
|
||||||
|
return "Use a saved remote profile from this vault"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MenuModel) OpenSelectedButtonLabel() string {
|
||||||
|
if !m.ShowSelectors {
|
||||||
|
return "Use Remote Sync"
|
||||||
|
}
|
||||||
|
return "Open Saved Remote"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MenuModel) ShowDirectRemoteSyncShortcut() bool {
|
||||||
|
return m.HasOpenVault && m.HasSelectedBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MenuModel) DirectRemoteSyncShortcutLabel() string { return "Use Remote Sync" }
|
||||||
|
|
||||||
|
func (m MenuModel) ShowRemoteSyncSettingsShortcut() bool {
|
||||||
|
return m.HasOpenVault && m.HasSelectedBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MenuModel) RemoteSyncSettingsShortcutLabel() string { return "Remote Sync Settings" }
|
||||||
|
|
||||||
|
func (m MenuModel) ShowRemoveRemoteSyncShortcut() bool { return m.ShowRemoteSyncSettingsShortcut() }
|
||||||
|
|
||||||
|
func (m MenuModel) RemoveRemoteSyncShortcutLabel() string { return "Stop Using Remote Sync" }
|
||||||
|
|
||||||
|
func (m MenuModel) ShowRemoteSyncSetupShortcut() bool {
|
||||||
|
return m.HasOpenVault && !m.HasSelectedBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MenuModel) RemoteSyncSetupShortcutLabel() string { return "Set Up Remote Sync" }
|
||||||
|
|
||||||
|
func (m MenuModel) ActionLabels() []string {
|
||||||
|
labels := []string{"Open Advanced Sync"}
|
||||||
|
if m.ShowRemoteSyncSetupShortcut() {
|
||||||
|
labels = append(labels, m.RemoteSyncSetupShortcutLabel())
|
||||||
|
}
|
||||||
|
if m.ShowDirectRemoteSyncShortcut() {
|
||||||
|
labels = append(labels, m.DirectRemoteSyncShortcutLabel())
|
||||||
|
}
|
||||||
|
if m.ShowRemoteSyncSettingsShortcut() {
|
||||||
|
labels = append(labels, m.RemoteSyncSettingsShortcutLabel())
|
||||||
|
}
|
||||||
|
if m.ShowRemoveRemoteSyncShortcut() {
|
||||||
|
labels = append(labels, m.RemoveRemoteSyncShortcutLabel())
|
||||||
|
}
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MenuModel) SaveCurrentRemoteBindingHeading() string {
|
||||||
|
return "Bind this local vault to the current remote target"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MenuModel) SaveCurrentRemoteBindingButtonLabel() string { return "Save Remote In Vault" }
|
||||||
|
|
||||||
|
func SummaryText(purpose DialogPurpose, source SourceMode, direction Direction) string {
|
||||||
|
if purpose == DialogPurposeRemoteSetup {
|
||||||
|
return "Push this local vault to a WebDAV target and save that target for future sync."
|
||||||
|
}
|
||||||
|
sourceLabel := "another local vault file"
|
||||||
|
if source == SourceRemote {
|
||||||
|
sourceLabel = "another WebDAV-backed vault"
|
||||||
|
}
|
||||||
|
action := "Pull changes from"
|
||||||
|
if direction == DirectionPush {
|
||||||
|
action = "Push the current vault into"
|
||||||
|
}
|
||||||
|
return action + " " + sourceLabel + "."
|
||||||
|
}
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
package appui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gioui.org/layout"
|
|
||||||
"gioui.org/op/clip"
|
|
||||||
"gioui.org/op/paint"
|
|
||||||
"gioui.org/unit"
|
|
||||||
)
|
|
||||||
|
|
||||||
type focusAppearance struct {
|
|
||||||
BorderColor color.NRGBA
|
|
||||||
OutlineColor color.NRGBA
|
|
||||||
OutlineWidth int
|
|
||||||
MinHeight int
|
|
||||||
}
|
|
||||||
|
|
||||||
func fieldFocusAppearance(metric unit.Metric, prefs accessibilityPreferences, focused bool) focusAppearance {
|
|
||||||
prefs = normalizeAccessibilityPreferences(prefs)
|
|
||||||
appearance := focusAppearance{
|
|
||||||
BorderColor: color.NRGBA{R: 202, G: 194, B: 180, A: 255},
|
|
||||||
OutlineColor: color.NRGBA{A: 0},
|
|
||||||
OutlineWidth: max(1, metric.Dp(unit.Dp(1))),
|
|
||||||
MinHeight: metric.Dp(unit.Dp(44)),
|
|
||||||
}
|
|
||||||
if prefs.DisplayDensity == displayDensityComfortable {
|
|
||||||
appearance.MinHeight = metric.Dp(unit.Dp(52))
|
|
||||||
}
|
|
||||||
if prefs.Contrast == contrastHigh {
|
|
||||||
appearance.BorderColor = color.NRGBA{R: 108, G: 101, B: 90, A: 255}
|
|
||||||
}
|
|
||||||
if focused {
|
|
||||||
appearance.BorderColor = accentColor
|
|
||||||
appearance.OutlineColor = color.NRGBA{R: 28, G: 83, B: 63, A: 72}
|
|
||||||
appearance.OutlineWidth = max(2, metric.Dp(unit.Dp(2)))
|
|
||||||
if prefs.Contrast == contrastHigh {
|
|
||||||
appearance.BorderColor = color.NRGBA{R: 16, G: 60, B: 44, A: 255}
|
|
||||||
appearance.OutlineColor = color.NRGBA{R: 20, G: 74, B: 55, A: 124}
|
|
||||||
}
|
|
||||||
if prefs.KeyboardFocus == keyboardFocusProminent {
|
|
||||||
appearance.OutlineWidth = max(3, metric.Dp(unit.Dp(3)))
|
|
||||||
appearance.OutlineColor = color.NRGBA{R: 20, G: 74, B: 55, A: 148}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return appearance
|
|
||||||
}
|
|
||||||
|
|
||||||
func buttonFocusColors(prefs accessibilityPreferences, focused bool) (background color.NRGBA, text color.NRGBA) {
|
|
||||||
prefs = normalizeAccessibilityPreferences(prefs)
|
|
||||||
background = color.NRGBA{R: 231, G: 239, B: 235, A: 255}
|
|
||||||
text = accentColor
|
|
||||||
if prefs.Contrast == contrastHigh {
|
|
||||||
background = color.NRGBA{R: 225, G: 235, B: 230, A: 255}
|
|
||||||
text = color.NRGBA{R: 19, G: 57, B: 43, A: 255}
|
|
||||||
}
|
|
||||||
if focused {
|
|
||||||
background = color.NRGBA{R: 214, G: 229, B: 221, A: 255}
|
|
||||||
if prefs.Contrast == contrastHigh || prefs.KeyboardFocus == keyboardFocusProminent {
|
|
||||||
background = color.NRGBA{R: 202, G: 222, B: 212, A: 255}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return background, text
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) accessibilityLabel(id focusID) string {
|
|
||||||
switch {
|
|
||||||
case id == focusSearch:
|
|
||||||
return "Search vault"
|
|
||||||
case strings.HasPrefix(string(id), "breadcrumb:"):
|
|
||||||
index := focusIndex(id)
|
|
||||||
crumbs := u.breadcrumbLabels()
|
|
||||||
if index >= 0 && index < len(crumbs) {
|
|
||||||
return fmt.Sprintf("Navigate to %s", crumbs[index])
|
|
||||||
}
|
|
||||||
case strings.HasPrefix(string(id), "list:"):
|
|
||||||
index := focusIndex(id)
|
|
||||||
if index >= 0 && index < len(u.visible) {
|
|
||||||
return fmt.Sprintf("Select entry %s", u.visible[index].Title)
|
|
||||||
}
|
|
||||||
case strings.HasPrefix(string(id), "detail:"):
|
|
||||||
name := strings.TrimPrefix(string(id), "detail:")
|
|
||||||
return fmt.Sprintf("Edit %s", detailFieldLabel(detailField(name)))
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawFocusOutline(gtx layout.Context, appearance focusAppearance, size image.Point) layout.Dimensions {
|
|
||||||
if appearance.OutlineColor.A == 0 || appearance.OutlineWidth <= 0 {
|
|
||||||
return layout.Dimensions{Size: size}
|
|
||||||
}
|
|
||||||
|
|
||||||
width := appearance.OutlineWidth
|
|
||||||
paint.FillShape(gtx.Ops, appearance.OutlineColor, clip.Rect{Max: image.Pt(size.X, width)}.Op())
|
|
||||||
paint.FillShape(gtx.Ops, appearance.OutlineColor, clip.Rect{Min: image.Pt(0, size.Y-width), Max: image.Pt(size.X, size.Y)}.Op())
|
|
||||||
paint.FillShape(gtx.Ops, appearance.OutlineColor, clip.Rect{Max: image.Pt(width, size.Y)}.Op())
|
|
||||||
paint.FillShape(gtx.Ops, appearance.OutlineColor, clip.Rect{Min: image.Pt(size.X-width, 0), Max: image.Pt(size.X, size.Y)}.Op())
|
|
||||||
return layout.Dimensions{Size: size}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) isFocused(id focusID) bool {
|
|
||||||
return u.keyboardFocus == id
|
|
||||||
}
|
|
||||||
|
|
||||||
func detailFieldLabel(field detailField) string {
|
|
||||||
switch field {
|
|
||||||
case detailFieldID:
|
|
||||||
return "ID"
|
|
||||||
case detailFieldTitle:
|
|
||||||
return "Title"
|
|
||||||
case detailFieldUsername:
|
|
||||||
return "Username"
|
|
||||||
case detailFieldPassword:
|
|
||||||
return "Password"
|
|
||||||
case detailFieldURL:
|
|
||||||
return "URL"
|
|
||||||
case detailFieldPath:
|
|
||||||
return "Path"
|
|
||||||
case detailFieldTags:
|
|
||||||
return "Tags"
|
|
||||||
case detailFieldPasswordProfile:
|
|
||||||
return "Password Profile"
|
|
||||||
case detailFieldNotes:
|
|
||||||
return "Notes"
|
|
||||||
case detailFieldFields:
|
|
||||||
return "Custom Fields"
|
|
||||||
case detailFieldHistoryIndex:
|
|
||||||
return "History Index"
|
|
||||||
default:
|
|
||||||
return strings.ReplaceAll(string(field), "-", " ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package appui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
|
|
||||||
"gioui.org/layout"
|
|
||||||
"gioui.org/op/paint"
|
|
||||||
"gioui.org/unit"
|
|
||||||
"gioui.org/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (u *ui) lifecycleBranding(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !u.usesCompactViewport() {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) brandMark(gtx layout.Context, widthDP, heightDP float32) layout.Dimensions {
|
|
||||||
if u.usesCompactViewport() {
|
|
||||||
return u.brandImage(gtx, u.splashSquare, widthDP, heightDP)
|
|
||||||
}
|
|
||||||
return u.brandImage(gtx, u.logoHorizontal, widthDP, heightDP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) brandImage(gtx layout.Context, src paint.ImageOp, widthDP, heightDP float32) layout.Dimensions {
|
|
||||||
width := gtx.Dp(unit.Dp(widthDP))
|
|
||||||
height := gtx.Dp(unit.Dp(heightDP))
|
|
||||||
if width > gtx.Constraints.Max.X {
|
|
||||||
width = gtx.Constraints.Max.X
|
|
||||||
}
|
|
||||||
if height > gtx.Constraints.Max.Y && gtx.Constraints.Max.Y > 0 {
|
|
||||||
height = gtx.Constraints.Max.Y
|
|
||||||
}
|
|
||||||
img := widget.Image{
|
|
||||||
Src: src,
|
|
||||||
Fit: widget.Contain,
|
|
||||||
Position: layout.W,
|
|
||||||
Scale: 1.0 / gtx.Metric.PxPerDp,
|
|
||||||
}
|
|
||||||
gtx.Constraints.Min = image.Point{}
|
|
||||||
gtx.Constraints.Max = image.Pt(width, height)
|
|
||||||
return img.Layout(gtx)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package appui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gioui.org/layout"
|
|
||||||
"gioui.org/unit"
|
|
||||||
"gioui.org/widget/material"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (u *ui) lifecycleScreen(gtx layout.Context) layout.Dimensions {
|
|
||||||
panel := card
|
|
||||||
if u.usesCompactViewport() {
|
|
||||||
panel = compactCard
|
|
||||||
}
|
|
||||||
return panel(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
||||||
rows := []layout.Widget{
|
|
||||||
u.lifecycleBranding,
|
|
||||||
layout.Spacer{Height: unit.Dp(8)}.Layout,
|
|
||||||
u.lifecycleControls,
|
|
||||||
}
|
|
||||||
return material.List(u.theme, &u.lifecycleList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions {
|
|
||||||
return rows[i](gtx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package appui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gioui.org/io/event"
|
|
||||||
"gioui.org/io/key"
|
|
||||||
"gioui.org/layout"
|
|
||||||
|
|
||||||
"git.julianfamily.org/keepassgo/internal/clipboard"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
shortcutSearch = "search"
|
|
||||||
shortcutSave = "save"
|
|
||||||
shortcutLock = "lock"
|
|
||||||
shortcutNewEntry = "new-entry"
|
|
||||||
shortcutCopyUser = "copy-user"
|
|
||||||
shortcutCopyPassword = "copy-password"
|
|
||||||
shortcutCopyURL = "copy-url"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (u *ui) processShortcuts(gtx layout.Context) {
|
|
||||||
event.Op(gtx.Ops, u)
|
|
||||||
for {
|
|
||||||
ev, ok := gtx.Event(
|
|
||||||
key.Filter{Name: "F", Required: key.ModShortcut},
|
|
||||||
key.Filter{Name: "S", Required: key.ModShortcut},
|
|
||||||
key.Filter{Name: "L", Required: key.ModShortcut},
|
|
||||||
key.Filter{Name: "N", Required: key.ModShortcut},
|
|
||||||
key.Filter{Name: "U", Required: key.ModShortcut},
|
|
||||||
key.Filter{Name: "P", Required: key.ModShortcut},
|
|
||||||
key.Filter{Name: "O", Required: key.ModShortcut},
|
|
||||||
key.Filter{Name: key.NameTab, Optional: key.ModShift},
|
|
||||||
key.Filter{Name: key.NameLeftArrow},
|
|
||||||
key.Filter{Name: key.NameRightArrow},
|
|
||||||
key.Filter{Name: key.NameUpArrow},
|
|
||||||
key.Filter{Name: key.NameDownArrow},
|
|
||||||
key.Filter{Name: key.NameReturn},
|
|
||||||
key.Filter{Name: key.NameBack},
|
|
||||||
key.Filter{Name: key.NameEscape},
|
|
||||||
)
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
ke, ok := ev.(key.Event)
|
|
||||||
if !ok || ke.State != key.Press {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
u.handleKeyPress(ke.Name, ke.Modifiers)
|
|
||||||
if ke.Name == key.NameBack || ke.Name == key.NameEscape {
|
|
||||||
_ = u.handlePhoneBack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) performShortcut(name string) error {
|
|
||||||
switch name {
|
|
||||||
case shortcutSearch:
|
|
||||||
u.keyboardFocus = focusSearch
|
|
||||||
return nil
|
|
||||||
case shortcutSave:
|
|
||||||
return u.saveAction()
|
|
||||||
case shortcutLock:
|
|
||||||
return u.lockAction()
|
|
||||||
case shortcutNewEntry:
|
|
||||||
u.state.BeginNewEntry()
|
|
||||||
u.loadSelectedEntryIntoEditor()
|
|
||||||
u.entryPath.SetText(strings.Join(u.state.CurrentPath, " / "))
|
|
||||||
u.keyboardFocus = detailFocusID(detailFieldTitle)
|
|
||||||
return nil
|
|
||||||
case shortcutCopyUser:
|
|
||||||
return u.copySelectedFieldAction(clipboard.TargetUsername)
|
|
||||||
case shortcutCopyPassword:
|
|
||||||
return u.copySelectedFieldAction(clipboard.TargetPassword)
|
|
||||||
case shortcutCopyURL:
|
|
||||||
return u.copySelectedFieldAction(clipboard.TargetURL)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package appui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.julianfamily.org/keepassgo/internal/appstate"
|
|
||||||
appuiactions "git.julianfamily.org/keepassgo/internal/appui/actions"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (u *ui) buildSyncMenuModel() appuiactions.SyncMenuModel {
|
|
||||||
model := appuiactions.SyncMenuModel{
|
|
||||||
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() appuiactions.SyncMenuBindingSummary {
|
|
||||||
profile, ok := u.selectedVaultRemoteProfile()
|
|
||||||
if !ok {
|
|
||||||
return appuiactions.SyncMenuBindingSummary{}
|
|
||||||
}
|
|
||||||
entry, ok := u.selectedVaultRemoteCredentialEntry()
|
|
||||||
if !ok {
|
|
||||||
return appuiactions.SyncMenuBindingSummary{}
|
|
||||||
}
|
|
||||||
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 appuiactions.SyncMenuBindingSummary{
|
|
||||||
ProfileLabel: profile.Name,
|
|
||||||
CredentialLabel: credentialLabel,
|
|
||||||
SyncLabel: syncLabel,
|
|
||||||
OK: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user