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"
|
||||
"git.julianfamily.org/keepassgo/internal/apiaudit"
|
||||
"git.julianfamily.org/keepassgo/internal/apitokens"
|
||||
apiui "git.julianfamily.org/keepassgo/internal/appui/api"
|
||||
)
|
||||
|
||||
func apiOperations() []apitokens.Operation {
|
||||
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, " "))
|
||||
}
|
||||
type apiAuditQuickFilter = apiui.AuditQuickFilter
|
||||
|
||||
func apiAuditFilterButtons(clicks *[]widget.Clickable, filters []apiAuditQuickFilter) []widget.Clickable {
|
||||
if len(filters) == 0 {
|
||||
@@ -126,7 +47,7 @@ func (u *ui) apiAuditQuickFilters(events []apiaudit.Event) ([]apiAuditQuickFilte
|
||||
}
|
||||
if _, ok := decisionSeen[event.Type]; !ok {
|
||||
decisionSeen[event.Type] = struct{}{}
|
||||
label := apiAuditDecisionLabel(event.Type)
|
||||
label := apiui.AuditDecisionLabel(event.Type)
|
||||
decisions = append(decisions, apiAuditQuickFilter{Label: label, Query: label})
|
||||
}
|
||||
if strings.TrimSpace(string(event.Operation)) == "" {
|
||||
@@ -136,7 +57,7 @@ func (u *ui) apiAuditQuickFilters(events []apiaudit.Event) ([]apiAuditQuickFilte
|
||||
continue
|
||||
}
|
||||
operationSeen[event.Operation] = struct{}{}
|
||||
label := apiAuditOperationLabel(event.Operation)
|
||||
label := apiui.AuditOperationLabel(event.Operation)
|
||||
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) {
|
||||
value := apitokens.Operation(strings.TrimSpace(text))
|
||||
for _, operation := range apiOperations() {
|
||||
for _, operation := range apiui.Operations() {
|
||||
if operation == value {
|
||||
return value, nil
|
||||
}
|
||||
@@ -450,7 +371,7 @@ func (u *ui) apiAuditEvents() []apiaudit.Event {
|
||||
}
|
||||
filtered := make([]apiaudit.Event, 0, len(events))
|
||||
for _, event := range events {
|
||||
haystack := apiAuditEventSearchTerms(event)
|
||||
haystack := apiui.AuditEventSearchTerms(event)
|
||||
if strings.Contains(haystack, query) {
|
||||
filtered = append(filtered, event)
|
||||
}
|
||||
@@ -785,7 +706,7 @@ func (u *ui) apiAuditQuickFilterRow(gtx layout.Context, title string, filters []
|
||||
click := &buttons[i]
|
||||
selected := strings.EqualFold(strings.TrimSpace(u.search.Text()), strings.TrimSpace(filter.Query))
|
||||
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...)
|
||||
@@ -799,7 +720,7 @@ func (u *ui) apiAuditQuickFilterRow(gtx layout.Context, title string, filters []
|
||||
click := &buttons[i]
|
||||
selected := strings.EqualFold(strings.TrimSpace(u.search.Text()), strings.TrimSpace(filter.Query))
|
||||
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...)
|
||||
@@ -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)
|
||||
}),
|
||||
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(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.apiPolicyGroupScopeW.Value {
|
||||
+24
-46
@@ -26,9 +26,13 @@ import (
|
||||
"git.julianfamily.org/keepassgo/internal/apiaudit"
|
||||
"git.julianfamily.org/keepassgo/internal/apitokens"
|
||||
"git.julianfamily.org/keepassgo/internal/appstate"
|
||||
detaillayout "git.julianfamily.org/keepassgo/internal/appui/layout/detail"
|
||||
listlayout "git.julianfamily.org/keepassgo/internal/appui/layout/list"
|
||||
detailmodel "git.julianfamily.org/keepassgo/internal/appui/detail"
|
||||
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"
|
||||
syncmodel "git.julianfamily.org/keepassgo/internal/appui/sync"
|
||||
keepassassets "git.julianfamily.org/keepassgo/internal/assets"
|
||||
"git.julianfamily.org/keepassgo/internal/clipboard"
|
||||
"git.julianfamily.org/keepassgo/internal/passwords"
|
||||
@@ -105,24 +109,17 @@ type uiSurface struct {
|
||||
Locked bool
|
||||
}
|
||||
|
||||
type lifecycleOpenIntent string
|
||||
type lifecycleOpenIntent = lifecyclemodel.OpenIntent
|
||||
|
||||
const (
|
||||
lifecycleOpenIntentNone lifecycleOpenIntent = ""
|
||||
lifecycleOpenIntentRemoteSyncSetup lifecycleOpenIntent = "remote_sync_setup"
|
||||
lifecycleOpenIntentRemoteSyncSettings lifecycleOpenIntent = "remote_sync_settings"
|
||||
lifecycleOpenIntentNone = lifecyclemodel.OpenIntentNone
|
||||
lifecycleOpenIntentRemoteSyncSetup = lifecyclemodel.OpenIntentRemoteSyncSetup
|
||||
lifecycleOpenIntentRemoteSyncSettings = lifecyclemodel.OpenIntentRemoteSyncSettings
|
||||
)
|
||||
|
||||
type emptyState struct {
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
type emptyState = detailmodel.EmptyState
|
||||
|
||||
type vaultSummary struct {
|
||||
Title string
|
||||
Detail string
|
||||
Context string
|
||||
}
|
||||
type vaultSummary = detailmodel.VaultSummary
|
||||
|
||||
type sessionStatus interface {
|
||||
HasVault() bool
|
||||
@@ -130,10 +127,7 @@ type sessionStatus interface {
|
||||
IsRemote() bool
|
||||
}
|
||||
|
||||
type attachmentItem struct {
|
||||
Name string
|
||||
Size int
|
||||
}
|
||||
type attachmentItem = detailmodel.AttachmentItem
|
||||
|
||||
type statePaths struct {
|
||||
DefaultSaveAsPath string
|
||||
@@ -195,32 +189,27 @@ const (
|
||||
autofillFirstFillApprovalBlock autofillFirstFillApprovalMode = "block"
|
||||
)
|
||||
|
||||
type entriesSectionState struct {
|
||||
Path []string
|
||||
SearchQuery string
|
||||
SelectedEntryID string
|
||||
Editing bool
|
||||
}
|
||||
type entriesSectionState = listmodel.EntriesSectionState
|
||||
|
||||
type syncSourceMode string
|
||||
type syncSourceMode = syncmodel.SourceMode
|
||||
|
||||
const (
|
||||
syncSourceLocal syncSourceMode = "local"
|
||||
syncSourceRemote syncSourceMode = "remote"
|
||||
syncSourceLocal = syncmodel.SourceLocal
|
||||
syncSourceRemote = syncmodel.SourceRemote
|
||||
)
|
||||
|
||||
type syncDirection string
|
||||
type syncDirection = syncmodel.Direction
|
||||
|
||||
const (
|
||||
syncDirectionPull syncDirection = "pull"
|
||||
syncDirectionPush syncDirection = "push"
|
||||
syncDirectionPull = syncmodel.DirectionPull
|
||||
syncDirectionPush = syncmodel.DirectionPush
|
||||
)
|
||||
|
||||
type syncDialogPurpose string
|
||||
type syncDialogPurpose = syncmodel.DialogPurpose
|
||||
|
||||
const (
|
||||
syncDialogPurposeAdvanced syncDialogPurpose = "advanced"
|
||||
syncDialogPurposeRemoteSetup syncDialogPurpose = "remote-setup"
|
||||
syncDialogPurposeAdvanced = syncmodel.DialogPurposeAdvanced
|
||||
syncDialogPurposeRemoteSetup = syncmodel.DialogPurposeRemoteSetup
|
||||
)
|
||||
|
||||
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 {
|
||||
if purpose == syncDialogPurposeRemoteSetup {
|
||||
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 + "."
|
||||
return syncmodel.SummaryText(purpose, source, direction)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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 (
|
||||
"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
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"git.julianfamily.org/keepassgo/internal/appui/actions"
|
||||
headerlayout "git.julianfamily.org/keepassgo/internal/appui/layout/header"
|
||||
"git.julianfamily.org/keepassgo/internal/appstate"
|
||||
syncmodel "git.julianfamily.org/keepassgo/internal/appui/sync"
|
||||
"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 {
|
||||
label := "Sync"
|
||||
spacing := unit.Dp(4)
|
||||
if u.usesCompactViewport() {
|
||||
spacing = unit.Dp(3)
|
||||
}
|
||||
row := func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncPrimaryButton(gtx, u.theme, &u.synchronizeVault, label, u.usesCompactViewport())
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: spacing}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.syncMenuToggle(gtx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
return row(gtx)
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncPrimaryButton(gtx, u.theme, &u.synchronizeVault, "Sync", u.usesCompactViewport())
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: spacing}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.syncMenuToggle(gtx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
actionWidth := menuActionWidth(gtx, actionRows)
|
||||
return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
rows := u.syncMenuRows(model, profiles, credentials, actionWidth)
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...)
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, u.syncMenuRows(model, profiles, credentials, actionWidth)...)
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) syncMenuActionRows(model actions.SyncMenuModel) []layout.Widget {
|
||||
func (u *ui) syncMenuActionRows(model syncmodel.MenuModel) []layout.Widget {
|
||||
rows := []layout.Widget{
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync")
|
||||
@@ -244,7 +99,7 @@ func (u *ui) syncMenuActionRows(model actions.SyncMenuModel) []layout.Widget {
|
||||
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 = append(rows, u.syncMenuSavedBindingRows(model, profiles, credentials)...)
|
||||
if model.ShowSaveCurrentBinding {
|
||||
@@ -253,7 +108,7 @@ func (u *ui) syncMenuRows(model actions.SyncMenuModel, profiles []vault.RemotePr
|
||||
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{
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
@@ -333,7 +188,7 @@ func (u *ui) syncMenuSavedBindingRows(model actions.SyncMenuModel, profiles []va
|
||||
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 {
|
||||
summary := model.SavedBindingSummary
|
||||
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{
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
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)
|
||||
for i, profile := range profiles {
|
||||
i := i
|
||||
@@ -422,125 +277,44 @@ func (u *ui) syncMenuSelectorRows(_ actions.SyncMenuModel, profiles []vault.Remo
|
||||
return rows
|
||||
}
|
||||
|
||||
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
|
||||
func (u *ui) buildSyncMenuModel() syncmodel.MenuModel {
|
||||
model := syncmodel.MenuModel{
|
||||
HasOpenVault: u.hasOpenVault(),
|
||||
ShowSelectors: u.shouldShowSavedRemoteBindingSelectors(),
|
||||
ShowShare: supportsVaultShare(runtime.GOOS) && u.vaultSharer != nil && strings.TrimSpace(u.currentShareableVaultPath()) != "",
|
||||
RemoteBaseURL: strings.TrimSpace(u.remoteBaseURL.Text()),
|
||||
RemotePath: strings.TrimSpace(u.remotePath.Text()),
|
||||
RemoteUsername: strings.TrimSpace(u.remoteUsername.Text()),
|
||||
RemotePassword: u.remotePassword.Text(),
|
||||
SelectedVaultSyncMode: normalizeUISyncMode(u.selectedVaultRemoteSyncMode),
|
||||
}
|
||||
if width > 0 {
|
||||
gtx.Constraints.Min.X = width
|
||||
gtx.Constraints.Max.X = width
|
||||
_, model.HasSelectedBinding = u.selectedVaultRemoteBinding()
|
||||
model.SavedBindingSummary = u.computeSavedRemoteBindingSummary()
|
||||
model.ShowSaveCurrentBinding = model.HasOpenVault && model.RemoteBaseURL != "" && model.RemotePath != "" && model.RemoteUsername != "" && model.RemotePassword != ""
|
||||
return model
|
||||
}
|
||||
|
||||
func (u *ui) computeSavedRemoteBindingSummary() syncmodel.MenuBindingSummary {
|
||||
profile, ok := u.selectedVaultRemoteProfile()
|
||||
if !ok {
|
||||
return syncmodel.MenuBindingSummary{}
|
||||
}
|
||||
return compactCard(gtx, w)
|
||||
}
|
||||
|
||||
func (u *ui) topRightActionOrder() []string {
|
||||
if u.isVaultLocked() {
|
||||
return nil
|
||||
entry, ok := u.selectedVaultRemoteCredentialEntry()
|
||||
if !ok {
|
||||
return syncmodel.MenuBindingSummary{}
|
||||
}
|
||||
return []string{"Sync", "Lock", "Menu"}
|
||||
}
|
||||
|
||||
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)
|
||||
credentialLabel := entry.Title
|
||||
if strings.TrimSpace(entry.Username) != "" {
|
||||
credentialLabel += " · " + strings.TrimSpace(entry.Username)
|
||||
}
|
||||
return button(gtx)
|
||||
}
|
||||
|
||||
func (u *ui) phoneHeaderMenus(gtx layout.Context) layout.Dimensions {
|
||||
if !u.usesCompactViewport() {
|
||||
return layout.Dimensions{}
|
||||
syncLabel := "Sync manually when you choose Use Remote Sync."
|
||||
if normalizeUISyncMode(u.selectedVaultRemoteSyncMode) == appstate.SyncModeAutomaticOnOpenSave {
|
||||
syncLabel = "Syncs automatically on open and save."
|
||||
}
|
||||
if !u.syncMenuVisibleOnPhone() && !u.mainMenuVisibleOnPhone() {
|
||||
return layout.Dimensions{}
|
||||
return syncmodel.MenuBindingSummary{
|
||||
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"
|
||||
"strings"
|
||||
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"git.julianfamily.org/keepassgo/internal/appstate"
|
||||
editormodel "git.julianfamily.org/keepassgo/internal/appui/editor"
|
||||
"git.julianfamily.org/keepassgo/internal/clipboard"
|
||||
)
|
||||
|
||||
type focusID string
|
||||
|
||||
type detailField string
|
||||
type detailField = editormodel.Field
|
||||
|
||||
const (
|
||||
focusSearch focusID = "search"
|
||||
|
||||
detailFieldID detailField = "id"
|
||||
detailFieldTitle detailField = "title"
|
||||
detailFieldUsername detailField = "username"
|
||||
detailFieldPassword detailField = "password"
|
||||
detailFieldURL detailField = "url"
|
||||
detailFieldPath detailField = "path"
|
||||
detailFieldTags detailField = "tags"
|
||||
detailFieldPasswordProfile detailField = "password-profile"
|
||||
detailFieldNotes detailField = "notes"
|
||||
detailFieldFields detailField = "fields"
|
||||
detailFieldHistoryIndex detailField = "history-index"
|
||||
detailFieldID = editormodel.FieldID
|
||||
detailFieldTitle = editormodel.FieldTitle
|
||||
detailFieldUsername = editormodel.FieldUsername
|
||||
detailFieldPassword = editormodel.FieldPassword
|
||||
detailFieldURL = editormodel.FieldURL
|
||||
detailFieldPath = editormodel.FieldPath
|
||||
detailFieldTags = editormodel.FieldTags
|
||||
detailFieldPasswordProfile = editormodel.FieldPasswordProfile
|
||||
detailFieldNotes = editormodel.FieldNotes
|
||||
detailFieldFields = editormodel.FieldFields
|
||||
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 {
|
||||
@@ -41,6 +55,68 @@ func detailFocusID(field detailField) focusID {
|
||||
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 {
|
||||
if u.handleShortcutKey(name, modifiers) {
|
||||
return true
|
||||
@@ -336,19 +412,7 @@ func (u *ui) focusedDetailIndex() int {
|
||||
}
|
||||
|
||||
func detailFocusOrder() []detailField {
|
||||
return []detailField{
|
||||
detailFieldID,
|
||||
detailFieldTitle,
|
||||
detailFieldUsername,
|
||||
detailFieldPassword,
|
||||
detailFieldURL,
|
||||
detailFieldPath,
|
||||
detailFieldTags,
|
||||
detailFieldPasswordProfile,
|
||||
detailFieldNotes,
|
||||
detailFieldFields,
|
||||
detailFieldHistoryIndex,
|
||||
}
|
||||
return editormodel.FocusOrder()
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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 {
|
||||
busy := u.lifecycleBusy()
|
||||
showLocalChooser := u.showLocalVaultChooser()
|
||||
@@ -1,4 +1,4 @@
|
||||
package list
|
||||
package layout
|
||||
|
||||
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/apitokens"
|
||||
"git.julianfamily.org/keepassgo/internal/appstate"
|
||||
headerlayout "git.julianfamily.org/keepassgo/internal/appui/layout/header"
|
||||
listlayout "git.julianfamily.org/keepassgo/internal/appui/layout/list"
|
||||
headerlayout "git.julianfamily.org/keepassgo/internal/appui/header/layout"
|
||||
listlayout "git.julianfamily.org/keepassgo/internal/appui/list/layout"
|
||||
"git.julianfamily.org/keepassgo/internal/clipboard"
|
||||
"git.julianfamily.org/keepassgo/internal/passwords"
|
||||
"git.julianfamily.org/keepassgo/internal/session"
|
||||
|
||||
@@ -2,6 +2,8 @@ package appui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -9,29 +11,28 @@ import (
|
||||
"time"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
displayDensityDense = "dense"
|
||||
displayDensityComfortable = "comfortable"
|
||||
displayDensityDense = settingsmodel.DisplayDensityDense
|
||||
displayDensityComfortable = settingsmodel.DisplayDensityComfortable
|
||||
|
||||
contrastStandard = "standard"
|
||||
contrastHigh = "high"
|
||||
contrastStandard = settingsmodel.ContrastStandard
|
||||
contrastHigh = settingsmodel.ContrastHigh
|
||||
|
||||
keyboardFocusStandard = "standard"
|
||||
keyboardFocusProminent = "prominent"
|
||||
keyboardFocusStandard = settingsmodel.KeyboardFocusStandard
|
||||
keyboardFocusProminent = settingsmodel.KeyboardFocusProminent
|
||||
)
|
||||
|
||||
type accessibilityPreferences struct {
|
||||
DisplayDensity string
|
||||
Contrast string
|
||||
ReducedMotion bool
|
||||
KeyboardFocus string
|
||||
}
|
||||
type accessibilityPreferences = settingsmodel.AccessibilityPreferences
|
||||
|
||||
type settingsFile struct {
|
||||
Sync syncSettings `json:"sync,omitempty"`
|
||||
@@ -63,37 +64,23 @@ type choiceSpec struct {
|
||||
Active bool
|
||||
}
|
||||
|
||||
type focusAppearance struct {
|
||||
BorderColor color.NRGBA
|
||||
OutlineColor color.NRGBA
|
||||
OutlineWidth int
|
||||
MinHeight int
|
||||
}
|
||||
|
||||
func defaultAccessibilityPreferences() accessibilityPreferences {
|
||||
return accessibilityPreferences{
|
||||
DisplayDensity: displayDensityForDenseLayout(true),
|
||||
Contrast: contrastStandard,
|
||||
KeyboardFocus: keyboardFocusStandard,
|
||||
}
|
||||
return settingsmodel.DefaultAccessibilityPreferences()
|
||||
}
|
||||
|
||||
func displayDensityForDenseLayout(dense bool) string {
|
||||
if dense {
|
||||
return displayDensityDense
|
||||
}
|
||||
return displayDensityComfortable
|
||||
return settingsmodel.DisplayDensityForDenseLayout(dense)
|
||||
}
|
||||
|
||||
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
|
||||
return settingsmodel.NormalizeAccessibilityPreferences(prefs)
|
||||
}
|
||||
|
||||
func (u *ui) applyAccessibilityPreferences(prefs accessibilityPreferences) {
|
||||
@@ -102,6 +89,96 @@ func (u *ui) applyAccessibilityPreferences(prefs accessibilityPreferences) {
|
||||
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() {
|
||||
u.settingsDraft = settingsDraft{
|
||||
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