Cleanup and Comments for publishing

* Added Readme and License
* Cleaned up the code
* Added comments to the code
pull/5/head
seiichiro 9 years ago
parent 0c0f10f7ab
commit f98444d5a4

@ -0,0 +1,26 @@
Copyright (c) 2013, 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.

@ -0,0 +1,36 @@
# SailOTP
SailOTP is a Sailfish Implementation of the Google-Authenticator algorithm,
also known as Timebased One Time Pad (TOPT) as described in RFC 6238. A growing
number of sites uses this algorithm for two-factor-authentication, including
Github, Linode and several Google services.
At the moment the App is quite basic. One can add new OTP-entries using the
pulley-menu. The main view of the app will show a List off all entries and
their current One-Time-Tokens. The entries will be regenerated every 30 seconds, the remaining time for the current tokens is shown through a progress bar at the top of the app. An entry can be deleted by long-pressing on it.
## Known Limitations
At the moment the only way to insert new entries into the app is to insert the
title and secret key by hand. It's not possible to use the QR-Codes some sites
provide directly.
## Contact and Issues
If you find any bugs or want to suggest a feature, feel free to use Githubs
Issues feature.
## License
SailOTP is licensed under a 3-Clause BSD-License. See COPYING for details.
## Accnowledgements
SailOTP uses the SHA-1 and HMAC-Implementation from
<a href="https://github.com/Caligatio/jsSHA" target="_blank">https://github.com/Caligatio/jsSHA</a>
The implementation of the TOTP-algorithm was inspired by:
<a href="http://blog.tinisles.com/2011/10/google-authenticator-one-time-password-algorithm-in-javascript/" target="_blank">http://blog.tinisles.com/2011/10/google-authenticator-one-time-password-algorithm-in-javascript/</a>

@ -30,19 +30,22 @@
import QtQuick 2.0
import Sailfish.Silica 1.0
// Define the Layout of the Active Cover
CoverBackground {
Image {
id: logo
source: "../sailotp.png"
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 48
}
// Show the SailOTP Logo
Image {
id: logo
source: "../sailotp.png"
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 48
}
Label {
id: label
anchors.centerIn: parent
text: "SailOTP"
}
// Show the Application Name
Label {
id: label
anchors.centerIn: parent
text: "SailOTP"
}
}

@ -33,8 +33,8 @@ import "pages"
ApplicationWindow
{
initialPage: Component { MainView { } }
cover: Qt.resolvedUrl("cover/CoverPage.qml")
initialPage: Component { MainView { } }
cover: Qt.resolvedUrl("cover/CoverPage.qml")
}

