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