yubioath-flutter/qml/SettingsView.qml
2019-10-03 09:06:50 +02:00

565 lines
23 KiB
QML

import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import QtGraphicalEffects 1.0
ScrollView {
readonly property int dynamicWidth: 864
readonly property int dynamicMargin: 32
id: settingsPanel
objectName: 'settingsView'
contentWidth: app.width
contentHeight: content.implicitHeight
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.width: 8
Keys.onEscapePressed: navigator.home()
function getDeviceLabel(device) {
if (!!device.serial) {
return ("%1 [#%2]").arg(device.name).arg(device.serial)
} else {
return ("%1").arg(device.name)
}
}
function getDeviceDescription() {
if (!!yubiKey.currentDevice) {
return yubiKey.currentDevice.usbInterfacesEnabled.join('+')
} else if (yubiKey.availableDevices.length > 0
&& !yubiKey.availableDevices.some(dev => dev.selectable)) {
return qsTr("No compatible device found")
} else {
return qsTr("No device found")
}
}
function clearPasswordFields() {
currentPasswordField.text = ""
newPasswordField.text = ""
confirmPasswordField.text = ""
}
function submitPassword() {
if (acceptableInput()) {
if (!!yubiKey.currentDevice && yubiKey.currentDevice.hasPassword) {
changePassword()
} else {
setPassword()
}
}
}
function acceptableInput() {
if (!!yubiKey.currentDevice && yubiKey.currentDeviceValidated) {
if (!!yubiKey.currentDevice && yubiKey.currentDevice.hasPassword
&& currentPasswordField.text.length == 0) {
return false
}
if (newPasswordField.text.length > 0
&& (newPasswordField.text === confirmPasswordField.text)) {
return true
}
}
return false
}
function changePassword() {
navigator.goToLoading()
yubiKey.validate(currentPasswordField.text, false, function (resp) {
if (resp.success) {
setPassword()
} else {
navigator.snackBarError(getErrorMessage(resp.error_id))
console.log("change password failed:", resp.error_id)
}
clearPasswordFields()
navigator.goToSettings()
})
}
function setPassword() {
navigator.goToLoading()
yubiKey.setPassword(newPasswordField.text, false, function (resp) {
if (resp.success) {
navigator.snackBar(qsTr("Password set"))
yubiKey.currentDevice.hasPassword = true
passwordManagementPanel.isExpanded = false
} else {
navigator.snackBarError(getErrorMessage(resp.error_id))
console.log("set password failed:", resp.error_id)
}
clearPasswordFields()
navigator.goToSettings()
})
}
function removePassword() {
navigator.goToLoading()
yubiKey.validate(currentPasswordField.text, false, function (resp) {
if (resp.success) {
yubiKey.removePassword(function (resp) {
if (resp.success) {
navigator.snackBar(qsTr("Password removed"))
yubiKey.currentDevice.hasPassword = false
passwordManagementPanel.isExpanded = false
} else {
navigator.snackBarError(getErrorMessage(resp.error_id))
console.log("remove password failed:", resp.error_id)
}
clearPasswordFields()
navigator.goToSettings()
})
} else {
navigator.snackBarError(getErrorMessage(resp.error_id))
console.log("remove password failed:", resp.error_id)
}
})
}
property string title: qsTr("Settings")
ListModel {
id: themes
ListElement {
text: qsTr("System Default")
value: Material.System
}
ListElement {
text: qsTr("Light Mode")
value: Material.Light
}
ListElement {
text: qsTr("Dark Mode")
value: Material.Dark
}
}
ListModel {
id: otpModeDigits
ListElement {
text: "Off"
value: 0
}
ListElement {
text: "6"
value: 6
}
ListElement {
text: "7"
value: 7
}
ListElement {
text: "8"
value: 8
}
}
spacing: 8
padding: 0
ColumnLayout {
id: content
anchors.fill: parent
Layout.fillHeight: true
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
spacing: 0
StyledExpansionContainer {
id: keyPane
sectionTitle: qsTr("Device")
StyledExpansionPanel {
id: currentDevicePanel
label: !!yubiKey.currentDevice ? getDeviceLabel(yubiKey.currentDevice) : qsTr("Insert your YubiKey")
description: getDeviceDescription()
keyImage: !!yubiKey.currentDevice ? yubiKey.getCurrentDeviceImage() : "../images/yubikeys-large-transparent"
isTopPanel: true
Layout.fillWidth: true
isEnabled: yubiKey.availableDevices.length > 1
ButtonGroup {
id: deviceButtonGroup
}
ColumnLayout {
Layout.fillWidth: true
Repeater {
model: yubiKey.availableDevices
RadioButton {
Layout.fillWidth: true
objectName: index
checked: !!yubiKey.currentDevice
&& modelData.serial === yubiKey.currentDevice.serial
text: getDeviceLabel(modelData)
enabled: modelData.selectable
ButtonGroup.group: deviceButtonGroup
}
}
StyledButton {
id: selectBtn
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
text: "Select"
enabled: {
if (!!yubiKey.availableDevices && !!deviceButtonGroup.checkedButton) {
var dev = yubiKey.availableDevices[deviceButtonGroup.checkedButton.objectName]
return dev !== yubiKey.currentDevice
} else {
return false
}
}
onClicked: {
yubiKey.refreshDevicesDefault()
var dev = yubiKey.availableDevices[deviceButtonGroup.checkedButton.objectName]
yubiKey.selectCurrentSerial(dev.serial,
function (resp) {
if (resp.success) {
entries.clear()
yubiKey.currentDevice = dev
currentDevicePanel.expandAction()
yubiKey.calculateAll()
} else {
console.log("select device failed", resp.error_id)
}
})
}
}
}
}
StyledExpansionPanel {
id: passwordManagementPanel
label: !!yubiKey.currentDevice && yubiKey.currentDevice.hasPassword ? qsTr("Change Password") : qsTr("Set Password")
description: qsTr("For additional security the YubiKey may be protected with a password.")
visible: !!yubiKey.currentDevice && !settings.otpMode
ColumnLayout {
StyledTextField {
id: currentPasswordField
visible: !!yubiKey.currentDevice && yubiKey.currentDevice.hasPassword
labelText: qsTr("Current Password")
echoMode: TextInput.Password
Keys.onEnterPressed: submitPassword()
Keys.onReturnPressed: submitPassword()
onSubmit: submitPassword()
}
StyledTextField {
id: newPasswordField
labelText: qsTr("New Password")
echoMode: TextInput.Password
Keys.onEnterPressed: submitPassword()
Keys.onReturnPressed: submitPassword()
onSubmit: submitPassword()
}
StyledTextField {
id: confirmPasswordField
labelText: qsTr("Confirm Password")
echoMode: TextInput.Password
Keys.onEnterPressed: submitPassword()
Keys.onReturnPressed: submitPassword()
onSubmit: submitPassword()
}
RowLayout {
Layout.alignment: Qt.AlignRight | Qt.AlignTop
StyledButton {
id: removePasswordBtn
visible: !!yubiKey.currentDevice && yubiKey.currentDevice.hasPassword
enabled: currentPasswordField.text.length > 0
text: "Remove"
flat: true
onClicked: navigator.confirm(
qsTr("Remove password?"),
qsTr("A password will not be required to access the credentials anymore."),
function () {
removePassword()
})
}
StyledButton {
id: applyPassword
text: !!yubiKey.currentDevice && yubiKey.currentDevice.hasPassword ? "Change" : "Set"
enabled: acceptableInput()
onClicked: submitPassword()
}
}
}
}
StyledExpansionPanel {
label: qsTr("Reset")
description: qsTr("Warning: Reset will delete all credentials and restore factory defaults.")
isEnabled: false
visible: !!yubiKey.currentDevice && !settings.otpMode
toolButtonIcon: "../images/reset.svg"
toolButtonToolTip: qsTr("Reset OATH Application")
toolButton.onClicked: navigator.confirm(
qsTr("Reset OATH application?"),
qsTr("This will delete all credentials and restore factory defaults."),
function () {
navigator.goToLoading()
yubiKey.reset(function (resp) {
if (resp.success) {
entries.clear()
navigator.snackBar(
qsTr("Reset completed"))
yubiKey.currentDeviceValidated = true
yubiKey.currentDevice.hasPassword = false
} else {
navigator.snackBarError(
navigator.getErrorMessage(
resp.error_id))
console.log("reset failed:",
resp.error_id)
}
navigator.goToSettings()
})
})
}
}
StyledExpansionContainer {
id: appPane
sectionTitle: qsTr("Application")
StyledExpansionPanel {
label: qsTr("Appearance")
description: qsTr("Change the appearance of the application.")
isTopPanel: true
ColumnLayout {
RowLayout {
Layout.fillWidth: true
StyledComboBox {
id: themeComboBox
label: qsTr("Theme")
comboBox.textRole: "text"
model: themes
onCurrentIndexChanged: {
settings.theme = themes.get(currentIndex).value
}
currentIndex: {
switch (settings.theme) {
case Material.System:
return 0
case Material.Light:
return 1
case Material.Dark:
return 2
default:
return 0
}
}
}
}
}
}
StyledExpansionPanel {
id: interfacePanel
label: qsTr("Interface")
description: qsTr("Configure how credentials are read from the YubiKey.")
property bool otpModeSelected: interfaceCombobox.currentIndex === 2
property bool customReaderSelected: interfaceCombobox.currentIndex === 1
property bool aboutToChange: (otpModeSelected !== settings.otpMode)
|| (slot1DigitsComboBox.currentIndex
!== getComboBoxIndex(
settings.slot1digits))
|| (slot2DigitsComboBox.currentIndex
!== getComboBoxIndex(
settings.slot2digits))
|| customReaderSelected !== settings.useCustomReader
|| readerFilter.text !== settings.customReaderName && readerFilter.text.length > 0
function isValidMode() {
return aboutToChange
&& ((otpModeSelected
&& (slot1DigitsComboBox.currentIndex !== 0
|| slot2DigitsComboBox.currentIndex !== 0))
|| !otpModeSelected)
}
function setInterface() {
settings.slot1digits = otpModeDigits.get(
slot1DigitsComboBox.currentIndex).value
settings.slot2digits = otpModeDigits.get(
slot2DigitsComboBox.currentIndex).value
settings.otpMode = otpModeSelected
settings.useCustomReader = customReaderSelected
settings.customReaderName = readerFilter.text
yubiKey.clearCurrentDeviceAndEntries()
yubiKey.refreshDevicesDefault()
navigator.goToSettings()
navigator.snackBar(qsTr("Interface changed"))
interfacePanel.isExpanded = false
}
function getComboBoxIndex(digits) {
switch (digits) {
case 0:
return 0
case 6:
return 1
case 7:
return 2
case 8:
return 3
default:
return 0
}
}
ColumnLayout {
RowLayout {
Layout.fillWidth: true
StyledComboBox {
id: interfaceCombobox
label: qsTr("Interface")
model: ["CCID (recommended)", "CCID - Custom reader", "OTP"]
currentIndex: getCurrentIndex()
function getCurrentIndex() {
if (settings.otpMode) {
return 2
}
if (settings.useCustomReader && !settings.otpMode) {
return 1
}
// default
return 0
}
}
}
}
RowLayout {
visible: interfacePanel.otpModeSelected
Label {
Layout.fillWidth: true
font.pixelSize: 12
color: formLabel
text: qsTr("Using OTP slots should be considered for special cases only.")
wrapMode: Text.WordWrap
Layout.rowSpan: 1
bottomPadding: 8
}
}
RowLayout {
visible: interfacePanel.otpModeSelected
StyledComboBox {
id: slot1DigitsComboBox
label: qsTr("Slot 1 Digits")
comboBox.textRole: "text"
model: otpModeDigits
currentIndex: interfacePanel.getComboBoxIndex(
settings.slot1digits)
}
Item {
width: 16
}
StyledComboBox {
id: slot2DigitsComboBox
label: qsTr("Slot 2 Digits")
comboBox.textRole: "text"
model: otpModeDigits
currentIndex: interfacePanel.getComboBoxIndex(
settings.slot2digits)
}
}
ColumnLayout {
visible: interfacePanel.customReaderSelected
RowLayout {
visible: yubiKey.availableReaders.length > 0
StyledComboBox {
id: connectedReaders
enabled: yubiKey.availableReaders.length > 0
visible: yubiKey.availableReaders.length > 0
label: qsTr("Connected readers")
model: yubiKey.availableReaders
}
StyledButton {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
text: qsTr("Use as filter")
flat: true
enabled: yubiKey.availableReaders.length > 0
visible: yubiKey.availableReaders.length > 0
onClicked: readerFilter.text = connectedReaders.currentText
}
}
StyledTextField {
id: readerFilter
enabled: interfacePanel.customReaderSelected
visible: interfacePanel.customReaderSelected
labelText: qsTr("Custom reader filter")
text: settings.customReaderName
}
}
StyledButton {
Layout.alignment: Qt.AlignRight | Qt.AlignTop
text: "Apply"
enabled: interfacePanel.isValidMode()
onClicked: interfacePanel.setInterface()
}
}
StyledExpansionPanel {
label: Qt.platform.os === "osx" ? "Menu Bar" : "System Tray"
description: qsTr("Configure where and how the application is visible.")
isBottomPanel: true
ColumnLayout {
CheckBox {
id: sysTrayCheckbox
checked: settings.closeToTray
text: Qt.platform.os === "osx" ? qsTr("Show in menu bar") : qsTr("Show in system tray")
padding: 0
indicator.width: 16
indicator.height: 16
onCheckStateChanged: {
if(!checked) {
hideOnLaunchCheckbox.checked = false
}
settings.closeToTray = checked
}
Material.foreground: formText
}
CheckBox {
id: hideOnLaunchCheckbox
enabled: sysTrayCheckbox.checked
checked: settings.hideOnLaunch
text: qsTr("Hide on launch")
padding: 0
indicator.width: 16
indicator.height: 16
onCheckStateChanged: settings.hideOnLaunch = checked
Material.foreground: formText
}
}
}
}
}
}