@ -29,49 +29,62 @@
.import "./sha.js" as SHA
// Helper Functions
// *** Helper Functions *** //
// Decimal to HEX
function dec2hex(s) { return (s < 15.5 ? '0' : '') + Math.round(s).toString(16); }
// HEX to Decimal
function hex2dec(s) { return parseInt(s, 16); }
// Convert Base32-secret to HEX Value
function base32tohex(base32) {
var base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
var bits = "";
var hex = "";
for (var i = 0; i < base32.length; i++) {
var val = base32chars.indexOf(base32.charAt(i).toUpperCase());
bits += leftpad(val.toString(2), 5, '0');
}
var base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
var bits = "";
var hex = "";
for (var i = 0; i+4 <= bits.length; i+=4) {
var chunk = bits.substr(i, 4);
hex = hex + parseInt(chunk, 2).toString(16) ;
}
return hex;
for (var i = 0; i < base32.length; i++) {
var val = base32chars.indexOf(base32.charAt(i).toUpperCase());
bits += leftpad(val.toString(2), 5, '0');
}
for (var i = 0; i+4 <= bits.length; i+=4) {
var chunk = bits.substr(i, 4);
hex = hex + parseInt(chunk, 2).toString(16) ;
}
return hex;
}
// Pad Strings to given length
function leftpad(str, len, pad) {
if (len + 1 >= str.length) {
str = Array(len + 1 - str.length).join(pad) + str;
}
return str;
if (len + 1 >= str.length) {
str = Array(len + 1 - str.length).join(pad) + str;
}
return str;
}
// *** Main Function *** //
// Calculate an OTP-Value from the given secret
// Parameter is the secret key in Base32-notation
function calcOTP(secret) {
var key = base32tohex(secret);
var epoch = Math.round(new Date().getTime() / 1000.0);
var time = leftpad(dec2hex(Math.floor(epoch / 30)), 16, '0');
// Convert the key to HEX
var key = base32tohex(secret);
// Get current Time in UNIX Timestamp format (Seconds since 01.01.1970 00:00 UTC)
var epoch = Math.round(new Date().getTime() / 1000.0);
// Get last full 30 / 60 Seconds and convert to HEX
var time = leftpad(dec2hex(Math.floor(epoch / 30)), 16, '0');
// Calculate the SHA-1 HMAC Value from time and key
var hmacObj = new SHA.jsSHA(time, 'HEX');
var hmac = hmacObj.getHMAC(key, 'HEX', 'SHA-1', "HEX");
var hmacObj = new SHA.jsSHA(time, 'HEX');
var hmac = hmacObj.getHMAC(key, 'HEX', 'SHA-1', "HEX");
// Finally convert the HMAC-Value to the corresponding 6-digit token
var offset = hex2dec(hmac.substring(hmac.length - 1));
var offset = hex2dec(hmac.substring(hmac.length - 1));
var otp = (hex2dec(hmac.substr(offset * 2, 8)) & hex2dec('7fffffff')) + '';
otp = (otp).substr(otp.length - 6, 6);
var otp = (hex2dec(hmac.substr(offset * 2, 8)) & hex2dec('7fffffff')) + '';
otp = (otp).substr(otp.length - 6, 6);
return otp;
// return the calculated token
return otp;
}

@ -31,52 +31,52 @@
// Get DB Connection
function getDB() {
return LS.LocalStorage.openDatabaseSync("harbour-sailotp", "1.0", "SailOTP Config Storage", 1000000);
return LS.LocalStorage.openDatabaseSync("harbour-sailotp", "1.0", "SailOTP Config Storage", 1000000);
}
// Initialize Table if not exists
function initialize() {
var db = getDB();
var db = getDB();
db.transaction(
function(tx) {
tx.executeSql("CREATE TABLE IF NOT EXISTS OTPStorage(title TEXT, secret TEXT);");
}
)
db.transaction(
function(tx) {
tx.executeSql("CREATE TABLE IF NOT EXISTS OTPStorage(title TEXT, secret TEXT);");
}
)
}
// Get all OTPs into the list model
function getOTP() {
var db = getDB();
var db = getDB();
db.transaction(
function(tx) {
var res = tx.executeSql("select * from OTPStorage;");
for (var i=0; i < res.rows.length; i++) {
mainPage.appendOTP(res.rows.item(i).title, res.rows.item(i).secret);
}
}
)
db.transaction(
function(tx) {
var res = tx.executeSql("select * from OTPStorage;");
for (var i=0; i < res.rows.length; i++) {
mainPage.appendOTP(res.rows.item(i).title, res.rows.item(i).secret);
}
}
)
}
// Add a new OTP
function addOTP(title, secret) {
var db = getDB();
var db = getDB();
db.transaction(
function(tx) {
tx.executeSql("INSERT INTO OTPStorage VALUES(?, ?);", [title, secret]);
}
)
db.transaction(
function(tx) {
tx.executeSql("INSERT INTO OTPStorage VALUES(?, ?);", [title, secret]);
}
)
}
// Remove an existing OTP
function removeOTP(title, secret) {
var db = getDB();
var db = getDB();
db.transaction(
function(tx) {
tx.executeSql("DELETE FROM OTPStorage WHERE title=? and secret=?;", [title, secret]);
}
)
db.transaction(
function(tx) {
tx.executeSql("DELETE FROM OTPStorage WHERE title=? and secret=?;", [title, secret]);
}
)
}

