From e4ad766315879e1ff05bb111229f073f8f0ed68e Mon Sep 17 00:00:00 2001 From: Andreas Grois Date: Mon, 10 Oct 2022 21:30:02 +0200 Subject: PassFish: Initial Commit Well, that's a lie. But nobody needs to see all the iterations I decided to sweep under the rug. That said, I think the repo is, while not clean, clean enough now, to not be embarrassed by uploading it to github. --- qml/pages/AboutPage.qml | 91 ++++++++++++ qml/pages/MainPage.qml | 241 ++++++++++++++++++++++++++++++ qml/pages/ProfileEditor.qml | 346 +++++++++++++++++++++++++++++++++++++++++++ qml/pages/ProfilesPage.qml | 129 ++++++++++++++++ qml/pages/SettingsEditor.qml | 90 +++++++++++ 5 files changed, 897 insertions(+) create mode 100644 qml/pages/AboutPage.qml create mode 100644 qml/pages/MainPage.qml create mode 100644 qml/pages/ProfileEditor.qml create mode 100644 qml/pages/ProfilesPage.qml create mode 100644 qml/pages/SettingsEditor.qml (limited to 'qml/pages') diff --git a/qml/pages/AboutPage.qml b/qml/pages/AboutPage.qml new file mode 100644 index 0000000..7326fe4 --- /dev/null +++ b/qml/pages/AboutPage.qml @@ -0,0 +1,91 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +Page { + id: aboutPage + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + contentWidth: parent.width + Column { + id: column + width: aboutPage.width + spacing: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + PageHeader { + title: qsTr("About PassFish") + } + Label { + width: parent.width - 2*Theme.horizontalPageMargin + x: Theme.horizontalPageMargin + wrapMode: Text.WordWrap + textFormat: Text.RichText + onLinkActivated : Qt.openUrlExternally(link) + color: Theme.highlightColor + text: qsTr(" +

+ PassFish is a native re-implementation of PasswordMaker for Sailfish OS. + It aims to be mostly compatible to the Javascript Edition. +

+

+ All credit for the development of the PasswordMaker Pro algorithm goes to the original authors of PasswordMaker Pro, Miquel Burns and Eric H. Jung. +

+

+ As PassFish is not merely a port, but a full reimplementation from scratch, compatibility is not guaranteed. While the original source code was + used as a guideline during implementation, the underlying technology is vastly different. Common use cases are tested by integration tests, but some + edge cases might have been missed. In case you encounter such an issue, please report it on the + github issue tracker of the project. +

+

+ Speaking of integration tests: The hash algorithms were not re-implemented for this project in order to reduce the risk of introducing bugs. Instead the + QCryptographicHash API is used where possible, and where not, the implementation from the RustCrypto Hashes repository is utilized (see list of dependencies below). +

+

