pragma ComponentBehavior: Bound import QtQuick import QtQuick.Window Item { id: root required property Theme theme required property Vault vaultService readonly property bool locked: !vaultService.unlocked readonly property bool setupMode: vaultService.setupEditing || !vaultService.profile.serverUrl || !vaultService.profile.account readonly property var detail: vaultService.detail || ({}) readonly property var fields: detail.fields || [] function focusPrimary() { if (setupMode) serverUrlSetup.input.forceActiveFocus(); else if (vaultService.status === "second-factor") secondFactorCode.input.forceActiveFocus(); else if (locked) masterPassword.input.forceActiveFocus(); else if (detail.requiresReprompt) repromptPassword.input.forceActiveFocus(); else searchField.input.forceActiveFocus(); } function clearPasswordInputs() { masterPassword.text = ""; repromptPassword.text = ""; showPassword.checked = false; showRepromptPassword.checked = false; } function fieldEnding(suffix) { for (let i = 0; i < fields.length; i++) if (String(fields[i].id || "").endsWith(suffix)) return fields[i]; return null; } function copyFieldEnding(suffix) { const field = fieldEnding(suffix); if (field && (field.kind === "text" || field.kind === "url")) vaultService.copy(field.id); } function fieldCopyable(suffix) { const field = fieldEnding(suffix); return !!field && (field.kind === "text" || field.kind === "url"); } function reconcileVisibleSelection() { const visible = vaultService.visibleItems; if (visible.length === 0) { vaultService.selectedId = ""; vaultService.detail = ({}); vaultService.resultScrollPosition = 0; vaultService.detailScrollPosition = 0; return; } if (!visible.some(item => item.id === vaultService.selectedId)) vaultService.select(visible[0].id); } onSetupModeChanged: if (visible) Qt.callLater(root.focusPrimary) onLockedChanged: if (visible) Qt.callLater(root.focusPrimary) Component.onCompleted: Qt.callLater(root.focusPrimary) onVisibleChanged: if (visible) Qt.callLater(root.focusPrimary) onDetailChanged: if (visible && detail.requiresReprompt) Qt.callLater(root.focusPrimary) Connections { target: root.vaultService function onStatusChanged() { if (root.visible && root.vaultService.status === "second-factor") Qt.callLater(root.focusPrimary); } function onSelectedIdChanged() { repromptPassword.text = ""; showRepromptPassword.checked = false; } function onLockGenerationChanged() { root.clearPasswordInputs(); } function onPanelOpenChanged() { if (!root.vaultService.panelOpen) root.clearPasswordInputs(); } } Connections { target: root.Window.window enabled: results.restoringScroll || detailScroll.restoringScroll function onFrameSwapped() { if (results.restoringScroll) results.restoreScroll(); if (detailScroll.restoringScroll) detailScroll.restoreScroll(); } } Shortcut { sequence: "Ctrl+U" context: Qt.WindowShortcut enabled: !root.locked && root.fieldCopyable(".username") onActivated: root.copyFieldEnding(".username") } Shortcut { sequence: "Ctrl+P" context: Qt.WindowShortcut enabled: !root.locked && root.fieldCopyable(".password") onActivated: root.copyFieldEnding(".password") } Shortcut { sequence: "Ctrl+T" context: Qt.WindowShortcut enabled: !root.locked && !!root.detail.totp onActivated: root.vaultService.copy("totp-current", root.detail.totp.currentSlot) } Shortcut { sequence: "Ctrl+Shift+T" context: Qt.WindowShortcut enabled: !root.locked && !!root.detail.totp onActivated: root.vaultService.copy("totp-next", root.detail.totp.nextSlot) } Item { id: browserPane anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left width: Math.round(parent.width * 0.37) PopupTextField { id: searchField objectName: "vaultSearchField" anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: 14 height: 42 theme: root.theme icon: "" placeholderText: "search vault" text: root.vaultService.query enabled: !root.locked onEdited: text => { root.vaultService.query = text; root.reconcileVisibleSelection(); } Keys.onUpPressed: event => { event.accepted = true; root.vaultService.moveSelection(-1); } Keys.onDownPressed: event => { event.accepted = true; root.vaultService.moveSelection(1); } Keys.onReturnPressed: event => { event.accepted = true; if (root.vaultService.selectedId.length > 0) root.vaultService.refreshDetail(); } } Flickable { id: results anchors.top: searchField.bottom anchors.topMargin: 8 anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: 14 anchors.bottom: statusRow.top anchors.bottomMargin: 8 contentHeight: resultColumn.implicitHeight clip: true boundsBehavior: Flickable.StopAtBounds property bool restoringScroll: true function restoreScroll() { contentY = Math.min(root.vaultService.resultScrollPosition, Math.max(0, contentHeight - height)); restoringScroll = false; } function revealItem(item) { const point = item.mapToItem(resultColumn, 0, 0); if (point.y < contentY) contentY = point.y; else if (point.y + item.height > contentY + height) contentY = Math.min(point.y + item.height - height, Math.max(0, contentHeight - height)); } onContentYChanged: if (!restoringScroll) root.vaultService.resultScrollPosition = contentY Column { id: resultColumn width: results.width spacing: 5 NuguText { visible: root.locked || root.vaultService.visibleItems.length === 0 width: parent.width height: 80 verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter theme: root.theme variant: "meta" text: root.locked ? "unlock to search vault" : (root.vaultService.status === "loading" ? "loading vault" : "no matching items") } Repeater { model: root.locked ? [] : root.vaultService.groupedItems delegate: Column { id: group required property var modelData width: resultColumn.width spacing: 4 NuguText { width: parent.width height: 24 verticalAlignment: Text.AlignBottom theme: root.theme variant: "meta" text: group.modelData.label } Repeater { model: group.modelData.items delegate: PopupCard { id: itemRow required property var modelData theme: root.theme padding: 0 active: modelData.id === root.vaultService.selectedId interactive: true width: group.width height: 56 activeFocusOnTab: true Accessible.role: Accessible.ListItem Accessible.name: modelData.title + ", " + modelData.itemType onActiveChanged: if (active) Qt.callLater(results.revealItem, itemRow) Component.onCompleted: if (active) Qt.callLater(results.revealItem, itemRow) Keys.onReturnPressed: event => { event.accepted = true; root.vaultService.select(modelData.id); } onClicked: root.vaultService.select(modelData.id) Item { id: iconSlot anchors.left: parent.left anchors.leftMargin: 8 anchors.verticalCenter: parent.verticalCenter width: 36 height: 36 Image { id: favicon anchors.fill: parent visible: source.toString().length > 0 && status !== Image.Error source: itemRow.modelData.favicon ? "file://" + itemRow.modelData.favicon : "" sourceSize.width: 36 sourceSize.height: 36 fillMode: Image.PreserveAspectFit asynchronous: true } Rectangle { anchors.fill: parent visible: !favicon.visible radius: 9 color: root.theme.withAlpha(root.theme.ui.accent, 0.1) Text { anchors.centerIn: parent color: root.theme.ui.accent font.family: "Symbols Nerd Font Mono" font.pixelSize: 16 text: itemRow.modelData.itemType === "login" ? "" : (itemRow.modelData.itemType === "card" ? "" : (itemRow.modelData.itemType === "identity" ? "" : (itemRow.modelData.itemType === "ssh key" ? "" : ""))) } } } Column { anchors.left: iconSlot.right anchors.leftMargin: 10 anchors.right: favoriteMark.left anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter spacing: 2 NuguText { theme: root.theme width: parent.width text: itemRow.modelData.title elide: Text.ElideRight } NuguText { theme: root.theme variant: "meta" width: parent.width text: itemRow.modelData.itemType + (itemRow.modelData.subtitle ? " · " + itemRow.modelData.subtitle : "") elide: Text.ElideRight } } Text { id: favoriteMark anchors.right: parent.right anchors.rightMargin: 11 anchors.verticalCenter: parent.verticalCenter width: 18 visible: itemRow.modelData.favorite color: root.theme.ui.accent font.family: "Symbols Nerd Font Mono" font.pixelSize: 14 text: "" } } } } } } } Row { id: statusRow anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: 14 anchors.bottom: parent.bottom height: 24 spacing: 6 Rectangle { anchors.verticalCenter: parent.verticalCenter width: 7 height: 7 radius: 4 color: root.vaultService.status === "unlocked" ? root.theme.info : (root.vaultService.status === "loading" ? root.theme.warning : root.theme.ui.minor) } NuguText { anchors.verticalCenter: parent.verticalCenter width: parent.width - 13 - (settingsButton.visible ? settingsButton.width + statusRow.spacing : 0) theme: root.theme variant: "meta" text: root.vaultService.status === "unlocked" ? "vault ready" : root.vaultService.status.replace(/-/g, " ") elide: Text.ElideRight } PopupButton { id: settingsButton theme: root.theme text: "settings" accessibleName: "vault settings" height: 24 visible: !root.setupMode enabled: !root.vaultService.setupSaving && !root.vaultService.lockPending onClicked: root.vaultService.beginSetup() } } } Rectangle { anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: browserPane.right width: 1 color: root.theme.withAlpha(root.theme.ui.minor, 0.35) } Item { id: detailPane anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: browserPane.right anchors.leftMargin: 18 anchors.right: parent.right Item { anchors.fill: parent visible: root.locked Column { visible: root.setupMode anchors.centerIn: parent width: Math.min(390, parent.width - 32) spacing: 12 NuguText { theme: root.theme variant: "title" font.pixelSize: 24 text: "configure vault" } NuguText { theme: root.theme text: "server url" } SetupField { id: serverUrlSetup objectName: "vaultSetupServerUrl" theme: root.theme placeholder: "vault.example.com" text: root.vaultService.setupUrlDraft enabled: !root.vaultService.setupSaving && !root.vaultService.connectionBusy onEdited: text => root.vaultService.setupUrlDraft = text onAccepted: accountSetup.input.forceActiveFocus() } NuguText { theme: root.theme text: "account" } SetupField { id: accountSetup objectName: "vaultSetupAccount" theme: root.theme placeholder: "email address" text: root.vaultService.setupAccountDraft enabled: !root.vaultService.setupSaving && !root.vaultService.connectionBusy onEdited: text => root.vaultService.setupAccountDraft = text onAccepted: root.vaultService.saveSetup() } Row { spacing: 8 PopupButton { theme: root.theme text: root.vaultService.connectionTesting ? "testing" : "test connection" enabled: !root.vaultService.setupSaving && !root.vaultService.lockPending && !root.vaultService.connectionBusy onClicked: root.vaultService.testConnection() } PopupButton { theme: root.theme text: root.vaultService.setupSaving ? "saving" : "save" role: "primary" enabled: !root.vaultService.setupSaving && !root.vaultService.lockPending && !root.vaultService.connectionBusy onClicked: root.vaultService.saveSetup() } PopupButton { theme: root.theme text: "cancel" visible: !!root.vaultService.profile.serverUrl && !!root.vaultService.profile.account enabled: !root.vaultService.setupSaving && !root.vaultService.lockPending onClicked: root.vaultService.cancelSetup() } } NuguText { width: parent.width theme: root.theme variant: "meta" text: root.vaultService.connectionResult visible: text.length > 0 wrapMode: Text.WordWrap } NuguText { width: parent.width theme: root.theme variant: "meta" color: root.theme.error text: root.vaultService.setupError || root.vaultService.message visible: text.length > 0 wrapMode: Text.WordWrap } } Column { visible: !root.setupMode anchors.centerIn: parent width: Math.min(390, parent.width - 32) spacing: 12 Rectangle { anchors.horizontalCenter: parent.horizontalCenter width: 66 height: 66 radius: 18 color: root.theme.withAlpha(root.theme.ui.accent, 0.1) border.width: 1 border.color: root.theme.withAlpha(root.theme.ui.accent, 0.45) Text { anchors.centerIn: parent color: root.theme.ui.accent font.family: "Symbols Nerd Font Mono" font.pixelSize: 28 text: "" } } NuguText { anchors.horizontalCenter: parent.horizontalCenter theme: root.theme variant: "title" font.pixelSize: 24 text: root.vaultService.status === "second-factor" ? "verify account" : (root.vaultService.status === "authentication-failure" ? "unlock failed" : "unlock vault") } NuguText { width: parent.width horizontalAlignment: Text.AlignHCenter theme: root.theme variant: "meta" text: String(root.vaultService.profile.serverUrl || "") + "\n" + String(root.vaultService.profile.account || "") } Row { visible: root.vaultService.status !== "second-factor" width: parent.width height: visible ? 48 : 0 spacing: 6 PopupTextField { id: masterPassword objectName: "vaultMasterPassword" theme: root.theme width: parent.width - 46 height: parent.height placeholderText: "master password" echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password inputMethodHints: Qt.ImhSensitiveData onAccepted: { const secret = text; text = ""; root.vaultService.unlock(secret); } } PopupButton { id: showPassword theme: root.theme width: 40 height: 40 anchors.verticalCenter: parent.verticalCenter icon: checked ? "" : "" text: "show master password" accessibleName: text tooltip: text showText: false onClicked: checked = !checked } } PopupButton { visible: root.vaultService.status !== "second-factor" anchors.horizontalCenter: parent.horizontalCenter height: visible ? implicitHeight : 0 theme: root.theme role: "primary" text: root.vaultService.status === "loading" ? "unlocking" : "unlock" enabled: root.vaultService.status !== "loading" onClicked: { const secret = masterPassword.text; masterPassword.text = ""; root.vaultService.unlock(secret); } } Column { visible: root.vaultService.status === "second-factor" width: parent.width height: visible ? implicitHeight : 0 spacing: 8 NuguText { width: parent.width horizontalAlignment: Text.AlignHCenter theme: root.theme variant: "meta" text: root.vaultService.secondFactorHint() } Row { anchors.horizontalCenter: parent.horizontalCenter spacing: 6 Repeater { model: root.vaultService.challenge.providers || [] delegate: PopupButton { required property var modelData theme: root.theme text: String(modelData.label || "verification") checked: Number(modelData.id) === root.vaultService.secondFactorProvider enabled: !!modelData.codeSupported && !root.vaultService.verificationPending onClicked: { root.vaultService.selectSecondFactor(modelData.id); secondFactorCode.input.forceActiveFocus(); } } } } PopupTextField { id: secondFactorCode width: parent.width height: 44 theme: root.theme placeholderText: "verification code" inputMethodHints: Qt.ImhSensitiveData text: root.vaultService.secondFactorDraft enabled: !root.vaultService.verificationPending onEdited: text => root.vaultService.secondFactorDraft = text onAccepted: root.vaultService.submitSecondFactor(text) } Row { anchors.horizontalCenter: parent.horizontalCenter spacing: 8 PopupButton { theme: root.theme text: root.vaultService.verificationPending ? "verifying" : "verify" role: "primary" enabled: root.vaultService.secondFactorProvider >= 0 && !root.vaultService.verificationPending onClicked: root.vaultService.submitSecondFactor(secondFactorCode.text) } PopupButton { theme: root.theme text: "cancel" onClicked: root.vaultService.cancelUnlock() } } } NuguText { visible: root.vaultService.message.length > 0 width: parent.width horizontalAlignment: Text.AlignHCenter theme: root.theme variant: "meta" color: root.vaultService.verificationPending || root.vaultService.message === "verification required" ? root.theme.ui.minor : root.theme.error text: root.vaultService.message wrapMode: Text.WordWrap } NuguText { width: parent.width horizontalAlignment: Text.AlignHCenter theme: root.theme variant: "meta" text: root.vaultService.cache.available ? (root.vaultService.cache.stale ? "encrypted cache stale" : "encrypted cache ready") : "no encrypted cache" } } } Item { anchors.fill: parent visible: !root.locked Item { id: detailHeader anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right height: 58 NuguText { id: detailTitle width: parent.width - headerActions.width - 12 theme: root.theme variant: "title" font.pixelSize: 22 text: String(root.detail.title || "select an item") elide: Text.ElideRight } NuguText { anchors.top: detailTitle.bottom anchors.topMargin: 2 width: parent.width - headerActions.width - 12 theme: root.theme variant: "meta" text: String(root.detail.itemType || "") + (root.detail.subtitle ? " · " + root.detail.subtitle : "") elide: Text.ElideRight } Row { id: headerActions anchors.right: parent.right anchors.top: parent.top spacing: 6 PopupButton { theme: root.theme width: 36 height: 36 icon: root.detail.favorite ? "" : "" text: root.detail.favorite ? "unfavorite" : "favorite" accessibleName: text tooltip: text showText: false checked: !!root.detail.favorite enabled: !!root.detail.id onClicked: root.vaultService.setFavorite(!root.detail.favorite) } PopupButton { theme: root.theme width: 36 height: 36 icon: "" text: "sync vault" accessibleName: text tooltip: text showText: false onClicked: root.vaultService.sync() } PopupButton { theme: root.theme width: 36 height: 36 icon: "" text: "lock vault" accessibleName: text tooltip: text showText: false onClicked: root.vaultService.lock() } } } Flickable { id: detailScroll anchors.top: detailHeader.bottom anchors.topMargin: 8 anchors.left: parent.left anchors.right: parent.right anchors.bottom: feedbackLabel.top anchors.bottomMargin: 5 contentHeight: detailColumn.implicitHeight clip: true boundsBehavior: Flickable.StopAtBounds property bool restoringScroll: true function restoreScroll() { contentY = Math.min(root.vaultService.detailScrollPosition, Math.max(0, contentHeight - height)); restoringScroll = false; } onContentYChanged: if (!restoringScroll) root.vaultService.detailScrollPosition = contentY Column { id: detailColumn width: detailScroll.width spacing: 8 PopupCard { visible: !!root.detail.requiresReprompt theme: root.theme active: true padding: 10 width: parent.width height: visible ? 108 : 0 Column { anchors.fill: parent spacing: 7 NuguText { width: parent.width theme: root.theme variant: "meta" text: root.vaultService.repromptError || "master password required for sensitive fields" color: root.vaultService.repromptError ? root.theme.error : root.theme.ui.minor elide: Text.ElideRight } Row { width: parent.width height: 44 spacing: 6 PopupTextField { id: repromptPassword theme: root.theme width: parent.width - 86 height: parent.height placeholderText: "master password" echoMode: showRepromptPassword.checked ? TextInput.Normal : TextInput.Password inputMethodHints: Qt.ImhSensitiveData onAccepted: { const secret = text; text = ""; root.vaultService.reprompt(secret); } } PopupButton { id: showRepromptPassword theme: root.theme width: 34 height: 34 anchors.verticalCenter: parent.verticalCenter icon: checked ? "" : "" text: "show master password" accessibleName: text tooltip: text showText: false onClicked: checked = !checked } PopupButton { theme: root.theme width: 34 height: 34 anchors.verticalCenter: parent.verticalCenter icon: "" text: "verify master password" accessibleName: text tooltip: text showText: false onClicked: { const secret = repromptPassword.text; repromptPassword.text = ""; root.vaultService.reprompt(secret); } } } } } PopupButton { visible: !!root.detail.totp theme: root.theme width: parent.width height: visible ? 34 : 0 text: "totp" icon: root.vaultService.sectionExpanded("totp") ? "" : "" accessibleName: (root.vaultService.sectionExpanded("totp") ? "collapse " : "expand ") + text tooltip: accessibleName onClicked: root.vaultService.toggleSection("totp") } PopupCard { visible: !!root.detail.totp && root.vaultService.sectionExpanded("totp") theme: root.theme active: true padding: 12 width: parent.width height: visible ? 106 : 0 Row { anchors.fill: parent spacing: 16 Column { width: parent.width * 0.62 spacing: 7 NuguText { theme: root.theme variant: "meta" text: "current code" } PopupButton { theme: root.theme height: 46 text: String(root.detail.totp ? root.detail.totp.current : "") accessibleName: "copy current code" tooltip: accessibleName onClicked: root.vaultService.copy("totp-current", root.detail.totp.currentSlot) } } Column { width: parent.width * 0.32 spacing: 7 NuguText { theme: root.theme variant: "meta" text: "next" } PopupButton { theme: root.theme width: parent.width height: 34 text: String(root.detail.totp ? root.detail.totp.next : "") accessibleName: "copy next code" tooltip: accessibleName onClicked: root.vaultService.copy("totp-next", root.detail.totp.nextSlot) } NuguText { theme: root.theme variant: "meta" text: String(root.detail.totp ? root.detail.totp.remaining : 0) + "s remaining" } } } } PopupButton { visible: root.fields.length > 0 theme: root.theme width: parent.width height: visible ? 34 : 0 text: "fields" icon: root.vaultService.sectionExpanded("fields") ? "" : "" accessibleName: (root.vaultService.sectionExpanded("fields") ? "collapse " : "expand ") + text tooltip: accessibleName onClicked: root.vaultService.toggleSection("fields") } Repeater { model: root.vaultService.sectionExpanded("fields") ? root.fields : [] delegate: PopupCard { id: fieldRow required property var modelData objectName: "vault-field-" + modelData.id theme: root.theme padding: 0 width: detailColumn.width height: 58 Column { anchors.left: parent.left anchors.leftMargin: 12 anchors.right: actions.left anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter spacing: 3 NuguText { width: parent.width theme: root.theme variant: "meta" text: String(fieldRow.modelData.label || "field") elide: Text.ElideRight } NuguText { width: parent.width theme: root.theme font.family: "IBM Plex Mono" text: fieldRow.modelData.kind === "restricted" ? (root.detail.requiresReprompt ? "re-prompt required" : "access denied") : fieldRow.modelData.sensitive && !root.vaultService.isRevealed(fieldRow.modelData.id) ? "••••••••••••" : String(fieldRow.modelData.value || "") elide: Text.ElideRight } } Row { id: actions anchors.right: parent.right anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter spacing: 5 PopupButton { visible: fieldRow.modelData.sensitive && fieldRow.modelData.kind !== "restricted" theme: root.theme width: 34 height: 34 icon: root.vaultService.isRevealed(fieldRow.modelData.id) ? "" : "" text: root.vaultService.isRevealed(fieldRow.modelData.id) ? "conceal " + fieldRow.modelData.label : "reveal " + fieldRow.modelData.label accessibleName: text tooltip: text showText: false onClicked: root.vaultService.toggleReveal(fieldRow.modelData.id) } PopupButton { visible: fieldRow.modelData.kind === "url" theme: root.theme width: 34 height: 34 icon: "" text: "open " + fieldRow.modelData.label accessibleName: text tooltip: text showText: false onClicked: root.vaultService.open(fieldRow.modelData.id) } PopupButton { visible: fieldRow.modelData.kind === "text" || fieldRow.modelData.kind === "url" theme: root.theme width: 34 height: 34 icon: "" text: "copy " + fieldRow.modelData.label accessibleName: text tooltip: text showText: false onClicked: root.vaultService.copy(fieldRow.modelData.id) } NuguText { visible: fieldRow.modelData.kind === "unsupported" || fieldRow.modelData.kind === "restricted" anchors.verticalCenter: parent.verticalCenter theme: root.theme variant: "meta" text: fieldRow.modelData.kind === "restricted" ? "restricted" : "unsupported" } } } } } } NuguText { id: feedbackLabel anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: 20 horizontalAlignment: Text.AlignRight theme: root.theme variant: "meta" color: root.theme.info text: root.vaultService.feedback } } } }