@ -30,56 +30,54 @@
import QtQuick 2.0
import Sailfish.Silica 1.0
// Define the Layout of the About Page
Page {
id: aboutPage
Image {
id: logo
source: "../sailotp.png"
anchors.horizontalCenter: parent.horizontalCenter
y: 200
}
Label {
id: name
anchors.horizontalCenter: parent.horizontalCenter
y: 320
font.bold: true
text: "SailOTP 0.1"
}
Text {
id: desc
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: name.bottom
anchors.topMargin: 20
text: "A Simple Sailfish TOTP Generator<br />(RFC 6238 compatible)"
color: "white"
id: aboutPage
Image {
id: logo
source: "../sailotp.png"
anchors.horizontalCenter: parent.horizontalCenter
y: 200
}
Label {
id: name
anchors.horizontalCenter: parent.horizontalCenter
y: 320
font.bold: true
text: "SailOTP 0.1"
}
Text {
id: desc
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: name.bottom
anchors.topMargin: 20
text: "A Simple Sailfish TOTP Generator<br />(RFC 6238 compatible)"
color: "white"
}
Text {
id: copyright
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: desc.bottom
anchors.topMargin: 20
text: "Copyright: Stefan Brand<br />License: BSD (3-clause)"
color: "white"
}
Button {
id: homepage
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: copyright.bottom
anchors.topMargin: 20
text: "<a href=\"https://github.com/seiichiro0185/sailotp\">SailOTP on Github</a>"
onClicked: {
Qt.openUrlExternally("https://github.com/seiichiro0185/sailotp")
}
Text {
id: copyright
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: desc.bottom
anchors.topMargin: 20
text: "Copyright: Stefan Brand<br />License: BSD (3-clause)"
color: "white"
}
Button {
id: homepage
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: copyright.bottom
anchors.topMargin: 20
text: "<a href=\"https://github.com/seiichiro0185/sailotp\">SailOTP on Github</a>"
onClicked: {
Qt.openUrlExternally("https://github.com/seiichiro0185/sailotp")
}
}
Text {
id: accnowledgement
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: homepage.bottom
anchors.topMargin: 20
text: "SailOTP uses the SHA-1 Implementation<br />from http://caligatio.github.io/jsSHA/"
color: "white"
}
}
Text {
id: accnowledgement
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: homepage.bottom
anchors.topMargin: 20
text: "SailOTP uses the SHA-1 Implementation<br />from http://caligatio.github.io/jsSHA/"
color: "white"
}
}

@ -30,52 +30,55 @@
import QtQuick 2.0
import Sailfish.Silica 1.0
import "../lib/storage.js" as DB
import "../lib/storage.js" as DB // Import the storage library for Config-Access
// Define Layout of the Add OTP Dialog
Dialog {
id: addOTP
property QtObject parentPage: null
SilicaFlickable {
id: addOtpList
anchors.fill: parent
VerticalScrollDecorator {}
Column {
anchors.fill: parent
DialogHeader {
acceptText: "Add"
}
TextField {
id: otpLabel
width: parent.width
label: "Title"
placeholderText: "Title for the OTP"
focus: true
horizontalAlignment: TextInput.AlignLeft
}
TextField {
id: otpSecret
width: parent.width
label: "Secret"
placeholderText: "Secret OTP Key"
focus: true
horizontalAlignment: TextInput.AlignLeft
}
}
id: addOTP
// We get the Object of the parent page on call to refresh it after adding a new Entry
property QtObject parentPage: null
SilicaFlickable {
id: addOtpList
anchors.fill: parent
VerticalScrollDecorator {}
Column {
anchors.fill: parent
DialogHeader {
acceptText: "Add"
}
TextField {
id: otpLabel
width: parent.width
label: "Title"
placeholderText: "Title for the OTP"
focus: true
horizontalAlignment: TextInput.AlignLeft
}
TextField {
id: otpSecret
width: parent.width
label: "Secret"
placeholderText: "Secret OTP Key"
focus: true
horizontalAlignment: TextInput.AlignLeft
}
}
onDone: {
if (otpLabel.text != "" && otpSecret.text != "") {
DB.addOTP(otpLabel.text, otpSecret.text);
parentPage.refreshOTPList();
}
}
// Save if page is Left with Add
onDone: {
// Some basic Input Check, we need both Values to work
if (otpLabel.text != "" && otpSecret.text != "") {
// Save the entry to the Config DB
DB.addOTP(otpLabel.text, otpSecret.text);
// Refresh the main Page
parentPage.refreshOTPList();
}
}
}

@ -34,139 +34,147 @@ import "../lib/storage.js" as DB
import "../lib/crypto.js" as OTP
Page {
id: mainPage
ListModel {
id: otpListModel
id: mainPage
ListModel {
id: otpListModel
}
// This holds the time of the last update of the page as Unix Timestamp (in Milliseconds)
property double lastUpdated: null
// Add an entry to the list
function appendOTP(title, secret) {
otpListModel.append({"secret": secret, "title": title, "otp": ""});
}
// Reload the List of OTPs from storage
function refreshOTPList() {
otpListModel.clear();
DB.getOTP();
refreshOTPValues();
}
// Calculate new OTPs for every entry
function refreshOTPValues() {
// get seconds from current Date
var curDate = new Date();
var seconds = curDate.getSeconds();
// Iterate over all List entries
for (var i=0; i<otpListModel.count; i++) {
// Only update on full 30 / 60 Seconds or if last run of the Functions is more than 2s in the past (e.g. app was in background)
if (otpListModel.get(i).otp == "" || seconds == 30 || seconds == 0 || (curDate.getTime() - lastUpdated > 2000)) {
var curOTP = OTP.calcOTP(otpListModel.get(i).secret)
otpListModel.setProperty(i, "otp", curOTP);
console.log("Updating Value ", i);
}
}
property double lastUpdated: null
// Add an entry to the list
function appendOTP(title, secret) {
otpListModel.append({"secret": secret, "title": title, "otp": ""});
// Update the Progressbar
updateProgress.value = 29 - (seconds % 30)
// Set lastUpdate property
lastUpdated = curDate.getTime();
}
Timer {
interval: 1000
running: Qt.application.active // Timer only runs when App is active
repeat: true
onTriggered: refreshOTPValues();
}
SilicaFlickable {
anchors.fill: parent
PullDownMenu {
MenuItem {
text: "About"
onClicked: pageStack.push(Qt.resolvedUrl("About.qml"))
}
MenuItem {
text: "Add OTP"
onClicked: pageStack.push(Qt.resolvedUrl("AddOTP.qml"), {parentPage: mainPage})
}
}
// Reload the List of OTPs from storage
function refreshOTPList() {
otpListModel.clear();
DB.getOTP();
refreshOTPValues();
ProgressBar {
id: updateProgress
width: parent.width
maximumValue: 29
anchors.top: parent.top
anchors.topMargin: 48
}
// Calculate new OTPs for every entry
function refreshOTPValues() {
var curDate = new Date();
var seconds = curDate.getSeconds();
for (var i=0; i<otpListModel.count; i++) {
if (otpListModel.get(i).otp == "" || seconds == 30 || seconds == 0 || (curDate.getTime() - lastUpdated > 2000)) {
var curOTP = OTP.calcOTP(otpListModel.get(i).secret)
otpListModel.setProperty(i, "otp", curOTP);
console.log("Updating Value ", i);
}
}
SilicaListView {
id: otpList
header: PageHeader {
title: "SailOTP"
}
anchors.fill: parent
model: otpListModel
width: parent.width
updateProgress.value = 29 - (seconds % 30)
lastUpdated = curDate.getTime();
}
ViewPlaceholder {
enabled: otpList.count == 0
text: "Nothing here"
hintText: "Pull down to add a OTP"
}
Timer {
interval: 1000
running: Qt.application.active
repeat: true
onTriggered: refreshOTPValues();
}
SilicaFlickable {
anchors.fill: parent
delegate: ListItem {
id: otpListItem
menu: otpContextMenu
width: otpList.width
contentHeight: Theme.itemSizeMedium
PullDownMenu {
MenuItem {
text: "About"
onClicked: pageStack.push(Qt.resolvedUrl("About.qml"))
}
MenuItem {
text: "Add OTP"
onClicked: pageStack.push(Qt.resolvedUrl("AddOTP.qml"), {parentPage: mainPage})
}
function remove() {
// Show 5s countdown, then delete from DB and List
remorseAction("Deleting", function() { DB.removeOTP(title, secret); otpListModel.remove(index) })
}
ProgressBar {
id: updateProgress
width: parent.width
maximumValue: 29
anchors.top: parent.top
anchors.topMargin: 48
ListView.onRemove: animateRemoval()
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
Label {
id: otpLabel
text: model.title
color: Theme.secondaryColor
anchors.horizontalCenter: parent.horizontalCenter
}
Label {
id: otpValue
anchors.top: otpLabel.bottom
text: model.otp
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.highlightColor
font.pixelSize: Theme.fontSizeLarge
}
}
SilicaListView {
id: otpList
header: PageHeader {
title: "SailOTP"
}
anchors.fill: parent
model: otpListModel
width: parent.width
ViewPlaceholder {
enabled: otpList.count == 0
text: "Nothing here"
hintText: "Pull down to add a OTP"
}
delegate: ListItem {
id: otpListItem
menu: otpContextMenu
width: otpList.width
contentHeight: Theme.itemSizeMedium
function remove() {
remorseAction("Deleting", function() { DB.removeOTP(title, secret); otpListModel.remove(index) })
}
ListView.onRemove: animateRemoval()
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
Label {
id: otpLabel
text: model.title
color: Theme.secondaryColor
anchors.horizontalCenter: parent.horizontalCenter
}
Label {
id: otpValue
anchors.top: otpLabel.bottom
text: model.otp
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.highlightColor
font.pixelSize: Theme.fontSizeLarge
}
}
Component {
id: otpContextMenu
ContextMenu {
MenuItem {
text: "Delete"
onClicked: remove()
}
}
}
}
VerticalScrollDecorator{}
Component.onCompleted: {
DB.initialize();
refreshOTPList();
Component {
id: otpContextMenu
ContextMenu {
MenuItem {
text: "Delete"
onClicked: remove()
}
}
}
}
VerticalScrollDecorator{}
Component.onCompleted: {
// Initialize DB (create tables etc..)
DB.initialize();
// Load list of OTP-Entries
refreshOTPList();
}
}
}
}

@ -1,32 +1,31 @@
/*
Copyright (C) 2013 Jolla Ltd.
Contact: Thomas Perl <thomas.perl@jollamobile.com>
All rights reserved.
You may use this file under the terms of BSD license as follows:
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* 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.
* Neither the name of the Jolla Ltd nor the
names of its contributors may 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 HOLDERS 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.
*/
* Copyright (c) 2013, 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.
*/
#ifdef QT_QML_DEBUG
#include <QtQuick>
@ -34,9 +33,8 @@
#include <sailfishapp.h>
int main(int argc, char *argv[])
{
return SailfishApp::main(argc, argv);
return SailfishApp::main(argc, argv);
}

Loading…
Cancel
Save