+ This program consists of two parts: The application itself (\"PassFish\"), and a Rust crate that contains the + implementation of the business logic (\"passwordmaker‑rs\"). + This is important, as the two parts use different licenses. PassFish is published under the GPLv3 license, while passwordmaker‑rs is published under LGPLv3. Please check the linked github pages for more details. +

+

+ This program utilises several third party libraries. This list is kept up-to-date to the best of my knowledge. Only direct dependencies are listed, + for transitive dependencies please see the linked websites. Similarly, the source code for those third-party dependencies that are published under an + open source license can be found on the linked websites. To my knowledge the only non-open-source dependency is Silica. The listed licenses are just those + used by this project, most libraries are available under multiple licenses. Please see the libraries' websites for details.
+ These libraries are linked dynamically: +

+ These libraries and their dependencies are linked statically: + + The passwordmaker‑rs library has the following statically linked runtime dependencies: + + While it is not a runtime dependency, the code generator for the Rust Qt bindings is worth noting: + + PassFish uses a modified version, which can be found in the mockall_support branch. +

+

+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. +

+ ") + } + } + + VerticalScrollDecorator { flickable: aboutPage} + } +} diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml new file mode 100644 index 0000000..b860e67 --- /dev/null +++ b/qml/pages/MainPage.qml @@ -0,0 +1,241 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import "../components" + +Page { + id: mainPage + + readonly property var isBackground : Qt.application.state + + onIsBackgroundChanged: { + if(isBackground === Qt.ApplicationInactive) { + mainFlickable.restart_timers(); + } + } + + SilicaFlickable { + id: mainFlickable + anchors.fill: parent + contentHeight: column.height + + function store_settings_with_error_message() { + var worked = passwordmaker.store_settings(); + if(!worked) { + storeFailureNotice.show(); + } + } + function restart_timers() { + if(autoClearMasterPasswordTimer.enabled && passwordmaker.master_password.length > 0) { + autoClearMasterPasswordTimer.restart(); + } + if(autoClearGeneratedPasswordTimer.enabled && (passwordmaker.used_text.length > 0 || passwordmaker.url.length > 0)) { + autoClearGeneratedPasswordTimer.restart(); + } + } + + PullDownMenu { + MenuItem { + text: qsTr("App Settings") + onClicked: { + var pg = pageStack.animatorPush(Qt.resolvedUrl("SettingsEditor.qml"), + { + clear_generated_password : passwordmaker.settings.clear_generated_password_seconds, + clear_master_password : passwordmaker.settings.clear_master_password_seconds, + hide_generated_password : passwordmaker.settings.hide_generated_password + }); + pg.pageCompleted.connect(function(pg) { + pg.accepted.connect(function() { + passwordmaker.settings.clear_generated_password_seconds + =pg.clear_generated_password; + passwordmaker.settings.clear_master_password_seconds + =pg.clear_master_password; + passwordmaker.settings.hide_generated_password + =pg.hide_generated_password; + + mainFlickable.store_settings_with_error_message() + }) + }) + } + } + MenuItem { + text: qsTr("About") + onClicked: pageStack.animatorPush(Qt.resolvedUrl("AboutPage.qml")) + } + } + VerticalScrollDecorator {} + Column { + id: column + + width: mainPage.width + spacing: Theme.paddingLarge + PageHeader { + title: qsTr("PassFish") + } + + + ValueButton { + id: profileButton + label: qsTr("Profile") + value: passwordmaker.profiles.current_profile_name + + onClicked: pageStack.animatorPush(Qt.resolvedUrl("ProfilesPage.qml")) + } + TextField { + id: url + width: parent.width + + text: passwordmaker.url + inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + + label: qsTr("URL") + placeholderText: qsTr("URL") + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: masterPassword.focus = true + Binding { + target: passwordmaker + property: "url" + value: url.text + } + } + PasswordField { + id: masterPassword + width: parent.width + + text: passwordmaker.master_password + + label: qsTr("Master Password") + placeholderText: qsTr("Master Password") + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false + Binding { + target: passwordmaker + property: "master_password" + value: masterPassword.text + } + } + TextField { + id: usedText + width: parent.width + + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + + label: qsTr("Used Text") + onFocusChanged: if(focus) text = passwordmaker.used_text + onPlaceholderTextChanged: if(!focus) text = "" + placeholderText: passwordmaker.used_text === "" ? qsTr("Used Text") : passwordmaker.used_text + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false + Binding { + target: passwordmaker + property: "used_text" + value: usedText.text + when: usedText.focus + } + } + Separator { + width: parent.width + horizontalAlignment:Qt.AlignHCenter + color: Theme.secondaryColor + } + Row{ + width: parent.width + PasswordField { + id: generatedPassword + width: parent.width - copyToClipboard.width + + + function password_text_from_generator_state(g, t, e) { + switch(g){ + case 0 : + return e === TextInput.Normal ? t : t.replace(/./g, passwordCharacter); + case 1 : + return qsTr("Generating"); + case 2 : + return qsTr("Missing text to use"); + case 3 : + return qsTr("Missing master password"); + case 4 : + return qsTr("Error in profile character list"); + default: + return ""; + } + } + + passwordEchoMode: passwordmaker.settings.hide_generated_password + ? TextInput.Password : TextInput.Normal + showEchoModeToggle: passwordmaker.settings.hide_generated_password + readOnly: true + focusOnClick: true + label: qsTr("Generated Password") + hideLabelOnEmptyField: false + placeholderText: + password_text_from_generator_state( + passwordmaker.generator_state + , passwordmaker.generated_password + , echoMode) + + onFocusChanged: if(focus) text = passwordmaker.generated_password; else text = ""; + placeholderColor: color + + BusyIndicator { + id: busy + parent: null + size: BusyIndicatorSize.Small + running: passwordmaker.generator_state === 1 + } + states: State { + when: passwordmaker.generator_state === 1 + PropertyChanges { + target: generatedPassword + rightItem: busy + } + } + } + IconButton{ + id: copyToClipboard + enabled: passwordmaker.generator_state === 0 && passwordmaker.generated_password.length > 0 + icon.source: "image://theme/icon-m-clipboard" + onClicked: Clipboard.text = passwordmaker.generated_password; + } + } + Timer { + id: autoClearMasterPasswordTimer + property bool enabled: typeof passwordmaker.settings.clear_master_password_seconds !== 'undefined' + interval: passwordmaker.settings.clear_master_password_seconds*1000 || 10000000 + running: false + onTriggered: if(enabled) + passwordmaker.master_password = ""; + } + Timer { + id: autoClearGeneratedPasswordTimer + property bool enabled: typeof passwordmaker.settings.clear_generated_password_seconds !== 'undefined' + interval: passwordmaker.settings.clear_generated_password_seconds*1000 || 10000000 + running: false + onTriggered: if(enabled){ + passwordmaker.used_text = ""; + passwordmaker.url = ""; + } + } + } + NoticeOptional { + id: storeFailureNotice + text: qsTr("Saving settings failed.") + useNotificationFallback: true + } + + Binding{ + target: autoClearMasterPasswordTimer + property: "running" + value: false + when: !autoClearMasterPasswordTimer.enabled || isBackground === Qt.ApplicationActive + } + Binding{ + target: autoClearGeneratedPasswordTimer + property: "running" + value: false + when: !autoClearGeneratedPasswordTimer.enabled || isBackground === Qt.ApplicationActive + } + } + readonly property int current_profile_index : passwordmaker.profiles.current_profile + onCurrent_profile_indexChanged: {mainFlickable.store_settings_with_error_message()} +} diff --git a/qml/pages/ProfileEditor.qml b/qml/pages/ProfileEditor.qml new file mode 100644 index 0000000..5dd28e0 --- /dev/null +++ b/qml/pages/ProfileEditor.qml @@ -0,0 +1,346 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import "../components" +import PWM 1.0 + +Dialog { + id: profileEditor + + property alias profileName: profileNameField.text + property alias useProtocol: protocolField.checked + property alias useSubdomain: subdomainField.checked + property alias useDomain: domainField.checked + property alias usePortPath: portPathField.checked + property alias useUserInfo: userInfoField.checked + property alias useDefaultFallbackForProtocol : useDefaultFallbackForProtocolField.checked + property alias passwordLength: passwordLengthSlider.value + property alias hashAlgorithm: hashAlgorithmComboBox.currentIndex + property alias useLeet : useLeetComboBox.currentIndex + property alias leetLevel : leetLevelSlider.value + property alias characters : charactersField.text + property alias username : usernameField.text + property alias modifier : modifierField.text + property alias prefix : prefixField.text + property alias suffix : suffixField.text + + canAccept: { + profileNameField.acceptableInput + && charactersField.acceptableInput + && urlPartsColumn.anySelected + } + + onAcceptBlocked: { + if(!urlPartsColumn.anySelected) { + urlNotice.show(); + } + else if(!profileNameField.acceptableInput) { + nameNotice.show(); + } + else if(!charactersField.acceptableInput) { + charactersNotice.show(); + } + } + + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + + VerticalScrollDecorator {} + + Column { + id: column + width: parent.width + bottomPadding: Theme.paddingLarge + DialogHeader { + title: qsTr("Edit Profile") + } + TextField { + id: profileNameField + width: parent.width + errorHighlight: !acceptableInput + + //description doesn't work on Sailfish 3. Use label instead if unavailable. + readonly property bool descriptionAvailable : typeof(description) !== "undefined" + label: !descriptionAvailable && errorHighlight ? qsTr("Required Field") : qsTr("Profile Name") + hideLabelOnEmptyField: descriptionAvailable + placeholderText: qsTr("Profile Name") + + Binding { + target: profileNameField + property: "description" + value: profileNameField.errorHighlight ? qsTr("Required Field") : "" + when: profileNameField.descriptionAvailable + } + + validator: RegExpValidator{regExp: /.+/} + //It doesn't make much sense to send the focus to the unrelated and rarely used fields waaaaay at the bottom and skip most relevant fields. Rather close the keyboard. + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false + } + NoticeOptional { + id: nameNotice + text: qsTr("Profile name required.") + useNotificationFallback: false + } + Column { + id: urlPartsColumn + width: parent.width + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + property bool anySelected : (useProtocol || useSubdomain || useDomain || usePortPath || useUserInfo) + + TextSwitch { + id: protocolField + text: qsTr("Use Protocol") + description: qsTr("Include URL protocol (e.g. \"http://\")") + palette.highlightColor : down || urlPartsColumn.anySelected ? Theme.highlightColor : Theme.errorColor + highlighted: down || !urlPartsColumn.anySelected + } + TextSwitch { + id: useDefaultFallbackForProtocolField + visible: protocolField.checked + text: qsTr("Use \"undefined\" if protocol is missing") + description: qsTr("Enable to mimic weird behaviour of PasswordMaker Pro.") + palette.highlightColor : down ? Theme.highlightColor : Theme.errorColor + highlighted: down + } + TextSwitch { + id: userInfoField + text: qsTr("Use Userinfo") + description: qsTr("Include userinfo (e.g \"jane_doe:12345\"") + palette.highlightColor : down || urlPartsColumn.anySelected ? Theme.highlightColor : Theme.errorColor + highlighted: down || !urlPartsColumn.anySelected + } + TextSwitch { + id: subdomainField + text: qsTr("Use Subomain(s)") + description: qsTr("Include URL subdomain(s) (e.g. \"www.\")") + palette.highlightColor : down || urlPartsColumn.anySelected ? Theme.highlightColor : Theme.errorColor + highlighted: down || !urlPartsColumn.anySelected + } + TextSwitch { + id: domainField + text: qsTr("Use Domain") + description: qsTr("Include URL domain (e.g. \"example.com\")") + palette.highlightColor : down || urlPartsColumn.anySelected ? Theme.highlightColor : Theme.errorColor + highlighted: down || !urlPartsColumn.anySelected + } + TextSwitch { + id: portPathField + text: qsTr("Use Port/Path") + description: qsTr("Include port and path (e.g \":8080/file\")") + palette.highlightColor : down || urlPartsColumn.anySelected ? Theme.highlightColor : Theme.errorColor + highlighted: down || !urlPartsColumn.anySelected + + } + NoticeOptional { + id: urlNotice + text: qsTr("At least one URL part required.") + useNotificationFallback: false + } + } + Column { + width: parent.width + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + Slider { + id: passwordLengthSlider + minimumValue: 1 + maximumValue: 50 + stepSize: 1 + width: parent.width + valueText : value + label: qsTr("Password Length") + } + ComboBox { + id: hashAlgorithmComboBox + label: qsTr("Hash Algorithm") + menu: ContextMenu { + MenuItem { text: "MD4" } + MenuItem { text: "HMAC-MD4" } + MenuItem { text: "MD5" } + MenuItem { text: qsTr("MD5 Version 0.6") } + MenuItem { text: "HMAC-MD5" } + MenuItem { text: qsTr("HMAC-MD5 Version 0.6") } + MenuItem { text: "SHA-1" } + MenuItem { text: "HMAC-SHA-1" } + MenuItem { text: "SHA-256" } + MenuItem { text: "HMAC-SHA-256" } + MenuItem { text: "RIPEMD-160" } + MenuItem { text: "HMAC-RIPEMD-160" } + } + } + ComboBox { + id: useLeetComboBox + label: qsTr("Use L33t") + menu: ContextMenu { + MenuItem { text: qsTr("not at all") } + MenuItem { text: qsTr("before generating") } + MenuItem { text: qsTr("after generating") } + MenuItem { text: qsTr("before and after generating") } + } + } + Slider { + id: leetLevelSlider + minimumValue: 1 + maximumValue: 9 + stepSize: 1 + width: parent.width + valueText: value + visible: useLeetComboBox.currentIndex > 0 + label: qsTr("Leet Level") + } + } + Column { + enabled: hashAlgorithmComboBox.currentIndex != 3 && hashAlgorithmComboBox.currentIndex != 5 + width: parent.width + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + ListModel { + id: defaultCharacterValues + ListElement { + name: qsTr("Default Characters") + chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`~!@#$%^&*()_-+={}|[]\\:\";'<>?,./" + userFacing: true + } + ListElement { + name: qsTr("Alphanumeric") + chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + userFacing: true + } + ListElement { + name: qsTr("Letters only") + chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + userFacing: true + } + ListElement { + name: qsTr("Numbers only") + chars: "0123456789" + userFacing: true + } + ListElement { + name: qsTr("Special only") + chars: "`~!@#$%^&*()_-+={}|[]\\:\";'<>?,./" + userFacing: true + } + ListElement { + name: qsTr("Hex") + chars: "0123456789abcdef" + userFacing: true + } + ListElement { + name: qsTr("Custom") + chars: "" + userFacing: false + } + } + ComboBox { + id: defaultCharactersMenu + + function matchIndex(text) { + for(var i=0; i < defaultCharacterValues.count - 1;++i) + if(defaultCharacterValues.get(i).chars === text) + return i; + return defaultCharacterValues.count - 1; + } + + Binding { + target: defaultCharactersMenu + property: "currentIndex" + value: defaultCharactersMenu.matchIndex(charactersField.text) + } + + label: qsTr("Characters Preset") + menu: ContextMenu { + Repeater { + model: defaultCharacterValues + MenuItem { + text: name + visible: userFacing + onClicked: if(userFacing) charactersField.text = chars + } + } + } + } + TextField { + id: charactersField + width: parent.width + errorHighlight: !acceptableInput + + //description doesn't work on Sailfish 3. Use label instead if unavailable. + readonly property bool descriptionAvailable : typeof(description) !== "undefined" + label: !descriptionAvailable && errorHighlight ? qsTr("Need at least 2 characters.") : qsTr("Characters") + hideLabelOnEmptyField: descriptionAvailable + placeholderText: qsTr("Characters") + validator: GraphemeCountValidator { minGraphemeCount: 2 } + + //The text fields below are conceptually different. Close the keyboard. + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false + + Binding { + target: charactersField + property: "description" + value: charactersField.errorHighlight ? qsTr("Need at least 2 characters.") : "" + when: charactersField.descriptionAvailable + } + states: State{ + name: "locked" + when: hashAlgorithmComboBox.currentIndex === 3 || hashAlgorithmComboBox.currentIndex === 5 + PropertyChanges { + target: charactersField + text: "0123456789abcdef" + + } + } + + } + NoticeOptional { + id: charactersNotice + text: qsTr("Need at least 2 characters.") + useNotificationFallback: false + } + } + Column { + width: parent.width + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + TextField { + id: usernameField + width: parent.width + label: qsTr("Username") + placeholderText: qsTr("Username") + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: modifierField.focus = true + } + TextField { + id: modifierField + width: parent.width + label: qsTr("Modifier") + placeholderText: qsTr("Modifier") + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: prefixField.focus = true + } + TextField { + id: prefixField + width: parent.width + label: qsTr("Prefix") + placeholderText: qsTr("Prefix") + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: suffixField.focus = true + } + TextField { + id: suffixField + width: parent.width + label: qsTr("Suffix") + placeholderText: qsTr("Suffix") + //There are many non-keyboard-input switches on this page. Do not let the user confirm using keyboard. + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false + } + } + } + } +} diff --git a/qml/pages/ProfilesPage.qml b/qml/pages/ProfilesPage.qml new file mode 100644 index 0000000..a2df8af --- /dev/null +++ b/qml/pages/ProfilesPage.qml @@ -0,0 +1,129 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import "../components" + +Page { + id: profilesSelector + SilicaListView { + id : profilesView + anchors.fill: parent + model : passwordmaker.profiles + + function store_profile_with_error_message() { + var worked = profilesView.model.store(); + if(!worked) { + storeFailureNotice.show(); + } + } + + PullDownMenu { + MenuItem { + text: qsTr("Add Profile") + onClicked: { + profilesView.model.insertRows(profilesView.model.rowCount(),1); + profilesView.store_profile_with_error_message(); + } + } + } + header: PageHeader { + title: qsTr("Select/Edit Profiles") + } + delegate: ListItem { + id: delegate + width: parent.width + ListView.onAdd: AddAnimation { + target: delegate + } + ListView.onRemove: RemoveAnimation { + target: delegate + } + Label { + x: Theme.horizontalPageMargin + text: name + anchors.verticalCenter: parent.verticalCenter + color: index === profilesView.model.current_profile ? Theme.highlightColor : Theme.primaryColor + } + menu: ContextMenu { + MenuItem { + text: qsTr("Edit") + onClicked: { + var pg = pageStack.animatorPush(Qt.resolvedUrl("ProfileEditor.qml"), + { + profileName : name, + useProtocol : use_protocol, + useSubdomain : use_subdomains, + useDomain : use_domain, + usePortPath : use_port_path, + passwordLength : password_length, + hashAlgorithm : hash_algorithm, + useLeet : use_leet, + leetLevel : leet_level > 0 ? leet_level : 1, + characters : characters, + username : username, + modifier : modifier, + prefix : prefix, + suffix : suffix, + useUserInfo : use_user_info, + useDefaultFallbackForProtocol : use_undefined_as_protocol_fallback + }); + pg.pageCompleted.connect(function(pg) { + pg.accepted.connect(function() { + name = pg.profileName; + use_protocol = pg.useProtocol; + use_subdomains = pg.useSubdomain; + use_domain = pg.useDomain; + use_port_path = pg.usePortPath; + password_length = pg.passwordLength; + hash_algorithm = pg.hashAlgorithm; + use_leet = pg.useLeet; + leet_level = pg.leetLevel; + characters = pg.characters; + username = pg.username; + modifier = pg.modifier; + prefix = pg.prefix; + suffix = pg.suffix; + use_user_info = pg.useUserInfo; + use_undefined_as_protocol_fallback = pg.useDefaultFallbackForProtocol; + + if(index === profilesView.model.current_profile) + passwordmaker.profile_changed(); + + profilesView.store_profile_with_error_message(); + }) + }) + } + } + MenuItem { + text: qsTr("Remove") + enabled: profilesView.count > 1 + onClicked: { + delegate.remorseDelete(function() { + var bWasCurrentProfile = index === profilesView.model.current_profile; + + profilesView.model.removeRows(index,1); + profilesView.store_profile_with_error_message(); + + if(bWasCurrentProfile) + passwordmaker.profile_changed(); + }) + } + } + } + onClicked: { + if(profilesView.model.current_profile !== index) + { + profilesView.model.current_profile = index; + passwordmaker.profile_changed(); + } + pageContainer.navigateBack(PageStackAction.Animated); + } + } + NoticeOptional { + id: storeFailureNotice + text: qsTr("Saving profiles failed.") + useNotificationFallback: true + } + + VerticalScrollDecorator {} + } +} diff --git a/qml/pages/SettingsEditor.qml b/qml/pages/SettingsEditor.qml new file mode 100644 index 0000000..9baa51c --- /dev/null +++ b/qml/pages/SettingsEditor.qml @@ -0,0 +1,90 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +Dialog { + id: settingsEditor + + property var clear_generated_password; + property var clear_master_password; + property alias hide_generated_password: hide_generated_passwordBox.checked; + + onAccepted: { + clear_generated_password = + clear_generated_passwordBox.checked + ? clear_generated_password_time.sliderValue * 60 + : null; + clear_master_password = + clear_master_passwordBox.checked + ? clear_master_password_time.sliderValue * 60 + : null; + } + + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + + VerticalScrollDecorator {} + + Column { + id: column + width: parent.width + bottomPadding: Theme.paddingLarge + DialogHeader { + title: qsTr("Edit Settings") + } + TextSwitch { + id: hide_generated_passwordBox + text: qsTr("Hide Generated Password") + palette.highlightColor : Theme.highlightColor + highlighted: down + } + TextSwitch { + id: clear_generated_passwordBox + text: qsTr("Auto-clear generated password") + palette.highlightColor : Theme.highlightColor + highlighted: down + checked: typeof clear_generated_password !== 'undefined' + } + Slider { + id: clear_generated_password_time + minimumValue: 1 + maximumValue: 15 + stepSize: 1 + width: parent.width + valueText: value + " min" + visible: clear_generated_passwordBox.checked + label: qsTr("Auto-clear generated pass timeout") + value: clear_generated_password/60 || 1 + } + TextSwitch { + id: clear_master_passwordBox + text: qsTr("Auto-clear master password") + palette.highlightColor : Theme.highlightColor + highlighted: down + checked: typeof clear_master_password !== 'undefined' + } + Slider { + id: clear_master_password_time + minimumValue: clear_generated_passwordBox.checked + ? clear_generated_password_time.sliderValue + : 1 + maximumValue: 30 + stepSize: 1 + width: parent.width + valueText: sliderValue + " min" + visible: clear_master_passwordBox.checked + label: qsTr("Auto-clear master pass timeout") + value: clear_master_password/60 || 5 + } + Label { + topPadding: Theme.paddingLarge + width: parent.width - 2*Theme.horizontalPageMargin + x: Theme.horizontalPageMargin + wrapMode: Text.WordWrap + color: Theme.highlightColor + text: qsTr("Profiles can be edited directly in the profiles selector.") + } + } + } +} -- cgit v1.2.3