1
0
Fork 0
mirror of https://github.com/seiichiro0185/sailotp.git synced 2024-12-22 11:42:30 +00:00
harbour-sailotp/qml/pages/MainView.qml
2022-11-14 21:23:12 +03:00

332 lines
12 KiB
QML

/*
* Copyright (c) 2014, Stefan Brand <seiichiro@seiichiro0185.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this
* list of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. The names of the contributors may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import QtQuick 2.0
import Sailfish.Silica 1.0
import "../lib/storage.js" as DB
import "../lib/crypto.js" as OTP
Page {
id: mainPage
allowedOrientations: Orientation.All
// This holds the time of the last update of the page as Unix Timestamp (in Milliseconds)
property double lastUpdated: 0
property double seconds_global: 0
property string searchTerm: ""
// Reload the List of OTPs from storage
function refreshOTPList() {
otpList.visible = false;
otpList.model = null; // Hack to prevent unaccessible pulley after List refresh
appWin.listModel.clear();
DB.getOTP();
refreshOTPValues();
otpList.model = appWin.listModel; // Hack to prevent unaccessible pulley after List refresh
otpList.visible = true;
}
// Calculate new OTPs for every entry
function refreshOTPValues() {
// get seconds from current Date
var curDate = new Date();
seconds_global = curDate.getSeconds() % 30
// Iterate over all List entries
for (var i=0; i<appWin.listModel.count; i++) {
if (appWin.listModel.get(i).type === "TOTP" || appWin.listModel.get(i).type === "TOTP_STEAM" ) {
// Take derivation into account if set
var seconds = (curDate.getSeconds() + appWin.listModel.get(i).diff) % appWin.listModel.get(i).period;
// Only update on full period Seconds or if last run of the Functions is more than 2s in the past (e.g. app was in background)
if (appWin.listModel.get(i).otp === "------" || seconds == 0 || (curDate.getTime() - lastUpdated > 2000)) {
var curOTP = OTP.calcOTP(appWin.listModel.get(i).secret, appWin.listModel.get(i).type, appWin.listModel.get(i).len, appWin.listModel.get(i).diff, 0, appWin.listModel.get(i).period);
appWin.listModel.setProperty(i, "otp", curOTP);
} else if (appWin.coverType === "HOTP" && (curDate.getTime() - lastUpdated > 2000) && appWin.listModel.get(i).fav === 1) {
// If we are coming back from the CoverPage update OTP value if current favourite is HOTP
appWin.listModel.setProperty(i, "otp", appWin.coverOTP);
}
}
}
// Set lastUpdate property
lastUpdated = curDate.getTime();
}
// Reload OTP List on Return to the Page (to e.g. accomodate changed settings)
onStatusChanged: {
if (status === PageStatus.Activating) {
refreshOTPList();
if (searchTerm != "") {
for (var i = 0; i < appWin.listModel.count; i++) {
appWin.listModel.get(i).itemVisible = appWin.listModel.get(i).title.toString().toLowerCase().indexOf(searchTerm.toLowerCase()) > -1
}
}
}
}
Timer {
interval: 500
// Timer only runs when app is acitive and we have entries
running: Qt.application.active && appWin.listModel.count
repeat: true
onTriggered: refreshOTPValues();
}
SilicaFlickable {
anchors.fill: parent
PullDownMenu {
MenuItem {
text: qsTr("About")
onClicked: pageStack.push(Qt.resolvedUrl("About.qml"))
}
MenuItem {
text: qsTr("Settings")
visible: true
onClicked: pageStack.push(Qt.resolvedUrl("Settings.qml"))
}
MenuItem {
text: qsTr("Export / Import")
onClicked: pageStack.push(Qt.resolvedUrl("ExportPage.qml"), {parentPage: mainPage, mode: "export"})
}
MenuItem {
text: qsTr("Add Token")
onClicked: pageStack.push(Qt.resolvedUrl("ScanOTP.qml"), {parentPage: mainPage})
}
}
SilicaListView {
id: otpList
anchors.fill: parent
model: appWin.listModel
width: parent.width
ViewPlaceholder {
enabled: otpList.count == 0
text: qsTr("Nothing here")
hintText: qsTr("Pull down to add a OTP")
}
header: Column {
width: parent.width
height: headerRow.height + searchRow.height
Row {
id: headerRow
height: Theme.itemSizeSmall
width: parent.width
ProgressBar {
id: updateProgress
anchors.topMargin: Theme.paddingLarge * 1.15
anchors.top: parent.top
anchors.bottom: parent.bottom
height: parent.height
width: parent.width * 0.65
maximumValue: 29
value: 29 - seconds_global
// Only show when there are enries
visible: appWin.listModel.count
}
PageHeader {
id: header
anchors.top: parent.top
height: Theme.itemSizeSmall
width: parent.width * 0.35
title: "SailOTP"
}
}
Row {
id: searchRow
width: parent.width
SearchField {
id: searchField
font.pixelSize: Theme.fontSizeMedium
width: parent.width
// This would be useful, but seems to break the button altogether. Perhaps it'll work later?
// canHide: true
EnterKey.enabled: false
inputMethodHints: Qt.ImhNoPredictiveText // Qt.ImhPreferUppercase | Qt.ImhNoAutoUppercase
placeholderText: qsTr("Search")
onTextChanged: {
searchTerm = searchField.text
for (var i = 0; i < appWin.listModel.count; i++) {
appWin.listModel.get(i).itemVisible = appWin.listModel.get(i).title.toString().toLowerCase().indexOf(searchField.text.toLowerCase()) > -1
}
}
}
}
}
delegate: ListItem {
id: otpListItem
menu: otpContextMenu
contentHeight: visible ? Theme.itemSizeMedium : 0
width: parent.width
visible: itemVisible
function remove() {
// Show 5s countdown, then delete from DB and List
remorseAction(qsTr("Deleting"), function() { DB.removeOTP(title, secret); appWin.listModel.remove(index) })
}
function moveEntry(direction, index) {
if (direction) {
appWin.listModel.move(index, index-1, 1);
} else {
appWin.listModel.move(index, index+1, 1);
}
for (var i=0; i<appWin.listModel.count; i++) {
DB.changeOTPSort(appWin.listModel.get(i).title, appWin.listModel.get(i).secret, i);
}
}
onClicked: {
if (settings.hideTokens) {
otpValue.visible = !otpValue.visible
} else if (settings.showQrDefaultAction) {
pageStack.push(Qt.resolvedUrl("QRPage.qml"), {paramQrsource: otp, paramLabel: title, paramQRId: index});
} else {
Clipboard.text = otp
notify.show(qsTr("Token for ") + title + qsTr(" copied to clipboard"), 3000);
}
}
ListView.onRemove: animateRemoval()
Rectangle {
id: listRow
width: parent.width
anchors.horizontalCenter: parent.horizontalCenter
IconButton {
icon.source: fav == 1 ? "image://theme/icon-m-favorite-selected" : "image://theme/icon-m-favorite"
anchors.left: parent.left
onClicked: {
if (fav == 0) {
DB.setFav(title, secret)
appWin.setCover(index)
if (type == "HOTP") appWin.coverOTP = otp
for (var i=0; i<appWin.listModel.count; i++) {
if (i != index) {
appWin.listModel.setProperty(i, "fav", 0);
} else {
appWin.listModel.setProperty(i, "fav", 1);
}
}
} else {
DB.resetFav(title, secret);
appWin.setCover(-1);
appWin.listModel.setProperty(index, "fav", 0);
}
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
Label {
id: otpLabel
text: model.title
color: Theme.secondaryColor
anchors.horizontalCenter: parent.horizontalCenter
}
Label {
id: otpValue
text: model.otp
color: Theme.highlightColor
font.pixelSize: Theme.fontSizeLarge
visible: !settings.hideTokens
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
// Show an update button on HOTP type tokens
IconButton {
icon.source: "image://theme/icon-m-refresh"
anchors.right: parent.right
visible: type == "HOTP" ? true : false
onClicked: {
appWin.listModel.setProperty(index, "counter", DB.getCounter(title, secret, true));
appWin.listModel.setProperty(index, "otp", OTP.calcOTP(secret, "HOTP", len, 0, counter));
if (fav == 1) appWin.coverOTP = otp;
}
}
Component {
id: otpContextMenu
ContextMenu {
MenuItem {
text: qsTr("Copy to Clipboard")
visible: settings.hideTokens || settings.showQrDefaultAction
onClicked: {
Clipboard.text = otp
notify.show(qsTr("Token for ") + title + qsTr(" copied to clipboard"), 3000);
}
}
MenuItem {
text: qsTr("Show Token as QR-Code")
visible: !settings.showQrDefaultAction
onClicked: pageStack.push(Qt.resolvedUrl("QRPage.qml"), {paramQrsource: otp, paramLabel: title, paramQRId: index});
}
MenuItem {
text: qsTr("Move up")
visible: index > 0 ? true : false;
onClicked: moveEntry(1, index);
}
MenuItem {
text: qsTr("Move down")
visible: index < appWin.listModel.count - 1 ? true : false;
onClicked: moveEntry(0, index);
}
MenuItem {
text: qsTr("Edit")
onClicked: {
pageStack.push(Qt.resolvedUrl("AddOTP.qml"), {parentPage: mainPage, paramLabel: title, paramKey: secret, paramType: type, paramLen: len, paramDiff: diff, paramCounter: DB.getCounter(title, secret, false), paramPeriod: period})
}
}
MenuItem {
text: qsTr("Delete")
onClicked: remove()
}
}
}
}
VerticalScrollDecorator{}
Component.onCompleted: {
// Load list of OTP-Entries
refreshOTPList();
}
}
}
}