aboutsummaryrefslogtreecommitdiff
path: root/qml
diff options
context:
space:
mode:
authorAndreas Grois <andi@grois.info>2022-10-10 21:30:02 +0200
committerAndreas Grois <andi@grois.info>2022-10-10 21:37:15 +0200
commite4ad766315879e1ff05bb111229f073f8f0ed68e (patch)
tree4b043ff47c78b2c00c80c94ebda622c32c8b6d3d /qml
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.
Diffstat (limited to 'qml')
-rw-r--r--qml/PassFish.qml10
-rw-r--r--qml/components/NoticeOptional.qml68
-rw-r--r--qml/cover/CoverPage.qml73
-rw-r--r--qml/helpers/NoticeLoadable.qml8
-rw-r--r--qml/helpers/NotificationLoadable.qml10
-rw-r--r--qml/pages/AboutPage.qml91
-rw-r--r--qml/pages/MainPage.qml241
-rw-r--r--qml/pages/ProfileEditor.qml346
-rw-r--r--qml/pages/ProfilesPage.qml129
-rw-r--r--qml/pages/SettingsEditor.qml90
10 files changed, 1066 insertions, 0 deletions
diff --git a/qml/PassFish.qml b/qml/PassFish.qml
new file mode 100644
index 0000000..70e095d
--- /dev/null
+++ b/qml/PassFish.qml
@@ -0,0 +1,10 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import "pages"
+
+ApplicationWindow {
+ initialPage: Component { MainPage { } }
+ cover: Qt.resolvedUrl("cover/CoverPage.qml")
+ allowedOrientations: defaultAllowedOrientations
+ _defaultPageOrientations: defaultAllowedOrientations
+}
diff --git a/qml/components/NoticeOptional.qml b/qml/components/NoticeOptional.qml
new file mode 100644
index 0000000..83e0198
--- /dev/null
+++ b/qml/components/NoticeOptional.qml
@@ -0,0 +1,68 @@
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+
+/// Helper to allow using Notice without breaking compilation on SailfishOS 3.4.
+/// This helper conditionally loads either a Notice or a Notification
+/// (or nothing if useNotificationFallback is disabled) based on the availability
+/// of the Notice type.
+Loader {
+ property string text
+ property string duration : "Notice.Short"
+ property bool useNotificationFallback : true
+ property bool transientNotificationFallback : duration === "Notice.Short"
+
+ readonly property bool noticesAvailable : typeof(Notices) !== "undefined"
+
+ function show() {
+ asynchronous = false;
+ if(item){
+ if(item.show) {
+ item.show();
+ return;
+ }
+ else if(item.publish){
+ item.publish();
+ return;
+ }
+ }
+ console.log("Notice could not be shown: " + text);
+ }
+
+ asynchronous: true
+ source: noticesAvailable
+ ? "../helpers/NoticeLoadable.qml"
+ : (useNotificationFallback ? "../helpers/NotificationLoadable.qml" : "")
+
+ Binding {
+ target: item
+ property: "text"
+ value: text
+ when: noticesAvailable && status == Loader.Ready
+ }
+ Binding {
+ target: item
+ property: "duration"
+ value: duration
+ when: noticesAvailable && status == Loader.Ready
+ }
+ //----------------------------------------------------------
+ //fallback bindings
+ Binding {
+ target: item
+ property: "summary"
+ value: text
+ when: !noticesAvailable && useNotificationFallback && status == Loader.Ready
+ }
+ Binding {
+ target: item
+ property: "previewSummary"
+ value: text
+ when: !noticesAvailable && useNotificationFallback && status == Loader.Ready
+ }
+ Binding {
+ target: item
+ property: "isTransient"
+ value: transientNotificationFallback
+ when: !noticesAvailable && useNotificationFallback && status == Loader.Ready
+ }
+}
diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml
new file mode 100644
index 0000000..5b5af6c
--- /dev/null
+++ b/qml/cover/CoverPage.qml
@@ -0,0 +1,73 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+
+CoverBackground {
+ Column{
+ x: Theme.paddingLarge
+ y: Theme.paddingLarge
+ width: parent.width - 2 * x
+ spacing: Theme.paddingLarge
+ Label {
+ id: label
+ text: qsTr("PassFish")
+ color: Theme.highlightColor
+ fontSizeMode: Text.Fit
+ font.pixelSize: Theme.fontSizeLarge
+ bottomPadding: Theme.paddingLarge
+ width: parent.width
+ elide: Text.ElideNone
+ }
+ Column{
+ width: parent.width
+ spacing: Theme.paddingSmall
+ Label {
+ id: urlLabel
+ color: Theme.highlightColor
+ text: qsTr("Used Text:")
+ font.pixelSize: Theme.fontSizeSmall
+ truncationMode: TruncationMode.Fade
+ width: parent.width
+ }
+ Label {
+ id: generated_for_url
+ text: passwordmaker.used_text
+ font.pixelSize: Theme.fontSizeSmall
+ truncationMode: TruncationMode.Fade
+ width: parent.width
+ }
+ }
+ Column{
+ width: parent.width
+ spacing: Theme.paddingSmall
+ Label {
+ id: readyLabel
+ color: Theme.highlightColor
+ text: passwordmaker.generated_password.length > 0
+ && passwordmaker.generator_state === 0
+ ? qsTr("Pass Ready")
+ : qsTr("Input Needed")
+ font.pixelSize: Theme.fontSizeSmall
+ truncationMode: TruncationMode.Fade
+ width: parent.width
+ }
+ }
+ }
+
+ CoverActionList {
+ id: coverAction
+ enabled: passwordmaker.generated_password.length > 0
+ && passwordmaker.generator_state === 0
+ CoverAction {
+ iconSource: "image://theme/icon-s-clipboard"
+ onTriggered: Clipboard.text = passwordmaker.generated_password;
+ }
+
+ CoverAction {
+ iconSource: "image://theme/icon-cover-cancel"
+ onTriggered: {
+ passwordmaker.master_password = "";
+ passwordmaker.url = "";
+ }
+ }
+ }
+}
diff --git a/qml/helpers/NoticeLoadable.qml b/qml/helpers/NoticeLoadable.qml
new file mode 100644
index 0000000..2b1226b
--- /dev/null
+++ b/qml/helpers/NoticeLoadable.qml
@@ -0,0 +1,8 @@
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+
+//Just a wrapper around Notice, because I need to use it in a ternary in loader and don't want to pass in a string representation.
+//See components/NoticeOptional.qml for details. If you know a simple and not too ugly solution to make it work without this file:
+//I'm all ears.
+
+Notice {}
diff --git a/qml/helpers/NotificationLoadable.qml b/qml/helpers/NotificationLoadable.qml
new file mode 100644
index 0000000..1e44148
--- /dev/null
+++ b/qml/helpers/NotificationLoadable.qml
@@ -0,0 +1,10 @@
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+import Nemo.Notifications 1.0
+
+//Just a wrapper around Notification, because I need to use it in a ternary in loader and don't want to pass in a string representation.
+//See components/NoticeOptional.qml for details. If you know a simple and not too ugly solution to make it work without this file:
+//I'm all ears.
+
+Notification {
+}
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("
+<p>
+ PassFish is a native re-implementation of <a href=\"https://passwordmaker.org/\">PasswordMaker</a> for Sailfish OS.
+ It aims to be mostly compatible to the <a href=\"https://sourceforge.net/projects/passwordmaker/files/Javascript%20Edition/\">Javascript Edition</a>.
+</p>
+<p>
+ All credit for the development of the PasswordMaker Pro algorithm goes to the original authors of PasswordMaker Pro, Miquel Burns and Eric H. Jung.
+</p>
+<p>
+ 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
+ <a href=\"https://github.com/soulsource/passfish/issues\">github issue tracker</a> of the project.
+</p>
+<p>
+ 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).
+</p>
+<p>
+ This program consists of two parts: The application itself (\"<a href=\"https://github.com/soulsource/passfish\">PassFish</a>\"), and a Rust crate that contains the
+ implementation of the business logic (\"<a href=\"https://github.com/soulsource/passwordmaker-rs\"><nobr>passwordmaker&#8209;rs</nobr></a>\").
+ This is important, as the two parts use different licenses. PassFish is published under the GPLv3 license, while <nobr>passwordmaker&#8209;rs</nobr> is published under LGPLv3. Please check the linked github pages for more details.
+</p>
+<p>
+ 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 <i>this</i> project, most libraries are available under multiple licenses. Please see the libraries' websites for details.<br>
+ These libraries are linked dynamically:
+ <ul>
+ <li><a href=\"https://www.qt.io/\">Qt Quick</a>: Used under <a href=\"https://www.gnu.org/licenses/lgpl-3.0.en.html\">LGPLv3</a></li>
+ <li><a href=\"https://sailfishos.org/develop/docs/silica/\">Sailfish Silica</a>: Proprietary, <a href=\"https://www.gnu.org/licenses/gpl-3.0\">GPL</a> system library exception</li>
+ <li><a href=\"https://github.com/sailfishos/libsailfishapp\">LibSailfishApp</a>: Used under LGPL 2.1</li>
+ </ul>
+ These libraries and their dependencies are linked statically:
+ <ul>
+ <li><a href=\"https://crates.io/crates/libc\">libc Rust bindings</a>: Used under MIT license</li>
+ <li><a href=\"https://crates.io/crates/serde\">serde</a>: Used under MIT license</li>
+ <li><a href=\"https://crates.io/crates/toml\">toml-rs</a>: Used under MIT license</li>
+ <li><a href=\"https://crates.io/crates/dirs\">dirs</a>: Used under MIT license</li>
+ <li><a href=\"https://crates.io/crates/ripemd\">RustCrypto: RIPEMD</a>: Used under MIT license</li>
+ </ul>
+ The <nobr>passwordmaker&#8209;rs</nobr> library has the following statically linked runtime dependencies:
+ <ul>
+ <li><a href=\"https://crates.io/crates/unicode-segmentation\">unicode-segmentation</a>: Used under MIT license</li>
+ </ul>
+ While it is not a runtime dependency, the code generator for the Rust Qt bindings is worth noting:
+ <ul>
+ <li><a href=\"https://invent.kde.org/sdk/rust-qt-binding-generator\">Rust Qt Binding Generator</a></li>
+ </ul>
+ PassFish uses a modified version, which can be found in the <a href=\"https://invent.kde.org/soulsource/rust-qt-binding-generator/-/tree/mockall_support\">mockall_support</a> branch.
+</p>
+<p>
+ 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 <a href=\"http://www.gnu.org/licenses/\">http://www.gnu.org/licenses/</a>.
+</p>
+ ")
+ }
+ }
+
+ 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.")
+ }
+ }
+ }
+}